본문 바로가기

프로젝트

간단한 Spring AOP 적용기

이전 글에서 Table의 칼럼에 Unique Constraint 조건을 추가함으로써 동시성 이슈를 해결할 수 있었다. 이러한 동시성 이슈는 청원에 동의를 할 때뿐만 아니라, 다른 도메인에서도 마찬가지로 필요성이 생겼고 이는 코드의 중복으로 이어졌다. Unique조건 위배시 발생하는 예외 처리하는 로직을 하나의 관심사로 분리하는 작업을 진행했고 이 적용 과정을 설명하고자 한다.

 

기존 코드

기존 코드의 구성은 아래와 같다.  user_id와 petition_id에 Unique조건을 걸어두었기 때문에 동시에 동일한 user_id와 petition_id의 Agreement를 생성하게 되면, DataIntegrityViolationException이 발생하게 된다. 이를 그 의미가 명확하게 드러날 수 있도록 에러를 전환해주고 싶었고, 그렇기 위해 마지막에 try-catch문을 추가하게 되었다. 다른 도메인에도 동일한 구성으로 코드의 중복이 발생했고, 이를 Spring AOP를 활용하여 관점을 분리할 수 있지 않을까 생각했다.

@Transactional
public void agree(AgreementRequest request, Long petitionId, Long userId) {
    Petition petition = findPetitionById(petitionId);
    User user = findUserById(userId);
    Agreement agreement = new Agreement(request.getDescription(), user.getId());
    agreement.setPetition(petition, LocalDateTime.now());
    try {
        agreementRepository.save(agreement);
    } catch (DataIntegrityViolationException e) {
        throw new DuplicatedAgreementException();
    }
}
@Entity
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "petition_id"}))
public class Agreement extends UnmodifiableEntity {

    @Lob
    private String description;
    @Column(name = "user_id")
    private Long userId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "petition_id")
    private Petition petition;
}

AOP 적용하기

SpringBoot에서 제공하는 `spring-boot-starter-aop`를 활용하여 원하는 기능을 쉽게 구현해 낼 수 있다. 

 

1. 기본 설정

implementation 'org.springframework.boot:spring-boot-starter-aop'

spring-boot-starter-aop의 의존성만 추가하면 모든 준비가 끝이다. starter-aop에서 제공하는 AopAutoConfiguration에서 @EnableAspectJAutoProxy설정을 진행해주기 때문에, 아래의 Configuration 설정을 하지 않아도 된다.

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}

2. @Aspect 정의하기

분리하고 싶은 관점에 대해 처리할 Bean을 정의하고, @Aspect 어노테이션을 붙여주면 끝이다. 우리 프로젝트에 경우 어노테이션을 붙인 메소드만 관점을 분리할 것이고, Exception을 입력받고 싶기 때문에 아래와 같이 정의했다.

@Aspect
@Component
public class DataIntegrityAspect {
    @Around("@annotation(dataIntegrityHandler)")
    public Object handleDataIntegrityException(ProceedingJoinPoint joinPoint, DataIntegrityHandler dataIntegrityHandler) throws Throwable {
        try {
            return joinPoint.proceed();
        } catch (DataIntegrityViolationException e) {
            Class<? extends Exception> exceptionClazz = dataIntegrityHandler.value();
            throw exceptionClazz.getDeclaredConstructor().newInstance();
        }
    }
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataIntegrityHandler {
    Class<? extends Exception> value();
}

추가) After Throwing Advice를 활용하여 구현

글을 작성하다 발견한 기능인데, Exception을 관리하는 AOP기능이 별도로 구현되어 있어 이 방식으로도 구현할 수 있었다.

@AfterThrowing(pointcut = "@annotation(dataIntegrityHandler)", throwing = "ex")
public Object handleDataIntegrityException(DataIntegrityViolationException ex, DataIntegrityHandler dataIntegrityHandler) throws Throwable {
    Class<? extends Exception> exceptionClazz = dataIntegrityHandler.value();
    throw exceptionClazz.getDeclaredConstructor().newInstance();
}

기존 코드 수정

기존의 코드에서 try-catch문이 삭제되고 @DataIntegrityHandler 어노테이션을 통해서 보다 비지니스 로직에 집중한 코드를 작성할 수 있게 되었다.

@Transactional
@DataIntegrityHandler(DuplicatedAgreementException.class)
public void agree(AgreementRequest request, Long petitionId, Long userId) {
    Petition petition = findPetitionById(petitionId);
    User user = findUserById(userId);
    Agreement agreement = new Agreement(request.getDescription(), user.getId());
    agreement.setPetition(petition, LocalDateTime.now());
    agreementRepository.save(agreement);
}

번외) Advise Ordering

이를 구현할 때 고민했던 부분은 @Transactional 어노테이션을 함께 사용하는 데 있어서 적용되는 순서였다. @Transactional이나 별도로 Aspect를 적용한 경우 Service의 프록시(AutoConfiguration의 기본은 CGLIB)에 아래 해당하는 Advisor가 등록된다. 때에 따라 advisor의 적용 순서를 커스터마이징 해야 할 것이라 생각했다.

Spring 공식 문서에서는 Aspect를 정의한 클래스에 Ordered 인터페이스를 구현하거나 @Order 어노테이션을 이용하는 방법으로 진행한다고 제시하고 있다. 우선 순위가 높은 advise가 더 바깥쪽에 위치하게 된다.(먼저 begin -> 나중에 end) @Transactional 같은 경우에는 기본적으로는 Ordered.LOWEST_PRECEDENCE로 적용되어 있기 때문에, 가장 안쪽 advise로 위치하게 된다. 이 또한 @EnableTransactionManagement(order={Number})을 통해서 수정할 수 있다. 우리 프로젝트에 경우에는 이 설정 자체는 큰 상관이 없음으로 별도의 Order 설정을 진행해주지는 않았다.

 

정리

AOP 또한 SpringBoot에서 starter로 제공해주기 때문에 빠르게 적용해볼 수 있었다. 이러한 방식으로 풀어내는 것이 Best Practice인지는 모르겠지만, 기존의 코드에서 공통적인 관심사를 구분하여 코드의 복잡도를 줄일 수 있는 것만으로도 그 의미가 있다고 생각한다.