[Spring Boot / 백엔드] API 설계하기 - API 명세서, RESTful API

백엔드 개발을 공부하다 보면 기능을 구현하는 것만큼이나 API를 어떻게 설계할지 고민하게 됩니다.

처음에는 저도 API를 단순히 “프론트와 서버가 통신하기 위한 주소” 정도로만 생각했습니다.

그런데 직접 기능을 만들다 보니, 같은 기능이어도

어떤 URL로 설계할지,

어떤 HTTP 메서드를 써야 할지,

어떤 데이터를 어디에 담아 보내야 할지에 따라

협업 난이도와 유지보수성이 꽤 달라진다는 걸 느꼈습니다. 그래서 이번 글에서는 워크북 내용을 바탕으로

API 명세서가 무엇인지,

RESTful API는 어떻게 설계하는지,

그리고 실제로 어떤 기준으로 API를 나누고 문서화해야 하는지를 정리해보려고 합니다.

이번 글에서 다룰 내용은 다음과 같습니다.

  • API란 무엇인가
  • REST API와 RESTful한 설계
  • HTTP 메서드와 멱등성
  • API Endpoint 설계 기준
  • Path Variable, Query Parameter, Request Body, Request Header
  • API 명세서는 왜 필요한가

API란?

API는 Application Programming Interface의 약자입니다.

처음 보면 조금 추상적으로 느껴지지만, 쉽게 말하면 프로그램끼리 소통할 수 있게 해주는 인터페이스라고 볼 수 있습니다.

우리가 자바에서 System.out.println() 을 쓰거나, 파이썬에서 print() 를 사용할 때 내부적으로 콘솔에 어떤 방식으로 출력되는지 전부 알 필요는 없습니다. 그냥 정해진 방식으로 호출하면 원하는 동작이 일어나죠.

이처럼 API도 복잡한 내부 구현은 감추고, 필요한 기능을 정해진 방식으로 사용할 수 있게 해주는 것이라고 이해하면 훨씬 편합니다.

그리고 웹 백엔드에서 자주 말하는 API는 대부분 클라이언트와 서버가 데이터를 주고받기 위한 인터페이스를 의미합니다.

즉, 프론트엔드는 API를 통해 서버에게 요청을 보내고, 서버는 그 요청을 처리한 뒤 결과를 응답합니다.


REST API란?

REST는 Representational State Transfer의 약자입니다.

조금 어렵게 들리지만,

웹에서는 보통 HTTP 메서드와 자원(Resource)을 기준으로 API를 설계하는 방식 정도로 이해하면 됩니다.

핵심은 단순합니다.

  • URL에는 무엇을 다루는지(자원)
  • HTTP 메서드에는 무슨 동작을 하는지
  • 요청 데이터에는 처리에 필요한 정보

를 담는 방식입니다.

예를 들어 사용자를 조회하는 API라면

GET /users/{userId}

이런 식으로 설계할 수 있습니다.

여기서

  • users 는 자원
  • GET 은 조회
  • {userId} 는 특정 사용자를 식별하는 값

을 의미합니다. 즉, REST API는 주소는 자원을 표현하고, 행위는 HTTP 메서드로 표현하는 방식이라고 볼 수 있습니다.


HTTP 메서드 종류

REST API를 설계할 때는 HTTP 메서드를 잘 구분해서 써야 합니다.

GET

GET은 리소스를 조회할 때 사용합니다.

예를 들어

GET /missions
GET /users/{userId}

처럼 사용할 수 있습니다. GET은 데이터를 읽어오는 요청이기 때문에 같은 요청을 여러 번 보내더라도 서버의 데이터가 바뀌지 않아야 합니다. 또한 GET 요청은 보통 검색 조건이나 페이지 번호 같은 값을 Query Parameter로 전달합니다.

 

예를 들면

GET /missions?region=anam&page=1

처럼 사용할 수 있습니다.

POST

POST는 주로 새로운 리소스를 생성하거나, 혹은 서버에 특정 처리를 요청할 때 사용합니다.

예를 들어 회원가입은

POST /auth/users

로그인은

POST /auth/login

처럼 설계할 수 있습니다. POST는 보통 민감한 정보나 생성에 필요한 데이터를 Request Body에 담아 보냅니다.

