사전 서비스 정의
개념

서비스 정의

gabury1고친 사람 github-actions[bot]

서비스 정의는 다른 컴퓨터에 있는 함수를 부르기 전에 그 이름과 주고받는 값을 정해 두는 약속입니다. 서버와 클라이언트는 이 약속을 적은 파일 하나를 함께 봅니다. 전용 컴파일러가 그 파일을 읽어 양쪽 코드의 뼈대를 만들어 줍니다.

쉽고 빠른 이해

서비스 정의는 다른 컴퓨터에 있는 함수를 무슨 이름으로 부르고 무엇을 주고받는지 미리 정해 둡니다. 회원 정보를 다루는 서버라면 「이름을 주면 사용자 정보를 돌려주는 사용자 찾기」 같은 줄이 그 안에 듭니다.

이 약속이 없으면 클라이언트는 함수 이름과 넘길 값을 말이나 문서로 전해 듣고 맞춰야 합니다. 한쪽이 이름을 바꾸면 프로그램을 돌려 봐야 어긋난 줄 압니다.

  1. 서비스 이름과 그 안의 함수를 파일 하나에 적습니다. 함수마다 받는 값과 돌려주는 값도 적습니다
  2. 도구가 이 파일을 읽어 클라이언트가 부를 코드와 서버가 채울 코드를 만듭니다
  3. 클라이언트가 만들어진 코드를 내 함수처럼 부르면, 그 호출이 네트워크를 건너 서버의 구현에 닿습니다

대가도 있습니다. 정의를 고치면 양쪽 코드를 다시 만들어야 합니다. 이미 배포된 옛 클라이언트가 있으면 함수 이름을 바꾸거나 지우는 순간 그 클라이언트의 호출이 실패합니다.

이 방식은 한 회사 안의 서버끼리 부를 때 잘 맞습니다. 한 사람이 같은 언어로 짜는 작은 프로그램에서는 드는 품이 얻는 것보다 클 수 있습니다.

상세

식당의 메뉴판을 떠올려 봅시다. 손님은 주방에 들어가지 않고 메뉴판을 보고 고릅니다. 메뉴판에는 음식 이름과 무엇이 나오는지가 적혀 있습니다. 주방은 메뉴판에 적힌 음식만 주문으로 받습니다.

서비스 정의가 이 메뉴판입니다. 함수를 부르는 쪽은 손님처럼 정의에 적힌 함수만 고릅니다. 함수를 가진 쪽은 주방처럼 정의에 적힌 함수만 받습니다.

서비스 정의는 원격에서 부를 함수들을 한 묶음으로 적어 둡니다. 묶음에는 이름이 붙습니다. 함수마다 이름과 받는 값, 돌려주는 값이 적힙니다. 회원 정보를 다루는 서버라면 「사용자 찾기」와 「사용자 만들기」 두 함수를 「사용자 서비스」 하나로 묶어 적습니다.

이 절은 먼저 원격 호출이 무엇이고 왜 약속이 필요한지 짚습니다. 이어서 서비스 정의를 적는 언어와 한 벌의 생김새를 봅니다. 그 정의에서 코드가 만들어지는 과정이 그다음입니다. 끝으로 정의를 고칠 때 깨지는 것, 이 방식이 맞는 때와 안 맞는 때, 같은 이름의 다른 뜻을 다룹니다.

원격 프로시저 호출

원격 프로시저 호출(RPC, Remote Procedure Call)은 다른 컴퓨터에 있는 함수를 내 코드의 함수처럼 부르는 방식입니다. 호출하는 쪽을 클라이언트, 함수를 가진 쪽을 서버라고 부릅니다. 함수 이름과 넘길 값이 네트워크로 건너갑니다. 서버가 함수를 돌린 결과가 되돌아옵니다.

내 함수처럼 부르려면 클라이언트가 서버 함수의 이름과 모양을 알아야 합니다. 그런데 서버와 클라이언트는 짜는 팀도, 쓰는 언어도 다를 수 있습니다. 둘이 함께 볼 문서가 없으면 함수 이름이나 값 하나가 어긋나도 돌려 봐야 압니다.

서비스 정의가 그 문서입니다. 서버는 정의에 적힌 함수를 구현할 책임을 집니다. 클라이언트는 정의에 적힌 함수만 부릅니다. 이렇게 두 쪽이 지키기로 한 약속을 계약이라고도 부릅니다.

인터페이스 정의 언어

