on
JPA 면접 질문 모음
참고
📌 ORM
❓ ORM; Object-Relational-Mapping 객체-관계-매핑이란?
- ORM 프레임워크는 자바 객체와 관계형 DB를 매핑해 객체가 DB 테이블이 되도록 만들어주는 것입니다.
- ORM을 사용하면, SQL을 작성하지 않아도 직관적인 메소드로 데이터를 조작할 수 있다는 장점이 있습니다. (개발자의 생산성 향상)
- 종류로는 Hibernate, EclipseLink, DataNucleus 등이 있습니다.
- 스프링 부트에서는 spring-boot-starter-data-jpa로 패키지를 가져와 사용하며, 이는 Hibernate 프레임워크를 활용한다.
- JPA는 애플리케이션과 JDBC 사이에서 동작하며, 개발자가 JPA를 활용했을 때 JDBC API를 통해 SQL을 호출하여 데이터베이스와 호출하는 전개가 이루어진다.
- 즉, 개발자는 JPA의 활용법만 익히면 DB 쿼리 구현없이 데이터베이스를 관리할 수 있다.
❓ Persistence Context 영속성 컨텍스트란?
- 엔티티를 영구 저장하는 환경을 의미합니다.
❓ JPA 영속성 컨텍스트의 이점(5가지)을 설명해주세요.
영속성 컨텍스트를 쓰는 이유는 1차 캐시, 동일성 보장, 쓰기 지연, 변경감지(Dirty checking), 지연로딩이 있습니다.
1차 캐시
- 조회가 가능하며 1차 캐시에 없으면 DB에서 조회하여 1차 캐시에 올려 놓습니다.
동일성 보장
- 동일성 비교가 가능합니다.(==)
쓰기 지연
- 트랜잭션을 지원하는 쓰기 지연이 가능하며 트랜잭션 커밋하기 전까지 SQL을 바로 보내지 않고 모아서 보낼 수 있습니다.
변경 감지(Dirty checking)
- Dirty: 상태의 변화가 생김 / Checking: 검사
- 트랜잭션 안에서 Entity의 변경이 일어났을 때 변경한 내용을 자동으로 DB에 반영하는 것을 의미합니다. 영속성 컨텍스트가 관리하는 Entity만 Checking의 대상이 됩니다. 준영속, 비영속 Entity는 반영하지 않습니다.
- 스냅샷을 1차 캐시에 들어온 데이터를 찍습니다. commit 되는 시점에 Entity와 스냅샷과 비교하여 update SQL을 생성합니다.
- ex) 주문 취소 메소드를 구현할 때 메소드 내에 update 로직이 없어도 데이터베이스에 update가 반영
- 엔티티에서 변경이 일어난 걸 감지한 뒤 데이터베이스에 반영(변경은 최초 조회 상태 기준)
@DynamicUpdate
를 사용해서 SQL에서는 변경된 엔티티의 모든 필드를 업데이트하지 않고 변경 필드만 반영시키도록 만들 수 있습니다.
지연 로딩
- 엔티티에서 해당 엔티티를 불러올 때 그 때 SQL을 날려 해당 데이터를 가져옵니다.
cf) 참고: 즉시로딩(Eager Loading) / 지연로딩(Lazy Loading)
- 즉시로딩
- 특정 엔티티를 조회할 때 연관된 모든 엔티티를 같이 로딩, 즉시 로딩이라고 합니다.
- 즉시 로딩은 연관된 엔티티를 모두 가져온다는 장점이 있지만, 실무에서 엔티티간의 관계가 복잡해질수록 조인으로 인한 성능 저하를 피할 수 없게 됩니다.
- JPA에서 연관관계의 데이터를 어떻게 가져올 것인가를 fetch(패치)라고 하는데, 연관관계의 어노테이션 속성으로 fetch모드를 지정합니다.
- EagerLoading(즉시 로딩)은 불필요한 조인까지 포함해서 처리하는 경우가 많기 때문에 LazyLoading(지연 로딩)의 사용을 권장하고 있습니다.
- 지연로딩
- 가능한 객체의 초기화를 지연시켜 로딩하는 방법입니다.
- 즉, 실제 객체 대신 프록시 객체를 로딩해두고 해당 객체를 실제 사용할 때만(ex. getter 메서드를 사용할 때) 영속성 컨텍스트를 통해 실체 객체를 호출하는 로딩입니다.
- @XToOne(OneToOne, ManyToOne) 관계는 기본이 EagerLoading(즉시 로딩)이므로 직접 지연로딩(fetch = FetchType.LAZY)으로 설정해야 합니다.
❓ JPA의 특징에 대해 설명해주세요.
- 객체 중심 개발 가능
- 생산성 증가
- 쿼리를 일일히 작성할 필요가 없어 코드 작업량이 줄어든다.
- 가독성이 뛰어나다.
- 수정이 간편해 유지보수, 리팩토링에 용이하다.
- 쿼리 수정이 필요할 때, 이를 담아야 할 DTO 필드도 모두 변경해야 하는 작업이 필요하지만 JPA에서는 엔티티 클래스 정보만 변경하면 되므로 유지보수에 용이하다.
❓ JPA를 쓴다면 그 이유에 대해서 설명해주세요.
- JPA를 사용하는 이유는 객체지향 프레임워크이기 때문입니다. JPA를 사용하면 비즈니스 로직이 RDBMS에 의존하는 것이 아니라, 자바 코드로 표현될 수 있기 때문입니다. 그로 인해서 생산성이 높아진다고 볼 수 있습니다.(이는 JPA에 익숙하다는 것을 전제로 합니다.)
- 또, JPA는 JPQL로 SQL을 추상화하기 때문에 RDBMS Vendor에 관계없이 동일한 쿼리를 작성해서 같은 동작을 기대할 수 있다는 장점도 가지고 있습니다. 이는 database dialect를 지원하기 때문에 가지는 장점입니다.
❓ JPA의 한계점에 대해 설명해주세요. (Mybatis와 연관)
- 매핑 설계를 잘못했을 때 성능 저하가 발생할 수 있다.
- JPA는 복잡한 쿼리보다는 실시간 쿼리에 최적화되어있다. 예를 들어 통계 처리와 같은 복잡한 작업이 필요한 경우에는 기존의 Mybatis와 같은 Mapper 방식이 더 효율적일 수 있다.
- Spring에서는 JPA와 Mybatis를 같이 사용할 수 있기 때문에, 상황에 맞는 방식을 택하여 개발하면 된다.
- 동적 쿼리를 사용하면 가독성이 떨어져 유지보수 시 어려움이 있다.
❓ JPA Propagation 전파단계를 설명해주세요.
- JPA Propagation은 트랜잭션 동작 도중 다른 트랜잭션을 호출(실행)하는 상황에 선택할 수 있는 옵션입니다.
- @Transactional의 propagation 속성을 통해 피호출 트랜잭션의 입장에서는 호출한 쪽의 트랜잭션을 그대로 사용할 수도 있고, 새롭게 트랜잭션을 생성할 수도 있습니다.
- REQUIRED(디폴트): 부모 트랜잭션 내에서 실행하며 부모 트랜잭션이 없을 경우 새로운 트랜잭션을 생성합니다.
❓ N + 1 문제는 무엇이고 이것이 발생하는 이유와 이를 해결하는 방법을 설명해주세요.
- N + 1 쿼리 문제는 즉시 로딩과 지연 로딩 전략 각각의 상황에서 발생할 수 있습니다.
- 하위 엔티티들이 존재하는 경우 한 쿼리에서 모두 가져오는 것이 아닌, 필요한 곳에서 각각 쿼리가 발생하는 경우를 이릅니다.
- 즉시 로딩에서 발생하는 이유는 JPQL을 사용하는 경우 전체 조회를 했을 때, 영속성 컨텍스트가 아닌 데이터베이스에서 직접 데이터를 조회한 다음 즉시로딩 전략이 동작하기 때문입니다.
- 지연 로딩에서 발생하는 이유는 지연로딩 전략을 사용한 하위 엔티티를 로드할 때, JPA에서 프록시 엔티티를 unproxy 할 때 해당 엔티티를 조회하기 위한 추가적인 쿼리가 실행되어 발생합니다.
해결 방법
- Fetch Join이라고 불리는 JPQL의 join fetch를 사용하는 방법
- N+1 자체가 발생하는 이유는 한쪽 테이블만 조회하고 연결된 다른 테이블까지 따로 조회되기 때문입니다.
- 미리 두 테이블을 JOIN 하여 한 번에 모든 데이터를 가져올 수 있다면 애초에 N+1 문제가 발생하지 않을 것입니다. 이런방식을 fetchJoin 이라고 부릅니다.
- fetchJoin : 연관된 엔티티나 컬렉션을 한 번에 같이 조회할 수 있습니다. fetchJoin을 사용하게 되면 연관된 엔티티는 프록시가 아닌 실제 엔티티를 조회하게 되므로 연관관계 객체까지 한번의 쿼리로 가져올 수 있습니다.
- ⚠ ️단점
- 쿼리 한번에 모든 데이터를 가져오기 때문에 JPA가 제공하는 Paging API 사용 불가능(Pageable 사용 불가)
- 1:N 관계가 두 개 이상인 경우 사용 불가
- 패치 조인 대상에게 별칭(as) 부여 불가능
- 번거롭게 쿼리문을 작성해야 함
- @EntityGraph를 사용하는 방법
- @EntityGraph 의 attributePaths는 같이 조회할 연관 엔티티명을 적으면 됩니다. ,(콤마)를 통해 여러 개를 줄 수도 있습니다.
- Fetch join과 동일하게 JPQL을 사용해 Query문을 작성하고 필요한 연관관계를 EntityGraph에 설정해야 합니다.
- ex.
@EntityGraph(attributePaths = {"pets"}) @Query("select DISTINCT o from Owner o")
- EntityGraph는 미리 데이터를 조인(outerJoin)해서 가져옵니다.
- @Fetch(FetchMode.SUBSELECT)를 사용하는 방법
- @BatchSize를 사용해 조절하거나 전역적인 batch-size를 설정하는 방법
❓ Fetch Join, EntityGraph를 사용할 때 주의할 점은 무엇인가요?
FetchJoin과 EntityGraph는 공통적으로 카테시안 곱(Cartesian Product)이 발생 하여 중복이 생길 수 있습니다.
※ 카테시안 곱 : 두 테이블 사이에 유효 join 조건을 적지 않았을 때 해당 테이블에 대한 모든 데이터를 전부 결합하여 테이블에 존재하는 행 갯수를 곱한만큼의 결과 값이 반환되는 것
카테시안 곱(Cartesian Product)을 일이키는 Cross Join 카테시안 곱이 일어나는 Cross Join은 JPA 기능 때문이 아니라, 쿼리의 표현에서 발생하는 문제이다.
Cross Join이 일어나는 조건 Join 명령을 했을 때 명확한 Join 규칙이 주어지지 않았을 때, join 이후 on 절이 없을 때, db는 두 테이블의 결합한 결과는 내보내야겠고, 조건이 없으니 M * N으로 모든 경우의 수를 출력하는 것이다.
JPA는 사용자가 보내준 코드를 해석해서 최적의 sql 문장을 조립하는데, 이 때 코드가 얼마나 연관관계를 명확히 드러냈냐에 따라 발생 할 수도 안 할 수도 있다.
Fetch Join과 @EntityGraph의 기능은 ‘Cross Join을 만들어라’ 나 ‘Inner Join을 만들어라’ 가 아니고,
‘연관관계 데이터를 미리(EAGER) 함께 가져와라’ 인 만큼 중복을 제거해야 합니다.
=> 이런 중복 발생 문제를 해결하기 위해서 DISTINCT 또는 Set 자료구조를 사용하면 됩니다.
- JPQL에 DISTINCT 를 추가하여 중복 제거
@Query(“select DISTINCT o from Owner o join fetch o.pets”)
List
findAllJoinFetch(); @EntityGraph(attributePaths = {"pets"}) @Query("select DISTINCT o from Owner o") List findAllEntityGraph(); - OneToMany 필드 타입을 Set으로 선언하여 중복 제거
@OneToMany(mappedBy = “owner”, fetch = FetchType.EAGER)
private Set
pets = new LinkedHashSet<>(); (Set은 순서가 보장되지 않는 특징이 있지만, 순서 보장이 필요한 경우 LinkedHashSet을 사용하자.)