다른 서비스를 어떻게 호출할까

https://www.clipartmax.com/middle/m2H7Z5m2b1Z5m2N4_office-clipart-phone-call-office-clipart-phone-call/

옛날에야 한 시스템에 모든 걸 다 때려박는 monolithic 구조를 썼지만 요즘은 서비스를 여러개로 나눠서 서로 호출하게 한다. 예전에 SI 프로젝트를 하는데 한 프로젝트에 모든 걸 다 넣어놓으니까 개발자 PC에서 메모리가 부족해서 어플리케이션이 돌아가지 않던 적이 있었다. 그래서 모듈을 나눠서 개발하는데 필요한 몇 개 모듈만 올리니까 잘 되었다. 사실상 프로젝트 안에 여러 서비스 모듈이 있는 셈이었고, 그것들 중에 일부를 죽였다 살렸다 해도 한 부분을 개발하는데는 문제가 없었다. 그러니까 원래 옛날에도 암묵적으로는 나눠서 쓰던 걸 명시적으로 분리해낸 것이 요즘의 구조다. 그걸 한때는 SOA(Service-oriented Architecture)라고 불러서 “오라클이 SOA(쏘아)라!”는 행사도 했었는데, 그게 이젠 한물 가서 요즘은 microservice라고 비슷한 걸 많이 쓴다.

이렇게 서비스를 나눠놓으면 개발하기도 좋고, 각각 살렸다 죽였다 할 수 있으니까 일부가 죽어도 전체가 죽지는 않는 등의 장점이 많다. 근데 그러면 서비스끼리 통신을 해야 하는게 문제다. 통신에서 일단 문제가 안되는 2가지부터 말하면, 서비스를 나눔으로서 그만큼 리소스를 더 먹는건데 (CPU, MEM, DISK) 그건 그렇게까지 부담이 안되니까 넘어가고, 그럼 서비스끼리 호출하는데 추가적인 지연(latency)가 들어가지만 어차피 같은 데이터센터 안에서 호출할거라 네트워크 지연도 아주 짧고, 요즘 서비스들은 응답속도도 빨라서 거의 10ms 안쪽으로 지연이 추가되는 거라 이런 것들은 제껴도 된다.

그럼 문제가 되는 건 뭐냐 하면 API 인터페이스다. 옛날에는 RPC라고 해서 그냥 원격으로 프로시저를 호출하는 걸 썼는데, 예를 들어 SAP ERP 시스템의 기능을 다른 시스템에서 쓰고 싶으면 SAP ERP에 프로시저를 만들어서 그걸 RPC로 호출해서 읽기도 하고 쓰기도 했다. 그러면 2가지 문제가 생기는데 첫째로는 sync 방식이어서 lock이 걸려서 오랫동안 기다리기도 하고, 아니면 내가 lock을 잡아서 다른 사람들이 기다리기도 하고 그랬다. 물론 async도 지원하지만 그러면 요청 -> 완료 1단계면 될 걸, 요청 -> 요청 완료, 처리 -> 처리 완료, 결과 확인 -> 결과 확인 완료 이렇게 3단계로 나눠서 처리해야 한다. 예를 들어 내가 삼성카드에서 카드값을 납부하면 납부 완료가 바로 뜨는 것이 아니라 납부 요청 완료라고 뜨고, 결과를 확인하려면 결과 확인 페이지에 가서 조회를 해야 나온다. 그게 내가 은행계좌에 돈이 부족해서 실패하면 실패라고 뜨고, 아니면 성공이라고 뜰 것이다. 실패하면 재시도할 수 있을거고. 그러면 이렇게 모든 걸 분리해서 만드는게 맞을 것이냐? 하면 그게 맞다. Microsoft Azure의 관리 Console을 가보면 다 async다. VM 생성 누르면 거기서 spinner가 뱅글뱅글 돌면서 생성이 끝날때까지 기다리는게 아니라, 그냥 생성 시작했다고 하고 나중에 생성이 끝나면 끝났다고 notification이 뜬다.

자 그래서 이제 옛날처럼 “처리하는 중…” “읽는 중…” 이렇게 기다리던 sync방식 대신에 async방식으로 한다고 치자. 그럼 남은 문제는 인터페이스 형식이다. SOA 시절에는 XML을 썼고 요즘 REST는 JSON을 쓰고, 바이너리가 필요하면 바이너리 형식을 쓰면 되고, 하여튼 그건 문제가 아니다. 데이터 포맷이 드물게 문제가 되긴 하는데 예를 들면 integer가 32비트 시스템과 64비트 시스템 간에 자리수 차이가 날 수 있는데 이거야 요즘은 다들 64비트를 쓰니까 넘어가고, 자바스크립트에서는 특이하게 integer가 53비트라서 잘리니까 트위터에서는 이걸 그냥 string으로 바꿔서 보내기도 한다. https://developer.twitter.com/en/docs/twitter-ids 그 외에 바이너리라면 big-endian과 little-endian 차이도 문제가 될 수 있지만 요즘은 웬만하면 다 little-endian이고, 바이너리로 저장할때 바이너리 형식에서 맞춰주면 되니까 이것도 넘어가자.

