Spring의 Event

2024. 3. 31. 21:51Spring

스프링 프레임워크는 개발자들이 애플리케이션 내에서 발생하는 다양한 이벤트를 효율적으로 관리하고 처리할 수 있도록, 강력한 이벤트 리스너(Event Listener) 메커니즘을 제공합니다. 이 메커니즘은 pub/sub 패턴의 구현을 단순화하며, 애플리케이션의 결합도를 낮추고 유지보수성을 높이는 데 도움을 줍니다. 이벤트 리스너를 활용하면, 특정 이벤트가 발생했을 때 실행되어야 할 비즈니스 로직을 캡슐화하고, 애플리케이션의 다른 부분과 독립적으로 관리할 수 있습니다.

 

이벤트 기반 프로그래밍의 이점

  • 낮은 결합도(Low Coupling): 이벤트 프로듀서(발행자)와 컨슈머(소비자) 간의 직접적인 의존성을 제거합니다. 이벤트 프로듀서는 이벤트가 발생했다는 사실만 알리고, 이벤트를 처리하는 로직은 별도의 리스너에서 관리합니다.
  • 높은 응집도(High Cohesion): 관련된 비즈니스 로직을 이벤트 리스너 내부에 명확하게 그룹화함으로써, 코드의 재사용성과 유지보수성을 향상시킵니다.
  • 확장성(Scalability): 새로운 이벤트 리스너를 추가함으로써 기능을 확장할 수 있으며, 기존 코드의 변경 없이도 새로운 기능을 쉽게 통합할 수 있습니다.

 

스프링의 이벤트 리스너 구현

이벤트 클래스 정의:

스프링에서 이벤트를 정의하기 위해서는 먼저 이벤트 클래스를 생성해야 합니다. 클래스는 ApplicationEvent 상속받거나, 간단한 POJO(Plain Old Java Object)일 수 있습니다. 스프링 4.2 이상에서는 ApplicationEvent 상속받지 않고, 어노테이션만으로 이벤트를 정의할 있습니다.

 

Application Context에서 제공하는 기능 중 하나로
간단한 어노테이션 하나만으로 이벤트가 발생했을 때 코드가 실행되도록 하여 쉽게 pub/sub 패턴을 구현할 수 있는 기능입니다.

일단 이벤트에 사용될 클래스를 하나 생성해 줍니다.

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class AccountEvent {
    private Account account ;


    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    public static class SMS extends AccountEvent {
        private String message;

        public SMS(Account account, String message) {
            super(account);
            this.message = message;
        }
    }
}

 

사용자의 액션에 따라 발생할 이벤트들을 만들고 AccountEvent를 상속받도록 해줍니다.

이후 이벤트를 처리할 EventListner를 구현해 줍니다.

 

이벤트 리스너는 @EventListener 어노테이션을 사용하여 구현할 있으며, 비동기 처리를 위해

@Async 어노테이션을 추가할 있습니다.

 

@TransactionalEventListener 사용하면, 트랜잭션의 특정 단계에 맞춰 이벤트 리스너를 실행할 있습니다.

@Slf4j
@Component
@RequiredArgsConstructor
public class AccountEventListener {

    private final SMSService smsService;
    private final SecurityService securityService;

    @EventListener
    public void handleAccountEvent(AccountEvent event) {
        log.info("[INFO] Account {} Event", event.getClass().getSimpleName());
    }


    // 비동기 처리 ( 지연시간 및 비지니스 로직 영향을 최소화 )
    @Async
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) // 트렌젝션 이후 실행
    public void handleSMSEvent(AccountEvent.SMS event) {
        try {
            String phoneNumber = securityService.decrypt(event.getAccount().getPhoneNumber());
            smsService.sendMsg(phoneNumber, event.getMessage());
            log.info("[INFO] account send msg {} ({})", event.getAccount().getUuid(),
                event.getMessage());
        } catch (Exception e) {
            log.error("[ERROR] sms send error {}", e.getMessage());
        }
    }

}

 

@EventListener를
이벤트 발행 후 수행될 메서드에 할당합니다.

 

해당 어노테이션이 붙은 메서드는 ApplicationEventPublisher에 의해 발행된 Event에 대해 반응하게 되고 발행된 Event와 같은 클래스를 파라미터로 갖고 있는 메서드를 실행시킵니다.

 

참고로 이벤트 리스너는 상속을 지원하기 때문에
부모 클래스의 이벤트 리스너가 먼저 실행된 후 자식 클래스의 이벤트 리스너가 동작하도록 되어 있습니다.

@TransactionalEventListner

TransactionalEventListner는 트랜잭션의 트랜잭션의 상태에 따라 이벤트를 처리하는 시점을 보다 세밀하게 제어할 수 있는 어노테이션입니다.

📖 TrandactionalEventListner 옵션

※ (value, condition은 EventListner에서도 사용 가능합니다)

  • phase: 이 옵션은 이벤트가 발생해야 하는 트랜잭션의 단계를 지정합니다.
    TransactionPhase 열거형에 정의된 값(예: BEFORE_COMMIT, AFTER_COMMIT, AFTER_ROLLBACK, AFTER_COMPLETION)을 사용하여 트랜잭션의 어떤 단계에서 이벤트 리스너가 호출될지 정의할 수 있습니다.
  • fallbackExecution: 이 옵션은 트랜잭션 컨텍스트가 존재하지 않을 때 이벤트 리스너의 실행 여부를 결정합니다. 기본값은 false이며, 이 경우 트랜잭션 컨텍스트가 없으면 이벤트 리스너가 호출되지 않습니다. true로 설정하면, 트랜잭션이 존재하지 않을 때도 리스너가 호출됩니다.
  • value: 이벤트 클래스를 지정합니다. 리스너가 처리해야 하는 이벤트의 타입을 나타냅니다.
  • condition: SpEL(Spring Expression Language) 표현식을 사용하여 이벤트 처리를 위한 조건을 제공할 수 있습니다. 이 옵션은 리스너가 특정 조건을 만족하는 이벤트에 대해서만 반응하도록 할 때 유용합니다.
    ex) @TrandactionalEventListner(condition = “event.success”) ⇒ event 객체의 success 변수가 true일 경우에만 처리

