이번 프로젝트의 목표는 짧은 시간 내에 기획부터 DevOps까지 효율적으로 구현하는 것이었습니다. 이를 위해 저는 어떻게 하면 업무를 효율적으로 처리할 수 있을지 깊이 고민했습니다.
새로운 팀을 구성한 직후 제가 팀원들에게 가장 먼저 요청한 두 가지는 첫째, 정말 필요한 것들만 진행하자는 것이었고 둘째, 회의 시간을 길게 가지지 말자는 것이었습니다.
이번 프로젝트까지 포함하면 총 네 번의 프로젝트를 수행했는데, 그동안 부트캠프에서 제공한 가이드라인을 따르는 것이 일종의 관습처럼 굳어져 있었습니다. 그러나 그 안에는 이번 프로젝트에서 불필요하거나 다소 부담스러운 요소들이 있었습니다.
예를 들어, 그라운드 룰을 작성할 때도 필요하면 좋겠지만, 이전 프로젝트들에서 실제로 지켜지지 않았던 이상적인 내용들은 제외했습니다. 또한 유사 프로젝트 분석, 프로젝트 기획서 작성, UML 작성, 화면 설계서 작성 같은 문서들도 이번에는 과감히 생략했습니다.

결국 이번 프로젝트에서 남긴 핵심 문서는 다음과 같습니다.
이 문서들은 기획부터 구현까지 팀원들 간 최소한의 생각과 기능을 공유하고 구현 계획을 세우는 데 필수적이라고 판단했습니다. 각 기능이 무엇이며 어떻게 동작하고 어떤 경로와 과정을 거치는지 공유할 수 있는 최소한의 뼈대였습니다. 오히려 실효성 없는 정보들은 노션 페이지에서 과감히 배제하면서 정보 접근성이 높아져 매우 효율적이었다고 생각합니다.
특히 이번 프로젝트에서 제가 가장 의미 있게 느낀 점은 컨벤션을 최대한 구체적이고 다양한 분야에 걸쳐 정의한 것이었습니다.
이전 DB, 백엔드, 프론트엔드 프로젝트들을 진행하면서 각자의 구현 방식이 다를 수밖에 없다는 것을 절실히 느꼈습니다. 하지만 제가 알고 있는 지식과 노하우를 컨벤션으로 정리해 공유하자, 팀원들이 그것이 효율적이고 효과적이라 판단될 경우 그에 맞춰 구현해 주었습니다. 그 결과 내가 직접 구현하지 않아도 의도한 방식으로 작업이 진행되었고, 자연스럽게 분업이 원활히 돌아가며 맡은 역할에 더 집중할 수 있는 좋은 협업 환경이 만들어졌다고 생각합니다.
이번 프로젝트 기획 단계에서 저는 노션 페이지 설계를 맡았습니다. 이전 백엔드 프로젝트에서 설계했던 템플릿이 충분히 효율적이고 효과적이라고 판단하여 기존 템플릿을 유지하되 몇 가지를 추가했습니다.

우선 회의록 템플릿을 적용하여, 안건함과 진행할 일 공유, 공통 과제 등을 매 회의록에 포함하도록 구성했습니다. 또한 이번에는 최대한 역할을 나눠서 일을 처리하기로 했기에 위에서 제공한 이미지와 같이 회의에서 나온 액션 아이템을 담당자별로 구분하고 진행 상황을 한눈에 볼 수 있는 액션 아이템 리스트를 중앙에 배치했습니다.
비록 이미 좋은 템플릿이었지만, 회의록-안건함-각각의 DB(예: 프로젝트 기획서, 서비스 기능 명세서, 컨벤션 등) 간 연결성이 부족하다는 점은 아쉬웠습니다. 현재 노션을 일종의 레포지토리처럼 사용하고 있는데 앞으로는 프로젝트 관리에 더 초점을 맞춘 노션 페이지 구성이 필요하다고 느꼈습니다.
아직 사실 여부를 직접 확인한 것은 아니지만, 제가 가장 먼저 페이지를 구성한 뒤 다른 팀들도 동일한 템플릿을 적용하는 모습을 보았을 때, 제가 만든 페이지를 다른 팀원들도 긍정적으로 평가해주었다고 생각해 뿌듯함을 느꼈습니다 ㅎㅎ.
이번 협업에서 새롭게 시도한 점은 지라(Jira) 툴을 사용한 것이었습니다.

