아래에서 말하는 API란, 웹개발에서 사용하는 REST API를 주로 칭하며, 이 외의 API들에는 적용되지 않는 말일 수도 있다.
화면을 위한 API
결국엔 API는 화면을 보여주기 위해 존재하는게 아닌가? 라는 생각을 한 적이 있다. 사실은 줄곧 그렇게 생각해왔다. 결국에 사용자가 보게되는 결과물은 클라이언트(프론트엔드)고, API는 어찌보면 이를 뒷받침 해주는 역할이니, 최대한 화면을 나타내기 편하도록 해주는 API가 좋은 API 아닐까? 라는 생각을 하곤 했다.
그래서 나는 최대한 화면에 맞춘 API를 설계해왔다. 엔드포인트 설계 단계에서부터 화면을 고려했고, 피그마를 보며 어떤 필드가 어떤 화면에 필요한지 확인하는데 주로 시간을 썼던 것 같다. 결론적으로, 프론트엔드 개발자가 1~2번의 API 호출만으로 화면의 모든 정보를 받아올 수 있도록 응답 필드를 구성했다. 처음에는 아주 만족스러웠다. 프론트엔드 개발자의 노고를 줄여준다는 느낌과, 화면과 API가 1:1로 맞아 떨어지는 느낌이 좋았다. 하지만 언제나 그렇듯, 서비스가 조금씩 커질 수록 생각이 바뀌었다.
화면을 위한 API가 가져오는 문제점
결국 나는 "화면을 위한 API" 에서 "도메인 위주의 API" 설계로 나의 방향을 바꾸었다. 아래는 이러한 생각을 하게 된 간단한 이유와 예시이다.
먼저, 화면에 맞추어 API를 설계하다보니, 화면이 바뀌면 API를 매번 수정해야하는 번거로움이 생겼다. 이는 단순히 귀찮음만의 문제는 아니다. 서비스의 방향이나 도메인이 함께 변한다면 API의 수정도 필연적일 것이다. 그러나, 단순 UI 변경만으로도 API가 영향을 받는다는게 뭔가 이상하게 느껴졌다.
백엔드 개발을 하면서 컨트롤러-서비스-레포지토리 같은 레이어 아키텍쳐를 공부하고, 한 계층의 변화가 다른 계층에 영향을 미치지 않도록 하는 것을 굉장히 중요시하면서, 정작 가장 중요한 레이어 중 하나인 프론트엔드를 간과하고 있었다는 사실을 깨달았다. 이를 깨닫고 나의 코드를 살펴보니, 화면의 변화가 코드 전체에 깊은 영향을 미치고 있었다. 그 예로, A에 대한 세부정보를 조회하는 API에서 연관된 B라는 도메인의 세부정보까지 모두 가져오고 있었다. 단순히 한 화면에 함께 나타난다는 이유만으로, A 도메인의 API에서 B 도메인의 역할까지 모두 담당하고 있었던 것이다.
좀 더 구체적으로, /orders/{orderId} 라는 API에서 주문 세부 정보와 결제 세부정보를 함께 반환다고 해보자.
// GET /orders/12345
{
"order_id": "12345",
"items": [
{"product_id": "A001", "quantity": 2},
{"product_id": "B002", "quantity": 1}
],
"total_price": 50000,
"payment": {
"payment_id": "P98765",
"method": "credit_card",
"transaction_id": "TXN123456",
"status": "PAID"
},
"shipping_address": "Seoul, Korea"
}
위와 같이 API를 설계할 경우 아래와 같은 문제들이 발생한다.
1) 결제 세부정보를 위해서 주문 정보 API를 호출해야 한다. 즉, 두 도메인 간의 결합도가 올라간다.
2) 주문 세부정보 화면에서 결제 정보가 필요없어진다면, 결제 세부정보는 무의미한 데이터가 된다.
3) 해당 API는 화면에 의존하기 때문에 서비스 기획(또는 디자인)이 바뀔 시 API 재활용이 불가능하다. (재사용성 하락)
4) 결국 결제 정보를 위한 API를 별도로 구현해야한다.
5) 백엔드 내부에서도 A 서비스에서 B를 호출하고, B에서도 A를 호출하게 되는 순환참조가 발생할 가능성이 높아진다.
6) 프론트엔드 입장에서 "결제 정보" API를 찾으려해도, 엔드포인트만 보고는 이를 알아차리기 어렵다. 결국에는 모든 API 문서를 뒤지거나 백엔드 개발자에게 질문을 해야하고, 불필요한 의사소통 비용을 발생시킨다.
나열하다보니 끝도 없는데, 결론적으로 화면을 기준으로 설계한 API는 너무나도 많은 단점들을 초래한다. 더 많은 예시를 들려 했지만, 위의 한 가지 사례만으로 너무 많은 문제가 나와 추가적인 예시가 불필요할 정도다.
결론적으로, 나는 위와 같은 단점로 인해 도메인 위주의 API를 설계하기로 결정하였고, 이를 적용해보자면 아래와 같다.
// GET /orders/12345
{
"order_id": "12345",
"items": [
{"product_id": "A001", "quantity": 2},
{"product_id": "B002", "quantity": 1}
],
"total_price": 50000,
"payment_id": "P98765", // 결제 ID만 제공
"shipping_address": "Seoul, Korea"
}
// GET /payments/P98765
{
"payment_id": "P98765",
"order_id": "12345",
"method": "credit_card",
"transaction_id": "TXN123456",
"status": "PAID"
}
이렇게 각각의 도메인별로 API를 분리함으로써, 도메인 간의 결합도가 낮아지고 API의 재사용성이 올라가는 효과를 얻을 수 있다.
CamelCase 응답
추가적으로, 나는 지금껏 snake case로 JSON 응답을 반환하고 있었다. 언제부터 그랬는지는 모르지만, snake case 가 일종의 컨벤션처럼 여겨졌었고, 이유는 모른채 이를 당연스럽게 여겨왔었다. 그러나 얼마 전에 Js를 다루다가 Js의 컨벤션도 카멜케이스라는 것을 깨닫고 갑작스레 의문이 들었고, IOS와 안드로이드 코드를 모두 살펴보니 모두 카멜케이스를 쓰고있었다.
더불어, 구글의 JSON 스타일 가이드를 찾아보니 카멜 케이스를 사용하라고 떡하니 명시되어있었다.
어디서부터 착각했는지는 모르겠지만, 몇 가지 사례를 더 찾아본 뒤에 CamelCase로 바꿀 생각이다. 지금이라도 알게 되어서 다행이라는 생각이 든다.
* 좀 더 찾아보니 여러 의견이 있는 것 같다. (언어별로, 회사별로 ..) 그러나 나의 경우에는 Java를 사용하고있고, 프론트엔드도 Kotlin이나 Swift이기 때문에 CamelCase가 적합해보인다.
세 줄 요약
- 도메인 위주의 API를 설계하자.
- Json 응답을 CamelCase로 반환해야겠다.
- 이유 없이 따라 하지 말고, 이유가 있더라도 타당한지 늘 의심해보자.
참고하면 좋은 글
1. API 설계
https://jojoldu.tistory.com/783
HTTP API 디자인 - URI편
마틴 파울러의 블로그를 가보면 Leonard Richardson이 제안한 HTTP API 성숙도 모델 (Richardson Maturity Model - RMM)을 소개한다.여기서는 다음과 같이 4단계를 설명한다.레벨 0: 하나의 URI를 정의하고 모든 작
jojoldu.tistory.com
나의 뒤죽박죽이던 기준들을 이 글을 통해서 조금이나마 바로잡을 수 있었다. 평소 API 설계에 대해 고민이 많은 사람에게 도움이 되는 글이다.
2. BFF 패턴
https://tech.kakaopay.com/post/bff_webflux_coroutine/
WebFlux와 코루틴으로 BFF(Backend For Frontend) 구현하기 | 카카오페이 기술 블로그
카카오페이 오프라인 결제 신규 서비스인 ‘내 주변 매장 찾기' 서비스를 개발하기 위해 BFF 서버 구조에서 WebFlux, 코루틴으로 비동기 API 서버 개발 경험을 공유합니다.
tech.kakaopay.com
https://www.youtube.com/watch?v=Zs3jVelp0L8
그렇다면 다시 처음으로 돌아가서, 프론트엔드에게 화면에 딱 맞는 데이터를 짠! 하고 제공할 수는 없을까? 라는 생각에 검색을 좀 해봤다. 그 결과 BFF(Backend for Frontend) 패턴이라는 것을 알게 되었다. 다만 BFF 패턴은 단순히 화면만을 위한 것이 아니라, 다중 플랫폼 + MSA 환경에서 효율적이고 효과적인 응답을 위해 사용하는 패턴인 것 같다. 일종의 게이트웨이나 인터페이스 같달까..? 단일 플랫폼, 단일 서버인 내 환경에서 하기에는 오버엔지니어링인 것 같지만, 알아두면 나중에 도움이 될 것 같다. 추가로, 토스에서도 이와 비슷한 내용의 영상이 있어 함께 첨부한다.
'개발' 카테고리의 다른 글
| [JAVA] JDBC에서 JPA(Hibernate)까지 (0) | 2025.07.10 |
|---|---|
| [트러블슈팅] Soft delete와 카카오 연결끊기(unlink) 400 오류 (1) | 2025.07.10 |
| [Python] Pipenv 부터 AWS Lambda 배포까지 (2) (1) | 2025.07.10 |
| [Python] Pipenv 부터 AWS Lambda 배포까지 (1) (0) | 2025.07.10 |
| [JPA] JPA 연관관계 간단 정리(ManyToOne, OneToMany) (0) | 2024.07.24 |