본문 바로가기

SpringFramework/Spring Cloud (스터디)

Spring Cloud - (6장) 마이크로서비스 간의 커뮤니케이션

다양한 커뮤니케이션 스타일

마이크로 서비스 간의 다양한 커뮤니케이션 스타일을 식별하고 그것을 2차원으로 분류가 가능하다.

1] 동기식/비동기식 커뮤니케이션 프로토콜로 나눌 수 있다.

- 동기식은 Restful API 등 HTTP 프로토콜로 진행

- 비동기식은 Rabbit MQ, Apache Kafka와 같은 메시지 기반 마이크로 서비스를 통해 구현할 수 있다.

2] 단일 메시지 혹은 다수의 수신기 여부에 따라 다양한 커뮤니케이션 타입을 나눈다.

- 일대일 커뮤니케이션에서는 각 요청이 정확히 하나의 서비스 인스턴스에 의해 처리

- 일대다 커뮤니케이션에서는 각 요청이 다수의 다른 서비스에 의해 처리

스프링 클라우드를 사용한 동기식 통신

1. RestTemplate는 클라이언트가 RESTful 웹서비스를 사용할 때 항상 사용된다. 이때 @LoadBalenced를 사용하는데 넷플릭스 리본을 사용하도록 자동으로 구성되고 IP주소 대신 서비스 이름을 사용해 서비스 디스커버리를 활용할 수 있게 된다.

 

2. 리본은 클라이언트측 부하분산기로서 HTTP와 TCP 클라이언트의 행동을 제어하는 간단한 인터페이스를 제공

서비스 디스커버리 또는 서킷 브레이커와 같은 스프링 클라우드 구성 요소와 쉽게 통합할 수 있다.

 

3. 페인(Feign)은 선언적인 Rest Client로써 부하분산 및 서비스 디스커버리에서 데이터를 가져오기 위해 위에서 설명한 리본을 사용한다.

@FeignClient 어노테이션을 선언해서 인터페이스를 쉽게 사용할 수 있다.

리본을 사용한 부하분산

이름 기반 서비스 호출 클라이언트라고 한다. 서비스 디스커버리에 접속할 필요 없이 호스트 이름과 포트를 사용한 전체 주소 대신이름을 사용하여 다른 서비스를 호출한다.

(단, 주소 목록이 .yml 속성파일에 미리 정의되어야 사용가능)

리본 클라이언트를 사용해 마이크로 서비스 간 커뮤니케이션하기

고객에 선택한 제품 목록을 구매하기로 결정하면 POST 요청이 order-service에 전달되고, 아래의 prepare() 에서 처리해준다고 했을 때의 예제이다.

@Autowired
RestTemplate template;
	