지라를 사용하면서 좋았던 점은 도메인별로 처리해야 할 이슈들을 관리할 수 있었고, 다른 팀원들이 어디까지 구현했는지 확인할 수 있었다는 것입니다.

지라에는 일정 및 업무 처리 분석 기능도 있는 것으로 알고 있지만, 이번 프로젝트에서는 해당 기능들을 충분히 활용하지 못했습니다. 주로 개인 일정 관리, 전체 업무 달성 확인, 브랜치 생성 측면에서 특히 효과적이었습니다. 다음 프로젝트에서는 새로운 협업 툴을 시도해 보거나, 지라의 다양한 기능들을 더 깊이 탐색해보는 것도 좋을 것 같습니다.
이번 프로젝트에서 제가 시도해보고자 했던 목표는 다음과 같습니다.
짧은 시간 안에 위의 모든 목표를 달성하려고 했지만, 역시 모든 것을 구현하기에는 시간이 부족했습니다. 그중 릴스 업로드 기능은 완벽히 구현했고, 로그 및 메트릭 수집과 알림은 Docker Compose 환경에서 ELK 스택과 Prometheus + Grafana 연동에 성공했으나, 쿠버네티스 환경에서는 한계에 부딪혔습니다. ELK 스택과 Prometheus, Grafana는 쿠버네티스 환경에서 매우 무거웠고, 로그 출력 및 actuator 추가 후 이미지를 다시 빌드하고 컨테이너로 실행하는 과정이 노트북 환경에서 상당히 부담스러웠습니다. 이로 인해 쿠버네티스 환경에서는 결국 구현에 실패했습니다.
GitHub Webhook은 Jenkins 파이프라인을 구성하여 CI/CD 자동화를 구현하는 데 활용했고, Discord Webhook은 파이프라인 마지막 단계에서 빌드 성공 여부를 Discord 알림으로 전달하는 데 적용했습니다.
이미지 빌드 과정에서는 테스트 코드까지 포함해 빌드하는 데 성공했고, 롤링 업데이트 방식의 무중단 배포도 구현했습니다.
다만 아쉬웠던 점은, 팀원 중 한 분이 Redis를 적용해 RefreshToken을 저장 및 관리하는 기능은 성공적으로 구현했지만, 좋아요 기능을 Redis로 전환하려던 계획은 끝내 실행하지 못한 채 프로젝트가 종료되었다는 점입니다. 처음에는 DB에 저장된 좋아요 데이터를 Redis로 이관하려 했으나, 배포까지 완료하고 남은 시간이 부족해 해당 부분은 다음으로 미루게 되었습니다.
또한 패키징 및 번들링 측면에서도 멀티 스테이지로 빌드 스테이지와 실행 스테이지를 분리해 이미지를 빌드하는 데는 성공했지만, Docker 이미지 사이즈 최소화나 캐시 최적화는 구현하지 못했습니다. 프론트엔드에서 Vite와 Webpack 빌드 결과를 분석하는 작업 역시 시도하지 못했습니다.
따라서 파이널 프로젝트에서는 이러한 미완성된 목표들을 차근차근 하나씩 테스트하고, 최대한 많은 실험과 성능 개선, 테스트 최적화에 초점을 맞춰 개발 역량을 한층 더 향상시켜볼 계획입니다.
우선 위에서 컨벤션을 언급했었는데 제가 담당했던 컨벤션은 백엔드, UI, 서비스 레이어 컨벤션을 맡아서 구조를 작성했었습니다.
백엔드 컨벤션
우선 프로젝트 구조에 대한 컨벤션을 작성했는데요. 위의 게시한 링크를 확인해보시면 확인할 수 있는 내용입니다.
src/main/java/com/project
├── command
│ ├── application
│ │ ├── controller
│ │ ├── dto
│ │ │ ├── request
│ │ │ ├── internal
│ │ │ └── response
│ │ └── service
│ └── domain
│ ├── aggregate
│ ├── repository
│ ├── service
│ └── constants
├── query
│ ├── application
│ │ ├── controller
│ │ ├── dto
│ │ │ ├── request
│ │ │ ├── internal
│ │ │ └── response
│ │ └── service
│ └── domain
│ ├── aggregate (command와 공유)
│ ├── repository (command와 공유)
│ └── constants
├── common
│ ├── dto
│ │ ├── ApiResponse.java
│ │ └── PageResponse.java
│ ├── aggregate
│ ├── util
├── config
│ │ ├── AppConfig.java
│ │ ├── JavaConfig.java
│ │ ├── AppConfig.java
│ │ ├── ModelMapperConfig.java
│ │ ├── SecurityConfig.java
│ │ └── PageResponse.java
├── exception
│ ├── GlobalExceptionHandler
│ ├── Errorcode
│ ├── ErrorResponse
│ └── BusinessException.java
├── security
│ ├── filter
│ │ └── JwtAuthenticationFilter.java
│ ├── handler
│ │ ├── RestAccessDeninedHandler.java
│ │ └── RestAuthenticationEntryPoint.java
│ └── jwt
│ │ └── JwtTokenProvider.java
└── Application.java
이전 백엔드 프로젝트에서의 프로젝트 구조와 동일하게 도메인 기준의 CQRS 구조 패턴으로 설계하였고 처음에는 Mybatis 없이 JPA로만 프로젝트를 구현하려고 했는데 Mybatis가 없다보니 복잡한 SQL 구문을 작성하는 것이 매우 번거로웠고 쿼리용과 커맨드용 엔티티가 많아지면서 오히려 좋지 않은 프로젝트 구조를 형성하게 되면서 이전 프로젝트와 동일하게 조회의 경우엔 Mybatis를 사용하고 명령의 경우엔 JPA를 사용하여 처리했습니다.
하지만 저의 경우엔 Mybatis를 사용하는 것이 다소 꺼리는 이유가 Mybatis를 사용하면 ORM이 아닌 SQL 구문 그 자체를 작성해서 실행하기 때문에 휴먼 에러가 발생할 여지가 높고 컴파일 시점에 에러를 확인할 수 없으므로 개발 용이성이 다소 떨어진다는 문제가 있어 해당 문제를 해결해줄 수 있는 jooq를 적용해보려고 했지만 역시 시간이 많은 것이 아니였기 때문에 새로운 스택을 추가하는 것이 다소 어렵다는 결론에 도달하였고 이번 프로젝트에선 Mybatis와 PageHelper 라이브러리를 사용하여 Mybatis로 조회 및 페이지네이션을 구현했지만 다음에는 jooq를 적용하여 조회를 처리해보는 것을 시도해보고자 합니다.

