[unispace] 시설 예약 도메인 요구사항 설계 및 구현

Deeeep Breath·2024년 7월 26일

unispace

목록 보기
3/12
post-thumbnail

시설 예약 요구사항 분석 및 구현

시간과 관련된 예약 조건을 어떻게 구현하면 좋을지 고민이 많았었다.
일단 다음과 같은 요구사항이 있다고 가정하고 설계를 시작했다.

시용자는 시스템 클라이언트에서 사용을 원하는 시설을 예약 할 수 있다.

사용을 원하는 날짜, 사용을 원하는 시간을 설정할 수 있다.

관리자가 시설의 시작시간, 종료시간, 지정시간을 설정하면 자동으로 예약 가능한 시간대가 생성되도록 하는 것이다.

1. 엔티티 설계

위 요구사항을 구현하기 위해선 4가지의 데이터베이스 테이블이 필요하다고 생각했다.

  1. 시설의 정보를 담고 있는 테이블
    시설의 이름, 시설의 위치, 보유하고 있는 기자재들, 시설에 대한 설명
  2. 시설의 예약 조건들을 담고 있는 테이블
    시설의 예약 가능한 시작 시간, 끝 시간, 학과/학부/학년에 따른 제약 등
  3. 예약 시간 조건과 관련한 테이블
    예약 가능한 시간대(시작시간, 끝시간)
  4. 사용자의 예약 정보를 담고 있는 테이블
    예약 날짜, 예약 시간, 예약한 시설, 같이 예약한 친구 등

시설의 일반적인 데이터와 예약 조건과 관련된 데이터는 둘 다 시설에 대한 데이터라는 공통점이 있지만 다른 속성을 가지고 있으므로 정규화 하는 것이 적절하다고 판단했다.

예약 시간 조건과 관련된 데이터를 어떤 방식으로 저장할 것인지 고민이 많았다. 고민 끝에 한가지 방법을 떠올렸다.

이 방법의 가장 큰 문제점은 ReservationPolicy가 생성될 때마다 시간 조건을 가지고 있는 ReservationTimeSlot이 여러개 생성된다는 점이다. 즉 같은 시간대를 나타내는 ReservationTimeSlot이 중복되어 생성된다.

중복되는 시간대를 제거하는 코드를 작성하거나 timeslot을 미리 생성한 뒤 이를 참조하는 방법을 사용할 수도 있지만 요구사항에 따라 결국에는 생성되는 timeslot의 개수 만큼의 인스턴스가 생성될 수 밖에 없다.

예약 시간 조건과 관련된 엔티티 최종 설계

2. 구현

@Entity
@Getter
@NoArgsConstructor
public class Room extends BaseEntity {
    ...
    @OneToOne(mappedBy = "room", cascade = CascadeType.ALL)
    private ReservationPolicy reservationPolicy;
    ...
}

@Entity
@Getter
@NoArgsConstructor
public class ReservationPolicy extends BaseEntity {
    @Id
    @GeneratedValue
    private Long id;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "ROOM_ID")
    @JsonIgnore
    private Room room;

    // 예약 시 관리자의 승인이 필요한가?
    private boolean requireApproval;
    // 한번에 예약할 수 있는 시간 => timeslot 자동 생성시 사용
    private Integer maxReservationHours;
    // 시설 예약 시작 시간
    private LocalTime openTime;
    // 시설 예약 종료 시간
    private LocalTime reserveCloseTime;
    // 며칠 전부터 예약이 가능한지
    private Integer minDaysBeforeReservation;

    // 예약 가능 시간대
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "reservationPolicy", 
    			cascade = CascadeType.ALL, orphanRemoval = true)
    private List<ReservationTimeSlot> timeSlots = new ArrayList<>();
    ...
}

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ReservationTimeSlot extends BaseEntity {
    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "POLICY_ID")
    private ReservationPolicy reservationPolicy;

    private LocalTime startTime;
    private LocalTime endTime;
    ...
 }
 
