[spring] 이벤트 By starseat 2023-07-21 13:00:14 java/spring Post Tags Spring 에서 Event 처리하는 것을 알아보자. 개념이나 다른 방법들은 [구글링:Spring Event](https://www.google.com/search?q=Spring+Event&rlz=1C1YTUH_koKR1027KR1027&sxsrf=AB5stBjW1EDlNrKVz08znDgwI_PM3yZ8Vg%3A1690183548733&ei=fCe-ZNOwLNGhhwPYiLeQBA&ved=0ahUKEwjT3puw6KaAAxXR0GEKHVjEDUIQ4dUDCA8&uact=5&oq=Spring+Event&gs_lp=Egxnd3Mtd2l6LXNlcnAiDFNwcmluZyBFdmVudDIHECMYigUYJzIHECMYigUYJzIEECMYJzIFEAAYgAQyBRAAGIAEMgUQABiABDIFEAAYgAQyBRAAGIAEMgUQABiABDIFEAAYgARIxw1QAFidCnAAeAGQAQCYAYQBoAH5AqoBAzAuM7gBA8gBAPgBAcICBxAjGLACGCfCAgcQABgNGIAE4gMEGAAgQYgGAQ&sclient=gws-wiz-serp) 하면 많이 나오니 바로 실습으로 들어간다. # 시작 이벤트 처리하는 로직은 다음과 같이 할 예정이다. 1. 데이터를 가공하여 대상 테이블에 저장 2. 저장 결과 여부를 Result Table 에 저장 (성공, 실패(+ 실패 사유) 정보 저장) # Event 기초 작업 ## gradle ```gradle dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' } ``` ## Event Model 모든 `Event` 는 기본 Event Model 을 상속 받아 쓸 것이기에 기초 모델을 생성한다. **이벤트 이름**과 **생성 시간** 정보만 두었다. ```java import lombok.Getter; @Getter public abstract class Event { private static final String EVENT_EMPTY = ""; protected String name; protected long timestamp; public Event() { this.name = EVENT_EMPTY; this.timestamp = System.currentTimeMillis(); } public Event(String name) { this.name = name; this.timestamp = System.currentTimeMillis(); } public Event(String name, long timestamp) { this.name = name; this.timestamp = timestamp; } public void setBaseInfo(String name) { this.name = name; this.timestamp = System.currentTimeMillis(); } public void setBaseInfo(String name, long timestamp) { this.name = name; this.timestamp = timestamp; } } ``` ## Event Publisher 다른 사람들은 명칭을 좀 다르게 하지만 난 `EventPublisher` 로 명명 하였다. ```java public class EventPublisher { private static ApplicationEventPublisher publisher; public static void setPublisher(ApplicationEventPublisher publisher) { EventPublisher.publisher = publisher; } /** * 이벤트 발생 시킨다. * @param event */ public static void raise(Event event) { if(publisher != null) { publisher.publishEvent(event); } } } ``` ## Event Configuration ```java @Configuration @RequiredArgsConstructor public class EventConfiguration { private final ApplicationContext context; @Bean public InitializingBean eventsInitializer() { return () -> EventPublisher.setPublisher(context); } } ``` # Event 사용 준비 ## ResultEvent Custom Event 인 `ResultEvent` 생성한다. `Event` 를 상속 받는다. ```java @ToString @Getter @Builder @NoArgsConstructor @AllArgsConstructor public class ResultEvent extends Event { private Long id; private ProcessStatus status; // 상태 (0: 진행 중, 1: 완료, 9: 실패) private String errorMessage; // 오류 메시지 내용 private LocalDateTime createDate; // 시작 일시 private LocalDateTime finishDate; // 종료 일시 public static ResultEvent convert(ResultQuery query) { ResultEvent event = ResultEvent.builder() .id(query.getId()) .status(ProcessStatus.of(query.getStatus()) .errorMessage(query.getErrorMessage()) .createDate(history.getCreateDate()) .finishDate(history.getFinishDate()) .build()); event.setBaseInfo("EVENT_RESULT"); return event; } } ``` ## handler Event 를 받아 처리하는 `ResultEventHandler` 를 생성한다. 여기선 결과 정보를 받아 Result Table 에 저장하는 역할을 한다. `@Async` 부분은 비동기 처리를 위해서 추가 하였고, [[spring] Async 설정](https://starseat.net/blog/view/184) 에 정리해 두었다. (`@어노테이션` 주석은 필요가 없어서 주석 하였는데 나중에 참고할 수 있으므로 냅뒀다.) ```java @Slf4j @Component @RequiredArgsConstructor public class ResultEventHandler { private final ResultHistoryRepository repository; @Async("ResultTaskExecutor") @EventListener(value = ResultEvent.class) // @TransactionalEventListener(value = ResultEvent.class) @Transactional( propagation = Propagation.REQUIRES_NEW // , noRollbackFor = RuntimeException.class ) // @javax.transaction.Transactional(dontRollbackOn = RuntimeException.class) public void saveResult(ResultEvent event) { ResultHistory savedHistory = event.toHistoryEntity(); log.info("[saveResult] received result data. " + "resultId: {}, status: {}({})", savedHistory.getId(), savedHistory.getStatus(), ProcessStatus.of(savedHistory.getStatus()).getMessage(); // 영속성 이슈로 db select 를 하지 않게 // JPA 대신 JPQL 로 update 직접 사용함. long saveResult = repository.saveResultHistory(savedHistory); log.info("[saveResult] received result data saved db. " + "historyId: {}, status: {}({}), result: {}", savedHistory.getId(), savedHistory.getStatus(),ProcessStatus.of(savedHistory.getStatus()).getMessage(), saveResult); } } ``` # Event 사용 (발행) 이제 사용 준비가 끝났으니 실제 사용해보자. ProcessService 라는 임시 서비스에서 사용하는 예 이다. ## Service `process` method 를 호출하기 전에 `0: 진행 중` 상태로 Result Table 에 Insert 한다. 그리고 그 entity 정보를 `ResultQuery resultQuery` 파라미터로 전달한다. 큰 흐름은 **try 부분에서 성공 처리**를 하고, **catch 부분에서 error 처리**를 한다. 그리고 **finally 부분에서 저장** 하는 `saveResultHistory` method 를 호출 하고 **saveResultHistory 에서 Event 를 발생** 시킨다. ```java @Slf4j @Service @RequiredArgsConstructor //@Transactional public class ProcessService { private final ProcessRepository repository; private final ResultHistoryRepository resultRepository; // ... @Async("ProcessTaskExecutor") public void process(final ProcessQuery query, final ResultQuery resultQuery) { try { // `query` process... resultQuery.success(); // 성공 상태로 변경. } catch (ParseException e) { resultQuery.failed("ParseException"); // 실패 사유 작성 및 상태 변경. } catch (SqlExecutionRuntimeException e) { resultQuery.failed("SqlExecutionRuntimeException"); // 실패 사유 작성 및 상태 변경. } catch (RuntimeException e) { resultQuery.failed("RuntimeException"); // 실패 사유 작성 및 상태 변경. } catch (Exception e) { resultQuery.failed("Exception"); // 실패 사유 작성 및 상태 변경. } finally { saveResultHistory(resultQuery); } } public void saveResultHistory(ResultQuery savedHistory) { log.info("[saveResultHistory] finish process. generate result event. historyId: {}, status: {}({})", savedHistory.getId(), savedHistory.getStatus(), ProcessStatus.getMessage(savedHistory.getStatus())); EventPublisher.raise(ResultEvent.convert(savedHistory)); } } ``` # 추가 혹시 `ProcessStatus` 도 궁금할 수 있으므로 추가한다. 결과 정보를 저장하는 `enum class` 이다. ```java @Getter @AllArgsConstructor public enum ProcessStatus { EMPTY("", ""), // empty PROCESSING("0", "processing"), SUCCESS("1", "success"), FAILED("9", "failed"); private final String code; private final String message; public static ProcessStatus of(String status) { if(ObjectUtils.isEmpty(status)) { status = ProcessStatus.EMPTY; } ProcessStatus retStatus = ProcessStatus.PROCESSING; for (ProcessStatus _status: ProcessStatus.values()) { if (_status.code.equals(status)) { retStatus = _status; break; } } return retStatus; } public static String getMessage(String code) { ProcessStatus result = null; for (ProcessStatus _status: ProcessStatus.values()) { if (_status.code.equals(code)) { return result = _status; } } return ProcessStatus.EMPTY.getMessage(); } } ``` # 참조 - [도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지](https://product.kyobobook.co.kr/detail/S000001810495) Previous Post [spring] 대량 데이터 및 트래픽 처리 경험담... Next Post [spring] 테스트 코드의 @Mock, @MockBean, @InjectMocks