- 스프링 핵심원리 고급편 예제만들기
- 스프링 핵심원리 고급편(2) 쓰레드 로컬(threadlocal)
- 스프링 핵심원리 고급편(3) 템플릿 메서드 패턴과 콜백 패턴
- 스프링 핵심원리 고급편 프록시 패턴과 데코레이터 패턴
- 스프링 핵심원리 고급편(2) 동적 프록시 기술
- 스프링 핵심원리 고급편 스프링이 지원하는 프록시
- 스프링 핵심원리 고급편(2) 빈 후처리기
- 스프링 핵심원리 고급편(3) @aspect aop
- 스프링 핵심원리 고급편(3) 스프링 aop 구현
- 스프링 핵심원리 고급편 스프링 aop 포인트컷
- 스프링 핵심원리 고급편 스프링 aop 실전 예제
- 스프링 핵심원리 고급편(2) 스프링 aop 실무 주의사항
스프링 핵심원리 고급편(3) 스프링 aop 구현
0. 예제 프로젝트 만들기
build.gradle
...
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-aop' //직접 추가
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
//테스트에서 lombok 사용
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
}
...
implementation 'org.springframework.boot:spring-boot-starter-aop'
은 원래 스프링부트에서 제공하는 jpa 등을 사용하면 자동으로 추가되지만 이번에는 추가해줘야 한다.
OrderRepository
package hello.aop.order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
@Slf4j
@Repository
public class OrderRepository {
public String save(String itemId) {
log.info("[orderRepository] 실행");
//저장 로직
if (itemId.equals("ex")) {
throw new IllegalStateException("예외 발생!");
}
return "ok";
}
}
OrderService
package hello.aop.order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public void orderItem(String itemId) {
log.info("[orderService] 실행");
orderRepository.save(itemId);
}
}
AopTest (test)
package hello.aop;
import hello.aop.order.OrderRepository;
import hello.aop.order.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@Slf4j
@SpringBootTest
public class AopTest {
@Autowired
OrderService orderService;
@Autowired
OrderRepository orderRepository;
@Test
void aopInfo(){
//aop 를 적용하지 않았으므로 결과는 false
log.info("isAopProxy, orderService={}", AopUtils.isAopProxy(orderService));
log.info("isAopProxy, orderRepository={}", AopUtils.isAopProxy(orderRepository));
}
@Test
void success(){
orderService.orderItem("itemA");
}
@Test
void exception(){
Assertions.assertThatThrownBy(() -> orderService.orderItem("ex"))
.isInstanceOf(IllegalStateException.class);
}
}
1. 스프링 AOP 구현1 - 시작
@Aspect 를 사용해서 가장 단순한 AOP를 구현해보자.
AspectV1
package hello.aop.order.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@Slf4j
@Aspect
public class AspectV1 {
@Around("execution(* hello.aop.order..*(..))")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
- @Around 애노테이션의 값인 execution(* hello.aop.order..*(..)) 는 포인트컷이 된다.
- @Around 애노테이션의 메서드인 doLog 는 어드바이스( Advice )가 된다.
- joinPoint.getSignature() : OrderService, OrderRepository 등의 메서드 (save, orderItem 메서드)
참고
@Aspect 를 포함한 org.aspectj 패키지 관련 기능은 aspectjweaver.jar 라이브러리가 제공하는 기능이다. 앞서 build.gradle 에 spring-boot-starter-aop 를 포함했는데, 이렇게 하면 스프링의 AOP 관련 기능과 함께 aspectjweaver.jar 도 함께 사용할 수 있게 의존 관계에 포함된다.
AopTest - 추가
package hello.aop;
import hello.aop.order.OrderRepository;
import hello.aop.order.OrderService;
import hello.aop.order.aop.AspectV1;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
@Slf4j
@SpringBootTest
@Import(AspectV1.class)
public class AopTest {
@Autowired
OrderService orderService;
@Autowired
OrderRepository orderRepository;
@Test
void aopInfo(){
log.info("isAopProxy, orderService={}", AopUtils.isAopProxy(orderService));
log.info("isAopProxy, orderRepository={}", AopUtils.isAopProxy(orderRepository));
}
@Test
void success(){
orderService.orderItem("itemA");
}
@Test
void exception(){
Assertions.assertThatThrownBy(() -> orderService.orderItem("ex"))
.isInstanceOf(IllegalStateException.class);
}
}
- @Aspect 는 애스펙트라는 표식이지 컴포넌트 스캔이 되는 것은 아니다. 따라서 AspectV1 를 AOP로 사용하려면 스프링 빈으로 등록해야 한다.
- 따라서 @Import 를 통해서 추가한다. @Import 는 주로 설정 파일을 추가할 때 사용한다.( @Configuration )
실행 - success()
-
[log] void hello.aop.order.OrderService.orderItem(String) [orderService] 실행 [log] String hello.aop.order.OrderRepository.save(String) [orderRepository] 실행
2. 스프링 AOP 구현2 - 포인트컷 분리
@Around 에 포인트컷 표현식을 직접 넣을 수 도 있지만, @Pointcut 애노테이션을 사용해서 별도로 분리할 수 도 있다
AspectV2
package hello.aop.order.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Slf4j
@Aspect
public class AspectV2 {
//hello.aop.order 패키지와 하위 패커지
@Pointcut("execution(* hello.aop.order..*(..))")
public void allOrder() {} //pointcut signature
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
@Pointcut
- 메서드 이름과 파라미터를 합쳐서 포인트컷 시그니처(signature)라 한다.
- 메서드의 반환 타입은 void 여야 하고 코드 내용은 비워둔다.
- 포인트컷 시그니처는 allOrder() 이다. 이름 그대로 주문과 관련된 모든 기능을 대상으로 하는 포인트컷이다.
- private , public 같은 접근 제어자는 내부에서만 사용하면 private 을 사용해도 되지만, 다른 애스팩트에서 참고하려면 public 을 사용해야 한다.
결과적으로 AspectV1 과 같은 기능을 수행한다. 이렇게 분리하면 하나의 포인트컷 표현식을 여러 어드바이스에서 함께 사용할 수 있으며 만 다른 클래스에 있는 외부 어드바이스에서도 포인트컷을 함께 사용할 수 있다.
AopTest (test)
- @Import(“AspectV2.class”) 추가 -> 이상없이 실행된다.
3. 스프링 AOP 구현3 - 어드바이스 추가(트랜잭션)
트랜잭션을 적용하는 코드도 추가해보자. 여기서는 진짜 트랜잭션을 실행하는 것은 아니다. 기능이 동작한 것 처럼 로그만 남기겠다.
AspectV3
package hello.aop.order.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Slf4j
@Aspect
public class AspectV3 {
//hello.aop.order 패키지와 하위 패커지
@Pointcut("execution(* hello.aop.order..*(..))")
public void allOrder() {} //pointcut signature
//클래스 이름 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))")
private void service() {}
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
//hello.aop.order 패키지와 하위 패키지이면서 클래스 이름 패턴이 *Service
@Around("allOrder() && service()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
log.info("[transaction start] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
log.info("[transaction commit] {}", joinPoint.getSignature());
return result;
} catch (Exception e){
log.info("[transaction rollback] {}", joinPoint.getSignature());
throw e;
} finally {
log.info("[resource release] {}", joinPoint.getSignature());
}
}
}
- allService() 포인트컷은 타입 이름 패턴이 Service 를 대상으로 하는데 쉽게 이야기해서 XxxService 처럼 Service 로 끝나는 것을 대상으로 한다. *Servi 과 같은 패턴도 가능하다.
- 여기서 타입 이름 패턴이라고 한 이유는 클래스, 인터페이스에 모두 적용되기 때문이다.
포인트컷이 적용된 AOP 결과는 다음과 같다.
- orderService : doLog() , doTransaction() 어드바이스 적용
- orderRepository : doLog() 어드바이스 적용
실행 - success()
-
@Import(AspectV3.class) 로 수정 후 실행
-
[log] void hello.aop.order.OrderService.orderItem(String) [트랜잭션 시작] void hello.aop.order.OrderService.orderItem(String) [orderService] 실행 [log] String hello.aop.order.OrderRepository.save(String) [orderRepository] 실행 [트랜잭션 커밋] void hello.aop.order.OrderService.orderItem(String) [리소스 릴리즈] void hello.aop.order.OrderService.orderItem(String)
실행순서
클라이언트 -> [ doLog() -> doTransaction() ] -> orderService.orderItem() -> [ doLog() ] -> orderRepository.save()
4. 스프링 AOP 구현4 - 포인트컷 참조
포인트컷을 공용으로 사용하기 위해 별도의 외부 클래스에 모아두어도 된다. 참고로 외부에서 호출할 때는 포인트컷의 접근 제어자를 public 으로 열어두어야 한다.
Pointcuts
package hello.aop.order.aop;
import org.aspectj.lang.annotation.Pointcut;
public class Pointcuts {
//hello.aop.order 패키지와 하위 패커지
@Pointcut("execution(* hello.aop.order..*(..))")
public void allOrder() {} //pointcut signature
//클래스 이름 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))")
private void allService() {}
//allOrder && allService 조합
@Pointcut("allOrder() && allService()")
public void allOrderAndService() {}
}
AspectV4Pointcut
package hello.aop.order.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Slf4j
@Aspect
public class AspectV4Pointcut {
@Around("hello.aop.order.aop.Pointcuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
@Around("hello.aop.order.aop.Pointcuts.allOrderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
log.info("[transaction start] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
log.info("[transaction commit] {}", joinPoint.getSignature());
return result;
} catch (Exception e){
log.info("[transaction rollback] {}", joinPoint.getSignature());
throw e;
} finally {
log.info("[resource release] {}", joinPoint.getSignature());
}
}
}
사용하는 방법은 패키지명을 포함한 클래스 이름과 포인트컷 시그니처를 모두 지정하면 된다. 포인트컷을 여러 어드바이스에서 함께 사용할 때 이 방법을 사용하면 효과적이다.
5. 스프링 AOP 구현5 - 어드바이스 순서
어드바이스는 기본적으로 순서를 보장하지 않는다. 순서를 지정하고 싶으면 @Aspect 적용 단위로 org.springframework.core.annotation.@Order 애노테이션을 적용해야 한다.
문제는 이것을 어드바이스 단위가 아니라 클래스 단위로 적용할 수 있다는 점이다. 그래서 지금처럼 하나의 애스펙트에 여러 어드바이스가 있으면 순서를 보장 받을 수 없다. 따라서 애스펙트를 별도의 클래스로 분리해야 한다.
로그를 남기는 순서를 바꾸어서 [ doTransaction() doLog() ] 트랜잭션이 먼저 처리되고, 이후에 로그가 남도록 변경해보자.
AspectV5Order
package hello.aop.order.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
@Slf4j
public class AspectV5Order {
@Aspect
@Order(2)
public static class LogAspect {
@Around("hello.aop.order.aop.Pointcuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
@Aspect
@Order(1)
public static class TxAspect {
@Around("hello.aop.order.aop.Pointcuts.allOrderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
log.info("[transaction start] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
log.info("[transaction commit] {}", joinPoint.getSignature());
return result;
} catch (Exception e){
log.info("[transaction rollback] {}", joinPoint.getSignature());
throw e;
} finally {
log.info("[resource release] {}", joinPoint.getSignature());
}
}
}
}
하나의 애스펙트 안에 있던 어드바이스를 LogAspect , TxAspect 애스펙트로 각각 분리했다. 그리고 각 애스펙트에 @Order 애노테이션을 통해 실행 순서를 적용했다. 참고로 숫자가 작을 수록 먼저 실행된다.
AspectV5Order 클래스의 @Aspect 는 빼야 한다.
실행 (test)
-
@Import({AspectV5Order.LogAspect.class, AspectV5Order.TxAspect.class})
- 각각의 클래스를 추가해야 한다.
-
[트랜잭션 시작] void hello.aop.order.OrderService.orderItem(String) [log] void hello.aop.order.OrderService.orderItem(String) [orderService] 실행 [log] String hello.aop.order.OrderRepository.save(String) [orderRepository] 실행 [트랜잭션 커밋] void hello.aop.order.OrderService.orderItem(String) [리소스 릴리즈] void hello.aop.order.OrderService.orderItem(String)
스프링 AOP 구현6 - 어드바이스 종류
어드바이스 종류
- @Around : 메서드 호출 전후에 수행, 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택, 반환 값 변환, 예외 변환 등이 가능
- @Before : 조인 포인트 실행 이전에 실행
- @AfterReturning : 조인 포인트가 정상 완료후 실행
- @AfterThrowing : 메서드가 예외를 던지는 경우 실행
- @After : 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)
AspectV6Advice (종류 예제)
package hello.aop.order.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
@Slf4j
@Aspect
public class AspectV6Advice {
@Around("hello.aop.order.aop.Pointcuts.allOrderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
//@Before
log.info("[transaction start] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
//@AfterReturning
log.info("[transaction commit] {}", joinPoint.getSignature());
return result;
} catch (Exception e){
//@AfterThrowing
log.info("[transaction rollback] {}", joinPoint.getSignature());
throw e;
} finally {
//@After
log.info("[resource release] {}", joinPoint.getSignature());
}
}
//ProceedingJoinPoint 는 @Around 에서만 사용 가능하다.
@Before("hello.aop.order.aop.Pointcuts.allOrderAndService()")
public void doBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
@AfterReturning(value = "hello.aop.order.aop.Pointcuts.allOrderAndService()", returning = "result")
public void doReturn(JoinPoint joinPoint, Object result) {
//리턴을 조작해서 출력은 가능하지만 return 값을 변경해서 보내는 건 불가능하다.
log.info("[after-returning] {}, {}", joinPoint.getSignature(), result);
}
@AfterThrowing(value = "hello.aop.order.aop.Pointcuts.allOrderAndService()", throwing = "ex")
public void doThrow(JoinPoint joinPoint, Exception ex) {
log.info("[after-throwing] {}, {}", joinPoint.getSignature(), ex.getMessage());
}
@After("hello.aop.order.aop.Pointcuts.allOrderAndService()")
public void doAfter(JoinPoint joinPoint) {
log.info("[after] {}", joinPoint.getSignature());
}
}
복잡해 보이지만 사실 @Around 를 제외한 나머지 어드바이스들은 @Around 가 할 수 있는 일의 일부만 제공할 뿐이다. 따라서 @Around 어드바이스만 사용해도 필요한 기능을 모두 수행할 수 있다.
모든 어드바이스는 org.aspectj.lang.JoinPoint 를 첫번째 파라미터에 사용할 수 있다. (생략해도 된다.) 단 @Around 는 ProceedingJoinPoint 을 사용해야 한다. (proceed() 메서드를 사용해야 하기 때문이다.)
JoinPoint 인터페이스의 주요 기능
- getArgs() : 메서드 인수를 반환
- getThis() : 프록시 객체를 반환
- getTarget() : 대상 객체를 반환
- getSignature() : 조언되는 메서드에 대한 설명을 반환
- toString() : 조언되는 방법에 대한 유용한 설명을 인쇄
ProceedingJoinPoint 인터페이스의 주요 기능
- proceed() : 다음 어드바이스나 타켓을 호출한다.
@Before
조인 포인트 실행 전
@Before("hello.aop.order.aop.Pointcuts.allOrderAndService()")
public void doBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
@Around 와 다르게 작업 흐름을 변경할 수는 없다. @Around 는 ProceedingJoinPoint.proceed() 를 호출해야 다음 대상이 호출된다. 만약 호출하지 않으면 다음 대상이 호출되지 않는다.
반면에 @Before 는 ProceedingJoinPoint.proceed() 자체를 사용하지 않는다. 메서드 종료시 자동으로 다음 타켓이 호출된다. 물론 예외가 발생하면 다음 코드가 호출되지는 않는다.
@AfterReturning
메서드 실행이 정상적으로 반환될 때 실행
@AfterReturning(value = "hello.aop.order.aop.Pointcuts.allOrderAndService()", returning = "result")
public void doReturn(JoinPoint joinPoint, Object result) {
//리턴을 조작해서 출력은 가능하지만 return 값을 변경해서 보내는 건 불가능하다.
log.info("[after-returning] {}, {}", joinPoint.getSignature(), result);
}
- returning 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 한다.
- returning 절에 지정된 타입의 값을 반환하는 메서드만 대상으로 실행한다. (부모 타입을 지정하면 모든 자식 타입은 인정된다.)
- 즉, 현재 Object result 로 받고 있는데, 만약에 String 으로 바꾼다면?
- 메서드의 반환값이 String 이어야만 @AfterReturning 이 실행된다.
- @Around 와 다르게 반환되는 객체를 변경할 수는 없다. 반환 객체를 변경하려면 @Around 를 사용해야 한다. 참고로 반환 객체를 조작할 수 는 있다.
@AfterThrowing
메서드 실행이 예외를 던져서 종료될 때 실행
@AfterThrowing(value = "hello.aop.order.aop.Pointcuts.allOrderAndService()", throwing = "ex")
public void doThrow(JoinPoint joinPoint, Exception ex) {
log.info("[after-throwing] {}, {}", joinPoint.getSignature(), ex.getMessage());
}
throwing 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 한다. throwing 절에 지정된 타입과 맞는 예외를 대상으로 실행한다. (부모 타입을 지정하면 모든 자식 타입은 인정된다.) (@AfterReturning 과 같음)
@After
- 메서드 실행이 종료되면 실행된다. (finally를 생각하면 된다.)
- 정상 및 예외 반환 조건을 모두 처리한다.
- 일반적으로 리소스를 해제하는 데 사용한다.
@Around
- 메서드의 실행의 주변에서 실행된다. 메서드 실행 전후에 작업을 수행한다.
- 조인 포인트 실행 여부 선택
joinPoint.proceed() 호출 여부 선택
- 전달 값 변환:
joinPoint.proceed(args[])
- 반환 값 변환
- 예외 변환
- 트랜잭션 처럼 try ~ catch~ finally 모두 들어가는 구문 처리 가능
- 조인 포인트 실행 여부 선택
- 어드바이스의 첫 번째 파라미터는 ProceedingJoinPoint 를 사용해야 한다.
- proceed() 를 통해 대상을 실행한다.
- proceed() 를 여러번 실행할 수도 있음(재시도)
실행 (test)
-
@Import(AspectV6Advice.class) 추가
-
[around][트랜잭션 시작] void hello.aop.order.OrderService.orderItem(String) [before] void hello.aop.order.OrderService.orderItem(String) [orderService] 실행 [orderRepository] 실행 [after-returning] void hello.aop.order.OrderService.orderItem(String), null [after] void hello.aop.order.OrderService.orderItem(String) [around][트랜잭션 커밋] void hello.aop.order.OrderService.orderItem(String) [around][리소스 릴리즈] void hello.aop.order.OrderService.orderItem(String)
실행 순서
- 실행 순서: @Around, @Before, @After, @AfterReturning, @AfterThrowing
- 물론 @Aspect 안에 동일한 종류의 어드바이스가 2개 있으면 순서가 보장되지 않는다. 이 경우 앞서 배운 것 처럼 @Aspect 를 분리하고 @Order 를 적용하자.
@Around 외에 다른 어드바이스가 존재하는 이유
@Around 가 가장 넓은 기능을 제공하는 것은 맞지만, 실수할 가능성이 있다.
반면에 @Before, @After 같은 어드바이스는 기능은 적지만 실수할 가능성이 낮고, 코드도 단순하다.
그리고 가장 중요한 점이 있는데, 바로 이 코드를 작성한 의도가 명확하게 드러난다는 점이다. @Before 라는 애노테이션을 보는 순간 아~ 이 코드는 타켓 실행 전에 한정해서 어떤 일을 하는 코드구나 라는 것이 드러난다.
좋은 설계는 제약이 있는 것이다. @Around 만 있으면 되는데 왜? 이렇게 제약을 두는가? 제약은 실수를 미연에 방지한다. 일종의 가이드 역할을 한다. 만약 @Around 를 사용했는데, 중간에 다른 개발자가 해당 코드를 수정해서 호출하지 않았다면? 큰 장애가 발생했을 것이다. 처음부터 @Before 를 사용했다면 이런 문제 자체가 발생하지 않는다. 제약 덕분에 역할이 명확해진다. 다른 개발자도 이 코드를 보고 고민해야 하는 범위가 줄어들고 코드의 의도도 파악하기 쉽다.
댓글남기기