@Entity
@Getter
@NoArgsConstructor
public class Reservation extends BaseEntity {
    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "USER_ID")
    private User reservedBy;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "ROOM_ID")
    private Room room;

    private LocalDate reservationDate;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TIME_SLOT_ID")
    private ReservationTimeSlot timeSlot;
    ...
 }

3. 비즈니스 로직 구현

ReservationPolicy에서 openTime과 reserveCloseTime, maxReservationHours을 이용해서 TimeSlot을 생성하는 메서드를 작성했다.

public void generateTimeSlot() {
        timeSlots.clear();
        LocalTime currentTime = openTime;

        if (reserveCloseTime.equals(LocalTime.of(0, 0, 0))) {
            reserveCloseTime = LocalTime.of(23, 59, 59); // 자정은 하루의 끝으로 설정
        }

        while ((currentTime.isBefore(reserveCloseTime) || currentTime.equals(reserveCloseTime))) {
            if (currentTime.plusHours(maxReservationHours).isBefore(currentTime)) break;

            LocalTime endTime = currentTime.plusHours(maxReservationHours);

            if (endTime.isAfter(reserveCloseTime)) {
                endTime = reserveCloseTime;
            }

            if (currentTime.equals(endTime) || endTime.equals(LocalTime.of(0, 0, 0))) break;

            ReservationTimeSlot timeSlot = ReservationTimeSlot.createTimeSlot(this, currentTime, endTime);
            timeSlots.add(timeSlot);
            currentTime = endTime;

        }
        
        if(reserveCloseTime.equals(LocalTime.of(23, 59, 59))) {
            ReservationTimeSlot timeSlot = ReservationTimeSlot.createTimeSlot(this, currentTime, LocalTime.of(0, 0, 0));
            timeSlots.add(timeSlot);
        }
        
        timeSlots.sort(Comparator.comparing(ReservationTimeSlot::getStartTime));

    }

4. 마무리

예약 시스템이라는 도메인을 처음 다루면서, 데이터베이스 설계부터 엔티티 구조화, 비즈니스 로직 개발 까지 모든 과정을 직접 해보는니 생각보다 힘들었다.

설계를 마친 뒤 구현하는 과정에서 느꼈던 가장 큰 어려움은 코드를 작성하는 것 자체가 아니라 도메인 설계의 적절성에 대한 확신이 없다는 점이었다. 이전에 fastapi를 이용한 간단한 벡엔드 프로젝트를 경험해본 적이 있었지만, 스프링을 공부한 뒤 진행하는 첫번째 프로젝트라서 내가 설계한 데이터베이스 구조와 엔티티 관계가 최적인지, 객체 지향적으로 올바르게 설계되었는지, 실제 운영 환경에 적합한 설계인지에 대한 의문이 끊임없이 들었다.

이러한 불확실성 속에서도 포기하지 않고 계속해서 학습하고 개선해 나갔다. 깃허브에 있는 다른 프로젝트들에서 비슷한 도메인을 어떻게 설계했는지 코드를 분석하고, 문서들을 찾아보며 더 나은 설계 방법을 고민했다. 이 과정에서 JPA와 관계형 데이터베이스 모델링에 대해 깊이 있게 공부할 수 있었고, 시간 데이터 처리와 관련된 Java의 LocalTime, LocalDate 클래스 사용법도 익힐 수 있었다.

여러 코드들을 보면서 느낀 점은 프로젝트에서 마주치는 문제들에는 교과서적인 해답이 없다는 것이다. 프로젝트의 현재 상황에 맞는 최선의 방법을 찾아 구현하고, 이후 지속적으로 개선해 나가는 것이 중요하다 사실을 배웠다.

내 설계와 구현이 최선인지에 대한 끊임없는 의문과 고민이 개발자로서 성장할 수 있는 원동력이 된다고 믿는다.

프로젝트 깃허브 주소 : https://github.com/DHkimgit/unispace

profile
안녕하세요!

0개의 댓글