지나공 : 지식을 나누는 공간

[직무인터뷰] JPA 이야기 본문

Tech Interview

[직무인터뷰] JPA 이야기

해리리_ 2021. 6. 7. 02:23

JPA란?

Java Persistence API. 자바 진영의 ORM 기술에 대한 API 표준 명세. 아, ORM을 설명하자면 Object-Relational Mapping으로, 객체는 객체대로 설계하고 관계형 데이터베이스는 관계형 데이터베이스대로 설계했을 때 이 둘 사이를 ORM 프레임워크가 중간에서 매핑하는 것이다. 

 

JPA는 아래와 같은 특징이 있는데 자세한 건 아래에서 다루겠다.

  • 1차 캐시와 동일성 보장 -> 동일한 트랜잭션에서 같은 엔티티에 대한 두 번의 조회를 하나의 SQL문으로 수행해서 캐싱을 함.
  • 지연로딩과 즉시로딩을 지원해 JOIN으로 연관객체를 같이 조회할 수 있다.
  • 생산성에 도움이 된다.

*Hibernate란?

JPA 구현체. ORM 프레임워크 중 하나다. SQL문을 직접 작성하지 않고 메소드 호출만으로 쿼리 수행을 가능하게 해서 생산성을 높일 수 있게 한다.

 

*Spring Data JPA란?

JPA를 편리하게 사용할 수 있도록 스프링에서 제공하는 프로젝트로, CRUD 처리를 위한 공통 인터페이스를 제공한다.

 

영속성 컨텍스트란?

엔티티를 영구 저장하는 환경으로, 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 환경이다.

영속성 컨텍스트의 생명 주기는 트랜잭션과 동일하다. 트랜잭션을 종료하면 얘도 종료된다.

 

엔티티의 생명주기

  • 비영속 : 영속성 컨텍스트와 전혀 관계가 없는 상태
  • 영속 : 영속성 컨텍스트에 저장된 상태
  • 준영속 : 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제 : 삭제된 상태

영속성 컨텍스트 왜 쓰는데? 장점이 뭔데?

  • 1차 캐시
    똑같은 걸 두 번 조회하는 경우, 처음 조회할 때 해당 데이터를 1차 캐시에 올려서 두 번째 조회 시에는 쿼리문을 수행하지 않아서 생산성을 높인다.
  • 동일성 보장
    1차 캐시에 이미 있는 엔티티라면 해당 엔티티를 그대로 반환해서 여러 번 조회했을 때의 동일성을 보장한다.
  • 트랜잭션을 지원하는 쓰기 지연
    커밋이 되기 전까지는 쿼리문을 쓰기 지연 저장소에 모아뒀다가 커밋하는 시점에 한번에 flush한다.
  • 변경 감지(Dirty Checking)
    이 기능은 영속성 컨텍스트가 관리하는 영속 상태의 엔티티만 적용된다.
    • 영속성 컨텍스트에 들어가 있는 엔티티에 대해 변경을 하면 어떻게 될까? 사실은 1차 캐시에 처음 저장할 때 동시에 스냅샷 필드를 저장한다. 그래서 COMMIT이나 FLUSH가 일어날 때 엔티티의 현재 값과 스냅샷을 비교하고 변경 사항이 있으면 update 쿼리를 수행해서 알아서 DB에 저장한다.
  • 지연 로딩
    • 프록시란?
      실제 엔티티 대신에 사용되는 객체로 원본 엔티티를 상속받은 객체다. 초기에 DB로부터 조회가 되면 프록시 객체의 타겟으로 실제 객체가 연결이 되면서 최초에 1회 초기화된다. 그 이후로는 지속적으로 호출해도 프록시가 초기화되지는 않고 한번 호출한 걸 계속 쓴다.

    • Member 안에 Team 객체가 있을 때 Team의 멤버변수를 조회하는 게 아니라면 굳이 실제로 데이터베이스에서 조회해서 1차캐시에 올려놓을 필요가 없다. 그래서 이때 가짜(프록시) 객체를 조회한다. 근데 이미 1차 캐시에 이전에 조회한 기록이 있다면 그 실제 엔티티를 가져온다.
    • 즉시로딩은 항상 연관 객체를 같이 가져온다. 지연로딩은 연관객체의 프록시를 가져온다.

JPA의 동작 순서

findById()같은 경우는 엔티티를 영속성 컨텍스트에서 먼저 찾아보고 없으면 데이터베이스에서 찾는 반면, JPQL은 항상 데이터베이스에 SQL을 실행해서 결과를 조회한다. 그리고 아래와 같은 작업을 한다.

1. JPQL을 호출하면 데이터베이스에서 우선 조회해본다.

2. 조회한 값을 영속성컨텍스트에 저장한다.

3. 영속성 컨텍스트에서 조회할 때 이미 조회한 게 있다면 데이터를 버린다.

N+1 문제란?

하나의 쿼리를 수행하는데 N개의 쿼리가 더 수행된다는 의미이다.