위의 링크로 이동하여 확인해보시면 아시겠지만 각각의 레이어에서의 역할이나 해야할 일들을 정리하였고 구현체와 인터페이스의 구분에 대해서도 정리하긴 했는데 그냥 이렇게 막연하게 정리하면 보시는 분들도 있으시고 안보시는 분들도 계신 것 같아서 다음에는 특히 필수적으로 확인을 했으면 하는 것들은 따로 요약용 버전도 만들어서 제공해야 할 것 같단 생각을 하게 되었습니다.

ErrorCode도 이전 프로젝트에서 하나의 파일로 관리하다보니 매번 병합시 사라지는 일이 발생하여 도메인별로 에러코드 Enum을 관리하자는 안건을 냈지만 이번 팀원분들은 깃 관리에 자신있다고 하셔서 하나의 파일로 관리하는 것으로 합의를 봤고 다행히도 모두가 깃 관리를 잘해주셔서 하나의 파일로 관리해도 문제가 없었습니다.
그리고 HTTP 상태 코드 뿐만 아니라 에러 코드도 Enum에 추가하자는 의견이 있어 추가하게 되었는데 하나의 페이지에서 다양한 API를 호출하는 것이 아니다 보니 에러 코드가 그렇게 의미가 있었던 것은 아니여서 최대한 활용할 수 있는 방법들에 대해 더 고민할 필요가 있을 것 같습니다.

GlobalExceptionHandler에 대한 컨벤션도 미리 정리하고 함께 공유하다보니 이번 프로젝트에선 모두 예외 처리를 동일하게 한 것이 매우 의미가 있었던 것 같습니다.

그리고 통합 ApiResponse도 컨벤션을 정리하여 공유하다보니 내가 직접 구현한 API가 아니여도 예상 가능한 Response를 받을 수 있어 다른 사람들이 만든 API를 사용하기 매우 쉬었고 코드 리뷰하는 것 역시 어려움이 없었던 것 같습니다.

