멘토님을 통해 다시 생각한 RESTful API 그리고 객체지향

dev_will_d·2024년 5월 24일
2
post-thumbnail

나에게는 개발 멘토님이 계신다. 그리고 나는 이분을 개발 롤모델로 생각한다. 이 전에 멘토님과 소주를 먹으면서 멘토님의 개발에 대한 태도와 생각, 가치관에 대해 들은적이 있다. 그때 시스템, 그리고 코드를 자신의 딸 만큼 생각한다고 말씀하시면서 개발을 할때의 과정을 상세히 설명해 주셨을때 깊은 감명을 받은 것이 아직도 생각난다. 그 이후 나는 이분을 진심으로 존경하게 되었고, 닮고 싶었다. 이번에도 우연히 멘토님과 소주챗을 하였다. 여러 개발 주제로 토론을 했다. 이번에도 말씀 하나 하나 모두 인상 깊었지만, 그중 RESTful API와 객체지향에 대한 이야기에 깊은 감명을 받아 이와 관련해서 연구하고 분석해서 글을 쓰게 되었다.

설계의 중요성

멘토님과 만났을때 나도 멘토님처럼 생각하고 싶어서 어떻게 생각하시는지 관찰했다. 여러 대화를 해보면서 특징을 곰곰이 생각해봤다. 내가 발견한 특징은 아래와 같다.

여러 시간에 걸쳐 많은 코드를 작성하며 좋은 코드와 나쁜 코드를 경험하고, 많은 경험과 지식이 쌓이면 자신의 코드를 보며 반성할 수 있다. 반성을 할 수 있으면 머리속으로 중요한 원칙과 경험을 근거로 머릿속에서 각 경계의 코드를 작성하며 생각의 사냥을 할 수 있다. 생각의 사냥을 하며 끊임없이 머릿속에서 작성한 코드의 정당성과 타당성을 스스로 설명하고 결정한다. 이 과정을 빠르고 정확하게 할 수 있다면 코드의 품질은 높을 수 밖에 없고, 체계적인 코드를 작성할 수 있다.

나는 내가 관찰한 특징을 토대로 앞으로 이렇게 해야겠다고 생각했다. 그리고 추가로 질문했다.

👨🏻‍💻 그렇게 할 수 있다면 무엇에 집중해야 하나요?

멘토님은 설계라고 답변해주셨다. 그리고 위 과정은 설계하는 과정이라고 할 수 있다. 즉, 설계는 코드의 품질을 높이고 지속가능한 시스템의 초석이 되는 체계적인 코드 작성의 시작이라고 할 수 있다.

REST API 기본

백엔드 설계의 시작은 Endpoint(ex) Controller)에서 시작된다고 할 수 있다. 무엇을 개발할지(집중할지) 알아야 무엇을 개발할지 알 수 있기때문이다. 많은 API 설계 기법이 있지만 오늘은 대표적인 API 설계 기법중 하나인 REST API에 대해 설명해보자 한다.

  • REST API
1. 자원(URI), 행위(Method), 표현(Representation)을 통해 클라이언트와 서버 간의 통신 방식을 규정
2. 자원, 행위, 표현을 통해 클라이언트와 서버가 어떻게 식별하고, 조작하며, 데이터를 주고 받을지를 정의
  • REST API 대표적인 Design Guide
1. URI는 정보의 자원을 표현
2. 자원에 대한 행위는 HTTP Method 표현 (GET(발급), POST(생성), PATCH((일부)수정), DELETE(삭제))
3. (/)는 계층 관계를 나타내는 데 사용
4. Domain 표현은 복수 사용 (teachers, lectures)
5. 하이픈 사용, 언더바 사용 X, 명사 사용, 소문자 사용
6. 확장자는 URI 포함 x, 헤더에 표현
7. 적절한 HTTP 응답 상태 코드 발급
  • REST API 특징
