코드 저장소.

일정충돌 로직을 만들면서 나온 고민들 본문

포폴/일정관리앱

일정충돌 로직을 만들면서 나온 고민들

slown 2025. 5. 11. 22:28

목차

1.문제점

2.고민

3.적용

4.후기

5.향후 개선 예정

 

1.문제점

프로젝트에서 일정 등록 기능을 구현하면서 가장 까다로웠던 부분은 일정 간의 충돌 여부를 어떻게 판별할 것인가였다.

처음에는 단순히 시작시간 < 기존 종료시간 && 종료시간 > 기존 시작시간이면 충돌이라고 생각했지만, 실제 서비스에선 다음과 같은 문제가 발생했다:

  • 하루 종일 일정과 시간대 일정이 겹치는 경우 어떻게 볼 것인가?
  • 이틀 이상 걸친 일정(MULTI_DAY)과 단일 일자의 겹침은 충돌인가?
  • 기존 일정을 수정할 때, 자기 자신도 충돌로 판단되어 등록이 안 되는 문제

결국, 일정이라는 도메인은 단순한 시간 범위의 겹침 문제가 아니라 일정의 “의미와 목적”에 따라 판단 기준이 달라져야 한다는 점을 깨달았다.

2.고민

기존 충돌 로직의 한계를 넘기 위해, 일정 자체에 타입(ScheduleType) 을 부여하기로 결정했다. 각 일정은 아래처럼 분류된다.

유형설명예시
유형 설명  예시
ALL_DAY 하루  전체를 점유 2025-05-11 00:00~23:59
SINGLE_DAY 같은 날의 특정 시간대 2025-05-11 14:00 ~ 16:00
MULTI_DAY 여러 날에 걸친 일정 2025-05-11 15:00 ~ 2025-05-13 09:00

→ 이 분류를 기준으로 충돌 판단 로직도 분리 처리한다는 원칙을 세웠다.

또한, 다음과 같은 세부 고민이 있었다:

  • 하루 종일 일정은 시간대 일정과 병렬로 존재할 수 있는가?
  • 병렬 표시 가능한 일정은 충돌로 볼 수 있을까?
  • 추후 캘린더 UI 확장 시 어떤 정보가 필요할까?

3.적용

설계상의 고민을 코드로 풀어내기 위해, 일정 등록 시 충돌 검사를 다음과 같은 흐름으로 구현했다.

 

3-1. 일정 등록 시 충돌 검사 전체 로직

public void validateScheduleConflict(SchedulesModel model) {
    // 1. 시작시간 < 종료시간 확인
    if (!model.getStartTime().isBefore(model.getEndTime())) {
        throw new ScheduleCustomException(ScheduleErrorCode.INVALID_TIME_RANGE);
    }

    // 2. 하루 종일 일정이면 날짜 단위 충돌만 검사하고 종료
    if (Boolean.TRUE.equals(model.isAllDay())) {
        validateAllDayScheduleConflict(model);
        return;
    }

    // 3. 시간대 충돌 검사 (단일 일자 일정만 해당)
    if (model.getScheduleType() == ScheduleType.SINGLE_DAY) {
        Long conflictCount = scheduleRepository.countOverlappingSchedules(
            model.getUserId(),
            model.getStartTime(),
            model.getEndTime(),
            model.getId() // 수정일 경우 자기 자신 제외
        );

        if (conflictCount != null && conflictCount > 0) {
            throw new ScheduleCustomException(ScheduleErrorCode.SCHEDULE_TIME_CONFLICT);
        }
    }

    // MULTI_DAY는 현재는 제외 (추후 확장)
}

 

이 로직은 단순히 시간 겹침 여부를 판단하는 게 아니라, 일정 유형에 따라 분기하여 충돌 기준을 다르게 적용하는 구조다.

 

3-2. 하루 종일 일정의 충돌 검사

 

하루 종일 일정은 해당 날짜 전체를 점유하므로, 동일 날짜에 이미 하루 종일 일정이 있으면 충돌로 간주한다.

public void validateAllDayScheduleConflict(SchedulesModel model) {
    LocalDate date = model.getStartTime().toLocalDate();
    Long count = scheduleRepository.countAllDayOnDate(model.getUserId(), date);

    if (count != null && count > 0) {
        throw new ScheduleCustomException(ScheduleErrorCode.SCHEDULE_TIME_CONFLICT);
    }
}

 

toLocalDate()를 사용해 시간 정보는 제거하고 날짜만 비교하는 점이 핵심이다.

 

3-3. 시간대 겹침 쿼리 – SINGLE_DAY 일정만 대상

@Query("""
    SELECT COUNT(s)
    FROM Schedules s
    WHERE s.userId = :userId
      AND s.scheduleType = 'SINGLE_DAY'
      AND (:startTime < s.endTime AND :endTime > s.startTime)
      AND (:excludeId IS NULL OR s.id != :excludeId)
""")
Long countOverlappingSchedules(@Param("userId") Long userId,
                               @Param("startTime") LocalDateTime startTime,
                               @Param("endTime") LocalDateTime endTime,
                               @Param("excludeId") Long excludeId);

 

해당 쿼리는 충돌기준을 startTime < 기존 endTime AND endTime > 기존 startTime으로 했습니다. 그리고 자기 자신은 제외를 했고 scheduleType = 'SINGLE_DAY'만 검사로 했습니다.

 

3-4. 하루 종일 일정 중복 검사 쿼리

@Query("""
    SELECT COUNT(s) FROM Schedules s
    WHERE s.userId = :userId
      AND s.isAllDay = true
      AND DATE(s.startTime) = :date
      AND s.isDeletedScheduled = false
""")
Long countAllDayOnDate(@Param("userId") Long userId, @Param("date") LocalDate date);

 

 

 

위 쿼리는 하루종일로 되어 있는 일정에 날짜(s.startTime)만을 비교하고 논리 삭제된 일정은 제외를 했습니다. 그리고 하루종일의 일정이므로 isAllDay를 true인 일정만 검사를 합니다.

 

3-4. MULTI_DAY 일정 – 아직 미지원

 

검사를 하지 않은 이유는  다음과 같습니다. 

며칠에 걸친 일정의 경우에는 병렬로 표시를 해야되기 때문이고 특히 화면에 보여질때 단순 차단보다는 좀 더 복잡한 로직이 들어갈것 같은 판단이 들었습니다. 

 

 

4.후기

이번 충돌 로직을 만들면서 가장 크게 느낀 점은 다음과 같습니다.

 

4-1.일정을  생성을 하는데 있어서 단순히 시간이 겹친다고 해서 '충돌'이 아니고 일정이 가진 의도와 유형에 따라서 겹침은 허용이 될수도 있고 안될수도 있다는 점이다.

 

4-2.기술적으로는 단순 쿼리지만, 그 안에는 도메인 해석과 서비스 정책에 대한 판단이 있어야 한다는 점을 알게 되었습니다. 특히 ScheduleType과 같은 분류는 단순 enum이 아니라, 도메인 기반 로직 분기 처리의 핵심 열쇠가 되었고 확장 가능성과 유연성을 유지하면서 서비스의 정확도를 높이는 데 크게 기여했습니다.

 

5.향후 개선 예정

  • MULTI_DAY 일정 충돌 판단 로직 추가
  • Redis 캐시 or 인메모리로 빠른 충돌 탐색 기능 도입