CI/CD 파이프라인 구성 방식에는 크게 두 가지가 있다.
하나는 GitHub Actions를 활용한 클라우드 기반 자동화 방식이고,
또 하나는 로컬에서 직접 Docker를 빌드하여 AWS ECR에 푸시한 후 서버에 배포하는 반자동 방식이다.
이번 포스팅에서는 GitHub Actions를 활용한 자동화 배포부터 MSA 아키텍처 구성까지 전체 흐름을 살펴보고, 실제 프로덕션 환경에서 사용할 수 있는 설정 코드를 통해 배포 과정을 이해해보자.
프로젝트 규모와 목적에 따라 적절한 배포 구성이 다르다.
AWS 기반으로 현업에서 자주 사용되는 구성을 정리하면 다음과 같다.
| 요구사항 | 추천 구성 | 설명 |
|---|---|---|
| 빠르게 시작하고 싶은 경우 | GitHub Actions + EC2 + Docker + SSM | 비교적 단순하고 빠르게 구축 가능하며 운영 부담이 적다. |
| 무중단 운영이 필요한 서비스 | GitHub Actions + ECR + ECS Fargate | 컨테이너 기반 자동 확장, 롤링 업데이트 등 안정적인 무중단 배포 제공. |
| GitOps 기반 고급 배포를 원하는 경우 | GitHub + ArgoCD + EKS(Helm) | 선언적 배포, 자동 동기화, 고도화된 운영 자동화가 가능. |
| 자체 관리형 CI/CD 선호 | Jenkins + Docker + EC2 또는 Kubernetes | 노드·파이프라인을 직접 관리하며 고도로 커스터마이징 가능. |
현업에서는 코드 변경부터 자동 테스트, 이미지 빌드, 보안 체크, 배포, 알림, 롤백 대비까지의 전체 과정이 자동화되어 있다. 이러한 자동화를 구축하기 위해서는 각 단계의 역할과 도구를 명확히 이해해야 한다.
AWS Systems Manager는 EC2 인스턴스와 온프레미스 서버, 기타 AWS 리소스를 중앙에서 관리하고 자동화할 수 있게 도와주는 통합 관리 서비스다. SSM은 EC2와 기타 인프라에 명령을 실행하고, 자동화 작업을 수행하며, 보안을 관리할 수 있는 운영 관리 도구로, CI/CD 파이프라인에서 EC2 자동 배포 시 매우 자주 활용된다.
CodeDeploy는 애플리케이션 배포를 자동화하는 도구다. EC2 인스턴스, Lambda 함수, 온프레미스 서버 등에 애플리케이션을 자동으로 배포하는 데 사용된다. Java, Node.js, Python 등의 웹 애플리케이션을 배포할 수 있으며, 서버에 jar, zip 또는 Docker 컨테이너를 배포할 수 있다. 다만 Docker 자체를 직접 관리하지는 않는다. GitHub Actions나 Jenkins 같은 CI/CD 도구와 연동하면 자동 배포 파이프라인을 구성할 수 있다.
AWS ECR은 AWS에서 제공하는 Docker 이미지 저장소다. GitHub가 코드를 저장하는 곳이라면, ECR은 Docker 이미지를 저장하는 곳이다. Docker 이미지를 로컬에서 만들어 ECR에 push하고, EC2, ECS, EKS, Lambda 등에서 pull해서 실행하는 방식으로 안정적이고 표준화된 배포가 가능하다.
ECR은 완전관리형 서비스로 서버 관리가 필요 없으며, AWS가 알아서 제공한다. Private 저장소와 공개 저장소를 모두 지원하여 기업용 비공개 이미지를 안전하게 저장할 수 있다. IAM 권한 관리를 통해 push와 pull 권한을 세밀하게 제어할 수 있으며, EC2, ECS, EKS, Lambda 등 모든 AWS 서비스와 연동 가능하다. 취약점 스캔 기능을 제공하여 보안성이 높고, 고성능 네트워크 기반으로 이미지 Pull 속도가 빠르다는 장점이 있다.
ECR은 Registry, Repository, Image 세 가지 구성 요소로 이루어진다. Registry는 ECR 전체 저장소 공간으로 AWS 계정당 1개가 생성된다. Repository는 컨테이너 이미지들을 프로젝트 단위로 저장하는 공간이다. 예를 들어 my-spring-app, my-react-app, nginx-proxy, ecommerce-backend 같은 이름으로 Repository를 생성할 수 있다. Image는 태그를 붙여 관리되는 실제 Docker 이미지 파일로, 1.0, latest, blue, green 같은 태그를 사용한다.
ECR의 동작 구조는 개발 PC에서 Docker를 빌드하여 ECR에 Push하고, EC2나 ECS에서 ECR로부터 Pull하여 실행하는 흐름으로 이루어진다. 구체적인 과정을 살펴보면, 먼저 개발자가 Dockerfile을 작성한다. 그다음 docker build -t myapp:1.0 . 명령으로 이미지를 빌드하고, aws ecr get-login-password | docker login ... 명령으로 ECR에 로그인한다. 이후 docker tag myapp:1.0 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:1.0 명령으로 태그를 붙이고, docker push 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:1.0 명령으로 ECR 레포지토리에 이미지를 푸시한다. EC2에서는 docker pull 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:1.0 명령으로 이미지를 가져온 뒤, docker run -d -p 8080:8080 myapp:1.0 명령으로 컨테이너를 실행한다.
CodeDeploy와 ECR의 차이를 정리하면, CodeDeploy는 배포 자동화가 목적이며 EC2와 Lambda 등에 jar, zip, Docker 등을 배포 스크립트를 통해 실행한다. 반면 ECR은 Docker 이미지 저장소로, Docker 기반 서비스인 ECS, EC2, EKS에서 이미지를 업로드하고 다운로드하는 용도로 사용된다. 두 서비스를 함께 사용하면 ECR에서 이미지를 pull한 후 CodeDeploy로 실행하는 구성이 가능하다.
포트폴리오에 포함된 프로젝트는 배포까지 완료했을 가능성이 크기 때문에, 면접에서 배포 경험에 대한 질문은 단골 질문이다. 이때 팀원이 "배포는 팀원이 담당했습니다."라고 대답한다면 당연히 떨어지지 않을까? 실제로 배포를 팀원이 했더라도 "팀원과 함께 구성해보았습니다." 말할 수 있을 정도로 배포 과정을 이해하고 있어야 한다.

