들어가기
- 서비스 디스커버리를 사용하거나 사용하지 않고 서비스 간 커뮤니케이션에 스프링 RestTemplate 사용하기
- 리본 클라이언트 사용자 정의하기
- 리본 클라이언트에서 제공하는 주요 기능에 대한 설명
- 예를 들면, 리본 클라이언트, 서비스 디스커버리, 상속, zone 지원과 통합
다양한 커뮤니케이션 스타일
- 동기식/비동기식 커뮤니케이션 프로토콜
- 비동기식 커뮤니케이션의 핵심은 응답을 기다리는 동안 클라이언트의 스레드가 멈추지 않아도 된다.
- AMQP 프로토콜을 많이 사용한다.
- 하지만 여전히 동기방식의 HTTP 프로토콜을 많이 사용한다.
- 비동기식 커뮤니케이션의 핵심은 응답을 기다리는 동안 클라이언트의 스레드가 멈추지 않아도 된다.
- 단일 메시지 수신기 또는 다수의 수신기 여부에 따라 다양한 커뮤니케이션 타입을 나눌 수 있다.
- 일대일 커뮤니케이션에서는 각 요청이 정확히 하나의 서비스 인스턴스에 의해 처리
- 일대다 커뮤니케이션에서는 각 요청이 다수의 다른 서비스에 의해 처리될 수 있다.
스프링 클라우드를 사용한 동기식 통신
-
스프링 클라우드는 마이크로서비스 간의 커뮤니케이션 구현을 도와주는 구성 요소 집합 제공
- RestTemplate
- 클라이언트가 RESTful 웹 서비스를 사용할 때 항상 사용된다.
- @LoadBalanced 한정자를 사용한다
- Netflix Ribbon
- IP 주소 대신 서비스 이름을 사용해 서비스 디스커버리를 활용할 수 있게 된다.
- Ribbon은 클라이언트 측 부하 분산기로서 HTTP, TCP 클라이언트의 행동을 제어하는 간단한 인터페이스 제공
- 개발자에게 완전 투명(Transparent) 하다.
- 추상화 계층이 하부의 복잡한 것을 감추는 덕분에 추상화 계층의 스펙에 따른 사용자의 설정에 따라 시스템이 일관되게 동작한다.
- Feign
- Netflix OSS 스택에서 온 선언적인 REST Client이다.
- 페인은 부하 분산 및 서비스 디스커버리에서 데이터를 가져오기 위해 리본을 사용한다.
- @FeignClient를 사용하여 인터페이스에 선언할 수 있다.
리본을 사용한 부하 분산
- 리본의 주요 개념은 이름 기반 서비스 호출 클라이언트(named client)라고 말할 수 있다.
- 서비스 디스커버리에 접속할 필요 없이 호스트 이름과 포트를 사용한 전체 주소 대신에 이름을 사용해 다른 서비스 호출이 가능하다.
- 주소 목록이 application.yml 파일의 리본 컨피규레이션 설정에 제공 되어야 한다.
(리본)리본 클라이언트를 사용해 마이크로 서비스 간 커뮤니케이션하기
- 예제로 재품 구매하는 간단한 주문 시스템 개발
- 해당 예제는 서비스 디스커버리 사용없이 진행한다.
- 주문 서비스 -> 제품 서비스, 고객서비스, 계정 서비스
- 고객 서비스 -> 계정 서비스
(리본)정적 부하 분산 컨피규레이션
- ribbon과 web 의존성 추가
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-ribbon --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-ribbon</artifactId> <version>1.4.2.RELEASE</version> </dependency>
- OrderService의 application.yml
- OrderService는 모든 서비스와 커뮤니케이션을 해야 한다.
- 그래서 ribbon.listOfServers 속성을 사용해 세개의 다른 리본 클라이언트에 네트워크 주소를 설정해야 한다. ``` server: port: 8090
AccountService: ribbon: eureka: enabled: false listOfServers: localhost:8091 CustomerService: ribbon: eureka: enabled: false listOfServers: localhost:8092 ProductService: ribbon: eureka: enabled: false listOfServers: localhost:8093 ```
- OrderApplication.java
- 메인클래스나 다른 스프링 컨피규레이션 클래스에 @RibbonClients를 사용한다.
-
RestTemplate 빈을 등록하고 @LoadBalanced를 사용해 스프링 클라우드 구성요소와 상호작용이 가능하도록 해야 한다. ```java @SpringBootApplication @RibbonClients({ @RibbonClient(name = “account-service”), @RibbonClient(name = “customer-service”), @RibbonClient(name = “product-service”) }) public class OrderApplication {
public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); }
@LoadBalanced @Bean RestTemplate restTemplate() { return new RestTemplate(); }
@Bean OrderRepository repository() { return new OrderRepository(); }
} ``
(리본)다른 서비스 호출하기
- 컨트롤러는 다른 HTTP 종단점을 호출할 수 있도록 RestTemplate을 사용한다.
- IP address, Port 는 application.yml을 사용하며, 등록한 이름으로 호출할 수 있다.
- OrderController
@RestController public class OrderController { @Autowired OrderRepository repository; @Autowired RestTemplate template; @PostMapping("/orders") public Order prepare(@RequestBody Order order) { int price = 0; Product[] products = template.postForObject("http://ProductService/ids", order.getProductIds(), Product[].class); Customer customer = template.getForObject("http://CustomerService//customers/withAccounts/{id}", Customer.class, order.getCustomerId()); for (Product product : products) price += product.getPrice(); final int priceDiscounted = priceDiscount(price, customer); Optional<Account> account = customer.getAccounts().stream().filter(a -> (a.getBalance() > priceDiscounted)).findFirst(); if (account.isPresent()) { order.setAccountId(account.get().getId()); order.setStatus(OrderStatus.ACCEPTED); order.setPrice(priceDiscounted); } else { order.setStatus(OrderStatus.REJECTED); } return repository.add(order); } @PutMapping("/orders/{id}") public Order accept(@PathVariable Long id) { final Order order = repository.findById(id); template.put("http://AccountService/accounts/{id}/withdraw/{amount}", null, order.getAccountId(), order.getPrice()); order.setStatus(OrderStatus.DONE); repository.update(order); return order; } private int priceDiscount(int price, Customer customer) { double discount = 0; switch (customer.getType()) { case REGULAR: discount += 0.05; break; case VIP: discount += 0.1; break; default: break; } int ordersNum = repository.countByCustomerId(customer.getId()); discount += (ordersNum*0.01); return (int) (price - (price * discount)); } }
- mvn cliean install로 빌드
- java -jar로 모든 마이크로 서비스를 실행한다.
- 호출하여 테스트할 수 있다.
서비스 디스커버리와 함께 RestTemplate 개발하기
- 서비스 디스커버리와의 통합은 리본 클라이언트의 기본 행동이다.
- 서비스 디스커버리의 존재는 예제에서 서비스 간의 커뮤니케이션 중에 스프링 클라우드 구성 요소의 컨피규레이션을 간단하게 만든다.
예제 애플리케이션 개발
- 새로운 discover-service 모듈
- 리본 클라이언트와 관련된 모든 컨피규레이션과 애노테이션을 제거한 후, 제거된 리본 클라이언트는 @EnableDiscoveryClient를 사용해 유레카 디스커버리 클라이언트를 사용하는 것으로 대체하고 유레카 서버 주소를 application.yml에 제공
- OrderService.java
@SpringBootApplication @EnableDiscoveryClient public class OrderApplication { @LoadBalanced @Bean RestTemplate restTemplate() { return new RestTemplate(); } publi static void main(String[] args) { SpringApplication.run(DiscoveryServiceApplication.class, args); } }
- application.yml
spring: application: name: OrderService server: port: ${PORT:8090} eureka: client: serviceUrl: defaultZone: ${EUREKA_URL:http://localhost:8761/eureka/}
- 다른 서비스를 실행하면 Eureka 서버에서 확인이 가능하다.
- OrderService.java
- 기본으로 리본 클라이언트는 등록된 모든 마이크로 서비스의 인스턴스 간에 균등하게 트래픽을 분배한다.
- 해당 알고리즘은 라운드 로빈(round robbin) 이라 한다.
- 클라이언트가 마지막 요청을 보낸 곳을 기억하고 이번 요청을 다음 순서의 서비스로 전달하는 것
- 서비스 디스커버리가 아닌 다른 방식으로 재정의할 수 있다.
- 서비스 디스커버리 없이 ribbon.listOfServers에 ,로 분리된 서비스 주소 목록을 설정해 구헝할 수 있다.
Feign 클라이언트 사용하기
- RestTemplate 은 스프링 클라우드와 마이크로 서비스의 상호작용을 위해 도입된 스프링 구성 요소
- Netflix에서 REST 커뮤니케이션을 위해 독립적으로 개발한 웹 서비스 클라이언트가 Feign
- @LoadBalanced을 사용하는 RestTemplate으로 동일하지만, 애노테이션을 템플릿화된 요청으로 처리해 동작하는 HTTP 클라이언트 바인더이다.
- 페인 클라이언트는 서비스 디스커버리에서 모든 네트워크 주소를 가져오는 부하 분산 HTTP 클라이언트를 제공하기 위해 리본 및 유레카와 통합한다.
여러 존의 지원
- 여러 존을 구성하기 위해 서비스 디스커버리에서 유레카 사용
- zone 내부에서 여러 서비스가 커뮤니케이션 하는 구조
애플리케이션에서 페인 사용하기
- 프로젝트에 페인을 포함하기 위해서는 spring-cloud-starter-feign 아티팩트 또는 스프링 클라우드 넷플릭스를 위한 spring-cloud-starter-openfeign 의존성을 추가 해야 한다.
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-feign</artifactId> </dependency>
- Main에 @EnableFeignClients 애노테이션을 추가해 페인 사용
@SpringBootApplicatioin @EnableDiscoveryClient @EnableFeignClients public class OrderApplication { public static void main(Strings[] args) { // ... } }
- @FeignClient Interface 생성
@FeignClient(name="order-service") public interface OrderServiceClient { @GetMapping("/order-service/{userId}/orders") List<ResponseOrder> getOrders(@PathVariable String userId); }
- @FeignClient에 등록된 name은 Eureka에 등록된 서비스 네임을 사용
-
서비스에서 @FeignClient Interface를 가져와서 사용
- FeignClient 에서 로그 사용
- application.yml 에 로그 레벨 설정
logging: level: com.example.userservice.client: DEBUG
- Feign Logger 빈 등록
@Bean public Logger.Level feignLoggerLevel() { return Logger.Level.FULL; }
- application.yml 에 로그 레벨 설정
- Feign Client 예외 처리
- FeignException 처리
- 해당 방식으로 try-catch를 사용하면 orders에 대한 정보를 보여주지 않는다.
try { ordersList = orderServiceClient.getOrders(userId); } catch(FeignException e) { log.error(e.getMessage()); }
- 해당 방식으로 try-catch를 사용하면 orders에 대한 정보를 보여주지 않는다.
- FeignException 처리
- ErrorDecoder 를 활용한 예외 처리
- ErrorDecoder에 decode 메소드를 오버라이딩하여 구현한다.
public class FeignErrorDecoder implements ErrorDecoder { @Override public Exception decode(String methodKey, Response response) { // TODO Auto-generated method stub switch(response.status()) { case 400: break; case 404: if(methodKey.contains("getOrders")) { return new ResponseStatusException(HttpStatus.valueOf(response.status()), "User's order list is empty"); } break; default: return new Exception(response.reason()); } return null; } }
- ErrorDecoder 구현체를 빈으로 등록
@Bean public FeignErrorDecoder feignErrorDecoder() { return new FeignErrorDecoder(); }
- try-catch가 필요 없이 실행하면 된다.
/* ErrorDecoder를 통해 예외 처리*/ List<ResponseOrders> orderList = orderServiceClient.getOrders(id);
- FeignClient에 ErrorDecoder 매핑
@FeignClient(name = "order-service", configuration = FeignErrorDecoder.class) public interface OrderServiceClient {
- 여러 Feign Client가 생성될 수 있기 때문에 사용하는 ErrorDecoder를 지정할 수 있다.
- ErrorDecoder에 decode 메소드를 오버라이딩하여 구현한다.
- 데이터 동기화 문제
- 만약 Orders Service 2개 기동
- Users의 요청 분산 처리
- Orders 데이터도 분산 저장 -> 동기화 문제 발생
- 해결 방법
- 같은 서비스이기 때문에 하나의 데이터베이스 사용 -> 트랜잭션 관리가 필요하다.
- 각각의 데이터베이스 간의 동기화 -> Messaging Queing Server(Kafka, RabbitMQ 등) 를 활용하여 데이터 동기화를 한다.
- Kafka Connector + DB -> Order Service 간에 발생한 트랜잭션에 대해 Messaging Queing Server에 전달, Messaging Queing Server가 따로 거대한 단일 DB를 구성하여 저장
- 만약 Orders Service 2개 기동
FeignClient 헤더 추가
- Annotation 으로 헤더 정의하여 사용
@FeignClient(value="another-service", url="${external.another-service.url}", configuration=FeignErrorDecoder.class) public interface AnotherClientService { @GetMapping("/account", header="key1=value1") public List<Account> findAllAccount(); @GetMapping("/account/{accountId}") public Account findAccountById(@PathVariable String accountId, @RequestHeader("key") String value); @org.springframework.web.bind.annotation.PostMapping("/account") @feign.Headers("key:value") public Account saveAccount(@RequestBody RequestAccount account) }
- @feignHeaders를 활용하기 위해서는 Contract 를 feign 에서 제공해준 Default Contract 를 사용해야 한다.
- feign.Contract.Default 를 사용하는 실수
- 위에서 말씀드렸듯이 Contract 는 2가지 존재
- org.springframework.cloud:spring-cloud-starter-openfeign 을 사용하게 되면 SpringMvcContract 를 사용하게 되어 @GetMapping, @PostMapping, @RequestMapping 을 사용할 수 있습니다.
- Feign 에서 제공하는 Default 를 사용하게 되면, 위에 있는 것이 아닌 feign.RequestLine 을 사용해야 한다.
- Feign 에서 제공하는 Default Contract 를 사용하는데 @GetMapping 과 같은 것을 사용한다면 오류가 발생한다.
- RequestInterceptor
- RequestInterceptor는 공통으로 사용하는 header를 추가하여 사용
- RequestInterceptor
public interface RequestInterceptor { /** * Called for every request. Add data using methods on the supplied {@link RequestTemplate}. */ void apply(RequestTemplate template); }
- 구현
public class FeignClientInterceptor { @Bean public ReequestHeader requestHeader() { return requestTemplate -> requestTemplate.header("header1", "header2"); } }
- @FeignClient에 configuration 설정 필요
- BasicAuth인증
public class BasicAuthConfiguration { @Bean public BasicAuthRequestInterceptor basicAuthRequestInterceptor() { return new BasicAuthRequestInterceptor("mayaul", "1234567890"); } }
- RequestInterceptor
- RequestInterceptor는 공통으로 사용하는 header를 추가하여 사용
출처
- Mastering Spring Cloud 책
- Header 정리: https://techblog.woowahan.com/2630/ (우아한 feign 적용기)
- https://blog.leocat.kr/notes/2019/03/27/feign-open-feign-configuration