개요
토이 프로젝트를 진행하던 중, 사용자가 저장한 이미지를 조회하는 과정에서 불필요하게 조회 쿼리가 추가로 발생하는 것을 발견했다. 이 현상에 대해 찾아보다 즉시 로딩과 지연 로딩에 관련된 문제임을 알아냈고, 이 내용을 정리하며 어떻게 이 문제의 해결법을 찾아보기로 했다.
문제 발생
Image 와 User entity는 아래와 같이 서로 다대일 단방향 매핑으로 설계되어 있다.
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Image {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String prompt;
@Column(name = "IMAGE_SIZE")
private String size;
private String url;
@ManyToOne
@JoinColumn(name = "OWNER_ID")
private User owner;
...
}
@Getter
@Entity
@Table(name = "Member")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String email;
private String password;
...
}
그리고, 현재 프로젝트에서 추가 조회 쿼리가 발생하는 시점은 아래와 같다
- 사용자가 저장한 모든 이미지를 조회할 때
- 각 이미지의 상세 정보를 확인할 때
1. 사용자가 저장한 모든 이미지를 조회
이 경우에는 pagination을 적용하여 ImageRepository (JpaRepository) 의 findAll(pageable) 을 호출했고, 이때 실행된 쿼리는 아래와 같다.
// 1.
select i1_0.id, i1_0.owner_id, i1_0.prompt, i1_0.image_size, i1_0.url
from image i1_0
where i1_0.owner_id=?
order by i1_0.id desc offset ? rows fetch first ? rows only
// 2.
select u1_0.id,u1_0.email,u1_0.password,u1_0.username
from member u1_0
where u1_0.id=?
1번 쿼리만 나갈 것이라는 예상과는 다르게, 사용자를 조회하는 쿼리 (2번) 가 추가로 수행된 것을 확인할 수 있다.
2. 각 이미지의 상세 정보를 확인할 때
이 경우, ImageRepository (JpaRepository) 의 findByOwnerIdAndImageId(Long userId, Long imageId) 를 호출했고, 이때 실행된 쿼리는 아래와 같다.
// 1.
select i1_0.id, i1_0.owner_id, i1_0.prompt, i1_0.image_size, i1_0.url
from image i1_0
where i1_0.owner_id=? and i1_0.id=?
// 2.
select u1_0.id, u1_0.email, u1_0.password, u1_0.username
from member u1_0
where u1_0.id=?
첫번째 상황과 같이, 사용자를 조회하는 쿼리 (2번) 가 추가로 수행된 것을 확인할 수 있다.
도대체 왜 예상하지 않은 추가 쿼리가 발생한 것일까?
원인은?
결론부터 말하자면, 즉시 로딩 방식을 사용하기 때문에 이런 현상이 발생하는 것이다.
즉시 로딩 방식
즉시 로딩 (Eager Loading) 이란, 데이터를 조회할 때, 연관된 모든 객체의 데이터까지 한번에 불러오는 것이다.
그렇기 때문에 Image를 조회하는 쿼리를 실행하면, 이 Image와 매핑된 User도 같이 조회가 되는 것이다.
이 User의 조회가 필요한 상황이라면 상관이 없겠지만, Image의 조회만 필요한 상황이라면 이 추가 쿼리는 불필요하다. 어떻게 하면 이런 추가 쿼리를 방지할 수 있을까?
지연 로딩 방식
불필요한 쿼리를 막기 위해서는 지연 로딩 (Lazy Loading) 방식을 사용하면 된다.
JPA에서 @ManyToOne 의 로딩 방식은 기본적으로 즉시 로딩이다. 그래서 지연 로딩 방식을 사용하려면 아래와 같이 @ManyToOne(fetch = FetchType.LAZY) 를 설정하면 된다.
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Image {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String prompt;
@Column(name = "IMAGE_SIZE")
private String size;
private String url;
@ManyToOne(fetch = FetchType.LAZY) // 지연 로딩
@JoinColumn(name = "OWNER_ID")
private User owner;
...
}
변경 후, 문제가 되는 부분을 다시 실행하니 아래와 같이 추가적인 쿼리가 발생하지 않게 되었다.
1. 사용자가 저장한 모든 이미지를 조회
// 1. 사용자가 저장한 모든 이미지를 조회
select i1_0.id, i1_0.owner_id, i1_0.prompt, i1_0.image_size, i1_0.url
from image i1_0
where i1_0.owner_id=?
order by i1_0.id desc offset ? rows fetch first ? rows only
2. 각 이미지의 상세 정보를 확인할 때
// 2.
select i1_0.id, i1_0.owner_id, i1_0.prompt, i1_0.image_size, i1_0.url
from image i1_0
where i1_0.owner_id=? and i1_0.id=?
마무리
이렇게 문제점을 해결하는 과정에서 지연 로딩과 즉시 로딩에 대해 간략하게나마 이해 할 수 있었다. 이 fetch 방식들은 JPA를 사용하면서 자주 발생할 수 있는 N+1 문제와도 연관이 있다고 하니 이번 기회를 통해 더 자세히 학습하면 좋을 것 같다.
'JPA' 카테고리의 다른 글
[Spring / JPA] Soft Delete를 JPA에서 적용하는 방법 (0) | 2024.05.21 |
---|---|
[Spring /JPA] Spring JPA Delete Query 작성 시 주의할 점 (0) | 2023.12.26 |
[JPA] JPA Auditing (0) | 2023.12.24 |
[JPA] JPA 연관 관계 정리 (0) | 2023.12.24 |
[JPA] Hibernate entity에 Lombok 사용 시 주의사항 (0) | 2023.12.24 |