** 국비교육 : 중앙정보처리기술인재개발원에서 진행한 팀프로젝트 내용입니다.
기록용으로 남기며, 참고만 부탁드립니다.
** https://github.com/toge-dog
● Scheduler 를 사용한 이유
데이터베이스의 테이블간의 관계도를 설계할 때, 시스템 설계상 너무 잦은 조회가 발생할 수 있는 구조이었다.
그렇기에 비정규화를 통하여 과도한 join문이 발생하는 것을 방지하였다.
비정규화를 사용하니 데이터의 정합성문제가 발생할 수도 있겠다라고 판단이 들어
매 00시 00분 마다 데이터 정합성 검증 테스트를 위해 Scheduler를 채용하였다.
package com.togedog.scheduler;
import com.togedog.matchingStandBy.service.MatchingStandByService;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
@RequiredArgsConstructor
public class TimeCheckScheduler {
private final MatchingStandByService service;
// 매일 00:00 (자정)에 실행
@Async
@Scheduled(cron = "0 0 0 * * *") // 초, 분, 시, 일, 월, 요일
public void checkTime() {
LocalDateTime now = LocalDateTime.now();
service.changeStatusToTimeOut(now);
System.out.println("스케줄 실행 - 현재 시간: " + now);
}
}
또한 해당 데이터정합성 테스트를 비동기로 동작하게 하기위함으로 비동기 설정또한 아래와 같이 진행하였다.
package com.togedog.scheduler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(1); // 최소 스레드 수
executor.setMaxPoolSize(5); // 최대 스레드 수
executor.setQueueCapacity(100); // 큐에 들어갈 작업의 최대 수
executor.setThreadNamePrefix("AsyncScheduler-");
executor.initialize();
return executor;
}
}
● EventListener 를 사용한 이유
서비스 구조상 서로 다른 목적의 서비스를 가지고 있음에도 비즈니스 로직이 서로가 과도하게 얽혀 있는 구조로 짜여진 경우가 잦았다. 유지보수성 및 두 컴포넌트간의 결합도를 낮추고자 하였고, 더 나아가 테스트코드 작성 시 독립적으로 테스트가 진행할 수 있도록 하기 위해서 이벤트 리스너를 사용하였다.
package com.togedog.eventListener;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;
import com.togedog.eventListener.EventCaseEnum.EventCase;
import java.util.ArrayList;
import java.util.List;
@Getter
public class CustomEvent extends ApplicationEvent {
private EventCase methodName;
private Object resource;
private List<Long> resources;
public CustomEvent(Object source, EventCase methodName,Object resource) {
super(source);
this.methodName = methodName;
this.resource = resource;
}public CustomEvent(Object source, EventCase methodName,List<Long> resources) {
super(source);
this.methodName = methodName;
this.resources = resources;
}
}
각 이벤트케이스에 맞추어 특정 서비스에서 동작할 수 있도록 명시해놨다.
package com.togedog.eventListener;
import lombok.AllArgsConstructor;
import lombok.Getter;
public class EventCaseEnum {
@Getter
@AllArgsConstructor
public enum EventCase{
DELETE_RELATED_MATCHING_STAND_BY_DATA(1,"연관된 MatchingStandBy 데이터 삭제"),
SUCCESS_RELATED_MATCHING_DATA(2,"연관된 Matching 데이터 성공으로 상태 변경"),
CREATE_CHAT_ROOM(3, "호스트와 게스트의 채팅방을 생성"),
DELETE_MARKER(4,"마커 삭제");
private int statusNumber;
private String statusDescription;
}
}
package com.togedog.eventListener;
import com.togedog.chatRoom.service.ChatRoomService;
import com.togedog.matching.service.MatchingService;
import com.togedog.matchingStandBy.entity.MatchingStandBy;
import com.togedog.matchingStandBy.service.MatchingStandByService;
import com.togedog.member.service.MemberService;
import com.togedog.redis.MarkerService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import static com.togedog.eventListener.EventCaseEnum.EventCase.*;
@Slf4j
@Component
@RequiredArgsConstructor
public class ServiceEventListener {
private final MatchingService matchingService;
private final ChatRoomService chatRoomService;
private final MatchingStandByService matchingStandByService;
private final MemberService memberService;
private final MarkerService markerService;
@EventListener
public void handleMyCustomEvent(CustomEvent event) {
switch (event.getMethodName()) {
case DELETE_RELATED_MATCHING_STAND_BY_DATA:
log.debug("Event : DELETE_RELATED_MATCHING_STAND_BY_DATA");
matchingStandByService.extractedDataAndChangeStatusToFail((Long) event.getResource(),
MatchingStandBy.Status.STATUS_WAIT);
break;
case SUCCESS_RELATED_MATCHING_DATA:
log.debug("Event : SUCCESS_RELATED_MATCHING_DATA");
matchingService.updateMatchForCustomEvent(event.getResources().get(0),event.getResources().get(1));
break;
case CREATE_CHAT_ROOM:
log.debug("Event : CREATE_CHAT_ROOM");
chatRoomService.createChatRoom(memberService.createChatRoomForCustomEvent(event.getResources()));
break;
case DELETE_MARKER:
log.debug("Event : DELETE_MARKER");
String markerInfo = "marker:" + (String)event.getResource();
markerService.deleteMarker(markerInfo);
}
}
}
● Scheduler / EventListener 을 활용하면서 얻게 된 이점
이벤트 리스너를 사용하게 되면서 시스템의 확장성, 유지보수성, 트랜잭션 관리등이 용이 및 향상되었다.
비동기 스케쥴러를 사용하면서 실시간 처리 부담을 줄이고 성능의 최적화와 장애 발생 시 자동 복구 기능을 할 수 있게 되었다.
● 팀 프로젝트를 진행하면서..
초기에 EventListener를 빠르게 적용했어야 했다고 생각한다. 소스를 작성하며 각 비즈니스 로직간의 결합도가 높게 쓰여짐을 뒤늦게 판단하여 넣은 기능인데, 다음 프로젝트 부터는 느슨한 결합도를 위해 조금 더 코드의 설계를 치밀하게 작성해야 겠다고 느끼게 되었다.
'Project' 카테고리의 다른 글
[ Project : 낭비없냉 ] #3 AWS S3 Upload (0) | 2024.09.13 |
---|---|
[ Project : 낭비없냉 ] #2 JPQL (0) | 2024.09.08 |
[ Project : 낭비없냉 ] #1 기획과 설계 (0) | 2024.09.01 |
[ Project : 함께걷개 ] #3 프로젝트를 마치며 (0) | 2024.08.30 |
[ Project : 함께걷개 ] #1 기획과 설계 (0) | 2024.08.10 |