본문 바로가기
IT/기타

chapter 13. 서비스 탐구하기 (책. Spring in Action)

by eddie_factory 2022. 3. 12.
반응형

13.1 마이크로서비스 이해하기

단일 어플리케이션이란?
배포 가능한 하나의 JAR 나 WAR 파일로 개발된 어플리케이션

단일 애플리케이션 문제점
전체를 파악하기 어렵다 : 코드가 점점 더 많아질수록 애플리케이션에 있는 각 컴포넌트의 역할을 알기 어려워진다.

테스트가 더 어렵다 : 애플리케이션이 커지면서 통합과 테스트가 더 복잡해진다.

라이브러리 간의 충돌이 생기기 쉽다 : 애플리케이션의 한 기능에서 필요한 라이브러리 의존성이 다른 기능에서 필요한 라이브러리 의존성과 호환되지 않을 수 있다.

확장 시에 비효율적이다 : 시스템 확장을 목적으로 더 많은 서버에 애플리케이션을 배포해야 할 때는 애플리케이션의 일부가 아닌 전체를 배포해야 한다. 애플리케이션 기능의 일부만 확장하더라도 마찬가지다.

적용할 테크놀로지를 결정할 때도 애플리케이션 전체를 고려해야 한다 : 애플리케이션에 사용할 프로그래밍 언어, 런타임 플랫폼, 프레임워크, 라이브러리를 선택할 때 애플리케이션 전체를 고려하여 선택해야 한다.

프로덕션으로 이양하기 위해 많은 노력이 필요하다 : 애플리케이션을 한 덩어리로 배포하므로 프로덕션으로 이양하는 것이 더 쉬운 것처럼 보일 수 있다. 그러나 일반적으로 단일 애플리케이션은 크기와 복잡도 때문에 더 엄격한 개발 프로세스와 더욱 철두철미한 테스트가 필요하다.

 

단일 애플리케이션의 문제를 해결하기 위해 지난 수년동안 마이크로서비스 아키텍처가 발전하였다.

개별적으로 개발되고 배포되는 소규모의 작은 애플리케이션들로 애플리케이션을 만드는 방법이다. 마이크로서비스는 상호 협력하여 더 큰 애플리케이션의 기능을 제공한다.

 

마이크로서비스 아키텍처 특성

마이크로서비스는 쉽게 이해할 수 있다 : 다른 마이크로서비스와 협력할 때 각 마이크로서비스는 작으면서 한정된 처리를 수행한다. 따라서 마이크로서비스는 자신의 목적에만 집중하므로 더 이해하기 쉽다.

마이크로서비스는 테스트가 쉽다 : 크기가 작을수록 테스트가 쉬어지는 것은 분명한 사실이다.

마이크로서비스는 라이브러리 비호환성 문제가 생기지 않는다 : 각 마이크로서비스는 다른 마이크로서비스와 공유되지 않는 빌드 의존성을 가지므로 라이브러리 충돌 문제가 생기지 않는다.

마이크로서비스는 독자적으로 규모를 조정할 수 있다 : 만일 특정 마이크로서비스의 규모가 더 커야 한다면, 애플리케이션의 다른 마이크로서비스에 영향을 주지 않고 메모리 할당이나 인스턴스의 수를 더 크게 조정할 수 있다.

각 마이크로서비스에 적용할 테크놀러지를 다르게 선택할 수 있다 : 각 마이크로서비스에 사용할 프로그래밍 언어, 플랫폼, 프레임워크, 라이브러리를 서로 다른게 선택할 수 있다. 실제로 자바로 개발된 마이크로서비스가 C#으로 개발된 다른 마이크로서비스와 함께 동작하도록 할 수 있다.

마이크로서비스는 언제든 프로덕션으로 이양할 수 있다 : 마이크로서비스 아키텍처 기반으로 개발된 애플리케이션이 여러 개의 마이크로서비스로 구성 되었더라도 각 마이크로서비스를 따로 배포할 수 있다. 그리고 마이크로서비스는 작으면서 특정 목적에만 집중되어 있고 테스트하기 쉬우므로, 마이크로서비스를 프로덕션으로 이양하는 데 따른 노력이 거의 들지 않는다. 또한, 프로덕션으로 이양하는 데 필요한 시간도 수개월이나 수주 대신 수시간이나 수분이면 된다.

 

