본문 바로가기
개발

@Cacheable은 this 호출로 동작하지 않는다.

by 상5c 2021. 12. 5.

1. 프록시

스프링의 Cache, AOP, Transaction은 Proxy를 사용합니다. 여긴 조금 더 조사가 필요하지만, 스프링에서 CGLIB 이라는, 동적으로 프록시 객체를 구성하는 라이브러리를 사용하고 있습니다.

프록시 객체는 실제 사용되는 객체를 상속받아, 원하는 작업(AOP, cache 등)을 수행하고 실제 객체를 호출하는 역할을 합니다.

프록시에 대한 예제 코드는 여기를 참고하시면 좋을 것 같습니다

 

2. Autowired

@Autowired 어노테이션은 ApplicationContext에서 알맞은 Bean을 찾아 주입해주는 역할을 합니다.

@EnableCaching 어노테이션이 사용된 경우, 의존성 주입 시에 @Cacheable이 사용된 Bean은 실제 구현체 대신 스프링에서 한번 더 감싼 프록시 객체를 주입받게 됩니다.

@Cacheable이 사용된 클래스를 SampleService라고 칭하고, @Service 어노테이션이 붙어있다고 가정하겠습니다.

다른 클래스에서 서비스를 스프링을 통해 주입받게 되면 @Autowired SampleService sampleService 형태로 사용합니다. 이때 스프링에서 다음과 같이 만들어줍니다.

  • SampleService sampleService = new 서비스를상속받은프록시()

때문에 sampleService.getClass()를 출력해보면

  • class com.example.demosample.cache.SampleService$$EnhancerBySpringCGLIB$$872c3def

이런 형태로 출력됩니다.

 

3. 외부에서 캐시 메소드 호출

호출하는 쪽에서는 @Autowired를 사용했기 때문에 SampleService의 프록시 객체를 갖고 있습니다. @Cacheable이 사용된 메소드를 cc()라고 하겠습니다.

캐시를 타는 로직을 정리하면,

  1. 외부에서 sampleService.cc() 호출 (실제로는 프록시의 cc가 호출됨)
  2. 프록시 내에서 캐시와 관련된 처리
    • (if 전에 사용되었다면 저장해둔 결과 반환, else 저장해둔 게 없으면 실제 객체 호출)
  3. (캐시가 없는 경우에만 동작) 실제 객체 내 데이터 조회, 가공 등 로직 수행
  4. 결과 반환

이렇습니다.

 

4. this를 통한 캐시 메소드 호출

마찬가지로 호출하는 쪽에서는 @Autowired를 사용했기 때문에 SampleService의 프록시 객체를 갖고 있습니다. 호칭도 마찬가지로 @Cacheable이 사용된 메소드를 cc(), 사용되지 않은 메소드를 dd() 라고 하겠습니다. dd() 는 내부에서 this.cc()를 호출하고 있습니다.

캐시를 타지 않는 로직을 정리하면,

  1. 외부에서 sampleService.dd() 호출
    • 여기도 실제로는 프록시의 dd() 가 호출됩니다.
  2. 프록시 내에서 처리 수행 dd()메소드에는 @Cacheable 어노테이션이 사용되지 않았기 때문에 별다른 처리 없이 실제 객체의 dd() 메소드가 호출됩니다.
  3. dd() 내부에서 this.cc() 메소드 호출 → 실제 객체 내부에서 this를 통해 호출했기 때문에 cc()의 로직만 수행합니다.
  4. 결과 반환

this를 사용하면 자기 자신을 직접 호출합니다. 때문에 위 3. 외부에서 캐시 메소드 호출 내용의 로직 2번이 수행되지 않습니다.

 

5. 해결 방법

ApplicationContext에서 자기 자신의 Bean을 꺼내와서 사용하면 프록시를 사용할 수 있습니다.

스프링에서 Bean을 다시 조회해서, 조회한 Bean의 cc() 메소드를 호출하는 방법을 사용하여 해결합니다.

 

전체 코드

@EnableCaching
@Service
public class SampleService {

    @Autowired
    private ApplicationContext applicationContext;

    @Cacheable("sample")
    public String cc(String s) {
        System.out.println("NOT CACHED!@ : " + s);
        return s + "#";
    }

    public String dd(String s) {
        System.out.println("DD");
        return this.cc(s) + "@";
    }

    public String aa(String s) {
        SampleService sampleService = applicationContext.getBean("sampleService", SampleService.class);
        return sampleService.cc(s);
    }

}
@SpringBootTest
class SampleServiceTest {

    @Autowired
    SampleService sampleService;

    @DisplayName("캐시 사용됨")
    @Test
    void cache() {
        sampleService.cc("GOOD!"); // 여기서만 NOT CACHED 출력됨
        sampleService.cc("GOOD!");
        sampleService.cc("GOOD!");
        sampleService.cc("GOOD!");
    }

    @DisplayName("캐시 사용 안됨")
    @Test
    void notCached() {
        sampleService.dd("FAIL"); // cc() 메소드가 프록시를 타지 않기 때문에 모두 NOT CACHED 출력됨
        sampleService.dd("FAIL");
        sampleService.dd("FAIL");
        sampleService.dd("FAIL");
        sampleService.dd("FAIL");
    }

    @DisplayName("캐시 사용 안되는거 해결")
    @Test
    void good() {
        sampleService.aa("GOOD"); // 여기서만 NOT CACHED 출력됨
        sampleService.aa("GOOD");
        sampleService.aa("GOOD");
        sampleService.aa("GOOD");

		System.out.println(sampleService.getClass()); // class com.example.demosample.cache.SampleService$$EnhancerBySpringCGLIB$$92c9bc2f
    }

}

참고