위 이미지만 보고서 배포 과정을 설명할 수 있도록 연습하자.
먼저 프로젝트 코드를 GitHub에 push하면 push 이벤트가 트리거가 되어 GitHub Actions 워크플로우가 실행된다. Actions는 설정된 스크립트에 따라 소스 코드를 빌드하고 테스트한 뒤 Docker 이미지를 생성한다.
생성된 Docker 이미지는 AWS의 프라이빗 컨테이너 이미지 저장소인 ECR에 push되어 저장된다. 배포 서버인 EC2 인스턴스는 ECR에서 이미지를 pull하여 Docker 컨테이너를 실행한다. Docker 이미지는 애플리케이션 실행에 필요한 환경인 JDK와 애플리케이션 빌드 결과물 등을 포함하고 있으며, EC2에는 Docker만 설치되어 있으면 동일한 실행 환경을 재현할 수 있다.
실행된 컨테이너는 AWS RDS의 MySQL에 연결하여 데이터베이스 작업을 수행하며, 사용자가 업로드한 파일이나 정적 리소스는 S3에 저장된다. 사용자는 Route 53의 DNS 서비스를 통해 도메인 이름으로 EC2 인스턴스에 접근할 수 있다.
Docker 환경을 포함한 프로젝트 아키텍처는 GitHub Actions에서 빌드하고 Docker 이미지를 ECR에 Push한 후, EC2 서버에서 docker-compose up 명령 또는 docker pull과 restart 명령을 실행하는 방식으로 구성된다. 이 방식은 단일 인스턴스에서 실행 중인 컨테이너를 중단하고 새 이미지로 재시작하기 때문에 아직까지는 무중단 배포가 아니다. 요청 중인 사용자가 중간에 연결이 끊길 수 있다.
프로젝트 디렉토리 구조는 다음과 같이 구성된다.
your-project/
│
├── .github/
│ └── workflows/
│ └── build-and-push.yml # GitHub Actions CI 스크립트
│
├── docker/
│ ├── app/ # Dockerfile용 디렉토리
│ │ └── Dockerfile # Spring Boot Docker 이미지용
│ ├── mysql/
│ │ └── my.cnf # (선택) MySQL 설정 파일
│ └── redis/
│ └── redis.conf # (선택) Redis 설정 파일
│
├── docker-compose.yml # EC2 배포용 전체 서비스 구성
├── .dockerignore # Docker 이미지 빌드시 제외할 항목
├── .env # (선택) Compose 환경 변수 설정
│
├── build.gradle # Spring Boot 프로젝트 빌드 파일
├── settings.gradle # Gradle 설정
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/example/project/
│ │ │ └── Application.java # Spring Boot 메인 클래스
│ │ └── resources/
│ │ ├── application.yml
│ │ └── static/, templates/
│ └── test/
│ └── java/
│
└── README.md
| 파일/디렉토리 | 설명 |
|---|---|
| .github/workflows/.yml | GitHub Actions CI 워크플로우 설정 |
| docker-compose.yml | EC2에서 Spring App + MySQL + Redis를 함께 구성하고 실행하기 위한 Docker Compose 설정 |
| docker/app/Dockerfile | ECR에 push할 Spring Boot 이미지 빌드용 |
| .env | docker-compose.yml에서 사용하는 환경 변수 정의 파일 (선택) |
| build.gradle | Gradle 스크립트 |
| application.yml | Spring Boot의 환경 설정 파일로, DB, Redis 연결 설정 포함 |
name: Build and Deploy to ECR
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@v2
- name: Set up JDK 17
uses: actions/setup-java@v1
with:
java-version: '17'
- name: Build with Gradle
run: ./gradlew build -x test
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-2
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build, tag, and push image
run: |
docker build -t rednotice-ecr:latest docker/app
docker tag rednotice-ecr:latest ${{ steps.login-ecr.outputs.registry }}/rednotice-ecr:latest
docker push ${{ steps.login-ecr.outputs.registry }}/rednotice-ecr:latest
FROM openjdk:17
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci
max_connections=200
sql_mode=""
default-time-zone='+09:00'
port 6379
bind 0.0.0.0
requirepass myStrongPassword
loglevel notice
dir /data
appendonly yes
version: '3.8'
services:
app:
image: rednotice-ecr:latest
build:
context: ./docker/app
ports:
- "8080:8080"
depends_on:
- mysql
- redis
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/mydb
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: rootpass
SPRING_REDIS_HOST: redis
SPRING_REDIS_PORT: 6379
mysql:
image: mysql:8.0
container_name: mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: mydb
volumes:
- mysql-data:/var/lib/mysql
- ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
ports:
- "3306:3306"
redis:
image: redis:7
container_name: redis
ports:
- "6379:6379"
volumes:
- ./docker/redis/redis.conf:/usr/local/etc/redis/redis.conf
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
volumes:
mysql-data:
**/target
**/build
**/.gradle
.dockerignore
.git
.gitignore
SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/mydb
SPRING_DATASOURCE_USERNAME=root
SPRING_DATASOURCE_PASSWORD=rootpass
SPRING_REDIS_HOST=redis
SPRING_REDIS_PORT=6379
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://mysql:3306/mydb
username: root
password: rootpass
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 10
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true
database-platform: org.hibernate.dialect.MySQL8Dialect
redis:
host: redis
port: 6379
password: myStrongPassword
timeout: 60000
logging:
level:
org.springframework.web: DEBUG
org.hibernate.SQL: DEBUG
GitHub Actions에서 SSH를 통해 직접 EC2에 배포하는 방식도 가능하다.
이 방식은 Gradle로 빌드한 jar 파일을 EC2로 복사하고 실행하는 단순한 구조다.
name: Deploy Spring Boot to EC2
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
- name: Build Spring Boot Application
run: ./gradlew clean build
- name: Copy jar to EC2
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_SSH_KEY }}
source: "build/libs/*.jar"
target: "~/app"
- name: Run jar on EC2
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_SSH_KEY }}
script: |
pkill -f 'java -jar' || true
nohup java -jar ~/app/*.jar > ~/app/app.log 2>&1 &
jobs는 GitHub Actions에서 작업을 정의하는 예약어이고,
deploy는 개발자가 자유롭게 정하는 Job 이름이다.
runs-on은 어떤 OS에서 Job을 실행할지 지정하는 예약어로, ubuntu-latest나 windows-latest 등을 사용할 수 있다.
steps는 Job에서 실행할 단계들을 정의하는 예약어다.
name은 각 단계의 표시 이름으로 개발자가 자유롭게 작성한다.
uses는 GitHub에서 제공하는 공식 액션을 사용하는 예약어다.
with는 액션에 설정 값을 전달하는 예약어이며, run은 셸 명령어를 실행하는 키워드다.
uses 키워드의 구조를 자세히 보면, actions/checkout@v3에서 actions는 GitHub 공식 액션 제작 조직을 의미하고, checkout은 현재 레포지토리를 체크아웃하는 액션이며, @v3는 해당 액션의 버전을 나타낸다.
GitHub Secrets는 민감한 정보를 안전하게 저장하는 기능이다. New repository secret을 클릭한 후 Name에 EC2_HOST를, Secret에 EC2 퍼블릭 IP 주소를 입력한다. EC2_USER에는 ubuntu 같은 EC2 로그인 사용자를, EC2_SSH_KEY에는 pem 파일의 전체 내용을 붙여넣는다.
워크플로우에서 ${{ secrets.VARIABLE_NAME }} 형식으로 이러한 Secrets를 참조할 수 있다.
script 섹션에서 | 기호는 YAML에서 여러 줄의 셸 명령어를 실행하겠다는 의미다.
pkill -f는 기존에 실행 중인 프로세스를 강제로 종료하고, nohup ... & 는 백그라운드로 애플리케이션을 실행한다.


마이크로서비스 아키텍처는 하나의 큰 애플리케이션을 여러 개의 작은 서비스로 나누어 개발하고 배포하는 방식이다. 각 서비스는 독립적으로 배포되고 확장될 수 있으며, 서비스 간 통신은 HTTP REST API나 메시지 큐를 통해 이루어진다. Nginx는 API Gateway 역할을 수행하여 클라이언트 요청을 적절한 백엔드 서비스로 라우팅한다.
위 이미지는 React 프론트엔드와 Spring Boot 백엔드로 구성된 전형적인 웹 애플리케이션 아키텍처를 보여준다. 웹 브라우저에서 온 요청은 Interceptor를 거쳐 Spring Boot의 Controller, Service, Repository 계층을 통과하여 MySQL 데이터베이스와 통신한다. 중간에 JWT 필터와 세션 인터셉터, AOP를 활용한 로그와 트랜잭션 처리가 이루어지며, Redis는 세션 관리에 사용된다.
Nginx의 리버스 프록시 기능은 클라이언트와 최종 목적지 서버 사이에 위치하여 클라이언트의 요청을 대신 받아 최종 서버로 전달하고, 그 응답을 다시 클라이언트에게 전달해주는 역할을 한다. 요청을 받아 처리하고 응답을 돌려주는 과정은 네 단계로 이루어진다.
첫 번째 단계는 요청 수신이다. 클라이언트는 www.myservice.com 같은 Nginx의 주소로 요청을 보내며, 백엔드 서버의 실제 IP 주소가 무엇인지 알지 못한다. 두 번째 단계는 요청 전달이다. Nginx는 nginx.conf 설정 파일에 정의된 규칙에 따라 이 요청을 처리할 적절한 백엔드 서버를 선택하고, 요청을 자신이 직접 보낸 것처럼 위장하여 선택된 백엔드 서버로 전달한다.
세 번째 단계는 응답 처리다. 백엔드 서버는 전달받은 요청을 처리하고 HTML, JSON, 이미지 같은 응답을 다시 Nginx로 보낸다. 백엔드 서버는 Nginx를 클라이언트인 것처럼 인식한다. 네 번째 단계는 최종 전달이다. Nginx는 백엔드 서버로부터 받은 응답을 그대로 클라이언트에게 최종적으로 전달하며, 클라이언트는 Nginx에서 응답을 받은 것으로 알게 된다.
Nginx 리버스 프록시는 여러 목적으로 활용된다. 로드 밸런싱을 통해 여러 대의 백엔드 서버로 트래픽을 분산시켜 서버 과부하를 방지할 수 있다. 보안을 강화하기 위해 백엔드 서버의 IP와 포트 정보를 숨겨 외부 공격으로부터 보호한다. SSL 종료 기능으로 클라이언트와의 암호화 통신을 Nginx에서 처리하여 백엔드 서버의 부담을 덜어준다. Nginx는 고객과 주방 사이의 매니저 역할로, 고객의 주문을 받아 주방에 전달하고 음식을 받아 고객에게 가져다주면서 전체 서비스의 효율성과 안전을 책임진다고 이해하면 된다.
간단한 MSA 구조를 만들기 위해 두 개의 Spring Boot 서비스를 준비한다. user-service는 8081 포트에서 실행되며 사용자 정보를 관리한다.
# user-service/src/main/resources/application.yml
server:
port: 8081
spring:
application:
name: user-service
// user-service/src/main/java/com/example/user/UserController.java
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping
public List<Map<String, Object>> getUsers() {
Map<String, Object> u1 = Map.of("id", 1, "name", "홍길동");
Map<String, Object> u2 = Map.of("id", 2, "name", "임꺽정");
return List.of(u1, u2);
}
@GetMapping("/{id}")
public Map<String, Object> getUser(@PathVariable int id) {
return Map.of("id", id, "name", "사용자" + id);
}
}
order-service는 8082 포트에서 실행되며 주문 정보를 관리한다.
# order-service/src/main/resources/application.yml
server:
port: 8082
spring:
application:
name: order-service
// order-service/src/main/java/com/example/order/OrderController.java
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@GetMapping
public List<Map<String, Object>> getOrders() {
Map<String, Object> o1 = Map.of("id", 1, "item", "노트북", "userId", 1);
Map<String, Object> o2 = Map.of("id", 2, "item", "마우스", "userId", 2);
return List.of(o1, o2);
}
@GetMapping("/{id}")
public Map<String, Object> getOrder(@PathVariable int id) {
return Map.of("id", id, "item", "상품" + id, "userId", 1);
}
}

위 이미지는 Nginx를 게이트웨이로 활용한 MSA 아키텍처를 보여준다. 클라이언트는 Nginx를 통해서만 서비스에 접근하며, Nginx는 요청 경로에 따라 Auth Server, SMS Server, Message Server, Board Server, Comment Server, FCM Server 등 여러 마이크로서비스로 요청을 라우팅한다. 각 서비스는 독립적으로 MySQL 데이터베이스에 연결되어 있고, Spring Cloud Config를 통해 중앙화된 설정 관리를 받으며, Eureka 서비스 디스커버리에 등록되어 관리된다.
# gateway-nginx/nginx.conf
events {}
http {
upstream user_service {
server user-service:8081;
}
upstream order_service {
server order-service:8082;
}
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri /index.html;
}
location /api/users/ {
proxy_pass http://user_service/;
}
location /api/orders/ {
proxy_pass http://order_service/;
}
}
}
Docker 환경에서 Nginx를 실행하고 Spring Boot도 각각 컨테이너로 띄우면 user-service와 order-service가 서비스 이름으로 사용된다. 웹 페이지가 로드된 후 클라이언트의 JavaScript 코드는 백엔드 데이터가 필요할 때 API 요청을 보낸다.
API 요청 발송 단계에서 클라이언트 브라우저는 http://localhost/api/users/123이나 http://localhost/api/orders/list 같은 요청을 보낸다. 여전히 Nginx 주소로 요청을 보내는 것이다. Nginx 라우팅 단계에서 Nginx는 이 요청을 받고 server 블록 내의 location 규칙과 비교하여 처리한다. /api/users/123 요청은 location /api/users/ 규칙에 일치하고, /api/orders/list 요청은 location /api/orders/ 규칙에 일치한다.
요청 전달 단계에서 Nginx는 일치하는 location 블록의 proxy_pass 지시어를 확인하여 요청을 적절한 백엔드 서버로 전달한다. /api/users/ 요청은 proxy_pass http://user_service/에 따라 user-service:8081 서버로 전달되고, /api/orders/ 요청은 proxy_pass http://order_service/에 따라 order-service:8082 서버로 전달된다.
// frontend-react/src/App.jsx
import { useEffect, useState } from "react";
function App() {
const [users, setUsers] = useState([]);
const [orders, setOrders] = useState([]);
useEffect(() => {
fetch("/api/users")
.then(res => res.json())
.then(setUsers);
fetch("/api/orders")
.then(res => res.json())
.then(setOrders);
}, []);
return (
<div style={{ padding: 20 }}>
<h2>MSA Demo (Nginx + Spring Boot + React)</h2>
<h3>Users</h3>
<ul>
{users.map(u => (
<li key={u.id}>
{u.id} - {u.name}
</li>
))}
</ul>
<h3>Orders</h3>
<ul>
{orders.map(o => (
<li key={o.id}>
{o.id} - {o.item} (userId: {o.userId})
</li>
))}
</ul>
</div>
);
}
export default App;
React는 항상 Nginx로만 요청을 보내고, 실제 서비스 분기는 Nginx가 담당한다.
version: "3.8"
services:
user-service:
image: user-service-image
container_name: user-service
ports:
- "8081:8081"
order-service:
image: order-service-image
container_name: order-service
ports:
- "8082:8082"
frontend:
image: frontend-react-image
container_name: frontend
gateway-nginx:
image: nginx:latest
container_name: gateway-nginx
volumes:
- ./gateway-nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./frontend-react/build:/usr/share/nginx/html:ro
ports:
- "80:80"
depends_on:
- user-service
- order-service
MSA 아키텍처를 구성하는 핵심은 세 가지다.
/api/* 경로 기준으로 서비스를 라우팅한다. 심화 단계로 가면 서비스끼리 REST, gRPC, 메시지 큐를 통해 통신하고, Eureka나 Consul 같은 서비스 디스커버리, Config Server, Resilience4j 같은 Circuit Breaker 등을 추가로 구성할 수 있다.