안심번호 마이크로서비스 개발하기

노영은
노영은
Feb 8 · 10 min read

안녕하세요. 당근마켓 플랫폼 개발팀 인턴 Jerry 입니다. 이번에 당근마켓의 안심번호 마이크로서비스를 개발한 이야기를 공유하려 합니다.

시작하며

나의 전화번호가 모르는 사람들에게 보여진다고 생각하면 불안감을 느낍니다. 특히 수 천 명의 사람들에게 전화번호가 노출되는 광고주 입장에서는 그 부담이 더할 수 밖에 없습니다.

그래서 당근마켓에서는 광고주를 위한 안심번호 서비스를 제공하고 있습니다. 안심번호는 실제 전화번호가 노출되는 것을 막기 위한 가상의 번호로서 일반적인 전화번호와 다르게 보통 050으로 시작합니다. 안심번호에 전화를 걸면 통신사가 실제 전화번호로 연결을 시켜주는, 일종의 proxy라고 봐도 될 것 같습니다.

안심번호를 사용하기 위해서는 먼저 회사가 통신사에 안심번호 할당을 요청해야 합니다. 그러면 회사는 사용자에게 안심번호를 줄 수 있는데 이를 mapping이라고 합니다. 그리고 사용자에게 매핑한 안심번호를 다시 가져오는 것은 unmapping이라고 합니다. 중요한 점은 매핑과 언매핑이 이루어질 때마다 통신사 서버에 알려야 실제 전화 연결이 가능하다는 것입니다.

개발 목표

개발하고자 하는 안심번호 서버의 구상도는 다음과 같습니다.

작은 서비스 하나 분리하는데 프로토콜을 3개나 사용해야 합니다..!

간단히 덧붙이자면,

  1. 사용자가 중앙서버에 안심번호 관련 API(매핑, 언매핑)를 요청한다.
  2. 중앙서버는 안심번호 서버에 gRPC로 요청한다.
  3. 안심번호 서버는 외부 통신사에 TCP로 요청하고 변경 내용을 DB에 저장한다.
  4. Admin은 웹페이지로 안심번호 현황을 볼 수 있다.(HTTP)

일단 Admin 페이지는 웹페이지이기 때문에 당연히 HTTP를 사용했습니다. 그리고 통신사에서 매핑, 언매핑 통신 규격을 TCP로 정해서 그대로 따를 수 밖에 없었습니다. 마지막으로 중앙 서버와의 통신은 아래와 같은 이유로 gRPC를 선택했습니다.

  • gRPC가 사용하는 HTTP/2는 한 Connection에서 여러 Stream을 요청할 수 있어서 여러 Connection을 맺지 않아도 됩니다. 그리고 Header Compression으로 헤더의 크기를 줄여서 메세지가 굉장히 가볍습니다.
  • gRPC의 Protocol Buffer는 key 값을 숫자로 사용하기 때문에 데이터 크기가 JSON에 비해 가볍고, 직렬화 및 역직렬화 속도가 빠릅니다.

게다가 마침 당근마켓의 내부 서비스들 통신 방식을 모두 gRPC로 잡아가고 있었기 때문에 큰 고민 없이 결정할 수 있었습니다.

Typescript + gRPC

원래는 괜찮은 Node.js gRPC 프레임워크가 있으면 사용하려고 했습니다. 그런데 찾아보니 마땅히 쓸 만한 프레임워크가 잘 보이지 않았습니다. 그나마 mali 프레임워크가 문서화도 잘 되어 있고 업데이트도 주기적으로 되는 것 같았지만, 버전이 아직 0.x 었습니다. 게다가 기능도 공식 gRPC 모듈에 비해 크게 추가된 것도 없어서 그냥 gRPC의 공식 모듈을 사용하기로 결정했습니다.

