Troubleshooting: 무엇이 문제였는가?/본캠프 5주 차: Spring 심화 프로젝트

6단계: 일정이 반복된다면? (수정 중)

writingforever162 2025. 1. 6. 13:42

1. [문제 인식 및 정의]

현재 일정은 하루 단위로만 생성할 수 있어 사용자는 반복되는 일정을 매번 새로 추가해야 하는 불편함을 겪는다. 예를 들어, 매주 수요일마다 팀 회의가 있다면 이를 수동으로 반복 입력해야 하며, 이 과정에서 사용자가 실수로 일정을 빠뜨리거나 잘못 입력해 중요한 일정을 놓칠 수 있다. 이러한 비효율성과 실수 가능성을 문제로 인식했다.

 

2. [해결 방안]

2-1. [의사결정 과정]

1안: 수동 날짜 계산

(1) 장점

- 구현이 간단하고 직관적이다.

- 외부 라이브러리(library) 없이 직접 구현할 수 있다. 

- 사용자가 직접 반복 주기를 설정하고 날짜를 계산하는 방식으로 유연하게 처리할 수 있다.

(2) 단점
- 반복 주기를 추가하면 복잡도가 증가하며, 코드가 길어질 수 있다. 
- 사용자가 반복 주기를 잘못 설정하거나 실수할 가능성이 있다.

(3) 특징

- 작은 규모의 프로젝트에 적합하다.

 

2안: Cron 표현식
(1) 장점
- 주기적인 작업을 정의하는 데 유용하다.

- 복잡한 반복 주기를 간결하게 처리할 수 있다.
- Spring에서 @Scheduled 어노테이션을 사용하여 효율적으로 작업을 스케줄링할 수 있다.
(2) 단점
- 일정 자체가 저장되는 방식은 아니어서 일정을 등록하고 반복 주기를 관리하는 부분을 잘 결합해야 한다.
- 복잡한 일정 관리에는 부적합할 수 있다.

(3) 특징

- 매일, 매주, 매월 등 다양한 주기를 쉽게 처리할 수 있다. 

- 시스템에 따라 실행 환경을 설정해야 한다. 

 

3안: 스케줄러 (Quartz, Spring Scheduler)
(1) 장점
- 복잡한 작업 스케줄링과 분산 처리를 지원한다.
- Spring 환경에 잘 통합되어 관리하기 쉽다.

- 고성능 백엔드 환경에서 효율적이다. 
(2) 단점
- 일정이 실제로 저장되고 사용자에게 보이는 방식과의 연계를 잘 설계해야 한다.
- 추가적인 설정과 관리가 필요할 수 있다.

- 초기 설정과 관리가 복잡하다. 

(3) 특징

- 장기적으로 스케줄링 관련 작업이 많을 때 유리하다. 

 

2-2. [해결 과정]

(1) 의사 결정

고민 끝에 1안을 선택했다. 현재 다른 라이브러리를 활용하기에는 공부해야 할 부분이 많고, 지금 진행 중인 개인 과제 규모가 작은 데다 마감이 월요일 오후 2시까지였기 때문에 기능을 빠르게 구현할 수 있는 1안을 골랐다. 수동으로 기능을 구현하면서 불편함을 느껴야 추후 2안이나 3안을 적용해 볼 수 있겠다는 판단도 들었다.

 

(2) 구현 과정 

Q1. Todo 엔티티(entity)가 이미 존재하는데, 별도로 엔티티를 만들어야 할까?

A1. 포함되는 필드(field), 즉 속성 구성이 달라서 별도의 엔티티로 구현했다. 각각 엔티티를 생성하는 방향이 확장성과 유지 보수 면에서 더 좋을 듯했다.

더보기
package org.example.expert.common.entity;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@NoArgsConstructor
@Table(name = "todos")
public class Todo extends Timestamped {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String title;

  private String contents;

  private String weather;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(
      name = "user_id",
      nullable = false
  )
  private User user;

  @OneToMany(
      mappedBy = "todo",
      cascade = CascadeType.REMOVE
  )
  private List<Comment> commentList = new ArrayList<>();

  @OneToMany(
      mappedBy = "todo",
      cascade = CascadeType.PERSIST
  )
  private List<Manager> managerList = new ArrayList<>();

