본문 바로가기

개발/Spring

리팩토링 - 패키지를 넘나드는 캐시

현재 프로젝트는 기능 기반 패키지 구조로 되어있다. service 레이어에 @Cacheable을 추가하고 해당 캐시 데이터를 수정하는 메서드에 @CacheEvict 설정을 했다. 메서드를 찾아 설정을 하다보니 패키지를 넘나들면서 캐시를 생성/제거하고 있었다. 같은 캐시는 같은 패키지 안에서 핸들링 할 수 있어야 하지 않을까? 라는 의문이 들었다.

서로 다른 패키지의 서비스에서 캐시 저장과 제거를 한다면 어디서 해당 캐시를 저장/제거하는지 일일이 찾아야 하기 때문에 유지보수가 어렵다. 같은 캐시는 같은 패키지에서 핸들링 할 수 있도록 리팩토링을 하려고 한다.

 

 

예시 상황

  1. 여러 서비스에서 사용 가능한 포인트를 관리하는 포인트 지갑이 있다.
  2. 회원 포인트 잔고 조회 → 포인트 지갑 서버로 현재 잔고 요청
    1. 포인트 지갑 서버에 짧은 시간 내 연속 요청하면 에러가 발생하여 캐시 적용 필요
    2. 실시간 포인트 잔고 조회가 필요하니 스케줄러를 통해 1분마다 캐시 제거
  3. 리워드로 포인트를 지급 → 포인트 지갑 서버로 포인트 전송 요청
    1. 스케줄러로 한번에 여러 회원에게 포인트 전송
    2. 회원 포인트 잔고 업데이트 필요 ⇒ 모든 회원 포인트 잔고 캐시 제거

 

 

현재 코드

member.service.MemberService

public class MemberService {
    @Cacheable(value = "getMemberPoint", key = "#memberId")
    public MemberPointDto getMemberPoint(Long memberId) {
        Member member = findByMember(memberId);
        int point = pointService.getMemberPoint(member.getAddress());
        return new MemberPointDto(point);
    }
}

 

 

point.service.PointTransferService

public class PointTransferService {
    @CacheEvict(value = "getMemberPoint", allEntries = true)
    public void sendPoints(List<String> addressList, int amount) {
            addressList.forEach(address -> pointclient.sendPoint(address, amount));
    }
}

 

 

문제점

  • 회원 패키지에서 캐싱한 캐시를 포인트 패키지에서 삭제
    • → 같은 캐시를 서로 다른 패키지에서 핸들링하면 유지보수와 이해가 어려워짐

 

 

캐시를 데이터를 소유하는 서비스에서 관리하도록 해보자.

  • getMemberPoint는 회원 관련 정보를 캐싱하기 때문에 캐시 관리 책임을 MemberService가 가진다.
  • PointTransferService가 직접 캐시를 삭제하지 않고 MemberService에 요청한다.

 

회원 서비스에서 회원에 대한 캐시를 처리

 

member.service.MemberService

@Cacheable(value = "getMemberPoint", key = "#memberId")
public MemberPointDto getMemberPoint(Long memberId) {
    ...
}

@CacheEvict(value = "getMemberPoint", allEntries = true)
public void updateMemberPoint(Long memberId) {
    // 캐시 제거 및 필요한 후처리 수행
}

 

 

point.service.PointTransferService

public void sendPoints(List<String> addressList, int amount) {
    pointClient.sendPoint(addressList, amount);
    // 회원 서비스에 캐시 제거 요청
    memberService.updateMemberPoint();
}

 

 

의문점

그럼 포인트 전송 서비스에서도 getMemberPoint가 존재한다는 것을 알고 있어야 하는 것 아닌가? 이게 @CacheEvict를 직접 사용 하는 것과 무슨 차이가 있을까?

→ 포인트 전송 서비스는 캐시 존재를 몰라도 되고 캐시를 직접 다루지 않는다. 회원 서비스에 포인트 잔고가 갱신된 사실만 전달하고 후처리는 회원 서비스에서 알아서 할 일이다.

 

도메인 이벤트 활용

회원 서비스에서 캐시 삭제를 하는 것이 아니라 데이터 변경 이벤트를 발행하고 감지하여 처리하는 방식을 사용할 수도 있다.

 

  1. 회원 업데이트 이벤트를 생성
  2. 포인트 전송 시 이벤트를 발행
  3. MemberService에서 이벤트 리스너로 캐시를 삭제

 

member.service.MemberService

@CacheEvict(value = "getMemberPoint", allEntries = true)
@TransactionalEventListener // default: after commit
public void updateMemberPoint(MemberPointUpdatedEvent event) {
    // 캐시 제거 및 필요한 후처리 수행
}

 

 

point.service.PointTransferService

private final ApplicationEventPublisher eventPublisher;

public void sendPoints(List<String> addressList, int amount) {
    ...
    eventPublisher.publishEvent(new MemberPointUpdatedEvent());
}

 

 

장점

  • 도메인 이벤트를 활용하면 서비스 간 결합도를 낮출 수 있다.
  • 다른 서비스도 활용할 수 있어 확장성이 높다.

단점

  • 이벤트 흐름이 숨겨져 있어 파악이 어렵다.
  • Spring 이벤트 시스템을 이해하고 활용할 수 있어야 한다.

 

현재 순환 참조 문제와 결합도를 해결하기 위한 리팩토링도 해야하기 때문에 Spring 이벤트에 대해 더 알아보고 리팩토링을 진행해야겠다.

 

 

참고

챗지피티