PUT

PUT은 리소스를 전체 교체하는 느낌에 가깝습니다.

예를 들어 사용자 정보를 통째로 덮어쓴다면 PUT으로 표현할 수 있습니다.

PUT /users/{userId}

다만 실무에서는 전체 교체보다 부분 수정이 더 자주 일어나기 때문에 PUT보다는 PATCH를 더 많이 보는 경우도 있습니다.

PATCH

PATCH는 리소스의 일부만 수정할 때 사용합니다.

예를 들어 닉네임만 변경한다면

PATCH /users/{userId}

처럼 설계할 수 있습니다. 즉, 기존 데이터 전체를 갈아끼우는 것이 아니라 일부 필드만 바꾸는 요청입니다.

DELETE

DELETE는 리소스를 삭제할 때 사용합니다.

DELETE /users/{userId}

처럼 표현할 수 있습니다. 내부적으로 실제 삭제를 하든, Soft Delete처럼 삭제 상태만 표시하든, 클라이언트 입장에서는 “삭제 요청”이기 때문에 DELETE로 표현하는 것이 자연스럽습니다.


멱등성이란?

API를 공부하다 보면 멱등성(Idempotency) 이라는 말을 자주 보게 됩니다.

멱등성은 같은 요청을 여러 번 보내도 결과가 같게 유지되는 성질을 의미합니다.

예를 들어

GET /users/1

을 여러 번 호출해도 사용자 조회 결과만 반환될 뿐, 데이터 자체가 바뀌지는 않습니다.

 

DELETE /users/1

다음 요청을 한 번 삭제한 뒤 같은 요청을 다시 보내면

이미 삭제된 상태일 뿐, 결과가 더 크게 달라지지는 않습니다.

반면 POST는 보통 멱등하지 않습니다.

POST /users

를 여러 번 보내면 사용자가 여러 명 생성될 수도 있기 때문입니다.

멱등성이 중요한 이유는 네트워크 오류나 재시도 상황에서

같은 요청을 다시 보내도 안전한지 판단하는 기준이 되기 때문입니다.

표로 정리하면 다음과 같습니다!

메서드 역할 멱등성 특징
GET 조회 O 서버의 데이터를 변경하지 않음 (Safe)
POST 생성 X 수행할 때마다 새로운 리소스가 생성됨 (중복 발생 가능)
PUT 전체 수정 O 요청 보낸 데이터로 리소스를 통째로 갈아 끼움
PATCH 부분 수정 리소스의 일부만 변경. 설계에 따라 멱등할 수도, 아닐 수도 있음
DELETE 삭제 O 이미 삭제된 리소스를 다시 삭제해도 결과(삭제됨)는 같음

 


RESTful API Endpoint는 어떻게 설계할까?

API를 설계할 때 가장 먼저 보게 되는 것이 Endpoint입니다.

Endpoint는 쉽게 말해 클라이언트가 어떤 자원에 접근하기 위해 호출하는 주소입니다.

예를 들어

GET /users/3
POST /auth/login
PATCH /users/me

이런 것들이 모두 API Endpoint입니다.

RESTful하게 설계하려면 보통 아래 기준을 많이 따릅니다.

1. URI에는 동사보다 명사를 사용한다

좋지 않은 예시:

/getUser
/createMission
/deleteReview

좋은 예시:

/users/{userId}
 /missions
 /reviews/{reviewId}

행위는 URL이 아니라 HTTP 메서드가 표현하는 것이 더 자연스럽습니다.

2. 자원은 보통 복수형으로 쓴다

/users
/missions
/reviews

처럼 설계하면 일관성이 좋습니다.

3. 특정 리소스는 식별자를 붙여 표현한다

GET /users/{userId}
GET /missions/{missionId}

처럼 특정 대상을 식별할 수 있어야 합니다.

4. 자원 간 관계가 있으면 계층적으로 표현할 수 있다

예를 들어 특정 사용자의 미션 목록을 조회한다면

GET /users/{userId}/missions

처럼 설계할 수 있습니다. 즉, 누구의 미션인지가 중요하다면 사용자 아래에 미션을 두는 방식이 자연스럽습니다.