Node.js에서는 protobuf 파일을 읽을 때 dynamic codegenstatic codegen 을 지원하는데, 저는 이 중에서 static codegen 방식으로 개발했습니다.

  • dynamic codegen 방식은 protobuf 파일을 직접 load해서 gRPC 통신을 합니다. protobuf에서 변경이 생겼을 때 다른 작업을 하지 않고 데이터 내용만 수정하면 된다는 장점이 있지만, Typescript와 함께 사용할 때는 Typing이 되어 있지 않다는 문제점이 있습니다.
  • static codegen 방식은 protobuf파일과 protoc 플러그인을 사용해서 Javascript로 만들어진 generated 파일을 생성합니다. 실제 통신할 때는 만들어진 generated 파일을 사용하고 protobuf 파일에는 접근하지 않습니다. Typescript를 사용하는 경우에는 모듈 정의 파일(.d.ts)이 있어야 하는데, 그럴 때는 Typescript declaration 파일을 생성해주는 모듈들이 있으니 원하는 것을 사용하면 될 것 같습니다. 다만 static codegen 방식에서 값을 가져올 때는 get 메서드를, 값을 수정할 때는 set 메서드를 사용해야 합니다.
rpc 메서드 예시

물론 아래처럼 하면 계속 get, set을 사용하지 않아도 되지만, eslint가 객체에 괄호로 접근하지 말라고 화를 내기도 했고 get, set을 사용하지 못할 정도로 많은 내용을 가진 API는 없어서 get과 set 메서드를 그대로 사용했습니다.

['id', 'name'].map(key => request['get' + key]());

그렇게 static codegen 방식으로 개발을 하던 중, 에러 핸들러의 필요성을 느꼈습니다. gRPC에서는 callback 함수의 첫 번째 인자에 에러를 넘겨서 응답할 수 있는데 매번 logging 같은 공통된 처리를 반복해야 합니다. 그래서 express 처럼 에러를 한 곳으로 모을 수 있으면 좋겠다는 생각을 했습니다.

app.use((req, res, next) => {
next(new Error('error'));
});
// express error handler
app.use((err, req, res, next) => {
console.error(err); // 'error'
});

저렇게 에러를 한 곳으로 보내기 위한 여러 방법이 생각났습니다.

  1. 에러 발생 시 이벤트를 emit 시키고, 에러 핸들러가 에러 발생 이벤트를 구독해서 처리한다.
try {
// something do...
} catch (e) {
event.emit('error', e);
}
event.on('error', (error) => {}) // error handler

2. 에러 처리 함수가 callback과 에러를 둘 다 인자로 받아서 callback을 호출한다. 이 방식을 사용하면 rpc 메서드에서 에러가 났을 때 callback을 호출하지 않고 대신 errorHandler를 호출합니다.

function rpcFunction({request}, callback) {
try {
// something do...
callback(null, null);
} catch (e) {
errorHandler(e, callback);
}
}
function errorHandler(error, callback) {
// something do...
callback(error, null);
}

3. gRPC Server를 extends 받아서 에러 처리 미들웨어 기능만 추가한다.

1, 2번 방법은 기존에 개발 중인 코드를 수정해야 했습니다. 반면 gRPC Server를 extends 받아서 구현하는 방법은 기존 코드를 거의 건드리지 않고, 크게 복잡하지 않아서 3번 방법을 선택했습니다.

에러 핸들러를 추가할 수 있는 gRPC Server

뭔가 Custom Type이 많아서 좀 복잡해 보일 수 있는데 단순히 gRPC Server의 addService 메서드를 수정한 것 뿐입니다. 18번 라인의 implementation에 rpc 메서드들이 정의되어 있는데, 수정된 addService는 rpc 메서드가 callback을 호출하면 errorHandler를 거치게 합니다. (Custom 타입은 원래 gRPC Server에서 사용하는 타입을 그대로 옮겨 썼습니다.)

Server를 ServerWithErrorHandler로 바꾸고 에러 핸들러 추가하면 끝!

마지막으로 기존 Server를 ServerWithErrorHandler로 변경하고 에러 핸들러를 작성하면 기존 rpc 메서드를 수정하지 않아도 됩니다!

gRPC 서버에 HTTP 서버 붙이기

안심번호 서버는 Admin 페이지를 위해서 gRPC 뿐만 아니라 일반 HTTP 프로토콜도 지원해야 했습니다. 처음에는 막연하게 ‘gRPC가 HTTP/2를 사용하니까 express랑 같이 쓸 수 있지 않을까?’ 라고 생각했습니다.

app.use('/grpc', grpcServer) //??? 

