본문 바로가기

Software Engineering

Git Submodule을 활용한 IDL(Interface Definition Language) 적용

시작하며

 

 내가 속한 팀이 담당하고 있는 대중교통 시스템은 대략 4가지 언어로 구성되어 있다. 일단 자바가 빠질 수 없겠고, 나머지는 스칼라, C++, 파이썬이다. 최근에는 업계에 몰아치는 코틀린 열풍에 힘입어, 시스템 내 신규 프로젝트(컴포넌트 수준)를 모두 코틀린으로 작성하고 있기도 하다. (사실 코틀린은 자바와 거의 100% 호환이 되기에, 자바라고 해도 무방할듯?)

 

 이와 같이 시스템의 각 컴포넌트(일종의 마이크로 서비스, 이하 '컴포넌트'로 명명)들이 서로 다른 언어로 작성된 상황에서 유용한 것이 제목에서 언급한 'Interface Definition Language' 이다. 위키백과에서는 이를 아래와 같이 정의하고 있다.

 

 인터페이스 정의 언어(Interface Description Language 또는 Interface Definition Language, IDL)는 소프트웨어 컴포넌트 인터페이스를 묘사하기 위한 명세 언어이다. IDL은 어느 한 언어에 국한되지 않는 언어중립적인 방법으로 인터페이스를 표현함으로써, 같은 언어를 사용하지 않는 소프트웨어 컴포넌트 사이의 통신을 가능하게 한다. 예를 들면, C++을 사용하여 작성한 컴포넌트와 자바를 사용한 컴포넌트 사이에서 국한되지 않고, 인터페이스를 묘사하는 개념이다.

 

 우리 대중교통 시스템내의 각 컴포넌트는 이러한 IDL로써 구글의 'Protocol Buffers'를 채택하여 `grpc` 나 스프링의 `RestTemplate` 로 다른 컴포넌트와 통신을 하고 있다. 예를들어 C++로 작성되어 있는 경로 엔진이 출발지와 도착지 등의 정보를 담고 있는 Protocol Buffers DTO 객체를 grpc를 통해 전달 받아 처리하는 식이다.

 

  IDL은 다양한 활용 방법이 있겠지만, 일단 우리는 주로 시스템내 공통된 DTO를 정의하여 각 컴포넌트에 적용하고 있다. 이때 또 궁합이 잘 맞는것이 'Git Submodule' 인데, 서두가 길었지만 이번 포스트에서는 Git Submodule을 활용한 IDL 적용 과정을 간단한 예제를 통해 정리하려고 한다.

 

IDL 프로젝트 생성

 

먼저, 시스템 내에서 공통적으로 사용할 DTO의 명세를 관리하는 IDL 프로젝트를 생성해 보자. 

 

 가령, 시스템 아키텍쳐 내에 각 컴포넌트에서 발생하는 'Log' 데이터를 전송받아 처리를 담당하는 컴포넌트가 따로 구축되었다고 하자. 이 로그처리 컴포넌트에 대한 Protocol Buffers의 Interface Definition은 아래와 같다.

 

syntax = "proto3";

package com.asuraiv.proto;

option java_package = "com.asuraiv.proto.messages";
option java_outer_classname = "LogProto";

import "google/protobuf/timestamp.proto";

message ActionLog {

  string createdBy = 2;
  Action action = 1;
  google.protobuf.Timestamp createdAt = 3;
}

enum Action {
  UNKNOWN_ACTION = 0;
  CREATE_USER = 1;
  DELETE_USER = 2;
  .
  .
  .
}

 

이러한 IDL 명세를 관리하는 프로젝트를 간단하게 구성하고 이에 대한 git repository를 생성했다.

 

 

 시스템 내 각 컴포넌트는 다양한 Action에 대한 로그를 발생시키고, 이러한 로그들은 위와 같은 공통된 명세로 변환되어 로그처리 컴포넌트에 전송된다. 굳이 그림을 그려보자면 대략 아래와 같은 느낌일 것이다.

 