그러나 모든 어플리케이션이 마이크로서비스 아키텍처에 적합한 것은 아니다. 마이크로서비스 아키텍처는 분산 아키텍처이므로 네트워크 지연과 같은 문제들이 발생할 수 있다. 애플리케이션이 상대적으로 작거나 간단하다면 일단 단일 애플리케이션으로 개발하는 것이 좋다. 그리고 점차 규모가 커질 때마이크로서비스 아키텍처로 변경하는 것을 고려할 수 있다.

 

13.2 서비스 레지스트리 설정하기
스프링 클라우드는 큰 프로젝트이며, 마이크로서비스 개발을 하는 데 필요한 여러 개의 부속 프로젝트로 구성된다. 이중 하나가 스프링 넷플릭스이며, 이것은 넷플릭스 오픈 소스로부터 다수의 컴포넌트를 제공한다. 이 컴포넌트 중에 넷플릭스 서비스 레지스트리인 유레카(Eureka)가 있다.

 

유레카(Eureka) 란?
 - 마이크로서비스가 서로를 찾을 때 사용되는 서비스 레지스트리
유레카는 마이크로서비스 애플리케이션에 있는 모든 서비스의 중앙 집중 레지스트리로 작동한다.
유레카 자체도 마이크로서비스로 생각할 수 있으며, 더 큰 애플리케이션에서 서로 다른 서비스들이 서로를 찾는 데 도움을 주는 것이 목적이다.
이러한 유레카의 역할 때문에 서비스를 등록하는 유레카 서비스 레지스트리를 가장 먼저 설정하는 것이 좋다.

 

서비스 인스턴스가 시작될 때 해당 서비스는 자신의 이름을 유레카에 등록한다. some service 의 인스턴스는 여러 개 생성될 수 있다. 그러나 이것들은 모두 같은 이름으로 유레카에 등록된다.

어느 순간 다른 서비스(other service)가 some service를 사용해야 한다. 이때 some servvice라는 이름을 유레카에서 찾으면 유레카는 모든 some service 인스턴스 정보를 알려준다. 

 

특정 인스턴스를 매번 선택하는 것을 피하기 위해 클라이언트 측에서 동작하는 로드밸런싱 알고리즘을 적용하는 것이 가장 좋다. 바로 이때 사용 될 수 있는 것이 또 다른 넷플릭스 프로젝트인 리본이다.

 

some service의 인스턴스를 찾고 선택하는 것이 other service가 해야할 일이지만, 이것을 리본에게 맡길 수 있다.

 

클라이언트 측의 로드밸런서를 사용하는 이유

클라이언트 측의 로드밸런서인 리본은 중앙 집중화된 로드 밸런서에 비해 몇 가지 장점을 갖는다.각 클라이언트에 하나의 로컬 로드 밸런서가 있으므로 클라이언트의 수에 비례하여 로드 밸런서의 크기가 조정된다.서버에 연결도니 모든 서비스에 획일적으로 같은 구성을 사용하는 대신, 로드 밸런서는 각 클라이언트에 가장 적합한 로드 밸런싱 알고리즘을 사용하도록 구성 할 수 있다.

 

유레카 서비스 시작하기

유레카 서버를 사용하기 위해서는 몇 가지 의존성을 추가해야함.

<dependencies>
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
	</dependency>
</dependencies>
<properties>
	<spring-cloud.version>Hoxton.SR3</spring-cloud.version>
</properties>

<dependencyManagement>
	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-dependencies</artifactId>
			<version>${spring-cloud.version}</version>
			<type>pom</type>
			<scope>import</scope>
		</dependency>
	</dependencies>
</dependencyManagement>

유레카 서버 스타터 의존성과 스프링 클라우드 버전 의존성을 추가한다.

 

