Files
project-auth-server/docs/examples/spring/application-event.md
T

3.9 KiB

ApplicationEvent 예시

좋은 예시 1: 핵심 작업 후 후속 반응 분리

public record UserRegisteredEvent(
        Long userId,
        String email,
        Instant occurredAt
) {}

@Service
public class RegisterUserService {

    private final ApplicationEventPublisher eventPublisher;
    private final Clock clock;

    public RegisterUserService(ApplicationEventPublisher eventPublisher, Clock clock) {
        this.eventPublisher = eventPublisher;
        this.clock = clock;
    }

    @Transactional
    public Long register(CreateUserCommand command) {
        User user = ...;
        userRepository.save(user);

        eventPublisher.publishEvent(
                new UserRegisteredEvent(user.getId(), user.getEmail(), Instant.now(clock))
        );

        return user.getId();
    }
}

왜 좋은가:

  • 핵심 등록 작업과 후속 반응을 분리한다
  • 이벤트 payload가 필요한 상태를 직접 담는다
  • publisher가 listener 구현을 모른다

좋은 예시 2: commit 후에만 처리

@Component
public class UserRegisteredAuditListener {

    @TransactionalEventListener
    public void handle(UserRegisteredEvent event) {
        auditLog.record("USER_REGISTERED", event.userId(), event.occurredAt());
    }
}

왜 좋은가:

  • 기본 AFTER_COMMIT 의미를 활용한다
  • rollback된 작업에 대해 잘못된 후속 기록을 남기지 않는다

좋은 예시 3: listener는 짧고 부가적

@Component
public class WelcomeMetricListener {

    @EventListener
    public void handle(UserRegisteredEvent event) {
        metrics.counter("user.registered").increment();
    }
}

왜 좋은가:

  • 짧고 효율적이다
  • 핵심 비즈니스 흐름을 숨기지 않는다

좋은 예시 4: 테스트에서 이벤트 검증

@RecordApplicationEvents
@SpringBootTest
class RegisterUserServiceTest {

    @Test
    void publishes_user_registered_event(ApplicationEvents events) {
        service.register(command);

        assertThat(events.stream(UserRegisteredEvent.class)).hasSize(1);
    }
}

왜 좋은가:

  • 이벤트 발행 사실을 테스트로 확인할 수 있다
  • “어딘가에서 되겠지” 상태를 줄인다

나쁜 예시 1: 핵심 오케스트레이션을 이벤트에 숨김

@Service
public class LoginService {

    public LoginResponse login(LoginCommand command) {
        eventPublisher.publishEvent(new LoginRequestedEvent(command));
        return LoginResponse.pending();
    }
}

문제:

  • 핵심 로그인 흐름이 listener들 뒤로 숨어버린다
  • 메인 결과가 이벤트 체인에 의존하게 된다

나쁜 예시 2: payload가 너무 빈약함

public record UserRegisteredEvent(Long userId) {}

문제:

  • 모든 listener가 다시 조회를 강요받을 수 있다
  • 필요한 최소 상태가 누락되면 결합과 조회 비용이 커진다

개선:

  • 정말 필요한 상태를 payload에 포함

나쁜 예시 3: 무거운 작업을 listener에 직접 넣음

@Component
public class HeavyListener {

    @EventListener
    public void handle(UserRegisteredEvent event) {
        externalApi.call(...);
        fileExporter.export(...);
        Thread.sleep(5000);
    }
}

문제:

  • listener가 너무 무겁고 느리다
  • event hand-off의 장점을 해친다
  • 장애 반경이 커진다

나쁜 예시 4: listener 순서에 핵심 의존

@Component
class FirstListener {
    @Order(1)
    @EventListener
    void handle(UserRegisteredEvent event) { ... }
}

@Component
class SecondListener {
    @Order(2)
    @EventListener
    void handle(UserRegisteredEvent event) { ... } // 첫 번째가 반드시 먼저 돌 것을 기대
}

문제:

  • 이벤트 기반 구조가 사실상 숨은 절차형 흐름이 된다
  • 순서 의존이 커질수록 명시적 호출이 더 낫다