-
[DOSI:RAK] “DOSI:RAK 서비스 개발 : Spring EventListener로 객체지향 설계 문제 해결하기”개발 ing 2024. 12. 16. 23:37
해당 글은 피우다 프로젝트 공모전 DOSI:RAK 서비스를 만들면서 했던 내용이다
이번 공모전 프로젝트를 하면서 맡은 기능중에 Green Commit 이라는 기능이 있다.
이 기능은 사용자가 Green Track 이나 Green Auth를 이용하면 사용자가 어떤 것을 했는지 기록해주는 것이다.
GitHub의 잔디(GitHub Contributions)와 유사하다.
이 기능을 개발하면서
처음에는 Track 기능에서 Create (등록) api 가 호출되고 완료가 되면 기록하는 식으로
Track service class의 track create 관련 메서드에 Track create 관련 로직과 사용자 활동 create 관련 로직을 단순하게 넣었다.
하지만 여기서 의문이 들었다.
- 이렇게 한다고 하면 Auth Create (등록) api 관련 로직에서도 똑같이 해줘야 하나?
- 그리고 Track Service class에 Commit create 관련 로직을 넣는게 객체지향적으로 보면 맞을까?
- Track create 관련 메서드가 하나의 책임을 가지고 있어야 하는데... 이렇게 한다고 하면 너무 많은 책임을 가지고 있게 된다.
- 객체지향 설계의 기본 원칙 중 하나인 단일 책임 원칙(Single Responsibility Principle) 에 따르면 하나의 클래스는 하나의 책임만을 가져야 한다
- 그리고 Track 과 Auth 각각 service 에 Commit create 관련 로직을 넣어야 하니 중복 코드도 발생한다.
그리고 또
- 다른 방법을 생각해봤을땐 Commit Service 에 Commit create 관련 메서드를 만들고 Track Service에 넣는거다.
- 하지만 이 방법을 사용했을땐 Service 계층 간에 의존성이 높아진다는 거다...
아 객체지향 재밌네
그래서! 생각한 게 이벤트 기반 아키텍처 (Event-Driven Architecture) 이다
이벤트 기반 아키텍처 (Event-Driven Architecture) 는 시스템의 모듈 간 결합도를 낮추고 유연성을 높이는 데 중요한 역할을 한다.
그럼 Spring 에서의 이벤트 기반 아키텍처는 무엇일까?
Spring Framework는 이벤트 기반 아키텍처를 손쉽게 구현할 수 있는 다양한 기능을 제공한다.
Spring Events의 구성요소
1. Event
- 시스템 내에서 발생하는 사건을 나타내는 객체이다
package com.example.dosirakbe.domain.green_commit.event; import com.example.dosirakbe.domain.activity_log.entity.ActivityType; import lombok.Getter; import org.springframework.context.ApplicationEvent; import java.math.BigDecimal; @Getter public class GreenCommitEvent extends ApplicationEvent { private final Long userId; private final Long contentId; private final ActivityType activityType; private BigDecimal distance; public GreenCommitEvent(Object source, Long userId, Long contentId, ActivityType activityType) { super(source); this.userId = userId; this.contentId = contentId; this.activityType = activityType; } public GreenCommitEvent(Object source, Long userId, Long contentId, ActivityType activityType, BigDecimal distance) { super(source); this.userId = userId; this.contentId = contentId; this.activityType = activityType; this.distance = distance; } }
2. Event Listener
- 특정 이벤트가 발생했을 때 이를 수신하고 처리하는 컴포넌트이다
@Component @RequiredArgsConstructor public class GreenCommitEventListener { private final ActivityLogService activityLogService; private final UserActivityService userActivityService; @EventListener public void handleGreenCommitEvent(GreenCommitEvent event) { activityLogService.addActivityLog(event.getUserId(), event.getContentId(), event.getActivityType(), event.getDistance()); userActivityService.createOrIncrementUserActivity(event.getUserId()); } }
3. Event Publisher- 이벤트를 생성하고 발행하는 역할을 한다
@RequiredArgsConstructor @Service @Transactional public class TrackService { private final UserRepository userRepository; private final ApplicationEventPublisher eventPublisher; private final SaleStoreRepository saleStoreRepository; private final TrackRepository trackRepository; private final TrackMapper trackMapper; public TrackMoveResponse recordTrackDistance(Long userId, TrackMoveRequest trackMoveRequest) { User user = userRepository.findById(userId) .orElseThrow( () -> new ApiException(ExceptionEnum.DATA_NOT_FOUND) ); SaleStore saleStore = saleStoreRepository.findBySaleStoreName(trackMoveRequest.getSaleStoreName()) .orElseThrow( () -> new ApiException(ExceptionEnum.DATA_NOT_FOUND) ); Track track = new Track(saleStore.getSaleStoreName(), saleStore.getSaleStoreAddress(), trackMoveRequest.getMoveDistance(), user); Track saveTrack = trackRepository.save(track); user.addTrackDistance(trackMoveRequest.getMoveDistance()); //Event Publisher eventPublisher.publishEvent(new GreenCommitEvent(this, user.getUserId(), null, ActivityType.LOW_CARBON_MEANS_OF_TRANSPORTATION, trackMoveRequest.getMoveDistance())); return new TrackMoveResponse(trackMoveRequest.getMoveDistance()); } }
이렇게 구현 했다
하지만 이렇게 한다면 또 문제가 발생한다
트랜잭션 문제이다
1.
왜냐하면 지금 구현 방식에서는 TrackService의 recordTrackDistance 메서드 내에서 트랙을 생성하고 나서 이벤트를 발행하고 있다.
이 말은 즉, 트랜잭션이 완료 되기전에 발행이 된다
이로 인해 만약 트랜잭션이 롤백되면? -> 이미 발행된 이벤트는 리스너에 의해 처리되어 데이터 불일치가 발생할 수 있다.
2.
지금 현재 동기로 처리가 되어 있는데 @Async 어노테이션을 사용하여 비동기적으로 이벤트를 처리할 수 있다.
하지만 둘다 트랜잭션이 롤백 될 경우 이벤트 처리의 일관성을 보장하기 어렵다는 것이다.
이로 인해, 해결방법은 Spring에서는 제공하는 @TransactionalEventListener를 활용할 수 있다.
이를 활용함으로써 트랜잭션이 성공적으로 커밋된 후에만 이벤트를 발행하도록 설정하면 트랜잭션이 롤백될 경우 이벤트가 발행되지 않아 데이터 일관성을 유지할 수 있다.
개선된 코드
이벤트 리스너 (GreenCommitEventListener)
@Component @RequiredArgsConstructor public class GreenCommitEventListener { private final ActivityLogService activityLogService; private final UserActivityService userActivityService; @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void handleGreenCommitEvent(GreenCommitEvent event) { activityLogService.addActivityLog(event.getUserId(), event.getContentId(), event.getActivityType(), event.getDistance()); userActivityService.createOrIncrementUserActivity(event.getUserId()); } }
위 코드와 같이 @transactionalEventListener를 사용함으로써 트랜잭션의 특정 시점에 이벤트를 처리할 수 있고 트랜잭션의 상태에 따라 이벤트 리스너의 동작을 제어 할 수 있게 된다.
이벤트 기반 아키텍처는 시스템의 모듈 간 결합도를 낮추고 유연성을 높일 수 있는 방법이다.
하지만 이벤트 리스너를 사용할 때 위 글과 같이 트랜잭션 문제도 생각해봐야한다
이번 DOSI:RAK 서비스의 Green Commit 기능 구현을 통해 이벤트 기반 아키텍처의 장점과 동시에 트랜잭션 문제를 어떻게 해결할 수 있는지에 대해 깊이 있게 이해하게 되었고
이러한 경험은 앞으로의 프로젝트에서도 유용하게 활용될 것 같다.
'개발 ing' 카테고리의 다른 글
[DOSI:RAK] 부하테스트 시작 - CPU, 메모리, 디스크 (0) 2025.02.17 [Agarang] 지금까지 내가 잘 못 알고 있었던 점 (1) - 문제제기 (0) 2025.02.08 [Backend] application.properties, application.yml 보안 관리와 협업 효율성 높이기 (0) 2024.12.10 [Solitour] 비용과 성능의 트레이드오프 (2) 2024.10.13 [Solitour] Builder 패턴을 써야할까? (0) 2024.09.21