실무를 하면서 내가 개발하고 있는 서비스가 어떤 구조로 이루어져있는지 배웠다.
Jenkins, Docker, Kubernetes, APM 도구 등 여러 기술이 사용되고 있었다.
전체 흐름
코드 변경 이후 배포까지의 흐름은 아래와 같다.
- 코드 수정 (Git push)
- Jenkins를 통한 빌드
- Docker 이미지 생성
- Kubernetes 환경에 배포
- APM 도구로 확인 가능
Git 브랜치 전략
배포일은 주 2회로 정해진 날이 존재한다. 물론 필요할 경우 언제든 가능하다.
main, develop, feature, hotfix, release 브랜치로 나뉜다.
- main 브랜치를 기준으로 feature 브랜치를 딴다.
- feature 브랜치에서 작업을 완료하면 develop 브랜치에 merge한다.
- develop 브랜치에 merge 후 jenkins를 통해 배포하면 검증계에 반영된다.
- 배포일이 되면 배포일의 release 브랜치가 만들어진다.
- develop 브랜치에 merge한 feature 브랜치의 작업내용들이 검증이 모두 된 경우 release 브랜치에 merge가 가능하다.
- 여러 검증된 feature 브랜치의 작업들이 release 브랜치에 merge되고 release 브랜치를 main에 merge해 운영계에 반영하는 방식.
- 긴급 배포 시 hotfix 브랜치를 만들어 feature 브랜치에서 작업 후 merge한다.
- 롤백 시 feature 브랜치를 활용해 revert하여 커밋 기록을 남긴다.
Jenkins + Docker
코드를 수정한 후 수동으로 서버에 올리는 방식이 아니라, Jenkins를 통해 빌드와 배포가 자동으로 이루어진다.
이 과정에서 Docker 이미지를 생성해 배포하는데, 환경마다 동일한 실행 환경을 보장하기 위한 목적이라고 이해했다.
Kubernetes
Docker로 생성된 이미지는 Kubernetes 환경에서 실행된다.
애플리케이션은 Pod 단위로 실행되며, 여러 Pod를 통해 트래픽을 분산 처리하는 구조로 되어있다.
이 구조를 통해
- 트래픽 증가 시 확장 가능
- 장애 발생 시 파드 단위 자동 복구
- 서비스 단위 관리
이러한 장점들을 가질 수 있다.
단일 서버에서 실행하는 구조가 아니라 Pod 단위로 분리되어 있기 때문에 장애가 발생해도 서비스 전체가 아닌 일부만 영향을 받도록 설계된 구조라고 이해했다.
openShift를 이용해 관리하고 있다.
모니터링
배포된 애플리케이션은 모니터링 도구를 통해 확인할 수 있다.
- 요청 처리 시간 (트랜잭션)
- 에러 발생 여부
- 파드 상태
- 트랜잭션 흐름
등을 확인할 수 있고, 문제가 발생했을 시 SMS 알림을 통해 빠르게 원인을 파악할 수 있게 도움을 준다.
환경 분리 (개발/검증/운영)
환경은 개발, 검증, 운영으로 나뉘어 있지만 실제로 망분리 환경에서 개발을 진행하고 검증 환경을 통해 확인하고 있다.
개발서버는 사용되지 않고 있다. 차세대 프로젝트를 진행할 때 사용되었지만, 검증계에서 충분히 테스트가 가능하고 개발계는 사용되지 않다보니 테스트용 데이터를 동기화시키는데의 비용문제 등 활용 가치가 떨어져 사용하고 있지 않다.
프로젝트 구조
백엔드 프로젝트는 도메인 기준으로 모듈이 나누어져 있었다.
- 물류
- 채권
- 공통
각 모듈은 독립적으로 실행이 가능하도록 구성되어 있었고,
필요한 모듈만 선택해서 서버를 실행할 수 있는 구조였다.
예를 들어 물류 서버만 실행하면
채권 관련 API는 동작하지 않고 404(Not Found)가 발생한다.
실행되는 모듈 기준으로 Component Scan 범위가 달라지면서 다른 도메인의 Bean은 로딩되지 않는 구조이다.
반대로 로그인과 같은 공통 기능은 정상적으로 동작한다.
이 구조를 보면서 단순히 패키지를 나눈 것이 아니라 실행 단위 자체를 분리한 구조라고 이해했다.
공통 모듈 동작 방식
공통 모듈에는 로그인, 인증, 예외 처리와 같은 기능들이 포함되어 있었고 각 도메인 모듈에서 공통적으로 사용되는 구조였다.
그리고 물류나 채권 도메인에 해당되지 않는 기능은 웹 모듈로 따로 빠져있다.
예를 들어
- GlobalExceptionHandler
- Interceptor
같은 기능들은 공통 모듈에 정의되어 있고, 각 서버 실행 시 함께 로딩되어 동일하게 동작하는 방식이었다.
즉, 물류 서버를 실행하더라도
- 예외 처리 방식
- 인증 처리
- 공통 로직
은 동일하게 유지되는 구조였다.
모듈 분리 이유
이렇게 모듈을 분리한 이유는 몇 가지로 생각해볼 수 있었다.
- 도메인별 책임 분리 (물류 / 채권)
- 특정 기능만 선택적으로 실행 가능
- 변경 영향 범위 최소화
- 테스트 및 배포 단위 분리
특히 하나의 애플리케이션에서 모든 기능을 같이 실행하는 구조보다 필요한 도메인만 실행할 수 있다는 점이 인상적이었다.
Gradle 기반 모듈 관리
이 구조는 Gradle을 통해 관리되고 있었고, 각 모듈이 의존성을 가지는 형태로 구성되어 있었다.
예를 들어
- 공통 모듈 → 물류, 채권에서 참조
- 물류 / 채권 → 독립 실행 가능
과 같은 구조였다.
또한 일부 라이브러리는 사내 Nexus 저장소를 통해 관리되고 있었고, 외부 의존성을 내부에서 통제하는 방식으로 보였다.
정리
전체 구조를 정리해보면서 단순히 기술을 붙여서 만든 구조가 아니라, 배포 안정성과 확장성을 고려해 설계된 구조라고 느꼈다.
- Jenkins를 통한 자동화된 배포
- Docker를 통한 실행 환경 통일
- Kubernetes를 통한 확장성과 장애 대응
- 모듈 단위 분리를 통한 책임 분리
각각의 요소들이 역할을 나눠서 동작하는 구조였다.
특히 모듈을 나누고 Pod 단위로 실행하는 구조는 트래픽 증가나 장애 상황에서 유연하게 대응하기 위한 설계라고 이해했다.
다만 실제로 사용하면서 불편했던 점도 있었다.
하나의 모듈이 실행될 때 Pod가 여러 개 생성되는데, 로그가 Pod마다 분산되어 있어 문제 상황을 분석할 때 한 번에 확인하기 어려웠다.
특정 요청에 대한 로그를 확인하려면 여러 Pod의 로그를 각각 확인해야 하는 경우도 있었고, 이 과정에서 시간이 많이 소요되었다.
추후 이 부분을 내가 담당해서 개선해볼 수 있겠다는 생각을 했다.
결과적으로 이 구조는 확장성과 안정성을 위한 설계이지만, 운영 관점에서는 로그 추적이나 문제 분석과 같은 문제도 있을 수 있다는 것을 느꼈다.
단순히 기능을 개발하는 것뿐만 아니라 이러한 운영 환경까지 고려해야 한다는 점을 알게 되었다.