그리고 페이지네이션의 경우에 수업에서 배운 방식이 아닌 PageResponse로 감싸서 PageHelper 라이브러리를 사용해서 페이지네이션을 처리해주는 것 역시 동일하게 구현해주셔서 통일성 있는 프로젝트를 진행한 것 같아 컨벤션을 정리해야 하는 이유에 대해 다시 한번 중요성을 인식할 수 있었습니다.

다만 제가 위와 같이 SpringSecurity에 대한 설계를 하고 실제로 제가 프로젝트 구현은 다음과 같이 CustomUserDetails를 Authentication으로 관리하는 것이 아니라 userId를 관리하는 것으로 구현하여 컨트롤러 단에서 @AuthenPrincipal로 접근하는 값들이 무엇인지에 대한 혼란이 있었습니다.

이를 통해 컨벤션을 설계해놓고 잘못 구현해놓으면 저 혼자만 실수하는 것이 아닌 모든 팀원들의 실책으로 이어져 큰 문제가 발생 할 수 있음을 이번 프로젝트에서 경험하게 되었습니다. 물론 저도 userId만 Authentication으로 관리하는 이유가 이번에는 role이 필요없다 보니 String 타입의 userId만 관리하면 될 것 같아서 변경을 한 것인데 변경을 했으면 컨벤션도 수정을 했어야 했는데 그러지 못해서 문제가 있었고 이번에는 작은 문제이지만 회사에선 큰 문제를 발생할 수도 있단 생각을 하게 되어 컨벤션 설계에서 끝나지 않고 업데이트 역시 중요하단 것을 이번 경험을 통해 알게 되었습니다.
UI 컨벤션
이번 디자인 시스템은 실제로 모두가 적용하고 이해할 수 있도록 UI 컨벤션을 정리했고 그 결과는 위의 링크를 따라가시면 확인하실 수 있습니다.

그 결과 위와 같이 모든 페이지가 통일성과 일관성을 유지한 프로젝트를 설계할 수 있게 되었고 디자인 시스템에 대한 컨벤션을 한번 정리하다보니 유지보수하는 것이 매우 쉬웠습니다.
다소 아쉬운 점은 실제 프로젝트 구현에선 미리 컴포넌트 단위로 구현하지 않고 각자 맡은 도메인에 따른 페이지를 구현하다보니 재사용성이 높은 컴포넌트가 있는 반면 재사용성이 떨어지는 컴포넌트도 있고 하나의 페이지에 많은 요소들이 포함되서 디버깅이 어려울 뿐만 아니라 유지보수하는 것이 매우 어려웠습니다.
그래서 다음에는 처음에 무조건 컴포넌트부터 구현한 뒤에 페이지를 구현하는 단계로 넘어가야겠단 생각을 하게 되었고 프론트엔드에서 주로 사용하는 패턴중 하나인 Atomic 패턴에 따라 구현해보고자 합니다.
백엔드 프로젝트 세팅

이번 프로젝트에서 가장 중요시 한 것이 효율적인 개발 환경이었기 때문에 서로 코드 개발에 대한 내용은 모두 PR로 공유하는 것으로 하기로 했고 남들의 코드 리뷰 시간을 최소한 줄이기 위해 PR을 최대한 구체적으로 잘 작성하기로 했고 위의 예시처럼 최대한 구체적으로 코드나 예시 위주로 작성하는데 중점을 두어 작성했습니다.

그리고 저 뿐만이 아니라 초기 세팅에서 가장 도움이 되었던 것은 바로 테스트 로그인 코드입니다. 제가 이전 프로젝트에서 인증과 사용자 관련된 모든 것들을 담당해서 구현했었기 때문에 SpringSecurity, JWT 인증 필터, 로그인과 관련된 코드들을 빠르게 작성할 수 있는 상황이었고 저는 이번에 백엔드와 프론트를 동시에 구현하는 것을 목표로 했고 프론트엔드에서 api 호출 시에 accessToken을 request에 담아서 받아서 서버로 전송하는 것들을 초기 세팅에서 구현해놨기 때문에 Vue 프로젝트 초기화 시점에 테스트 로그인 api를 호출해서 accessToken은 응답의 body로 받고 refreshToken은 HttpOnly 쿠키에 저장하도록 구현하는 코드가 필요했습니다.

