스프링에서는 이미 많은 부분에서 AOP들이 적용되어있다. 이번 포스트에서는 여러가지 예를 통해서 어떤 면에서 스프링의 AOP가 활용되는지 살펴보자.
singleton의 비밀
스프링은 빈을 관리할 때 특별히 scope 속성을 변경하지 않는다면 singleton으로 관리해준다. 사실 @Bean의 메서드를 호출하기만 하면 언제나 새로운 빈이 만들어지는데 어떻게 그런일이 가능할까?
package com.doding.aoptest.internalusage;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BeanConfig {
@Bean
public Engine engine() { // engine()가 불리면 Engine이 만들어졌을 것!
return new Engine();
}
@Bean
public Car car() { // car()가 불리면 Car를 만드는데.
return new Car(engine()); // 내부적으로 engine()을 부른다. 또 만드는 것일까?
}
class Engine {
}
class Car {
Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
}
}
@Configuration이 적용되어있는 클래스의 실제 타입을 살펴보자.
@SpringBootTest
@Slf4j
@ActiveProfiles("test")
public class InternalUsageTest {
@Autowired
private BeanConfig config;
@Test
public void configTest() {
assertNotNull(config);
log.debug("config: {}", config.getClass().getName());
}
}
출력 결과 실제 타입은 원래의 클래스가 아닌 프록시 객체임을 알 수 있다. 즉 외부에서 빈을 요청할 때 @Configuration의 빈 생성 메서드를 요청하는것이 아니라 프록시에게 요청하는 것이고 프록시는 만약 해당 타입의 빈이 있을 경우 새롭게 빈을 생성하지 않고 기존 빈 객체를 리턴해주는 것이다.
스프링에서 비동기 작업의 처리
무거운 작업을 비동기로 병렬처리하기 위해 스레드나 스레드 풀을 만들고 관리하는 일은 상당히 번거롭다.스프링에서는 간단한 애너테이션 몇 개로 편하게 비동기 처리를 구현한 수 있다.미리 밝히지만 이것도 역시AOP가 있기에 가능하다.
@Async
@Async는 비동기로 수행할 작업이 있는 메서드에 선언한다.
class GPTEngine {
@Async
public void heavyWork(int i) throws InterruptedException {
Thread.sleep(1000);
log.debug("{}에 의해 {}번째 요청 처리 중", Thread.currentThread(), i);
}
}
heavyWork는 무거운 작업을 수행하는 메서드이고 비동기로 처리할 계획이다. 그냥 처리한다면 별도의 Thread를 만들고 run 메서드를 override 하는 등 복잡한 절차를 거쳤을 것이다. 하지만 여기서는 @Async 가 전부이다.
application.properties 파일에서는 thread pool의 기본 thread 개수나 최대 thread 개수 등을 설정할 수 있다.
@Async를 활성화 하기 위해서는 @Configuration 클래스에서 @EnableAsync 를 선언해주어야 한다.
@SpringBootApplication
@EnableAsync
public class AoptestApplication {
public static void main(String[] args) {
SpringApplication.run(AoptestApplication.class, args);
}
}
단위테스트
단위테스트를 통해 비동기 작업의 동작을 테스트 해보자.
@Autowired
private GPTEngine engine;
@Test
public void asyncTest() throws InterruptedException {
log.debug("engine type: {}", engine.getClass().getName());
for (int i = 0; i < 10; i++) {
engine.heavyWork(i);
}
}
테스트 코드에서는 단지 heavyWork를 실행시키기만 할 뿐 역시 Thread에 대한 언급은 없다.
16:20:35 [DEBUG] c.d.a.i.InternalUsageTest.asyncTest.34
- engine type: com.doding.aoptest.internalusage.BeanConfig$GPTEngine$$SpringCGLIB$$0
16:20:36 [DEBUG] c.d.a.i.BeanConfig.heavyWork.27 - Thread[task-1,5,main]에 의해 0번째 요청 처리 중
16:20:36 [DEBUG] c.d.a.i.BeanConfig.heavyWork.27 - Thread[task-5,5,main]에 의해 4번째 요청 처리 중
16:20:36 [DEBUG] c.d.a.i.BeanConfig.heavyWork.27 - Thread[task-3,5,main]에 의해 2번째 요청 처리 중
16:20:36 [DEBUG] c.d.a.i.BeanConfig.heavyWork.27 - Thread[task-4,5,main]에 의해 3번째 요청 처리 중
16:20:36 [DEBUG] c.d.a.i.BeanConfig.heavyWork.27 - Thread[task-6,5,main]에 의해 5번째 요청 처리 중
16:20:36 [DEBUG] c.d.a.i.BeanConfig.heavyWork.27 - Thread[task-2,5,main]에 의해 1번째 요청 처리 중
16:20:36 [DEBUG] c.d.a.i.BeanConfig.heavyWork.27 - Thread[task-7,5,main]에 의해 6번째 요청 처리 중
16:20:36 [DEBUG] c.d.a.i.BeanConfig.heavyWork.27 - Thread[task-8,5,main]에 의해 7번째 요청 처리 중
16:20:37 [DEBUG] c.d.a.i.BeanConfig.heavyWork.27 - Thread[task-1,5,main]에 의해 8번째 요청 처리 중
16:20:37 [DEBUG] c.d.a.i.BeanConfig.heavyWork.27 - Thread[task-5,5,main]에 의해 9번째 요청 처리 중
실행 결과를 살펴보면 빈의 타입이 proxy이며 Thread에 대한 코드 한줄 없이 ThreadPool이 잘 동작하고 있음을 알 수 있다.
즉 proxy 객체가 앞에 끼어들어서 무거운 작업을 thread pool에서 동작하도록 처리하는 것이다.