1. 자원(URI)
- 자원은 고유한 URI를 통해 식별
2. 행위(Method)
- 자원에 대한 특정 행위 (GET, POST, DELETE, PUT) 정의
3. 표현(Representation)
- 자원의 상태나 데이터를 표현 (JSON, XML)
4. 상태 무결성 (Statelessness)
- 각 요청은 독립적, 서버는 클라이언트의 상태를 유지하지 않는다.
- 클라이언트는 필요한 모든 정보를 요청에 포함
5. 캐싱 (Caching)
- 응답이 캐시 가능하도록 설정 가능
- HTTP 헤더를 통해 캐시 제어 가능
6. 계층화 시스템 (Layered System)
- 클라이언트는 중간 서버(프록시, 게이트웨이)를 통해 서버와 상호작용 가능
  • RESTful API
REST API의 정의 및 특징과 REST API Design Guide를 잘 따른 API를 RESTful 하다고 표현한다.

REST API 설계 표준은 없다. 즉, 정답은 없다. 그러나 REST API의 정의 및 특징, 잘 알려진 설계 기법들, 각 팀에서 정한 일관성 있는 설계를 따른다면 RESTful 하다고 할 수 있을것이다.

REST API 설계 with 객체지향

필자는 REST API 정의, 대표적인 REST API Guide와 함께 REST API를 객체지향에 집중해 설명해 보도록 하겠다.

필자는 멘토님의 말씀과 매달 플레이를 하는 유저의 수가 1억명이 넘는 Riot Games Open API를 기반으로 연구 / 분석을 실시했다. https://developer.riotgames.com/apis

URI 의미 해석

  1. 깊이: REST API에서 URI는 깊이를 나타낸다.
예시)
GET teachers
* 설명: 모든 선생님
GET teachers/{teacherId}
* 설명: 특정 선생님
  1. 도메인 관계: REST API에서 URI는 도메인 간의 관계를 나타낸다.
  • 1:1
  설명: 특정 도전(challenges/{challengeId})의 설정(config)
  RESPONSE: 단수 (ChallengesConfigInfoDTO)
