안녕하세요. 그님스에서 일정, 팔로우 도메인을 맡고 있는 이재헌입니다.
오늘은 일정 도메인 기능 중 디데이와 관련 부분에 큰 변화가 있어 공유해보려고 합니다.
목차
1. 기존 d-day 방식
2. 변경된 d-day 방식
3. scheduler 선택 이유
4. d-day 조회 방식 변경 사유
기존 d-day 방식
아래 사진은 현재 그님스의 메인페이지 와이어 프레임입니다.
일정 카드를 보시면 좌측 하단에 디데이가 표기되는걸 볼 수 있습니다. 기존 코드에서 디데이를 계산하는 방식은 아래와 같습니다.
1. DB에서 조회 API에 필요한 Event 테이블 정보 SELECT
2. 메모리 상에서 디데이를 계산하여 프론트 서버로 응답
1. DB에서 Event 테이블의 정보를 가지고 옵니다.
@Query(value = "select new com.gnims.project.domain.schedule.dto.EventAllQueryDto" +
"(e.id, e.appointment.date, e.appointment.time, e.cardColor, " +
e.subject, u.username, u.profileImage) from Schedule s " +
"join s.event e " +
"join s.user u " +
"where e.id in (select e.id from Schedule s2 where s2.user.id =:userId) " +
"and s.isAccepted = true and e.isDeleted = false ")
List<EventAllQueryDto> readAllSchedule(Long userId);
2. 메모리 상에서 디데이를 계산하여 프론트 서버로 응답합니다.
@Getter
public class EventAllQueryDto {
private Long eventId;
private LocalDate date;
private LocalTime time;
private String cardColor;
private String subject;
private Long dDay;
private String username;
private String profile;
public EventAllQueryDto(Long eventId, LocalDate date, LocalTime time, String cardColor, String subject, String username, String profile) {
this.eventId = eventId;
this.date = date;
this.time = time;
this.cardColor = cardColor;
this.subject = subject;
this.dDay = calculateDDay();
this.username = username;
this.profile = profile;
}
private long calculateDDay() {
return ChronoUnit.DAYS.between(LocalDate.now(), date);
}
}
calculateDday()
는 EventAllQueryDto
, EventOneQueryDto
에 정의되어 입니다.
변경된 d-day 방식
변경된 코드에서 디데이를 계산하는 방식은 아래와 같습니다.
1. 이벤트 생성 시, 디데이 계산
2. 매일 0시 마다 d-day를 1씩 차감하는 스케줄러 동작
1. 이벤트 생성 시, 디데이 계산
// .. 이벤트 Entity 중 일부
// 생성자
public Event(Appointment appointment, ScheduleForm form) {
this.appointment = appointment;
this.subject = form.getSubject();
this.content = form.getContent();
this.cardColor = form.getCardColor();
this.isDeleted = false;
this.dDay = calculateDDay();
}
// 디데이 계산 방식
private long calculateDDay() {
return ChronoUnit.DAYS.between(LocalDate.now(), appointment.getDate());
}
2. 매일 0시 마다 d-day를 1씩 차감하는 벌크성 쿼리 및 스케줄러 작성
// 이벤트 리포짓토리
@Transactional
@Modifying(clearAutomatically = true)
@Query("update Event e set e.dDay = e.dDay - 1")
void updateDDay();
JPQL 벌크성 쿼리는 쉽게 SQL의 UPDATE, DELETE 문이라고 합니다.
벌크성 쿼리를 짠 이유는 JPA 변경 감지를 통해 데이터를 업데이트 하려면 업데이트 하려는 레코드만큼 쿼리가 나가야 합니다. 서버가 아야할 것 같다는 생각이 들었어요. 그래서 쿼리 한방으로 데이터를 변경시킬 수 있는 벌크성 쿼리를 사용했습니다.
// 이벤트 스케줄러
@Scheduled(cron = "0 0 0 * * *")
private void updateEventDDay() {
try {
eventRepository.updateDDay();
}
catch (Exception e) {
log.info("[디데이 처리 중 오류가 발생했습니다]");
throw new RuntimeException("디디에 처리 오류 발생");
}
log.info("[디데이 처리가 완료되었습니다]");
}
}
크론 표현식을 통해 매일 0시에 디데이 처리를 해주었습니다.
크론 표현식이 생소하다면 크론표현식 사이트 혹은 CronExpression.java
주석을 참고하시면 좋을 것 같습니다.
스케줄링과 관련해서는 검색을 해보니 기술적 선택지가 다양했습니다.
spring web - scheduler
spring batch
aws lambda
스케줄러를 선택한 이유
가장 강력한 이유는 spring web
만 의존하더라도 쉽고 빠르게 적용할 수 있었기 때문입니다.
spring batch
의 경우, 대량의 데이터를 통계 처리내거나 대용량 레코드를 처리하는 것이 아니라 단순히 벌크성 쿼리가 한 번 실행되면 되는 것이기 때문에 선택에서 제외했습니다. 물론 해당 기술을 학습할 시간이 부족한 것도 맞습니다.
그리고 scheduler
는 인스턴스마다 실행되기 때문에 aws lambda
를 써야되지 않겠냐는 팀원의 제안도 있었지만
schedule lock을 통해 다중 인스턴스에서 스케줄러가 여러번 실행되는 것을 차단할 수 있다는 것을 알게되었기 때문입니다.
d-day 조회 방식 변경 사유
가장 주된 이유는 서버의 메모리를 효율적으로 사용하고 싶었기 때문입니다. d-day의 경우, 스케줄 다건 조회, 단건 조회 API에서 제공합니다. 그리고 두 API는 그님스 앱의 메인 페이지, 상세 페이지 등 트래픽이 가장 많을 것으로 예상되는 곳에서 활용됩니다.
그래서 메모리 상에서 연산을 하는 대신, DB에서 d-day 컬럼을 두고 조회하는 방식과 매일 0시 마다 디데이 계산 작업을 하도록 변경하였습니다.
긴 글 읽어주셔서 감사합니다. 그님스 이재헌이었습니다. :)
'BE 프로젝트 일대기' 카테고리의 다른 글
[BE] 소셜 로그인 (0) | 2023.03.09 |
---|---|
GNIMS - 유저검색하기 (1) | 2023.02.27 |
그님스 BE는 코드 리뷰를 합니다. 그런데 잘되고 있나요...? (0) | 2023.02.26 |
GNIMS - 수락/거절 대기중인 일정 조회 API 최적화 일대기 (0) | 2023.02.22 |
GNIMS - JPA 쿼리 최적화를 해봅시다! (0) | 2023.02.14 |