Dayner 영업시간 디자인 변경에 따른 리팩터링 일지 [1]
개요#
Dayner에는 매달 정기 휴무일, 연중 휴일, 급한 일정으로 인한 운영시간 변경 등을 등록하는 기능이 있었습니다. 처음엔 그냥 날짜와 설명 정도만 보여주면 되는 화면이라 큰 고민 없이 갔습니다.
초기 디자인에서는 대부분 매주 월요일 정기 휴무만 표시하고 있었기 때문에, 영업시간 변경/휴일을 표시하는 type, date, description 필드만 있으면 충분했습니다.
~/yyyyMM 형식의 엔드포인트로 API 요청을 하면 영업일정(id, type, date, description)을 담은 DB에서 between 쿼리로 해당 월의 첫날~말일 데이터를 반환하는 구조였습니다.
문제는 디자인이 바뀌면서 시작됐습니다. 우측 사진의 24~25일처럼 연속된 일정 컴포넌트를 만들기 위해 프론트에서 과도한 자원을 소모한다는 의견이 제시된 것입니다.
변화가 적고 GET 요청이 대부분인 도메인 특성을 고려하면, 서버에서 프론트가 표현하기 쉬운 형태로 데이터를 가공한 뒤 캐싱으로 제공하는 방식이 가장 효율적이라고 판단해 여러 방안을 검토했습니다.
아이디어 1#
현재 기능을 유지하되, 같은 description 의 경우에는 date에 리스트 형식으로 표현합니다.
해당 기능을 담당했던 개발자분이 연속된 디자인을 구현하기 위해 description 비교로 리스트를 만들었는데, 이 로직을 서버로 옮기면 어떻겠냐는 아이디어를 제시했습니다.
장점: DB 마이그레이션이 필요하지 않습니다. 주어진 api 에 충실한 구현. 단점: 리스트를 만드는 과정에서 연속되어있는지, description이 같은지를 검증해보아야하기에 자원이 낭비됩니다.
서버 측 O(N), 프론트 측 O(N)
아이디어 2#
db 마이그레이션을 통해 startDate, endDate 속성을 추가합니다.
장점: 직관적인 data, 서버단에서 추가적인 작업을 할 필요가 없습니다. 단점: DB 마이그레이션이 이루어져야합니다! 또한 Between 쿼리를 이용하지 못해서 full scan이 이루어집니다.
서버 측 O(N), 프론트 측 O(1)
아이디어 3#
hasNext Boolean 옵션을 추가하여 연결 디자인에 대응합니다.
장점: DB 마이그레이션이 필요하지 않습니다. 단점: head, tail 에 대한 구분을 하기 위한 로직이 필요합니다.
서버 측 O(N), 프론트 측 O(N)
아이디어 4#
결국 우리가 필요한건? 타입을 나누어서 생각해보자.
연속된 일정을 위한 head와 tail 그리고 mid가 있겠습니다. 단일 일정을 위한 single 타입이 있고, 이 모든게 영업시간 변경/휴일 인지로 크게 나뉘어집니다.
blockType이라는 새로운 속성을 만들어서 각 일정의 날짜가 어떤 블록 타입(start, middle, end, single)인지 서버에서 판단한 뒤, 이 정보를 포함한 date 목록을 클라이언트로 전달하는 방식입니다.
장점: DB 마이그레이션 불필요, blockType으로 쉽게 판별, 프론트 로직 불필요. 서버 측 O(N), 프론트 측 O(1)
아이디어 5#
저장 시점에 blockType도 함께 저장하면 어떨까? (DB에 속성 추가) → 일정에 변화가 생기면 변경된 일정 앞뒤의 blockType을 모두 수정해야 하는 문제가 있습니다.
요청직전에 계산 + 캐싱 기능을 이용하자!
- 변화가 없다면 요청마다 계산을 하지 않아도 되고
- 캐싱 전략의 경우에는 GET 요청에 yyyyMM 파라미터를 이용해 저장해두고 yyyyMM에 생성/업데이트/삭제가 수행될 경우에는 캐시 evict을 수행하는 방식
여기까지 비교해보니 결국 중요했던 건 "어디서 계산하는 게 더 맞는가"였습니다. 서버에서 조금 더 가공해서 내려주면 프론트가 훨씬 단순해질 수 있었고, 반대로 서버 효율만 보다가 프론트에 부담을 미루는 선택도 얼마든지 할 수 있었습니다.
이번 리팩터링은 그 균형을 다시 보게 만든 작업이었습니다. 시스템 특성과 요청 패턴을 같이 놓고 보지 않으면, 한쪽 최적화가 다른 쪽 비용으로 돌아온다는 걸 꽤 분명하게 느꼈습니다.