Spring에서는 이벤트 기반 프로그래밍을 지원한다. EventPublisher를 이용해 이벤트를 발행하고 구독하고 있는 객체에서 이벤트를 처리한다. 덕분에 비즈니스 로직과 직접적으로 관련없는 로직들을 이벤트를 통해 처리할 수 있다. 객체간 낮은 결합도를 유지한 상태로 부가적인 로직을 처리할 수 있다는게 매력적이다.
'개발 한 스푼' 서비스에서는 질문 답변이나 게시글을 작성할 때마다 활동 테이블에 카운트를 증가시키고 활동점수에 따라 뱃지 등을 취득할 수 있다. 답변이나 게시글 작성 후 활동점수 집계는 문맥이 다르기 때문에 이들은 이벤트를 발행해 처리하고자 했다.
하지만 이벤트를 발행하는 로직 또한 비즈니스 로직 내에 포함된다. 때문에 한 단계 더 나아가 이벤트 발행 로직을 AOP로 분리하여 처리해보았다. 이번 글에서는 그 과정을 공유해본다.
해당 글은 다음과 같은 순서로 진행됩니다.
1. 이벤트 기반으로 처리할때의 문제점
2. 개선하기 - AOP로 이벤트 발행
2.1 Transaction 내부에서 실행시키기
2.1 적용하기
1. 이벤트 기반으로 처리할때의 문제점
일반적으로 이벤트 기반으로 처리한다면 비즈니스 로직내부에서 발행하는 로직들이 들어간다.
class BoardPostDomainService(
private val eventPublisher: ApplicationEventPublisher
) {
@Transactional
fun registerBoardPost(request: RegisterPostRequestDto, userId: Long): BoardPost {
// 비즈니스 로직
eventPublisher.publishEvent(BoardPostActivityEvent(it.user.memberId))
// return
}
}
해당 이벤트를 받아 처리하는 로직은 다음과 같이 작성한다. 트랜잭션이 커밋된 후에 실행되도록 TransactionalEventListener 를 사용한다. 이를 통해 데이터 일관성을 맞춰줄 수도 있다.(롤백되었을때도 실행되면 일관성이 깨질 수도 있음)
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional
fun handleBoardPostEvent(event: AttendanceActivityEvent) {
// 비즈니스 로직
}
이벤트를 발행하는 로직을 보면 해당 비즈니스 로직과 관련없는 로직을 잘 분리시킨 것 같아보이지만 여전히 비즈니스 로직이 EventPublisher에 의존하고 있는 모습이 보인다. 해당 로직의 자취를 아예 감출 수는 없을까?
비즈니스로직과 관련없는 부가기능을 처리할때 AOP를 활용할 수 있다. 이를 활용해서 활동 Event Publishing용 Advisor를 구현해볼 수 있다. 다음과 같은 모습으로 바꿔보자.
class BoardPostDomainService(
private val eventPublisher: ApplicationEventPublisher
) {
@ActivityEvent(ActivityEventType.BOARD_POST)
@Transactional
fun registerBoardPost(request: RegisterPostRequestDto, userId: Long): BoardPost {
// 비즈니스 로직
// return
}
}
2. 개선하기 - AOP로 이벤트 발행
우선 Event Publishing을 사용할 메서드들에 적용할 어노테이션을 만들었다. 프로퍼티로 어떤 활동인지를 명시할 수 있도록 Type도 추가해줬다.
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class ActivityEvent(
val type: ActivityEventType // 질문답변, 게시글작성 등 여러가지 활동
)
그리고 Advisor는 비즈니스 로직이 마무리 된 후에 Event를 발행할 수 있도록 다음과 같이 구성해줬다.
@Aspect
@Component
class ActivityEventAdvisor(
private val eventPublisher: ApplicationEventPublisher
) {
@AfterReturning(pointcut = "@annotation(com.adevspoon.domain.common.annotation.ActivityEvent)", returning = "result")
fun afterReturningAdvice(joinPoint: JoinPoint, result: Any?) {
val activityAnnotation = getAnnotation(joinPoint)
// 타입별 Event 발행
when(activityAnnotation.type) {
ActivityEventType.ATTENDANCE -> attendanceEventPublish(result)
ActivityEventType.ANSWER -> answerEventPublish(result)
ActivityEventType.BOARD_POST -> boardPostEventPublish(result)
}
}
}
여기서 중요한 점이 있다. 이벤트를 받아 소비하는 로직에서 보았듯이 TransactionalEventListener 를 이용해 트랜잭션 커밋 후 동작한다. 이것은 이벤트 발행 로직이 트랜잭션 내부에서 실행되어야 커밋을 인지하고 실행될 수 있다는 뜻이다.
2.1 Transaction 내부에서 실행시키기
트랜잭션 또한 AOP로 실행된다. 트랜잭션의 AOP가 이벤트 발행 AOP보다 우선순위가 높아야 이벤트 발행 로직이 트랜잭션 내부에서 동작할 수 있다.
트랜잭션의 우선순위는 기본값으로 최하로 설정되어 있다. 때문에 이 우선순위를 조금 더 높은 우선순위로 고쳐주고 위에서 만든 AOP의 우선순위를 이보다 후순위로 설정해주면 된다.
트랜잭션의 우선순위는 EnableTransactionManagement 어노테이션을 이용해서 수정할 수 있다.
// 트랜잭션 - 우선순위 높이기
@EnableTransactionManagement(order = Ordered.LOWEST_PRECEDENCE - 10)
class JpaConfig
// 우선순위 최후순위로 설정
@Order(Ordered.LOWEST_PRECEDENCE)
class ActivityEventAdvisor
2.2 적용하기
필요한 메서드에 어노테이션을 적용하면 '트랜잭션 시작 -> 비즈니스 로직 -> 이벤트 퍼블리싱 -> 트랜잭션 커밋' 의 순서로 잘 동작한다!
@Service
class BoardPostDomainService(
private val eventPublisher: ApplicationEventPublisher
) {
@ActivityEvent(ActivityEventType.BOARD_POST)
@Transactional
fun registerBoardPost(request: RegisterPostRequestDto, userId: Long): BoardPost {
// 비즈니스 로직
// return
}
}
3. 마치며
새로운 관점에서 AOP를 적용해보았다. AOP를 처음 배울때는 어디에 사용할 수 있을까 많은 생각을 하기도 했는데 직접 개발을 하다보니 필요해보이는 부분에서 유용하게 잘 사용하고 있는 것 같다.
모든 코드는 아래 레포에서 확인가능하다.
Feat/#34. 유저 활동 데이터 누적 AOP로 처리 by RokwonK · Pull Request #38 · kids-ground/adevspoon-backend
Issue close #34 Summary 유저 활동 데이터 관련 처리를 Event & AOP로 처리 Description 게시글 쓰기, 답변 작성, 추석 등 유저의 행동마다 UserActivity에 count를 올려야하는 부가 기능을 AOP로 처리했습니다. AOP
github.com
'Spring' 카테고리의 다른 글
Spring Batch, AWS Lambda로 무비용으로 운용하기 (2) | 2024.06.08 |
---|---|
MySQL 네임드락으로 분산락 구성, AOP로 유연하게 적용하기 (0) | 2024.04.24 |
예외 정보를 인터페이스와 infix함수로 확장성, 가독성 높게 관리하기 (0) | 2024.04.18 |
API별 인증 해제, 어노테이션으로 효율적으로 처리하기 (2) | 2024.04.09 |
문자열 응답 시 공통 응답형식이 적용되지 않는 문제 해결하기 (0) | 2024.04.05 |