서비스 정의는 대개 특정 프로그래밍 언어가 아닌 따로 마련된 문법으로 적습니다. 이 문법을 인터페이스 정의 언어(IDL, Interface Definition Language)라고 부릅니다. 서버는 Go 로, 클라이언트는 자바로 짜여 있어도 둘은 같은 IDL 파일 하나를 봅니다.

IDL 은 여럿입니다. 함수 묶음을 부르는 낱말만 다릅니다. 하는 일은 같습니다.

문법 함수 묶음을 적는 말 함수 하나
프로토콜 버퍼 service rpc 로 시작하는 선언
Apache Thrift service 함수 선언 한 줄
CORBA(Common Object Request Broker Architecture) IDL interface 오퍼레이션(함수) 선언
ONC RPC(Open Network Computing RPC) 언어 program(묶음) 안의 version(판 번호) 프로시저(함수) 선언

이름은 갈려도 「묶음 하나에 함수 여럿」이라는 뼈대는 같습니다. 이 편은 아래부터 프로토콜 버퍼의 문법으로 봅니다.

gRPC(gRPC Remote Procedure Calls)는 원격 호출을 실제로 주고받게 해 주는 프레임워크입니다. 프로토콜 버퍼로 적은 서비스 정의를 읽습니다. 그 정의에 적힌 함수대로 요청과 응답을 네트워크로 나릅니다.

서비스 정의 한 벌

아래는 사용자 서비스를 proto 파일로 적은 것입니다. proto 파일은 프로토콜 버퍼의 정의를 담는 텍스트 파일입니다. 오른쪽 주석이 그 줄이 정하는 것입니다.

proto
service UserService {     // 묶음 이름
  rpc GetUser             // 함수 이름
    (GetUserRequest)      // 받는 값
    returns (User);       // 돌려주는 값
}

service 블록 하나가 서비스 하나입니다. 그 안에서 rpc 로 시작하는 선언 하나가 원격에서 부를 수 있는 함수 하나입니다. 이 함수를 rpc 메서드라고 부릅니다. 메서드는 몇 개든 한 블록 안에 더 적을 수 있습니다.

받는 값과 돌려주는 값은 메시지 하나씩입니다. 메시지는 이름 붙은 값의 묶음입니다. 아래는 받는 값으로 쓴 GetUserRequest 메시지입니다.

proto
message GetUserRequest {
  string name = 1;        // 찾을 이름
}

메시지 안의 한 줄을 필드라고 부릅니다. = 1 은 값이 아니라 필드에 붙인 번호입니다. 메시지를 바이트로 바꿀 때 필드 이름 대신 이 번호가 실립니다.

인자를 메시지 하나로 감싸 두면 나중에 받을 값이 늘어도 메시지에 필드를 더하면 됩니다. 함수의 모양은 바뀌지 않습니다.

호출 방식 네 가지

rpc 선언은 한 번 묻고 한 번 답하는 꼴만 적지 않습니다. 받는 값이나 돌려주는 값 앞에 stream 을 붙이면 메시지를 여러 개 잇달아 보낸다는 뜻이 됩니다. 이렇게 메시지를 끊지 않고 이어 흘려보내는 것을 스트리밍이라고 부릅니다.

선언 클라이언트가 보내는 것 서버가 돌려주는 것 쓰는 예
rpc A (Req) returns (Res) 메시지 하나 메시지 하나 사용자 한 명 찾기
rpc A (Req) returns (stream Res) 하나 여럿 검색 결과를 찾는 대로 보내기
rpc A (stream Req) returns (Res) 여럿 하나 파일을 조각으로 올리고 결과 하나 받기
rpc A (stream Req) returns (stream Res) 여럿 여럿 채팅

첫째 줄처럼 하나 보내고 하나 받는 꼴을 단항 RPC라고 부릅니다. 나머지 셋은 호출 하나가 열려 있는 동안 메시지를 여러 번 주고받을 수 있는 통신이 있어야 씁니다. gRPC 는 그런 통신인 HTTP/2(HyperText Transfer Protocol 2) 위에서 돌기 때문에 네 꼴을 다 나릅니다.

정의에서 코드가 만들어지는 과정

서비스 정의 파일은 그 자체로는 실행되지 않습니다. 컴파일러가 이 파일을 읽어 언어별 코드를 만듭니다. 이렇게 정의 파일에서 코드를 뽑아내는 일을 코드 생성이라고 부릅니다.

프로토콜 버퍼에서는 protoc 이 그 컴파일러입니다. 자바 코드가 필요하면 자바 코드를, 파이썬 코드가 필요하면 파이썬 코드를 같은 proto 파일에서 만듭니다.

