Ehcache를 활용한 API 성능개선

Y_Sevin·2023년 12월 22일
0

도입 계기

현재 개발중인 서비스는 사용자의 얼굴을 분석하고 이에 알맞은 헤어 추천 보고서를 출력해준다.
보고서는 얼굴을 분석한 요소의 값(눈이 크다, 작다 등)과 헤어스타일을 추천하는 로직을 통해 결정이 되고 보고서의 문장들은 DB에서 가져온다.

DB에서 보관하는 이유
보고서는 자주 변경되지 않지만 해당 내용 안에 연예인의 이름이 포함되어 있다. 만일 보고서에 포함된 연예인이 사회적 이슈가 생긴다면, 보고서에 내용을 지우던 수정하던 빠른 대응을 해야한다.때문에 자주 변경되지는 않더라도 일부러 DB에 저장한 후 언제든 수정할 수 있도록 구현하였다.

변경하는 이유
서비스를 운영하며 서버 과부하가 발생해 서비스 최적화를 해야하는 상황이었다. 헤어 보고서 API 에서 AI를 제외하고 가장 긴 응답시간을 차지했던 것이 이 DB와의 통신 시간이었다. 서비스를 운영하며 보고서를 수정할 일이 존재하지 않았고, 정말 간혹 일어나는 일에 대해서만 수정하면 될 뿐인데 굳이 DB에서 관리해야할 필요성이 없다고 생각했다. 뿐만아니라 보고서 로직에 필요한 가중치 값들도 DB에 보관했는데 생각보다 변경하는 일이 적었으며 해당 값은 즉각적으로 변경되지 않아도 되는 값이었다.

때문에 매번 5~6개 테이블을 조회하며 네트워크를 통해 DB에 접근하는 비용을 줄이고자 Ehcache(JPA 2차 캐시)를 활용하여 성능을 향상시켰던 방법을 공유하고자한다.
이미 운영중인 서비스였기에 구조를 완전히 바꾸지는 못했다

왜 Ehcache(hibernate 2차 캐시)인가?

필자의 서비스는 Spring + JPA 기반으로 구현되어있다.
JPA는 영속성 컨텍스트에 테이블과 매핑되는 엔티티를 트랜잭션 범위 내에서 저장하고 관리한다. 이를 1차 캐시라고 하는데, 앞서 말했듯 1차 캐시는 트랜잭션을 시작하고 종료할 때까지만 유효하다. 때문에 트랜잭션 범위 외의 요청이 들어온다면 이전에 저장된 엔티티 정보를 사용하지못하고 DB에 다시 접근하여 데이터를 가져와야한다.

하지만 JPA는 애플리케이션 범위 내에서 관리하는 캐시도 지원한다. 이게 바로 2차 캐시이다.

위의 동작방식을 보면 영속성 컨텍스트는 DB 조회를 통해 결과를 가져오는 것이 아니라, 우선 2차 캐시를 조회하여 원하는 값이 존재한다면 2차캐시의 엔티티를 복사해 반환한다.

복사본을 반환하는 이유는 동시성을 극대화하기 위함이다.
1차캐시는 영속성 컨텍스트 저장 2차캐시는 SessionFactory

위와 같은 방식을 사용한다면 네트워크를 통해 DB접근하지 않고 내부 메모리에만 접근하면 되기때문에 효율적으로 사용할 수 있다.

hibernate의 구현체로 Ehcache를 사용했다.


그렇다면 굳이 hibernate 2차 캐시를 써서 엔티티 조회를 캐싱해야할까...? 그냥 Ehcache만 사용해서 DTO를 캐싱하면 안되나? 라고 생각할 수 있다.

예를 들어 네이버의 인기검색어를 생각해보자. 다수의 사람들이 인기검색어 내의 단어를 검색한다면 같은 파라미터로 메서드가 동작할 것이고, 이 안에는 여러 DB테이블과 수많은 외부 API가 동작할 것이다. 때문에 엔티티를 캐싱하는 것이 아니라 DTO 자체를 캐싱한다면 훨씬 더 빠르게 캐싱이 이뤄질 것이다.

하지만 우리의 서비스는 사용자 맞춤형 서비스이다. 같은 테이블을 매번 조회할 뿐이지 다수의 사람들이 같은 값을 입력하지않는다. 또한 보고서 생성에 필요한 값은 약 30가지의 얼굴 요소를 특징별로 가지고 있기 때문에 DTO단위로 캐싱한다면 어마어마하게 캐시의 크기가 커질 것이라 판단했다...

실무에서 2차 캐시는 거의 사용하지 않는다고 한다..

적용

https://kimyhcj.tistory.com/253 //옵션 정리

<ehcache>
    <defaultCache
        maxElementsInMemory="10000"
        eternal="true" // 영구 유지
        memoryStoreEvictionPolicy="LRU"
        ...
                  />
</ehcache>

// 캐시 설정
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@Entity
public class FaceReport {
		@Id @GeneratedValue
		private Long id;
        ...

		@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
		@OneToMany(mappedBy = "faceReport", cascade = CascadeType.ALL)
		private List<ValidDescriptionOfEffect> validDescriptionOfEffect = new ArrayList<>();
		...
}



// 보고서 업데이트 하기
@Service
@RequirmentArgsConstructor
public class CacheService {
    private final SessionFactory sessionFactory;
    
    @Transactional
    public void updateEntity(Object entity) {
        Session session = sessionFactory.getCurrentSession();
        session.refresh(entity); // 새로운 상태로 엔티티 다시로드하여 캐시 업데이트
    }
}

후기

hibernate L2캐시를 적용한 자료가 있겠지 했는데 캐시 갱신까지는 없어 공식문서를 보며 구현했다. 이렇게 자료가 보이지 않을땐 내가 잘못 생각했나..? 하며 스스로 의심하게 되는 것 같다.😥
아무래 생각해도 현재로서는 이게 최선인 것 같다. 만약 더 나은 방법이 있다면 언제든지 댓글로 남겨주시면 감사하겠습니다..!

https://docs.jboss.org/hibernate/core/3.3/reference/en/html/performance.html#performance-sessioncache

profile
매일은 아니더라도 꾸준히 올리자는 마음으로 시작하는 개발블로그😎

0개의 댓글