import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootAppliation
@EnableEurekaServer
public class ServiceRegistryApplication {

	public static void main(String[] args) {
    	SpringApplication.run(ServiceRegistryApplication.class, args);
    }
}

@EnableEurekaServer 애노테이션을 추가한다.

 

유레카 대시보드(localhost:8080)는 여러 가지 유용한 제공한다.

어떤 서비스 인스턴스가 유레카에 등록되었는지 알려준다. 아무 서비스도 등록되지 않았을때는 Application에 'No instances available' 메세지가 나타난다.

또한 REST API도 제공하므로 자신을 등록하거나 다른 서비스를 발견하기 위해 사용할 수 있다. 예를 들어, 유레카 REST 엔드포인트로 접속해 레지스트리의 모든 서비스 인스턴스를 확인 할 수 있다.

애플리케이션이 완전하게 구성되지 않았을 경우에는 30초에 한번씩 예외메시지를 콘솔에 출력한다.

 

13.2.1 유레카 구성하기

하나보다는 여러 개의 유레카 서버가 함께 동작하는 것이 안전하므로 유레카 서버들이 클러스터로 구성되는 것이 좋다. 왜냐하면 하나에 문제가 발생하더라도 단일 장애점은 생기지 않기 때문이다. 따라서 기본적으로 유레카는 다른 유레카 서버로부터 서비스 레지스트리를 가져오거나 다른 유레카 서버의 서비스로 자신을 등록하기도 한다.

프로덕션(실무 환경) 설정에서는 유레카의 고가용성이 바람직하지만, 개발 시에 두개 이상의 유레카 서버를 실행하는 것은 불필요하다. 

개발시 유레카 서버가 혼자임을 알수 있도록 application.yml을 설정한다.

server:
  port: 8761
  
eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: <http://$>{eureka.instance.hostname}:${server.port}/eureka/

port와 hostname 속성은 생략 가능하며, 지정하지 안을 경우 유레카가 환경 변수를 참고해 결정한다. 그러나 속성 값을 확실하게 알려주기 위해 지정하는 것이 좋다.

eureka.client.fetchRegistry와 eureka.client.registerWithEureka는 유레카와 상호 작용하는 방법을 알려주기 위해 다른 마이크로서비스에 설정할 수 있는 속성들이다.
두 속성을 false로 설정하면 연결되는 다른 마이크로 서비스가 없다는 의미이다. (기본값은 true)

eureka.client.serviceUrl 속성은 영역(zone) 이름과 이 영역에 해당하는 하나 이상의 유레카 서버 URL을 포함하며, 이 값은 Map에 저장된다. Map의 키인 defaultZone은 클라이언트가 자신이 원하는 영역을 지정하지 않았을 때 사용된다.

 

유레카의 서버포트 지정하기

개발 시에는 로컬 컴퓨터에서 다수의 애플리케이션이 8080을 사용하므로 port설정을 지정해서 사용 할 수 있다.

 

자체-보존 모드를 비활성화 시키기

eureka:
	...
  server:
    enable-self-preservation: false

일반적으로 세 번의 갱신 기간(또는 90초) 동안 서비스 인스턴스로부터 등록 갱신 요청을 받지 못하면 해당 서비스 인스턴스의 등록을 취소하게 된다. 그리고 만일 이렇게 중단되는 서비스의 수가 임계값을 초과하면 유레카 서버는 네트워크 문제가 생긴 것으로 간주하고 레지스트리에 등록된 나머지 서비스 데이터를 보존하기 위해 자체-보존 모드가 된다. 따라서 추가적인 서비스 인스턴스의 등록 취소가 방지된다.

개발 환경에서는 자체-보존 모드를 활성화하면 중단된 서비스의 등록이 계속 유지되어 다른서비스가 해당 서비스를 사용하려고 할 때 문제를 발생시킬 수 있기 때문에 false 로 설정한다. (실무 환경에서는 true)

 

13.2.2 유레카 확장하기

