안녕!
우아한테크코스 5기 [스탬프크러쉬]팀 깃짱이라고 합니다.
💋 인트로
2차, 3차 데모데이 기간 동안에는 서비스의 API를 계속해서 설계하고 수정하는 과정이 있었다.
그 과정에 대해서 기록해보려고 한다!
💋 API 설계에 대한 현재 나의 생각
나는 한 가지 API를 설계할 때 다른 케이스에서 재사용될 수 있는지, 그러니깐 '재사용성'에 대해서 가장 중요하게 생각한다.
우리 서비스의 경우에는 많은 사람들에게 오픈된 API가 아닌, 정해진 클라이언트와의 약속 하에 정해진 데이터를 뿌려주는 API다. 따라서 어떻게 보면, 반드시 범용성 있는 API, RESTful한 API를 만들 필요가 없을 지도 모른다. 프론트엔드와 약속한 URI에 약속한 형태로 데이터를 전송해주기만 하면 되는 것이다. 하지만, 여전히 나는 재사용성이 좋은 API에 대한 욕심이 있었다.
2차 데모데이 때부터 API 설계 관련해서 나는 정말 많은 아이디어를 냈었다.
사실 가장 편한 설계는 함께 만들어놓은 와이어프레임을 보면서 이 화면에서 어떤 데이터가 필요한지 보고 정리해서 한번에 뿌려주는 방식이다. 이렇게 생각하기 쉬운 방식을 깨고, 재사용성을 위해서 당장에 큰 변화를 주기 위해서 정말 많은 설득이 필요했었다. 진짜 열변을 토했었다..ㅋㅋ
💋 스탬프크러쉬의 API 설계
✔ 에피소드 1: 복잡한 요청은 여러 개의 요청으로 나눈다! (임시 VS 가입 고객의 기존 VS 신규 쿠폰에 스탬프 적립.. 어질어질)
우리 팀이 가장 헤맸던 포인트 중 하나다.
우리 서비스는 편의성을 중요시하기 때문에 아직 스탬프크러쉬 서비스에 가입하지 않은 고객이더라도, 일정 기간 동안(6개월을 임의로 정했다.)은 서비스 내 자신의 스탬프를 유지할 수 있어야 한다. 또한 편의성을 위해서 고객이 가입했는지 여부는 고객의 전화번호를 통해서 조회할 수 있다.
우리 팀이 상당히 집착(?)했던 포인트는, 고객은 단지 전화번호만 입력하면, 스탬프크러쉬에 가입을 했던 고객이든 아니든, 해당 카페에 방문을 해서 현재 쿠폰을 소유한 상태가 맞든 아니든 자동으로 스탬프가 하나 쌓인다는 것이다.
고객이 특정 카페를 방문해서 스탬프를 찍으려고 할 때, 카페에 이미 가지고 있는 쿠폰이 있는지 여부, 또 스탬프크러쉬 서비스에 이미 가입되어 있는지 여부에 따라서 서비스가 진행되는 플로우가 아주 다르게 흘러갔다.
뭉쳐있던 시나리오를 4가지 경우의 수로 풀면 아래와 같다. (쉬워 보여도 이 네 가지를 추려내는 과정도 은근히 오래 걸렸다.)
아래에서 하나의 동그라미를 한 번의 요청이라고 생각하면 이해하기 쉽다.
- 스탬프크러쉬 가입 고객이 카페A에 현재 가지고 있는 쿠폰이 있는 경우
- 요청 1: 전화번호를 통해서 가입 고객을 찾는다. 쿠폰이 있으므로, 가지고 있는 쿠폰에 스탬프를 찍어줘야 한다. 스탬프크러쉬에 별도로 가입 절차는 필요하지 않다.
- 스탬프크러쉬 가입 고객이 카페A에 현재 가지고 있는 쿠폰이 없는 경우
- 요청 1: 전화번호를 통해서 가입 고객을 찾는다. 쿠폰을 발급하고, 쿠폰에 스탬프를 찍어줘야 한다. 스탬프크러쉬에 별도로 가입 절차는 필요하지 않다.
- 스탬프크러쉬 미가입 고객이 카페A에 현재 가지고 있는 쿠폰이 있는 경우
- 요청 1: 전화번호를 통해서 가입 고객을 찾지만, 발견하지 못한다. 해당 전화번호로 임시 고객을 스탬프크러쉬에 자동으로 가입시키고, 가지고 있는 쿠폰에 스탬프를 찍어줘야 한다.
- 스탬프크러쉬 미가입 고객이 카페A에 현재 가지고 있는 쿠폰이 없는 경우
- 요청 1: 전화번호를 통해서 가입 고객을 찾지만, 발견하지 못한다. 해당 전화번호로 임시 고객을 스탬프크러쉬에 자동으로 가입시키고, 쿠폰을 발급하고, 쿠폰에 스탬프를 찍어줘야 한다.
현실 세계로 생각하면 아주 easy하지만, 기계적으로 동작하게 하려면 은근히 어려운 플로우다. 특히나 우리 팀이 처음에 겪었던 문제의 근본적인 원인은 이 모든 행동을 단 한 번의 요청과 응답으로 해결하려 했다는 점이다.
백엔드의 로직을 생각해봤을 때,
쿠폰 발급을 위해서는 회원의 id가 필요하고, 스탬프를 찍어주기 위해서는 쿠폰의 id가 필요하다.
위의 로직을 한 번의 API 요청과 응답으로 묶어버린다면 중간중간에 분기가 있겠지만,
하나의 API 속에서 고객, 쿠폰, 스탬프의 조회와 생성 명령이 동시에 존재하게 된다.
따라서 나는 이 로직을 세 가지로 분리했다.
- 스탬프크러쉬 가입 고객이 카페A에 현재 가지고 있는 쿠폰이 있는 경우
- 요청 1: 전화번호를 통해서 가입 고객을 조회하고, 고객 id를 반환한다.
- 요청 2: 조회된 고객의 id를 통해서 해당 카페에 고객이 가지고 있는 쿠폰을 조회하고, 쿠폰 id를 반환한다.
- 요청 3: 조회한 고객의 쿠폰 id를 통해서 쿠폰에 스탬프를 추가로 찍어준다.
- 스탬프크러쉬 가입 고객이 카페A에 현재 가지고 있는 쿠폰이 없는 경우
- 요청 1: 전화번호를 통해서 가입 고객을 조회하고, 고객 id를 반환한다.
- 요청 2: 조회된 고객의 id를 통해서 해당 카페에 고객이 가지고 있는 쿠폰을 조회하지만, 쿠폰을 발견하지 못한다.
- 요청 3: 조회된 고객의 id로 새로운 쿠폰을 발급하고, 쿠폰의 id를 반환한다.
- 요청 4: 반환받은 쿠폰의 id를 통해서 쿠폰에 스탬프를 추가로 찍어준다.
- 스탬프크러쉬 미가입 고객이 카페A에 현재 가지고 있는 쿠폰이 있는 경우
- 요청 1: 전화번호를 통해서 가입 고객을 조회하지만, 고객을 발견하지 못한다.
- 요청 2: 전화번호를 통해서 임시 가입 고객을 생성하고, 고객 id를 반환한다.
- 요청 3: 조회된 고객의 id를 통해서 해당 카페에 고객이 가지고 있는 쿠폰을 조회하고, 쿠폰 id를 반환한다.
- 요청 4: 조회한 고객의 쿠폰 id를 통해서 쿠폰에 스탬프를 추가로 찍어준다.
- 스탬프크러쉬 미가입 고객이 카페A에 현재 가지고 있는 쿠폰이 없는 경우
- 요청 1: 전화번호를 통해서 가입 고객을 조회하지만, 고객을 발견하지 못한다.
- 요청 2: 전화번호를 통해서 임시 가입 고객을 생성하고, 고객 id를 반환한다.
- 요청 3: 조회된 고객의 id를 통해서 해당 카페에 고객이 가지고 있는 쿠폰을 조회하지만, 쿠폰을 발견하지 못한다.
- 요청 4: 조회된 고객의 id로 새로운 쿠폰을 발급하고, 쿠폰의 id를 반환한다.
- 요청 5: 반환받은 쿠폰의 id를 통해서 쿠폰에 스탬프를 추가로 찍어준다.
이렇게 나누니, 결과적으로 조회를 통해서 찾거나, 찾지 못한 경우에 id를 반환하는 식으로 공통적인 부분에 대해서 플로우를 모두 맞춰갈 수 있게 되었다.
따라서 만들어야 할 API는 분리되었지만, 여러 곳에서 재사용하고 있고 크게 5가지로 정리가 되었다.
- 전화번호로 고객 조회 API
- 전화번호로 임시 고객 생성 API
- 고객 id로 해당 카페에 고객의 쿠폰 조회 API
- 고객 id로 해당 카페에 고객의 쿠폰 생성 API
- 쿠폰 id로 쿠폰에 스탬프 추가 API
결론적으로, 한 번에 처리하려고 애쓰던 API를 여러 번의 call로 분리하니 API 자체도 훨씬 이해하기 쉽고 직관적이며 깔끔해졌다.
중간에 실패해서 다시 실행하더라도 문제가 없기 때문에 하나의 call로 처리하지 않아도 된다!
(문제가 없다는 말은, 임시 회원을 가입시키고 쿠폰을 조회하다가 실패해서 다시 처음으로 돌아오게 되면, 이후에는 임시 회원을 가입하지 않고 조회에서 찾을 수 있기 때문에 문제가 없다는 뜻임.)암튼 아래와 같이 결과적으로 정리할 수 있다.
(+ 23.09.05 추가)
우리 서비스는 API 공개용이 아니라 오직 프론트엔드와의 소통용이기 때문에, 하나의 API call에서 처리하는 것이 더 효율적이라고 생각할 수 있겠지만, 그렇지 않았다.
API call을 나누면서 각각이 어떤 역할을 하는지가 더 뚜렷해져서 팀 내 소통은 오히려 원활해졌다.
또 실제로 나눈 것의 득을 직접적으로 보게 된 에피소드가 있다.
최종 데모데이 때, 전화번호 한 글자만을 잘못 입력해도 존재하지 않는 전화번호로 조회되어서 임시 회원이 자동으로 생성되는 이슈가 발생했다. 이 이슈는 굉장히 간단히 해결되었는데, 프론트엔드에서 먼저 해당 전화번호의 고객이 있는지 서버에 조회 요청을 보내고, 서버는 해당하는 회원이 없으면 응답으로 빈 배열을 보낸다. 프론트엔드는 빈 배열을 받으면 사용자에게 한 번 더 (alert든 어떤 방식으로든) 해당 전화번호가 맞는지 확인하고, 전화번호를 다시 입력받을지 새 임시회원을 생성할 지에 대해서 물어볼 수 있다.
따라서 API의 변경이나 추가가 전혀 없이, 프론트의 작은 동작 변화만으로 해당 이슈에 대응할 수 있었다.
추가 설명
물론 백엔드 데이터 관점에서 문제가 없다는거지, 프론트엔드는 아래와 같이 문제를 해결했다고 한다.
✔ 에피소드 2: 조회를 했지만, 발견하지 못한 것은 예외 상황일까?
위에서 설명한 API 중 전화번호를 통해서 고객을 조회하는 API를 설계하던 중 발생한 에피소드다.
아래와 같은 request를 통해서 전화번호에 해당하는 고객이 존재하는지 조회한다.
가입했거나 임시로 등록된 고객이 존재한다면, 아래와 같은 응답을 보내주면 된다. 간단하다.
그치만, 고객이 존재하지 않는다면..?
두 가지 의견이 있었다.
- 발견되지 않았다는 예외를 던지고, 이 예외를 ControllerAdvice에서 잡아서 별도의 예외 Response를 보내준다.
- 그냥 빈 customer List를 json 형태로 반환한다.
우리 팀이 선택한 건 두 번째 의견이다. 사실 첫 번째 의견은 내가 주장했던 것이었다.
API의 목적이 전화번호로 고객을 '조회'하는 것이기 때문에, 필터링 조건을 통해서 조회가 되지 않은 상황을 예외라고 규정할 수 없다고 생각했다. 또다른 이유는, 프론트엔드가 빈 리스트가 편하다고 했다(?)
지금 생각해봐도 어떤 서비스를 이용하던 도중에 고객 정보를 찾을 수 없어서 인가가 되지 않는 상황이 아니고, 처음부터 단순히 '조회'를 위한 API라고 규정했기 때문에 해당 API를 우리 서비스와 독립적으로 생각해봤을 때도 조회가 되지 않는 경우에는 조회한 결과가 아무것도 없음을 보여주는 것이 더 적절할 것이라고 생각하게 되었다.
✔ 에피소드 3: API 재사용성을 통해서 10시간을 절약했다
우리팀의 서비스는 사장님에게는 카페의 '개성'을 살릴 수 있는 것을 중요하게 생각한다.
따라서 기존에 카페에서 사용하던 종이쿠폰을 그대로 온라인 상으로 옮겨올 수 있도록 한다.
쿠폰의 앞면과 뒷면을 스캔해서 업로드하고, 뒷면에 들어갈 스탬프의 위치까지 좌표로 저장해서 커스텀할 수 있는 기능을 기획했다.
현실의 종이쿠폰을 곧바로 옮겨오는 것도 좋지만, 때로는 어떤 사장님들은 쿠폰을 사용하고 있지 않다가, 스탬프크러쉬를 통해서 처음으로 쿠폰 정책을 도입할 수도 있다. 따라서 몇 가지의 샘플을 제공해주기로 했다.
2차 데모데이 당시의 API 설계다.
커스텀은 조금 더 세밀한 구현이 필요하기 때문에 2차 데모데이 때는 우선, 기본적인 템플릿으로 제공되는 쿠폰 중에서 선택하는 기능을 먼저 구현해서 제공하기로 했다. 기본 배경화면을 맥북에서 제공해주는 것과 비슷하게 생각하면 이해가 편할 것 같다.
초기에 생각했던 형태에서는 쿠폰의 앞면, 뒷면 이미지를 클라이언트에서 전송할지, 서버에서 가지고 있으면서 클라이언트에는 id만 노출할 지도 정해지지 않았다. 또 쿠폰의 앞면과 뒷면을 세트처럼 운영할 계획도 있었다.
어려웠던 점은 아래와 같다.
- 최대 스탬프의 개수라고 선택한 후에는, 쿠폰 뒷면에 그려진 스탬프 찍는 칸의 모양에 따라서 스탬프 좌표로 등록되는 개수가 결정되므로, 모든 쿠폰 뒷면이 선택 가능하지 않다.
- 쿠폰 앞면과 뒷면이 세트처럼 움직여서 선택권이 제한된다.
따라서 앞면과 뒷면을 세트가 아닌 각각 선택으로 분리하기로 했다.
그리고, 뒷면에 스탬프의 좌표를 함께 보내주면서, 앞서 선택한 최대 스탬프 개수에 따라서 선택 가능한 쿠폰 뒷면 사진을 필터링해서 보내주기로 했다.
스탬프 개수별로 기본 샘플 조회(8개, 10개, 12개)
Request
GET /coupon-samples?max-stamp-count=8 HTTP/1.1
Response
HTTP/1.1 200 OK
{
"sampleFrontImages": [
{
"id": 1,
"imageUrl": "http://localhost:3000"
},
{
"id": 2,
"imageUrl": "http://localhost:3000"
}, // ...
],
"sampleBackImages": [
{
"id": 1,
"imageUrl": "http://localhost:3000",
"stampCoordinates": [
{
"order": 1,
"xCoordinate": 1,
"yCoordinate": 2
},
{
"order": 2,
"xCoordinate": 2,
"yCoordinate": 4
},
// ... 총 8개
]
},
{
"id": 2,
// ...
},
// ... 다른 다양한 쿠폰 뒷면 템플릿들
],
"sampleStampImages": [
{
"id": 1,
"imageUrl": "http://localhost:3000"
},
{
"id": 2,
"imageUrl": "http://localhost:3000"
},
// ... 추가적인 스탬프 템플릿들
]
}
결과적으로 API를 위와 같이 설계했다.
그리고 3차 데모데이가 되었다.
이제는 커스텀에 대한 부분까지도 구현해야만 했다.
백엔드에서 3차 데모데이를 위한 첫 스프린트 회의에서 커스텀 쿠폰 이미지의 저장에 추정한 일정은 10시간이었다.
하지만, 그대로 위의 API를 재사용할 수 있게 되었다.
S3 사용이나, 이미지를 저장하는 방식에 대한 고민은 잠시 미뤄두고 무료 호스팅 API를 사용해서 백엔드에 이미지 주소만을 넘겨주기로 했다. 이후에 S3를 사용하게 되면, 이미지 주소를 넘겨준다는 방식은 그대로 유지하면서 내용만 바꾸면 되기 때문에 여전히 API를 유지할 수 있다.
따라서 백엔드가 처음 추정한 10시간을 그대로 아낄 수 있게 되었다 ><
위의 API를 강력히 주장했을 때는 그닥 찬성을 많이 받지는 못했다.
사실 이후의 커스텀에서도 재사용할 수 있다고 생각했고 주장했지만, 팀원들을 제대로 이해시키고 설득시키지 못했기 때문이다.
비슷하게 아쉬운 사례가 너무너무 많지만, 그래도 라잇이 어떻게 이후에 나올 모든 케이스를 다 잘 고려하냐고 대단하다고, 그리고 제나가 나는 하나를 생각할 때 그 외의 다양한 변수와 발생할 사건에 대해서 다 떠올릴 수 있다고 칭찬해줬을 때는 그간의 나만 오바하나 싶던 열변이 모두 보상받는 느낌이었다. 라잇, 제나, 레고가 엄청 칭찬해줘서 뭔가 그때의 힘듦을 인정받은 것 같고 기분이 아주 좋았다
머릿속에서 재사용성, 확장성에 대한 내용이 굉장히 잘 돌아가는 편인 것 같다. 그래서 그런지 API 설계와 객체 설계를 할 때 다른 크루들보다 유난히 나만 너무 힘든 것 같고 그랬었는데, 내가 백그라운드로 굉장히 많이 돌리고 있다고 생각이 드니 좀 위안이 되는 것 같다. (이제와서)
암튼 확장 가능성 고려하는게 소질인지 암튼 복잡하고 그런게 되게 좋다. API 설계 진짜 재밌다.
💋 아웃트로
이번 프로젝트 동안에는 최대한 프론트엔드와 약속된 플레이로, 하나의 요청으로 페이지를 최대한 표현할 수 있도록 만들었다.
우리 서비스는 API를 외부에 공개하기 위한 서비스는 아니기 때문에 이런 방식으로 해도 괜찮다고 생각은 한다.
따라서 API의 스펙이 화면에 보여지는 내용에 어느 정도 의존적일 수밖에 없다고는 생각한다.
하지만, 한 번의 요청으로 화면에 보이는 모든 내용을 해결하기 위해서 무리하게 내용을 많이 넣는 것은 반대한다.
필요한 경우에는 하나의 요청이 아닌 여러 번의 요청으로 나누어 화면을 표시할 수 있도록 하는 경우도 많다는 것을 배웠다.
'PROJECT > Stamp Crush' 카테고리의 다른 글
[우테코] 임시 회원 ↔ 가입회원 데이터 연동기(2): 테이블 구조 대공사, 데이터 연동 API 구현! (0) | 2023.09.11 |
---|---|
[우테코] 임시 회원 ↔ 가입회원 데이터 연동기(1): 6가지 시도와 실패한 이유(JPA 상속 관계 매핑의 한계) (2) | 2023.09.07 |
[우테코] 스탬프크러쉬 팀의 서비스 기능 목록을 공유합니다! (0) | 2023.07.24 |
[우테코] 스탬프크러쉬 팀의 배포 자동화: EC2 환경에서 Docker, Jenkins를 사용한 CI, CD (feat. Java 17) (0) | 2023.07.23 |
[우테코] 스탬프크러쉬 팀의 클라우드 서버 역할 분담: 개발 서버, 데이터베이스 서버, 인프라 서버로 분리한 이유 (6) | 2023.07.20 |