그래서 저는 빠르게 해당 기능만 구현하는 코드를 구현했고 추가적으로 로그인 시에 필요한 유효성 검사나 refreshToken을 Redis에서 관리하는 코드는 모두 빼고 accessToken과 refreshToken을 생성하고 refreshToken은 body에 넣고 refreshToken은 헤더에 넣어서 HttpOnly 쿠키에 저장하도록 하는 것만 구현해서 초기부터 로그인 했을 땡와 동일한 환경을 구축했고 userId가 필요한 api를 구현할 때 테스트 로그인을 사용해서 다른 팀원분들도 빠르게 api를 구현할 수 있으셔서 참 뿌듯했던 것 같습니다.
프론트엔드 프로젝트 세팅

프론트엔드 프로젝트 세팅에 대한 PR역시 자세하게 작성하기 위해 노력했고 이전 프로젝트에서 TaiwlindCSS를 사용해본 팀원분들이 많으셨고 클래스 기반의 스타일 적용 방식의 편의성을 모두 좋게 생각하셔서 tailwindCSS를 적용하기로 했고 다른 팀원분들의 경우에 tailwind.config.js 파일로 스타일 시스템을 확장 할 수 있다는 것을 모르셨다고 하셔서 피그마에서 적용한 스타일 시스템과 동일하게 tailwindCSS에서 구현에서 사용할 수 있는 시스템을 확장했습니다.
그리고 위에서 언급한 바와 같이 초기화 시점에 loginUserTest api를 호출하여 token값을 가져와서 Pinia에서 관리하고 axios 인터셉터로 api 호출 시점에 header에 Authorizaiton에 token을 포함해서 전달하도록 모두 세팅을 해두었습니다. 이를 통해 대략적인 프론트-백엔드 프로세스를 구축하였고 이를 기반으로 매우 빠르게 프론트엔드와 백엔드를 동시에 구현할 수 있게 되어 백엔드와 프론트엔드 프로젝트에 대한 초기 프로젝트는 템플릿으로 남겨놔야 할 것 같습니다.
동영상 업로드 기능 및 썸네일 자동 추출 기능 구현

동영상 업로드에서 중요한 것은 동영상의 경우엔 최소 10MB부터 크게는 아주 큰 크기의 데이터가 서버로 전달되어야 하기 때문에 대용량 트래픽 처리 문제가 발생하게 되고 어차피 서버에 전달되었다가 서버에서 S3로 다시 동영상을 전달해야 하는 것이라면 프론트엔드에서 바로 S3로 동영상을 업로드하는 것이 좋겠다고 판단했습니다.
그래서 서버에서 Presigned URL을 발급한 뒤 발급한 URL로 프론트엔드에서 S3로 PUT 요청으로 동영상을 업로드하는 방법을 사용했고 해당 과정이 가능하도록 S3 버킷 정책을 설정하고 DB에는 파일키와 caption만 저장하고 실제 조회시에는 파일키와 버킷명과 지역의 조합으로 조회할 수 있도록 설정하였고 이 역시 버킷 정책에서 AllowPublicRead가 가능하도록 설정을 해주었습니다.
또한 업로드한 동영상에서 1프레임을 기준으로 썸네일을 자동으로 추출하는 기능도 구현했고 이는 HtmlCanvas 기능을 사용하여 구현했습니다.
그런데 썸네일 이미지의 경우엔 이미지다 보니 동영상보단 용량이 크지 않고 최대 용량을 10MB로 제한했기 때문에 서버로 전달해도 되겠다고 판단했고 서버에선 전달받은 파일 이미지를 S3에 저장하고 프론트에는 접근할 수 있는 CloudFront Domain을 제공하여 조회할 수 있도록 구현했습니다.
고양이 카드 맞추기 게임

해당 게임은 우리 팀이 시연 1등으로 만들어주는데 일등공신을 해주는 기능이라고 개인적으로 생각하는 아주 귀여운 기능입니다. 사용자가 직접 자기 고양이 이미지를 선택하여 카드 맞추게 게임을 할 수 있는데요. 백엔드는 어려울 것이 없었지만 프론트 구현에서 다소 어려움이 있었고 시간에 따라 타이머바가 자연스럽게 채워지는 애니메이션을 적용하기 위해 requestAnimationFrame을 적용했습니다.
또한 카카오 공유 API를 연결하여 게인 랭킹을 공유하는 기능도 구현했습니다.
CI/CD 자동화 시연

아키텍처 설계