회원가입, 로그인, 탈퇴는 어떻게 설계할까?

RESTful 설계를 처음 적용할 때 가장 헷갈리는 부분 중 하나가 회원가입, 로그인, 탈퇴 같은 인증 관련 API라고 생각합니다.

회원가입

회원가입은 새로운 사용자를 생성하는 동작이므로 보통

POST /auth/users

처럼 설계할 수 있습니다.

이때 아직 사용자가 생성되기 전이기 때문에 /auth/users/{userId} 처럼 설계하기는 어렵습니다.

왜냐하면 요청 시점에는 아직 식별할 ID가 없기 때문입니다.

로그인

로그인은 새로운 자원을 조회한다기보다 서버에 로그인 처리를 요청하는 성격이 강합니다.

그래서

POST /auth/login

또는

POST /auth/users/login

처럼 설계할 수 있습니다. 완벽하게 REST 원칙만 고집하기보다 의도가 잘 드러나는지가 더 중요하다고 느꼈습니다.

회원 탈퇴

회원 탈퇴는 보통

DELETE /users/me

또는

DELETE /users/{userId}

처럼 설계할 수 있습니다. 다만 서비스 요구사항에 따라

실제로 DB에서 완전히 삭제하지 않고 상태값만 바꾸는 Soft Delete를 사용할 수도 있습니다.

그렇더라도 API 레벨에서는 클라이언트가 “탈퇴 요청”을 보내는 것이므로 DELETE가 가장 자연스럽습니다.


리소스 간 연관 관계가 있을 때

API 설계는 결국 데이터 구조와도 연결됩니다.

예를 들어 한 명의 사용자가 여러 개의 미션을 수행할 수 있다면

다음과 같이 설계할 수 있습니다.

GET /users/{userId}/missions

이렇게 하면

“특정 사용자의 미션 목록을 조회한다”는 의미가 훨씬 분명해집니다.

그런데 N:M 관계에서는 조금 더 애매해질 수 있습니다.

대표적인 예가 게시글과 해시태그입니다.

  • 하나의 게시글은 여러 해시태그를 가질 수 있고
  • 하나의 해시태그는 여러 게시글에 연결될 수 있습니다

이런 경우에는 무조건 정답이 있다기보다

비즈니스적으로 어떤 자원이 더 중심인지를 보고 결정하는 것이 좋습니다.

예를 들어 게시글이 중심이라면

/articles/{articleId}/hashtags

처럼 표현하는 것이 더 자연스러울 수 있습니다.

즉, API 설계는 단순히 문법 문제가 아니라

서비스에서 어떤 자원을 중심으로 보는지와도 연결된다고 느꼈습니다.


API 설계에서 꼭 구분해야 하는 4가지

실제로 API를 만들 때는 Endpoint만 정한다고 끝나지 않습니다.

서버가 요청을 제대로 처리하려면 어떤 데이터를 어디에 담아 보낼지도 함께 정해야 합니다.

여기서 자주 나오는 것이 다음 4가지입니다.

  • Path Variable
  • Query Parameter
  • Request Body
  • Request Header

Path Variable

Path Variable은 특정 리소스 하나를 식별할 때 사용합니다.

예를 들어 게시글 상세 조회라면

GET /articles/{articleId}

처럼 설계할 수 있습니다. 여기서 {articleId} 는

“어떤 게시글을 조회할 것인지”를 나타내는 값입니다.

즉,

GET /articles/4

라고 요청이 들어오면

서버는 articleId가 4인 게시글을 찾게 됩니다.

Query Parameter

Query Parameter는 보통

검색 조건, 정렬, 필터링, 페이징 같은 값들을 전달할 때 사용합니다.

예를 들어 지역별 미션 목록 조회라면

GET /missions?region=anam

처럼 설계할 수 있습니다.

여러 조건도 함께 전달할 수 있습니다.

GET /missions?region=anam&status=ongoing&page=1

즉, Query Parameter는

특정 하나를 정확히 지목하기보다는 목록을 조건에 맞게 조회하기 위한 값에 가깝습니다.

Request Body

Request Body는 주로

POST, PUT, PATCH처럼 서버에 전달할 데이터가 많은 경우 사용합니다.

