176 lines
3.9 KiB
Markdown
176 lines
3.9 KiB
Markdown
# ApplicationEvent 예시
|
|
|
|
## 좋은 예시 1: 핵심 작업 후 후속 반응 분리
|
|
|
|
```java
|
|
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 후에만 처리
|
|
|
|
```java
|
|
@Component
|
|
public class UserRegisteredAuditListener {
|
|
|
|
@TransactionalEventListener
|
|
public void handle(UserRegisteredEvent event) {
|
|
auditLog.record("USER_REGISTERED", event.userId(), event.occurredAt());
|
|
}
|
|
}
|
|
```
|
|
|
|
**왜 좋은가:**
|
|
|
|
- 기본 AFTER_COMMIT 의미를 활용한다
|
|
- rollback된 작업에 대해 잘못된 후속 기록을 남기지 않는다
|
|
|
|
## 좋은 예시 3: listener는 짧고 부가적
|
|
|
|
```java
|
|
@Component
|
|
public class WelcomeMetricListener {
|
|
|
|
@EventListener
|
|
public void handle(UserRegisteredEvent event) {
|
|
metrics.counter("user.registered").increment();
|
|
}
|
|
}
|
|
```
|
|
|
|
**왜 좋은가:**
|
|
|
|
- 짧고 효율적이다
|
|
- 핵심 비즈니스 흐름을 숨기지 않는다
|
|
|
|
## 좋은 예시 4: 테스트에서 이벤트 검증
|
|
|
|
```java
|
|
@RecordApplicationEvents
|
|
@SpringBootTest
|
|
class RegisterUserServiceTest {
|
|
|
|
@Test
|
|
void publishes_user_registered_event(ApplicationEvents events) {
|
|
service.register(command);
|
|
|
|
assertThat(events.stream(UserRegisteredEvent.class)).hasSize(1);
|
|
}
|
|
}
|
|
```
|
|
|
|
**왜 좋은가:**
|
|
|
|
- 이벤트 발행 사실을 테스트로 확인할 수 있다
|
|
- “어딘가에서 되겠지” 상태를 줄인다
|
|
|
|
## 나쁜 예시 1: 핵심 오케스트레이션을 이벤트에 숨김
|
|
|
|
```java
|
|
@Service
|
|
public class LoginService {
|
|
|
|
public LoginResponse login(LoginCommand command) {
|
|
eventPublisher.publishEvent(new LoginRequestedEvent(command));
|
|
return LoginResponse.pending();
|
|
}
|
|
}
|
|
```
|
|
|
|
**문제:**
|
|
|
|
- 핵심 로그인 흐름이 listener들 뒤로 숨어버린다
|
|
- 메인 결과가 이벤트 체인에 의존하게 된다
|
|
|
|
## 나쁜 예시 2: payload가 너무 빈약함
|
|
|
|
```java
|
|
public record UserRegisteredEvent(Long userId) {}
|
|
```
|
|
|
|
**문제:**
|
|
|
|
- 모든 listener가 다시 조회를 강요받을 수 있다
|
|
- 필요한 최소 상태가 누락되면 결합과 조회 비용이 커진다
|
|
|
|
**개선:**
|
|
|
|
- 정말 필요한 상태를 payload에 포함
|
|
|
|
## 나쁜 예시 3: 무거운 작업을 listener에 직접 넣음
|
|
|
|
```java
|
|
@Component
|
|
public class HeavyListener {
|
|
|
|
@EventListener
|
|
public void handle(UserRegisteredEvent event) {
|
|
externalApi.call(...);
|
|
fileExporter.export(...);
|
|
Thread.sleep(5000);
|
|
}
|
|
}
|
|
```
|
|
|
|
**문제:**
|
|
|
|
- listener가 너무 무겁고 느리다
|
|
- event hand-off의 장점을 해친다
|
|
- 장애 반경이 커진다
|
|
|
|
## 나쁜 예시 4: listener 순서에 핵심 의존
|
|
|
|
```java
|
|
@Component
|
|
class FirstListener {
|
|
@Order(1)
|
|
@EventListener
|
|
void handle(UserRegisteredEvent event) { ... }
|
|
}
|
|
|
|
@Component
|
|
class SecondListener {
|
|
@Order(2)
|
|
@EventListener
|
|
void handle(UserRegisteredEvent event) { ... } // 첫 번째가 반드시 먼저 돌 것을 기대
|
|
}
|
|
```
|
|
|
|
**문제:**
|
|
|
|
- 이벤트 기반 구조가 사실상 숨은 절차형 흐름이 된다
|
|
- 순서 의존이 커질수록 명시적 호출이 더 낫다
|