이번 프로젝트의 CI/CD 자동화 구현은 최신 DevOps 트렌드와 실전 환경을 고려하여 효율성, 자동화, 확장성을 중심으로 설계하였으며, Jenkins 기반의 CI/CD 파이프라인과 ArgoCD 기반의 GitOps 배포 자동화를 연계하여 완전 자동화된 배포 체계를 구축했습니다.
Jenkins 파이프라인 구축
이번 파이프라인은 Groovy 기반 Jenkins Declarative Pipeline으로 작성되었으며, 주요 설계 포인트는 다음과 같습니다.
설계에서 중점을 둔 부분에 대해 얘기해보자면 Jenkins가 단순히 Docker 이미지 빌드 및 Push만 하는것이 아니라 manifests 리포지토리까지 자동으로 업데이트하고 ArgoCD가 이를 감지해 배포까지 이어지도록 설계하여 GitOps 완전 자동화를 달성하였습니다.
또한 parallel stage로 백엔드, 프론트엔드를 동시에 처리해 빌드 속도를 최적화하고자 한 부분도 성능 개선에 있어 힘쓴 부분입니다.
Docker 빌드 및 Docker hub에 push
위에서 언급했듯이 Jenkins가 Webhook을 통해 이벤트를 감지하는데 이때 Git 변경 감지를 통해 변경된 영역별 조건부 빌드로 변경된 프로젝트만 빌드하도록 설정하였습니다.
Backend용 멀티 스테이지 Dockerfile을 활용해 Gradle 빌드용 레이어와 실행 환경 레이어를 분리하였고 최종 이미지는 Gradle 기반 JAR 파일만 포함하도록 경량화하여 이미지 최적화를 실현하고자 노력했습니다. 또한 빌드된 이미지는 latest, 빌드 번호, 커밋 해시 3개의 태그로 Docker Hub에 Push하여 버전 관리를 할 수 있도록 구현했습니다.
또한 Frontend용 Dockerfile은 Node 빌드 스테이지와 Ngnix 서빙 스테이지를 분리하여 관리했고 Vite 기반 정적 파일 처리 및 Ngnix 설정 적용을 명확히 하여 구현했고 최종적으론 용량 최적화를 시도한 이미지를 Docker Hub에 push하여 외부 K8s에서 바로 Pull 가능하도록 구현했습니다.
Ingress 및 Nginx 구축
또한 이번 프로젝트에서 Ingress 및 Nginx 구축은 Kubernetes 환경에서 서비스 라우팅을 효율적으로 구성하고 Vue 프론트엔드의 정적 파일을 안정적으로 서빙하는 것을 핵심 목표로 삼았습니다
이 설계는 서비스 접근성을 높이고 라우팅 및 자원 관리를 체계화하며 실전 운영 환경에서 발생할 수 있는 문제를 사전에 탐색하기 위한 실험적이자 실용적인 시도라고 생각합니다.
특히 Kubernetes 환경에서는 LoadBalancer 방식보다 Ingress Controller를 도입해 경로 기반 및 호스트 기반 라우팅을 설계하는 것이 관리와 확장성 측면에서 훨씬 유리하고 이번 프로젝트처럼 프론트엔드와 백엔드가 독립적으로 배포되고, 클러스터 내부에서 여러 서비스가 협력하는 구조에서는 Ingress 기반 경로 분리 설계가 필수적이라는 판단하에 다음과 같은 주요 내용들을 구현했습니다.
동영상 처리를 위해 Presigned URL을 사용해 브라우저에서 S3로 직접 파일을 업로드할 때, CORS(Cross-Origin Resource Sharing) 문제가 발생했고 이를 해결하기 위해 Nginx 설정에서 Access-Control-Allow-* 헤더를 강제로 추가하여 클라이언트가 브라우저 환경에서 S3로 직접 접근할 수 있도록 조치하였습니다.
다만 현재 Ingress에는 TLS가 적용되지 않아 HTTPS로 운영하지 못하는 한계가 있고 추후 cert-manager 등을 연동해 TLS 인증서를 자동 관리하고, 보안성을 강화할 계획입니다.
또한 Helm Chart나 Kustomize를 도입해 환경별로 설정을 동적으로 관리하는 개선 방안도 반영하여 다음 파이널 프로젝트에서 시도해볼 예정입니다.
무중단 배포 전략
이번 프로젝트에서 무중단 배포 전략은 Kubernetes의 Deployment 리소스를 중심으로 구현했습니다. 서비스 가용성을 유지하면서도 안정적으로 신규 버전을 배포하는 것을 목표로 하였고 이를 통해 운영 중에도 사용자가 서비스 중단을 느끼지 않게 하는 경험을 구현하고자 노력했습니다. 주요 구현 내용은 다음과 같습니다.
Deployment에서 strategy type을 RollingUpdate를 설정하여 기존 애플리케이션의 버전을 점진적으로 업데이트 하는 방식을 사용하여 무중단 배포를 구현하였습니다. 배포 시 추가 생성 가능한 최대 Pod 수와 배포 중 사용할 수 없는 최대 Pod 수를 1개로 설정하여 배포 중 최소한의 서비스 장애만 허용하고 항상 일정 수의 정상 Pod가 서비스 요청으 처리하도록 보장하였습니다.
이렇게 설정한 이유는 현실적인 테스트 환경제약 때문입니다.
로컬 환경에선리소스 한계로 동시에 3개 이상의 Pod를 안정적으로 띄울 수 없었기 때문에 결국 테스트용 배포에선 최대 2개의 Pod까지만 운영할 수 있도록 구현했고 이로 인해 실제 운영 환경의 완전한 무중단 배포 시뮬레이션은 어려웠지만 구조 설계 및 파이프라인 연계 실험에선 충분한 학습 효과를 거둘 수 있었고 추후 AWS 배포 과정에선 더 많은 테스트를 해보길 기대하고 있습니다.
PV 및 PVC 설정
저희 프로젝트에서는 데이터베이스로 MariaDB, 인메모리 캐시로 Redis를 사용했습니다. 이 두 컴포넌트 모두 Kubernetes 클러스터에서 운영될 때 단순히 컨테이너 레벨에서만 관리해서는 안 되며 특히 MariaDB는 데이터 영속성, Redis는 상태 데이터 유지 측면에서도 별도의 스토리지 설계가 필수적이라고 판단했습니다.
컨테이너는 언제든 재시작되거나 교체될 수 있기 때문에 컨테이너 내부의 디스크에 의존하면 데이터 손실 위험이 있고 때문에 K8S의 PV와 PVC 리소스를 활용했습니다. 해당 내용은 다음과 같습니다.
MariaDB의 경우 클러스터 내에서 독립된 스토리지 자원(PV)에 연결되어 컨테이너가 교체되더라도 임의로 지정한 /var/lib/mysql 경로에 저장된 데이터가 안전하게 보존되도록 설계했습니다.
PVC를 통해 Deployment가 필요한 스토리지 용량, 액세스 모드 등을 선언했고 PVC는 사전에 정의된 PV와 바인딩되어 컨테이너와 물리적 스토리지를 연결하는 역할을 수행하였습니다.
ConfigMap과 Secret을 통한 환경 변수 설정
저희는 프로젝트 구현때부터 외부 환경에 따라 달라질 수 있는 설정 값이나 민감 정보는 깃허브로 관리되면 보안상 취약하기 때문에 환경변수로 관리했었고 쿠버네티스 환경에서도 이미지로 하드코딩하면 관리 측면에서 보안상 큰 위협 요소가 될 수 있기 때문에 Kubernetes의 ConfigMap과 Secret을 활용하여 민감 정보와 비민감 정보를 구분하여 관리하였습니다.
우선 ConfigMap에선 ConfigMap(catchy-config)에는 비민감성 환경 설정 값들을 집중적으로 관리했습니다. 주요 항목은 다음과 같습니다.
이 값들은 서비스 실행 시 컨테이너 환경 변수로 주입되어 환경별로 손쉽게 교체 가능하며 코드나 이미지 변경 없이도 동적으로 반영될 수 있도록 설정을 했습니다.
Secret에는 외부에 노출되면 치명적인 민감 정보만을 관리했습니다. 주요 항목은 다음과 같습니다.
Secret은 Kubernetes 내부에서 base64로 인코딩되어 저장되며,
RBAC(권한 관리) 및 Secret 마운팅 정책을 통해 외부 노출 위험을 최소화 했습니다.
이를 통해 운영 및 유지보수 효율성을 높이면서 민감 정보 노출 경로를 최소화 할 수 있었습니다.
ArgoCD를 통한 K8S 클러스터 배포 자동화 구축
위에서 언급한 바와 같이 ArgoCD를 활용한 GitOps 기반 배포 자동화 체계를 구축했고 이는 단순히 CI/CD 파이프라인으로 끝내는 것이 아닌 배포의 최종 단계인 K8S 클러스터 적용까지 모든 배포 프로세스를 Git을 통해 관리하도록 설계 및 구현하였습니다.
해당 과정은 Jenkins가 새로운 버전의 Docker 이미지를 빌드하고 Docker Hub로 푸시하는 동시에 K8S manifests GitHub 리포지토리도 함께 업데이트하는 작업을 진행했고 ArgoCD는 주기적으로 Git을 Pull해서 변경사항을 감지하고 만약 불일치가 감지되면 자동 동기화 설정을 통해 K8S 클러스터를 업데이트하도록 구현했습니다.
물론 ArgoCD의 자동 동기화는 휴먼 에러를 줄일 수 있고 편의성이 높은 기능이긴 하지만 잘못된 manifest가 커밋되면 실시간으로 클러스터에 문제가 전파될 수 있는 위험성도 내포하고 있어 ArgoCD를 통한 배포 자동화를 일부러 지원하지 않는 기업들도 많다고 들었습니다. 따라서 젠킨스 파이프라인에서 manifests 수정 전 체계적인 사전 Ling, 테스트, 검증 단계를 반드시 거쳐야 하며 안전하게 서비스가 업데이트가 될 수 있는 방안들을 고민해봐야 겠단 생각을 했습니다.
이번 프로젝트에선 저는 CI/CD 영역을 다루면서 Docker, Kubernetes, Ingress, Nginx, ArgoCD, Jenkins, GitHub Webhook, Discord Webhook, ELK 스택, Prometheus, Grafana, Spring Actuator 등 실로 다양한 DevOps 도구들을 한꺼번에 접하고 연동하는 경험을 했습니다.
처음에는 단순히 도구들을 연결해보자는 마음으로 시작했지만 이 과정이 결코 간단하진 않았습니다.
예로들어 ArgoCD를 GitHub와 연동할 때, 처음에는 최신 인증 방식인 Fine-grained Personal Access Token(PTA)을 사용했지만 의외로 이 방식은 ArgoCD와의 호환성 문제를 일으켜 결국 Classic 버전의 Token으로 변경하자 문제가 해결되었는데 이는 모든 블로그들을 모두 찾아본 뒤에 알게된 방법이고 문제 해결 과정이 다소 허무하지만 최신 기능이 항상 호환성을 보장하지는 않는다는 중요한 교훈을 얻게 되어 다음에는 동일 문제가 발생하면 호환성 문제는 없는지 부터 확인해볼 것 같습니다.
그래도 CI/CD 자동화 및 배포 구현 경험을 통해 릴리즈 자동화의 전체 그림을 이해할 수 있었고 단순히 Jenkins에서 빌드 후 Docker 이미지 푸쉬에서 끝나는 것이 아니라 ArgoCD가 manifests 리포지토리를 감지해 배포까지 연동되는 End-to-End GitOps 흐름을 직접 설계해보니 CI/CD의 전체 흐름과 각 단계의 역할을 명확히 이해할 수 있게 된 것 같습니다.
그리고 K8S에 연결하진 못했지만 Docker Compose에서 Prometheus, Grafana, Actuator로 성능 모니터링을 붙이고, ELK 스택을 연계해 로그 수집 및 분석까지 시도하면서 각 도구의 역할과 실행 과정에 대한 대략적인 과정을 이해할 수 있게 되었습니다.
이제는 ArgoCD나 Jenkins 외에도 Tekton, Spinnaker, GitLab CI, CircleCI, Flux 등 다른 CI/CD 및 GitOps 툴들을 더 많이 직접 경험해보고 각각의 철학과 강점, 단점을 비교하여 저만의 DevOps 설계 역량을 넓히고 싶다는 열망이 강하게 생겼고 이를 통해 보안, 확장성, 멀티 클러스터 관리, Secret 관리, Canary/Blue-Green 배포 같은 운영 환경 수준의 고급 설계로 확장하여 웹 개발과 관련된 모든 프로세스를 이해하고 실제 구현할 수 있는 전문가가 될 수 있겠단 생각이 들어 매우 뿌듯하고 의미있던 프로젝트였던 것 같습니다.