그래서 도대체 문제가 되는게 뭐냐 하면 API 인터페이스가 바뀔 때다. 사람이 살다보면 인터페이스에 뭘 추가할수도 있고, 뺄 수도 있고, 고칠수도 있고, 아니면 같은 항목의 데이터 형식을 바꿀수도 있다. 이런걸 schema evolution 이라고 하는데 그럼 그럴때마다 보내는 쪽과 받는 쪽을 동시에 고치면 되겠네… 싶은데 그게 그렇게 간단하지 않다. 예를 들어 인증 서비스에서 사용자 ID 타입을 바꿨는데, 이걸 모바일 서비스에서 로그인할때 쓴다고 하자. 그럼 이 로그인 API 인터페이스를 바꿀때마다 인증 서비스도 바꾸고, 모바일 서비스도 바꾸고, 그래서 2개를 동시에 재시작해서 적용해야 하나? 그러면 다운타임이 생긴다. 이게 짧으면 그냥 순단(intermittent downtime)해서 모바일 사용자들이 한 3초 정도 잠깐 로그인이 안되는 걸 적당히 넘어갈 수 있고, 아니면 “지금은 점검 모드입니다. 새벽 2시부터 4시까지.” 이렇게 시간을 정해놓고 점검 모드로 들어간 다음에 적용할수도 있다. 어찌됬건 다운타임이 생긴다. 이걸 제로 다운타임으로 만들 순 없을까?

당연히 있다. 새 로그인 API가 기존 API와 호환되게 만들면 된다. 인증 서비스의 로그인 API를 바꿔서 배포하고, 모바일 서비스의 로그인 API는 그대로 둔다. 그래서 예를 들어 인증 서비스가 2대가 돌고 있어서 1대씩 rolling update를 할 때, 중간에 1대는 기존 API, 1대는 새 API를 제공할 수 있다. 그래서 모바일 서비스가 기존 API를 호출하던 새 API를 호출하던 둘 다 로그인이 잘 되면 된다.

그런데 인증 서비스가 너무 많이 바뀌어서, 새 API가 도저히 기존 API와 호환되게 만들 수 없으면 어떡할까? 애초에 처음에 API를 만들때부터 미래를 내다보고 넉넉하게 앞으로 쓸 것 같은 것들까지 충분히 포함해서 만들었으면 좋았을텐데! (물론 이렇게 만드는 것도 방법이다) 하지만 도저히 그럴 수 없다면… API 버전을 올려야 한다.

그래서 예를 들어 로그인 API v1, v2가 있다고 하자. 가능하면 v1, v2가 둘 다 돌도록 해서 호환성을 맞춰주면 좋다. 근데 언젠가 결국 v1을 종료해야 한다면, 미리 다른 서비스들에게 알려서 바꾸라고 해야겠다. 그래서 모바일 서비스의 로그인 API도 v1에서 v2로 버전업해서 올리면 이제 v1을 내려도 될 것이다.

이게 내부 서비스들은 강제해서 빨리 바꿀 수 있겠지만, 이게 open API여서 외부에도 공개되어있다면, 이걸 쓰는 분들이 말을 잘 들을리가 없다. 이메일 등의 공지로 3개월 후에 닫겠습니다, 라고 하면, 넵 알겠습니다, 바로 바꾸겠습니다! 이렇게 재깍재깍 바꿔주는 분들도 있을 거고 아닌 분들도 있을 거다. 그러면 좀 공포를 줘야 하는데 일단 3개월 후에 닫겠다고 하면 1개월 후까지 개선 계획을 보내달라고 하고, 안 보내주면 그분들만 먼저 닫는 것이다. 그래서 왜 닫았냐, 뭐라고 하면 일단 열어주되 거기서부터 협상에 들어간다. 아니 선생님, 저희가 이메일 드리지 않았습니까? 개선 계획을 보내주셔야죠. 그러면 아니 우리가 어떻게 1개월안에 바꿉니까? 5개월은 주십시오! 그러면 네 그럼 5개월까지는 봐드리겠습니다. 그땐 정말로 닫습니다! 이렇게 협상의 여지를 두고, 필요하면 연장해서 닫으면 될 것이다. stripe.com 이 이렇게 했다.

그러면 인증 서비스에서 내부적으로 로그인 API를 v1, v2 둘 다 운영을 어떻게 할까? 내부적으로 v1 데이터와 v2 데이터가 분리되어 있고, 중간에 약간의 변환을 거쳐서 v1 데이터를 v2 데이터에도 쓸 수 있고, v2 데이터를 v1 데이터에서 쓸 수 있다면, API v1으로 들어온 요청도 v1, v2 데이터에 모두 쓰고, API v2로 들어온 요청도 v1, v2 데이터에 모두 쓰면 v1, v2 데이터 모두 똑같이 남을 것이다. 똑같이 안 남는다면 버그가 있는거니까 고치면 되고, 이렇게 버그를 잡을 기회도 더 생긴다. 그러다가 API v1을 닫을 때 v1, v2 둘다 쓰던 것 중에서 v1 부분만 끊으면 된다. strangler pattern이라고 볼 수 있다.

마지막으로, API 버전을 올릴때마다 로그인 서비스, 모바일 서비스 둘 다 수정하는 대신에, API 버전을 다른 서비스에서 대신 관리하는게 낫지 않을까? Apache Avro를 쓰면 된다. 그럼 보내는 서비스도 받는 서비스도 Avro에서 읽어오면 된다.

이 내용은 “Designing data-intensive applications”(2015) 4장 “Encoding and evolution”입니다.

Loading

Published
Categorized as xacdo

By xacdo

Kyungwoo Hyun

Leave a comment

Your email address will not be published. Required fields are marked *