Spring Boot 아키텍처에서 마이크로서비스 통신: 동기 호출, 이벤트 및 AI 통합
요약
본 글은 Spring Boot 기반 마이크로서비스 아키텍처에서 서비스 간의 효과적인 통신 전략을 다룹니다. OpenFeign, Service Discovery 등을 활용하여 동기 호출과 비동기 이벤트 처리 방식을 비교하고, AI 서비스를 백엔드와 분리하는 방법을 제시합니다.
핵심 포인트
- Service Discovery는 물리적 주소 대신 논리적 이름으로 서비스에 접근하게 합니다.
- OpenFeign은 HTTP 호출을 Java 인터페이스로 추상화하여 사용 편의성을 높입니다.
- 마이크로서비스 간 통신 시 동기/비동기 전략 선택이 중요합니다.
- AI 서비스를 독립적인 계층으로 분리하는 아키텍처 패턴을 검토했습니다.
요약
본 글은 Spring Boot 서비스 간의 통신 전략을 분석하며, 동기 호출과 이벤트 기반 메커니즘 중 어떤 것을 선택할지에 대한 기준을 논합니다. 또한, 나머지 애플리케이션에 종속되지 않으면서 인공지능(AI) 서비스를 통합하는 방법도 다룹니다. 개발 중인 SaaS 애플리케이션의 예시를 바탕으로 Spring Cloud OpenFeign, Service Discovery, Spring Cloud LoadBalancer, 웹훅(webhooks), 메시징 및 복원력 메커니즘을 검토합니다.
1. 서론
애플리케이션이 성장하고 책임이 분리됨에 따라, 서비스들이 어떻게 통신할지 정의하는 것이 필요해집니다. 독립적인 Spring Boot 프로젝트를 생성하는 것은 비교적 간단합니다. 어려움은 한 서비스가 다른 서비스에 속한 데이터나 규칙(예: 사용자 정보 조회, 비즈니스 규칙 실행 또는 외부 API 접근)에 의존할 때 발생합니다. 또한, 처리 과정 전체 동안 HTTP 요청을 열어두지 않고 지연되는 작업을 처리해야 할 필요성도 생깁니다.
이러한 맥락에서, 서비스 간의 통신은 구현 세부 사항이 아니라 아키텍처적 결정이 됩니다. 본 글은 Spring Cloud OpenFeign, Service Discovery, Spring Cloud LoadBalancer를 중심으로 동기 HTTP 호출, 웹훅(webhooks), 이벤트 및 비동기 처리에 중점을 두고 이 주제를 논합니다. 또한 AI 계층을 메인 백엔드와 분리하는 실제 SaaS 적용 사례도 제시합니다.
2. 서비스 위치 지정의 과제
모놀리식 아키텍처는 간단하게 표현할 수 있습니다. 책임이 분리되면서, 메인 API는 인증, AI, 결제 및 알림과 같은 여러 서비스에 의존하게 됩니다. 이때 핵심적인 문제가 발생합니다: 메인 API가 이러한 서비스들을 어떻게 찾고 호출하는가?
가장 간단한 대안은 코드에 고정된 주소를 등록하는 것입니다:
이 접근 방식은 인스턴스 변경, 다중 복제 실행, 환경 마이그레이션 또는 Docker나 Kubernetes 채택과 같은 인프라 변화가 발생할 때까지는 작동합니다. 이러한 경우, 코드에 내장된 물리적 주소는 결합(coupling)의 원천이자 유지보수 비용을 증가시키는 요인이 됩니다. **서비스 디스커버리 (Service Discovery)**는 이 문제를 해결하기 위해 고안되었습니다.
3. 서비스 디스커버리 (Service Discovery)
서비스 디스커버리에서는 소비자가 더 이상 "어디에 서비스가 있나요?"라고 묻지 않고, "어떤 서비스가 필요한가요?"를 지정하게 됩니다. Spring Cloud에서 이는 다음과 같이 표현됩니다:
@FeignClient(name = "ai-service")
코드는 인스턴스의 구체적인 주소(localhost:8082, 10.0.2.15:8082 또는 ai-service:8082)에 독립적이게 되며, 이 주소는 디스커버리 인프라를 통해 해결됩니다. Spring 생태계의 고전적인 예시는 Netflix Eureka로, 등록된 서비스들의 카탈로그를 유지합니다:
ai-service
├── 10.0.1.20:8082
└── 10.0.1.21:8082
...
애플리케이션은 서비스의 논리적 이름만을 알게 되며, 이것이 그 정체성을 나타내게 됩니다.
4. Spring Cloud OpenFeign
Spring Cloud OpenFeign은 HTTP 호출을 Java 인터페이스로 표현할 수 있게 해줍니다. 이 기능이 없다면 URL, 헤더, 직렬화(serialization), 역직렬화(deserialization), HTTP 메서드 및 응답 처리를 수동으로 관리해야 합니다:
restClient
.post()
.uri("http://ai-service/api/process")
...
OpenFeign을 사용하면 계약(contract)이 명시적으로 선언됩니다:
@FeignClient(name = "ai-service")
public interface AiServiceClient {
...
private final AiServiceClient aiServiceClient;
public void execute(AiRequest request) {
...
HTTP 호출 자체는 남아 있지만, Java 계약으로 표현됩니다.
여기서 name 속성과 url 속성 간의 차이점이 두드러집니다. @FeignClient(name = "ai-service", url = "http://localhost:8082")를 선언하면 지정된 주소가 직접 호출됨을 결정합니다. name만 선언할 경우, 서비스의 사용 가능한 인스턴스를 찾도록 디스커버리 메커니즘에 요청합니다. 문법적인 차이는 작지만, 결합도(coupling)에 미치는 영향은 상당합니다.
5. 동기 통신 (Synchronous Communication)
OpenFeign은 다음 단계가 이전 응답에 의존하는 동기적 통신에 특히 적합합니다:
UserDTO user = userClient.findById(userId);
if (!user.isActive()) {
...
이 시나리오에서는 호출을 이벤트로 변환해도 명확한 이점이 없습니다.
동기 통신은 작업에 여러 단계의 지연 시간이 포함될 때 문제가 될 수 있습니다. 다음 작업을 수행해야 하는 POST /process-audio 엔드포인트를 고려해 봅시다:
(1) 파일을 AI 모델로 전송하고;
(2) 전사(transcription)를 기다린 후;
(3) 텍스트를 해석하고;
(4) 다른 서비스에 문의하며;
(5) 작업을 실행하고;
(6) 오디오를 생성하여;
(7) 결과를 반환합니다.
결과로 나오는 체인은 연결된 동기 호출들로 구성됩니다. 각 서비스가 몇 초가 걸린다면, 원래 요청은 전체 프로세스가 진행되는 동안 열려 있게 됩니다. 따라서 작업이 정말로 동기적 동작을 요구하는지 평가할 필요성이 생깁니다.
6. 비동기 통신: 웹훅 및 브로커 (Asynchronous Communication: Webhooks and Brokers)
동기 통신(REST, OpenFeign, RestClient, WebClient)은 결과를 즉시 받아야 할 때 적합합니다. 비동기 통신에서는 송신 서비스가 메시지 브로커(message broker) (RabbitMQ, Kafka, AWS SQS, Google Pub/Sub 등)에 이벤트를 게시하고, 수신 서비스의 처리를 기다리지 않고 계속 진행합니다.
**웹훅(Webhooks)**은 다른 논리를 가진 HTTP 기반 통신 방식입니다. 클라이언트가 작업 상태를 반복적으로 조회하는(폴링, polling) 대신, 담당 서비스가 처리가 완료되면 클라이언트에게 알림을 보냅니다. 20초짜리 작업을 처리한다고 가정할 때, _폴링_은 연속적인 GET /jobs/123 호출을 필요로 합니다. 반면 웹훅은 작업이 준비되었을 때 POST /webhooks/jobs/123를 통해 결과를 전송합니다. 이 접근 방식은 처리에 시간이 오래 걸리거나, 소비자가 차단 상태에 머물러서는 안 되거나, 서비스 간의 경계가 명확하거나, 프로듀서가 소비자에게 알림을 보낼 수 있을 때 유리합니다.
하지만 웹훅과 메시지 브로커(message brokers)를 구분할 필요가 있습니다. 웹훅은 두 서비스 간의 직접적인 HTTP 통신에 머무릅니다. 반면 브로커는 메시지의 영속성(persistence), 다중 소비자, 재시도(retry), 재처리, 시간적 비동기화(temporal decoupling), 전달 제어 등의 기능을 제공하는 중간 계층을 도입합니다. 따라서 웹훅은 특정 통합에 대한 간단한 해결책이 될 수 있는 반면, 브로커는 이벤트 기반 아키텍처에 더 적합합니다.
7. 하이브리드 아키텍처
동기(synchronous) 및 비동기(asynchronous) 모델은 배타적이지 않으며 공존할 수 있습니다. 선택은 각 작업의 특성을 고려해야 합니다:
- 즉시 응답이 필요한 경우: OpenFeign;
- 응답 없이 지속 가능한 경우: 이벤트/큐(event/queue);
- 외부 시스템에 의한 완료 알림: 웹훅(webhook).
8. 인공지능 서비스 통합
Uma 애플리케이션은 AI 전용 서비스를 갖출 수 있습니다. 이 설계에서 주요 API는 비즈니스 로직에 대한 책임을 유지하는 반면, AI 서비스는 언어를 해석하고 오디오를 처리하며 텍스트와 응답을 생성하고 모델 제공업체들을 통합합니다. 이러한 분리는 명확한 경계를 만듭니다. 예를 들어, "iFood에서 R$ 40 지출을 생성해 주세요"라는 명령어는 AI 서비스에 의해 해석되어 CreateExpenseCommand로 변환된 후 주요 API로 전달되고, 주요 API가 비즈니스 로직을 적용하고 PostgreSQL에 데이터를 영속화합니다. AI 서비스는 데이터베이스에 접근하거나 영속화 방식을 알 필요가 없습니다. 오직 의도를 해석하는 역할만 합니다.
가능한 제공업체들 중에서는 Google Gemini가 이러한 유형의 애플리케이션에 적합하며, 특히 API를 통한 통합 용이성과 다양한 유형의 입력을 처리할 수 있는 능력 덕분입니다. 하지만 핵심적인 측면은 제공업체를 선택하는 것이 아니라, 비즈니스 코드가 그 제공업체에 직접 의존하지 않도록 하는 것입니다. 애플리케이션은 "이 입력을 해석해야 한다"는 필요성을 표현해야 하며, "비즈니스 로직의 이 지점에서 API X를 호출해야 한다"는 식으로 해서는 안 됩니다.
Spring AI는 ChatClient와 같은 추상화를 제공하여 이러한 목표에 기여합니다. 이는 모델/제공업체의 구성과 애플리케이션 로직을 분리하기 때문입니다:
String response = chatClient
.prompt()
.user("이 메시지를 분석하세요...")
...
이러한 접근 방식은 통합을 체계화하고, 애플리케이션 코드가 각 제공업체의 HTTP 세부 사항, 인증 및 페이로드 형식을 처리할 필요가 없으므로 모델 교체나 실험을 덜 침습적으로 만듭니다.
AI는 비동기 흐름에서도 작동할 수 있습니다. 오디오 처리에 있어서 사용자가 전체 응답을 기다리는 대신, API는 큐에 _job_을 기록하고 202 Accepted와 함께 jobId를 반환합니다. AI 서비스가 작업을 처리하고 Gemini를 조회한 후 웹훅(webhook)이나 이벤트로 API에 알리고, 최종적으로 API가 사용자에게 알립니다.
9. OpenFeign의 한계점과 복원력 메커니즘
OpenFeign은 다른 HTTP 서비스를 호출하는 필요성을 적절히 해결하지만, 장기 실행 프로세싱에는 적합하지 않습니다. 또한, Idempotency(멱등성), 분산 일관성(distributed consistency), 안전한 재시도(safe retry), Circuit Breaker(회로 차단기), 메시지 큐(queues), 이벤트, 관찰 가능성(observability) 또는 분산 트랜잭션(distributed transactions)을 자동으로 해결해주지도 않습니다. 이는 전체 아키텍처가 아니라 통신 도구에 불과합니다.
9.1 재시도 및 멱등성. 클라이언트 Feign에 의해 호출된 POST /payments를 가정해 봅시다. 요청이 서버에 도달하여 결제가 생성되었지만 응답이 손실되었다고 할 때, 클라이언트는 이 이벤트를 실패로 해석할 수 있습니다. 자동 재시도는 이 경우 두 번째 결제를 생성하게 됩니다. 따라서 상태를 변경하는 호출은 자동 재시도를 받기 전에 신중하게 분석되어야 합니다. 일반적인 해결책은 멱등성 키(Idempotency-Key: 8f3c...) 사용이며, 이는 서버가 이미 처리된 작업을 인식할 수 있게 해줍니다.
9.2 타임아웃. 각 종속성에 대해 최대 대기 시간을 정의해야 합니다. 내부 API는 연결 타임아웃(connect timeout)을 2초, 읽기 타임아웃(read timeout)을 5초로 운영할 수 있는 반면, AI 서비스 호출은 각각 5초와 60초를 요구할 수 있습니다. 과도한 값은 리소스를 점유하고, 부족한 값은 정상적인 작업을 오류로 변환시킵니다. 따라서 타임아웃은 단순한 설정 매개변수가 아니라 종속성의 계약(contract)의 일부입니다.
9.3 Circuit Breaker. Gemini가 사용 불가능하고 API가 계속해서 새 시도를 할 경우, 연속적인 타임아웃과 재시도는 문제를 증폭시킬 수 있습니다. Circuit Breaker는 세 가지 상태를 통해 이러한 동작을 변경합니다: 정상적으로 호출이 발생하는 CLOSED 상태; 많은 실패 후에 도달하며 요청이 빠르게 거부되는 OPEN 상태; 그리고 테스트 호출이 수행되고 성공하면 CLOSED 상태로 복구되는 HALF-OPEN 상태. 이는 이미 실패 중인 종속성을 과부하하는 것을 방지합니다.
10. 통합적 관점
그림은 전체 아키텍처를 보여주며, 표 1은 논의된 전략들을 요약합니다.
표 1. 상황별 통신 전략.
| 상황 | 전략 |
|---|---|
| API가 즉시 응답이 필요한 경우 | OpenFeign |
| ... |
11. 결정 기준
SaaS 애플리케이션 개발 경험을 바탕으로, 통신 전략의 선택은 네 가지 질문에 의해 안내될 수 있습니다:
- 즉각적인 응답이 필요한가요? 그렇다면 HTTP/OpenFeign을 사용합니다.
- 이 응답 없이 진행하는 것이 가능한가요? 그렇다면 이벤트/큐를 사용합니다.
- 처리 완료 시 누가 알게 될까요? 만약 다른 시스템이라면, 웹훅(webhook)을 사용합니다.
- 작업이 여러 번 실행될 수 있나요? 그렇다면, 아이덴티티(idempotency)가 보장되어야 합니다.
마지막 질문은 부분적 장애가 시스템의 정상적인 동작에 포함되는 분산 시스템에서 특히 중요합니다.
12. 모놀리스(monolith)에서 전환할 때의 시사점
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기