흔히 볼수 있는 E-Commerce 도메인의 시스템 구조. 조금 극단적이지만 각 컴포넌트마다 모두 다른 언어로 구성되어 있다고 가정한다.

 

 그림을 그려놓고 생각해보니, 굳이 위와 같은 구조를 가져간다면 Kafka나 RabbitMQ와 같은 메시지(일반적으로 JSON) 처리 계층을 두어서 컴포넌트간 의존성을 느슨하게 하는 Producer-Consumer 방식을 적용하거나, 아싸리(?) ELK Stack과 같은 로그 처리에 유용한 기술 스택을 사용하는 것이 더 적절한 것 같다는 생각이 들었다.

 

 하지만 멀티랭귀지가 적용될 수 있는 설계(MSA 같은)에서 각 컴포넌트간 통신에는 규모와 요구되는 상황에 따라 여러가지 방법이 있을 수 있고, 이렇게 'IDL'을 사용하는 것은 그 중에 하나이다. 이를 설명하기 위해 단지 우리에게 친숙한 'E-Commerce' 도메인을 예시로 든 것이라고 이해해 주었으면 한다.

 

IDL 적용

 

 예전엔 공통화된 코드 사용에 대한 요구가 있을때 해당 코드를 별도의 '공통 라이브러리'로 작성하여 Nexus Repository에 업로드하고, Maven이나 Gradle로 의존성을 추가하여 사용하곤 했었다. 이 방식을 지금 생각해보면 해당 라이브러리 코드 수정시 발생하는 배포 및 버전 변경 등을 고려했을 때 여간 번거로운것이 아니다. 하지만 'Git Submodule'을 사용한다면 변경사항을 fetch로 적용하면 그만이기 때문에, 해당 라이브러리의 관리와 적용이 매우 간단해진다.

 

 이제 위 IDL 프로젝트를 Git Submodule을 이용해 해당 IDL을 사용하고자 하는 프로젝트에 적용 해보자. 바로 전 단락에서 언급한 'E-Commerce' 도메인을 그대로 구현하여 예제로 만드는 것은 무리가 있으므로, 내가 요즘 스터디 목적으로 작성하고 있는 'kotlin-backend-example' 에 적용 해보겠다.

 

일단 'kotlin-backend-example'  프로젝트를 clone 받은 상태에서, 서브모듈을 추가한다.

 

$ git submodule add https://github.com/asuraiv/protobuf-idl.git generated-sources/protobuf-idl

 

위 명령어를 수행하고 나면 프로젝트에 `.gitmodules` 라는 파일이 추가되는데, 내용은 아래와 같다.

 

 

이쯤되면 대충 알겠지만 Submodule은 몇개라도 더 추가가 가능하다. 결과적으로, 프로젝트 root에 'generated-sources' 디렉토리가 만들어지고 그 하위에 'protobuf-idl' 프로젝트가 내려받아진다.

 

 

`git status` 로 로컬 working directory를 확인해보면 아래와 같은 결과가 나타난다.

 

 

프로젝트 root에 추가된 'protobuf-idl' 프로젝트의 하위 파일들이 아닌 프로젝트 자체가 '새 파일'로 취급 되고 있다. 정확히 말하면 git 에서 서브모듈을 감지하여 모듈 자체를 tracking 한다고 볼 수 있다. 위 변경사항을 커밋한다.

 

한가지 짚고 넘어가야 할 부분이 있다. 만약 'protobuf-idl' 프로젝트에 변경사항이 발생했다면 어떤일이 일어날까? 아래와 같이 'ActionLog' Protocol Buffers 명세에 필드를 하나 추가하고, push 해보자.

 

로그가 어느 서비스에서 발생했는지를 나타내는 필드를 추가

 

그리고 다시 IDL을 적용하려는 프로젝트로 돌아와, 'generated-sources/protobuf-idl' 디렉토리에서 `git pull`을 사용해 'protobuf-idl' 변경사항을 내려받고 `git status` 로 상태를 확인해보면, 아래와 같이 서브모듈 자체의 변경사항으로 감지 된다.

 

 

간단하게 Submodule 사용법을 정리해봤다. 이제 해당 IDL을 프로젝트에서 사용해보자.

 