예를 들어 회원가입이라면

{
  "name": "홍길동",
  "phoneNumber": "010-1234-5678",
  "nickname": "min"
}

처럼 Request Body에 담아 보낼 수 있습니다. 특히 아이디, 비밀번호, 이메일 같은 값은 URL에 노출하기보다 Body에 담는 것이 일반적입니다.

Request Header

Request Header는

요청에 대한 부가 정보(메타데이터) 를 담는 영역입니다.

대표적으로 많이 보는 것은 다음과 같습니다.

Content-Type: application/json
Authorization: Bearer {accessToken}
  • Content-Type 은 바디 데이터 형식을 의미하고
  • Authorization 은 인증 정보를 전달할 때 많이 사용합니다

즉, Header는

요청의 본문 내용 자체라기보다는

이 요청이 어떤 방식으로 보내졌는지 알려주는 정보라고 볼 수 있습니다.


API 명세서는 왜 필요할까?

API를 설계했다면

그 내용을 문서로 정리해야 프론트엔드와 원활하게 협업할 수 있습니다.

이 문서를 API 명세서라고 합니다.

API 명세서에는 보통 이런 내용이 들어갑니다.

  • API Endpoint
  • HTTP 메서드
  • Path Variable
  • Query Parameter
  • Request Header
  • Request Body
  • 응답 형태

예를 들어 닉네임 변경 API를 명세한다면 다음처럼 정리할 수 있습니다.

예시) 닉네임 변경 API

API Endpoint

PATCH /users/me

Request Header

Authorization: Bearer {accessToken}
Content-Type: application/json

Request Body

{
  "nickname": "archive99"
}

Query Parameter

없음

이렇게 정리해두면

프론트엔드는 어떤 URL로 요청해야 하는지,

어떤 토큰이 필요한지,

어떤 형태의 JSON을 보내야 하는지 한눈에 파악할 수 있습니다.

즉, API 명세서는 단순한 문서가 아니라

협업 비용을 줄여주는 약속서에 가깝다고 느꼈습니다.


노션 명세서와 Swagger는 어떻게 다를까?

API 명세서는 보통 노션이나 Swagger로 정리하는 경우가 많습니다.

노션으로 작성할 때

노션으로 작성한 api 명세서

 

노션으로 작성한 api 명세서 2

장점은 구현 전에도 빠르게 문서를 공유할 수 있다는 점입니다.

즉, 백엔드가 아직 완성되지 않았더라도 프론트와 먼저 API 구조를 맞춰볼 수 있습니다.

다만 단점은 직접 수정하고 계속 최신 상태를 유지해야 한다는 점입니다.

Swagger를 사용할 때

Swagger는 구현된 API를 기준으로

문서를 자동화해서 보여주는 도구에 가깝습니다.

장점은 실제 코드와 문서가 비교적 잘 맞아떨어진다는 점입니다.

하지만 구현 전 단계에서는 프론트가 미리 확인하기 어렵다는 한계가 있을 수 있습니다.

그래서 실제 협업에서는 초기 설계 단계에서는 노션, 구현 이후에는 Swagger를 함께 활용하는 방식이 가장 실용적이라고 느꼈습니다.


마무리

이번 내용을 정리하면서 느낀 것은 API 설계는 단순히 URL을 만드는 작업이 아니라는 점이었습니다.

어떤 자원을 중심으로 볼지, 어떤 HTTP 메서드를 선택할지, 어떤 데이터를 어디에 담아 보낼지,

그리고 그것을 어떻게 문서화할지까지 모두 포함해서 비로소 좋은 API 설계가 된다고 생각합니다.

특히 백엔드는 혼자만 보는 코드가 아니라 프론트엔드, 기획자, 다른 백엔드 개발자와 함께 맞춰가는 작업이기 때문에

명확하게 설계하고, 명확하게 문서화하는 습관이 정말 중요하다고 느꼈습니다.

저도 처음에는 POST, GET, PATCH를 단순히 외우는 수준에 가까웠는데, 이번 정리를 통해 “이 기능은 왜 이 방식으로 설계해야 하지?”를 더 많이 고민하게 되었습니다.