변경내역 남기기 (Changelogs in ZARI)

안녕하세요, 자리를 만들고 있는 개발자 이진혁입니다.

자리를 만들며 겪어온 재미난 시도와 도전들을 주변분들끼리만 나누기 아쉬워서 이제 하나씩 풀어보려고합니다.

이것을 2년째 하고 있지만 자꾸 건망증도 심해지는 것 같고 이러다 지금의 가슴 뛰는 순간을 글로 적어놓지 않아서 까먹으면 안되는데… 무서운 생각이 들어 큰 마음을 먹고 적어내려갑니다.

10명도 안되는 아주 작은 팀이 도대체 무엇을 하길래 국내 십년넘은 호텔솔루션회사를 이겨가고 있고 더불어 익스피디아, 부킹닷컴, 에어비앤비 같은 회사들이 왜 이 팀에 관심을 가지는가 궁금하신 분들도 많을 것이라 생각됩니다. 이 글에 그 중 하나를 풀어보겠습니다.

“이거 누가 수정했는지 보고싶어요”

자리(ZARI)에서는 수 많은 고객, 결제, 예약 정보들이 매일매일 생겨나고 수정되며 많은 스텝들이 그 정보를 보게됩니다. 정보를 안전히 잘 저장하고 잘 표시해주는 기본에 충실하기도 바쁜데, 그 와중에 점점 많은 게스트하우스 스텝분들과 사장님들이 “누가 무엇을 바뀌었는지 알고싶어요.” 라는 목소리가 들려왔습니다. 처음에는 그냥 누군가 실수를 해서 다음부터 그러지 말라고 혼내는 용도(blame)인가 싶었지만, 실제 현장의 이야기를 들어보니 더 큰 이유가 있었습니다. 한 고객을 위한 방문 전부터 체크인, 지내는 동안 고객사항, 체크아웃, 떠난 후까지 모든 것을 여러 사람이 관리하다보니 변경내역이 당연히 필요한 부분이었습니다.

기술적으로는 UPDATE 내용을 저장하고 잘 보여주면 되겠지 간단히 생각을 했지만, 결국 몇가지 깨달음을 얻고 2014년 12월 4주에 드디어 기능을 내놓게 됩니다.

12월 4주 자리 서비스 업데이트 내역

Figure 1. 자리 서비스 업데이트 내역 일부

예약 변경내역이란?

전화로 예약을 받을 때 고객 이름이 ‘홍일동’이었는데 알고보니 ‘홍길동’이어서 변경을 하면 “ㅇㅇㅇ 스텝이 고객이름을 홍일동에서 홀길동으로 변경” 이라는 내용을 보여주어야합니다. 이름뿐만 아니라 고객 생일, 고객 메모, 받을 돈, 지불한 현금, 예약메모(문자보냄,수건챙겨드림,충전기빌려감), 체크인상태, 도착예정시각, 유입경로, 환불금액, 할인금액등 거의 모든 부분에서 이 내역이 남아야만 갑자기 스텝이 병원을 가거나, 누군가 급히 해당 예약을 처리해도 당황스럽지 않겠죠?

사본이 반드시 필요한 결제/영수증 부분이나 유효기간/만료일이 필요한 데이터의 경우 UPDATE를 하지않고 항상 INSERT하고 똑똑한 SELECT를 통해 최신본만 쓰는 방법도 있습니다. 하지만 자리의 경우 거의 모든 데이터가 n:m 관계에 가깝고, 주로 하나의 필드씩 수정된다는 특징이 있습니다.

예약창 결제정보, 예약정보, 고객정보

Figure 2. 예약창 결제정보, 예약정보, 고객정보 상단부분

아주 간단한 구현

  1. 관련 모델(customer, receipt, reservation)에 대한 업데이트가 발생하면 특별한 아카이브 로직을 실행한다.
  2. 변경 내용은 이전데이터 before와 이후데이터 after를 통해 만든다.
  3. 각 데이터 필드에 맞는 방법으로 추가/변경/삭제, 변경/미변경을 확인한다.
  4. 비교 후 달라진 부분만 history로 담당자 정보와 함께 저장한다.
  5. 예약 화면에 history를 이쁘게 표시한다.

변경이 되었는지 안되었는지 판단하기

일단 해당하는 POST, PUT API 마지막 부분에 특별한 아카이브 로직을 모두 추가했습니다. 아카이브 내부에서는 before[field] !== after[field]로 비교합니다. 물론 express.js스럽게 미들웨어로 분리 후 route callback이 완료되면 알아서 내부에 규정된 값을 가져오도록하면 기존 router(controller)코드를 고치지 않고 적용이 가능합니다. 하지만 저는 implicit passing을 선호하지 않기 때문에 아래와 같이 뻔한 코드로 시작했습니다.