Protocol Buffers의 Gradle 설정

 

 `.proto` 파일에 정의된 Protocol Buffers 명세를 자바 클래스로 변환하려면 `protoc` 라는 별도의 명령어를 사용해야 한다. 하지만 빌드 환경에 해당 명령어를 사용하기 위한 툴을 설치할 필요 없이, Gradle의 `protobuf-gradle-plugin` 에서 제공하는 task를 통해 자바 클래스 변환이 가능하다. 먼저 `buildscript` 블럭에 해당 플러그인 설정을 하자.

 

buildscript {

    ext {
        protoVersion= '3.4.0'
        protobufGradlePluginVersion = "0.8.5"
    }

    repositories {
        mavenCentral()
    }

    dependencies {
        classpath("com.google.protobuf:protobuf-gradle-plugin:${protobufGradlePluginVersion}")
    }
}

apply plugin: "com.google.protobuf"

 

그리고 `protobuf` 블럭에 `protoc` 라이브러리 설정 및 변환될 자바코드가 위치할 디렉토리를 지정, 해당 디렉토리를 `sourceSets` 로 묶는다.

 

def generatedDir="generated-sources"
def protoOutputPath="${generatedDir}/protobuf-idl"

protobuf {

    protoc {
        artifact = "com.google.protobuf:protoc:${protoVersion}"
    }

    generateProtoTasks.generatedFilesBaseDir = "${generatedDir}"
}

sourceSets {

    main {

        proto {
            srcDirs "${protoOutputPath}/proto"
        }

        kotlin {
            srcDirs "/src/main/kotlin", "${project.rootDir}/generated-sources/main/java"
        }
    }
}

 

마지막으로 런타임 시 필요한 Protocol Buffers 라이브러리 의존성을 추가하면 준비는 끝난다.

 

dependencies {
    implementation "com.google.protobuf:protobuf-java:${protoVersion}"
    implementation "com.google.protobuf:protobuf-java-util:${protoVersion}"
    implementation "com.googlecode.protobuf-java-format:protobuf-java-format:1.4"
}

 

Protocol Buffers 자바 클래스 변환

 

Gradle 설정이 정상적으로 되었다면, 아래와 같은 태스크를 통해 Protocol Buffers 명세를 자바 클래스로 변환한다.

 

$ ./gradlew generateProto

 

또는 IDE(IntelliJ)에서 제공하는 Gradle View에서 간편하게 태스크를 실행할 수도 있다.

 

 

태스크를 실행하고 나면, 지정한 디렉토리에 변환된 Protocol Buffers 자바 클래스가 위치 된다.

 

 

이제 이 클래스를 프로젝트 어디에서든 사용 할 수 있다.

 

마치며

 

 이번 포스트에서 예시로 든 'E-Commerce' 시스템은 앞서 언급 했듯이 각 컴포넌트 들이 메시지 처리 브로커를 통해 통신하는 경우가 많다. 또 경우에 따라 API-Gateway 와 같은 계층을 두어 모든 Endpoint를 관리하게 하기도 한다. 

 

 하지만 소프트웨어 설계에서 모든 상황에 마땅한 'Deus Ex Machina' 와 같은 솔루션이란 있을 수 없고, 다양한 Variation이 존재한다고 생각한다. 이러한 맥락에서 적어도 내가 담당하고 있는 대중교통 시스템에서는 - 물론 어떤 부분은 Kafka, RabbitMQ를 사용하기도 하지만 - Protocol Buffers 포맷으로 통신하는 편이 훨씬 심플한 설계로 저지연, 고효율의 효과를 보았다.

 

 사실 이번 포스트를 작성하게 된 계기는 신규 프로젝트를 세팅할때마다 'Submodule' 설정 방식을 매번 구글링을 통해 찾아보는 것에 문제 의식을 느껴 머리속에 정리(그리고 공유)하려고 한 것이었다. 본래 설명충(..) 기질이 있어 글이 뭔가 장황해진 감이 있지만, 이제 더 이상 구글링을 하지 않아도 될만큼 머리속에 각인 되었으니 나름 만족을 느끼며 글을 마친다.