레디스와 프록시

위성구·2024년 8월 27일

Redis

목록 보기
7/7
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Slf4j
public class MemberService {
    private  final MemberCacheService memberCacheService;
    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;
    private  final JWTUtil jwtUtil;


...

    @Transactional(readOnly = true)
    public String attemptLogIn(RequestLogInDto member) {
        // DB에서 사용장 정보 가져 오기
        ResponseMemberInfoDto db_member = getMemberInfo(member.getEmail());
        // 비번 비교 로직, 토큰 생성
        String token = null;
        if(passwordEncoder.matches(member.getPassword(), db_member.getPassword())){
            token = jwtUtil.createToken(db_member.getId(),db_member.getEmail(),db_member.getRole());
        }else{
            throw new LoginFailException("이메일이나 비밀번호가 틀렸습니다.");
        }
        return token;
    }
    @Transactional(readOnly = true)
    @Cacheable(cacheNames = "memberCache", key = "args[0]")
    public ResponseMemberInfoDto getMemberInfo(String email) {

        Member member = memberRepository.findByEmail(email).orElseThrow(() -> new LoginFailException("사용자가 없습니다."));

        ResponseMemberInfoDto m_info = new ResponseMemberInfoDto(member);
        return m_info;
    }
}

이렇게 코드를 작성했다.
하지만 attemptLogIn메서드를 실행하면 getMemberInfo를 실행하면서 캐싱을 줄거라 생각했다. 하지만 예상과 다르게 케싱은 안되었다.

  • 이유: 같은 클래스 내에서 메서드를 직접 호출할 경우 Spring의 AOP 프록시가 적용되지 않아 캐싱이 동작하지 않기 때문이다.
  • Spring의 캐싱은 기본적으로 AOP(Aspect-Oriented Programming) 프록시를 사용하여 동작합니다. 이는 외부에서 프록시를 통해 메서드가 호출될 때만 @Cacheable과 같은 어노테이션이 적용된 메서드가 가로채져 캐싱 로직이 실행된다는 것을 의미합니다. 따라서, 같은 클래스 내에서 메서드를 직접 호출하면 프록시를 우회하게 되어 캐싱이 적용되지 않습니다. 사용자의 코드에서 attemptLogIn 메서드가 같은 클래스 내의 getMemberInfo 메서드를 직접 호출하고 있기 때문에 @Cacheable 어노테이션이 무시되고 캐싱이 동작하지 않는 것입니다.

해결방법

  • 메서드를 다른 빈으로 분리하기
    • getMemberInfo를 다른 클래스로 이동하고 @Service어노테이션을 작성하여 빈으로 관리하면 됩니다. 이 방법이 간단하고 좋습니다.
      MemberService.class
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Slf4j
public class MemberService {
    private  final MemberCacheService memberCacheService;
    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;
    private  final JWTUtil jwtUtil;

    @Transactional
    public Member createMember(RequestSignUpDto member){
        UserRole role = UserRole.fromString(member.getRoleCode());
        Member user;
        if(memberRepository.existsByEmail(member.getEmail())){
            throw new DuplicateKeyException("이미 가입한 이메일 입니다.");
        }
        user = Member.builder()
                .email(member.getEmail())
                .password(passwordEncoder.encode(member.getPassword()))
                .username(member.getUsername())
                .nickname(member.getNickname())
                .role(role)
                .build();
        return memberRepository.save(user);

    }
    @Transactional(readOnly = true)
    public String attemptLogIn(RequestLogInDto member) {
        // DB에서 사용장 정보 가져 오기
        ResponseMemberInfoDto db_member = memberCacheService.getMemberInfo(member.getEmail());
        // 비번 비교 로직, 토큰 생성
        String token = null;
        if(passwordEncoder.matches(member.getPassword(), db_member.getPassword())){
            token = jwtUtil.createToken(db_member.getId(),db_member.getEmail(),db_member.getRole());
        }else{
            throw new LoginFailException("이메일이나 비밀번호가 틀렸습니다.");
        }
        return token;
    }
}

MemberCacheService.class

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Slf4j
public class MemberCacheService {
    private final MemberRepository memberRepository;
    @Transactional(readOnly = true)
    @Cacheable(cacheNames = "memberCache", key = "args[0]")
    public ResponseMemberInfoDto getMemberInfo(String email) {

        Member member = memberRepository.findByEmail(email).orElseThrow(() -> new LoginFailException("사용자가 없습니다."));

        ResponseMemberInfoDto m_info = new ResponseMemberInfoDto(member);
        return m_info;
    }
}
profile
안녕하세요.

0개의 댓글