  public Todo(
      String title,
      String contents,
      String weather,
      User user
  ) {
    this.title = title;
    this.contents = contents;
    this.weather = weather;
    this.user = user;
    this.managerList.add(new Manager(user, this));
  }
}
더보기
package org.example.expert.common.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import lombok.Getter;
import org.example.expert.common.enums.DayOfWeek;

@Getter
@Entity
@Table(name = "recurring_todos")
public class RecurringTodo extends Timestamped {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String title;

  private String contents;

  private String weather;

  private String repeatInterval;

  private LocalDateTime startedAt;

  private LocalDateTime endedAt;
  
  @Enumerated(EnumType.STRING)
  private DayOfWeek dayOfWeek;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(
      name = "user_id",
      nullable = false
  )
  private User user;

  protected RecurringTodo() {
  }

  public RecurringTodo(
      String title,
      String contents,
      String weather,
      String repeatInterval,
      LocalDateTime startedAt,
      LocalDateTime endedAt,
      DayOfWeek dayOfWeek
      User user
  ) {
    this.title = title;
    this.contents = contents;
    this.weather = weather;
    this.repeatInterval = repeatInterval;
    this.startedAt = startedAt;
    this.endedAt = endedAt;
    this.dayOfWeek = dayOfWeek;
    this.user = user;
  }
}

반복 일정 생성 기능은 금방 끝냈다. 문제는 이 기능을 사용자가 어떻게 사용하도록 할지 정하지 않았다는 데에 있었다. CRUD의 C를 구현한 다음에는 꼬리에 꼬리를 무는 고민과 문제를 하나씩 해결해 나갔다.

 

Q2. 사용자가 5주 동안 매주 수요일 2시에 회의가 있다고 가정하자. 문제는 특정 일정의 시간이 바뀔 수도 있고, 회의 내용이 바뀔 수도 있다. 이 문제는 어떻게 해결해야 좋을까?

A2. 사용자가 일정을 생성할 때 몇 번 반복되는지도 같이 입력하도록 수정했다. 이 값을 정수형 int로 받은 다음, 반복문을 돌려서 입력된 횟수만큼 데이터베이스에 일정이 저장되도록 전체 흐름과 엔티티(Entity), DTO를 모두 수정했다. 이렇게 전체 흐름을 다듬으면, 일정마다 고유 식별자가 생기니까 사용자가 이 기능을 제대로 쓸 수 있을 듯했다.

더보기
package org.example.expert.common.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import lombok.Getter;
import org.example.expert.common.enums.DayOfWeek;

@Getter
@Entity
@Table(name = "recurring_todos")
public class RecurringTodo extends Timestamped {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String title;

  private String contents;

  private String weather;

  private String frequency;

  private LocalDateTime startedAt;

  private LocalDateTime endedAt;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(
      name = "user_id",
      nullable = false
  )
  private User user;

  private int repeatCount;

  @Enumerated(EnumType.STRING)
  private DayOfWeek dayOfWeek;

  protected RecurringTodo() {
  }

  public RecurringTodo(
      String title,
      String contents,
      String weather,
      User user,
      String frequency,
      LocalDateTime startedAt,
      LocalDateTime endedAt,
      DayOfWeek dayOfWeek,
      int repeatCount
  ) {
    this.title = title;
    this.contents = contents;
    this.weather = weather;
    this.user = user;
    this.frequency = frequency;
    this.startedAt = startedAt;
    this.endedAt = endedAt;
    this.dayOfWeek = dayOfWeek;
    this.repeatCount = repeatCount;
  }
}
더보기
package org.example.expert.domain.recurring.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;

public record CreateRecurringTodoRequestDto(
    @NotBlank
    String title,

    @NotBlank
    String contents,

    @NotNull
    LocalDateTime startedAt,

    @NotNull
    LocalDateTime endedAt,

    @NotNull
    String dayOfWeek,

    @NotNull
    int repeatCount
) {

}
더보기
package org.example.expert.domain.recurring.dto.response;

import java.time.LocalDateTime;
import org.example.expert.common.enums.DayOfWeek;
import org.example.expert.domain.user.dto.response.UserResponseDto;

