안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.
오늘은, 영한님 스프링 핵심 원리 - 고급편의 강의를 듣고, 스프링 제공 빈 후처리기를 추가로 실습하고 정리한 내용을
작성하고자 합니다.
빈 후처리기 적용에 앞서, 빈 후처리기와 같은 기능을 사용하는 이유를 정리해보고자 합니다.
애플리케이션 실행 시 의도대로 사용되어야 하는 기능은 비즈니스 로직입니다. 하지만, 기획 의도나 개발 운영에 있어 부가적인 기능을 추가로 실행시켜야 하는 상황이 생길 수 있습니다. 이를 부가 기능이라고 하는데, 만약 프록시 기술 적용 없이, 부가 기능을 적용하려고 한다면 기존 애플리케이션에 존재하는 비즈니스 로직에 추가 코드를 삽입해야 하는 상황이 생길 수 있습니다.
만약, 단순하게 로그를 찍는 상황이 아니라, 해당 클라이언트의 요청에 따라, controller -> service -> repository 등의 순서로 이동하는 단계별 로그 추적기를 적용해야하는 상황이라면, 파라미터로 로그의 단계를 남겨야 하므로, 코드를 전부 수정해야 하는 상황이 생길 수 있습니다. 이럴 경우, 프록시를 삽입하며 target 인스턴스의 기능은 정상적으로 실행시키고 중간에서 부가적으로 실행할 기능을 적용하는 것이 프록시 기능의 의도라고 할 수 있습니다.
@Slf4j
public class OrderService {
public void orderItem(Item item) {
item.minusQuantity(); // 주요 기능 (주문하면 수량을 제거하는 로직)
log.info("주문을 수행 합니다."); //부가 기능 (로그를 남기는 기능)
}
@Getter
@NoArgsConstructor
@AllArgsConstructor
private class Item {
private String name;
private int quantity;
public void minusQuantity() {
if (checkQuantity()) {
if (quantity > 0) {
quantity--;
}
}
}
private boolean checkQuantity () {
if (quantity < 0) {
throw new IllegalStateException("error");
}
return true;
}
}
}
1. 빈 후처리기
스프링은 스프링 빈으로 등록된 객체를 생성하고 컨테이너에 등록합니다. 빈 후처리기는 객체를 빈으로 등록하기 전에 조작하는 객체입니다. 빈 후처리기는 다른 빈보다 먼저 컨테이너에 등록되며, 다른 빈들이 등록될 때 후 처리기를 수행합니다.
- 생성 : 스프링 빈 대상이 되는 객체를 생성
- 전달 : 생성된 객체를 빈 저장소에 등록하기 직전, 빈 후처리기로 전달
- 후처리 작업 : 전달된 스프링 빈 객체를 조작한다.
- 등록 : 후처리기는 객체를 조작 후 조작된 객체를 반환하는데, 이 반환된 객체가 빈으로 등록, 스프링 컨테이너로 등록된다.
스프링에서 제공하는 빈 후처리기는 aop 라이브러리를 추가하여 사용할 수 있습니다.
implementation 'org.springframework.boot:spring-boot-starter-aop'
2. AnnotationAwareAspectAutoProxyCreator
스프링이 제공하는 빈 후처리기로 스프링 빈으로 등록된 Advisor를 찾아서 자동으로 프록시 적용이 필요한 곳에 프록시를 적용하여 스프링 빈으로 등록합니다.
Pointcut / Advice / Advisor??
Pointcut | 필터링할 위치를 지정하는 로직으로, 어디에 부가 기능을 적용할지 초점을 둡니다. |
Advice | 프록시가 적용할 부가기능 로직이라고 할 수 있습니다. |
Advisor | 하나의 pointcut과 하나의 advice를 가지며, pointcut과 advice를 가지고 빈에 등록되어 빈 후처리기 적용이 됩니다. |
3. 실습 코드 작성하기
(코드는 영한님 강의를 바탕으로 구성되었지만, 저작권이 있으므로 따로 수정하여 작성하였습니다. 자세한 내용은 영한님 스프링 핵심원리 - 고급편을 수강하시면 확인하실 수 있습니다.!)
빈 후처리기를 사용하여 프록시를 적용하면, 비즈니스 로직을 구현한 객체와 부가 기능을 구현한 객체를 분리하여 코드를 작성할 수 있습니다. 하단에는 repository, service, controller가 작성되어 있는데 각 메소드에는 핵심 로직만 구성되어 있습니다.
@Repository
public class AutoProxyRepository {
public void join(String name) {
try{
if (name.equals("error")) {
throw new IllegalArgumentException("error");
}
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Service
@RequiredArgsConstructor
public class AutoProxyService {
private final AutoProxyRepository autoProxyRepository;
public void joinTeam(String name) {
autoProxyRepository.join(name);
}
}
@RestController
@RequestMapping("/api/v1/join")
@RequiredArgsConstructor
public class AutoProxyController {
private final AutoProxyService autoProxyService;
@GetMapping
public ResponseEntity joinTeam(@RequestParam("name") String name) {
autoProxyService.joinTeam(name);
return new ResponseEntity<>(HttpStatus.OK);
}
}
@Slf4j
@Component
public class LogTraceV1 {
public void doLog(String message) {
log.info("method = {}", message);
}
}
비즈니스 로직이 아닌 부가기능을 수행하는 부분은 LogTraceV1입니다. 만약 클라이언트 요청에 따라, 수행되는 일련의 과정에 모두 로그를 남기고 싶을 때, 해당 객체를 의존성 주입받아서 로직을 변경하기보다 빈 후처리기를 활용하여 프록시 패턴으로 적용할 수 있습니다.
@RequiredArgsConstructor
public class LogTraceV1Advice implements MethodInterceptor {
private final LogTraceV1 logTraceV1;
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
try {
Method method = invocation.getMethod();
String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()";
logTraceV1.doLog(message);
Object result = invocation.proceed();
return result;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
LogTraceV1을 Advice로 구현하는 역할을 수행하는 클래스가 LogTraceAdvice입니다. 주의할 사항은 MethodInterceptor는 org.aopalliance.interceptor.MethodInterceptor를 import 해야 합니다.
MethodInterceptor는 부가 기능을 제공하는 역할을 수행할 수 있도록 하는 콜백 오브젝트입니다. proceed() 메소드를 실행하면 타겟 오브젝트의 메소드를 내부적으로 실행해주는 기능이 있으며, 해당 클래스를 구현하면 일종의 공유 가능한 템플릿처럼 사용할 수 있습니다.
@Configuration
public class AutoProxyConfigV1 {
@Bean
public Advisor advisorV2(LogTraceV1 logTraceV1) {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression(
"execution(* hello.advanced.autoproxy..*(..)) " +
"&& !execution(* hello.advanced.autoproxy..join(..))"
); //autoproxy 하위 모든 패키지, 파라미터 모두 적용
LogTraceV1Advice advice = new LogTraceV1Advice(logTraceV1);
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
앞서 생성한 Advice와 Pointcut을 파라미터로 받아, Advisor를 생성하는 역할을 수행하는 Configuration이 AutoProxyConfigV1입니다.
Pointcut을 생성하는 방법은 여러 가지가 있는데, NameMatchMethodPointcut과 AspectJExpressionPointcut 등이 있습니다. NameMatchMethodPointcut은 클래스와 메소드의 이름 패턴을 비교하여, AspectJExpressionPointcut은 AspectJ의 포인트컷 표현식을 활용하여 Pointcut에 적용되는 메소드를 판별할 수 있습니다.
이상으로 빈 후처리기에 관한 글을 마치도록 하겠습니다.
부족하지만 읽어주셔서 감사합니다.!
'SpringBoot' 카테고리의 다른 글
[SpringBoot] AOP 적용하기(1) (0) | 2023.01.09 |
---|---|
[SpringBoot] NativeSQL / JPQL CASE WHEN 로직 구현(1) (0) | 2023.01.06 |
[SpringBoot] 인터페이스 의존성 주입을 활용한 테스트 코드 작성하기 (0) | 2023.01.03 |
[SpringBoot] 인터셉터(Interceptor) (0) | 2022.12.29 |
[SpringBoot] REST 어노테이션 정리 (0) | 2022.12.27 |