들어가기 전

프로젝트를 진행하다 보면 불필요한 객체를 의존하고 있는 코드들이 무수히 많습니다.

필자 또한 업무를 보다가 불필요한 의존성을 가지고 있는 것을 끊어주고 싶어서 EventListener에 대해 알아보고 적용을 해보았습니다.

EventListener를 사용하는 이유는 강한 의존성을 끊기 위해 사용합니다.

EventListener 사용하는 경우에 대해서 예를 들면 사용자가 회원가입을 하였을 때 회원에게 회원가입 축하 메시지를 보낼 때 사용합니다.  이유에 대해서는 EventListenerTransactionalEventListener에 대해 알아보고 예제로 알아보겠습니다.

 

Event

스프링에서 Event는 빈과 빈 사이에 데이터를 전달해 주는 방법 중 하나입니다.

Event에는 이벤트를 발행하는 Publisher과 발행된 이벤트를 받아들이는 Listener가 있습니다.

스프링 4.2 이전에는 이벤트 발행을 하기 위해서 ApplicationEvent를 상속을 받고 이벤트를 발행 및 구독을 할 수 있었습니다.

4.2 이후에는 이벤트 발행은 ApplicationEventPublisher 함수형 인터페이스를 빈으로 주입받고 이벤트 구독은 ApplicationListener 인터페이스를 구현하거나 @EventListener를 사용하면 됩니다.

 

이벤트 발행과 구독을 사용했을 때의 장단점

  • 장점
    • 의존성을 분리하여 클래스 간의 결합도를 낮추고 추후의 별도의 서비스로 분리하기 쉽습니다.
    • 클래스가 독립적이므로 재사용성이 높습니다.
    • 이벤트 구독에 대한 내용을 수정하더라도 다른 모듈에는 영향을 주지 않습니다.
  • 단점
    • 특정 프레임워크 APi에 의존하게 됩니다.
    • 이벤트의 구독과 발행에 대한 테스트가 어렵습니다.
    • 코드의 흐름을 따라가기 어렵고 이벤트 구독이 많을수록 순서 고려가 복작해집니다.

 

Event Publisher

이벤트 발행을 해주는 함수형 인터페이스 ApplicationEventPublisher 스프링 컨텍스트가 구현한 것입니다.

이벤트 발행은  ApplicationEventPublisher에 정의되어 있는 publishEvent를 통해서 이벤트를 발행할 수 있습니다.

publishEvent 인자에는 이벤트 내용을 전달할 객체를 전달해 주면 됩니다.

 

 

 

 

Event Listener

이벤트 구독하는 함수형 인터페이스 ApplicationListener 또는 @EventListener 또한 이벤트 구독하는 인터페이스와 같이 스프링 컨텍스트가 구현한 것입니다.

 

 

 

 

이벤트 발행과 구독을 하는 방법에 대해서 알아보았습니다. 이제 예제를 통해서 사용하는 방법에 대해 알아보겠습니다.

 

요구사항 : 사용자가 회원가입을 하면 회원에게 "회원가입을 축하합니다"라는 메시지를 보내는 기능 구현

(실제로 메시지를 전송하지 않고 축하메시지를 로그로 보여주는 걸로 대체하겠습니다.)

 

회원 엔티티

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Account extends BaseEntity {

    @Column(unique = true, nullable = false)
    private String loginId;
    @Column(nullable = false)
    private String email;
    @Column(nullable = false)
    private String password;
    @Enumerated(value = EnumType.STRING)
    private JoinStatus joinStatus;
    @Enumerated(value = EnumType.STRING)
    private Role role;

    private Account(String loginId, String email, String password, JoinStatus joinStatus, Role role) {
        this.loginId = loginId;
        this.email = email;
        this.password = password;
        this.joinStatus = joinStatus;
        this.role = role;
    }
    public static Account create(String loginId, String email, String password,Role role) {
        return new Account(loginId,email,password,JoinStatus.READY,role);
    }
    public void successJoin() {
        joinStatus = JoinStatus.JOIN;
    }
}

 

 

이벤트 내용을 전달할 객체

 

@AllArgsConstructor
@Getter
public class SignUpCongratulationsEvent {
    private String loginId;
}

 

 

메시지 보내는 서비스 로직

 

@Transactional
@Service
@Slf4j
public class MessageService {

    public void sendSignUpCongratulationsMessage(SignUpCongratulationsEvent event) {
        log.info("{}님 회원가입을 축하합니다.", event.getLoginId());
    }
}

 

 

회원가입 서비스 로직

 

@RequiredArgsConstructor
@Transactional
@Service
public class AccountWriteService {
     private final AccountRepository accountRepository;

    public void join(Join join) {
        if(accountRepository.existsByLoginId(join.getLoginId())) {
            throw new CustomException(ErrorCode.ALREADY_LOGIN_ID);
        }
        Account account = join.toEntity(Role.USER);
        accountRepository.save(account);
    }
}

 

 

EventListener를 사용하지 않았을 때

 

회원가입 서비스 로직

 

@RequiredArgsConstructor
@Transactional
@Service
public class AccountWriteService {
    private final AccountRepository accountRepository;
    private final MessageService messageService;

    public void join(Join join) {
        if(accountRepository.existsByLoginId(join.getLoginId())) {
            throw new CustomException(ErrorCode.ALREADY_LOGIN_ID);
        }
        Account account = join.toEntity(Role.USER);
        accountRepository.save(account);
        messageService.sendSignUpCongratulationsMessage(new SignUpCongratulationsEvent(join.getLoginId()));
    }
}

 

 

EventListener를 사용하지 않고 회원가입 축하메시지를 보내게 되면 AccountService는 MessageService를 의존하고 있습니다.

 

 

