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();
}
}
테스트를 위한 클래스를 생성해서 테스트해준다.
본래 생각대로면 캐싱되어있는 데이터를 불러오기 때문에
캐싱은 최초 수행 때에만 없고 그 뒤부터는 있다고 생각할 것이다.
테스트 결과
이렇게 나온다.
왜인지 모르겠는데, 캐싱이 계속 없다.
캐싱이 없는 이유
우리는 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()
메서드를 보면
이렇게 나온다.
위에 보면 해당 메서드를 통해 Aop Proxy를 반환할 수 있다고 적혀 있다.
그리고 내부에서도 proxy라는 object를 만들어 반환한다.
근데 좀 살펴보면 저게 AOP를 통해 호출되고 AOP 프레임워크가 프록시를 노출하도록 설정된 경우에만 사용할 수 있다는 말이 있다. 그렇지 않으면 exception이 던져진다고 한다.
저 문제는 Spring Boot에서 매우 쉽게 해결될 수 있다.
@EnableAspectJAutoProxy(exposeProxy = true)
얘를 써주면 된다.
이것도 내부를 보면
요렇게 되어 있다.
AOP 프레임워크에 의해 스레드로컬로 노출되어야 함을 나타내는 것으로, 이걸 true로 해주면 노출되게 될 것이다.
그래서 이제 한번 시행해 보면
이렇게 캐싱이 잘 동작함을 확인 가능하다!!!
해결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을 써서 스스로 서비스의 클래스를 통해 빈을 등록시켜 주면 될것이다!!
테스트 해보면
이렇게 잘 나온다.
결론
이상으로 self invocation의 원인과 문제점, 여러 방법을 통해 이를 해결하는 방안에 대해 알아보았다.
이게 참 문제가 되는데 모르면 당할수밖에 없는 것이라 미리미리 알아두어야 할 것 같다.
'백엔드 공부 > Spring Boot' 카테고리의 다른 글
IntelliJ plugin을 만들어 보자 - method tester (2) (0) | 2024.11.13 |
---|---|
IntelliJ plugin을 만들어 보자 - method tester (1) (2) | 2024.11.12 |
querydsl에서 다대다 구현하기. 일대다-다대일(feat. Result Aggregation) (2) | 2023.02.21 |
save랑 saveAll(feat. Transactional) (0) | 2023.01.18 |
lucy필터로 xss 방어하기(feat JSON, time, 이모지) (0) | 2022.06.27 |