Member 객체와 Team 객체가 연관되어 있다고 하자. 10명의 Member를 조회할 때 지연 로딩으로 가져오면 Team은 프록시 객체를 사용하고 직접 DB에서 조회하지는 않는다. 근데 즉시로딩이라면 가져올 때 무조건 값이 실제 데이터로 채워져 있어야 해서 Team까지 DB에서 조회해야 한다. 이때 10명의 Member를 조회하기 위해 select * from ~이라는 식의 쿼리문 1개를 사용해서 10개의 데이터를 조회했는데, 이 10개의 멤버에 대해 각각 select * from Team where member.id = ~~ 의 식으로 쿼리문이 N개(여기선 10개겠지) 나가서 매우 비효율적인 상황이 된다. 

근데 지연로딩으로 해놔도! N+1문제가 발생할 때가 있다. 언제인가? 어떻게 해결하나?

일단 JPQL은 JPA에서 제공하는 메소드와 달리 영속성컨텍스트 조회가 아니라 바로 DB에 접근한다는 점을 기억하자.

 

Members와 Team이 있다고 하자. Team은 각각 Member Id를 외래키로 가지고 있다.

이 상태에서 member.team.id와 같이 team 의 멤버변수에 접근해보자. 이 경우에는 연관객체의 멤버변수에 직접 접근해서 가져오기 때문에 지연로딩이라고 설정했더라도 연관객체인 team에 대한 정보를 조회해야 할 것이다.

 

일단 member를 1명 조회하면 team 테이블에서도 select * from team where member.id = ? 와 같은 SQL이 실행된다. 아직은 1명 조회라 괜찮은데, member를 여러 명 조회하게 된다면 각 멤버에 대해 team을 조회하는 SQL이 하나씩 나가서 결국 지연로딩이라고 썼음에도 불구하고 N+1문제가 발생한다.

 

해결을 어떻게 할까?

 

1. Batch Size를 활용한다.

@BatchSize 어노테이션을 활용하면 설정한 size만큼의 데이터를 미리 로딩한다. 즉 연관 엔티티를 조회할 때 size만큼 where in 쿼리를 통해 조회하고, size를 넘어갔을 때 추가로 where in 쿼리를 수행한다. 하지만 이 방법도 결국 정해진 batch size에 따라 반복적인 쿼리가 수행되니까 추천방안은 아니다.

 

2. 페치 조인 사용하기

@Query("select m from Member m left join fetch m.orders")와 같이 적으면 fetch를 사용해서 조인쿼리를 수행한다. fetch 키워드는 연관 객체나 컬렉션을 한 번에 같이 조회하게 한다. 즉, 페치 조인을 사용하면 연관 엔티티는 프록시가 아닌 실제 엔티티를 조회하게 되고 이로써 연관 객체까지 한번의 쿼리로 다 가져올 수 있다. N번 실행하지 않게 된다.

 

*일반 조인과 페치 조인의 차이

일반 조인은 select m from Member m join m.orders이고, 페치 조인은 select m from Member m join fetch m.orders 이다. 일반 조인으로 현 상황을 적용하면 N+1문제가 난다. 페치 조인만 문제 없이 가져와진다.

 

페치 조인의 한계

  • 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
    count 쿼리에서 count할 대상이 달라진다.
  • 2개 이상의 OneToMany 자식 테이블에 Fetch join을 하면 MutipleBagFetchException이 발생한다.
    • 해결책은 자식 테이블 하나만 Fetch Join을 걸고 나머지 하나는 Lazy Loading으로 가져오거나, 모든 자식 테이블을 다 Lazy로 가져오는 것이 있다. 하지만 이건 성능상으로 개선되는 것이 없다.
    • 다른 해결책은 A 리스트와 함께 각각의 자식 b, c를 가져와야 하는 상황이라고 할 때 A들을 먼저 조회한 다음 A1의 b,c 조회 , A2의 b,c조회, A3의 b,c조회... 와 같이 조회하는 방식이 있다고 생각할 텐데 이것도 자식 테이블 여러 곳에 fetch를 사용하기 때문에 에러가 발생한다.
    • 그래서! batch_fetch_size를 설정하는 것이 또다른 해결책이다.

@EntityGraph를 통한 조회도 페치 조인을 사용한다.

JPA saveAll과 save 성능차이?

save는 기존 트랜잭션이 존재할 때 이에 참여하긴 하지만 애초에 시작부터 @Transactional이 걸려있어서 프록시 로직을 타고 리소스가 더 크게 든다. 기존 트랜잭션이 없으면 트랜잭션이 생성됐다가 종료된다.

 

Optional은, java8 에 생겼고, NullPointerException을 방지한다.

JPA의 Entity Id를 Long으로 사용하는 이유는?

일단 primitive type이 아닌 Wrapper type인 Long을 사용해야 Null을 사용할 수 있다. 참고로 Wrapper 클래스는 참조형이다. 하이버네이트 공식 문서에도 Nullable한 값을 사용하라고 권장한다.