개발 시에는 단일 유레카 인스턴스가 편리하지만, 프로덕션으로 이양할 때는 고가용성을 위해 최소한 두개 이상의 유레카 인스턴스를 가져야 한다.

 

프로덕션 환경의 스프링 클라우드 서비스

server:
  port: 8761
  
eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
  server:
    enable-self-preservation: false

---
spring:
  profiles: eureka-1
  application:
    name: eureka-1

server:
  port: 8761

eureka:
  instance:
    hostname: eureka1.tacocloud.com

other:
  eureka:
    host: localhost
    port: 8762

---
spring:
  profiles: eureka-2
  application:
    name: eureka-2

server:
  port: 8762

eureka:
  instance:
    hostname: eureka1.tacocloud.com

other:
  eureka:
    host: localhost
    port: 8762

두 개 이상의 유레카 인스턴스를 구성하는 가장 쉽고 간당한 방법은 application.yml 파일에 지정하는 것이다. 

기본 프로파일 다음에 두 개의 프로파일인 eureka-1과 eureka-2가 구성되어 있으며, 각 프로파일에는 자신의 포트와 eureka.instance.hostname이 설정되어 있다.

 

13.3 서비스 등록하고 찾기

애플리케이션을 서비스 레지스트리 클라이언트로 활성화하기 위해서는 해당 서비스 애플리케이션의 pom.xml 파일에 유레카 클라이언트 스타터 의존성을 추가해야 한다.

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>
		spring-cloud-starter-netflix-eureka-client
	</artifactId>
</dependency>

 

13.3.1 유레카 클라이언트 속성 구성하기


서비스의 기본 이름인 UNKNOWN을 그대로 두면 유레카 서버에 등록되는 모든 서비스 이름이 같게 되므로 변경해야한다. 

spring:
    application:
        name: ingredient-service

그리고 충돌을 막기 위해 각 서비스 애플리케이션의 포트를 0으로 설정할 수 있다. (포트가 0이면 임의 포트로 지정 됨)

eureka:
        client:
        service-url:
            defaultZone: http://eureka1.tacocloud.com:8761/eureka/,
                         http://eureka2.tacocloud.com:8762/eureka/

위 처럼 설정하게 되면 유레카 서버에 등록 되도록 클라이언트가 구성된다.

이렇게 되면 첫 번째 유레카 서버에 등록을 시도하고, 실패하면 두번째에 피어(peer)로 지정된 유레카 서버의 레지스트리에 등록을 시도하게 된다. 실패했던 유레카 서버가 온라인이 되면 해당 서비스의 등록 정보가 포함된 피어 서버 레지스트리가 복제된다.

 

13.3.2 서비스 사용하기

스프링 클라우드의 유레카 클라이언트 지원에 포함된 리본 클라이언트 로드 밸런서를 사용하여 서비스 인스턴스를 쉽게 찾아 선택하고 사용할 수 있다.

유레카 서버에서 찾은 서비스를 선택 및 사용하는 방법
 - 로드 밸런싱된 RestTemplate
 - Feign에서 생성된 클라이언트 인터페이스

 

RestTemplate 사용해서 서비스 사용하기

public Ingredient getIngredientById(String ingredientId) {
  return rest.getForObject("<http://localhost:8080/ingredients/{id}>",
                           Ingredient.class, ingredientId);
}

 

RestTemplate이 생성되거나 주입되면 HTTP 요청을 수행하여 원하는 응답을 받을 수 있다. 다만 위 코드는 getForObejct()의 인자가 하드코딩 되어있다. 이는 유레카 클라이언트로 애플리케이션을 활성화했다면 로드 밸런싱된 RestTemplate 빈을 선언, @Bean과 @LoadBalanced 애노테이션을 메서드에 같이 지정해 해결 할 수 있다.

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
  return new RestTemplate();
}

 예를 들어, 우선 로드 밸런싱된 RestTemplate을 필요로 하는 빈에 주입한다.

@Component
public class IngredientServiceClient {

  private RestTemplate rest;

  public IngredientServiceClient(@LoadBalanced RestTemplate rest) {
    this.rest = rest;
  }

}