추가설명
  Collection: 집합 데이터 ex) challenges
  - REST API 에서 집합 데이터를 나타내는 도메인은 복수로 명시한다.
  Document: 단건 문서 (데이터) ex) config
  - REST API 에서 단건 문서를 나타내는 도메인은 단수로 명시한다.
  • N:1
  설명: 모든 도전(challenges)의 설정(config)
  RESTPONSE: 복수 (List<ChallengesConfigInfoDTO>)
  GET teachers/events/{eventId}
  설명: 모든 선생님(teachers)의 특정 이벤트 (events/{eventId})
  RESTPONSE: 복수 (List<TeacherEvent>)
  해석: 같은 이벤트를 가지는 여러 선생님들의 이벤트 리스트를 주세요.
  • 1:N
  설명: 특정 도전(challenges/{challengeId)의 모든 백분위수(percentiles)
  RESTPONSE: 복수 (Map<Level, Double>)
  GET /teachers/{teacherId}/courses
  설명: 특정 선생님의 (teachers/{teacherId}) 모든 강의(courses)
  RESPONSE: 복수 (List<TeacherCourse>)
  • N:M
  설명: 모든 도전(challenges)의 모든 백분위수(percentiles)
  RESTPONSE: 복수(Map[Long, Map[Integer, Map<Level, Double>]])
  GET /teachers/courses
  설명: 모든 선생님의 (teachers) 모든 강의 (courses)
  RESPONSE: 복수 (List<TeacherCourse>)
  1. 책임: URI 역할, 책임을 나타낸다.

  GET users/{userId}/coupons
  설명: 특정 유저에 대한 모든 쿠폰 
  추가설명: 이는 유저와 관련한 요청과 응답에 대한 책임을 가지는 UserController에 속하는것이 적합한다.
  RESTPONSE: List<UserCoupon>
  class: UserController
  GET coupon/{couponId}/users
  설명: 특정 쿠폰에 대한 모든 유저들
  추가설명: 이는 쿠폰에 관련한 요청과 응답에 대한 책임을 가지는 CouponeController에 속하는것이 적합하다.
  RESTPONSE: List<CouponUser>
  class: CouponController
  GET courses?teacherId=?
  설명: 모든 강의 그 중 특정 선생님
  추가설명: 이는 강의에 관련한 요청과 응답에 대한 책임을 가지는 CourseController에 속하는것이 적합하다.
  RESTPONSE: List<Course>
  class: CourseController
  GET teachers/{teacherId}/courses
  설명: 특정 선생님의 강의들
    추가설명: 이는 선생님에 관련한 요청과 응답에 대한 책임을 가지는 TeacherController에 속하는것이 적합하다.
  RESTPONSE: List<TeacherCourse>
  class: TeacherController

각 상황과 맥락에 따라 도메인들은 충분히 얽히고 섥힐수 있다. 또한 URI의 관계를 어떻게 표현하느냐에 따라 무엇에 집중해야 할지가 달라진다. 즉, 책임과 역할이 달라진다고 할 수 있다. 결론적으로 URI를 통해 역할과 책임을 파악 할 수 있다.

결론

기획 -> 도메인 -> 도메인 간의 관계 -> URI 표현 -> REST API를 객체지향 적으로 설계

특정 서비스를 개발하기 위한 기획서가 있다. 개발자는 기획서를 읽고 도메인을 추산할 수 있다.
이렇게 추산된 도메인은 각각 관계를 가진다. 각 상황과 맥락에 따라 도메인의 관계는 다르게 표현이 된다.
즉, 책임과 역할이 달라진다. 이를 통해 우리는 객체지향적으로 REST API를 설계할 수 있다.

아래는 REST API를 객체지향적으로 설계한 예시다.

REST API 객체지향 설계 예시

  • POST: reviews
설명: 리뷰 등록
Response: Review
Class: ReviewController
  • GET: reviews
설명: 리뷰 리스트 조회
Response: Review
Class: ReviewController

  • POST teachers/{teacherId}/reviews
설명: 특정 선생님의 리뷰 등록
Response: TeacherReview
Class: TeacherController
  • GET teachers/{teacherId}/reviews
설명: 특정 선생님의 리뷰 정보 리스트
Response: List<TeacherReview>
Class: TeacherController

  • POST courses/{courseId}/reviews
설명: 특정 강자의 리뷰 등록
Response: CourseReview
Class: CourseController
  • GET courses/reiews
설명: 모든 강의의 리뷰 리스트
Response: List<CourseReview>
Class: CourseController
  • GET courses/{courseId}/reviews
설명: 특정 강의의 리뷰 리스트
Response: List<CourseReview>
Class: CourseController
  • GET courses/by-teacher/{teacherId}/reviews
설명: 선생님의 아이디를 기반으로 한 특정 강의의 리뷰 리스트
Response: List<CourseReview>
Class: CourseController

Path Parameter vs Query Parameter (Optional)

- Path Parameter: Require 
설명: 반드시 필요한 값들에 대해서는 Path Parameter를 통해 값을 받는다.
1. PK O
	ex) courses/{courseId}
    설명: PK는 by keyword를 path로 명시하지 않는다.
2. PK X
	ex) courses/by-teacher/{teacherId}
    설명: PK가 아닌 값은 by keyword를 path에 명시하여 작성한다.
    
- Query Parameter: Optional
설명: 선택적으로 필요한 값들에 대해서는 Query Parameter를 통해 값을 받는다.
	ex) teachers?page=&perPage=&isEncrypt=
    