만들어지는 코드는 두 쪽으로 갈립니다. 클라이언트 쪽에는 스텁이 생깁니다. 스텁은 서비스에 적힌 rpc 메서드와 같은 이름의 메서드를 가진 객체입니다. 앞의 정의라면 GetUser 에 맞춰 getUser 메서드가 생깁니다. 클라이언트는 이 객체의 메서드를 부르기만 하면 됩니다.

스텁의 메서드는 넘겨받은 메시지를 바이트로 바꿔 서버로 보냅니다. 값을 네트워크로 보낼 수 있는 바이트로 바꾸는 일을 직렬화라고 부릅니다. 응답이 오면 거꾸로 바이트를 메시지로 되돌려 돌려줍니다.

서버 쪽에는 채워 넣을 틀이 생깁니다. 서비스의 메서드가 본문 없이 선언된 클래스입니다. 개발자는 이 클래스를 상속해서 메서드 본문만 채웁니다. 이 틀을 스켈레톤이라고 부릅니다.

스켈레톤은 틀이면서 요청을 받는 입구이기도 합니다. 네트워크로 온 요청의 바이트를 메시지로 되돌린 뒤, 개발자가 채운 메서드에 넘깁니다. 아래 그림의 「구현 코드」가 그 메서드입니다. 메서드가 돌려준 값은 다시 바이트로 바꿔 클라이언트에 보냅니다.

한 번의 호출이 지나가는 길을 그리면 이렇습니다.

sequenceDiagram
    participant 클라이언트 코드
    participant 스텁
    participant 스켈레톤
    participant 구현 코드
    클라이언트 코드->>스텁: GetUser 호출
    스텁->>스켈레톤: 직렬화한 요청을 네트워크로
    스켈레톤->>구현 코드: 메시지로 되돌려 넘긴다
    구현 코드-->>스켈레톤: User 메시지
    스켈레톤-->>스텁: 직렬화한 응답
    스텁-->>클라이언트 코드: User 객체

그림에서 네트워크를 건너는 것은 스텁과 스켈레톤 사이뿐입니다. 클라이언트 코드와 구현 코드는 상대가 다른 컴퓨터에 있다는 것을 모릅니다. 둘 다 서비스 정의에서 만들어진 같은 메시지 타입만 봅니다.

자바로 만든 스텁을 부르는 코드는 대략 아래와 같습니다. 서버가 kim 의 정보를 돌려줬다고 하면 마지막 줄의 결과는 오른쪽 주석과 같습니다.

Java
var req = GetUserRequest.newBuilder()
    .setName("kim")
    .build();
User user = stub.getUser(req);
user.getName();                // "kim"

getUser 는 평범한 메서드 호출처럼 생겼습니다. 그 안에서 직렬화와 네트워크 왕복이 일어납니다. 그래서 일반 메서드와 달리 네트워크가 끊기면 실패할 수 있습니다. 응답을 기다리는 시간도 듭니다.

정의를 고칠 때 깨지는 것

서비스 정의는 한번 배포하면 마음대로 못 고칩니다. 옛 정의로 만든 스텁을 쓰는 클라이언트가 이미 돌고 있기 때문입니다. 새 정의가 옛 클라이언트의 호출을 계속 받아 주는 성질을 하위 호환이라고 부릅니다.

gRPC 의 호출에는 서비스 이름과 메서드 이름이 실려 갑니다. 서버는 그 이름으로 어느 구현을 부를지 찾습니다. 그래서 이름을 바꾸거나 메서드를 지우면 옛 클라이언트의 호출은 찾아갈 메서드가 없어 실패합니다.

바꾸는 것 옛 클라이언트
새 메서드를 더한다 안 깨진다. 옛 클라이언트는 새 메서드를 모를 뿐이다
받는 메시지에 새 필드를 더한다 안 깨진다. 옛 클라이언트가 안 채운 필드는 비어 있는 값으로 읽힌다
메서드 이름을 바꾸거나 지운다 깨진다. 옛 이름으로 온 호출이 갈 곳이 없다
쓰던 필드 번호를 다른 필드에 준다 깨진다. 옛 클라이언트가 그 번호를 옛 뜻으로 보낸다

크게 바꿔야 할 때는 옛 서비스를 두고 새 서비스를 따로 세웁니다. 이때 이름이 겹치지 않게 해 주는 것이 패키지입니다. proto 파일은 맨 위에 패키지 이름을 적습니다. 서비스 이름 앞에는 이 패키지 이름이 붙습니다.

