백엔드 공부/Spring Boot

spring boot의 self invocation, 이유와 해결법

철매존 2023. 5. 18. 05:36
728x90

spring boot의 self invocation, 이유와 해결법

회사에서 개발하는데 캐싱과 관련하여 self invocation 관련해서 이슈를 들었다.
그래서 한번 찾아보게 되었다.

invocation

이거는 메서드 호출이라고 생각하면 된다.

self-Injection??

일단 문제상황에 대해 알아본다

다음과 같은 코드를 작성한다. (그리고 캐시 설정은 되어있다고 가정한다.)

Ryoochan.java

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Ryoochan {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String look;

    public Ryoochan(String look) {
        this.look = look;
    }

    public Ryoochan() {

    }
}

간단한 class이다.

TestService.java

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import com.blossom.healthyblossom.hello.domain.Ryoochan;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Service
@RequiredArgsConstructor
@Slf4j
public class TestService {

    @Cacheable(cacheNames = "ryoochan", cacheManager = "testCacheManager")
    public Ryoochan ryoochanIsHandsome() {
        log.info("캐싱 없음!");
        return new Ryoochan("잘생김");
    }

    public void ryoochanIsGreat() {
        log.info("류찬은 최고다!");
        this.ryoochanIsHandsome();
    }

}

이렇게 있다.
여기서 ryoochanIsGreat같은 서비스 내에 있는 ryoochanIsHandsome을 사용하고 있다.

TestServiceTest.java

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cache.annotation.EnableCaching;


@SpringBootTest
@EnableCaching
class TestServiceTest {
    @Autowired
    private TestService testService;

    @Test
    public void test() {
        this.testService.ryoochanIsGreat();
        this.testService.ryoochanIsGreat();
        this.testService.ryoochanIsGreat();
    }

}

테스트를 위한 클래스를 생성해서 테스트해준다.
본래 생각대로면 캐싱되어있는 데이터를 불러오기 때문에
캐싱은 최초 수행 때에만 없고 그 뒤부터는 있다고 생각할 것이다.

테스트 결과

image

이렇게 나온다.
왜인지 모르겠는데, 캐싱이 계속 없다.

캐싱이 없는 이유

우리는 Spring에서 Cache를 사용할 때에 @EnableCaching을 사용한다.
저 어노테이션을 설정하는 것만으로 Cache를 사용할 수 있게 되는 것인데, 그 이유는 spring에서 이거를 proxy로 생성해서(알아서 동작할 때에 interface 기반의 인터페이스-구현체로 만듦) 동작시키기 떄문이다.

근데 문제는 Spring AOP에서 self-invocation(자신 내부의 메서드 호출)을 하는 경우는 이런 처리가 되지 않는다.

그래서 caching이 동작하지 않게 된 것이다!!

해결법

이제 이유를 알았으니 해결하면 되는데, 해결법은 간단하다.

저 Proxy가 만들어지게 하면 되는 것이다.

방법은 여러 가지가 있는데 내가 여기서 다루는 것은

  • 외부 service를 통한 호출
  • AopContext를 통한 호출
  • Bean사용

이다.

해결1) 외부 service를 통한 호출

걍 self invocation 하지 말고 외부에서 호출하는거다.
간단하니까 넘어감.

해결2) AopContext를 통한 호출

그냥 지금 메서드에서 aop proxy를 만들도록 하는 것이다.

import org.springframework.aop.framework.AopContext;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import com.blossom.healthyblossom.hello.domain.Ryoochan;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Service
@RequiredArgsConstructor
@Slf4j
public class TestService {

    @Cacheable(cacheNames = "ryoochan", cacheManager = "testCacheManager")
    public Ryoochan ryoochanIsHandsome() {
        log.info("캐싱 없음!");
        return new Ryoochan("잘생김");
    }

    public void ryoochanIsGreat() {
        log.info("류찬은 최고다!");
        ((TestService)AopContext.currentProxy()).ryoochanIsHandsome();
    }

}

이런 식으로 하는 것인데, 저기 AopContext.currntProxy() 메서드를 보면

image

이렇게 나온다.
위에 보면 해당 메서드를 통해 Aop Proxy를 반환할 수 있다고 적혀 있다.
그리고 내부에서도 proxy라는 object를 만들어 반환한다.

근데 좀 살펴보면 저게 AOP를 통해 호출되고 AOP 프레임워크가 프록시를 노출하도록 설정된 경우에만 사용할 수 있다는 말이 있다. 그렇지 않으면 exception이 던져진다고 한다.

저 문제는 Spring Boot에서 매우 쉽게 해결될 수 있다.

@EnableAspectJAutoProxy(exposeProxy = true)

얘를 써주면 된다.
이것도 내부를 보면

image

요렇게 되어 있다.
AOP 프레임워크에 의해 스레드로컬로 노출되어야 함을 나타내는 것으로, 이걸 true로 해주면 노출되게 될 것이다.

그래서 이제 한번 시행해 보면

image

이렇게 캐싱이 잘 동작함을 확인 가능하다!!!

해결3) Bean사용

위의 방식은 매번 저 Aop어쩌고를 써야하고, @EnableAspectJAutoProxy(exposeProxy = true)이것도 설정해줘야 했다.

알다시피 다양한 annotation들.. 예를 들어 @Service @Controller @Component 등을 쓰면 이는 bean으로 주입된다.

그러면 걍 해당 사용 위치에서 자기 자신을 미리 bean으로 등록시켜 둔다면??
간단하게 해결될 것이다.

ApplicationContextProvider.java
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class ApplicationContextProvider implements ApplicationContextAware{

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext ctx) throws BeansException {
        applicationContext = ctx;
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

}

ApplicationContext의 관리를 위한 클래스를 만들어 준다.
이렇게 하면 getApplicationContext()를 통해 applicationContext를 받아올 수 있게 된다.

Util.java
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

@Component

public class Util {
    public static <T> T getBean(Class<?> classType) {
        ApplicationContext applicationContext = ApplicationContextProvider.getApplicationContext();
        return (T) applicationContext.getBean(classType);
    }
}

여기서는 위에서 만든 applicationContextProvider를 통해 getBean으로 applicationContext bean을 가져올 수 있게 된다.

generic method를 사용하여 어떤 곳에서든 바로바로 사용할 수 있도록 했다.

그럼 이제 service에서

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import com.blossom.healthyblossom.hello.domain.Ryoochan;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import static com.blossom.healthyblossom.config.Util.getBean;

@Service
@RequiredArgsConstructor
@Slf4j
public class TestService {
    private TestService getTestService() {
        return getBean(TestService.class);
    }

    @Cacheable(cacheNames = "ryoochan", cacheManager = "testCacheManager")
    public Ryoochan ryoochanIsHandsome() {
        log.info("캐싱 없음!");
        return new Ryoochan("잘생김");
    }

    public void ryoochanIsGreat() {
        log.info("류찬은 최고다!");
        this.getTestService().ryoochanIsHandsome();
    }

}

이런 식으로, getBean을 써서 스스로 서비스의 클래스를 통해 빈을 등록시켜 주면 될것이다!!

테스트 해보면

image

이렇게 잘 나온다.

결론

이상으로 self invocation의 원인과 문제점, 여러 방법을 통해 이를 해결하는 방안에 대해 알아보았다.
이게 참 문제가 되는데 모르면 당할수밖에 없는 것이라 미리미리 알아두어야 할 것 같다.