이유: 1번 사진과, 2번 사진과 같이 검색을 하기 위해 여러 개의 값을 모두 받아야 검색이 가능한 경우가 있다.
     이럴때 Query Parameter 보다 Path Parameter를 통해 클라이언트에게 의도를 분명히 할 수 있다.
     또한 3번의 사진과 같이 여러개의 값을 받지 않더라도 필수 값에 대해 명확한 의도를 제시해야 하는 경우
     Path Parameter를 사용한다. 그 외의 선택적 조건을 통한 검색은 Query Parameter를 통해 값을 받는다.
     
     위의 정의를 다시 한번 살펴보고 아래의 예시를 다시한번 살펴보며 차이점을 이해해보자.
	 ex 1) active-shards/by-game/{game}/by-puuid/{puuid}
     ex 2) active-shards?game=&puuid=
     ex 3) teams/{teamId}
     ex 4) teams?teamId=

추가적으로 필자가 Design한 REST API

대표적으로 제시된 REST API Design Guide와 함께 개발을 하면서 필요하다고 생각을 한 부분에 대해 추가적으로 설계한 부분에 대해 정리하면 아래의 예시와 설명과 같다.

[1. open-api or api] /
[2. app or admin or 생략] /
[3. service_name] /
[4. API version] /
[5. 자원의 깊이, 관계, 책임] /

  • 예시
  POST api/admin/product/v1/teachers
  POST api/app/product/v1/course/{courseId}/reviews
  GET open-api/product/v1/teachers/{teacherId}/reviews
  • 설명
    1. open-api or api: 공개 API인지 보호된 API인지 구분
    2. app or admin or 생략: 보호된 API일때 클라이언트 권한 구분
    3. service_name: 서비스 이름 명시 (user, product, notification)
    4. API version: API 버전 명시 (v1, v2)
    5. 자원의 깊이, 관계, 책임 명시 (/teachers, /course/{courseId}/reviews)
    • 단, POST, GET, DELETE, PATCH를 통해 행위를 설명하기 불가능하거나 애매한 경우 행위를 명시한다.
        ex)
        POST lecture/{lectureId}/move
        설명: 강좌의 순서를 바꾸고 싶을때 사용
        Response: Lecture
        Class: LectureController

        이유: 행위를 명시하는 것은 특정 REST API 가이드에서는 위배되는 조건이다. 그러나 필자가 생각했을때
        REST API의 제일 중요한 원칙은 클라이언트와 서버 간의 통신이다. 즉, 직관적으로 API의 URI 및 메소드를
        보고 어떠한 행위가 일어나고, 어떠한 자원을 식별할 수 있다는것이 중요하다. 그래서 설명이 불가능하거나
        애매한 경우 행위를 명시하는 것으로 Design 했다.
profile
질문의 질이 답의 질을 결정한다.

1개의 댓글

comment-user-thumbnail
2024년 5월 25일

제가 ResfulAPI 원칙을 지키면서 엔드포인트 설계를 하려고 하는데, 궁금한게 있어서 질문 드립니다😆
회원권 구현을 하는데 금액권과 횟수권을 나눠 상속으로 구현하고 있습니다. type(PRICE/COUNT)에 따라 저장되는 데이터가 달라, type을 전달받는게 중요해서 아래처럼 구현했습니다.

  • [POST] /api/v1/membership-plans/{type}
  • [POST] /api/v1/memberships/{type}
  • [GET] /api/v1/membership-plans/{type}/{membershipPlanId}
  • [GET] /api/v1/memberships/{type}/{membershipId}
    블로그 글을 보고 나서는 아래처럼 작성하는 것이 더 Restful한 것이라고 생각합니다.
  • [POST] /api/v1/membership-plans/by-type/{type}
  • [POST] /api/v1/memberships/by-type/{type}
    문제는, 위 방식을 채택했을 때, GET 매핑에서는 어떻게 구현해야할 지 감이 안오네요.
  • [GET] /api/v1/membership-plans/by-type/{type}/{membershipPlanId}
  • [GET] /api/v1/memberships/by-type/{type}/{membershipId}

    위 API에 대해서는 어떻게 생각하시나요? 더 Restful하게 구현할 수 있는 방법이 있을까요?
답글 달기