본문 바로가기
백엔드 개발

#042. 리팩터링: 멱등성 있는 API 만들기 (feat. 토글기능 구현하기)

by iamjoy 2023. 6. 24.

개념

종종 토글링 하는 기능을 구현할 때가 있다. 글을 스팸 처리하거나 팔로우/언팔로우, 즐겨찾기 설정/해제를 할 때 기능을 켜거나 꺼야한다. 토글 기능을 구현하는 방법이 크게 두 가지가 있다.
(1) 현재와 반대되도록 설정한다.
(2) 설정하고자 하는 상태 값을 받아 그대로 되도록 설정한다.

두 가지 모두 개발, QA 환경에서는 비슷하게 정상 작동한다. 하지만 실제 사용 환경은 다양한 요소가 있다. 예를 들어 네트워크 이슈등으로 인해 팔로우 세팅을 2번 되었다고 가정해볼 수 있다. (1)번은 현재상태와 반대로 설정되기 때문에 팔로우 ON 을 했다가 -> 팔로우 OFF 가 되면서 결과적으로 팔로우 하지 않은 상태가 된다. 반면 (2)번은 팔로우 ON 을 두 번 요청하는 것이 되면서 결과적으로 팔로우 ON 상태가 된다. 팔로우 ON 에 대해서는 항상 팔로우 ON으로 설정하기 때문에 멱등성이 보장된다고 할 수 있다.

http 호출에서 멱등성이란?
동일한 요청을 한 번 보내는 것과 여러 번 연속으로 보내는 것이 같은 효과를 지니고, 서버의 상태도 동일하게 남을 때, 해당 HTTP 메서드가 멱등성을 가졌다고 말한다.  올바르게 구현한 경우 GET, HEAD, PUT, DELETE, OPTIONS, TRACE 메서드는 멱등성을 가지며, POST, PATCH, CONNECT 메서드는 그렇지 않다. 

멱등성을 따질 땐 실제 서버의 백엔드 상태만 보면 되며, 각 요청에서 반환하는 응답 코드는 다를 수 있다. 첫 번째 DELETE 요청이 200을 반환한다면, 그 이후는 아마 404를 반환할 것이다. 여기에서 DELETE가 멱등성을 가진다는 것은, REST API에서 개발자는 DELETE 메서드를 사용해 "목록의 마지막 항목 제거" 기능을 구현해서는 안된다는 것이다. 해당 데이터가 삭제된 상태라는 것은 불변하기 때문에 멱등성이 있다.

금융 쪽은 이러한 멱등성이 더욱 민감해진다. 관련해 토스 페이먼트의 API 멱등성에 관한 글을 같이 읽어보면 좋을 듯 하다. 멱등성을 보장하지 않는 POST, PATCH, CONNECT 매서드에 대해 하나의 트랜젝션 단위를 설정해 이를 멱등성키로 보낸다. 이 트랜젝션의 결과는 항상 동일하다는 보증을 해준다. 즉 30만원을 가진 사람이 -10만원을 2번 누른다고 해도 같은 API 요청 조건에 같은 멱등성 키를 보내면 2번 요청 값에 모두 20만원을 내려준다.

적용

위에서 설명한 두 가지 API 를 구현해봤다. 실제 실행되는 코드는 레포지토리에서 확인할 수 있다.
먼저 유저 객체를 만들기 위한 클래스를 만들고 (1) 과 (2)를 위한 매서드를 만들었다.

export class User {
  private name: string;
  private isFollowing: boolean;

  constructor(name: string, isFollowing: boolean) {
    this.name = name;
    this.isFollowing = isFollowing;
  }

  static withFollowing(name: string) {
    return new User(name, true);
  }

  toggleFollowingStatus() {
    this.isFollowing = !this.isFollowing;
  }

  updateFollowingStatus(follow: boolean) {
    this.isFollowing = follow;
  }
}



(1) 번의 코드로 User 객체에서 toggleFollowingStatus() 매서드를 이용한다. 기존에 조회한 User의 isFollowing 상태를 반전하도록 설정했다.

  toggleFollowingStatus() {
    this.isFollowing = !this.isFollowing;
  }
app.post('/api/v1/follow', (req, res) => {
  const user = User.withFollowing(req.body.name); // 팔로우 중인 유저를 조회대신 생성
  user.toggleFollowingStatus(); // 팔로우 상태를 토글
  res.json(user);
});

반면 (2)번 구현은 아래와 같이 follow를 파라미터로 받아 그 값으로 수정했다.

  updateFollowingStatus(follow: boolean) {
    this.isFollowing = follow;
  }
app.post('/api/v2/follow', (req, res) => {
  const { follow } = req.body;
  const user = User.withFollowing(req.body.name);

  user.updateFollowingStatus(follow); // 팔로우 상태를 설정
  res.json(user);
});




(참고)
MDN 의 멱등성 문서
코드 레포지토리(링크)