이 API는 누가 부르나요?

조회·수정 API를 만들고 나서 이런 생각이 들었습니다. 다른 도메인의 서버가 이 데이터를 쓸 수도 있으니 인터널 API도 지금 함께 뚫어두는 게 낫지 않을까. 알아보니 원칙은 간단했습니다. 필요할 때 만든다. 그 한 줄 안에 API를 종류로 나누는 기준과, 만들지 않는 것도 설계라는 감각이 함께 들어 있었습니다.
같은 데이터를 내주는 API인데 왜 종류가 나뉘고, 그 기준은 무엇일까요?
이 글에서 다루는 내용
지난 글 끝에서 "권한을 어디에 거는가"를 한 층 더 밀면 API의 종류 이야기가 된다고 예고했습니다. 이번 글은 그 이야기입니다. API를 퍼블릭·오퍼레이션·인터널로 나누는 기준(호출자), 인터널 API의 경계가 네트워크가 아니라 도메인 사이에 그어지는 이유, 그리고 종류마다 인가의 주체가 사람에서 서비스로 바뀌는 구조를 다룹니다.
API는 세 종류입니다
기능적으로는 같은 데이터를 내주더라도 호출자가 누구냐에 따라 API는 세 종류로 나뉩니다.
| 종류 | 호출자 | 권한의 기준 |
|---|---|---|
| 퍼블릭(public) | 실제 고객의 클라이언트 | 최종 사용자의 역할과 리소스 범위 |
| 오퍼레이션(operation) | 어드민·백오피스 | 운영자 권한 |
| 인터널(internal) | 다른 도메인의 서버 (서버-투-서버) | 호출하는 서비스의 신원과 범위 |
처음엔 "API면 다 같은 API지, 왜 굳이 종류를 나누나" 싶었습니다. 그런데 이 셋은 호출자가 다르기 때문에 계약의 성격이 다릅니다. 퍼블릭은 불특정 다수의 고객이 부르니 하위 호환에 민감하고, 권한을 최종 사용자 단위로 판단합니다. 오퍼레이션은 운영자용이라 강력한 기능이 열리되 운영자 권한으로 잠급니다. 인터널은 사람이 아니라 서비스가 호출자이므로, 사용자 권한 대신 호출하는 서비스가 누구인지를 확인하는 것이 기준이 됩니다. 하나의 컨트롤러에 뭉쳐두면 이 세 가지 성격이 한 코드에 섞이게 됩니다.
프론트엔드에도 이 감각의 대응물이 있습니다. 패키지를 만들 때 public export(index.ts에서 내보내는 것)와 내부 모듈을 구분하는 것, 같은 코드라도 "누가 가져다 쓰는가"에 따라 계약의 엄격함이 달라지는 그 구분입니다. 파일 안 주석("내부용입니다, 직접 import 금지")이 아니라 모듈 경계로 구분을 그어버린다는 점까지 같습니다.
Recap
API는 호출자를 기준으로 퍼블릭(고객 클라이언트), 오퍼레이션(어드민), 인터널(다른 도메인 서버)로 나뉩니다. 셋은 권한을 무엇으로 판단하는지와 하위 호환의 민감도가 서로 달라서 분리되며, 패키지의 public export와 내부 모듈을 나누는 프론트엔드의 감각과 같은 결입니다.
인터널의 경계는 네트워크가 아니라 도메인입니다
셋 중에서 프론트엔드 개발자에게 가장 낯선 것이 인터널입니다. 처음엔 "내부망에서 부르는 API"라는 뜻인 줄 알았는데, 인터널의 경계는 네트워크(클러스터 안/밖)가 아니라 도메인과 도메인 사이입니다.
예를 들어 급여 도메인이 급여 명세를 만드는 상황을 생각해 보겠습니다. 급여 도메인이 가진 것은 구성원의 식별자뿐이고 이름과 소속 같은 구성원 정보는 프로필 도메인의 것입니다. 이때 클라이언트가 두 도메인을 각각 불러서 화면에서 합치는 게 아니라, 급여 서버가 프로필 도메인의 인터널 API를 불러 완성본을 만들어 내려줍니다. 서버가 서버를 부르는 것입니다.
이 목적은 프론트엔드가 이미 아는 패턴과 정확히 같습니다. BFF(Backend for Frontend)가 여러 서비스를 합쳐 뷰모델을 만들어 내려주는 것. 클라이언트가 여러 번 왕복하는 대신 서버가 조립을 대신한다는 목적이 동일하고, 다만 그 조립이 BFF라는 전용 층이 아니라 도메인 서버들 사이의 인터널 호출로 일어난다는 점이 다릅니다.
Recap
인터널 API의 경계는 네트워크가 아니라 도메인 사이입니다. 다른 도메인의 데이터가 필요할 때 클라이언트가 여러 번 부르는 게 아니라 서버가 서버를 불러 완성본을 내려주며, 이는 BFF가 여러 서비스를 조립해 뷰모델을 만드는 것과 같은 목적입니다.
종류마다 권한의 결이 다릅니다
세 종류를 나눠 두면 따라오는 원칙이 세 가지 있습니다.
첫째, 모든 API가 인터널을 갖는 것은 아닙니다. 필요할 때 만듭니다. 서두의 그 원칙입니다. 바로 YAGNI(You Aren't Gonna Need It)입니다. 요구가 없는데 미리 뚫어두면 쓰이지 않는 계약을 유지 보수하는 비용만 남습니다. 게다가 인터널은 뚫는 순간 공격 표면이 한 칸 늘어나므로, 만들지 않는 것 자체가 가장 값싼 방어이기도 합니다. 인터페이스는 소비자가 나타났을 때 소비자의 요구로 설계되는 것이 건강합니다.
둘째, "인터널은 읽기 전용" 같은 규칙을 미리 못 박기는 어렵습니다. 알림 생성처럼 인터널이 쓰기를 맡는 것이 자연스러운 경우가 있습니다. 다른 도메인의 서버가 "이 사용자에게 알림을 만들어 달라"고 부르는 식입니다. 상황마다 다른 것을 규칙으로 묶으면 규칙이 곧 기술 부채가 됩니다. 대신 쓰기를 여는 인터널은 그만큼 인가를 더 좁게 잡아야 합니다.
셋째, 사용자 단위 권한은 퍼블릭에서 판단합니다. 최종 사용자의 역할과 리소스 범위를 아는 자리는 퍼블릭 경계이고, 인터널은 그 판단이 끝난 뒤의 호출을 받습니다. 프론트엔드에서 라우트 가드가 페이지 진입을 막으면 그 안의 컴포넌트들이 권한을 다시 묻지 않는 것과 같은 배치입니다.
이 배치를 흔히 "현관에서 통제하고 안에서는 신뢰한다"고 표현하는데, 이 말은 절반만 맞습니다. 안쪽의 신뢰는 공짜가 아니라 "인터널을 부를 수 있는 주체가 실제로 통제되고 있다"는 전제를 사는 것이고, 그 전제는 스스로 유지되지 않습니다. 네트워크 안쪽에 있다는 사실은 호출자의 신원이 아닙니다. 클러스터 안의 워크로드 하나가 뚫리거나 설정 하나가 잘못 열리면, 그 전제는 조용히 사라집니다.
그래서 기준선은 이렇게 잡는 편이 낫습니다.
- 인터널도 호출자를 인증합니다. mTLS나 서비스 토큰처럼 "어느 서비스가 부르는가"를 증명하게 하고, 서비스마다 부를 수 있는 인터널의 목록을 좁힙니다.
- 테넌트와 리소스 범위는 인터널에서도 다시 검사합니다. 사용자 역할은 퍼블릭이 판단하더라도, "이 테넌트의 데이터를 이 호출이 만질 수 있는가"는 데이터를 가진 쪽이 답해야 합니다.
- "안에서는 신뢰"를 인가 생략으로 읽지 않습니다. 경계 하나가 뚫렸을 때 막을 것이 남아 있어야 합니다. 이것이 제로 트러스트가 말하는 바이고, 급여나 인사처럼 민감한 데이터를 다루는 도메인에서는 특히 그렇습니다.
정리하면 퍼블릭과 인터널의 차이는 인가의 유무가 아니라 인가의 주체입니다. 퍼블릭은 사람을 확인하고, 인터널은 서비스를 확인합니다. 어느 쪽도 확인을 건너뛰지 않습니다.
이렇게 어떤 API를 낼지 정하고 나면 다음 질문이 기다립니다. 그 API의 응답은 어떤 모양이어야 하는가, 모델을 닮아야 하는가. 다음 글에서 다루겠습니다.
Recap
인터널은 필요할 때 만들고(YAGNI), 상황마다 다른 것은 규칙화하지 않습니다. 사용자 단위 권한은 퍼블릭에서 판단하지만, 그것이 인터널에 인가가 없다는 뜻은 아닙니다. 퍼블릭과 인터널의 차이는 인가의 유무가 아니라 인가의 주체입니다. 퍼블릭은 사람을, 인터널은 호출하는 서비스를 확인하고, 테넌트와 리소스 범위는 데이터를 가진 쪽이 다시 검사합니다. "안에서는 신뢰"는 호출 주체가 통제되고 있다는 전제 위에서만 성립하며, 그 전제는 스스로 유지되지 않습니다.
FE ↔ BE 대응표
이 글에서 다룬 대응입니다.
| BE 개념 | FE에서 가장 가까운 것 | 대응의 핵심 |
|---|---|---|
| 퍼블릭 / 인터널 분리 | public export vs 내부 모듈 | 호출자에 따라 계약의 엄격함이 다르다 |
| 인터널 API | BFF의 서버 간 조립 | 클라이언트 대신 서버가 합친다 |
| 사용자 권한의 자리 | 라우트 가드 | 관문에서 사람을 확인한다 |
| 서비스 간 인증 | (대응물 없음) | 안쪽에서는 서비스를 확인한다 |
References
API 경계와 조립
만들지 않는 설계
서비스 간 인증과 신뢰 경계
