WHOA 서비스는 주제가 꽃이기 때문에 하나의 꽃 종류에 대해서도 색에 따라서 꽃말이 달라질 수 있다는 점을 고려해 꽃, 꽃말, 꽃이미지, 키워드 테이블 간 도메인 지식을 반영해 연관 관계가 맺어져 있다.
그 중에서 "색깔에 따라 꽃말이 달라질 수 있는 부분"을 고려해서 꽃과 꽃말은 일대다, 꽃말과 꽃이미지는 일대일 양방향, 꽃말과 키워드는 다대다이기 때문에 매핑 테이블을 추가로 두어 1:N:1 매핑을 하고 있으며, 키워드는 꽃다발 구매 목적 테이블과 다대다 관계를 이루어 이 중간에도 1:N:1 관계의 매핑 테이블을 두고 있는 구조이다.
문제 상황
이 서비스 특성 상 특정 꽃에 대해서 특정 꽃말에 따라서 그에 맞는 꽃 사진을 보여주는 로직이 많다. 그래서 꽃말 → 꽃이미지로 get을 통해서 꽃이미지 객체를 들고 오는 부분이 많다. flowerExpression 꽃말 객체를 response dto from static method에 넘겨 이 객체의 flowerExpression.getFlowerImage().getImageUrl() 이러한 식으로 flowerImage 테이블의 imageUrl 칼럼이 필요했다.
사실 끌고 오는 방향은 항상 꽃말에서 꽃이미지 방향이라 양방향 관계가 과연 필요할까에 대한 고민도 했다. 그러나 flowerImage 테이블에는 flower_id, flower_expression_id와 flower_image_id pk가 함께 공존하여 어떤 꽃의 어떤 꽃말의 이미지인지 하나의 테이블 상에서 유지 관리가 용이하기 때문에 꽃말과 꽃이미지 간 단방향이 아니라 양방향으로 맺어서 관리를 했다.
그리고 dto에서 하나의 flowerExpression에서 flowerImage imageUrl 값을 들고오기 위해 매번 하나의 꽃말 객체에 대해서 from flowerExpression, from flowerImage에 대한 두 번의 쿼리가 항상 찍혔다.
2024-11-22T21:38:37.161+09:00 TRACE 24028 --- [nio-8080-exec-1] org.hibernate.orm.jdbc.bind : binding parameter (1:BIGINT) <- [22]
2024-11-22T21:38:37.174+09:00 INFO 24028 --- [nio-8080-exec-1] c.w.w.d.b.s.BouquetCustomizingServiceV2 : flowerExpressionId: 1
Hibernate:
select
fe1_0.flower_expression_id,
fe1_0.flower_id,
fe1_0.flower_color,
fe1_0.flower_language
from
flower_expression fe1_0
where
fe1_0.flower_expression_id=?
2024-11-22T21:38:37.184+09:00 TRACE 24028 --- [nio-8080-exec-1] org.hibernate.orm.jdbc.bind : binding parameter (1:BIGINT) <- [1]
Hibernate:
select
fi1_0.flower_image_id,
fi1_0.flower_id,
fi1_0.flower_expression_id,
fi1_0.image_url
from
flower_image fi1_0
where
fi1_0.flower_expression_id=?
물론 두 테이블에서 서로 상대 객체 get을 할 때는 fetchType LAZY로 하여 필드를 가져와야하는 상황에서만 쿼리가 찍히도록 되어있었다.
그래서 이걸 한번에 가져올 수 있는 방법은 없는지에 대한 고민에서 출발했다.
해결 방안 : EntityGraph && Cacheable
하나의 flowerExpression에서 매번 하나의 flowerImage 추가 쿼리가 생기는 상황에서 매번 get으로 들고오기 때문에 예기치 않은 쿼리는 아니지만 join을 통해서 해결해야겠다고 생각했다.
이전에 N+1 문제를 querydsl의 fetch join을 통해 해결을 해본 경험(내 블로그 링크)이 있어서 이번에는 다른 방법을 이용해보고 싶었고 @EntityGraph annotation을 알게 되었다.
flowerExpressionRepository에 아래와 같이 코드를 작성하였다. flowerExpression에 flowerImage 필드가 있으므로 entityGraph를 통해서 join하여 한번에 가져오도록 하였다.
@EntityGraph(attributePaths = {"flowerImage"})
FlowerExpression findByFlowerExpressionId(Long flowerExpressionid);
그 결과 이제 하나의 flowerExpression에 대해서 flowerImage 테이블의 imageUrl로 같이 가져올 수 있게 되었다.
Hibernate:
select
fe1_0.flower_expression_id,
fe1_0.flower_id,
fe1_0.flower_color,
fi1_0.flower_image_id,
fi1_0.flower_id,
fi1_0.flower_expression_id,
fi1_0.image_url,
fe1_0.flower_language
from
flower_expression fe1_0
left join
flower_image fi1_0
on fe1_0.flower_expression_id=fi1_0.flower_expression_id
where
fe1_0.flower_expression_id=?
그리고 이 과정이 비즈니스 로직 상 반복적인 로직이었기 때문에 캐시를 적용함으로써 조회 속도도 함께 높였다. 파라미터 0번째 인자인 flowerExpressionId를 key 값으로 두었다.
@Transactional(readOnly = true)
@Cacheable(cacheNames = "flowerImage", key = "#p0")
public String getFlowerImageUrlByFlowerExpressionId(Long flowerExpressionId) {
return flowerExpressionRepository.findByFlowerExpressionId(flowerExpressionId).getFlowerImage().getImageUrl();
}
그 결과 코드 성능 개선 전에 비해 캐시까지 적용한 이후로 약 64.7% 성능 개선을 이룰 수 있었다. 속도가 대략 3배 정도 빨라졌다.
N+1 문제를 해결하는 여러 방법에 대해서 앞으로 많이 배우고 적용해볼 것
'SpringBoot > 성능 개선 흔적들' 카테고리의 다른 글
[Spring] RestTemplate -> WebClient 외부 API 호출 방식 변경 및 bulk 처리 (0) | 2024.12.07 |
---|---|
[Spring] DTO 반환 시 필요한 응답만 QueryProjection으로 매핑하여 반환 (0) | 2024.12.07 |
[Spring] Local Cache vs Global Cache 성능 비교 실험을 통해 배운 점 (0) | 2024.08.26 |
[Spring] 레디스 없이 caffeine cache로 조회 API 성능 개선 (0) | 2024.08.26 |
[Spring] Redis @Cacheable를 이용한 조회 API 성능 개선 (0) | 2024.08.15 |