그 후 메서드에 호스트와 포트대신 서비스의 등록된 이름을 사용하도록 변경한다.

public Ingredient getIngredientById(String ingredientId) {
  return rest.getForObject(
      "<http://ingredient-service/ingredients/{id}>", 
      Ingredient.class, ingredientId);
}

 

WebClient로 서비스 사용하기
RestTemplate을 사용했던 것과 같은 방법으로 WebClient를 로드 밸런싱된클라이언트로 사용할 수 있다. 가장 먼저 @LoadBalanced 애노테이션이 지정된 WebClient.Builder 빈 메서드를 선언한다.

@Bean
@LoadBalanced
public WebClient.Builder webClientBuilder() {
  return WebClient.builder();
}

이후 로드 밸런싱된 WebClientBuilder를 필요로 하는 어떤 빈에도 주입 할 수 있다.

@Service
@Profile("webclient")
public class IngredientServiceClient {

  private WebClient.Builder wcBuilder;

  public IngredientServiceClient(@LoadBalanced WebClient.Builder wcBuilder) {
    this.wcBuilder = wcBuilder;
  }

  public Mono<Ingredient> getIngredientById(String ingredientId) {
    return wcBuilder.build()
            .get()
            .uri("http://ingredient-service/ingredients/{id}", ingredientId)
            .retrieve().bodyToMono(Ingredient.class);
  }

이 경우 로드 밸런싱된 RestTemplate처럼 호스트나 포트를 지정할 필요가 없다. 해당 서비스 이름이 URL에서 추출되어 유레카에서 서비스를 찾는데 사용된다.

 

Feign 클라이언트 인터페이스 정의하기
Feign은 REST 클라이언트 라이브러리이며, 인터페이스를 기반으로 하는 방법을 사용해서 REST 클라이언트를 정의한다.  스프링 데이터가 리퍼지터리 인터페이스를 자동으로 구현하는 것과 유사한 방법을 사용한다.

Feign을 사용 하려면 우선 의존성을 추가한다.

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

의존성을 추가해도 자동-구성으로 Feign이 활성화되지는 않는다. 구성 클래스 중 하나에 @EnableFeignClients 애노테이션을 추가해야 한다.

@Configuration
@EnableFeignClients
public class FeignClientConfig {
  
}

이 후 다음과 같이인퍼테이스만 정의 하면 된다.

@FeignClient("ingredient-service")
public interface IngredientClient {

  @GetMapping("/ingredients/{id}")
  Ingredient getIngredient(@PathVariable("id") String id);

  @GetMapping("/ingredients")
  Iterable<Ingredient> getAllIngredients();

}

@FeignClient 애노테이션을 사용하면 내부적으로 ingredient-service는 리본을 통해 찾게 된다.  RestTemplate, WebClient, Feign 클라이언트 인터페이스를 사용하면 클라이언트가 특정 호스트 이름이나 포트를 하드코딩하지 않고 유레카에 등록된 서비스를 사용할 수 있다.

 

요약
 - 스프링 클라우드 넷플릭스 자동-구성과 @EnableEurekaServer 애노테이션을 사용해서 넷플릭스 유레카 서비스 레지스트리를 쉽게 생성할 수 있다.
 - 다른 서비스가 찾을 수 있도록 마이크로서비스는 이름을 사용해서 자신을 유레카 서버에 등록한다.
 - 리본은 클라이언트 측의 로드 밸런서로 동작하면서 서비스 이름으로 서비스 인스턴스를 찾아 선택한다.
 - 리본 로드 밸런싱으로 처리되는 RestTemplate 또는 Feign에 의해 자동으로 구현되는 인터페이스를 사용해서 클라이언트 코드는 자신의 REST 클라이언트를 정의할 수 있다.
 - 로드 밸런싱된 RestTemplate, WebClient 또는 Feign 클라이언트 인터페이스 중 어느 것을 사용하더라도 서비스의 위치(호스트 이름과 포트)가 클라이언트 코드에 하드 코딩되지 않는다. 

반응형

댓글