본문 바로가기
BE 프로젝트 일대기

GNIMS - 디데이 계산 방식이 변경됩니다!

by 포도자몽 2023. 2. 16.

안녕하세요. 그님스에서 일정, 팔로우 도메인을 맡고 있는 이재헌입니다.

오늘은 일정 도메인 기능 중 디데이와 관련 부분에 큰 변화가 있어 공유해보려고 합니다.

 

목차

 

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시 마다 디데이 계산 작업을 하도록 변경하였습니다.

 

 

긴 글 읽어주셔서 감사합니다. 그님스 이재헌이었습니다. :)