📖 phase  option

TransactionPhase 값 설명
BEFORE_COMMIT 트랜잭션이 커밋되기 직전에 이벤트가 처리됩니다.
AFTER_COMMIT 트랜잭션이 성공적으로 커밋된 후에 이벤트가 처리됩니다.
AFTER_ROLLBACK 트랜잭션이 롤백된 후에 이벤트가 처리됩니다.
AFTER_COMPLETION 트랜잭션이 커밋되거나 롤백된 후에 이벤트가 처리됩니다. 즉, 트랜잭션이 완료된 후에 호출됩니다.

 

이렇게 EventListner와 TransactionalEventListner를 선언 후

ApplicationEventPublisher를 주입받고
아래와 같이 publishEvent에 발행할 Event의 인스턴스를 넣어주시면 됩니다.

@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class KaKaoServiceV1 implements LoginService, OAuthService {

    private final AccountRepository accountRepo;
    private final JwtTokenProvider jwtTokenProvider;
    private final ApplicationEventPublisher eventPublisher;


    @Override
    public JwtDto login(OAuthToken authToken) {
        KakaoProfile kakaoProfile = getKakaoProfile(authToken.getAccess_token());
        KakaoProfile.KakaoAccount kakaoAccount = kakaoProfile.getKakao_account();

        Account account = Account.builder()
            .email(kakaoAccount.getEmail())
            .profileImageUrl(kakaoAccount.getProfile().getProfile_image_url())
            .gender(Account.Gender.FEMALE)
            .birthDay(LocalDate.now())
            .build();

        // 가입자 혹은 비가입자 체크 해서 처리
        Account originUser = accountRepo.findByOauthUuid(account.getOauthUuid())
            .orElseGet(() -> {
                log.info("[INFO] New Account ! {}", account.getOauthUuid());
                Account saveAccount = accountRepo.save(account);
                eventPublisher.publishEvent(new AccountEvent.SMS(saveAccount, SMS.WELCOME_MESSAGE));
                return saveAccount;
            });
            
        return JwtDto.builder()
            .jwt(jwtTokenProvider.createToken(originUser.getUuid(), originUser.getRole()))
            .build();
}

 

 

 

스프링 이벤트 리스너의 원리

애플리케이션 컨텍스트(Application Context)

- 스프링의 핵심 컨테이너 하나로, (Bean) 생명주기 관리, 설정 정보를 제공하는 등의 역할을 수행합니다.

- 애플리케이션 컨텍스트는 이벤트 발행과 리스닝에 필요한 ApplicationEventPublisher ApplicationEventListener 인터페이스의 구현체를 제공합니다.

 

이벤트 클래스

- 이벤트 정보를 담는 클래스로 스프링 4.2 이상부터는 ApplicationEvent 클래스를 상속받지 않고 POJO 형태로 정의할 수 있습니다.

- 이벤트 클래스는 이벤트 타입을 정의하며 이에 대한 관련된 데이터를 담습니다.

 

이벤트 발행

- ApplicationEventPublisher 인터페이스의 publishEvent 메소드를 사용하여 이벤트를 발행하게 됩니다.

- ApplicationEventPublisher는 이미 빈에 자동으로 등로고디어 있으며 이벤트 객체를 인자로 받아 애플리케이션 내의 리스너들에게 해당 이벤트의 발행을 알리게 됩니다.

 

이벤트 리스닝

- @EventListner 어노테이션으로 등록되어 있는 이벤트 리스너들을 Application Context를 통해 리스너들을 찾고 가장 적합한 리스너 메소드를 실행하게 됩니다.

 

 

결론

스프링 프레임워크의 이벤트 리스너 기능은 현대적인 애플리케이션 개발에서 필수적인 패턴인 이벤트 기반 프로그래밍을 쉽고 효율적으로 구현할 수 있도록 해줍니다.

 

이벤트 리스너를 활용함으로써, 개발자는 애플리케이션의 다양한 부분 사이의 결합도를 낮추고, 코드의 재사용성 및 유지보수성을 향상시킬 수 있습니다.

 

또한, 이벤트 기반의 비동기 처리를 통해 애플리케이션의 성능을 최적화하고, 사용자 경험을 개선할 수 있는 기회를 제공합니다.

 

이벤트 리스너는 애플리케이션의 확장성에도 큰 이점을 가져다줍니다. 새로운 이벤트와 이에 대응하는 리스너를 손쉽게 추가함으로써, 애플리케이션에 새로운 기능을 통합할 수 있습니다. 이는 특히 대규모 시스템이나 마이크로서비스 아키텍처를 사용하는 경우 유용합니다.

 

트랜잭션과의 통합을 통해, 이벤트 리스너는 데이터 일관성과 무결성을 보장하는 데에도 중요한 역할을 합니다.

이를 통해 개발자는 비즈니스 로직의 복잡성을 관리하고, 데이터 처리 과정에서 발생할 수 있는 예외 상황을 효과적으로 처리할 수 있습니다.

'Spring' 카테고리의 다른 글

Spring Validation 정복하기  (0) 2024.05.07
Spring 에서 전략패턴 적용해보기  (1) 2024.04.14
Annotaion  (0) 2023.06.18
Servlet과 PSA  (0) 2023.05.29
스프링의 3대 핵심 요소  (0) 2023.05.21