그래서 패키지가 다르면 서비스 이름이 같아도 겹치지 않습니다. 패키지 이름에 v1 · v2 같은 판 번호를 넣으면 같은 UserService 를 옛판과 새판으로 한 서버에서 나란히 돌릴 수 있습니다.

정의를 먼저 쓰는 개발 방식

서비스 정의를 쓰는 순서는 둘로 갈립니다. 정의를 먼저 쓰고 거기서 코드를 만드는 방식을 계약 우선 개발이라고 부릅니다. 코드를 먼저 짜고 거기서 정의를 뽑아내는 방식은 코드 우선 개발이라고 부릅니다.

gRPC 는 앞의 방식을 씁니다. proto 파일 없이는 스텁도 스켈레톤도 생기지 않기 때문입니다. 그래서 서버를 짜기 전에 proto 파일부터 놓고 함수 이름과 메시지를 맞출 수 있습니다.

웹에서 흔히 쓰는 HTTP API(Application Programming Interface)에도 같은 생각이 있습니다. OpenAPI 문서가 주소마다 받는 요청과 돌려주는 응답의 모양을 적습니다. 도구가 그 문서에서 클라이언트 코드를 만듭니다. 이름은 다르지만 서비스 정의와 하는 일이 같습니다.

서비스 정의가 맞는 때와 안 맞는 때

서비스 정의와 코드 생성은 두 쪽이 같은 정의 파일을 받아 코드를 만들 수 있을 때 값을 냅니다. 한 회사 안의 서버끼리 부르는 경우가 그렇습니다. 정의 파일을 고치면 컴파일 단계에서 어긋난 호출이 드러납니다.

클라이언트가 누군지 모르는 공개 API 라면 사정이 다릅니다. 부르는 쪽마다 정의 파일을 받아 코드를 만들어야 하는 부담이 생깁니다.

브라우저도 걸림돌입니다. 브라우저 안의 코드는 HTTP/2 의 세부를 직접 다룰 수 없어서 gRPC 호출을 그대로 보내지 못합니다. 그래서 브라우저와 서버 사이에 프록시를 둡니다. 프록시는 브라우저가 보낼 수 있는 꼴의 요청을 받아 gRPC 호출로 바꿔 서버에 넘깁니다.

정의 파일이 하나 더 생기는 대가도 있습니다. 함수 하나를 더하려 해도 정의를 고쳐야 합니다. 그다음 코드를 다시 만들어 양쪽에 배포합니다. 서버와 클라이언트를 한 사람이 같은 언어로 짜는 작은 프로그램에서는 이 단계가 얻는 것보다 더 클 수 있습니다.

의존성 주입의 서비스 정의

의존성 주입은 객체가 쓸 다른 객체를 스스로 만들지 않고 밖에서 넣어 받는 방식입니다. 객체를 만들어 넣어 주는 프로그램을 컨테이너라고 부릅니다. 이 컨테이너가 만들어 주는 객체를 서비스라고 부르는 프레임워크가 있습니다.

이 뜻의 서비스 정의는 어떤 클래스로 객체를 만들고 무엇을 넣어 줄지 적은 설정입니다. Symfony 가 이 말을 씁니다. Spring 에서는 같은 것을 빈 정의라고 부릅니다. 원격 호출의 서비스 정의와는 이름만 겹칩니다.

관련 항목

서비스 정의를 적는 언어와 파일

인터페이스 정의 언어 · proto 파일 · 프로토콜 버퍼 · Apache Thrift · CORBA · ONC RPC · WSDL · OpenAPI

서비스 정의를 이루는 구성 요소

rpc 메서드 · 원격 호출 가능한 메서드 · 메시지 · 필드 · 필드 번호 · 패키지 · 스트리밍 · 단항 RPC

서비스 정의에서 만들어지는 코드

스텁 · 스켈레톤 · 코드 생성 · protoc · 직렬화 · 역직렬화

서비스 정의를 읽어 호출을 나르는 기술

gRPC · 원격 프로시저 호출 · RPC · HTTP-2 · gRPC-Web · 프록시

서비스 정의를 바꿀 때 지키는 성질

하위 호환 · 상위 호환 · 스키마 진화 · API 버전 관리

서비스 정의를 앞세우는 개발 방식

계약 우선 개발 · 코드 우선 개발 · API 설계 · API 계약 · 스키마

같은 이름을 쓰는 의존성 주입 용어

의존성 주입 · DI 컨테이너 · 빈 정의 · Symfony · Spring

다른 이름: service definition · 서비스 선언 · service 블록