이번에는 공동구매 게시물 조회 기능을 구현해볼 것이다.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User writer;
현재 `Copurchasing` 도메인의 `User` 필드는 `FetchType.LAZY`로 설정 돼 있다.
📌 FetchType
- `지연 로딩(Lazy Loading)` 연관된 엔티티를 처음 접근할 때까지 로드하지 않고 필요할 때만 데이터베이스에서 데이터를 로드
- `즉시 로딩(Eager Loading)` 엔티티가 로드될 때 연관된 모든 엔티티를 즉시 로드
JPA는 지연 로딩을 위해 `프록시 객체`를 사용한다. 실제 데이터는 필요할 때까지 로드되지 않고, 프록시 객체로 대체된다.
`LAZY`는 성능 최적화에 유용하지만, N+1 문제가 발생할 수 있다.
`N+1 문제`란 지연 로딩을 사용할 때, 연관된 엔티티를 반복적으로 조회하면 데이터베이스 쿼리가 반복 실행될 수 있는 문제를 말한다.
현재 `Copurchasing`은 `User`를 LAZY 로딩하고 있기 때문에, `User` 에 접근하려고 할 때마다 User 를 id 조건으로 조회하는 쿼리가 추가적으로 발생한다.
`copurchasing` 의 모든 데이터를 조회하고, 조회된 각 데이터의 `User`를 찾기 위한 select 쿼리가 한 번 더 발생한다.
이를 통해 N + 1 문제가 발생했음을 알 수 있다.
📌 EAGER 로딩으로 변경 (해결 X)
`User` 를 LAZY 하게 로딩하고 있기 때문에, 쿼리로 `Copurchasing` 객체를 조회할 때 연관된 User를 바로 조회하도록
`EAGER` 로딩으로 변경해보았다.
그러나 LAZY 로딩과 동일한 쿼리가 발생했고, 각 공동구매 객체에 대한 user select 쿼리가 처음 한 번에 발생한다는 차이점만 존재했다.
EAGER 로딩 시 join 을 통해 데이터를 하나의 쿼리로 조회하지 않는 이유는 hibernate 가 `findAll` 쿼리 생성 시
EAGER 로딩되는 연관관계 필드를 join 으로 조회하지 않기 때문이다.
그와 반대로 JPA 의 `findById` 를 사용하면 EAGER 로딩 전략을 사용하는 연관관계 필드를 left join 으로 조회한다.
따라서 EAGER 로딩으로는 `findAll`의 N + 1 문제를 해결하기 어렵다.
📌 DTO 프로젝션을 사용한 쿼리 작성
응답에 필요한 필드만 조회 결과에 포함하는 쿼리이다.
`인터페이스 DTO 프로젝션`을 사용하여 조회된 속성들의 이름으로 getter 인터페이스 메서드로 선언해주어 구현한다.
필요한 필드만 불러올 수 있다는 장점이 있다.
public interface CopurchasingWithWriter {
Long getWriterId();
String getWriterNickname();
Copurchasing getCopurchasing();
}
@Query("SELECT c.writer.id AS writerId, c.writer.nickname AS writerNickname, c AS copurchasing "
+ "FROM Copurchasing c JOIN c.writer")
List<CopurchasingWithWriter> findAllWithWriter();
📌 Fetch Join
하나의 쿼리로 즉시 로딩되어야 하는 연관관계를 함께 불러오는 쿼리이다.
모든 필드를 다 읽어온다.
@Query("SELECT c FROM Copurchasing c JOIN FETCH c.writer")
List<Copurchasing> findFetchAllWithWriter();
`JOIN FETCH` 를 사용하여 연관관계로 이어진 객체를 함께 조회한다.
JOIN FETCH를 사용하면 기본적으로 지연 로딩(LAZY) 전략이 즉시 로딩(EAGER)처럼 동작하게 되어 N+1 문제를 해결할 수 있다.
📌 엔티티 그래프 사용하기
@Query("SELECT c FROM Copurchasing c")
@EntityGraph(attributePaths = "writer", type = EntityGraphType.FETCH)
List<Copurchasing> findAllEntityGraphWithWriter();
`@Query` 에 들어가는 쿼리에는 일반 조회 쿼리를 작성하고, `@EntityGraph` 어노테이션으로 어떤 속성을 FETCH 해올 것인지 선택한다.
`@EntityGraph`의 type은 `EntityGraphType.FETCH`와 `EntityGraphType.LOAD` 2가지가 있다.
`FETCH` entity graph에 명시한 attribute는 EAGER로 패치하고, 나머지 attribute는 LAZY로 패치
`LOAD` entity graph에 명시한 attribute는 EAGER로 패치하고, 나머지 attribute는 entity에 명시한 fetch type이나 디폴트 FetchType으로 패치
(e.g. @OneToMany는 LAZY, @ManyToOne은 EAGER 등이 디폴트입니다.)
따라서 우리는 User 객체(writer)만 필요하기에 `FETCH` 타입으로 설정했다.
엔티티 그래프 또한 연관관계로 조회되는 엔티티의 전체 속성을 로딩하기 때문에
특정 속성만 필요한 경우 `DTO 프로젝션`을 사용하는 것이 좋다.
이 외에도 그냥 `@Query` 어노테이션을 사용하여 직접 조인 쿼리문을 작성하는 방법도 있다.
JPA는 참 어렵군 ...
'공부 > Project' 카테고리의 다른 글
Springboot 키워드 검색 API 구현하기 (QueryDSL) (1) | 2024.09.01 |
---|---|
Springboot JPA + QueryDSL 사용 시 no property found for type 오류 해결 (0) | 2024.08.31 |
Springboot, JUnit 테스트 코드 작성하기 (0) | 2024.06.25 |
@Builder 패턴을 사용하는 이유 (0) | 2024.06.21 |
어노테이션을 사용하는 방법 vs 별도의 유효성 검사 메서드를 만드는 방법 (0) | 2024.06.20 |