ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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 기능 구현을 통해 이벤트 기반 아키텍처의 장점과 동시에 트랜잭션 문제를 어떻게 해결할 수 있는지에 대해 깊이 있게 이해하게 되었고

    이러한 경험은 앞으로의 프로젝트에서도 유용하게 활용될 것 같다.

Designed by Tistory.