프로모션이 걸린 상품의 가격 수정이 실제로 막히는지와 막힌다면 무엇을 돌려주는지를 확인해 달라는 요청을 받았다. 경로를 따라가 보니 세 계층에서 모두 막고 있었고 그중 둘은 아무것도 돌려주지 않았다.
Table of contents
Open Table of contents
상황 — 세 계층에 중복된 보호
첫째는 products 에서 channel_products 로 값을 전파하는 syncPrice() 였는데 프로모션이 살아 있으면 넘어온 price 를 하위의 기존 값으로 되돌린다. 둘째는 저장 직전의 ChannelProductRequest 이고 price 와 discount_rate 를 요청 데이터에서 unset() 으로 통째로 제거해 데이터베이스까지 가지 않게 한다.
셋째는 Vue 화면이며 가격 입력과 할인 토글이 disabled 가 되고 적용된 프로모션 목록이 함께 표시된다. 같은 보호가 여러 계층에 중복돼 있으므로 각각이 어디서 막는지를 분리해 봐야 어느 경로에 안내가 빠졌는지가 드러난다.
원인 — 오류 없이 값만 안 바뀌는 처리
앞의 두 계층은 오류도 경고도 내지 않으므로 저장 요청이 성공하고 응답도 200 으로 돌아온다. 바뀌는 것은 값뿐이고 그 사실을 알릴 통로가 없다.
되돌리기와 unset() 은 둘 다 예외를 발생시키지 않는 방식이라 호출자가 실패를 감지할 수단이 없다. 화면 계층만 disabled 로 상태를 보여 주므로 그 경로를 타지 않는 사용자에게는 아무 단서가 없다.
증상 — 사용자 눈에 남는 것
담당자가 products 화면에서 discount_rate 를 바꾸고 저장하면 성공 응답을 받는다. 그 뒤 channel_products 화면을 열면 값이 그대로이므로 반영이 안 된다고 인식한다.
몇 번 더 시도해도 같은 결과라 결국 문의로 이어진다. 정상 동작인데 문의가 발생하는 것이 이 구조의 실제 비용이었다.
결론 — 로직이 아니라 안내가 빠진 문제
확인 요청의 답은 막힌다는 것이었지만 거기서 끝내면 요청자가 다음에 무엇을 할지 정할 수 없다. 막는 것은 맞고 왜 막혔는지를 알려 주지 않는 것이 문제였다.
두 가지 방법이 있었는데 syncPrice() 가 되돌릴 때 422 로 거절하게 바꾸는 것과 차단 규칙은 그대로 두고 응답에 skipped_fields 와 사유를 실어 화면이 안내 문장을 띄우게 하는 것이다. 뒤쪽을 골랐다. 이유는 배치가 같은 경로를 타므로 거절로 바꾸면 밤마다 프로모션 상품 수만큼 실패가 쌓여서다. 대신 저장이 성공했는데 일부 필드는 무시됐다는 두 겹의 상태를 화면이 표현해야 하는 것을 감수했다. 필요한 것은 프로모션이 진행 중이면 가격 수정이 적용되지 않는다는 문장 하나이지 로직 수정이 아니었다.
사용자가 없던 코드에 사용자가 생길 때
이런 조용한 처리가 어떻게 생기는지는 짐작이 된다. syncPrice() 를 만들 때는 프로모션 값을 지켜야 한다는 것만 생각하고 그 코드가 배치에서만 돌기 때문이다.
나중에 화면에서 상위 상품 수정 기능이 열리면 같은 코드를 사람이 타게 된다. 배치용으로 만든 조용한 처리가 사용자 경로에 노출되는 순간부터 그것은 결함으로 읽힌다.
정리
- 같은 보호가 여러 계층에 중복돼 있을 수 있다
- 각각이 어디서 막는지 분리해서 본다
- 되돌리기와
unset()은 오류를 내지 않는다 - 요청은 성공하고 값만 안 바뀐다
- 정상 차단도 피드백이 없으면 고장으로 읽힌다
- 필요한 것이 로직 수정이 아니라 안내일 때가 있다
- 배치용 조용한 처리가 사용자 경로에 노출되면 결함이 된다
- 확인 요청에는 답과 그 답의 뜻을 함께 준다