AOP란 무엇인가?
AOP란 Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라는 의미를 가집니다. 스프링에서는 이러한 AOP 기능을 제공합니다. AOP를 간단하게 설명하자면 인터셉터와 필터와는 다르게 내가 원하는 코드에 접근할때 공통으로 처리하는 전후 처리 기능을 구현하는 것이라고 생각하면 편리합니다. @Transactional 또한 AOP를 활용하여 구현된 스프링의 어노테이션입니다. 이번 글에서는 AOP를 활용하여 어노테이션 없이 트랜잭션을 보장하는 방법을 알아보고자 합니다. AOP에 대한 개념은 쉽게 이해하기 어렵기 때문에 자세한 내용은 추후에 다루도록 하겠습니다.
@Transactional을 제외하고 만드는 이유가 무엇인가?
여기서 만들고자 하는 것은 @Transactional 어노테이션 없이 Service 단에서만 트랜잭션을 보장받는 방법으로 굳이 어노테이션을 제거하고 만드는 이유는 프레임워크와 연관이 있습니다. 기존 업무에서 사용하던 상용 프레임워크에서는 비즈니스 로직이 동작하는 서비스 단에서만 트랜잭션이 보장되는 것을 확인할 수 있었는데, 이는 프레임워크에서 트랜잭션이 서비스단에서만 적용되도록 커스텀하였기 때문에 어노테이션 없이 동작하는 것이었습니다. 어노테이션을 굳이 제거하려는 이유는 모든 개발자가 @Transactional 어노테이션을 빼놓지 않고 쓰면 상관이 없지만, 대형 프로젝트의 경우 모든 개발자가 그것을 인식해서 사용하지 않기 때문에 애초에 프레임워크에서 보장을 해주자는 취지였습니다. 그렇기 때문에 이러한 업무 경험을 토대로 이번 프로젝트에서도 또한 서비스 단에서는 @Transactional 없이 트랜잭션을 보장해줘야 한다는 요구사항이 나왔고, 제가 담당하는 업무는 아니였지만 이러한 문제를 AOP로 해결할 수 있을 것 같아 AOP 클래스 파일을 작성하여 적용 할 수 있게 되었습니다. 이러한 내용을 공유하고자 글을 작성하게 되었으며, 아래와 같은 예제를 기반으로 설명을 드리고자 합니다.
샘플 프로젝트 동작 예제 - 트랜잭션이 보장되지 않는 경우
SampleService.java 함수 코드
public void updateSampleServiceImpl(SampleInVo sampleInVo) throws Exception {
/**
* 수정 쿼리 호출
*/
int result = dbMapper.sampleUpdate(sampleInVo);
/**
* DB 접근 후 예외 발생! - 트랜잭션 보장시 롤백
*/
if(true) {
throw new CustomException("CUSTOM00001", "임의 오류");
}
}
위 코드는 Mybatis의 Mapper를 사용한 DB UPDATE 코드와 DB 접근 이후 임의의 오류를 발생시키는 코드입니다. 위처럼 동작시 트랜잭션이 보장되지 않는 경우 트랜잭션이 롤백되지 않고, 커밋을 수행합니다. 그 경우 예시는 아래 화면과 같습니다.
Sample.html
위와 같이 오류가 발생하였음에도 15 -> 20으로 수정하는 업데이트 쿼리가 적용이 된 것을 볼 수 있습니다. 서비스 코드 내에서 오류가 발생했음에도 DB에 적용이 되는 것은 실무에 있어서는 치명적인 문제로 이어지게 됩니다. 이렇듯 트랜잭션을 보장해주는 역할이 중요하기 때문에 @Transactional을 넣지 않은 것을 운영까지 가서 알게 된다면, 실무에서는 돈과 연결되는 리스크로 이어지기 때문에 이런 리스크를 제거하고자 하는 것입니다. 그렇기 때문에 다음으로 트랜잭션을 적용하기 위한 코드들을 만들어볼 것입니다.
AOP 클래스 작성
서비스 단에서 트랜잭션을 적용하기 위해 3개의 클래스를 만들것입니다.
TransactionContextHolder.java
import org.springframework.transaction.TransactionStatus;
public class TransactionContextHolder {
private static final ThreadLocal<TransactionStatus> transactionStatusHolder = new ThreadLocal<>();
public static TransactionStatus getTransactionStatus() {
return transactionStatusHolder.get();
}
public static void setTransactionStatus(TransactionStatus transactionStatus) {
transactionStatusHolder.set(transactionStatus);
}
public static void clearTransactionStatus() {
transactionStatusHolder.remove();
}
}
해당 클래스는 ThreadLocal 변수를 사용하여 TransactionStatus의 트랜잭션 상태를 유지해주는 코드입니다. 트랜잭션의 상태를 유지한다는 의미는 멀티 쓰레드 환경에서 서비스 동작시 각 쓰레드가 전역변수 등에 의한 문제 없이 A 쓰레드, B쓰레드의 일관 된 처리를 보장해준다는 의미입니다. 쓰레드 로컬을 사용하지 않는 경우, A 트랜잭션의 쓰레드, B 트랜잭션 쓰레드가 동시 발생시 A가 B의 처리 혹은 B가 A의 처리에 영향을 주는 경우가 생깁니다. 이러한 문제를 방지하는 것이 쓰레드 로컬의 역할입니다.
CustomTransactionAspect.java
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
@Aspect
@Component
public class CustomTransactionAspect {
@Autowired
private PlatformTransactionManager transactionManager;
// com.sample 하위 패키지의 ServiceImpl 명을 가지는 프로그램 실행시 동작하는 AOP 메소드
@Around("execution(* com.sample..*ServiceImpl*.*(..))")
public Object manageTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
// 전 처리 : 트랜잭션 시작
TransactionStatus transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition());
TransactionContextHolder.setTransactionStatus(transactionStatus);
try {
// 호출 할 메서드 호출
Object result = joinPoint.proceed();
// 후처리 : 트랜잭션 커밋
transactionManager.commit(transactionStatus);
return result;
} catch (Exception ex) {
// 서비스에서 예외 발생 시 트랜잭션 롤백
transactionManager.rollback(transactionStatus);
throw ex; // 예외를 다시 던져서 상위 레벨에서 처리할 수 있도록 함
} finally {
TransactionContextHolder.clearTransactionStatus();
}
}
}
위 코드에서 @Aspect는 이 클래스가 AOP 클래스인것을 명시합니다.
@Around 어노테이션은 AOP를 어떤 패키지 경로에 적용할지 지정하는 것으로 위에서는 com.sample 하위 경로의 클래스명이 ServiceImpl가 포함된 클래스에 AOP를 적용시키겠다는 의미입니다.
ProceedingJoinPoint 객체는 실제로 호출할 메소드 정보를 담고 있으며, manageTransaction 안에서 동작하는 함수는 CGLIB 혹은 JDK 다이나믹 프록시로 동작하여 기존의 호출을 인터셉터 하여 동작하는 방식으로 함수가 동작합니다. 그렇기 때문에 이 함수 내에서 전처리,후처리를 할 수 있으며 구현한 함수 내에서 전처리로 트랜잭션을 시작하고 joinPoint.proceed()를 통해 실제 호출 함수를 동작 시킵니다. 동작 후 다시 후처리로 넘어와 트랜잭션을 커밋 혹은 롤백 시키는 전후처리를 구현하였습니다.
이렇게 내가 원하는 클래스 혹은 패키지 경로에 전후 처리를 구현하는 것을 AOP(관점 지향 프로그래밍) 이라고 합니다.
마지막으로 작성할 클래스는 AOP를 사용할 것이라는 Config 클래스로 @Aspect 코드를 찾아주는 클래스입니다.
AopConfig.java
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.ibks.cwcore.common.aop") // CustomTransactionAspect가 속한 패키지 지정
public class AopConfig {
// 추가적인 설정이 필요한 경우 작성
}
위 코드는 설명하면 @ComponentScan으로 실제 동작 시킬 AOP 클래스 경로를 지정하고, @Configuration을 통해 스프링의 환경설정 파일로 등록하겠다는 의미입니다. 마지막으로 @EnableAspectJAutoProxy는 AOP 사용을 위한 프록시를 사용하겠다는 의미로 위와 같은 설정에서는 기본 값 설정으로 JDK 다이나믹 프록시를 사용하며, @EnableAspectJAutoProxy(proxyTargetClass = true)로 옵션을 줄 시 CGLIB를 사용하게 됩니다. JDK 다이나믹 프록시와 CGLIB의 개념 또한 프록시의 어려운 개념이기에 다음에 따로 정리하도록 하겠습니다. 현재로써의 가장 큰 차이점은 JDK 다이나믹 프록시는 인터페이스를 구현한 클래스에 대해서만 프록시 객체를 생성하는 의미라고 하며, CGLIB는 모든 클래스에 대해서 프록시 객체를 생성하겠다는 뜻입니다.
이렇듯 3개의 클래스를 작성하게 되면, ServiceImpl이 포함된 클래스 안의 메소드에서는 @Transactional 없이 트랜잭션이 보장 되는 것을 알 수 있습니다. 아래는 적용된 코드의 예시입니다.
샘플 프로젝트 동작 예제 - AOP 클래스 적용 후
Sample.html
AOP 적용 후 똑같은 케이스로 UPDATE 후 예외 발생시, 트랜잭션이 롤백 처리 되어 트랜잭션의 일관성이 보장되는 것을 확인할 수 있습니다.
정리
이번 글에서는 스프링에서 트랜잭션을 보장해주는 @Transactional 어노테이션 없이 함수의 트랜잭션을 보장받는 방법을 알아보았습니다. 트랜잭션의 전파 속성 등을 고려하면 아직 초안이라고 생각합니다. 하지만 이러한 방법을 잘 완성하였을때에는 코드의 반복성을 줄일 수 있고, 인적 리스크를 줄일 수 있습니다. 그렇기 때문에 다른 프레임워크에서 지원하는 기능을 비슷하게나마 만들어 현재 프로젝트에 적용하게 되었습니다. 트랜잭션 처리를 위한 전후 처리 이외에도 이러한 AOP는 클래스별 로깅, 공통 처리 등이 가능합니다. 저 또한 아직 기본적인 개념만 알기 때문에 꾸준히 공부하여 효율적인 아키텍처를 만들 수 있는 고급 개발자가 되고 싶습니다. 글에 대해서 부족한 점이나 수정해야 할 점이 있을때에는 말해주시면 수정 보완하도록 하겠습니다. 읽어주신 분들께 감사드립니다.
'Spring' 카테고리의 다른 글
[Spring]@Transactional 격리 수준(Isolation Level) 동시성에 대하여 (0) | 2023.09.06 |
---|---|
[Springframework]JavaMailSender text/html로 안보내질때 해결 방법 (0) | 2023.08.30 |
[SpringBoot] 메시지 국제화 적용하기 (0) | 2023.08.13 |