문제 상황
저는 ABC Studio에서 MessagingHub라는 플랫폼 프로젝트를 담당하고 있습니다. MessagingHub는 다양한 비즈니스 요구에 맞춰 엔드 유저를 대상으로 메시징 기능을 제공하는 맞춤형 메시징 플랫폼입니다.
아래 그림은 MessagingHub가 Demaecan 서비스의 각 도메인에서 몇 개의 컴포넌트와 연동돼 사용 중인지 나타내고 있습니다(올해 도입 예정인 컴포넌트도 일부 포함돼 있습니다).
위 그림을 보며 알 수 있는 사실이 두 가지 있습니다. 하나는 MessagingHub가 플랫폼 프로젝트로서 확장해 나가고 있다는 것이고, 다른 하나는 그에 따라 이해관계자 또한 늘어간다는 것입니다.
플랫폼 프로젝트는 성장 과정에서 필연적으로 여러 번 확장합니다. 이에 따라 단기적인 성과를 달성하는 것을 넘어 지속적으로 발전하며 확장하기 위한 도전 과제에 끊임없이 직면합니다. 주어진 현실에 안주하지 않고 지속적으로 성장의 의미를 찾아야 하는 이유입니다.
MessagingHub도 그와 같은 과정을 거쳐오고 있습니다. 최초에 폴링(polling) 이슈를 해결하기 위해 시작해서 이후 앱 푸시와 이메일, 문자, 이제는 채팅에 이르기까지 그 기능을 지속적으로 확장해 왔습니다. 이에 따라 서비스나 컴포넌트와의 접점도 점차 넓어지고 있습니다.
그런데 한 사람이 감당할 수 있는 업무량에는 분명한 한계가 있습니다. 해야 할 일은 많고 커뮤니케이션 비용은 높아지는데 인력이 부족하다면 업무 진행 과정에서 반드시 병목 지점이 발생합니다. 프로젝트의 지속 가능한 운영과 성장을 위한 중요한 기로에 서 있는 이 상황은 어렵고 힘들긴 하지만 한편으론 흥미로운 도전 과제이기도 합니다.
이 상황을 짧게 정리하면 이렇습니다.
- 플랫폼 프로젝트로서 초기 성장 단계를 성공적으로 넘어섰습니다.
- 그러나 업무 범위가 확장된 것에 비해 실무 담당자의 수는 매우 부족합니다.
- 따라서 지속 가능한 업무 환경을 만들기 위한 돌파구가 필요한 중요한 시점입니다.
커뮤니케이션 부채 해결하기
MessagingHub는 플랫폼 프로젝트로서 범용적인 사용성을 제공합니다. 따라서 사용처가 다양해지는 것은 매우 반가운 일인데요. 한편으로는 그로 인해 운영 측면에서 쌓이는 피로감을 해소하는 것 또한 중요합니다.
연동 포인트의 증가는 커뮤니케이션 비용 상승으로 이어집니다. 물론 업무 관련 커뮤니케이션은 담당자의 당연한 책무이지만, 반복되는 질의 응답에 갇히거나 질문자에게 실시간으로 원하는 답변을 해야 한다는 압박감이 담당자에게 쳇바퀴를 도는 듯한 피로감을 안겨줄 때도 있습니다. 저 역시 이런 상황이 계속 되다 보니 서서히 옥죄이는 기분이 들었고, 어느 날 커뮤니케이션에 들어가는 비용이 제가 지불할 수 있는 비용을 넘어서 부채로 쌓이고 있다는 것을 자각했습니다.
아래 그림은 제 느낌을 시각화한 것입니다. 생산성과 복잡도가 시간의 경과에 따라 서로 어떤 어떠한 상호관계를 갖는지 보여줍니다. 우리는 때때로 여러 가지 요인으로 인해 생산성이 급격히 떨어지는 것을 체감합니다. 이를 체감하는 순간 우리는 언제나 그렇듯 생산성을 높이기 위해서 복잡도를 붕괴시키려는 노력을 하는데요. 그 시점을 제대로 인지하는 것이 중요합니다. 만약 그 시점을 너무 늦게 인지하거나 아예 인지하지 못하고 안주한다면 이는 생산성의 붕괴로 이어질 것입니다.
이를 개발자들에게 익숙한 기술 부채와 비교해 보겠습니다. 기술 부채 역시 언젠가는 갚아야 할 마음의 빚이지만, 한편으로는 도전 욕구를 자극하는 과제로 다가오기도 합니다. 반면 커뮤니케이션 부채 는 시시때때로 갚으라고 독촉하는 사채 업자에게 진 빚처럼 느껴집니다. 독촉에 시달리다 보면 정작 진짜로 해야 하는 업무는 하기 힘들어지고 응대 모드로 전환할 수 밖에 없는 악순환에 빠져 피폐해지는 것이죠.
MessagingHub에 인입되는 대부분의 질문은 시스템의 구조나 작동 방식, 연동 방법과 같은 것을 물어보는 세부적이고 실무적인 질문입니다. 따라서 연동 컴포넌트가 늘어날수록 질문의 양도 비례해서 늘어날 수밖에 없기에 정말 해야 할 일에 제대로 몰입하려면 이 부채를 반드시 해결해야 했습니다. 이를 위해 저는 아래 두 가지를 실행 목표로 정했습니다.
- 질문의 양을 줄인다.
- 질문을 내가 좋아하는 방식으로 바꾼다.
각 실행 목표를 하나씩 살펴보겠습니다.
질문의 양을 줄이기
질문은 그 자체로 나쁜 것은 없습니다. 오죽하면 '세상에 바보 같은 질문은 없다'는 말까지 있으니까요. 반면 '세상에 바보 같은 답변은 없다'는 말은 없습니다. 실제로 조금만 찾아보면 바보 같은 답변은 쉽게 찾아볼 수 있습니다. 저는 이 사실을 곱씹다가 재밌는 사실을 하나 깨달았습니다. 질문하는 쪽보다 답변하는 쪽이 짊어져야 할 책임이 크다는 것입니다. 질문은 바보 같지 않은데 답변은 바보 같을 수 있으니까요. 그렇기 때문에 답하는 쪽에서는 질문을 최대한 시스템화하는 것이 중요하다는 결론에 도달했습니다. 그동안 받아왔던 질문의 유형과 관심사를 분류하며 결국 원하는 답은 무엇이었는가를 정리해 보면 질문에 대한 답을 체계화할 수 있습니다.
제가 질문의 양을 줄이기 위해 사용자의 질문과 답변을 체계화하고 시스템화한 결과는 다음과 같습니다.
- 통합 가이드
- 뉴비를 위한 흐름 차트
- GitHub README
- 기타 자료(기술 블로그 혹은 컨퍼런스 발표 자료 등)
하나씩 살펴보겠습니다.
통합 가이드 - 당신이 뭘 알고 싶은지 몰라서 다 준비했어요
개발자에게 '배경이 어떻고, 구조가 어떻고, 철학이 어떻고'는 중요하지 않습니다. 개발자가 제일 궁금해 하는 것은 '그래서 내가 뭘하면 되는데?'입니다. 통합 가이드는 이런 개발자에게 연동 작업을 안내하기 위한 올인원 가이드입니다. 당연하지만 이 가이드를 처음부터 끝까지 샅샅이 읽으며 모든 것을 다 보고 이해할 필요는 없습니다. 본인이 원하는 기능에 대한 가이드는 최대한 해당 기능 페이지 안에 전부 설명돼 있습니다. 물론 참조 링크도 제공하고요.
뉴비를 위한 흐름 차트: 이미지 게임하듯 순서대로 따라가다 보면 필요한 것을 찾을 수 있어요.
앞서 MessagingHub와 연동할 때 참고할 수 있는 올인원 연동 가이드를 만들어 놓았더니 이제는 MessagingHub 자체에 대해 조금 더 자세히 알고 싶다고 합니다. 개발자뿐 아니라 기획자 혹은 의사 결정 권한을 가진 이해 관계자들을 이해시킬 자료가 더 필요하다는 건데요. 이를 위해 그동안 쌓아왔던 자료들을 흐름도로 정리했습니다. 전체를 한눈에 보면 처음에는 복잡해 보일 수 있지만, 왼쪽 위 시작 지점에서 출발해 화살표를 따라가며 분기에서 적절하게 선택해 나가면 필요한 것을 얻을 수 있도록 구성했습니다.
GitHub README - 모든 구현의 핵심은 여기 있어요
따로 스크린숏이 없어도 개발자라면 익숙한 말이 있죠. 'README를 읽어보세요'.
기타 자료 - 다양한 활동을 통해 자연스레 쌓여온 산출물들
기술 블로그나 컨퍼런스 발표 자료 등 다양한 활동을 통해 자연스레 쌓여온 산출물들이 있습니다. 이들은 모두 좋은 교보재가 됩니다.
- 기술 블로그
- 컨퍼런스
이와 같은 결과물을 만들 때 주의해야 할 점이 하나 있는데요. 문서의 양을 늘리는 것을 목표로 여기면 안 된다는 것입니다. 문서의 양적 증가가 답변의 질적 향상을 의미하지는 않기 때문입니다. 관리 가능한 수준을 유지할 수 없다면 이런 문서는 반드시 또 다른 부채가 될 것입니다. 따라서 어떤 자료가 더 이상 유지 관리하기 어려운 수준에 도달했다면 미련없이 통폐합을 결정해야 합니다. 문서도 코드와 다를 바 없습니다. 과도한 애착은 쓸데없이 상황을 복잡하게 만들며, 불필요하게 보존한 문서는 곧 레거시가 됩니다.
질문을 내가 좋아하는 방식으로 바꾸기
만약 답변자가 질문자에게 '질문의 방식을 바꾸세요'라고 요구한다면 상대방은 이를 어떻게 받아들일까요? 아마도 '이 사람 뭐지?' 싶을 것입니다. 누군가에게는 싸우자는 말의 다른 표현으로 다가올 수도 있습니다.
그런데 커뮤니케이션 부채를 해소하기 위한 제 두 번째 목표는 '내가 좋아하는 방식으로 질문을 바꾸는 것'입니다. 물론 쉽지 않습니다. 상대의 반응을 내가 원하는 방식으로 유도할 때에는 자연스러움이 중요합니다. 내가 원하는 방식으로 질문해 달라고 요구하는 것은 상대의 기분과 상황을 고려한다면 사실상 하기가 매우 어려운 요구입니다. 그리고 애초에 그렇게 해야만 하는 이유에 대한 설득 근거가 취약하고요. 설득 비용도 만만치 않죠. 그래서 직접적인 요청은 현실적으로 불가능합니다.
질문을 통해 답변을 찾아나가는 과정은 질문자의 성향에 따라 달라질 수 있습니다. 결과적으로 같은 답변에 도달하는 사람이라도 아래와 같은 여러 변수에 영향을 받아 각자의 과정은 달라질 수 있는 것이죠.
- 개인 성향, 감정 상태, 표현 방식, 대화 스킬, 업무 환경, 개발 경험, 주요 가치관, 직/간접적 질문 형태, 그 밖에 대화에 영향을 주는 모든 외부 요인들
이와 같은 상황에서 답변자가 제대로 된 답변을 하려면 질문자의 의중을 제대로 파악할 수 있어야 하는데요. 네 가지 혈액형과 16가지 MBTI가 자유롭게 섞여 있는 60억 인구 각각의 의중을 어떻게 빠르게 파악할 수 있을까요? 현실적으로 불가능합니다. 알 수 없습니다. 저는 일찌감치 불가능하다고 인정하고, 대신 질문의 답을 찾기 위한 최적의 경로를 탐색하는 방법은 무엇일까를 고민했습니다.
저는 MessagingHub의 구조와 작동 방식을 모든 사람에게 장황하게 설명할 필요는 없다고 판단했습니다. MessagingHub 시스템 내부에 대한 고민은 내부에서 하는 게 맞으니까요. 개발할 때 흔히 얘기하는 일종의 추상화라고 할 수 있습니다. 추상화하면 내부 구현을 하나하나 자세히 알고 있지 않더라도 작동 결과를 예측할 수 있습니다. 추상화된 인터페이스와 매뉴얼 및 가이드를 보며 호출하는 쪽에서 기대하는 결괏값을 얻어낼 수 있다면 그것으로 충분합니다. 수많은 디자인 패턴들이 유연하고 확장성 있고 유지 보수하기 좋은 핵심 기법으로 추상화를 언급하는데요. 업무 커뮤니케이션 방식에도 추상화를 적용할 수 있습니다. 더구나 대화의 대상이 개발자라면 더욱 기술 중심인 코드 수준의 대화가 명확하고 좋을 것입니다. 이런 측면에서 연동 측 담당자의 질문 유형을 바꿀 수 있는 재밌는 전략은, 바로 코드입니다. 코드를 이용한 소통은 연동 작업에 대한 모호한 질문과 불확실한 답변을 서로 명확하게 이해할 수 있는 질문과 답변으로 바꿀 수 있는 좋은 수단입니다.
그래서 MessagingHub 서버 SDK를 개발했습니다. MessagingHub 서버 SDK는 MessagingHub를 사용하려는 개발자들의 질문에 효율적으로 대처하며 커뮤니케이션 부채를 해소하기 위해 제가 선택한 방식입니다. 또한 단순히 대화의 편의를 개선하는 것을 넘어서 개발 작업의 효율을 극대화할 수 있는 수단인데요. MessagingHub의 서버 SDK가 무엇인지 자세히 살펴보겠습니다.
MessagingHub의 서버 SDK 소개
개발 목표
MessagingHub를 연동하는 개발자가 시스템의 내부 작동 원리를 상세히 이해할 필요는 없습니다. 추상화하는 게 중요합니다. MessagingHub의 역할은 엔드 유저에게 발송하고 싶은 메시지를 전달 받았을 때 이를 안전하게 목적지까지 전달하는 것인데요. 이와 더불어 플랫폼으로서 쉽고 빠르게 MessagingHub와 연동할 수 있도록 연동 허들을 낮춰 커뮤니케이션 비용이나 작업 부담을 줄이는 역할도 감당해야 합니다. 후자의 역할은 플랫폼 프로젝트라면 응당 고민하며 풀어가야 할 숙제이기도 합니다.
연동 방식에 따라 질문의 유형과 범위가 달라질 것이며, 연동 허들을 낮추면 질문의 양도 줄일 수 있습니다. 연동하려는 개발자가 따로 추가 질문을 하지 않고도 MessagingHub를 자연스럽게 사용할 수 있게 된다면 이는 MessagingHub가 플랫폼으로서의 완성도를 높였다는 증거가 될 것입니다. 이를 달성하기 위해서는 사용자(개발자)가 직관적으로 활용할 수 있는 사용자 친화적인 인터페이스를 제공해야 합니다.
서버 SDK 개발 전후 비교
MessagingHub의 서버 SDK를 만들 기 전 전과 후가 어떻게 바뀌었는지부터 살펴보겠습니다.
서버 SDK 개발 전
MessagingHub와 연동하는 시스템은 크게 gRPC와 REST API로 통신합니다. 따라서 MessagingHub와 연동하기 위해서 연동측 개발자는 아래와 같은 작업이 필요합니다(참고로 웹소켓은 앱 클라이언트와의 통신 프로토콜이기 때문에 여기서는 다루지 않으며, 이와 관련해서는 Flutter 패키지로 공통 모듈 리팩토링하기 포스팅을 참고하세요).
- MessagingHub로 메시지를 전달하기 위해서 MessagingHub에서 정의한 gRPC 스펙을 확인합니다.
- MessagingHub의 gRPC 모델 라이브러리인 external-lib 라이브러리를 프로젝트의 의존성으로 설정합니다.
- gRPC 라이브러리 의존성을 설정합니다.
- gRPC 클라이언트를 만듭니다.
- gRPC에 익숙하지 않은 개발자는 gRPC 스펙과 구현 방법을 별도로 학습해야 합니다.
- MessagingHub의 API를 사용하기 위해서 HTTP 클라이언트를 만듭니다.
- gRPC 클라이언트와 HTTP 클라이언트의 재시도 전략을 수립하고 구현합니다.
- 에러 핸들링을 위해 MessagingHub의 응답 스펙과 예외 케이스를 확인합니다.
- MessagingHub의 단계(phase)별 엔드포인트를 확인합니다.
- 각 단계의 엔드포인트마다 통신을 위한 인증 토큰을 MessagingHub 개발자에게 요청합니다.
- 사용하려는 기능에 대한 MessagingHub 스펙 문서를 확인하고 메시지와 함께 호출합니다.
그동안 MessagingHub에서는 gRPC 연동을 위한 protocol-buffers IDL을 빌드한 external-lib라는 라이브러리를 제공해 왔습니다. 이 라이브러리는 최소한의 인터페이스만 제공했기 때문에 연동을 위한 허들은 온전히 연동측 개발자들이 부담해야 했습니다. 따라서 개발자의 개발 숙련도나 MessagingHub에 대한 이해도에 따라 개발 속도에 차이가 발생했고, 개발자마다 개발 스타일이 다르기 때문에 일관된 방식의 연동을 기대하는 것은 어려웠습니다. 이는 연동 과정에서 휴먼 에러가 발생할 확률을 높일 뿐 아니라 이슈가 발생했을 때 상황을 파악하고 추적하는 것 또한 어렵게 만들었습니다.
이런 문제는 MessagingHub 운영 관점에서도 제어하기 어려운 회색 영역을 만드는 것이며, 연동 측은 결국 사용성에 대한 신뢰를 점차 잃을 것입니다. 마치 깨진 창문을 방치하고 있는 것과 다름 없는데요. 이를 해결하기 위해 MessagingHub 연동을 일관적으로 진행할 수 있도록 정비할 필요가 있었습니다.
서버 SDK 개발 후
서버 SDK를 도입한 후 연동하는 측에서 구현해야 하는 부분은 아래와 같습니다. 이전과 비교해 수행 목록의 길이가 대폭 줄어든 것을 알 수 있습니다.
- MessagingHub가 제공하는 서버 SDK 라이브러리를 의존성에 추가합니다.
- 라이브러리를 통해 MessagingHubClient 인스턴스를 생성합니다.
- 이 인스턴스를 이용해서 사용하려는 MessagingHub의 기능을 선택하고, 메시지를 담아서 호출합니다.
MessagingHub와 연동하기 위한 코드는 서버 SDK에서 제공하는 것만 사용하면 됩니다. 연동측은 코드 수준의 가이드 라인을 따라 필요한 정보를 설정하고 호출하면 됩니다. 참 쉽습니다. 개발 전에 발생하고 있던 문제들은 자연스럽게 해소됐습니다. 연동은 쉬워졌고, 유지 보수는 단순해졌으며, 에러 트래킹도 쉬워졌습니다. 심지어 원한다면 MessagingHub와 자신을 포함한 수많은 연동 개발자를 위해 서버 SDK에 기여할 수도 있게 되었습니다.
서버 SDK 시작하기
그럼 실제로 서버 SDK를 어떻게 사용하는지 사례와 함께 살펴보겠습니다.
웹소켓 기반으로 폴백(fallback)으로 FCM을 보낼 수 있는 연결 메시지(connection message) 사례
private val token = Token.getDevelopmentToken(Phase.DEV).......................[1]
private val messagingHubClient = MessagingHubClient.builder()..................[2]
.setNetwork(Network.INTERNAL)..............................................[3]
.setPhase(Phase.DEV).......................................................[4]
.setTokens(mapOf(MessagingHubServer.MESSAGE_ROUTER to token))..............[5]
.setRetryConfig()..........................................................[6]
.setKeepAliveConfig()......................................................[7]
.build()
@Test
fun sendConnectionMessageTest() {
val request = ConnectionMessageRequest(command = "CLIENT_PUSH_TEST",.......[8]
payload = PAYLOAD,
fcm = FCM,
targets = TARGETS,
strategy = STRATEGY)
messagingHubClient.sendConnectionMessage(request).also { assert(it) }......[9]
}
- MessagingHub와 통신하기 위해서는 토큰이 필요합니다. 프로덕션 환경을 위해서는 서비스마다 토큰을 발급해 드리지만, 개발 환경에서는 하나만 받아서 공유해도 됩니다. 일례로 PoC를 하고자 할 때 매번 개발 환경 토큰을 발급받아야 하는 불편한 허들을 놓을 필요는 없다고 생각하기 때문입니다. 그래서 개발 환경에서의 토큰은 단계별로 SDK에 담아뒀습니다.
- MessagingHubClient 인스턴스는 MessagingHub와 통신하기 위한 인터페이스가 담긴 유일한 소통 창구입니다. 빌더를 이용해 쉽게 만들 수 있으며, SDK에서는 빌더를 통해서만 인스턴스를 생성하도록 제한하고 있습니다. 여기서 빌더 패턴이 좋은 점은 메서드 체이닝을 통해 필요한 설정을 쉽게 파악할 수 있고, 궁금할 땐 언제든 관련 정보로 접근할 수 있다는 점입니다.
- MessagingHub의 네트워크는 내부용과 외부용이 있으며, 연동 측의 인프라 환경에 따라 적절히 선택합니다.
- MessagingHub는 각 단계를 Alpha, Dev, Beta, Rc, Release 환경으로 나눠 사용하고 있으며 연동 목적에 맞게 해당 단계를 선택합니다.
- MessagingHub는 크게는 하나의 시스템이지만, 내부적으로는 목적에 따라 모듈이 분리돼 있습니다. 어떤 연동 기능을 사용하는지에 따라 해당 모듈을 선택할 수 있으며, 개발 환경이라면 앞서 1번에서 얻은 토큰도 함께 전달합니다.
- MessagingHubClient로 호출하는 기능에 대한 재시도 정책을 정의합니다. 별다른 설정이 없다면, SDK에서 지정된 기본 재시도 정책인 Exponential backoff 정책이 사용됩니다.
- 통신에 관한 킵얼라이브(keepalive) 설정을 할 수 있습니다.
- 엔드 유저에게 보낼 메시지를 정의합니다. 9번에서 호출할 기능에 따라 각 요청 모델이 정의돼 있습니다.
- 8번에서 정의한 메시지를 MessagingHubClient를 통해 MessagingHub로 전달해 원하는 기능을 호출합니다. 호출 후 MessagingHub가 응답하는 내용도 모델링돼 있지만 여기서는 생략합니다.
웹소켓 기반으로 폴백(fallback)으로 FCM을 보낼 수 있는 연결 메시지(connection message) 사례를 살펴봤습니다. MesagingHub에서는 이외에도 아래와 같은 기능을 지원합니다.
- 서버 푸시
- FCM(Firebase Cloud Messaging)
- SMS, 이메일
- 채팅(추후 지원 예정)
- 메시지 전송 및 트래킹을 위한 API 지원
프레임워크에 의존하지 않도록
앞서 발행한 메시징 시스템(a.k.a messaging-hub) 톺아보기에서 말씀드렸듯 MessagingHub에서는 코어 라이브러리를 사용하고 있습니다. MessagingHub는 그동안 여러 차례 시스템 구조를 변경해 왔고, 그보다 많은 수의 리팩토링을 거쳐 왔는데요. 직접 만들어 사용하고 있는 코어 라이브러리는 그때마다 저희의 수고를 덜어준 1등 공신입니다.
이번에 새로 개발한 서버 SDK 역시 첫 의존성은 코어 라이브러리였습니다. 덕분에 서버 SDK를 빠르게 PoC할 수 있었지만, 한 가지 고민이 생겼습니다. 코어 라이브러리는 MessagingHub 프로젝트 내부에서 사용하는 커스텀 라이브러리였기 때문에 MessagingHub를 구성하는 Spring 프레임워크에 의존합니다. 예를 들어 AOP나 빈(bean) 주입, 예외 처리, 로그 관리 같은 것들이 포함돼 있습니다. 따라서 서버 SDK를 사용하는 쪽에서도 Spring 프레임워크에 의존하게 됩니다.
Spring 프레임워크는 강력하고 좋은 프레임워크이지만, 그렇다고 연동 측에서도 반드시 이 프레임워크에 의존하게 만드는 것은 바람직하지 않다고 생각합니다. 라이브러리를 제공하는 입장에서 더 많은 개발자가 각자의 다양한 시스템 환경에서 편하게 사용할 수 있도록 특정 프레임워크에 구애받지 않는 SDK를 제공하고 싶었고, 이에 따라 MessagingHub의 서버 SDK에서 Spring 의존성을 제거한다는 목표를 세웠습니다.
이 목표를 달성하기 위해 먼저 1차로 MessagingHub의 코어 라이브러리를 바탕으로 서버 SDK를 PoC한 뒤, 이후 다시 코어 라이브러리와 Spring 프레임워크 의존성을 제거하는 2차 작업을 진행했습니다. 1차 PoC 결과에서 제가 원하는 대로 작동하는 것을 확인한 뒤, 코어 라이브러리를 제거하고 그 외 Spring 프레임워크 기반의 코드들을 제거해 나갔습니다. 당연히 그 과정에서 에러가 발생했는데요. 이들을 하나하나 고쳐 나가면서 전체적인 라이브러리의 개발 스타일을 정립할 수 있었습니다. 또한 결과적으로 서버 SDK의 의존 관계를 최소화해 콤팩트한 라이브러리로 만들 수 있었습니다.
서버 SDK 내에서의 라이브러리 의존성 설정
external-lib는 gRPC 통신을 위한 필수 스키마 정보를 포함하고 있기에 서버 SDK를 만들더라도 유지해야 하는 라이브러리입니다. 새로 만드는 서버 SDK는 기존 external-lib를 래핑(wrapping)해 손쉽게 연동할 수 있도록 만든 것이지 external-lib를 대체하는 것은 아닙니다. 따라서 연동 측은 기존과 같이 external-lib를 사용할지 아니면 서버 SDK를 사용할지 고를 수 있습니다. 물론 장기적으로는 기존에 external-lib를 사용하고 있던 곳도 서버 SDK로 교체하는 것을 권장하고 있고, 향후 external-lib 배포를 중단할 계획도 있지만 external-lib 자체는 사라지지 않습니다. 서버 SDK 내에는 결국 gRPC 통신을 위한 external-lib에 대한 구현이 포함돼 있기 때문입니다.
아래 build.gradle.kts은 서버 SDK 내에 external-lib 의존성을 추가하기 위한 커스텀 설정입니다.
val mergeExternalLib by configurations.creating<Configuration> {}.................[1]
tasks.jar {.......................................................................[2]
val jars = mergeExternalLib.filter {..........................................[3]
it.name.endsWith("jar") && it.path.contains("external-lib")
}
from({ jars.map { zipTree(it) } })............................................[4]
duplicatesStrategy = DuplicatesStrategy.EXCLUDE...............................[5]
}
dependencies {
mergeExternalLib(project(":external-lib"))....................................[6]
compileOnly(project(":external-lib")).........................................[7]
testImplementation(project(":external-lib"))..................................[8]
api("com.google.protobuf:protobuf-java-util:$protobufJavaUtilVersion")........[9]
implementation("io.grpc:grpc-protobuf:$grpcVersion")
implementation("io.grpc:grpc-stub:$grpcVersion")
implementation("io.grpc:grpc-netty-shaded:$grpcVersion")
...
}
mergeExternalLib
라는 Gradle 설정을 생성합니다. 의존성을 추가하기 위해 사용합니다.jar
태스크를 커스터마이징합니다. 여기서는 컴파일된 코드를 JAR 파일로 패키징합니다.- 앞서 만든
mergeExternalLib
설정에 추가된 파일 중 이름이jar
로 끝나거나 경로에external-lib
가 포함된 JAR 파일만 필터링해서jars
에 넣습니다. jars
에 넣은 JAR 파일들의 압축을 풀어서 포함시킵니다.- JAR 파일을 만들 때 같은 파일의 중복 포함을 방지하기 위해 중복된 파일은 제외합니다.
mergeExternalLib
의존성에external-lib
프로젝트를 추가합니다.external-lib
프로젝트는 컴파일 시에 사용합니다.- 테스트 의존성으로
external-lib
를 사용합니다. - Protocol Buffers(protobuf)로 정의된 스키마를 서버 SDK를 사용하는 연동 측에서 접근하기 위해서는 해당 인터페이스가 정의된 protobuf 라이브러리를 명시적으로 설정해야 합니다. 예를 들어
external-lib
에서는TargetType
이란enum
을 정의하고 있는데요. 연동 측에서도 사용할 수 있도록 API 키워드로 해당 protobuf 라이브러리를 추가했습니다.
주석
함께 프로젝트를 만들어가며 자연스레 공통 컨텍스트가 형성되는 내부 개발 멤버와는 달리, 아무런 공통 컨텍스트가 없는 외부 사용자에게는 상냥하게 접근할 필요가 있습니다. 따라서 외부 사용자가 사용할 서버 SDK에는 주석이 반드시 필요합니다. MessagingHub를 만드는 입장에서는 당연히 이해하고 있어야 할 내용을 연동 측 개발자도 당연히 알고 있을 것이라고 생각하는 것은 오산입니다. 사용자 자신이 설계하고 만든 것이 아니기 때문에 개념을 이해할 때 러닝 커브가 생기며, 이를 무시하면 결국 더 큰 커뮤니케이션 비용 지출로 돌아옵니다. 글을 쓰거나 대화할 때 독자와 화자가 누구인지 고려해 상대방과 눈높이를 맞추는 것처럼, 라이브러리 역시 사용자가 누구인지 파악해 그 눈높이에 맞추는 게 필요하며, 그 핵심 무기가 바로 주석입니다.
그런데 오랫동안 주석을 쓰지 않았던 탓에 어떻게 쓰는 것이 잘 쓰는 것인지 고민됐고, 요즘 트렌드를 알고 싶었습니다. MessagingHub는 Kotiln 기반입니다. Kotlin 기반의 코딩 컨벤션을 검토해 보니 아래와 같이 주석을 쓰는 것을 권장하고 있었습니다. @param
과 @return
은 가급적 사용하지 말고 한 줄에 묶어서 적되 파라미터는 링크로 표시하라는 것입니다. MessagingHub의 서버 SDK 주석 스타일은 기본적으로 이 방식을 사용하고 있습니다.
// Avoid doing this:
/**
* Returns the absolute value of the given number.
* @param number The number to return the absolute value for.
* @return The absolute value.
*/
fun abs(number: Int): Int { /*...*/ }
// Do this instead:
/**
* Returns the absolute value of the given [number].
*/
fun abs(number: Int): Int { /*...*/ }
아래는 MessagingHub에서 제공하는 SDK를 통해서 주석을 메서드 체이닝하며 확인하는 예시입니다. 주석을 촘촘하게 넣어서 SDK를 사용하는 개발자가 언제든 기능이나 설명을 확인할 수 있도록 만들었습니다.
클래스 파일 버전
MessagingHub 시스템에 서는 최신 LTS(long term support) 버전의 JDK를 사용하는 것을 기본으로 합니다. 하지만 서버 SDK 입장에서는 연동 컴포넌트의 JDK 환경을 고려해야 이로 인한 충돌을 최소화할 수 있습니다. 예를 들어 SDK의 JDK 버전이 연동 측보다 높다면, 연동 자체가 어려운 경우가 생깁니다.
따라서 버전은 가장 보편적으로 허용 가능한 수준인 1.8에 맞추기로 했습니다. 참고로 gRPC-java의 지원 범위는 JDK 1.8 이상이기 때문에 이보다 더 낮추면 gRPC 통신이 어려워지며, 그 밖에 서버 SDK를 구동하기 위해 의존하는 라이브러리의 기능 지원과 한계를 떠안아야 합니다.
물론 JDK 1.8 이하를 사용하는 곳도 지원한다는 요구 사항이 생기는 것에 대비해 대안도 고민할 필요가 있습니다. 예를 들어 더 낮은 버전에 대해서는 REST API를 지원하도록 연동 방식을 확대하는 것입니다. 현재 MessagingHub는 외부 연동 시스템과의 통신 프로토콜로 gRPC를 사용하고 있는데요. 그 한계를 고려해 유사 시에 대비하는 차원에서 REST API에 대응하는 것은 플랫폼 프로젝트로서도 고민해 볼 주제입니다. 이와 같이 클래스 파일의 버전을 고민하면서 그 외의 것들러 고민이 확장되는 것은, 한편으로는 플랫폼 프로젝트이기에 할 수 있는 고민이라고 느낍니다.
결론을 말씀드리자면, 서버 SDK의 버전은 아래와 같이 1.8로 맞춰 빌드하는 것으로 결정했고, Kotlin 버전은 1.6에 맞췄습니다.
java.sourceCompatibility = JavaVersion.VERSION_1_8
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = JavaVersion.VERSION_1_8.toString()
languageVersion = KotlinVersion.KOTLIN_1_6.version
}
}
그 뒤 연동 테스트를 하기 위해 서버 SDK를 JDK 1.8 환경의 프로젝트에 올려서 구동했는데요. 아래와 같은 에러가 발생했습니다.
Exception in thread "main" java.lang.UnsupportedClassVersionError: com/linecorp/abc/messaging/external/lib/proto/message/ExternalMessageGrpc has been compiled by a more recent version of the Java Runtime (class file version 55.0), this version of the Java Runtime only recognizes class file versions up to 52.0
클래스 파일의 버전은 55인데 현재 구동 환경은 52라서 호환되지 않는다는 에러입니다. 클래스 파일 버전과 JDK 버전 매핑은 아래와 같습니다.
JDK | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
---|---|---|---|---|---|---|---|---|---|---|
Class Major Version | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
external-lib의 클래스 메이저 버전을 확인해 보니 55였습니다.
$ javap -verbose ExternalMessageGrpc.class | grep version
major version: 55
서버 SDK가 external-lib를 주요 의존 관계로 두고 있는 만큼 external-lib의 클래스 버전도 52가 되도록 다운그레이드했습니다. 이처럼 클래스 파일 버전이 연동 서버의 JDK 버전보다 높으면 연동 측에서 구동이 안 될 수 있기 때문에 이를 방지하기 위해 MessagingHub에서 제공하는 연동 라이브러리인 external-lib와 서버 SDK의 JDK 최소 지원 버전은 1.8로 결정했습니다.
커뮤니케이션 부채 청산 이후의 변화
앞서 소개한 통합 가이드와 뉴비를 위한 흐름 차트 등과 같은 문서를 통해 여러 관계자의 이해를 돕고 더욱 효율적으로 가이드할 수 있게 되었습니다. 현재 MessagingHub가 궁금하다면 문서와 영상을 참고하도록 가이드하고 있습니다.
또한 서버 SDK를 릴리스한 후 발생하던 단순 질의 응답 업무의 비중을 크게 낮출 수 있었습니다. 물론 질문량이 0이 된 것은 아니지만, 이후 들어오는 질문은 실제로 잘못돼 있어서 수정해야 하는 경우이거나 혹은 참고해 보완하면 좋을 질문들이 주를 이뤘습니다. 실무 관점에서 실제로 개선해야 할 포인트에서의 대화가 훨씬 많아진 것입니다. 아래 화면 캡처는 서버 SDK 도입 후 개발자들 간의 대화가 명료해진 사례입니다.
개발자는 코드로 얘기하는 게 그나마 제일 편합니다. 그런 관점에서 서버 SDK 라이브러리는 대화의 포문을 코드 수준에서 시작할 수 있는 일종의 가이드라인이 되기도 합니다.
결과적으로 뻔한 질문의 양은 줄었고, 올바른 방향으로의 밀도 있고 생산적인 질문 은 늘었습니다. 구체적으로 아래와 같이 개선 효과를 정리할 수 있을 것 같습니다.
- 상세하게 시스템이나 배경을 이해하지 않아도 연동할 수 있게 되었습니다.
- 연동 측이 MessagingHub를 알고 싶거나 이해하고 싶을 때 탐색에 들이는 시간이 단축됐습니다.
- 매번 반복되는 뻔한 질문이 줄었습니다.
- 연동할 때 맥락을 이해하기 위해 투자해야 하는 시간이 줄었습니다.
- 문서화 및 연동 라이브러리를 통해 개선점이 명확해졌습니다.
- MessagingHub의 기능을 엔드 유저에게 전달할 때 거치는 라이프 사이클이 개선됐습니다.
- 이를 통해 결과적으로 엔드 유저에게 서비스를 효과적으로 전달할 수 있게 됐습니다.
저는 여전히 질문을 환영합니다. 사용자의 질문을 통해 현재의 부족함을 깨닫고 해야할 일의 실마리를 얻는 경우가 많기 때문입니다. 이 글은 모든 질문을 없애기 위한 것이 아닙니다. 그저 하지 않아도 될 질문의 절대적인 양을 줄이면서 원하는 방향으로 대화의 내용과 방식을 바꾸기 위한 노력의 정리입니다. 그 노력의 결과 현재 인입되는 질문은 이전보다 훨씬 더 건설적인 내용과 방향의 질문이 주를 이루고 있습니다. 소기의 목적은 달성한 셈입니다.
마치며
지금까지 커뮤니케이션 부채를 청산하고 업무 몰입도를 더 높이기 위해 사용자의 질문에 보다 효율적으로 대처할 수 있는 돌파구를 찾아 나섰던 개발자의 이야기를 들려드렸습니다.
외부 연동 라이브러리를 만드는 작업은, 특히 어떤 환경인지도 모르는 시스템에서도 잘 작동하도록 만들어야 하는 경우라면 내부 라이브러리를 만들 때보다 고민의 깊이가 조금 더 깊어집니다. 비유하자면 혼자 사는 세 상에서 더 넓은 세상으로 나와 누군가에게 내 모습을 공개하고 기여하는 행위라고 할 수 있습니다. 이를 위해서는 자신의 모습을 그 목적에 맞게 사회화하는 과정이 필요하며, 그 과정에서 필연적으로 스스로를 세심하게 객관화하는 연습을 하게 되고, 필요에 따라 각 부분을 추상화하거나 일반화하게 됩니다. 즉, 어떻게 하면 나와 상대방 모두 보다 쉽고 효율적으로 서로의 목적을 달성할 수 있을지 고민하게 되는 것입니다.
때로는 환경을 탓하며 불만을 가득 품은 채 그냥 도피하고 싶어질 수 있습니다. 당장 보기에 쉬운 선택을 하는 것이죠. 하지만 이는 미봉책에 불과합니다. 언젠가 더 큰 문제를 만나게 됩니다. 어렵고 힘들더라도, 불편한 상황을 개선하기 위해서는 모두가 납득할 수 있는 논리를 만들어 이것이 지금 우리에게 최선의 선택이라고 합의하는 과정을 거치며, 더 멀리 바라보고 조금 더 나은 시스템으로 만들어가는 선택을 해야 합니다. 최소 인력으로 최대의 효과를 내기 위해서 일하는 방식을 슬기롭게 바꿔 나가야 하며, 이런 노력들이 쌓이면 개발자와 사용자 모두 이전보다 조금 더 나아진 환경에서 만나게 될 것이라고 믿습니다.
MessagingHub에 관심이 있고 더 알고 싶으시다면 아래 글을 참고해 주시길 바랍니다.
다음에 또 재미난 이야기로 찾아오겠습니다. 긴 글 읽어주셔서 감사합니다.