■ 프로젝트 배경 및 목적


사용자의 요청에 대해 승인, 보류, 거절의 세 가지 상태를 정의하고, 이를 이메일로 안내하는 API를 개발하였습니다. 초기 설계에서는 POST HTTP 메소드를 사용하여 사용자의 상태를 저장하고 이메일을 발송하였으나, 멱등성이 고려되지 않아 상태가 반복적으로 변경되는 문제가 발생하였습니다.

■ 프로젝트 목표 및 성과


목표 성과
1 사용자 상태 저장 & 상태에 따른 메일 전송 API 구현 세 가지 상태에 대한 메일 템플릿을 선택 및 수정할 수 있고, 메일 작성 완료 후 대상자에게 전송하는 API 구현
2 상태 저장 & 메일 전송을 POST method로 요청하되, 해당 요청이 멱등하도록 구현 멱등한 REST API를 위한 리서치 & 학습
3 빠른 시간 내에 멱등성 해결 Alert 창을 통해 사용자가 멱등한 요청을 보낼 수 있도록 유도

■ 문제 발생 및 해결 과정


  1. 멱등하지 않은 구현으로 인해서 사용자의 상태가 여러 번 저장되는 것 뿐만 아니라 메일 전송 또한 반복적으로 수행되어 서비스 이용에 불편함을 남겼습니다.

    해당 문제에 대해 개발팀 회의 시간에 API의 멱등성에 대해 알게 되었습니다. API 멱등성을 보장하기 위한 다양한 방법을 리서치하였습니다. 하지만 당시 회사 리소스 부족 문제로, 해당 문제를 빠르게 직접 해결해야 했습니다. 따라서 Admin 유저가 상태 저장 요청을 보낸 후, 이에 대한 alert 창을 띄움으로써 요청이 멱등적이도록 유도하는 아이디어를 고안했습니다. 직접적으로 멱등성을 구현하지 않았지만 배포가 지난 3개월까지 문제없이 의도대로 잘 사용되는 점을 확인하였습니다.

  2. 회사의 메일 관련 API는 모두 메일 템플릿이 html 양식으로 고정되어 있었습니다. 하지만 이 API는 Admin 사용자가 유동적으로 수정할 수 있는 템플릿이 요구되었습니다.

    해당 예시는 승인 관련 메일입니다. 이 밖에 보류와 거절에 관련한 템플릿 또한 생성했습니다.

    해당 예시는 승인 관련 메일입니다. 이 밖에 보류와 거절에 관련한 템플릿 또한 생성했습니다.

    Admin 사용자의 유동적인 메일 수정을 위해 Quill 에디터를 구현하였습니다. 3 가지 메일 양식을 선택하면 quill 에디터에 이와 관련한 텍스트가 뜨고, 이를 사용자가 직접 수정할 수 있도록 구현하였습니다.

■ 프로젝트를 통해 배운 점과 앞으로 활용할 점


  1. 멱등한 POST API를 직접 구현하는 방법에 대해 학습하고, 앞으로 API를 개발할 때 이 지식을 활용할 계획입니다.

    제가 구현한 API는 우회적인 문제 해결 방법이 잘 통하는 특수한 케이스였기에 가능했습니다. 또한 백엔드 레벨에서 직접적인 처리를 하지 않았기에, 개발자로서 궁금증은 여전했고 직접적으로 처리하는 방법에 대해 학습하였습니다.

    1. 멱등키 헤더 사용

      멱등키를 API 요청 헤더에 포함하는 방식입니다. 서버는 같은 멱등키가 2 번 이상 요청되면 중복으로 판단한 뒤, 실제로 처리하지 않는 방식으로 동작합니다.

      실제로 토스페이먼츠에서는 결제 취소 API를 해당 방법으로 구현한 점을 확인했습니다. API 서버는 취소 요청마다 헤더에 멱등키가 있는지 확인하고, 멱등키 저장을 위한 DB에서 이를 확인합니다.

      image.png

      만약, 요청에서 이미 존재하는 멱등키가 또 발견된다면 멱등성 보장을 위해 실제 요청을 진행하지 않고 저장된 응답 데이터를 돌려줍니다.

    2. Entity Tag 사용하기

      헤더를 사용하는 방식과 유사합니다. 특정 URL의 리소스가 변경될 때, 새로운 E tag를 생성하고 이 값의 비교를 통해 자원이 동일한지 여부에 대해 확인합니다.

      HTTP/1.1 200 OK
      Etag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
      
      {
      "name": "cobinding",
      "email": "[email protected]"
      }
      

      If-Match를 통해 Etag를 확인합니다. 구체적으로는, Apache 라이브러리의 DigestUtils 클래스를 사용하여 해시값을 생성하고, 이를 If-Match 해더와 비교합니다.

      @PostMapping("/resource")
      public ResponseEntity<Resource> updateResource(@RequestBody Resource resource,@RequestHeader(value = "If-Match", required = false) String ifMatch,
      HttpServletRequest request) {
         
         String currentEtag = "\\"" + resource.getEtag() + "\\"";
          if (ifMatch != null && !ifMatch.equals(currentEtag)) {
              return ResponseEntity.status(HttpStatus.PRECONDITION_FAILED).build();
          }
          
          Resource updatedResource = resourceService.updateResource(resource);
          String newEtag = "\\"" + updatedResource.getEtag() + "\\"";
          return ResponseEntity.ok()
                               .eTag(newEtag)
                               .body(updatedResource);
      }
      

image.png