본문 바로가기
SpringBoot/성능 개선 흔적들

[Spring] 반복적인 추가 쿼리 @EntityGraph와 @Cacheable을 통한 성능 개선

by rla124 2024. 12. 8.

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 

성능 개선 완료한 pr 링크 

 

하나의 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 문제를 해결하는 여러 방법에 대해서 앞으로 많이 배우고 적용해볼 것