router.post '/:reservation_id', [...], (req, res, next) ->
  # 원래 코드

  # 새로운 코드
  before = req.reservation # router param을 통해 불러온 값
  after = reservation # req.body를 통해 불러온 값

  models.history.archive_reservation before, after

  # 원래 코드

그런데 무언가 이상하네요. 눈으로 보았을때 “불필요하게 이력이 남는다” 싶은 부분이 생각보다 많았고, 이를 없애기 위해 케이스 분류를 시작합니다.

  • 문자열은 공백 처리 trim
  • 타입에 맞는 비교하기: 숫자는 numeric
  • 날짜를 비교하기: YYYY-MM-DDHH:mm
  • 전화번호를 비교하기: +82 010 0000 0000(+82) 010-0000-0000

그리고 위의 조건에 맞는 특별한 함수를 정의하기 시작합니다.

false === is_changed('customer.name', '홍길동 ', ' 홍길동')
false === is_changed('receipt.discount', '+10000', '10000.00')
false === is_changed('reservation.eta_until', '15:00', '15:00 ')

비로소 누가봐도 정말 변경이 된 것만 기록에 남기게 됩니다.

추가와 수정을 구분하기

변경내역이라는 관점에서는 이전 데이터와 다음 데이터를 기록으로 잘 남기면 되지만 그걸 이쁘게 표시하는 입장에서는 아래와 같이 달라집니다.

  • 똑똑이가 고객성별을 M으로 추가
  • 똑똑이가 고객성별을 M에서 F으로 변경

하지만 이런 경우도 있습니다.

  • 똑똑이$10.00 할인 추가
  • 똑똑이가 할인금액을 $10.00에서 $5.00으로 변경
  • 똑똑이가 할인금액을 $10.00에서 $0으로 삭제

앞으로 자리에 추가될 데이터들이 매우 많기 때문에 이런 수 많은 케이스들에 대한 대책이 필요했습니다.

State → State

현실 세계의 복잡함을 다 던져버리고 아주 작은 기본을 생각해보았을때… 특정 시점 기준으로 데이터 모델의 이전/이후 상태가 곧 변경내역이라고 결론 지었습니다.

  • model_id
  • model_type
  • field
  • before
  • after

매우 일반화된 이 모델의 경우 무한한 종류의 다른 모델을 담을 수 있습니다. 필요에 따라 company_id, user_id, session_id, Permission Override Code(매니저 master key를 통한 일시적 권한 상승)등 여러가지를 추가할 수 있습니다.

한눈에 보기하기 쉽도록

상태를 데이터베이스에 잘 남기더라도 그걸 보여주는 UI가 더 중요하겠죠? 어떻게 보여주면 잘 확인할까 고민했습니다. 다른 서비스들의 activity log도 살펴보고, 특히 revision history 기능이 있는 서비스들도 살펴봤습니다. 끝내 얻은 결론은

“ㅇㅇ씨 ㅇㅇㅇ 고객분 결제 받았어요? 네, ㅇㅇ씨가 현금 5만원으로 받으셨어요.”

이와 같은 맥락에 벗어나지 않고, 또는 혼잣말로 읊으며

“어디보자 .. 이거는~ 음.. 기욱 스텝이 총금액 15만원에서 13만원. 맞고, 이거는~ 여름 스텝이 문자발송 완료. 체크인전까지 할거 다했네. (큰소리로) 내일 체크인 확인 끝났어요!”

사고의 흐름에 방해가 되지 않도록 해야했습니다.

고객들도 딱보기에 잘 모르겠지만 저의 개인적인 마음 속 요구사항은 아래와 같았습니다.

  • 이진혁이 고객명 Sam Smith에서 Sam Smith Jr.
  • 서포터가 고객명을 추가 Sam Smith
  • 서포터가 예약상태를 예약중에서 결제완료

하지만 위와 같은 자연스러운 문장을 표시하려면 을-를, 이-가 한글 조사를 처리해야만 합니다. 아마 자연어처리(NLP)를 좋아하는 분들은 어릴적에 심심풀이로 해보셨겠지만 안타깝게도 저는 그러지 못했습니다. 백수일때 해봤으면 좋았을텐데 왜 하필 바쁘게 개발하는데 모르는게 등장하는걸까요.

하지만 언어파일에 단수(singular), 복수(plural)도 나눈 마당에 못할게 있겠나 조금 찾아보니 형태소 분리와 조사 구분은 운좋게 많은 사람들이 고민을 해놓아 의외로 쉽게 해결됐습니다.

그리고, 다국어를 지원하고 있는 자리 서비스 특성상 분리된 언어파일의 구조를 유지했어야하는데 이를 바꾸기는 어려워 한글을 위한 특수처리(조사표시)를 분리해야했습니다. 결국 한글 언어파일(ko.yml)을 아래와 같이 구성하여 이것을 client-side에서 렌더링하도록 만들었습니다.

