본문 바로가기
개발 이론/Spring

[Spring] 스프링 이벤트?

by dal_been 2023. 11. 18.
728x90

곧 부트캠프에서 개인프로젝트를 본격적으로 코딩하는데...멘토님께서 이런이런 기능쓰면 좋을것같다! 라는 멘트를 해주시는데 다 하나도 모르고 써본적 없는 아이들...하... 그냥 들어보기만 했지 한번도 적용해본적 없어서...
그래서 요즘은 그런 기술들 맛보기로 블로그를 탐방하거나 다른 분들 깃허브 코드 보고있는데 @EventListner와 같은 어노테이션을 보았는데...이게 무엇인고...에서 시작한 탐방

 

 

일단 왜 스프링 이벤트를 사용할까??를 생각해보자. 그럼 자연스럽게 스프링 이벤트가 뭔지 알게될것이다.

 

왜 스프링 이벤트를 사용해??

 

예약 서비스를 생각해보자

예약를 진행하면 예약이 완료되었다는 메세지나 알림이 온다.

이과정에서 나는 보통 reservesevice 클래스 안에서 메세지와 관련된 alarmservice의존성을 추가해서 진행하였다.

 

근데 여기서 문제점이 있다

 

1. 강한 결합과 의존성 문제이다

-> 예약 서비스 안에 지금 메세지 전송이라는 기능또한 추가된것이다. 이렇게 되면 서로 강하게 결합되어있기때문에 유지보수가 어렵고 코드의 구조가 복잡해진다

 

2. 트랜젝션

-> 만약 메세지 전송을 하다가 예외가 발생하면 예약서비스 이력까지 롤백하게 된다. 이는 좋은 방법이 아니다

 

3. 성능

-> 만약 예약서비스만으로는 2분으로 처리가 완료될 것을 메세지 전송 기능을 추가함으로써 추가적으로 완료될때까지의 2+a 의 시간이 거리게 된다

 

그래서 스프링 이벤트를 사용하는 것이다. 

 

간단 예제

 

@Service
@RequiredArgsConstructor
public class ReservationService{
	
    private final MessageService messageService;
    
    
}

 

스프링 이벤트를 적용하기 전에는 이렇게 강한 의존성, 결합으로 되어있다. 

 

다만 스프링 이벤트로 분리하게 되면 비동기 방식 처리를 합치면 전체적인 프로세스 시간도 짧아질수 있다

 

 

이벤트를 처리하는 데이터를 포함하는 클래스 만들기(이벤트는 상태가 바뀐후 발생하기 때문에 클래스 이름은 과거시제가 적당함)

public class ReservedEvent{
	private String name;
    
    public ReservedEvent(String name){
    	this.name=name;
    }
    
    public String getName(){
    	return name;
    }
}

 

 

서비스 만들기

-> 이벤트 보내기 기능을 위해 ApplicationEventPublisher를 주입

-> 예약 서비스 처리후 publishEvent()를 통해서 이벤트를 주입

@Service
@RequiredArgsConstructor
public class ReservationService{
	
    private final ApplicationEventPublisher publisher;
    private final ReserveRepository repository;
    
    public void reserve(String name){
    	repository.save(new Reseve(name));
        publisher.publishEvent(new ReseredEvent(name));
    }
    
    
}

 

 

이벤트 핸들러 만들기

->@EventListener어노테이션을 통해 이벤트를 캐치하여(수신하여) 처리가능

@Component
@RequiredArgsConstructor
public class MessageEventHandler{
	
    private final MessageService service;
    
    @EventListner
    public void sendPush(ReservedEvent event){
    	service.send(event);
        
    }
}

 

 

여기까지 이벤트 를 간단하게 만들어봤다. 그럼 비동기 처리는 어떻게 하면 될까??

 

 

@EnableAsync
@SpringBootApplication
public class EventApplication{
	
    public static void main(String[] args){
    	SpringApplication.run(EventApplication.class,args);
    }
}

 

일단 비동기 처리를 위해 Application에 @EnableAsync를 추가해줘야한다(config파일로 해도 상관없음)

 

@Component
@RequiredArgsConstructor
public class MessageEventHandler{
	
    private final MessageService service;
    
    @Async
    @EventListner
    public void sendPush(ReservedEvent event){
    	service.send(event);
        
    }
}

 

 

 

@TransactionalEventListener

 

근데 만약 예약 서비스와 메세지 서비스가 정상적으로 동작한뒤 갑자기 exception이 발생한다면 어떻게 될까??

예약서비스경우 트랜젝션에 의해 롤백이 되겠지만 메세지 부분은 롤백이 되지 않는다

 

이때 사용되는 게 @TransactionalEventListener 이다

 

@Component
@RequiredArgsConstructor
public class MessageEventHandler{
	
    private final MessageService service;
    
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void sendPush(ReservedEvent event){
    	service.send(event);
        
    }
}

 

 

1. @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)

  • default 값이며, 트랜잭션이 commit 되었을 때 이벤트를 실행합니다.

2. @TransactionalEventListener(phase = TransactionPhase.ROLLBACK)

  • 트랜잭션이 rollback 되었을 때 이벤트를 실행합니다.

3. @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)

  • 트랜잭션이 completion(commit 또는 rollback) 되었을 때 이벤트 실행합니다.

4. @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)

  • 트랜잭션이 commit 되기 전에 이벤트를 실행합니다.

 

 

근데 여기서 또 생각할 점이 있다.

일단 메세지가 보내고 관련 메세지를 db에 추가하기 마련이다

 

@Component
@RequiredArgsConstructor
public class MessageEventHandler{
	
    private final MessageService service;
    private final MessageRespository repository;
    
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void sendPush(ReservedEvent event){
    	service.send(event);
        repository.save(new Message(event.getName());
    }
}

 

이때 정상적으로 진행되었음에도 db에는 meessage데이터가 생성되지 않는다. 

이는 @TransactionalEventListener가 event publisher의 트랜젝션 안에서 동작하며, 커밋된 이후 추가 커밋을 허용하지 않기 때문이다.

따라서 추가적인 insert,update,delete가 필요하다면 @Transactional(propagation=Propagation.REQUIRES_NEW)를 추가로 붙여주면 된다.

 

또 다른 방법에는 @Async르 통해 이벤트 리스너를 별도의 스레드에서 실행하는 방법니다. 그럼 이벤트 리스너 로직이 별도의 스레드에서 실행되어 트랜젝션이 커밋도기 때문에 의도한 결과를 얻을 수 있다.
또 다른 방법은 After commit -> before commit으로 하는 방법니다. 다만 이방법은 이벤트 리스너 로직에서 예외가 발생하면 핵심 로직 트랜젝션에 영향을 줄 수 있다.

 

 

근데 만약에 이벤트에서 예외가 발생하면??

 

일단 만약 이벤트에서 예외가 발생해도 원래 예약서비스는 롤백되지 않는다. @TransactionalEventListener를 달면 부모 메서드가 커밋된후에(after commit) 이벤트 리스너가 작동하기 때문.

 

 

근데 그럼 이벤트 예외 발생하면 어떻게 처리해야하지..??

 

간단하게 생각해보면 그냥 무한루프마냥 최대 재시도 횟수만 정해서 추가적으로 로직을 구현하면된다.

또다른 방법은 메세지 큐를 도입하여 이벤트 발생시 kafka등으로 메세지를 보내고, 이에 대한 별도의 분리된 마이크로 서비스를 처리하는 방법이 있다.

사실 아직 kafka에 대해서 잘 몰라서 코드로 보여주기 애매하다..

나중에 kafka에 대해서 공부를 하고 더 자세하게 올리도록 하겠다!