QueryDSL이란? 왜 쓰나?

JPQL에서 기본으로 제공하는 @Query로는 다양한 조회 기능을 사용하는 데 한계가 있어서 QueryDSL을 사용한다. 

 

가장 큰 장점은 IDE의 도움을 받을 수 있고 컴파일 타임에 오류를 잡을 수 있다는 점이다.

그리고 동적 쿼리가 가능하다보니 다양한 조회가 가능하다. 예를 들어 문자열을 더하기 해야 한다고 할 때 JPQL은 정적 쿼리이기 때문에 어려운데 QueryDSL은 코드를 더하는 것이니 편하고, 자바이다 보니 중복 코드들을 함수로 만들어낼 수도 있다. 원하는 필드만 뽑아서 DTO로 만드는 기능도 모두 지원한다.

어떻게 대용량 트래픽을 처리할까? -> scale up, scale out

  • Scale Out : 서버의 개수를 늘려서 시스템 성능을 향상시키는 방법
  • Scale Up : 서버의 CPU, RAM 등 하드웨어 스펙을 향상시켜서 처리 능력을 향상시키는 방법

EC2란?

독립된 컴퓨터를 임대해주는 서비스인데, 컴퓨팅 요구사항에 따라 컴퓨팅 파워를 조정할 수 있다. (이런 질문은... 안나올 거긴 한데 좀 더 확실히 하려고 한 번 적어봤다.)

로드밸런싱이란?

인터넷 서비스에서 발생하는 대량의 트래픽을 각 인스턴스에 분산시켜주는 기술이다.

Maven과 Gradle이 뭐고, 둘의 차이는?

일단 둘은 모두 빌드 관리 도구다. 

우리가 프로젝트에서 작성한 java 코드와 프로젝트 내에 필요한 각종 xml, properties, jar 파일들을 JVM이나 WAS가 인식할 수 있도록 패키징하는 것이 빌드 과정인데 이런 일을 하는 빌드 자동화 도구이다. 여러 개의 라이브러리를 번거롭게 모두 다운받지 않고, 빌드 도구 설정파일에 필요한 라이브러리의 종류와 버전 등의 정보를 명시해서 자동으로 다운로드 해준다.

 

둘 다 모듈 빌드를 병렬로 실행할 수 있지만 Gradle은 어떤 task가 업데이트 되었고 안되었고를 체크하기 때문에 incremental build를 허용한다. 이미 업데이트된 task에 대해서는 작업이 실행되지 않아서 빌드 시간이 훨씬 단축된다.

 

Memory의 Constant Pool이 뭔가?

이건 java 1편 모음에서 말한 거긴 한데... 일단 질문이 있으니 대답을 적어보겠다. 상수풀은 힙 영역의 고정 영역에 생성되어 Java 프로세스의 종료까지 계속 유지되는 메모리 영역이다.기본적으로 JVM에서 관리하고 프로그래머가 작성한 상수에 대해서 최우선적으로 찾아본 뒤 없으면 상수풀에 추가하고 이후 그 주솟값을 리턴한다. 상수풀이 있기 때문에 메모리 절약 효과를 낼 수 있다.

 

TDD란?

테스트 주도 개발이라는 의미로, 우선 테스트를 작성하고 그걸 통과하는 코드를 만드는 것을 반복하면서 제대로 동작하는지에 대한 피드백을 적극적으로 받는 것이다. 보통의 경우처럼 코딩을 다 한뒤 완성된 것 같을 때 테스트를 하는 게 아니다. 

장점은, 디버깅 시간을 줄여주고, 딱 필요한 만큼만 코딩하니까 불필요하게 복잡해지지 않는다. 뭐 예를 들어 미래에 필요할 것 같아서 적어놓은 코드를 결과적으로 끝까지 사용하지 않게 되면 낭비인 건데 TDD를 통해 미리 테스트를 하면 이런 코드를 방지할 수 있다.

DDD란?

도메인 주도 개발이라는 의미의 방법론. 데이터에 종속적으로 개발하거나 모델링-개발이 불일치하는 할 때가 있었다. 이걸 해결하기 위해 개념이고 DDD를 적용하면 디자이너와 개발자가 똑같이 공유할 수 있는 언어로 협업할 수 있다.

Aggregate Root는 DDD에서 엔티티마다 Repository를 만드는 경우가 많은데 서로 연관관계를 가진 게 많다면 여러 엔티티를 묶어서 하나처럼 사용하는 경우가 많을 것이다. 이런 연관 객체의 묶음을 Aggregate라고 하고 이 묶음을 가져오는 중심이 되는 Entity가 Aggregate Root다. 이 Aggregate Root를 가져오는 Repository에서 한 번에 연관 객체들의 묶음을 가져올 수 있다.

MapStruct란?

DTO - ENTITY간 객체 매핑을 편하게 도와주는 라이브러리이다. 속도가 빠름..!

728x90
Comments