4/2일 발표를 끝낸후 일주일만에 회고를 해본다.
1차 미니 프로젝트에 10개의 팀이 각자 주제를 선정한 후, 프로젝트를 진행했었다.
2차 미니 프로젝트에서는 팀을 섞고, 주제는 1차 프로젝트에서 선택하여 MSA 및 클라우드화를 진행하였다.
나는 4조에 속했으며 1차 프로젝트에서 5조가 진행한 암호화폐 정보 아카이브인 'Cryptory'를 선택하여 리팩토링을 진행하였다.
| AS-IS | TO-BE |
|---|---|
![]() | ![]() |
원인
기존 프로젝트에서는 Nginx가 웹서버의 역할과 gateway의 역할을 겸하였다. 정적 파일을 직접 서빙 하는 것과, 리버스 프록시 역할을 동시에 수행하는 구조는 관리 복잡성과 비용측면에서 바람직하지 않다고 판단하였다.
수정: Spring 생태계와의 통합 강화를 위해 Gateway 기능을 Spring Cloud Gateway로 대체하였고, 비용 절감 및 운영 복잡도 감소를 위해 정적 리소스는 S3의 웹 호스팅 기능을 활용하도록 수정하였다.
원인: 프로젝트의 설정 파일에 민감한 정보가 포함되어 있어, 이를 .gitignore에 추가하였고, 이로 인해 협업이나 배포 시, 환경별 설정을 수동으로 전달하거나 별도로 관리해야 하는 불편함과 일관성 부족이 발생했습니다.
수정: 설정 정보의 중앙에서 일관되게 관리하기 위해 Config Server를 도입하였고, 민감정보의 경우 placeholder로 대체하여 운영 환경에서 환경 변수 형태로 실행시점에 주입하도록 처리하였음.
Config Monitor 적용으로 Git webhook과의 통합 적용
원인: Outer Arch에 해당되는 외부 서비스들이 점차 추가되었고, Inner Arch에 해당되는 서비스들은 ECS를 통해 DHCP 방식으로 IP가 매번 변경되는 문제가 발생.
수정: Eureka를 도입하여 각 서비스가 Eureka Server에 자신을 등록하고, 서로의 위치를 동적으로 조회할 수 있도록 구성하였음.
원인: Nginx를 사용하여 Docker compose 환경에서 컨테이너 이름을 dns로 사용하는 방식의 라우팅이었지만, 각각의 서비스가 ECS 인스턴스로 분산됨에 따라 고정된 DNS 기반 라우팅으로는 관리가 어려워짐.
수정: Spring Cloud Gateway를 도입하였고, Eureka와 연동하여 서비스 인스턴스를 자동 인식하여 라우팅 및 부하 분산 기능을 적용하였음.
${} 형태의 placeholder가 Config Server에서 치환되어 클라이언트로 전달되는 줄로 착각하였습니다.오해한 로직 : Git -> Config Server + (env) -> Config Client
{ConfigServer}/{profile}/{tag} 경로를 통해 설정 정보를 조회한 결과, placeholder는 단순 문자열 상태의 JSON으로 전달되고 있었습니다.${SOME_SECRET}같은 값은 Config Server에서는 그 자체가 문자열이어서 ${}를 치환 불가하고, Config Client에서 Spring Context가 초기화될 때 비로소 환경 변수나 외부 값을 사용해 치환 가능함을 알게되었습니다.실제 동작 로직 : Git -> Config Server -> Config Client + (env)
${}형태의 값이 운영 환경에 주입된 환경 변수나 시스템 프로퍼티를 통해 치환됩니다.${}형태의 placeholder를 사용하고, 운영 환경에서는 실제 값을 ECS Environment에 추가하여 사용하도록 구성하였습니다.{Eureka_URL}/eureka/apps에서 확인하였을 때 ECS Task에서 실제 사용하는 서비스 포트가 아닌 80 포트로 등록되는 문제가 발생.Spring Cloud Eureka는 기본적으로 InetAddress.getLocalHost()를 사용하여 Host IP를 조회하여 Eureka에 등록하지만, ECS환경(Fargate)에서는 해당 방식으로 조회 시 ECS Task 자체의 IP가 아닌, ECS 내부 Metadata Server의 IP가 반환되는 경우가 있음을 확인하였다.
ECS 환경에서는 컨테이너의 실행 포트가 동적으로 할당될 수 있지만, Spring Boot가 실행되는 포트가 명시되어 있지 않거나 eureka.instance.nonSecurePort 설정이 누락된 경우, 기본값인 80 포트로 등록되버리는 현상이 있었음.
eureka.instance.ip-address에 할당하도록 코드를 수정하였음.eureka.instance.nonSecurePort에 명시하는 것으로 해결하였음.문제 요약
ECS문제를 해결하였지만 여전히 Gateway에서 특정 경로에 대한 요청이 정상적으로 라우팅되지 않는 현상이 발생했습니다.
원인 분석
아래와 같이 설정된 Gateway의 로그에서, /api/v1/users/1 요청은 정상적으로 처리되었으나 /api/v1/coins는 라우팅 매칭이 되지 않는 현상을 발견했습니다.
predicates에서 - 로 배열형식을 사용할 수 있다는 것을 알아 무의식적으로 패턴을 나열했을경우 or방식으로 매칭되는 줄 알았으나, 실제로는 모든 Path 조건이 동시에 만족해야만 라우팅되는 방식이었습니다.
routes:
- id: coin-service
uri: lb://COIN-SERVICE
predicates:
- Path=/api/v1/coins/**
- Path=/api/v1/news/**
- Path=/api/v1/issues/**
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/api/v1/users/**
routes:
- id: coin-service
uri: lb://COIN-SERVICE
predicates:
- Path=/api/v1/coins/**
- id: coin-service
uri: lb://COIN-SERVICE
predicates:
- Path=/api/v1/news/**
- id: coin-service
uri: lb://COIN-SERVICE
predicates:
- Path=/api/v1/issues/**
Outer Arch를 설정하며 많은 오류를 해결하다 Eureka 등록, Config Server 등이 모두 REST API 기반으로 동작함을 알게 되었다. 따라서 해당 기능을 Spring만이 아닌 Django같은 다른 언어에서도 활용 가능함을 알게 되었지만, 시간상 적용하지 못한 점이 아쉬웠다.
Eureka, Gateway, Config각각에서 오류를 해결하느라 Grafana같은 모니터링 기능을 적용하지 못한 점이 아쉬웠다.
최종 프로젝트에서는 observability를 충족할수있는 기능을 도입하고자 한다.