public record CreateRecurringTodoResponseDto(
    Long id,
    String title,
    String contents,
    String weather,
    String frequency,
    LocalDateTime startedAt,
    LocalDateTime endedAt,
    DayOfWeek dayOfWeek,
    int repeatCount,
    UserResponseDto user
) {

}
더보기
package org.example.expert.domain.recurring.controller;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.example.expert.common.annotation.Auth;
import org.example.expert.domain.auth.dto.AuthUserDto;
import org.example.expert.domain.recurring.dto.request.CreateRecurringTodoRequestDto;
import org.example.expert.domain.recurring.dto.response.CreateRecurringTodoResponseDto;
import org.example.expert.domain.recurring.service.RecurringTodoService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/todos/repeated")
@RequiredArgsConstructor
public class RecurringTodoController {

  private final RecurringTodoService recurringTodoService;

  @PostMapping
  public ResponseEntity<CreateRecurringTodoResponseDto> createRecurringTodo(
      @Auth AuthUserDto authUserDto,
      @Valid @RequestBody CreateRecurringTodoRequestDto requestDto
  ) {
    CreateRecurringTodoResponseDto responseDto = recurringTodoService.createRecurringTodo(
        authUserDto,
        requestDto
    );

    return new ResponseEntity<>(responseDto, HttpStatus.CREATED);
  }
}
더보기
package org.example.expert.domain.recurring.service;

import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.example.expert.common.entity.RecurringTodo;
import org.example.expert.common.entity.User;
import org.example.expert.common.enums.DayOfWeek;
import org.example.expert.common.weather.WeatherClient;
import org.example.expert.domain.auth.dto.AuthUserDto;
import org.example.expert.domain.recurring.dto.request.CreateRecurringTodoRequestDto;
import org.example.expert.domain.recurring.dto.response.CreateRecurringTodoResponseDto;
import org.example.expert.domain.recurring.repository.RecurringTodoRepository;
import org.example.expert.domain.user.dto.response.UserResponseDto;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class RecurringTodoService {

  private final RecurringTodoRepository recurringTodoRepository;
  private final WeatherClient weatherClient;

  @Transactional
  public CreateRecurringTodoResponseDto createRecurringTodo(
      AuthUserDto authUserDto,
      CreateRecurringTodoRequestDto requestDto
  ) {
    User userFromAuth = User.fromAuthUser(authUserDto);

    String foundWeather = weatherClient.getTodayWeather();

    DayOfWeek dayOfWeek = DayOfWeek.of(requestDto.dayOfWeek());

    String frequency = "WEEKLY";

    if (dayOfWeek == DayOfWeek.DAILY) {
      frequency = "DAILY";
    }

    List<RecurringTodo> recurringTodoList = new ArrayList<>();

    for (int i = 0; i < requestDto.repeatCount(); i++) {
      RecurringTodo recurringTodo = new RecurringTodo(
          requestDto.title(),
          requestDto.contents(),
          foundWeather,
          userFromAuth,
          frequency,
          requestDto.startedAt().plusWeeks(i),
          requestDto.endedAt().plusWeeks(i),
          dayOfWeek,
          requestDto.repeatCount()
      );
      recurringTodoList.add(
          recurringTodoRepository.save(recurringTodo)
      );
    }

    RecurringTodo savedRecurringTodo = recurringTodoList.get(
        recurringTodoList.size() - 1
    );

    return new CreateRecurringTodoResponseDto(
        savedRecurringTodo.getId(),
        savedRecurringTodo.getTitle(),
        savedRecurringTodo.getContents(),
        savedRecurringTodo.getWeather(),
        savedRecurringTodo.getFrequency(),
        savedRecurringTodo.getStartedAt(),
        savedRecurringTodo.getEndedAt(),
        savedRecurringTodo.getDayOfWeek(),
        savedRecurringTodo.getRepeatCount(),
        new UserResponseDto(
            userFromAuth.getId(),
            userFromAuth.getEmail()
        )
    );
  }
}
더보기
package org.example.expert.domain.recurring.repository;

import org.example.expert.common.entity.RecurringTodo;
import org.springframework.data.jpa.repository.JpaRepository;

public interface RecurringTodoRepository extends JpaRepository<RecurringTodo, Long> {

}

 

 

Q3. todo와 어떻게 조회를 한꺼번에 해줄까? 

A3. 

 

3. [해결 완료]

3-1. [회고]

3-2. [전후 데이터 비교]