스프링에서는 이미 많은 부분에서 AOP들이 적용되어있다. 이번 포스트에서는 여러가지 예를 통해서 어떤 면에서 스프링의 AOP가 활용되는지 살펴보자.
singleton의 비밀
스프링은 빈을 관리할 때 특별히 scope 속성을 변경하지 않는다면 singleton으로 관리해준다. 사실 @Bean의 메서드를 호출하기만 하면 언제나 새로운 빈이 만들어지는데 어떻게 그런일이 가능할까?
@SpringBootApplication
public class AopApplication {
public static void main(String[] args) {
SpringApplication.run(AopApplication.class, args);
}
@Bean // 메서드 호출 시 빈이 생성됨
public HeavyWorkBean heavyWorkBean(){
System.out.println("HeavyWorkBean 생성됨.");
return new HeavyWorkBean();
}
}
public class HeavyWorkBean {
public void doHeavyWork(){
// do heavy work
}
}
실제로 테스트 과정에서 Spring Framework가 관리하는 빈을 얻어와 보고 직접 heavyWorkBean()을 호출해서 객체를 얻어와본 결과 두 객체는 동일하다는 것을 알 수 있다.
@SpringBootTest
@ActiveProfiles("dev")
@Slf4j
public class CafefulTest {
@Autowired
HeavyWorkBean hbean;
@Autowired
AopApplication app;
@Test
public void beanTest(){
Assertions.assertNotNull(hbean);
HeavyWorkBean bean = app.heavyWorkBean();
Assertions.assertSame(hbean,bean) ;
log.debug("app: {}", app.getClass().getName());
}
}
마지막에 출력해본 AopApplication의 실제 타입은 원래의 클래스가 아닌 프록시 객체임을 알 수 있다. 즉 외부에서 빈을 요청할 때 @Configuration의 빈 생성 메서드를 요청하는것이 아니라 프록시에게 요청하는 것이고 프록시는 만약 해당 타입의 빈이 있을 경우 새롭게 빈을 생성하지 않고 기존 빈 객체를 리턴해주는 것이다.(@SpringBootApplication은 내부적으로 @Configuration을 갖는다.)
스프링에서 비동기 작업의 처리
무거운 작업을 비동기로 병렬처리하기 위해 스레드나 스레드 풀을 만들고 관리하는 일은 상당히 번거롭다.스프링에서는 간단한 애너테이션 몇 개로 편하게 비동기 처리를 구현한 수 있다.미리 밝히지만 이것도 역시AOP가 있기에 가능하다.
@Async
@Async는 비동기로 수행할 작업이 있는 메서드에 선언한다.
@Slf4j
public class HeavyWorkBean {
@Async
public void doHeavyWork(int workId) throws InterruptedException {
Thread.sleep(1000);
log.debug("{}에서 {} 작업 처리 중", Thread.currentThread().getName(), workId);
}
}
doHeavyWork는 무거운 작업을 수행하는 메서드이고 비동기로 처리할 계획이다. 그냥 처리 한다면 별도의 Thread를 만들고 run 메서드를 override 하는 등 복잡한 절차를 거쳤을 것이다. 하지만 여기서는 @Async 가 전부이다.
@EnableAsync
@Async를 활성화 하기 위해서는 @Configuration 클래스에서 @EnableAsync 를 선언해주어야 한다.
@SpringBootApplication
@EnableAsync
public class AoptestApplication {
public static void main(String[] args) {
SpringApplication.run(AoptestApplication.class, args);
}
}
참고로 application.properties 파일에서는 thread pool의 기본 thread 개수나 최대 thread 개수 등을 설정할 수 있다.
테스트 코드에서는 단지 doHeavyWork를 실행시키기만 할 뿐 역시 Thread에 대한 언급은 없다.
18:04:21 [DEBUG] com.doding.aop.CafefulTest.heaavyWorkTest.32 :
bean type: com.doding.aop.beans.HeavyWorkBean$$SpringCGLIB$$0
18:04:22 [DEBUG] c.d.aop.beans.HeavyWorkBean.doHeavyWork.11 : task-5에서 4 작업 처리 중
18:04:22 [DEBUG] c.d.aop.beans.HeavyWorkBean.doHeavyWork.11 : task-1에서 0 작업 처리 중
18:04:22 [DEBUG] c.d.aop.beans.HeavyWorkBean.doHeavyWork.11 : task-2에서 1 작업 처리 중
18:04:22 [DEBUG] c.d.aop.beans.HeavyWorkBean.doHeavyWork.11 : task-3에서 2 작업 처리 중
18:04:22 [DEBUG] c.d.aop.beans.HeavyWorkBean.doHeavyWork.11 : task-6에서 5 작업 처리 중
18:04:22 [DEBUG] c.d.aop.beans.HeavyWorkBean.doHeavyWork.11 : task-8에서 7 작업 처리 중
18:04:22 [DEBUG] c.d.aop.beans.HeavyWorkBean.doHeavyWork.11 : task-4에서 3 작업 처리 중
18:04:22 [DEBUG] c.d.aop.beans.HeavyWorkBean.doHeavyWork.11 : task-7에서 6 작업 처리 중
18:04:23 [DEBUG] c.d.aop.beans.HeavyWorkBean.doHeavyWork.11 : task-5에서 8 작업 처리 중
18:04:23 [DEBUG] c.d.aop.beans.HeavyWorkBean.doHeavyWork.11 : task-1에서 9 작업 처리 중
실행 결과를 살펴보면 너무나 당연하게도 빈의 타입이 proxy이며 Thread에 대한 코드 한줄 없이 ThreadPool이 잘 동작하고 있음을 알 수 있다.
즉 proxy 객체가 앞에 끼어들어서 무거운 작업을 thread pool에서 동작하도록 처리하는 것이다.
Spring AOP의 한계
proxy 기반의 AOP
사실 AOP의 컨셉은 스프링이 만든것은 아니다. Spring 이전에 AspectJ라는 기술이 횡단 관심사의 모듈화 개념을 위해 사용되고 있었고 스프링에서는 이를 좀 더 간편하게 구성했다.
기본적으로 AspectJ는 훨씬 더 정교하며 성능도 좋지만 복잡하다. 스프링의 AOP는 AspectJ의 간편화 버전이라고 생각할 수 있다. 이때 가장 중요하게 생각해야하는 차이는 프록시 메커니즘이다. 스프링 AOP는 빈에 대한 프록시를 이용해서 AOP를 적용한다. 따라서 proxy를 통한 접근에 AOP가 적용되며 반대로 proxy를 통하지 않은 접근은 AOP의 대상이 아니다.
빈 내부에 AOP의 대상인 methodA와 methodB가 있다고 생각해 보자. 이 메서드들을 외부에서 호출할 때는 당연히 Proxy를 통하게 된다. 이 과정에서 methodB가 methodA를 내부적으로 호출한다면 어떻게 될까?
직접 methodA를 호출했을 때와는 달리 proxy 입장에서는 methodB만 호출된 것이므로 advice를 동작하지 않는다. 즉 내부에서 호출되는 경우는 AOP의 적용대상이 아님을 주의하자.