서버 구현 챕터 : 회고 - leonroars/slam GitHub Wiki
들어가며.
이번 프로젝트를 진행하면서 다양한 선택지들 사이에서 고민하는 과정이 꽤 흥미로웠습니다. 그 중 가장 오랜 시간을 빼았겼으며 설날 연휴 동안 과제에 대한 부담은 잠시 내려놓고 매섭게 달려들어 볼 주제를 몇 가지 소개해보려 합니다.
예약 가능 공연 일정 목록 조회 : 외래 키와 조인 없이 효율적으로 해결할 수 있을까?
“예약 가능 공연 일정 목록 조회” 는 처음부터 상당히 까다롭게 느껴졌던 부분입니다. 이번 프로젝트에서는 학습 목적으로 도메인 모델과 JPA Entity를 완전하게 분리하여 구현하는 방향을 선택함과 더불어 모든 외래 키와 JPA 에서 지원해주는 연관관계 매핑과 이를 바탕으로 한 지연 로딩과 같은 편의 기능을 배제했기 때문에 더욱 어렵게 느껴졌던 것 같습니다.
우선 '예약 가능 공연' 이라 함은 크게 두 가지 조건을 만족해야 합니다.
- 예약 시도 시점(현재) 기준 시간 상 예약 가능할 것.
- 공연 일정(
ConcertSchedule
) 테이블에는 해당 공연 일정에 대한 예약이 시작되는 일자와 종료되는 일자가 있었습니다. - 따라서 예약을 시도하는 시점은 바로 이 두 일자 사이에 위치해야 예약이 가능한 것입니다.
- 시점 상 예약 가능한 공연 일정에 대해, 가용한 좌석이 존재할 것.
- 위에서 일정 기준으로 한 번 공연 일정에 대한 필터링을 했다면 다시, 이에 대해 '가용 좌석 존재 여부'를 기준으로 한 번 더 걸러낼 필요가 있습니다.
도메인 모델도, 테이블 설계 상으로도 ConcertSchedule
과 Seat
은 분명하게 나누어져야 하고 실제로 그렇게 설계 했습니다.
도메인 주도 개발을 익혀보자는 취지로 이에 기반한 설계 및 개발을 하다보니 쉽사리 이 두 가지 도메인 모델을 함께 활용하여 원하는 결과를 도출하는 방법을 생각해내기가 쉽지 않았습니다.
처음 떠올렸던 방식은 양쪽의 테이블로부터 각각의 조건에 부합하는 List<ConcertSchedule>
두 가지를 받아 애플리케이션 수준에서 교집합을 생성해 반환하는 것이었습니다.
하지만 피드백 이후 다시 고민한 결과, 이러한 구현이 N+1 문제를 반드시 야기한다는 것을 뒤늦게 알게 되었습니다.
이후 떠올린 방법은 두 가지였습니다.
1.ConcertSchedule
에게 'available_seat_count필드 정의 2. '예약 가능 공연 목록 조회' 요청 시
Seat테이블에 대해
ConcertScheduleId` 와 관련된 좌석의 수를 COUNT 하는 방식
첫 번째 방식은 어떤 좌석에 대한 상태 변경(사용 가능
<-> 사용 불가
) 발생 할 때마다 ConcertSchedule
테이블의 available_seat_count
갱신이 원자적으로 이루어지도록 보장해야 합니다. 따라서 동시성 제어가 구현되어야 하고, 이로 부터 발생 가능한 오버 헤드 또한 무시할 수 없습니다.
두 번째 방식을 선택할 경우, COUNT
로 인해 발생하는 오버헤드가 걱정되었습니다. 하지만 구현 난이도가 낮고 복잡한 동시성 제어 구현에 대한 부담이 낮아 결과 예측이 용이하다는 장점이 있었습니다.
결론적으로 두 가지 모두 서로에 비해 뾰족하게 우월하지 않아 보였고 저는 조금 더 도전적인 방향으로 나아가보기로 결정하여, 첫 번째 방식을 선택하여 구현하였습니다. 하지만 여전히, 어떤 상황(동시 요청 사용자의 규모 수, 서버 구축 환경 등)에 따라 상대적인 이용 가치가 달라진다는 점은 유효하기에 추가적인 조사를 해보고 싶습니다.
만료된 예약 : 삭제하는가, 만료 처리 후 해당 좌석에 대한 재예약이 가능하도록 하는가?
또한 ReservationStatus를 ‘만료’ 상태로 변경했을 때, 삭제와 ‘만료 표시’ 후 재예약 가능 상태로 만드는 것 중 어떤 접근이 나을지 고민도 컸습니다. 삭제해버리면 테이블이 깔끔해지지만 과거 기록이 필요할 경우 추적이 어려울 수 있고, 단순히 만료 상태만 표시해두면 중복 데이터가 쌓여 쿼리 효율성에 영향을 줄 수 있습니다. 인덱스를 잘 설계한다면 만료된 예약이 있어도 조회 성능을 충분히 유지할 수 있겠지만, 만료 예약 건이 상당히 많아질 시엔 고민이 더 필요하겠다고 느꼈습니다.
Token
모델의 expiredAt 설정 방식
도메인 모델 Service Layer에서 만료 시간을 세팅하면 트랜잭션 내에서 한눈에 로직을 파악할 수 있다는 장점이 있었지만, 엔티티 라이프사이클 콜백인 @PostPersist에서 처리하고 싶을 때는 콜백 내에서 엔티티를 다시 저장해야 한다는 점이 조금 번거로웠습니다.
중복 예약 방지 방안 : DB 수준? 애플리케이션 수준? 혹은 둘 다!?
마지막으로, 중복 예약 방지에 대해서도 애플리케이션 레벨에서 예약 생성 시 논리적으로 체크하는 것과 DB 레벨에서 UNIQUE 제약 조건을 통한 중복 방지를 병행하는 방식을 택했습니다. 결국 ‘이중 방어’ 구조가 안전하긴 하지만, 두 곳 모두에 로직이 분산되다 보니 유지보수 시 신경 써야 할 지점이 늘어난 느낌이었습니다. 그래도 실제 운영 상황에서는 예기치 못한 동시성 이슈나 로직 허점을 DB가 한 번 더 막아줄 수 있다는 점이 결정에 도움이 되었습니다.
프로젝트 전체적으로 “성능과 데이터 정합성 사이의 균형”을 잡으려는 시도가 가장 인상 깊었습니다. 선택지마다 장단이 분명했기 때문에 ‘무엇이 정답이다’ 보다는 “현재 우리에게 중요한 우선순위는 무엇인지”를 고민하게 됐습니다. 이러한 과정을 통해 설계 시 트레이드오프를 명확히 인지하고, 필요 시 리팩토링할 여지를 남겨두는 것이 중요하다는 점을 배웠습니다.