하지만 위 형태의 라이브러리는 찾기 힘들었고, 직접 구현하는 것도 짧은 시간 안에는 힘들 것 같았습니다. 그래서 다른 방법을 찾아야 했습니다.

  1. gRPC 서버와 HTTP 서버를 따로 개발한다.
  2. gRPC 서버를 베이스로 두고, HTTP 요청은 gateway를 거쳐서 gRPC를 요청하게 한다.

1번 방법은 ‘HTTP 서버와 gRPC 서버가 같은 서비스와 모델을 사용하는데 굳이 따로 개발해야 되나?’ 싶었습니다. 그래서 2번 방법을 생각했는데, 역시 적절한 방법은 아니었습니다. 안심번호 서버는 gRPC에 필요한 API와 HTTP에 필요한 API가 서로 달랐는데 gateway를 둔다면 gRPC로 모든 API를 구현하고 거기에 HTTP를 덧붙여야 하기 때문입니다.

그래서 결론은, 공통된 서비스 로직은 그대로 두고 API controller 부분만 각각 개발하기로 했습니다. 그리고 gRPC를 실행 시킬 것인지, HTTP를 실행 시킬 것인지는 SERVICE환경변수로 구분했습니다.

‘grpc’로 실행된 서버는 오직 gRPC 요청만 받는다는 것을 간단하게 표현해 봤습니다.
서비스 로직은 같은 코드를 사용합니다.

통신사와 TCP로

안심번호 서버에서는 통신사 서버에 TCP로 매핑, 언매핑을 알려야 합니다. 그래서 사용한 것이 바로 Node.js에서 Socket을 개발할 때 쓰는 net 모듈입니다. net 모듈은 Node.js의 내장 모듈이여서 따로 설치할 필요가 없습니다.

개인적으로 놀랐던 사실인데, net 모듈에는 connection timeout 설정이 없습니다. timeout 이벤트가 있기는 하지만 connection 뿐만 아니라 평소에 통신이 없어도 발생하는 이벤트였습니다.

connection timeout은 필요하지만 입출력이 없을 때마다 계속 socket이 끊기는 것을 원하지 않아서 시간 제한 함수를 만들었습니다.

clearTimeout은 처음 사용해 봤습니다.

execInTime 함수 내부에서 setTimeout으로 reject를 걸어 놓았습니다. 그리고 그 시간 내에 callback 함수의 작업이 완료되면 clearTimeout 함수로 setTimeout을 해제하고 resolve를 호출합니다. 여기서는 connect 함수가 100ms 안에 완료되지 않으면 reject가 호출됩니다.


안심번호를 매핑, 언매핑할 때 보내는 요청은 통신사에서 정한 포맷을 따라야 합니다. 그 포맷이 상당히 복잡한데 예를 들자면, 전화번호는 문자열로 20자를 맞춰야하는데, 만약 글자수가 부족하면 뒤쪽은 공백으로 붙여야 합니다. 이건 Javascript 문자열 함수인 padEnd 로 해결할 수 있었습니다. padEnd는 문자열을 인자로 주어진 길이만큼 맞춰서 리턴합니다.

'01012345678'.padEnd(20); // '01012345678         '

이런 식으로 만든 전화번호, 안심번호, 회사 전용 코드 등을 하나의 문자열로 연결해서 통신사로 요청하면, 통신사가 성공, 실패 여부를 다시 TCP로 응답해줍니다.

마무리

지금까지 안심번호 마이크로서비스를 개발한 과정에 대해서 정리해 보았는데, gRPC를 처음 사용해 보아서 부족한 점이 많습니다. 틀리거나 더 좋은 방법이 있다면 댓글이나 gist 코멘트로 달아주세요. 감사합니다.

당근마켓 팀블로그

당근마켓은 동네 이웃 간의 연결을 도와 따뜻하고 활발한 교류가 있는 지역 사회를 꿈꾸고 있어요.

노영은

Written by

노영은

당근마켓 팀블로그

당근마켓은 동네 이웃 간의 연결을 도와 따뜻하고 활발한 교류가 있는 지역 사회를 꿈꾸고 있어요.

More From Medium

More on Programming from 당근마켓 팀블로그

More on Programming from 당근마켓 팀블로그

deploy 브랜치 전략 활용 방법

Related reads

Related reads

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade