본문으로 바로가기
반응형

개요

일반적으로 데이터베이스와 연결된 상황에서 코드를 작업할 때 일관성을 위해 트랜잭션을 걸어서 사용한다. (@Transactional) 만약 아래와 같은 코드가 있다고 가정해보자.

@Service
public class RoomService {
    @Transactional
    public void createRoom() {
    	// 1. 룸 생성
        // 2. 메일 전송 코드(외부 시스템)
        // 3. 유저 정보를 가져와서 소유한 룸의 개수 + 1
    }
}

주석을 보면 알겠지만, DB영속화를 하는 작업은 룸 생성/유저 소유 룸 개수+1 총 2개의 작업이 있다. 나머지 하나는 외부 시스템을 통해 메일을 전송한다. 1, 3번의 경우 @Transactional을 통해 하나의 트랜잭션으로 묶여있기에 둘 중 하나라도 오류가 발생하면 모두 롤백한다. 

만약 3번에서 오류가 발생한다면 어떻게 될까? 2번은 외부 시스템이기에 트랜잭션으로 묶을 수 없다. 따라서 1, 3번 DB영속화는 실패하겠지만 2번 메일 전송은 성공적으로 이루어지게 된다. 이렇게 된다면 실제로 룸은 생성되지 않았지만 유저에게 룸이 생성되었다는 알림이 나가게 됨으로써 일관성이 깨지게 된다.

또한 위처럼 하나의 서비스에 많은 해당하는 역할이 아닌 다른 역할을 포함한 코드들이 많이 들어가있다면 해당 서비스의 의존도는 높아지고 타 객체와 강결합하는 형태가 되어버린다. 이는 즉, 유지보수에도 많은 영향을 미치게 될 것이다.

따라서 서비스는 해당 서비스의 역할에 맡는 작업만 진행하고 나머지는 이벤트를 발생시킨 후 핸들러에서 해당 이벤트를 받아서 처리하도록 하면 의존도를 낮출 수 있고, 첫 번째로 말한 트랜잭션을 통한 일관성 문제도 해결할 수 있다.

ApplicationEventPublisher

먼저 발생시킬 이벤트를 생성한다. 예제를 위해 간단하게 정수 값 하나를 전달하는 이벤트이다.

public class TestEvent {
    private int data;

    public TestEvent(int data) {
        this.data = data;
    }

    public int getData() {
        return data;
    }
}

다음으로 이벤트를 받아서 처리할 클래스를 생성한다.

@Component
public class TestEventHandler {
    @EventListener
    public void testEventHandlerListener(TestEvent event) {
        System.out.println("[*] testEventHandlerListener Start");
        System.out.println(event.getData());
        System.out.println("[*] testEventHandlerListener End");
    }
}

인자를 보면 알겠지만 위에서 생성한 이벤트를 받아서 해당 이벤트의 값을 출력하는 단순한 핸들러이다. @EventListener를 붙여줘야 리스너로 등록되니 주의하자. 마지막으로 서비스 레이어에서 이벤트를 발생시키면 된다.

@Service
@RequiredArgsConstructor
public class RoomService {
    private final ApplicationEventPublisher eventPublisher;

    public void testEvent() {
        System.out.println("[*] 룸 생성");
        eventPublisher.publishEvent(new TestEvent(123));
        System.out.println("[*] 유저 룸 소유 개수 + 1");
    }
}

ApplicationEventPublisher를 보면 publishEvent라는 메소드가 존재하는데 해당 메소드를 통해 우리가 생성했던 이벤트를 발생시키면 된다. 위처럼 코드를 작성하고 실행시켜보면 결과는 아래와 같다.

[*] 룸 생성
[*] testEventHandlerListener Start
123
[*] testEventHandlerListener End
[*] 유저 룸 소유 개수 + 1

 

 

참고로 인터넷에 검색하다보면 이벤트는 ApplicationEvent를 상속받아 구현하고 핸들러는 ApplicationListener를 상속받아 구현하는 예제를 심심찮게 볼 수 있는데, 스프링 4.2부터는 상속없이 구현해도 된다고 한다.

여기까지하면 끝인 것 같은데 한가지 생각해볼점이 있다. 위에서 이벤트 리스너를 보면 @EventListener 어노테이션을 사용하고 있는데, 해당 어노테이션의 경우 publishEvent 메소드를 통해 이벤트를 발생시키는 즉시 이벤트가 발생된다. 그런데 개요에서 설명했던 것 처럼, 룸 생성이 끝나고 유저에게 룸이 생성되었다는 사실을 메일로 알려줬는데, 유저의 룸 개수를 증가시킬 때 예외가 발생한다면 생성되지도 않았는데 이메일만 발송되는 문제가 발생한다.

따라서 이 부분에서는 @TransactionalEventListener를 사용해줘야 한다. 해당 어노테이션의 옵션은 총 4개로 다음과 같다.

- BEFORE_COMMIT - 커밋 전 실행
- AFTER_COMMIT(기본값) - 커밋 후 실행
- AFTER_ROLLBACK – 롤백됐을 때 실행
- AFTER_COMPLETION – 커밋 또는 롤백됐을 때 실행

@Component
public class TestEventHandler {
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void testEventHandlerListener(TestEvent event) {
        System.out.println("[*] testEventHandlerListener Start");
        System.out.println(event.getData());
        System.out.println("[*] testEventHandlerListener End");
    }
}

위처럼 @EventListener -> @TransactionalEventListener로 교체해준다면 트랜잭션이 성공적으로 커밋됐을때만 이벤트가 실행된다. 따라서 위에서 말한 룸이 생성되지 않았는데 이메일만 발송되는 문제를 해결할 수 있다. 

public void testEvent() {
    System.out.println("[*] 룸 생성");
    eventPublisher.publishEvent(new TestEvent(123));
    throw new RoomNotFoundException();
}

서비스쪽 코드를 위처럼 수정하고 다시 실행시켜보면

[*] 룸 생성

룸 생성 이후 RoomNotFoundException을 던졌기 때문에 이벤트도 실행되지 않는 걸 볼 수 있다.

@Order를 통해 순서대로 실행하기

만약 여러개의 이벤트를 원하는 순서대로 실행하고 싶다면 @Order 어노테이션을 사용하면 된다. 먼저 이벤트를 하나 더 생성한다.

public class SecondEvent {
    private int data;

    public SecondEvent(int data) {
        this.data = data;
    }

    public int getData() {
        return data;
    }
}

다음으로 기존에 만들어뒀던 리스너에 해당 이벤트도 등록한다.

@Component
public class TestEventHandler {
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void testEventHandlerListener(TestEvent event) {
        System.out.println("[*] testEventHandlerListener Start");
        System.out.println(event.getData());
        System.out.println("[*] testEventHandlerListener End");
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void testSecondEventHandlerListener(SecondEvent event) {
        System.out.println("[*] testSecondEventHandlerListener Start");
        System.out.println(event.getData());
        System.out.println("[*] testSecondEventHandlerListener End");
    }
}

서비스 코드를 아래와 같이 수정한다.

@Transactional
public void testEvent() {
    System.out.println("[*] 룸 생성");
    eventPublisher.publishEvent(new TestEvent(123));
    eventPublisher.publishEvent(new SecondEvent(456));
    System.out.println("[*] 유저 룸 소유 개수 + 1");
}

실행시켜보면 결과는 아래와 같다.

[*] 룸 생성
[*] 유저 룸 소유 개수 + 1
[*] testEventHandlerListener Start
123
[*] testEventHandlerListener End
[*] testSecondEventHandlerListener Start
456
[*] testSecondEventHandlerListener End

이제 순서를 바꿔본다. 

@Component
public class TestEventHandler {
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    @Order(Ordered.HIGHEST_PRECEDENCE + 1)
    public void testEventHandlerListener(TestEvent event) {
        System.out.println("[*] testEventHandlerListener Start");
        System.out.println(event.getData());
        System.out.println("[*] testEventHandlerListener End");
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public void testSecondEventHandlerListener(SecondEvent event) {
        System.out.println("[*] testSecondEventHandlerListener Start");
        System.out.println(event.getData());
        System.out.println("[*] testSecondEventHandlerListener End");
    }
}

리스터쪽에 @Order 어노테이션을 붙여주면 되는데, 보다시피 처음에 생성한 핸들러의 순서에 +1을 해줌으로써 나중에 생성한 이벤트가 먼저 실행되도록 했다.

[*] 룸 생성
[*] 유저 룸 소유 개수 + 1
[*] testSecondEventHandlerListener Start
456
[*] testSecondEventHandlerListener End
[*] testEventHandlerListener Start
123
[*] testEventHandlerListener End

두번째 이벤트가 먼저 실행된 모습을 확인할 수 있다.

반응형