ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Rest API 설계 기본 팁 : 계층 구조에 대한 식별자를 명확히 하기
    개발 나누고 더하기/기타 2023. 7. 25. 22:28

     시스템을 설계하다보면 1:1 또는 1:N 관계로 계층 모델이 많이 나온다. 이커머스를 예로 들면 상품과 하위 아이템인 경우인데, 상품에 상품명(PSG 이강인 티셔츠), 상세 설명(상세 이미지)와 같은 정보를 담고, 이 상품을 참조하는 아이템에는 사이즈(S, M, L)와 재고 등을 담는 구조이다. 백오피스 같은 관리 시스템에서 상품과 아이템별로 관리해야 하는 요구가 있다고 치자. 이를 조회하는 API 스펙은 대충 다음과 같이 나올 수 있다.

     

    상품 조회 API

    url : {host}/api/v1/goods/{id}

    method : GET

    param : id - 상품 ID

    response : name - 상품명, description - 상세 설명

    아이템 조회 API

    url : {host}/api/v1/goods/{goodsId}/items/{id}

    method : GET

    param : goodsId - 상품 ID, id - 아이템 ID

    response : goodsName - 상품명, name - 아이템명, size - 사이즈, color - 색상, quantity - 재고

     

     상품과 아이템에 대한 웹 리소스 계층구조가 잘 표현된 것 같지만 아니다. 포함관계는 이해가 되지만, API간 중복된 키 이름이 존재하고 어디에 매칭되는지 확신하기가 어렵다. 왜 매칭하기 어렵냐고? 이 스펙은 설계하고 구현을 담당한 개발자에게는 자연스러워 보이지만, 이를 보고 연동해야 하는 다른 조직의 백엔드 개발자나 클라이언트 개발자에게는 생소할 수 있다. 상품 조회할 때 사용하는 id를 items API에서 그대로 id에 넣어야 할지 뭔가 느낌상으론 goodsId가 맞는거 같은데 여기에 넣어야 할지 호출해봐야 알기 때문이다. 응답 모델에 사용되는 name과 goodsName도 마찬가지이다. 즉, 인터페이스만 보고 결과를 추론할 수 없다. 이러한 명세는 외부 시스템과 연동할 때 기반이 되는 계약이므로 주관적인 내부의 시선에서 작성되면 안되고, 외부 클라이언트의 시선으로 설계되어야 한다.

     이러한 설계가 나온 이유는 내부 구현을 먼저 생각해서이다. JPA 환경이라는 가정 아래 다음 엔티티를 보자(테이블은 생략).

    @Entity
    public class Goods {
        @Id
        private Integer id;
        @Column(name = "name")
        private String name;
        @Column(name = "description")
        private String description;
    }
    
    @Entity
    public class Item {
        @Id
        private Integer id;
        @Column(name = "name")
        private String name;
        @Column(name = "size")
        private String size;
        @Column(name = "color")
        private String color;
        @Column(name = "quantity")
        private int quantity;
        
        @Column(name = "goodsId")
        private int goodsId;
    }

    Goods와 Item이라는 엔티티가 goodsId를 매개로 1:N의 관계를 맺고있다.

    그럼 이제 이 API를 매핑할 컨트롤러를 만들어보자. 엔티티를 응답 모델로 반환하는 과감성과 기본 생성자로 리턴하는 성능적 우수함이 있다!!! - 절대 따라하지 말것.

    @RestController
    public class GoodsController {
        @GetMapping("/api/v1/goods/{id}")
        private Goods getGoodsDetail(@PathVariable int id) {
            return new Goods();
        }
    }
    
    @RestController
    public class ItemController {
        @GetMapping("/api/v1/goods/{goodsId}/items/{id}")
        private Item getItemDetail(@PathVariable int goodsId, @PathVariable int id) {
            return new Item();
        }
    }

    아무튼 코드를 들여다 보자. GoodsController는 Goods와 관련된 정보를 반환하는 역할을 맡고 있다. 그러니 path variable로 받은 id는 타당하다. ItemController는 Item과 관련된 정보를 반환하는 역할을 맡으니 Item의 식별자인 id와 그 참조키인 goodsId를 파라미터로 받는 것 또한 타당하다. 코드 베이스에 기반한 컨텍스트로는 아무런 문제가 없이 잘 짜여진 웹 경로이기 때문에 위와 같은 설계가 나온 것이다. 나무만 보고 숲을 보지 못한 경우랄까?

     

    이제 시선을 외부로 옮겨서 API 스펙을 다시 설계하면 아래와 같다. 큰 차이없이 간단한 작업이다. 모델에 해당하는 접두어를 붙여주면 된다.

      상품 조회 아이템 조회
    url /api/v1/goods/{goodsId} api/v1/goods/{goodsId}/items/{itemId}
    response goodsName, description goodsName, itemName, size, color, quantity

    이제 이 스펙으로 작업하는 외부 조직의 개발자는 goodsId와 itemId에 대한 확신을 가질 수 있다. 그만큼 확인해야 할 요소가 적어지며 소통비용도 줄어든다. 별 것 아니지만 이렇게 배려하면 서로 편한 것 같다.

     

    사실 이 글은 이번에 끝난 개발건에 대한 회고에 가깝다. 어느정도 조직과 동료에 익숙해져 별 생각없이 API 스펙을 만들어서 담당자에게 공유했다. 이전에 작업했던 프론트엔드 개발자라면 척하고 매칭을 시켰겠지만, 이번에는 입사한지 얼마 안된 분이었다. 당연히 한번 확인해볼만 하고, 꼼꼼하게 문의했던 것에 오히려 감사했다. 시스템도 큰 단위의 객체이다. OOP적인 관점에서 출발은 역시 외부와의 협력이다. 내부가 아니다!!!!!

     

    댓글

Designed by Tistory.