23.06.15
DEVOTEE를 활성화 시키면
지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.
버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.
| 컨텐츠 유형 | 제목 | 저장일 | 삭제 |
|---|
본인인증 로그인에 실패하였습니다.
회원이 아니시거나 본인인증 등록이
완료되지 않은 사용자입니다.
안녕하세요~ Comm Divergence개발팀 김예찬입니다.
대용량 트래픽을 소화하는 에이닷 전화, 통화요약, 문자 서비스를
지난 2년 동안 개발/배포하며 무수히 많은 API 들의 생성과 변경이 있어왔습니다.
"인터페이스" 는 하나의 약속임에 따라
이를 관리하는데 적지 않은 리소스가 들어가는 것을 공감하실 거라 생각합니다.
스펙/명세의 버져닝/히스토리 관리
스펙/명세를 구현하기 위한 코드 작업
스펙/명세의 Viewing, 인증, 접근 권한
협업 및 관련 부서와의 소통, 히스토리 등등
저 역시 아직도 번번이 이러한 이슈를 겪고 있습니다만
적어도 담당하고 있는 서비스들 내에서라도 이러한 이슈를 해결하고자
하나의 Workflow, 하나의 프로젝트
를 모토로, API 명세 & SDK 용 프로젝트를 개발했고
지난 2년 동안 개발/운영해온 후기를 적고자합니다.
서버 개발자 A 와 클라이언트 개발자 B 의 사례로 문제점들을 설명드리려고 합니다.
개발자 A 가 출근한 어느 날, 급하게 연락이 와 새로운 요구사항을 전달받았습니다.
일단 코드는 수정하고 명세는 나중에 변경하면 되지라고 생각하며, 명세 변경을 미루었습니다.
그리고 그새 버그가 발생해 대응을 하는 동안, 명세 최신화를 까먹습니다.
문제 1: 명세와 코드의 inconsistency
이제 연동하려는 다른 개발자 B 는 명세에 나와있는 대로 구현을 해도 실패가 납니다.
이전에도 이런 경험이 있어, 더이상 명세를 신뢰할 수 없게 되었습니다.
명세대로 동작하지 않아 매번 확인을 위해 메신저를 보내보지만, 개발자 A 는 바빠서 답장 확인이 느립니다
연동이 성공할 때까지 커뮤니케이션 리소스가 더 들어가겠네요.
개발자 B는 드디어 연동을 위한 올바른 API 명세를 확인했습니다.
이제 코드로 구현을 해볼텐데요
이럴수가! JSON 객체 필드가 10개가 넘는데, Nested JSON 도 있습니다!
한 땀 한 땀 데이터 모델 클래스로 변환하다가 오타가 났는데,
실제로 연동을 시도하기 전까지 어떤 부분을 실수했는지 확인도 어렵습니다.
클라이언트 종류가 다양해, Android, iOS (Swift), Flutter, Javascript 코드로 각각 이를 반복합니다.
문제 2: 실수가 자주 날 수 밖에 없는 지루한 명세 -> 코드 작업 반복
시간이 흘러 개발자 B는 기존 API 를 업데이트하고자, 담당자인 개발자 A에게 연락을 했습니다.
새로운 API 버젼이 많이 나와있는데, 현재 몇 버젼까지 배포가 되어있는지 묻습니다.
그런데 개발자 A가 휴가를 가있어 대무를 맡은 다른 담당자는
현재 배포되어있는 서버는 어떤 버젼의 명세를 구현하고 있는지 모른다고 합니다.
문제 3: 명세와 어플리케이션의 버져닝 부재
이 후 개발자 B 는 개발자 A 가 휴가에서 복귀했다는 소식을 듣고 다시 연락을 했습니다.
기존 버젼과 신규 버젼의 변경점이 많은데, 어떤게 변경되었는지 질문을 합니다.
개발자 A는 시간이 많이 지나 잘 기억이 안나니, 직접 보고 비교해서 보라고 합니다.
새로 추가된 필드들이 너무 많아 실수할 것만 같습니다.
문제 4: 기존 버젼과의 히스토리 관리
문제 1: 명세와 코드의 inconsistency
문제 2: 실수가 자주 날 수 밖에 없는 지루한 명세 -> 코드 작업 반복
문제 1과 2의 원인은 명확합니다.
"명세를 수정하는 작업"과 "코드를 작성하는 작업"이 분리되어 있기 때문입니다.
이 작업을 하나로 합쳐
명세 = 코드
로 만든다면 해결할 수 있습니다.
이를 가능하게 해주는 오픈소스가 바로 OpenAPI Generator 입니다.
사실 개발자 분이시라면 이러한 Concept 을
OpenAPI Generator 보다 protobuf 를 사용하는 gRPC 를 통해 미리 알고 계실 수도 있으실텐데요.
인터페이스 명세를 원하는 프로그래밍 언어의 코드 파일로 생성해주는 오픈소스입니다.
아직 지원하지 않는 기능이 좀 있지만, 이를 감안하더라도 생산성이 극대화되기 떄문에
사용할 이점이 훨씬 더 크다고 판단했습니다.
참고로, 팀 내 메인 Tech Stack 이 Kotlin, Gradle 프로젝트인 관계로
이 오픈소스를 Gradle Plugin 으로 포팅한 플러그인 방식으로 프로젝트에 적용하였습니다.
문제 4: 기존 버젼과의 히스토리 관리
(문제 3은 후술) 문제 4는 이미 익숙한 문제라고 생각합니다.
Version Control System, Git 을 통해 해결할 수 있는 문제입니다.
버져닝이 되는 Wiki 도 방법이겠지만, OpenAPI 명세 파일은 일반적인 자연어가 아닌, Yaml /JSON 포맷이고
API 관리 주체인 개발자가 관리하는 측면에서, 개발자에게 친숙한 Git 이 더 좋은 해결책이라고 생각합니다.
API 명세의 변경과 히스토리를 커밋과 PR/MR, 코멘트 등을 통해 관리하도록 Git 프로젝트로 만들었습니다.
문제 3: 명세와 어플리케이션의 버져닝 부재
명세 파일은 근본적으로 Yaml 혹은 JSON 포맷입니다.
이를 잘 렌더링된 UI 로 보아야 비로소 가독성이라는 진가가 발휘된다고 생각합니다. (예쁜 UI는 마음이 편해지죠)
UI 로 제공하는 방법들은 여러 가지 있는데요, 직접 Web UI 서버(Swagger UI, Redoc 등)를 구축해도 되지만,
추가적인 서버 비용 및 운영/관리 없이
Gitlab 에서 자체 제공하는 OpenAPI 렌더링 기능을 이용할 수도 있습니다.
참고로, SKT 는 전사에서 관리하는 Gitlab 이 있습니다.
저는 Gitlab 의 다양한 기능들을 leverage 하는 방식, 즉 Gitlab-native 한 방식으로 다양한 리소스를 아끼고 있습니다.
사용법은 간단합니다.
OpenAPI 파일명에
openapi-라는 prefix 를 붙이면, Gitlab UI 에서 자동으로 렌더링을 해줍니다.
저는 CI 를 통해 이러한 prefix 가 자동으로 들어가게끔 강제하였습니다.
이렇게 브라우저, Gitlab UI 를 통해 볼 수 있게 됨에 따라
API 명세를 공유하는 방법 역시 Git Repository 의 코드 파일 URL 을 공유하기만 하면 됩니다.
따라서 자동으로 Git 에 접근할 수 있는 권한을 가진 사람만 볼 수 있도록, 인증 및 인가도 처리되구요.
GItlab 공식 이미지
추가로 위에서 설명드린 OpenAPI Generator 로 생성된 코드 파일을 사용하는 방법도
Gitlab 을 통해 손쉽게 이용할 수 있습니다.
바로 Gitlab Package Registry 에 코드 파일을 빌드해서 배포하면, SDK/Library 처럼 pull 받아서 이용하는 방식입니다.
Maven 과 같이 Public Registry 가 아닌 Private Registry 를 이용하려면, SaaS 를 사용하거나 직접 On-premise 로 구축해야할텐데요
Github 뿐만 아니라 Gitlab Server 에서도 이를 지원하고 있어, 별도 구축 비용 없이 전사 시스템을 이용할 수 있었습니다.
SDK/Library 를 publish 하는 작업도 maven-publish Gradle plugin 를 통해, 손쉽게 코드로 설정할 수 있었는데요.
Publish 간에는 버젼값을 설정할 수 있으므로, 명세의 버젼을 그대로 이용하여 SDK 버젼으로 배포하였습니다.
이를 통해 SDK 를 pull 받아 사용하는 Application 프로젝트에서는,
의존성 관리 (ex. build.gradle.kts) 파일에서 특정 버젼을 명확하게 명시하게 되고,
이는 배포되어 있는 Application이 어떤 API 의 버젼을 사용하는지 알 수 있게 되는 방식입니다.
// build.gradle.kts
dependencies {
implementation("com.skt.adot:***-mobile-api-kotlin-spring:1.0.4")정리하자면,
명세의 버젼 = Package Registry 에 publish 된 SDK/Library 버젼 = 배포된 Application 의 의존성에서 확인
이 가능해지는 것입니다.
위에서 설명드린 해결책을 적용하고, 약 2년 정도 개발/운영하며 업데이트된 코드 프로젝트의 구조를 설명드리고자 합니다.
API 명세와 SDK 를 관리하는 책임을 담은 하나의 프로젝트로, 본부 내에서 담당하는 거의 모든 서비스들의 API 명세들을 담고 있습니다.
(대 클라이언트 Application 용, 내부 서버 연동 간에도 사용)
따라서 프로젝트 명칭을 openapi-monorepo 로 명명하였습니다.
.
├── CODEOWNERS
├── build-logic
│ ├── build.gradle.kts
│ ├── settings.gradle.kts
│ └── src
│ // 생략
├── build.gradle.kts
├── settings.gradle.kts
├── specs
└── templates
├── android-kotlin-client-gradle
├── java-client-gradle
├── java-spring-boot-2-gradle
├── java-spring-boot-3-gradle
├── kotlin-client-gradle
├── kotlin-client-gradle-v2
├── kotlin-spring-boot-2-gradle
├── kotlin-spring-boot-3-gradle
└── kotlin-spring-boot-3-webflux-gradle/specs 하위에 Application 프로젝트 별로 OpenAPI 명세들을 작성하여 Commit 합니다.
OpenAPI 는 $ref 라는 syntax 를 통해 다른 파일도 참조할 수 있어, 목적에 맞게 하나의 명세를 여러 파일로 관리해도 됩니다.
> Tip. API endpoint 별로 파일을 분리해서 관리했습니다.git branch 를 생성하여 PR/MR 를 만듭니다
코멘트를 통해 협업 구성원 및 담당자들과 명확하게 코드를 보며 논의를 나눕니다. (이는 Gitlab 에도 기록됩니다)
PR/MR 간 CI 를 통해, 명세를 하나의 파일로 merge 합니다. 이는 Gitlab 의 OpenAPI UI 렌더링이 서로 다른 파일 간 참조를 지원하지 않기 때문입니다.
이 때 merge 한 파일명에 openapi- prefix 를 붙여, Gitlab 이 렌더링할 수 있게 표시해줍니다.
모든 협의가 되어 main 브랜치에 merge 하면, CI 를 통해 사용자가 설정한 언어 별 코드 파일을 만들어 Build 후 Gitlab Package Registry 에 Publish 합니다.
각 Application 프로젝트에서 4. 에서 publish 된 SDK/Library 를 pull 해서 사용합니다.
명세를 보고 싶을 경우, 브라우저에서 Gitlab 을 통해 OpenAPI 파일을 확인합니다.
└── templates
├── android-kotlin-client-gradle
├── java-client-gradle
├── java-spring-boot-2-gradle
├── java-spring-boot-3-gradle
├── kotlin-client-gradle
├── kotlin-client-gradle-v2
├── kotlin-spring-boot-2-gradle
├── kotlin-spring-boot-3-gradle
└── kotlin-spring-boot-3-webflux-gradle프로젝트를 보시면 /templates 라는 디렉토리가 있습니다.
하위 폴더들은 Java/Kotlin base 의 프레임워크 이름으로 되어있는데요.
이는 저희 팀에서 관리하는 Application 들이 언어 혹은 프레임워크 별로 버젼 차이가 있어 이를 지원하기 위함입니다.
또한 동일한 언어와 프레임워크 버젼이어도 의존성이 다른 경우,
의존성을 사용자가 원하는 방식으로 관리하는 옵션을 제공하고자 /templates 디렉토리를 통해
다양한 버져닝, 의존성 관리를 가능하게 해주었습니다.
위 "프로젝트 이용 Workflow" 의 4번째 항목인, "코드 파일을 만들어 Build" 할 때 원하는 template 으로 코드를 옮겨 build 되도록 CI 와 설정을 구성했습니다.
본 프로젝트는 OpenAPI Generator Gradle Plugin 을 한 번 Wrapping 해서
다른 사용자(개발자)들이 편하게 이용하게끔 기능들을 제공하고 있습니다.
위에서 Java / Kotlin 코드 생성만 언급했으나, SKT 와 에이닷 서비스들이 이용하는 Swift5, Dart, Typescript 까지 코드 파일을 생성하여 이용하고 있습니다.
실제로 오픈소스가 제공하는 Generator 들은 더욱 많으니 참고해서 원하는 설정을 이용해볼 수 있습니다.
그런데 언어 별로 Build 환경, Registry 가 다름에 따라, CI 간 적절한 Build 용 Image 지원 및 Registry 설정이 필요합니다.
참고로 Swift5 와 Dart 는 Git Branch 를 이용한 SDK/Library Package 관리를,
Typescript 는 동일하게 Gitlab Package Registry 를 이용하고 있습니다.
Dart Registry 지원을 위한 별도 Git Repository
monorepo 라는 명칭이 들어간 것처럼, 서로 다른 서비스와 컴포넌트들이 공통으로 사용하는 단일 프로젝트임에 따라
프로젝트 내에서 디렉토리 관리도 중요합니다.
이에 Gitlab CODEOWNERS 기능을 통해, 프로젝트 내 서로 다른 디렉토리 간에도 권한을 설정할 수 있게 됩니다.
디렉토리 별 담당자를 지정하고, 수정이 발생하면 반드시 PR/MR 간 approve 를 해야합니다.
당연하지만
main브랜치로의 direct push 는 불가능합니다.
이를 통해 실수를 예방하고, 개발자는 자신감을 갖고 작업이 가능합니다.
본 프로젝트에서는 Pre-compiled Script Plugin 을 통해 Build 와 Gradle 관련 로직을 관리하고 있습니다.
프로젝트를 처음 만들었을 때는 root 의 build.gradle.kts 파일에 모든 로직이 들어가있었으나,
시간이 흘러 점점 파일이 커져 유지보수성이 떨어진다 판단했고,
더 모듈성 있게 관리를 하고자 Plugin 방식으로 Gradle 로직을 분리하여 관리하고 있습니다.
Plugin 및 buildSrc 방식과의 비교 참고 해당 내용도 읽어보시면 좋을 것 같습니다.
사용자 A
OpenAPI 프로젝트 이후로 코드 쓸 필요 없어서 너무 편해요.
사용자 B
내가 원하지 않는 의존성이나 사용하지 않는 코드 파일도 포함되길래, 그냥 제가 다 만들래요
사용자 C
집에 가고 싶어요
이렇게 사용자마다 반응이 천차만별이었는데요
아무래도 오픈소스이다보니 contribute 하지 않는 이상, 지원되지 않는 기능들 (주로 Polymorphism)과
필요한 것보다 많이 생성되는 코드 파일들과 라이브러리 의존성과 같이 컨트롤하기 어려운 부분들도 많이 있었습니다.
이러한 점을 고려해서 잘 선택하시면 될 것 같습니다.
총 1,437 개의 패키지 발행
하루 평균 1.3 개의 main 브랜치 커밋 = SDK/Library 발행. 총 1176개의 main 브랜치 커밋
다른 사용자분들의 꾸준한 API 작업
Gitlab Analyze 로 돌아보았을 때 구성원분들이 열심히 사용해주셨던 것 같습니다.
개인으로 시작한 프로젝트이지만 팀 전체, 다른 팀 사용자들까지 사용하면서 점차 규모가 커졌던 것 같습니다.
저 개인적으로도 위에 언급해놓은 문제점들이 해결되어, 쏠쏠하게 잘 이용하고 있는 프로젝트였습니다.
개인적으로 리뷰를 해보자면
생각보다 MR/PR 간 코멘트가 많지 않았다
내부 마이크로서비스 연동 간, 작은 규모 혹은 적은 개수의 API 라면, 굳이 이용할 필요는 없다. 다만 외부 팀과의 협업일 때는 규모가 작아도 만드는게 낫다. (명세 공유 차원)
정도의 생각이 들었습니다.
마지막으로 지속적으로 Contribute 하고 이용/피드백 주신 저희 팀장님들, 팀원분들이 아니었다면, 이 프로젝트는 저 혼자만의 힘으로 절대 유지되지 못했을 것입니다. 가장 큰 감사의 말씀을 전하고 싶습니다.
추가로 최근에 메이저 버젼 업데이트를 작업해주신 creator.koo 님께도 감사의 말씀 전하고 마치겠습니다.
읽어주셔서 감사합니다.
DEVOTEE를 활성화 시키면
지금 작성한 댓글에 AI가 댓글을 달아줍니다.