본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 21. · 2 Views
Spring Cloud 서비스 간 통신 완벽 가이드
마이크로서비스 아키텍처에서 서비스 간 통신은 필수입니다. OpenFeign과 RestClient를 활용한 선언적 HTTP 클라이언트 구현 방법부터 타임아웃 설정, 에러 처리까지 실무에 바로 적용할 수 있는 내용을 담았습니다.
목차
1. OpenFeign 소개
어느 날 김개발 씨는 주문 서비스에서 상품 서비스의 정보를 가져와야 하는 상황에 직면했습니다. RestTemplate으로 코드를 작성하니 URL 조합, 헤더 설정, 에러 처리까지 신경 쓸 게 너무 많았습니다.
OpenFeign은 넷플릭스에서 개발한 선언적 HTTP 클라이언트입니다. 인터페이스에 어노테이션만 추가하면 HTTP 요청 코드가 자동으로 생성됩니다.
마치 로컬 메서드를 호출하듯이 다른 서비스의 API를 호출할 수 있게 해주는 강력한 도구입니다.
다음 코드를 살펴봅시다.
// 의존성 추가 (build.gradle)
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
// 메인 클래스에 @EnableFeignClients 추가
@SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
김개발 씨는 입사 4개월 차 백엔드 개발자입니다. 오늘은 주문 서비스를 개발하는 중입니다.
주문을 생성할 때 상품 서비스에서 상품 정보를 가져와야 하는데, 처음에는 RestTemplate으로 작성했습니다. 코드가 금방 복잡해졌습니다.
URL을 직접 조합하고, 헤더를 설정하고, 응답을 파싱하는 코드가 반복됩니다. 더 큰 문제는 다른 서비스를 호출할 때마다 비슷한 코드를 계속 작성해야 한다는 것입니다.
선배 개발자 박시니어 씨가 코드 리뷰를 하다가 말했습니다. "김 개발님, 이런 경우에는 OpenFeign을 사용하면 훨씬 간단해집니다." OpenFeign이란 정확히 무엇일까요?
쉽게 비유하자면, OpenFeign은 마치 비서와 같습니다. 여러분이 "저 건물 3층에 가서 문서 A를 가져와"라고 지시만 하면, 비서가 알아서 찾아가서 가져오는 것처럼 말이죠.
개발자는 "어떤 서비스의 어떤 API를 호출할 것인지"만 선언하면, Feign이 알아서 HTTP 요청을 만들고 실행합니다. Feign이 없던 시절에는 어땠을까요?
개발자들은 RestTemplate이나 HttpClient를 사용해서 모든 것을 직접 작성해야 했습니다. URL을 문자열로 조합하다가 오타가 나면 런타임에 에러가 발생했습니다.
헤더 설정을 깜빡하면 인증 오류가 났습니다. 더 큰 문제는 코드가 중복되고 유지보수가 어려워진다는 것이었습니다.
바로 이런 문제를 해결하기 위해 OpenFeign이 등장했습니다. OpenFeign을 사용하면 인터페이스 정의만으로 HTTP 클라이언트를 만들 수 있습니다.
컴파일 타임에 타입 체크가 되므로 오타로 인한 오류를 미리 방지할 수 있습니다. 무엇보다 코드가 선언적이고 읽기 쉬워진다는 큰 이점이 있습니다.
위의 코드를 단계별로 살펴보겠습니다. 먼저 Gradle 파일에 spring-cloud-starter-openfeign 의존성을 추가합니다.
이 한 줄이면 OpenFeign 사용 준비가 끝납니다. 다음으로 메인 애플리케이션 클래스에 @EnableFeignClients 어노테이션을 추가합니다.
이것은 Spring에게 "이 프로젝트에서 Feign 클라이언트를 사용할 것이니 스캔해서 등록해줘"라고 알려주는 것입니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 전자상거래 플랫폼을 개발한다고 가정해봅시다. 주문 서비스, 상품 서비스, 재고 서비스, 결제 서비스가 각각 분리되어 있습니다.
주문을 처리하려면 상품 정보를 확인하고, 재고를 체크하고, 결제를 진행해야 합니다. OpenFeign을 사용하면 각 서비스를 호출하는 클라이언트를 인터페이스로 깔끔하게 정의할 수 있습니다.
쿠팡, 네이버쇼핑 같은 대형 서비스에서도 이런 패턴을 적극 활용합니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 @EnableFeignClients를 추가하지 않고 Feign 클라이언트를 사용하려는 것입니다. 이렇게 하면 빈을 찾을 수 없다는 에러가 발생합니다.
따라서 반드시 메인 애플리케이션 클래스에 이 어노테이션을 추가해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 이렇게 설정만 하면 되는군요!" OpenFeign을 제대로 이해하면 마이크로서비스 간 통신 코드를 훨씬 깔끔하게 작성할 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - Spring Cloud 버전과 Spring Boot 버전 호환성을 확인하세요
- @EnableFeignClients의 basePackages 속성으로 스캔 범위를 지정할 수 있습니다
- 로컬 개발 시에는 Feign 로깅을 활성화하면 디버깅이 쉬워집니다
2. FeignClient 사용법
설정을 마친 김개발 씨는 이제 실제로 상품 서비스를 호출하는 클라이언트를 만들어야 합니다. 박시니어 씨가 "인터페이스 하나만 만들면 됩니다"라고 말했지만, 정확히 어떻게 작성해야 할까요?
@FeignClient 어노테이션은 HTTP 클라이언트 인터페이스를 정의하는 핵심 어노테이션입니다. 서비스 이름, URL, 설정을 지정하고, 메서드에는 Spring MVC와 동일한 어노테이션을 사용합니다.
마치 컨트롤러를 작성하듯이 클라이언트를 작성할 수 있습니다.
다음 코드를 살펴봅시다.
@FeignClient(name = "product-service", url = "${product.service.url}")
public interface ProductClient {
// 상품 조회
@GetMapping("/api/products/{id}")
ProductResponse getProduct(@PathVariable("id") Long id);
// 상품 목록 조회
@GetMapping("/api/products")
List<ProductResponse> getProducts(@RequestParam("category") String category);
// 재고 확인
@PostMapping("/api/products/check-stock")
StockResponse checkStock(@RequestBody StockRequest request);
}
김개발 씨는 이제 본격적으로 상품 서비스를 호출하는 클라이언트를 만들 차례입니다. 새로운 인터페이스 파일을 열고 @FeignClient 어노테이션을 입력했습니다.
그런데 name에는 무엇을 써야 하고, url은 어떻게 설정해야 할까요? 박시니어 씨가 옆에서 설명합니다.
"name은 이 클라이언트를 식별하는 이름입니다. 유레카 같은 서비스 디스커버리를 사용한다면 서비스 이름을, 직접 URL을 지정한다면 임의의 이름을 써도 됩니다." @FeignClient란 정확히 무엇일까요?
쉽게 비유하자면, @FeignClient는 마치 전화번호부와 같습니다. "상품 서비스에 연락하려면 이 번호로 전화해"라고 등록해두는 것처럼, "product-service라는 이름의 서비스는 이 URL에 있어"라고 Spring에게 알려주는 것입니다.
그러면 Spring이 알아서 그 서비스와 통신할 수 있는 객체를 만들어줍니다. 예전에는 어땠을까요?
RestTemplate을 사용할 때는 매번 URL을 직접 조합해야 했습니다. "http://product-service/api/products/" + id 이런 식으로 문자열을 붙여야 했죠.
오타가 나면? 컴파일은 되지만 런타임에 404 에러가 발생합니다.
디버깅하기도 어렵습니다. 바로 이런 문제를 해결하기 위해 선언적 방식이 도입되었습니다.
@FeignClient를 사용하면 타입 안전성이 보장됩니다. ProductResponse 타입을 반환한다고 선언하면, 컴파일러가 타입을 체크합니다.
@PathVariable로 경로 변수를 바인딩하고, @RequestParam으로 쿼리 파라미터를 전달하고, @RequestBody로 요청 바디를 보낼 수 있습니다. Spring MVC를 다뤄본 개발자라면 익숙한 문법입니다.
위의 코드를 자세히 살펴보겠습니다. 첫 번째 줄의 @FeignClient에서 name은 "product-service"로 지정했습니다.
url은 application.yml의 설정값을 참조하도록 했습니다. 이렇게 하면 환경별로 다른 URL을 사용할 수 있습니다.
개발 환경에서는 localhost, 운영 환경에서는 실제 도메인을 사용하는 식으로 말이죠. getProduct 메서드를 보면 @GetMapping으로 경로를 지정하고, @PathVariable로 id를 받습니다.
이것은 컨트롤러와 똑같은 방식입니다. Feign이 이 정보를 읽어서 실제 HTTP GET 요청을 만들어줍니다.
getProducts 메서드는 쿼리 파라미터를 사용하는 예시입니다. @RequestParam으로 category를 전달하면, Feign이 자동으로 /api/products?category=전자제품 같은 URL을 만들어줍니다.
checkStock 메서드는 POST 요청에 JSON 바디를 보내는 경우입니다. @RequestBody로 StockRequest 객체를 전달하면, Feign이 자동으로 JSON으로 직렬화해서 전송합니다.
실제 현업에서는 어떻게 활용할까요? 배달의민족 같은 서비스를 생각해봅시다.
주문 서비스에서 음식점 서비스의 영업시간을 확인하고, 라이더 서비스에 배달을 요청하고, 결제 서비스로 결제를 진행해야 합니다. 각각의 통신을 Feign Client로 정의하면 코드가 매우 깔끔해집니다.
서비스가 수십 개로 늘어나도 관리가 쉽습니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수는 @PathVariable의 name 속성을 생략하는 것입니다. @PathVariable("id")처럼 명시적으로 이름을 지정해야 합니다.
그렇지 않으면 컴파일 옵션에 따라 제대로 매핑되지 않을 수 있습니다. 또 다른 실수는 반환 타입을 잘못 지정하는 것입니다.
실제 API가 List를 반환하는데 단일 객체로 받으려고 하면 역직렬화 에러가 발생합니다. API 명세를 정확히 확인하고 반환 타입을 맞춰야 합니다.
김개발 씨는 이제 인터페이스를 완성했습니다. 박시니어 씨가 말합니다.
"이제 이 인터페이스를 서비스에 주입해서 사용하면 됩니다. 일반 빈처럼요." @FeignClient로 정의한 인터페이스는 Spring이 자동으로 구현체를 만들어서 빈으로 등록합니다.
여러분은 그냥 @Autowired나 생성자 주입으로 받아서 사용하기만 하면 됩니다. 정말 간단하지 않나요?
실전 팁
💡 - name과 url을 동시에 지정하면 url이 우선합니다
- 컨트롤러와 동일한 어노테이션을 사용하므로 학습 비용이 낮습니다
- 인터페이스 메서드명은 자유롭게 지정해도 되며, 실제 HTTP 요청은 어노테이션 정보로 결정됩니다
3. 선언적 HTTP 클라이언트
김개발 씨가 동료 개발자에게 Feign 코드를 자랑했더니, "선언적이라는 게 정확히 무슨 의미야?"라는 질문을 받았습니다. 명령형과 선언형의 차이가 뭘까요?
선언적 프로그래밍은 "무엇을 할 것인지"만 선언하고 "어떻게 할 것인지"는 프레임워크에 맡기는 방식입니다. Feign은 인터페이스로 API 스펙만 선언하면, 실제 HTTP 통신 로직은 자동으로 생성됩니다.
코드가 간결해지고 의도가 명확해집니다.
다음 코드를 살펴봅시다.
// 선언적 방식 - Feign
@FeignClient(name = "order-service")
public interface OrderClient {
@GetMapping("/api/orders/{id}")
OrderResponse getOrder(@PathVariable Long id);
}
// 명령형 방식 - RestTemplate
public class OrderClientImpl {
private final RestTemplate restTemplate;
public OrderResponse getOrder(Long id) {
String url = "http://order-service/api/orders/" + id;
ResponseEntity<OrderResponse> response =
restTemplate.getForEntity(url, OrderResponse.class);
return response.getBody();
}
}
김개발 씨는 동료의 질문에 잠시 말문이 막혔습니다. 분명히 코드가 간결해지는 건 알겠는데, "선언적"이라는 개념을 정확히 설명하기는 어려웠습니다.
퇴근 후 집에서 곰곰이 생각해봤습니다. 다음 날 아침, 박시니어 씨에게 물어봤습니다.
"선배님, 선언적이랑 명령형의 차이가 정확히 뭔가요?" 선언적 프로그래밍이란 정확히 무엇일까요? 쉽게 비유하자면, 택시를 탈 때를 생각해봅시다.
명령형 방식은 "직진 100미터, 좌회전, 다시 200미터, 우회전" 이렇게 모든 경로를 일일이 지시하는 것입니다. 선언적 방식은 "강남역으로 가주세요"라고 목적지만 말하면 기사님이 알아서 최적의 경로로 데려다주는 것입니다.
코드로 돌아가서 생각해봅시다. RestTemplate을 사용하는 명령형 방식에서는 어떤 일들이 벌어질까요?
먼저 URL을 문자열로 조합해야 합니다. 변수를 넣을 때는 실수하지 않도록 조심해야 합니다.
그다음 restTemplate.getForEntity()를 호출하고, 응답을 받아서, 바디를 추출하고, 타입 캐스팅을 합니다. 에러 처리도 직접 해야 합니다.
연결 타임아웃은? 읽기 타임아웃은?
모두 개발자가 신경 써야 합니다. 코드가 길어지고 복잡해집니다.
더 큰 문제는 비즈니스 로직이 HTTP 통신 세부사항에 묻혀버린다는 것입니다. 코드를 읽는 사람은 "아, 주문 정보를 가져오는구나"보다는 "URL을 조합하고, GET 요청을 보내고..." 같은 기술적 세부사항에 집중하게 됩니다.
바로 이런 문제를 해결하기 위해 선언적 방식이 필요합니다. Feign을 사용하면 어떻게 달라질까요?
인터페이스에 @GetMapping("/api/orders/{id}")라고 선언합니다. "주문 ID로 주문 정보를 조회한다"는 의도가 명확합니다.
URL 조합은? HTTP 메서드 선택은?
응답 파싱은? 모두 Feign이 알아서 처리합니다.
위의 코드를 비교해보겠습니다. Feign 방식은 단 세 줄입니다.
어노테이션으로 HTTP 메서드와 경로를 지정하고, 메서드 시그니처로 파라미터와 반환 타입을 선언합니다. 끝입니다.
코드를 읽으면 "아, 이 메서드는 주문 정보를 가져오는구나"가 바로 이해됩니다. RestTemplate 방식은 여섯 줄 이상입니다.
URL을 조합하고, getForEntity를 호출하고, 바디를 추출합니다. 기술적 세부사항이 많아서 비즈니스 의도를 파악하기가 상대적으로 어렵습니다.
실제 프로젝트에서는 이런 차이가 더 극명해집니다. 쿠팡처럼 수백 개의 마이크로서비스가 있는 환경을 생각해봅시다.
각 서비스가 다른 서비스들과 통신해야 합니다. RestTemplate 방식이라면 수천 개의 URL 조합 코드, HTTP 요청 코드가 프로젝트 곳곳에 흩어져 있을 것입니다.
유지보수 악몽입니다. Feign 방식이라면?
각 서비스에 대한 Client 인터페이스만 정의하면 됩니다. 코드가 깔끔하고, 테스트하기 쉽고, 변경하기도 쉽습니다.
인터페이스이므로 Mock 객체로 대체하기도 간단합니다. 하지만 주의할 점도 있습니다.
선언적 방식의 단점은 내부 동작을 이해하기 어렵다는 것입니다. 마법처럼 동작하는 것처럼 보이죠.
문제가 생겼을 때 디버깅이 어려울 수 있습니다. 따라서 Feign이 내부적으로 어떻게 동작하는지 기본 개념은 이해하고 있어야 합니다.
또한 선언적 방식이 항상 정답은 아닙니다. 매우 특수한 HTTP 요청을 보내야 하거나, 세밀한 제어가 필요한 경우에는 명령형 방식이 나을 수 있습니다.
상황에 맞는 도구를 선택하는 것이 중요합니다. 김개발 씨는 이제 선언적 프로그래밍의 개념을 이해했습니다.
동료에게 자신 있게 설명할 수 있을 것 같습니다. 선언적 방식을 제대로 이해하면 더 읽기 쉽고 유지보수하기 쉬운 코드를 작성할 수 있습니다.
여러분도 프로젝트에서 적극 활용해 보세요.
실전 팁
💡 - 선언적 = 무엇을(What), 명령형 = 어떻게(How)로 기억하세요
- SQL, HTML도 선언적 언어의 대표적인 예시입니다
- 추상화 레벨이 높아질수록 생산성이 올라가지만, 디버깅은 어려워질 수 있습니다
4. RestClient 사용법
김개발 씨는 Spring 3.2 릴리스 노트를 읽다가 RestClient라는 새로운 API를 발견했습니다. RestTemplate의 후속 버전이라는데, Feign과는 어떻게 다를까요?
RestClient는 Spring 6.1부터 도입된 새로운 동기 HTTP 클라이언트입니다. RestTemplate의 단순함과 WebClient의 유연성을 결합했습니다.
플루언트 API로 직관적이고, 함수형 스타일로 간결하며, Feign보다 세밀한 제어가 가능합니다.
다음 코드를 살펴봅시다.
@Configuration
public class RestClientConfig {
@Bean
public RestClient productRestClient() {
return RestClient.builder()
.baseUrl("http://product-service")
.defaultHeader("Content-Type", "application/json")
.requestInterceptor((request, body, execution) -> {
// 요청 인터셉터 - 로깅, 인증 토큰 추가 등
return execution.execute(request, body);
})
.build();
}
}
// 사용 예시
@Service
public class ProductService {
private final RestClient restClient;
public ProductResponse getProduct(Long id) {
return restClient.get()
.uri("/api/products/{id}", id)
.retrieve()
.body(ProductResponse.class);
}
}
김개발 씨는 호기심이 생겼습니다. Feign도 좋은데 왜 Spring에서 새로운 HTTP 클라이언트를 만들었을까요?
박시니어 씨에게 물어봤습니다. "RestClient는 RestTemplate을 대체하기 위해 만들어졌습니다.
RestTemplate은 오래되고 구식이죠. 하지만 동기 방식이 필요한 경우가 여전히 많습니다.
WebClient는 비동기이고 러닝 커브가 있고요." RestClient란 정확히 무엇일까요? 쉽게 비유하자면, RestClient는 스위스 아미 나이프 같은 도구입니다.
RestTemplate의 단순함, WebClient의 강력함, Feign의 편리함을 적절히 섞어놓은 것이죠. 상황에 맞게 원하는 방식으로 사용할 수 있습니다.
과거에는 어땠을까요? 간단한 HTTP 요청에는 RestTemplate을 썼습니다.
하지만 RestTemplate은 2000년대 API 스타일입니다. 함수형 프로그래밍이나 플루언트 API를 지원하지 않습니다.
복잡한 요청을 만들려면 코드가 장황해집니다. 비동기가 필요하면 WebClient를 써야 했습니다.
하지만 Reactive 프로그래밍을 모르면 사용하기 어렵습니다. 간단한 동기 요청에도 Mono, Flux를 배워야 한다면 오버엔지니어링입니다.
Feign은 편리하지만 외부 라이브러리입니다. Spring 생태계 외부의 의존성을 추가해야 하고, 선언적 방식만 지원하므로 세밀한 제어가 어렵습니다.
바로 이런 간극을 메우기 위해 RestClient가 등장했습니다. RestClient는 Builder 패턴과 플루언트 API를 제공합니다.
baseUrl(), defaultHeader(), requestInterceptor() 같은 메서드를 체이닝해서 설정을 구성합니다. 가독성이 좋고 직관적입니다.
위의 코드를 자세히 살펴보겠습니다. 먼저 RestClient를 빈으로 등록합니다.
builder()로 시작해서 baseUrl로 기본 URL을 설정합니다. defaultHeader로 모든 요청에 포함될 헤더를 지정할 수 있습니다.
requestInterceptor를 추가하면 요청 전후에 원하는 로직을 실행할 수 있습니다. 예를 들어 JWT 토큰을 자동으로 추가하거나, 모든 요청을 로깅하는 식으로 말이죠.
실제 사용할 때는 restClient.get()으로 GET 요청을 시작합니다. uri()로 경로와 파라미터를 지정합니다.
retrieve()로 응답을 가져오고, body()로 원하는 타입으로 변환합니다. 한 줄로 쭉 이어지므로 읽기 쉽습니다.
POST 요청은 어떻게 할까요? restClient.post().uri("/api/products").body(request).retrieve().body(ProductResponse.class) 이렇게 작성하면 됩니다.
body()로 요청 바디를 전달하고, 나머지는 GET과 동일합니다. 에러 처리는 어떻게 할까요?
onStatus()를 사용하면 HTTP 상태 코드별로 다른 처리를 할 수 있습니다. 예를 들어 404일 때는 null을 반환하고, 500일 때는 예외를 던지는 식으로 말이죠.
실제 현업에서는 어떻게 활용할까요? 외부 API를 호출하는 경우를 생각해봅시다.
네이버 검색 API, 카카오 지도 API 같은 것들이죠. Feign을 쓰기에는 API 스펙이 복잡하고, 커스터마이징이 많이 필요합니다.
이럴 때 RestClient를 사용하면 유연하게 대응할 수 있습니다. Interceptor로 API 키를 자동으로 추가하고, 에러 처리를 세밀하게 제어하고, 타임아웃도 요청마다 다르게 설정할 수 있습니다.
하지만 주의할 점도 있습니다. RestClient는 Spring 6.1 이상에서만 사용할 수 있습니다.
Spring Boot로 치면 3.2 이상이죠. 기존 프로젝트를 업그레이드하기 어렵다면 RestTemplate이나 Feign을 계속 써야 합니다.
또한 선언적 방식의 깔끔함은 Feign보다 떨어집니다. 간단한 CRUD 작업이 많다면 Feign이 더 나을 수 있습니다.
반대로 복잡한 요청 처리가 필요하다면 RestClient가 적합합니다. 김개발 씨는 새로운 프로젝트에서 RestClient를 적극 활용해보기로 했습니다.
최신 Spring 기술을 배우는 것은 항상 즐겁습니다. RestClient를 제대로 이해하면 상황에 맞는 최적의 HTTP 클라이언트를 선택할 수 있습니다.
도구는 많을수록 좋습니다.
실전 팁
💡 - Spring Boot 3.2 이상에서 사용 가능합니다
- 동기 블로킹 방식이므로 간단한 요청에 적합합니다
- WebClient의 동기 버전이라고 생각하면 이해하기 쉽습니다
5. 타임아웃 설정
어느 날 김개발 씨의 주문 서비스가 느려졌습니다. 상품 서비스가 응답하지 않는데도 한없이 기다리고 있었습니다.
타임아웃을 설정하지 않아서 생긴 문제였습니다.
타임아웃은 연결 시간과 응답 대기 시간의 최대값을 지정하는 설정입니다. 연결 타임아웃은 서버에 접속하는 시간, 읽기 타임아웃은 응답을 받는 시간을 제한합니다.
적절한 타임아웃 설정은 장애 전파를 막고 시스템 안정성을 높입니다.
다음 코드를 살펴봅시다.
// Feign 타임아웃 설정 (application.yml)
feign:
client:
config:
product-service:
connectTimeout: 2000 # 연결 타임아웃 2초
readTimeout: 5000 # 읽기 타임아웃 5초
default:
connectTimeout: 1000 # 기본값 1초
readTimeout: 3000 # 기본값 3초
// RestClient 타임아웃 설정
@Bean
public RestClient productRestClient() {
return RestClient.builder()
.baseUrl("http://product-service")
.requestFactory(new JdkClientHttpRequestFactory(
HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(2))
.build()
))
.build();
}
김개발 씨는 당황했습니다. 주문 서비스가 갑자기 응답을 하지 않습니다.
모니터링 도구를 확인해보니 스레드들이 모두 WAITING 상태입니다. 무엇을 기다리고 있는 걸까요?
로그를 추적해보니 상품 서비스를 호출하는 부분에서 멈춰 있었습니다. 상품 서비스가 장애로 응답을 하지 못하는데, 주문 서비스는 응답이 올 때까지 영원히 기다리고 있었던 것입니다.
박시니어 씨가 재빨리 타임아웃을 설정했습니다. 몇 분 후 시스템이 정상화되었습니다.
"타임아웃은 필수입니다. 없으면 이렇게 됩니다." 타임아웃이란 정확히 무엇일까요?
쉽게 비유하자면, 타임아웃은 약속 시간과 같습니다. 친구와 약속했는데 30분이 지나도 오지 않으면 그냥 가버리는 것처럼, 서버가 일정 시간 내에 응답하지 않으면 요청을 포기하는 것입니다.
무한정 기다리면 본인도 피해를 입습니다. 타임아웃이 없으면 어떻게 될까요?
마이크로서비스 환경을 생각해봅시다. 주문 서비스는 Tomcat의 스레드 풀을 사용합니다.
기본적으로 200개 정도의 스레드가 있습니다. 상품 서비스가 느려지면?
주문 서비스의 스레드들이 상품 서비스 응답을 기다리며 블로킹됩니다. 스레드가 하나씩 묶이기 시작합니다.
10개, 50개, 100개... 결국 모든 스레드가 WAITING 상태가 됩니다.
새로운 요청이 들어와도 처리할 스레드가 없습니다. 주문 서비스도 다운됩니다.
이것을 장애 전파라고 합니다. 더 심각한 것은 연쇄 반응입니다.
주문 서비스가 다운되면, 주문 서비스를 호출하는 다른 서비스도 같은 방식으로 다운됩니다. 도미노처럼 전체 시스템이 무너집니다.
바로 이런 재앙을 막기 위해 타임아웃이 필수입니다. 타임아웃은 크게 두 가지가 있습니다.
연결 타임아웃과 읽기 타임아웃입니다. 연결 타임아웃은 서버에 TCP 연결을 맺는 시간의 한계입니다.
네트워크가 느리거나 서버가 죽어있으면 연결 자체가 안 됩니다. 이때 무한정 기다리지 않고 빠르게 포기하는 것이죠.
보통 1-2초로 설정합니다. 읽기 타임아웃은 요청을 보낸 후 응답을 받는 시간의 한계입니다.
서버가 처리하는 시간이 오래 걸릴 수 있으므로 연결 타임아웃보다는 길게 설정합니다. 보통 3-5초 정도로 설정하지만, API 특성에 따라 다릅니다.
복잡한 계산을 하는 API라면 10초 이상으로 설정할 수도 있습니다. 위의 코드를 살펴보겠습니다.
Feign 설정에서는 YAML 파일로 타임아웃을 지정합니다. product-service에 대해서는 연결 2초, 읽기 5초로 설정했습니다.
default 항목은 이름을 지정하지 않은 모든 클라이언트에 적용됩니다. 이렇게 서비스별로 다른 타임아웃을 설정할 수 있습니다.
RestClient에서는 HttpClient를 커스터마이징해서 타임아웃을 설정합니다. connectTimeout()으로 연결 타임아웃을 지정합니다.
읽기 타임아웃은 JdkClientHttpRequestFactory의 setReadTimeout()으로 설정할 수 있습니다. 실제 현업에서는 어떻게 설정할까요?
경험상 대부분의 API는 연결 1-2초, 읽기 3-5초면 충분합니다. 하지만 외부 API는 더 길게 설정해야 할 수 있습니다.
예를 들어 결제 API는 10-15초로 설정하는 경우도 있습니다. 결제 처리가 느릴 수 있기 때문이죠.
내부 서비스 간 통신은 짧게 설정하는 게 좋습니다. 내부 네트워크는 빠르므로 1초 안에 응답이 와야 정상입니다.
타임아웃이 짧으면 문제를 빨리 감지할 수 있습니다. 하지만 주의할 점도 있습니다.
타임아웃을 너무 짧게 설정하면 정상 요청도 실패할 수 있습니다. 서버가 일시적으로 바쁠 때 타임아웃에 걸려서 실패하는 것이죠.
반대로 너무 길게 설정하면 타임아웃의 의미가 없습니다. 적절한 균형이 필요합니다.
타임아웃이 발생하면 예외가 던져집니다. 이 예외를 어떻게 처리할지도 중요합니다.
재시도? 폴백?
에러 응답? 비즈니스 로직에 맞게 설계해야 합니다.
김개발 씨는 이제 모든 Feign Client에 타임아웃을 설정했습니다. 장애가 전파되는 악몽은 다시 겪고 싶지 않습니다.
타임아웃을 제대로 설정하면 시스템이 훨씬 안정적으로 동작합니다. 반드시 설정하세요.
실전 팁
💡 - 연결 타임아웃은 짧게(1-2초), 읽기 타임아웃은 API 특성에 맞게 설정하세요
- 프로덕션 환경에서 타임아웃 미설정은 치명적인 장애로 이어질 수 있습니다
- 모니터링을 통해 실제 응답 시간을 측정하고 타임아웃을 조정하세요
6. 에러 처리와 폴백
타임아웃을 설정했지만 김개발 씨는 또 다른 문제에 직면했습니다. 상품 서비스가 500 에러를 반환하면 주문이 실패합니다.
일시적인 오류로 전체 주문이 막혀버리는 것은 너무 가혹합니다.
에러 처리는 HTTP 에러를 감지하고 적절히 대응하는 것이고, 폴백은 에러 발생 시 대체 로직을 실행하는 것입니다. FeignClient의 fallback, ErrorDecoder, RestClient의 onStatus로 세밀한 에러 처리가 가능합니다.
시스템 회복탄력성을 높입니다.
다음 코드를 살펴봅시다.
// Feign Fallback 설정
@FeignClient(
name = "product-service",
fallback = ProductClientFallback.class
)
public interface ProductClient {
@GetMapping("/api/products/{id}")
ProductResponse getProduct(@PathVariable Long id);
}
@Component
public class ProductClientFallback implements ProductClient {
@Override
public ProductResponse getProduct(Long id) {
// 폴백 로직 - 기본값 반환 또는 캐시 사용
return ProductResponse.builder()
.id(id)
.name("상품 정보를 불러올 수 없습니다")
.available(false)
.build();
}
}
// RestClient 에러 처리
public ProductResponse getProduct(Long id) {
return restClient.get()
.uri("/api/products/{id}", id)
.retrieve()
.onStatus(status -> status.value() == 404,
(request, response) -> {
throw new ProductNotFoundException(id);
})
.onStatus(HttpStatusCode::is5xxServerError,
(request, response) -> {
throw new ProductServiceException("서버 오류");
})
.body(ProductResponse.class);
}
김개발 씨는 또 다른 장애 알림을 받았습니다. 이번에는 타임아웃이 아니라 상품 서비스에서 500 에러가 발생했습니다.
데이터베이스 연결 문제였습니다. 상품 정보를 못 가져오니까 주문이 전부 실패합니다.
고객 입장에서는 억울합니다. 주문만 하려고 했는데 상품 상세 정보를 못 가져온다고 주문 자체가 막히다니요.
뭔가 방법이 없을까요? 박시니어 씨가 말합니다.
"폴백을 구현하세요. 상품 정보를 못 가져오더라도 주문은 진행되게 하는 겁니다." 폴백이란 정확히 무엇일까요?
쉽게 비유하자면, 폴백은 플랜 B와 같습니다. A안이 실패하면 B안으로 넘어가는 것이죠.
영화관에서 포스터를 못 불러오면? 기본 이미지를 보여줍니다.
추천 상품 목록을 못 가져오면? 인기 상품 목록으로 대체합니다.
사용자는 일부 기능이 제한될 뿐 서비스는 계속 이용할 수 있습니다. 폴백이 없으면 어떻게 될까요?
하나의 서비스가 장애가 나면 그 서비스를 호출하는 모든 기능이 멈춥니다. 상품 이미지를 못 불러온다고 주문이 안 된다면?
리뷰를 못 불러온다고 상품 페이지가 안 뜬다면? 사용자는 화면에서 에러만 보고 떠나버립니다.
넷플릭스의 사례를 봅시다. 추천 알고리즘이 느려지거나 장애가 나도 넷플릭스는 멈추지 않습니다.
대신 인기 콘텐츠를 보여줍니다. 완벽하지는 않지만 서비스는 계속됩니다.
이것이 회복탄력성입니다. 바로 이런 탄력성을 구현하기 위해 폴백이 필요합니다.
Feign에서 폴백을 구현하는 방법을 봅시다. @FeignClient의 fallback 속성에 폴백 클래스를 지정합니다.
이 클래스는 원래 인터페이스를 구현해야 합니다. 에러가 발생하면 Feign이 자동으로 폴백 구현을 호출합니다.
위의 코드를 살펴보겠습니다. ProductClientFallback 클래스는 ProductClient 인터페이스를 구현합니다.
getProduct() 메서드에서는 실제 API 호출 대신 기본값을 반환합니다. "상품 정보를 불러올 수 없습니다"라는 메시지와 함께 available을 false로 설정합니다.
주문 서비스는 available이 false면 "상품 정보를 확인할 수 없지만 주문은 진행합니다"라고 사용자에게 알릴 수 있습니다. 주문 자체는 차단하지 않습니다.
일부 기능이 제한될 뿐입니다. 더 정교한 폴백도 가능합니다.
예를 들어 캐시에서 이전 데이터를 가져올 수 있습니다. 상품 정보는 자주 바뀌지 않으므로 10분 전 캐시 데이터를 보여줘도 괜찮을 수 있습니다.
RestClient에서는 onStatus()로 에러를 처리합니다. HTTP 상태 코드를 체크해서 다른 예외를 던질 수 있습니다.
404일 때는 ProductNotFoundException을, 5xx일 때는 ProductServiceException을 던집니다. 왜 이렇게 구분할까요?
404는 상품이 없는 것이므로 재시도해도 소용없습니다. 사용자에게 "상품을 찾을 수 없습니다"라고 알려야 합니다.
반면 500은 서버 오류이므로 재시도하면 성공할 수 있습니다. 또는 폴백 로직을 실행해야 합니다.
실제 현업에서는 에러 처리를 어떻게 할까요? 보통 ErrorDecoder를 커스터마이징합니다.
Feign이 HTTP 에러를 받으면 ErrorDecoder가 그것을 비즈니스 예외로 변환합니다. 예를 들어 400 에러에 특정 에러 코드가 있으면 InvalidProductException을 던지는 식입니다.
또한 Resilience4j 같은 라이브러리와 함께 사용합니다. Resilience4j는 Circuit Breaker, Retry, RateLimiter 같은 패턴을 제공합니다.
서비스가 계속 실패하면 Circuit이 열려서 빠르게 폴백으로 넘어갑니다. 일정 시간 후 자동으로 재시도합니다.
하지만 주의할 점도 있습니다. 폴백이 항상 좋은 것은 아닙니다.
중요한 데이터를 폴백으로 대체하면 안 됩니다. 결제 정보를 못 가져온다고 임의의 값을 넣으면 큰일 납니다.
폴백은 "없어도 서비스가 돌아가는" 데이터에만 적용해야 합니다. 또한 폴백이 실행되고 있다는 것을 모니터링해야 합니다.
폴백이 계속 실행되면 원본 서비스에 문제가 있다는 신호입니다. 알림을 받고 빨리 수정해야 합니다.
김개발 씨는 이제 중요한 API에는 폴백을 구현하고, 덜 중요한 API는 에러를 명확히 처리하도록 설계했습니다. 장애가 나도 서비스가 완전히 멈추지 않습니다.
에러 처리와 폴백을 제대로 구현하면 사용자 경험이 크게 개선됩니다. 완벽한 시스템은 없지만, 탄력적인 시스템은 만들 수 있습니다.
실전 팁
💡 - 폴백은 필수 데이터가 아닌 부가 데이터에만 사용하세요
- 폴백 실행을 모니터링하고 알림을 설정하세요
- Circuit Breaker와 함께 사용하면 효과가 배가됩니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Istio 설치와 구성 완벽 가이드
Kubernetes 환경에서 Istio 서비스 메시를 설치하고 구성하는 방법을 초급 개발자도 쉽게 이해할 수 있도록 실무 스토리와 비유로 풀어낸 가이드입니다. istioctl 설치부터 사이드카 주입까지 단계별로 학습합니다.
서비스 메시 완벽 가이드
마이크로서비스 간 통신을 안전하고 효율적으로 관리하는 서비스 메시의 핵심 개념부터 실전 도입까지, 초급 개발자를 위한 완벽한 입문서입니다. Istio와 Linkerd 비교, 사이드카 패턴, 실무 적용 노하우를 담았습니다.
Helm 마이크로서비스 패키징 완벽 가이드
Kubernetes 환경에서 마이크로서비스를 효율적으로 패키징하고 배포하는 Helm의 핵심 기능을 실무 중심으로 학습합니다. Chart 생성부터 릴리스 관리까지 체계적으로 다룹니다.
관찰 가능한 마이크로서비스 완벽 가이드
마이크로서비스 환경에서 시스템의 상태를 실시간으로 관찰하고 모니터링하는 방법을 배웁니다. Resilience4j, Zipkin, Prometheus, Grafana, EFK 스택을 활용하여 안정적이고 관찰 가능한 시스템을 구축하는 실전 가이드입니다.
Prometheus 메트릭 수집 완벽 가이드
Spring Boot 애플리케이션의 메트릭을 Prometheus로 수집하고 모니터링하는 방법을 배웁니다. Actuator 설정부터 PromQL 쿼리까지 실무에 필요한 모든 내용을 다룹니다.