[스프링] Spring disable Aop in test
오랜만에... 글을 씁니다.! 디프만 12기 끝나고 ( 13기 운영진도 할 예정입니다. ㅎㅎ )
고스락 티켓예매 세번째 프로젝트로 두둥이라는 프로젝트를 시작하게 되었다.
기존엔 고스락만을 위한 예매 페이지였다면, 이제는 누구나 공연을 만들고 티켓을 팔 수 있는 그런 플랫폼형태의 프로젝트이다.
이 글의 내용은 아래 PR 내용과 관련이 있습니다.
목차
1. 프로젝트 구성 관련
2. 문제점
3. @ConditionalOnExpression 으로 Aop 적용 제어
4. @TestPropertySource 로 환경 변수 변경
1. 프로젝트 구성 관련
결제 기능이 들어가기 때문에 동시성이슈를 해결하기 위해서 락을 잡아야 하는데 ,
분산락도 되고 , Aop로 편하게 락을 잡을 수 있는 Redisson 기반 분산락을 구상했다.
또한 각 도메인간의 의존성을 낮추기 위해서 도메인 이벤트를 적극 활용중이다. 도메인 이벤트 구현방식은 도메인 엔티티 내부에 상속을 받아야만하는 AbstractAggregateRoot 방식이 아닌, Events 유틸 클래스와 , Aop 를 활용한 도메인 이벤트의 발행을 수행하고있다.
/** 관리자가 주문을 취소 시킵니다 */
public void cancel() {
orderStatus.validCanCancel(); // 동시성 이슈가 발생할 수 있는 부분
validCanRefundDate();
this.orderStatus = OrderStatus.CANCELED;
Events.raise(WithDrawOrderEvent.from(this)); // 도메인 이벤트의 발행
}
주문 도메인의 주문 취소 관련 메서드
위처럼 Order 도메인 내부에 별다른 의존성 없이 도메인 이벤트를 발행하게 되면, Transaction 어노테이션이 달린 메소드를 관점으로 가지고있는 Aop가 동작하면서 이벤트를 발행시키는 방식으로 구성했다.
2. 문제점
@RedissonLock(LockName = "주문", identifier = "orderUuid")
public String cancelOrder(String orderUuid, Long userId) {
Order order = orderAdaptor.findByOrderUuid(orderUuid);
order.validOwner(userId);
order.cancel(); // 주문 취소 이벤트가 발생...
return orderUuid;
}
도메인 모듈내 도메인 서비스의 주문취소관련 메서드 with 분산락
위와같이 주문 취소 부분의 중복 취소가 될수 있는 부분을 락으로 감싸, 중복 취소를 없앨 수 있는 코드를 작성하고있는데,
WithDrawOrderEvent 또한 발행되면서 해당 이벤트를 리스닝하고 있는 부분도 실행 되어 버린 것이였다.
@Component
@RequiredArgsConstructor
@Slf4j
public class WithDrawOrderHandler {
@Async
@TransactionalEventListener(
classes = WithDrawOrderEvent.class,
phase = TransactionPhase.AFTER_COMMIT)
@Transactional
public void handleWithDrawOrderEvent(WithDrawOrderEvent withDrawOrderEvent) {
// 분산락 관련 코드만 테스트하길 원하는데 도메인이벤트 리스닝하는 코드까지 수행
도메인 모듈내 WithDrawOrderEvent 이벤트 핸들러
테스트를 원하는 부분만 테스트해야하는데 다른 부분까지 실행이 되므로 본인이 원할 때만 도메인 이벤트의 발행을 억제 시킬수 있는 방법을 찾아보았다.
3. @ConditionalOnExpression 으로 Aop 적용 제어
위에서 나온 내용을 바탕으로 도메인 이벤트 관련 소스를 수정했다.
@Aspect
@Component
@ConditionalOnExpression("${ableDomainEvent:true}") // 추가된 어노테이션
public class EventPublisherAspect implements ApplicationEventPublisherAware {
추가된 CoditionalOnExpression 의 value 부분은 : 이전은 환경변수 이름이고 , : 다음은 디폴트 값을 넣을 수 있다.
${환경변수:디폴트값}
위 어노테이션을 활용해서 aop 자체를 전체적으로 적용시킬지 안 할지 지정할 수 있다.
처음엔 컴포넌트 스캔이나, around 쪽에서 따로 설정을 해줘야 하나 고민했었지만,
지금 목적은 테스트 코드 수행중에서 해당 Aop 를 아예 끄고, 킬 수 있는 기능을 원하므로 CoditionalOnExpression 어노테이션이 적절하다고 보았다.
실제 돌아가는 코드에서는 따로 환경변수를 세팅하고 싶지 않았기 때문에 디폴트 값을 냅뒀고,
위코드를 이용해서 테스트 환경에서의 환경변수를 세팅하면 된다.
4. @TestPropertySource 로 환경 변수 변경
테스트 패키지 내부에 resource 안에 yml로 테스트 만의 환경변수를 지정할 수도 있긴하지만,
본인의 프로젝트의 구조가 멀티모듈 구조로 잡혀있기 때문에, 여러 모듈이 필요한 통합테스인 경우 , 프로파일을 다른 모듈의 이름으로 지정해줘야 해당하는 모듈의 환경변수를 불러올수 있고, 컴포넌트 스캔 범위 또한 지정을 해줘야한다.
/** 도메인 모듈의 통합테스트의 편의성을 위해서 만든 어노테이션 -이찬진 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@SpringBootTest(classes = DomainIntegrateTestConfig.class)
@ActiveProfiles(resolver = DomainIntegrateProfileResolver.class)
@Documented
public @interface DomainIntegrateSpringBootTest {}
따라서 도메인 모듈의 통합테스트는 편의를 위해서 위와같은 커스텀 어노테이션을 만들어서 수행하고있다.
이럴때 테스트 패키지 내부에 resource 안에 yml로 테스트 만의 환경변수를 지정하는 방법을 사용하게 되면,
지정된 프로파일의 정보가 이미 common,domain,infra 인데 추가적으로 application-test.yml 같은 파일명과함께
프로파일에 test를 추가해야하는 경우가 발생한다.
이렇게 되면 분산락을 테스트하면서 도메인 이벤트 발행이 필요한 경우와, 도메인 이벤트 발행이 필요하지 않는 경우 두 경우를 모두 대응하기 쉽지않은 상황이 발생하므로 어노테이션으로 환경변수를 지정해줄수 있는 방법을 찾아보았다.
@TestPropertySource(properties = {"ableDomainEvent=false"})
위와 같은 어노테이션을 사용하면 되는데, "환경변수=값" 으로 properties 파일 형식이다.
환경변수를 덮어쓰거나, 지정이 안되어있어도 어노테이션으로 컴파일 타임에 프로퍼티를 집어넣을 수 있다.
/** 도메인 이벤트의 발행을 중지 시킬 수 있습니다. -이찬진 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@TestPropertySource(properties = {"ableDomainEvent=false"})
@Documented
public @interface DisableDomainEvent {}
필자는 좀더 명확한 의미를 어노테이션에 부여하기위에 다음과 같은 커스텀 어노테이션을 만들어 사용하고 있다.
실 사용 모습을 보면 아래와 같다.
@DomainIntegrateSpringBootTest
@DisableDomainEvent
@Slf4j
class OrderApproveServiceConcurrencyTest {
마치며
Aop 자체를 테스트 패키지내에서 끄고 킬 수 있는 방법을 알게되었더니,
@DomainIntegrateSpringBootTest
@DisableDomainEvent
@DisableRedissonLock // 분산락을 꺼놓고 실패 테스트 진행
@Slf4j
class OrderApproveServiceConcurrencyFailTest {
위와같은 방식으로 분산락을 끄고 동시성 관련 테스트를 진행해서,
실패하는 테스트를 만들 수 있었다.
원래였다면 @RedissonLock 어노테이션을 주석쳐가며 동시성 이슈가 해소가 되었는지 판단했어야했지만,
좀 더 스마트한 접근 방법이 생겨 난것 같다.