일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | |
7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 |
- javaJRE
- 항해15기
- java참조자료형
- java기본자료형
- java set 저장
- 작은수제거하기
- java map 출력
- 노베이스부트캠프
- 프로그래머스제일작은수
- 항해99후기
- 프로그래머스
- 격파르타합격후기
- 격파르타장점
- java최솟값구하기
- 비전공자sqld
- java 자료구조 활용
- javaJVM
- java list 출력
- 코딩부트캠프후기
- 인터프린터언어
- 격파르타비전공자
- java알고리즘문제풀이
- java map 저장
- java map
- sqld자격증합격
- 격파르타후기
- java알고리즘
- java set 출력
- java list 저장
- 컴파일
- Today
- Total
코딩과 결혼합니다
[JPA] 지연 로딩과 조회 성능 최적화(1) 본문
등록이나 수정 등은 데이터 한 건을 다루는 것이기 때문에 거의 성능 문제가 발생하지 않는다. 주로 조회하는 데에 문제가 발생하는데 이번에는 여러 방법으로 데이터를 조회해 보면서 JPA로 성능을 최적화하는 방법을 알아본다.
@GetMapping("/api/v1/simple-orders")
public List<Order> orderV1() {
List<Order> all = orderRepository.findAllByString(new OrderSearch());
return all;
}
아주 간단한 주문 조회 API이다. Order Entity를 그대로 사용하며 Order 안에는 Member와 Delivery가 포함되어 있다.
무한루프
이렇게 조회를 하게 되면 어떤 문제가 생기게 될까?
public class Order {
...
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "member_id")
private Member member;
@OneToOne(fetch = LAZY, cascade = ALL)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
...
}
Order는 memver의 정보를 가져와야 하므로 member를 참조한다.
@Entity
@Getter @Setter
public class Member {
...
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
그런데 이 meber에도 orders 가 존재한다. 그럼 member는 orders를 참조하며 무한 루프에 빠진다.
이를 해결하는 방법으로 @JsonIgnore를 사용한다. 양방향 관계일 때 둘 줄 하나에 걸어주는 것이다.
@JsonIgnore
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "order_id")
private Order order;
Order 객체를 포함한 곳에 위와 같이 @JsonIgnore를 걸어주었다.
Type Definition Error
하지만 간단하게 해결되지 않는다. 멤버와 딜리버리에 지연로딩이 걸려있기 때문이다.
- 지연로딩은 new 객체를 해서 가져오지 않는다. 즉, Order의 데이터만 가지고 오고 멤버나 딜리버리의 데이터는 손대지 않는다.
- member에 null 값을 넣을 수는 없기 때문에, 하이버네이트에서는 멤버를 상속받은 가짜 프록시 객체를 생성해서 넣어놓는다. 이것을 byteBuddy라 한다.
- 제이슨이 루프를 돌리며 오더를 가지고 멤버를 뽑아 보려 하는데 멤버가 아니고 bytebuddy라는 것이 존재하며, 이것을 어떻게 처리할 수가 없어서 오류가 난 것이다.
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'
Spring 버전 3.0이상
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5-jakarta'
+ 나는 하이버네이트 버전이 6이상이라서
'com.fasterxml.jackson.datatype:jackson-datatype-hibernate6' 를 사용해주었다.
버전 간의 호환성 문제를 방지하기 위해서이다.
bulid.gradle에 하이버네이트 6 모듈을 깔아주고 이를 사용해
지연로딩인 경우 제이슨라이브러리가 위와 같이 어떠한 행동을 하지 못하도록 해준다.
@SpringBootApplication
public class JpashopApplication {
public static void main(String[] args) {
SpringApplication.run(JpashopApplication.class, args);
}
@Bean
Hibernate6Module hibernate6Module() {
return new Hibernate6Module();
}
}
하지만 지연로딩이 걸린 것들은 모두 null을 반환한다.
DB에서 조회한 것이 아니기 때문에 제이슨이 다 무시해 버리기 때문이다.
@Bean
Hibernate6Module hibernate6Module() {
Hibernate6Module hibernate6Module = new Hibernate6Module();
//강제 지연 로딩 설정
hibernate6Module.configure(Hibernate6Module.Feature.FORCE_LAZY_LOADING,
true);
return hibernate6Module;
}
옵션을 통해서 강제로 레이지로딩을 해버려 값을 가져올 수 있다.
성능저하
이건 어떤 식으로 동작하는지 스스로 이해하기 위해 적어보았지만, 사실 애초에 엔티티 자체를 노출하는 것은 좋지 않기 때문에 자세히 알고 있을 필요는 없다.
나는 멤버와 딜리버리 정보만 필요했지만 이런 식으로 코드를 짜게 되면 굉장한 성능 낭비도 이뤄진다.
- 필요 없는 정보까지 노출 또한 DB에서 추가로 끌고 오기 때문에 쿼리가 나간다.
- 포스 레이지로딩으로 레이지로딩된 애들을 모두 가져오며 어마어마한 쿼리가 나가게 된다.
@GetMapping("/api/v1/simple-orders")
public List<Order> orderV1() {
List<Order> all = orderRepository.findAllByString(new OrderSearch());
for (Order order : all) {
order.getMember().getName();//Lazy 강제 초기화
order.getDelivery().getAddress();//Lazy 강제 초기화
}
return all;
}
이를 해결하는 방법이 있긴하다. 강제 지연 로딩하는 옵션을 주석처리하고 name, adrress를 꺼내 옴으로 강제로 지연 로딩을 초기화한다. 강제 초기화가 된 것은 데이터를 가지고 있기 때문에 정상적으로 잘 출력이 된다.
그러나 여전히 member를 보면 다른 필요 없는 엔티티까지 모두 포함한다. 이를 이미 외부에서 써버리게 된다면
나중에 그 내용을 하나 바꾸려 할 때에 문제가 생긴다.
📌지연로딩(LAZY)을 피하기 위해 즉시로딩(EARGR)으로 설정하면 안된다! 즉시 로딩 때문에 연관관계가 필요없는 경우에도 데이터를 항상 조회하기 때문에 성능 문제가 발생할 수 있다. 즉시 로딩으로 설정하면 성능 튜닝이 매우 어려워 진다.
항상 지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우에는 패치 조인(fetch join)을 사용한다.
'2세 > JPA' 카테고리의 다른 글
[JPA] 프록시 (0) | 2024.02.21 |
---|---|
[JPA] 지연 로딩과 조회 성능 최적화(2) (0) | 2024.02.20 |
[JPA] API 설계 시 주의할 점(DTO 사용하기) (0) | 2024.02.17 |
[JPA] 아키텍처 패턴(+ 디자인 패턴) (0) | 2024.02.15 |
[JPA] 상속관계 매핑 - 테이블 전략 (0) | 2024.02.14 |