@PostMapping
public Order prepare(@RequestBody Order order) {
    int price = 0;
    Product[] products = template.postForObject("http://product-service/ids", order.getProductIds(), Product[].class);
    Customer customer = template.getForObject("http://customer-service/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);
}

order -> product, customer, account

정적 부하 분산 컨피규레이션

order-service가 필요한 operation을 수행하려면 예제에 있는 모든 다른 마이크로서비스와 커뮤니케이션 해야한다.

ribbon.listOfServers 속성을 사용해 세 개의 다른 리본 클라이언트에 네트워크 주소를 설정해주어야 한다.

 

= 유레카 내의 서비스 디스커버리를 사용하지 않도록 해야함

server:
  port: 8090
  
account-service:
  ribbon:
    eureka:
      enabled: false
    listOfServers: localhost:8091
customer-service:
  ribbon:
    eureka:
      enabled: false
    listOfServers: localhost:8092
product-service:
  ribbon:
    eureka:
      enabled: false
    listOfServers: localhost:8093

= RestTemplate을 함꼐 사용하도록 spring-clout-starter-ribbon 프로젝트 의존성 추가

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-ribbon</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

- 다른 스프링 컨피규레이션 클래스에 @RibbonClients를 사용

- RestTemplate Bean을 등록하고 @LoadBalanced를 사용해 스프링 클라우드 구성요소와 상호작용이 가능하도록 해야한다.

 

아래의 예제는 SpringBoot Application main에 등록하였다.

@SpringBootApplication
@RibbonClients({
    @RibbonClient(name = "account-service"),
    @RibbonClient(name = "customer-service"),
    @RibbonClient(name = "product-service")
})
public class OrderApplication {
    @LoadBalanced
    @Bean
    RestTemplate restTemplate() {
        return new RestTemplate();
    }

    public static void main(String[] args) {
        new SpringApplicationBuilder(OrderApplication.class).web(true).run(args);
    }

    @Bean
    OrderRepository repository() {
        return new OrderRepository();
    }
}

페인(Feign) 클라이언트

넷플릭스에서 자체적으로 개발한 도구이다. 독립적인 REST 서비스 간의 커뮤니케이션을 즉시 제공하는 웹 서비스 클라이언트

@LoadBalanced를 사용하는 RestTemplate와 동일하지만, 조금 더 고급스러운(?) 방식으로 사용할 수 있다.

-> 애노테이션을 템플릿화 된 요청으로 처리해 동작하는 자바 HTTP 클라이언트 바인더

 

- 오픈 페인 클라이언트 사용시, 인터페이스를 만들고 애노테이션을 붙이면 된다.

- Feign은 필요한 모든 네트워크 주소를 가져오는 부하분산 HTTP 클라이언트를 제공하기 위해 리본 및 유레카와 통합

- 스프링 클라우드는 스프링 MVC 애노테이션과 스프링 웹에서 동일한 HTTP 메시지 변환기를 지원

애플리케이션에서 페인 사용하기

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-feign</artifactId>
</dependency>

 

main 또는 configuration Class에 @EnableFeignClients 애노테이션을 추가

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class OrderApplication {
    public static void main(String[] args) {
        new SpringApplicationBuilder(OrderApplication.class).web(true).run(args);
    }

    @Bean
    OrderRepository repository() {
        return new OrderRepository();
    }
// ...
}

페인 인터페이스 개발

- 애노테이션을 가진 인터페이스를 만들어 구성요소로 제공하는 것은 스프링 프레임워크의 표준이라고 한다..

 

- @FeignClient(name = "test-service")

=> name은 필수 속성이며, 서비스 디스커버리를 사용할 경우 호출되는 마이크로 서비스 이름에 해당 (그렇지 않은 경우 구체적인 네트워크 주소를 가지는 url 속성과 함께 사용됨)

 

- 클라이언트 인터페이스의 모든 메서드는 @RequestMapping 또는 구체적인 애노테이션인 @GetMapping, @PostMapping, @PutMapping을 이용해 특정 HTTP API 종단점과 연관 지을 수 있다. 아래 예제코드로 확인해보자.

@FeignClient(name = "account-service")
public interface AccountClient {
    @PutMapping("/withdraw/{accountId}/{amount}")
    Account withdraw(@PathVariable("accountId") Long id, @PathVariable("amount") int amount);
}

// FeignClient는 스프링 빈이기 때문에 Controller 빈에 주입이 가능하다.
@RestController
public class OrderController {
	private static final Logger LOGGER = LoggerFactory.getLogger(OrderController.class);
	private ObjectMapper mapper = new ObjectMapper();

	@Autowired
	OrderRepository repository;

	@Autowired // FeignClient 주입
	AccountClient accountClient;

	@Autowired // FeignClient 주입
	CustomerClient customerClient;

	@Autowired // FeignClient 주입
	ProductClient productClient;

    // ...
}

상속 지원

추상 REST 메서드 정의를 가지는 인터페이스를 생성할 수 있으며, 그 인터페이스는 컨트롤러 클래스에 의해 구현되거나 페인 클라이언트 인터페이스에 의해 확장될 수 있다.

/**
 * 일반적으로 우리가 사용하는 상속 지원 - 인터페이스를 구현
 */
public interface AccountService {
    @PostMapping
    Account add(@RequestBody Account account);

    @PutMapping
    Account update(@RequestBody Account account);
    //...
}

@RestController
public class AccountController implements AccountService {
    @Autowired
    AccountRepository repository;

    public Account add(@RequestBody Account account) {
        return repository.add(account);
    }

    public Account update(@RequestBody Account account) {
        return repository.update(account);
    }
    // ...
}

/**
 * FeignClient도 상속 지원이 가능하다.
 */
@FeignClient(name = "account-service")
public interface AccountClient extends AccountService {

}

@RestController
public class CustomerController implements CustomerService {
    @Autowired
    AccountClient accountClient;

    @Autowired
    CustomerRepository repository;
    // ...

    public Customer findByIdWithAccounts(@PathVariable("id") Long id) {
        List<Account> accounts = accountClient.findByCustomerId(id);
        Customer c = repository.findById(id);
        c.setAccounts(accounts);
        return c;
    }
    // ...
}

 

 

수동으로 클라이언트 생성하기

반대로 애노테이션을 사용하지 않고 페인 빌더 API를 이용해 페인 클라이언트를 수동으로 만들 수 있다. (사용자 정의할 수 있는 기능들이 있음) ...굳이?

AccountClient accountClient = Feign.builder().client( new OkHttpClient())
                .encoder( new JAXBEncoder())
                .decoder( new JAXBDecoder())
                .contract( new JAXBContract())
                .requestInterceptor(new BasicAuthRequestInterceptor("user","password"))
                .target(AccountClient.class, "http://account-service");

 

= configuration 속성을 사용에서 설정도 가능하다.

@FeignClient(name = "account-service", configuration = AccountConfiguration.class)
public interface AccountClient extends AccountService {

}

@Configuration
public class AccountConfiguration {

    @Bean
    public Contract feignContract() {
        return new JAXRSContract();
    }

    @Bean
    public Encoder feignEncoder() {
        return new JAXRSContract();
    }

    @Bean
    public Decoder feignDecoder() {
        return new JAXRSContract();
    }

    @Bean
    public BasicAuthRequestInterceptor basicAuthRequestInterceptor () {
        return new BasicAuthRequestInterceptor("user","password");
    }
}

 

 

= 스프링 클라우드는 스프링 빈을 선언해 다음의 속성을 재정의 하도록 지원

속성 설명
Decoder ResponseEntityDecoder 기본 제공
Encoder SpringEncoder 기본 제공
Logger Slf4j Logger 기본제공
Contract SpringMvcContract 기본 제공
Feign.Builder HystrixFeign.Builder 기본 제공
Client 리본을 사용할 경우 LoadBalancerFeignClient 제공
그렇지 않을 경우 기본 페인 클라이언트 사용
Logger.level 페인의 기본 로그 레벨을 결정. (NONE, BASIC, HEADERS, FULL)
Retryer 커뮤니케이션 장애를 대비한 재시도 알고리즘을 구현
ErrorDecoder HTTP 상태 코드를 애플리케이션 예외로 맵핑
Request.Options 요청의 읽기와 연결 타임아웃을 설정
Collection<RequestInterceptor> 요청의 데이터에 기반한 어떤 액션을 구현하는 RequestInterceptor의 등록된 목록

 

- 페인 클라이언트는 컨피규레이션 속성을 사용해 사용자 정의를 할 수 있다.

속성 적용 우선순위

    1. application.yml 의 설정

    2. @Configuration

우선순위를 변경하려면 feign.client.default-to-properties 속성을 false로 설정하면 된다.

참고자료

= 서적 - 마스터링 스프링 클라우드 제 6장

= https://www.notion.so/6-37645deeecad40799fd78a69f723268b