history reservation rooms created: %s이(가) %s을(를) 추가
history reservation rooms changed: %s이(가) %s을(를) 변경
history reservation memo changed: %s이(가) %s을(를) 변경
history reservation status created: %s이(가) %s

드디어 결과물이 나왔습니다.

변경내역 기존 예제

Figure 3. 그렇게 탄생한 변경내역

그때 당시 over-engineering 아니냐고 반대할까봐 아무 일 없다는 듯이 새초롬하게 결과물을 내놓았는데, 이제 이 글로 기획자가 알게될겁니다.

복잡한 정보는 고전적인 방법으로

문자열 단위 diff

예약메모의 경우 게스트하우스, 호스텔, 중소규모 호스텔에서 일어나는 정말 다양한 케이스를 정리하는 중요한 곳입니다. 이리저리 고민하다가 결국 순정이 최고라는 결론에 깨닫고 고전적인 text diff를 가져왔습니다.

신기한 것은 이걸 잘 활용하는 분들이 많다는 것입니다. 예를 들어 키보증금을 체크아웃 때 환불해주라고 담당자에게 메모를 키보증금 남기면 처리하고나서 (환불)키보증금 이렇게 적게됩니다. 이제는 (일부) 과감하게 키보증금을 지워버립니다. 어차피 빨간줄이 그어지고, 누가했는지 표시가 되니까요.

불필요한 메모가 줄어든다는 것은 고객을 더 빠르고 정확하게 응대할 수 있다는 것이겠죠?

변경내역 메모

Figure 4. 변경내역 메모. 아래에서부터 추가, 줄추가, 단어추가, 단어삭제

문자열-줄 단위 diff

숙박업체에게는 예약한 객실을 더 고객이 원하는 곳으로 업그레이드 해준다거나, 혼성 예약을 각 성별에 맞는 도미토리로 옮기는등 스텝들의 신중한 판단에 따라 다양한 케이스가 발생합니다. 문제는 이 과정에서 실수가 없어야하고, 실수가 있더라도 (변경내역을 통해) 상황을 빠르게 판단하여 누구나 해결할 수 있어야합니다. 객실 추가는 아래와 같이 나옵니다.

객실추가

Figure 5. 객실하나 추가됨

객실 변경은 아래와 같이 나옵니다. 소스버전관리의 line by line diff와 매우 유사합니다.

객실변경

Figure 6. 객실이동하여 하나가 삭제, 하나가 추가됨

이 멋진 것은 kpdecker/jsdiff를 이용했습니다. 논문 “An O(ND) Difference Algorithm and its Variations” (Myers, 1986)에 따른 구현체입니다.

생각해 볼 것

  • 아카이브를 비동기로 처리해야 API 응답시간에 영향이 없다.
  • 트랜잭션이 있는 경우 커밋 전과 비교하고, 커밋 후에만 저장해야한다.
  • 변경이력을 오름차순? 내림차순?
  • 유저 인터페이스로 보면 변경이력은 table에 가깝습니다. 만약에 table 형태가 더 인식이 빠르다면?
  • 리드타임(예약부터 체크인까지 기간)이 2개월이 넘으니 이 내용을 타임라인으로 표시해야 날짜(오늘, 어제, 지난주, 7월, 6월)를 더 자연스럽게 보여주지 않을까?

결론

때로는 개발자란, 고객과 기획자들이 상상도 못한 방법으로 문제를 풀어내야합니다.

제품의 발전은 끝이 없다고 합니다. 이런 작은 기능과 디테일이 추가되었다고 시장에서 승리하고, 고객 전환율이 바로 높아지지는 않겠죠. 하지만 이렇게 꾸준히 나아지기를 실천하면서 소중한 고객의 일부만이라도 감동을 받고 제품을 사랑하게되며 좋은 소문을 퍼뜨린다면 그만큼 행복한 결말이 어디있겠나 생각합니다.

작년까지만 해도 자리 개발팀 지표는 속도에 있었습니다. 2016년에 들어오면서 성장하는 개발팀으로 초점을 두어 지금은 매일/매주/매월/매년 같이 반복하는 요구사항분석/기획/디자인/개발/피드백까지 최대한 고객으로부터 더 많이 배우고 깨달을 수 있는 구조를 중요시하고 있습니다. 타율/성공률을 높이기 위해 부단히 노력하고 있구요. 그만큼 성공사례와 실패사례를 공유하는게 내부적으로 매우 중요해지고 있습니다.

저는 이 글로 하여금 여러분 스스로에게 “아, 저렇게도 쓰는구나. 우리 제품에도 이렇게 풀어내면 고객들이 좋아할까?” 질문을 던져 주었으면 좋겠습니다. 그 다음 best practice와 how-to recipe는 더 뛰어난 분들이 완성해주리라 믿습니다 :)

감사합니다.

자리를 만드는 사람들