EventListener를 사용했을 때

 

회원가입 서비스 로직

 

@RequiredArgsConstructor
@Transactional
@Service
public class AccountWriteService {
    private final AccountRepository accountRepository;
    private final ApplicationEventPublisher publisher;
    public void join(Join join) {
        if(accountRepository.existsByLoginId(join.getLoginId())) {
            throw new CustomException(ErrorCode.ALREADY_LOGIN_ID);
        }
        Account account = join.toEntity(Role.USER);
        accountRepository.save(account);
        publisher.publishEvent(new SignUpCongratulationsEvent(account.getLoginId()));
    }
}

 

 

메시지 보내는 서비스 로직

 

@Transactional
@Service
@Slf4j
public class MessageService {

    @EventListener
    public void sendSignUpCongratulationsMessage(
        SignUpCongratulationsEvent signUpCongratulationsEvent) {
        log.info("{}님 회원가입을 축하합니다.", signUpCongratulationsEvent.getLoginId());
    }
}

 

 

 

EventListener를 사용하여 회원가입 축하메시지를 보내게 되면 사용하지 않을 때와 반대로 AccountService는 MessageService를 의존하지 않고 있습니다.

 

 

이벤트 발행과 구독 동작 방식

 

 

 

지금까지 EventListener에 대해서 알아보았습니다. 이제부터 TransactionalEventListener에 대해 알아보고 TransactionalEventListener의 속성들에 대해서 알아보겠습니다.

 

TransactionalEventListener

TransactionalEventListener은 이벤트 발행에 대한 로직의 트랜잭션 연산에 따른 이벤트를 실행시키는 역할을 합니다. 옵션은 아래와 같이 있습니다.

  • BEFORE_COMMIT : 커밋되기 전에 이벤트를 실행시킵니다.
  • AFTER_COMMIT : 커밋된 이후에 이벤트를 실행시킵니다.(defalut 옵션)
  • AFTER_ROLLBACk : 롤백된 이후에 이벤트를 실행시킵니다.
  • AFTER_COMPLETION : 트랜잭션이 마무리되었을 때 이벤트를 실행시킵니다.

 

 

 

BEFORE_COMMIT, AFTER_COMMIT

 

@RequiredArgsConstructor
@Transactional
@Service
@Slf4j
public class AccountWriteService {
    private final AccountRepository accountRepository;
    private final ApplicationEventPublisher publisher;
    public void join(Join join) {
        if(accountRepository.existsByLoginId(join.getLoginId())) {
            throw new WantedException(ErrorCode.ALREADY_LOGIN_ID);
        }
        Account account = join.toEntity(Role.USER);
        publisher.publishEvent(new TransactionEventListnerMessage());
        accountRepository.save(account);
    }
}

 

 

@Transactional
@Service
@Slf4j
public class MessageService {

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void testAfterCommitMessage(TransactionEventListnerMessage message) {
        log.info("After Commit 메시지 입니다.");
    }
    @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
    public void testBeforeCommitMessage(TransactionEventListnerMessage message) {
        log.info("Before Commit 메시지 입니다.");
    }
}

 

 

 

AFTER_ROLLBACK속성을 가진 이벤트 리스너가 있는데 롤백이 안되고 커밋이 될 경우

 

@Transactional
@Service
@Slf4j
public class MessageService {

    @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
    public void testAfterRollBackMessage(TransactionEventListnerMessage message) {
        log.info("After RollBack 메시지 입니다.");
    }
}

 

 

 

로그 메시지를 보면 After RollBack에 대한 메시지가 보이지 않습니다.

왜냐하면 롤백이 안되고 커밋이 되어서 After RollBack의 속성을 가진 이벤트 리스너가 동작을 안 했기 때문입니다.

 

 

AFTER_ROLLBACK

 

@RequiredArgsConstructor
@Transactional
@Service
@Slf4j
public class AccountWriteService {
    private final AccountRepository accountRepository;
    private final ApplicationEventPublisher publisher;
    public void join(Join join) {
        if(accountRepository.existsByLoginId(join.getLoginId())) {
            throw new WantedException(ErrorCode.ALREADY_LOGIN_ID);
        }
        Account account = join.toEntity(Role.USER);
        publisher.publishEvent(new TransactionEventListnerMessage());
        accountRepository.save(account);
        throw new RuntimeException("AFTER_ROLLBACK");
    }
}

 

@Transactional
@Service
@Slf4j
public class MessageService {

    @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
    public void testAfterRollBackMessage(TransactionEventListnerMessage message) {
        log.info("After RollBack 메시지 입니다.");
    }
}

 

 

 

AFTER_COMPLETION

 

예외가 발생하지 않고 정상동작하여 커밋이 되었을 경우

 

@Transactional
@Service
@Slf4j
public class MessageService {

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void testAfterCommitMessage(TransactionEventListnerMessage message) {
        log.info("After Commit 메시지 입니다.");
    }
    @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
    public void testBeforeCommitMessage(TransactionEventListnerMessage message) {
        log.info("Before Commit 메시지 입니다.");
    }
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
    public void testAfterCompletionMessage(TransactionEventListnerMessage message) {
        log.info("After Completion 메시지 입니다.");
    }
}

 

 

 

예외가 발생하여 롤백이 되었을 경우

 

@Transactional
@Service
@Slf4j
public class MessageService {
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
    public void testAfterCompletionMessage(TransactionEventListnerMessage message) {
        log.info("After Completion 메시지 입니다.");
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
    public void testAfterRollBackMessage(TransactionEventListnerMessage message) {
        log.info("After RollBack 메시지 입니다.");
    }
}

 

 

 

 

지금까지 EventListener와 TransactionalEventListener에 대해서 알아보았습니다.
감사합니다.