🤔 사용 이유
우리 회사 프로그램 로직상 서비스가 동작을 하고 프로토콜을 보내는 작업을 하게 된다.
쉽게 프로세스의 흐름에 대해서 설명을 하자면, 사용자가 자산을 구매를 하고 등록을 한다. 이후에 해당 자산을 사용자에게 할당 해주고 회수를 하는 서비스가 존재한다. 이때 자산에 대해서 할당 및 회수를 하게 되는 경우 프로토콜을 서버쪽에 보낸다.
여기서 프로토콜을 서버에 보낼때 프로토콜의 내용이 변경이 되면서 Exception 이 발생하게 되었고, 할당 및 회수도 같이 롤백이 되버리는 이슈가 발생하게 된다. 같은 트랜젝션으로 물려있기 때문에 발생한 이슈로 해당 이슈를 수정하기 위해서 Spring Event 기능을 사용하게 되었다.
프로세스마다 다를수는 있지만 메인 기능의 성공과 서브 기능의 성공은 분리가 되어야 한다.
⚡ ApplicationEventPublisher 에 대해서 정리
1. ApplicationContext (스프링 컨테이너)
ApplicationContext 는 BeanFactory 인터페이스를 상속 받는데 BeanFactory는 Spring Container의 최상위 개념이다.
이때 ApplicationContext 는 BeanFactory를 직접 상속받는 것이 아니라, Bean Factory를 상속받은 다른 상위개념을 상속받는다.
1-1. ApplicationContext 기능
ApplicationEventPublisher 에 대해서 정리를 하기 위해서 쓰고 있기 때문에 짧게 부가 기능에 대해서 설명 하도록 한다.
- EnvironmentCapable : 환경 설정 관리
- ListableBeanFactory : 스프링 빈(Bean)의 목록을 조회
- HierarchicalBeanFactory : 스프링 빈 팩토리의 계층 구조를 지원
- MessageSource : 다국어 지원과 메시지 처리
- ApplicationEventPublisher : 스프링 이벤트 기반 아키텍처를 지원
(이벤트를 발행하고 다른 컴포넌트에서 해당 이벤트를 수신할 수 있도록 도와 준다. ) - ResourcePatternResolver : 클래스패스 및 파일 시스템과 같은 리소스 패턴을 해석하고 검색
간단하게 주요 기능에 대해서만 적어 놨지만, spring의 핵심 기능이기때문에 다시 한번 찾아서 동작 방식과 커스텀해서 사용하는 방법에 대해서 추가적으로 정리를 해야겠다. :)
2. ApplicationEventPublisher
ApplicationEventPublisher 을 사용하기 전에 먼저 프로젝트의 Spring FrameWork 버전을 확인 해봐야 한다.
4.2 버전을 기점으로 사용자 편의성이 달라졌다 :)
내용을 설명 할때는 4.2 버전 이상에서 적용된 내용을 기준으로 설명하도록 하겠다.
📜 Spring Event의 동작 방식 및 내용
이벤트 클래스 정의 -> 이벤트 발행 -> 이벤트 핸들링 -> 이벤트 리스너 등록 -> 이벤트 발생
👉 Event Class
이벤트를 처리 하는데 필요한 객체이다.
public class AssetStatusEvent {
private Long assetKey;
private String targetTypeCode;
}
👉 Event Listener
발생 시키고 싶은 이벤트를 구현 한다.
@Slf4j
@Component
public class OamAssetAllocationInfoChangeEventListener {
@EventListener
// 메소드명이 afterCommit인 이유는 밑에서 설명 하도록 하겠다.
public void afterCommit(AssetStatusEvent assetStatusEvent) {
log.info("test");
}
}
👉 Event Publisher
기존 프로토콜을 보내려고 주입 받은 Service 대신 이벤트를 발행 하기 위한 코드를 넣어준다.
@Service
@Slf4j
@RequiredArgsConstructor
public class AssetProcessService {
private final ApplicationEventPublisher eventPublisher;
@Transactional
public boolaen allocationAsset(AssetInfo assetInfo) {
// 자산 할당 소스
...
// 프로토콜 전달 event
eventPublisher.publishEvent(new AssetStatusEvent(assetInfo.assetKey, assetInfo.assetType));
retrun true;
}
}
try - catch 부터 , 예외처리 등 할게 많지만 핵심만 설명 하도록 하겠다. :)
원래 기존 소스였다면 eventPublisher 대신 프로토콜을 전달 하는 Service를 주입받아서 구현된 내용이 존재 했을것이다. 이부분에 대해서 이벤트로 변경을 했다면 기존과 동일하게 동작하는지 여부를 확인 해보면 좋을거 같다.
위의 내용을 적용을 했다면 결합도를 느슨하게 만드는것에 성공했다. 이제 하나로 되어있는 트랜잭션을 분리해서 메인 기능과 서브 기능을 분리를 해야한다.
3. @TransactionalEventListener 사용 및 주의 사항
phase 옵션을 통해 이벤트의 처리를 지원해주는 기능을 하는 어노테이션 이다.
- TransactionPhase.AFTER_COMMIT
-> Commit 됐을 때 이벤트를 실행 - TransactionPhase.AFTER_ROLLBACK
-> Rollback 됐을 때 이벤트를 실행 - TransactionPhase.AFTER_COMPLETION
-> 위의 내용을 합친 기능이다. (AFTER_COMMIT 또는 AFTER_ROLLBACK 됬을때 발생한다.) - TransactionPhase.BEFORE_COMMIT
-> Commit 되기전에 이벤트를 실행
트랜잭션을 처리하는 여러가지 방법이 있지만, 내가 사용해야 하는 기능은 AFTER_COMMITM 이다.
기존 Event Listener을 구현 해놓은 부분에 트랜잭션을 추가하고 해당 기능을 추가한다.
@Slf4j
@Component
public class OamAssetAllocationInfoChangeEventListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT
public void afterCommit(AssetStatusEvent assetStatusEvent) {
log.info("test");
}
}
위의 내용을 추가 했다면 eventPublisher 을 추가한 Service에도 추가해야되는 어노테이션이 존재한다.
@Transactional에 REQUIES_NEW 옵션을 추가 해야한다.
REQUIRES_NEW : Create a new transaction, and suspend the current transaction if one exists.
@Transactional(propagation = Propagation.REQUIRES_NEW)
public boolaen allocationAsset(AssetInfo assetInfo) {
...
}
REQUIRES_NEW 옵션의 경우 Transactional의 전파 레벨 옵션중 하나 이다.
해당 어노테이션을 사용 함으로써 기존의 트랜잭션을 사용하는것이 아닌 별도의 새로운 트랜잭션을 생성해서 동작 하도록 해준다.
이 옵션을 사용함으로써 서브 기능이 실패해도 메인 기능에 까지 영향을 미치지 않도록 작업이 가능하다.
🚫 주의 사항
물론 해당 옵션만 사용한다고 해서 메인 기능에 영향이 없는것이 아니다.
동일 스레드 내에서 메인 트랜잭션과 별도의 트랜잭션이 생성되서 실행이 되고 있을때, 별도의 트랜잭션에서 Exception이 발생했을때 메인 트랜잭션이 아직 끝이난게 아니기 때문에 메인 트랜잭션 또한 롤백이 일어나게 된다.
이 문제를 해결 하기 위해서 위에서 추가한 AFTER_COMMIT 기능과 ApplicationEventPublisher을 사용한 것이다.
메인 기능에 Exception이 발생해서 롤백이 일어나면 트랜잭션 기능으로 인해서 서브 기능에 대해서 실행이 되지 않고, 메인 기능이 성공하고, 서브 기능에서 문제가 발생하더라도 이미 메인 기능은 커밋이 되었기 때문에 같이 롤백이 되지 않는다.
⚡ 생각
글을 쓰다보니 내용의 순서를 바꿔서 설명 했으면 이해가 더 쉬웠을거라고 생각한다... 😅
내부적으로 개선 작업을 하면서 발생했던 내용에 대해서 찾아보고 순차적으로 적용을 하는 과정에 대해서 설명을 하다보니 이렇게 되었다.
위의 방법 말고도 메세지 큐를 이용하는 방법도 있고, 내부적으로 소스상에서 예외처리를 하는 방법도 있다.만약 내부적으로 서브 기능이 실패 하더라도 재시도를 해야 한다거나, 추가적인 작업이 필요한 경우라면 메시지큐 도입이 더 좋은 방향성이 될수도 있다. 각각 프로젝트의 상황에 맞게 장단점을 파악하고 잘 사용하면 좋을거 같다.
'Spring' 카테고리의 다른 글
[QueryDsl] new CaseBuilder() 내용 정리 (1) | 2023.10.07 |
---|---|
[Spring] Spring boot 3.0 + JPA 관련 gradle 설정 정리 (0) | 2023.09.25 |
[JPA] Spring Boot 에서 QueryDsl 사용 방법 정리 (0) | 2023.06.24 |
[Linux] JAVA에서 SFTP 접속 관련 이슈 사항 내용 정리 (1) | 2023.06.10 |
[Spring Boot] QueryDsl Qclass 에 대한 내용 정리 (0) | 2023.04.23 |