스프링 클라우드 마이크로서비스 아키텍처

단일 애플리케이션의 문제점

  • 비즈니스가 발전함에 따라 개복잡도가 증가합니다.
  • 특정 기능 수정이나 추가 시 전체 시스템 테스트와 재배포가 필요합니다.
  • 한 모듈의 문제가 전체 시스템 장애로 이어질 수 있습니다.
  • 여러 개발팀이 동시에 데이터를 관리할 경우 보안 취약점이 발생할 수 있습니다.
  • 모든 모듈이 동일한 기술로 개발되어 실제 상황에 맞는 최적의 기술 선택이 어렵습니다.
  • 모듈이 너무 복잡하여 직원 퇴사 시 업무 인수인계에 오랜 시간이 소요될 수 있습니다.

분산 시스템과 클러스터링

클러스터링: 단일 서버가 높은 동시성 데이터 요청을 처리할 수 없을 때, 여러 서버가 부하를 분담하는 방식(물리적 계층). 많은 수의 서버가 동일한 작업을 수행하여 부하를 분산시킵니다.

분산 시스템: 복잡한 문제를 여러 개의 간단한 하위 문제로 분해하고, 대규모 프로젝트 아키텍처를 여러 마이크로서비스로 나누어 협업하는 방식(소프트웨어 설계 계층). 거대한 작업을 여러 작은 단계로 분할하여 각자 다른 사람이 이를 수행하고 최종 결과를 통합하여 큰 요구사항을 구현합니다.

서비스 거버넌스의 핵심 구성 요소는 세 부분으로 이루어집니다: 서비스 제공자, 서비스 소비자, 레지스트리 센터.

분산 시스템 아키텍처에서 각 마이크로서비스는 시작 시 자신의 정보를 레지스트리 센터에 저장하며, 이를 서비스 등록이라고 합니다.

서비스 소비자는 레지스트리 센터에서 서비스 제공자의 네트워크 정보를 가져와 해당 정보를 통해 서비스를 호출하며, 이를 서비스 디스커버리라고 합니다.

스프링 클라우드의 서비스 거버넌스는 Eureka를 구현하며, Eureka는 Netflix가 개발한 REST 기반의 서비스 거버넌스 솔루션입니다. 스프링 클라우드는 Eureka를 통합하여 서비스 등록 및 서비스 디스커버리 기능을 제공하며, 스프링 부트로 구축된 마이크로서비스 애플리케이션과 쉽게 통합하여 사용할 수 있습니다.

스프링 클라우드 Eureka

  • Eureka Server: 레지스트리 센터
  • Eureka Client: 등록할 모든 마이크로서비스가 Eureka Client를 통해 Eureka Server에 연결하여 등록을 완료합니다.

Eureka Server 구현

  • 부모 프로젝트 생성, pom.xml
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.0.7.RELEASE</version>
</parent>

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <!-- JDK 9 이상에서 JAXB API 문제 해결 -->
  <dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.0</version>
  </dependency>

  <dependency>
    <groupId>com.sun.xml.bind</groupId>
    <artifactId>jaxb-impl</artifactId>
    <version>2.3.0</version>
  </dependency>

  <dependency>
    <groupId>com.sun.xml.bind</groupId>
    <artifactId>jaxb-core</artifactId>
    <version>2.3.0</version>
  </dependency>

  <dependency>
    <groupId>javax.activation</groupId>
    <artifactId>activation</artifactId>
    <version>1.1.1</version>
  </dependency>
</dependencies>

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>Finchley.SR2</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
  • 부모 프로젝트 하에 모듈 생성, pom.xml
<dependencies>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    <version>2.0.2.RELEASE</version>
  </dependency>
</dependencies>
  • 설정 파일 application.yml 생성, Eureka Server 관련 구성 추가
server:
  port: 8761
eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      defaultZone: http://localhost:8761/eureka/

속성 설명

  • server.port: 현재 Eureka Server 서비스 포트

  • eureka.client.register-with-eureka: 현재 Eureka Server 서비스를 클라이언트로 등록할지 여부

  • eureka.client.fetch-registry: 다른 Eureka Server 서비스 데이터를 가져올지 여부

  • eureka.client.service-url.defaultZone: 레지스트리 센터 접속 주소

  • 시작 클래스 생성

package com.example.cloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

주석 설명:

  • @SpringBootApplication: 해당 클래스가 스프링 부트 서비스의 진입점임을 선언
  • @EnableEurekaServer: 해당 클래스가 Eureka Server 마이크로서비스로서 서비스 등록 및 서비스 디스커버리 기능, 즉 레지스트리 센터임을 선언

Eureka Client 구현

  • 모듈 생성, pom.xml
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        <version>2.0.2.RELEASE</version>
    </dependency>
</dependencies>
  • 설정 파일 application.yml 생성, Eureka Client 관련 구성 추가
server:
  port: 8010
spring:
  application:
    name: data-service
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true

속성 설명:

  • spring.application.name: 현재 서비스가 Eureka Server에 등록될 이름

  • eureka.client.service-url.defaultZone: 레지스트리 센터 접속 주소

  • eureka.instance.prefer-ip-address: 현재 서비스의 IP를 Eureka Server에 등록할지 여부

  • 시작 클래스 생성

package com.example.cloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DataServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(DataServiceApplication.class, args);
    }
}
  • 엔티티 클래스
package com.example.cloud.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private long id;
    private String name;
    private int age;
}
  • 리포지토리 인터페이스
package com.example.cloud.repository;

import com.example.cloud.entity.User;

import java.util.Collection;

public interface UserRepository {
    public Collection<User> findAll();
    public User findById(long id);
    public void saveOrUpdate(User user);
    public void deleteById(long id);
}
  • 리포지토리 구현체
package com.example.cloud.repository.impl;

import com.example.cloud.entity.User;
import com.example.cloud.repository.UserRepository;
import org.springframework.stereotype.Repository;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

@Repository
public class UserRepositoryImpl implements UserRepository {

    private static Map<Long, User> userMap;

    static {
        userMap = new HashMap<>();
        userMap.put(1L, new User(1L, "홍길동", 25));
        userMap.put(2L, new User(2L, "김철수", 30));
        userMap.put(3L, new User(3L, "이영희", 28));
    }

    @Override
    public Collection<User> findAll() {
        return userMap.values();
    }

    @Override
    public User findById(long id) {
        return userMap.get(id);
    }

    @Override
    public void saveOrUpdate(User user) {
        userMap.put(user.getId(), user);
    }

    @Override
    public void deleteById(long id) {
        userMap.remove(id);
    }
}
  • 핸들러
package com.example.cloud.controller;

import com.example.cloud.entity.User;
import com.example.cloud.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Collection;

@RestController
@RequestMapping("/user")
public class UserHandler {
    @Autowired
    private UserRepository userRepository;

    @GetMapping("/findAll")
    public Collection<User> findAll() {
        return userRepository.findAll();
    }

    @GetMapping("/findById/{id}")
    public User findById(@PathVariable("id") long id) {
        return userRepository.findById(id);
    }

    @PostMapping("/save")
    public void save(@RequestBody User user) {
        userRepository.saveOrUpdate(user);
    }

    @PutMapping("/update")
    public void update(@RequestBody User user) {
        userRepository.saveOrUpdate(user);
    }

    @DeleteMapping("/deleteById/{id}")
    public void deleteById(@PathVariable("id") long id) {
        userRepository.deleteById(id);
    }
}

RestTemplate 사용법

RestTemplate이란?

RestTemplate은 스프링 프레임워크가 제공하는 REST 기반 서비스 구성 요소로, HTTP 요청 및 응답을 캡슐화하며 REST 서비스에 접근하는 다양한 방법을 제공하여 코드 개발을 간소화합니다.

RestTemplate 사용 방법

  1. Maven 프로젝트 생성, pom.xml

  2. 엔티티 클래스

package com.example.cloud.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private long id;
    private String name;
    private int age;
}
  1. 핸들러
package com.example.cloud.controller;

import com.example.cloud.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

import java.util.Collection;

@RestController
@RequestMapping("/api")
public class ApiHandler {
    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/findAll")
    public Collection<User> findAll() {
        return restTemplate.getForEntity("http://localhost:8010/user/findAll", Collection.class).getBody();
    }

    @GetMapping("/findAll2")
    public Collection<User> findAll2() {
        return restTemplate.getForObject("http://localhost:8010/user/findAll", Collection.class);
    }

    @GetMapping("/findById/{id}")
    public User findById(@PathVariable("id") long id) {
        return restTemplate.getForEntity("http://localhost:8010/user/findById/{id}", User.class, id).getBody();
    }

    @GetMapping("/findById2/{id}")
    public User findById2(@PathVariable("id") long id) {
        return restTemplate.getForObject("http://localhost:8010/user/findById/{id}", User.class, id);
    }

    @PostMapping("/save")
    public void save(@RequestBody User user) {
        restTemplate.postForEntity("http://localhost:8010/user/save", user, null).getBody();
    }

    @PostMapping("/save2")
    public void save2(@RequestBody User user) {
        restTemplate.postForObject("http://localhost:8010/user/save", user, null);
    }

    @PutMapping("/update")
    public void update(@RequestBody User user) {
        restTemplate.put("http://localhost:8010/user/update", user);
    }

    @DeleteMapping("/deleteById/{id}")
    public void deleteById(@PathVariable("id") long id) {
        restTemplate.delete("http://localhost:8010/user/deleteById/{id}", id);
    }
}
  1. 시작 클래스
package com.example.cloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

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

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

서비스 소비자 consumer

  • Maven 프로젝트 생성, pom.xml
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        <version>2.0.2.RELEASE</version>
    </dependency>
</dependencies>
  • 설정 파일 application.yml 생성
server:
  port: 8020
spring:
  application:
    name: consumer
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true
  • 시작 클래스 생성
package com.example.cloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

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

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
  • 핸들러
package com.example.cloud.controller;

import com.example.cloud.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

import java.util.Collection;

@RestController
@RequestMapping("/consumer")
public class ConsumerHandler {
    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/findAll")
    public Collection<User> findAll() {
        return restTemplate.getForEntity("http://localhost:8010/user/findAll", Collection.class).getBody();
    }

    @GetMapping("/findAll2")
    public Collection<User> findAll2() {
        return restTemplate.getForObject("http://localhost:8010/user/findAll", Collection.class);
    }

    @GetMapping("/findById/{id}")
    public User findById(@PathVariable("id") long id) {
        return restTemplate.getForEntity("http://localhost:8010/user/findById/{id}", User.class, id).getBody();
    }

    @GetMapping("/findById2/{id}")
    public User findById2(@PathVariable("id") long id) {
        return restTemplate.getForObject("http://localhost:8010/user/findById/{id}", User.class, id);
    }

    @PostMapping("/save")
    public void save(@RequestBody User user) {
        restTemplate.postForEntity("http://localhost:8010/user/save", user, null).getBody();
    }

    @PostMapping("/save2")
    public void save2(@RequestBody User user) {
        restTemplate.postForObject("http://localhost:8010/user/save", user, null);
    }

    @PutMapping("/update")
    public void update(@RequestBody User user) {
        restTemplate.put("http://localhost:8010/user/update", user);
    }

    @DeleteMapping("/deleteById/{id}")
    public void deleteById(@PathVariable("id") long id) {
        restTemplate.delete("http://localhost:8010/user/deleteById/{id}", id);
    }
}

서비스 게이트웨이

스프링 클라우드는 Zuul 구성 요소를 통합하여 서비스 게이트웨이를 구현합니다.

Zuul이란?

Zuul은 Netflix가 제공하는 오픈소스 API 게이트웨이 서버로, 클라이언트와 웹사이트 백엔드 간의 모든 요청 중간 계층 역할을 합니다. API를 하나의 통합 진입점으로 노출하여 서버 측의 구체적인 구현 로직을 숨기고, 역방향 프록시 기능을 구현합니다. 내부에서 동적 라우팅, 인증, IP 필터링, 데이터 모니터링 등을 구현할 수 있습니다.

  • Maven 프로젝트 생성, pom.xml
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        <version>2.0.2.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        <version>2.0.2.RELEASE</version>
    </dependency>
</dependencies>
  • 설정 파일 application.yml 생성
server:
  port: 8030
spring:
  application:
    name: gateway
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
zuul:
  routes:
    data-service: /d/**

속성 설명:

  • zuul.routes.data-service: 서비스 제공자 data-service에 대한 매핑 설정

  • 시작 클래스 생성

package com.example.cloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@EnableZuulProxy
@EnableAutoConfiguration
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

주석 설명:

  • @EnableZuulProxy: @EnableZuulServer를 포함하며, 해당 클래스가 게이트웨이의 시작 클래스임을 설정

  • @EnableAutoConfiguration: 스프링 부트 애플리케이션이 현재 스프링 부트가 생성하고 사용하는 IoC 컨테이너에 모든 조건에 맞는 @Configuration 구성을 로드하도록 도움

  • Zuul은 기본적으로 로드 밸런싱 기능을 제공하며, data-service 코드를 수정합니다.

package com.example.cloud.controller;

import com.example.cloud.entity.User;
import com.example.cloud.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

import java.util.Collection;

@RestController
@RequestMapping("/user")
public class UserHandler {
    @Autowired
    private UserRepository userRepository;

    @Value("${server.port}")
    private String port;

    @GetMapping("/findAll")
    public Collection<User> findAll() {
        return userRepository.findAll();
    }

    @GetMapping("/findById/{id}")
    public User findById(@PathVariable("id") long id) {
        return userRepository.findById(id);
    }

    @PostMapping("/save")
    public void save(@RequestBody User user) {
        userRepository.saveOrUpdate(user);
    }

    @PutMapping("/update")
    public void update(@RequestBody User user) {
        userRepository.saveOrUpdate(user);
    }

    @DeleteMapping("/deleteById/{id}")
    public void deleteById(@PathVariable("id") long id) {
        userRepository.deleteById(id);
    }

    @GetMapping("/index")
    public String index() {
        return "현재 포트: " + this.port;
    }
}

Ribbon 로드 밸런싱

Ribbon이란?

스프링 클라우드 Ribbon은 로드 밸런싱 솔루션입니다. Ribbon은 Netflix가 발표한 로드 밸런서이며, 스프링 클라우드 Ribbon은 Netflix Ribbon을 기반으로 구현된 HTTP 요청을 제어하는 로드 밸런싱 클라이언트입니다.

레지스트리 센터에 Ribbon을 등록한 후, Ribbon은 라운드 로빈, 무작위, 가중 라운드 로빈, 가중 무작위 등과 같은 특정 로드 밸런싱 알고리즘을 기반으로 서비스 소비자가 인터페이스를 호출할 서비스 제공자를 자동으로 선택합니다. 개발자는 특정 요구사항에 따라 Ribbon 로드 밸런싱 알고리즘을 사용자 정의할 수 있습니다. 실제 개발에서 스프링 클라우드 Ribbon은 스프링 클라우드 Eureka와 함께 사용되며, Eureka Server는 호출 가능한 모든 서비스 제공자 목록을 제공하고 Ribbon은 특정 로드 밸런싱 알고리즘을 통해 호출할 특정 인스턴스를 선택합니다.

  • 모듈 생성, pom.xml
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        <version>2.0.2.RELEASE</version>
    </dependency>
</dependencies>
  • 설정 파일 application.yml 생성
server:
  port: 8040
spring:
  application:
    name: load-balancer
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true
  • 시작 클래스 생성
package com.example.cloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

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

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

@LoadBalanced: Ribbon 기반의 로드 밸런싱을 선언합니다.

  • 핸들러
package com.example.cloud.controller;

import com.example.cloud.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.Collection;

@RestController
@RequestMapping("/balance")
public class LoadBalancerHandler {
    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/findAll")
    public Collection<User> findAll() {
        return restTemplate.getForObject("http://data-service/user/findAll", Collection.class);
    }

    @GetMapping("/index")
    public String index() {
        return restTemplate.getForObject("http://data-service/user/index", String.class);
    }
}

Feign

Feign이란?

Ribbon과 마찬가지로 Feign도 Netflix가 제공합니다. Feign은 선언적, 템플릿 기반의 Web Service 클라이언트로, 개발자가 Web 서비스 클라이언트 작성을 간소화합니다. 개발자는 간단한 인터페이스와 애너테이션을 사용하여 HTTP API를 호출할 수 있습니다. 스프링 클라우드 Feign은 Ribbon과 Hystrix를 통합하여 플러그 가능, 애너테이션 기반, 로드 밸런싱, 서비스 서킷 차단 등 일련의 편리한 기능을 제공합니다.

Ribbon + RestTemplate 방식에 비해 Feign은 코드 개발을 크게 간소화합니다. Feign은 Feign 애너테이션, JAX-RS 애너테이션, 스프링 MVC 애너테이션 등을 지원하며, 스프링 클라우드는 Feign을 최적화하여 Ribbon과 Eureka를 통합하여 Feign 사용을 더욱 편리하게 만듭니다.

Ribbon과 Feign의 차이점

Ribbon은 일반적인 HTTP 클라이언트 도구이며, Feign은 Ribbon을 기반으로 구현됩니다.

Feign의 특징

  1. Feign은 선언적 Web Service 클라이언트입니다.
  2. Feign 애너테이션, 스프링 MVC 애너테이션, JAX-RS 애너테이션을 지원합니다.
  3. Feign은 Ribbon을 기반으로 구현되어 사용이 더 간단합니다.
  4. Feign은 Hystrix를 통합하여 서비스 서킷 차단 기능을 갖추고 있습니다.
  • 모듈 생성, pom.xml
<dependencies>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    <version>2.0.2.RELEASE</version>
  </dependency>

  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>2.0.2.RELEASE</version>
  </dependency>
</dependencies>
  • 설정 파일 application.yml 생성
server:
  port: 8050
spring:
  application:
    name: feign-client
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true
  • 시작 클래스 생성
package com.example.cloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class FeignClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(FeignClientApplication.class, args);
    }
}
  • 선언적 인터페이스 생성
package com.example.cloud.feign;

import com.example.cloud.entity.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.Collection;

@FeignClient(value = "data-service")
public interface FeignDataServiceClient {
    @GetMapping("/user/findAll")
    public Collection<User> findAll();

    @GetMapping("/user/index")
    public String index();
}
  • 핸들러
package com.example.cloud.controller;

import com.example.cloud.entity.User;
import com.example.cloud.feign.FeignDataServiceClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collection;

@RestController
@RequestMapping("/feign")
public class FeignHandler {

    @Autowired
    private FeignDataServiceClient feignDataServiceClient;

    @GetMapping("/findAll")
    public Collection<User> findAll() {
        return feignDataServiceClient.findAll();
    }

    @GetMapping("/index")
    public String index() {
        return feignDataServiceClient.index();
    }
}
  • 서비스 서킷 차단(application.yml에 서킷 차단 메커니즘 추가)
server:
  port: 8050
spring:
  application:
    name: feign-client
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true
feign:
  hystrix:
    enabled: true

feign.hystrix.enabled: 서킷 차단기 여부 설정

  • FeignDataServiceClient 인터페이스의 구현체 FeignError 생성,容错 처리 로직 정의, @Component 애너테이션을 사용하여 FeignError 인스턴스를 IoC 컨테이너에 주입
package com.example.cloud.feign.impl;

import com.example.cloud.entity.User;
import com.example.cloud.feign.FeignDataServiceClient;
import org.springframework.stereotype.Component;

import java.util.Collection;

@Component
public class FeignError implements FeignDataServiceClient {
    @Override
    public Collection<User> findAll() {
        return null;
    }

    @Override
    public String index() {
        return "서버 점검 중......";
    }
}
  • FeignDataServiceClient 정의 부분에서 @FeignClient의 fallback 속성을 통해 매핑 설정
package com.example.cloud.feign;

import com.example.cloud.entity.User;
import com.example.cloud.feign.impl.FeignError;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.Collection;

@FeignClient(value = "data-service", fallback = FeignError.class)
public interface FeignDataServiceClient {
    @GetMapping("/user/findAll")
    public Collection<User> findAll();

    @GetMapping("/user/index")
    public String index();
}

Hystrix 오류 처리 메커니즘

각 마이크로서비스 호출 관계를 변경하지 않은 상태에서 오류 상황에 대해 사전 처리합니다.

설계 원칙

  1. 서비스 격리 메커니즘
  2. 서비스 다운그레이드 메커니즘
  3. 서킷 차단 메커니즘
  4. 실시간 모니터링 및 경고 기능 제공
  5. 실시간 구성 수정 기능 제공

Hystrix 데이터 모니터링은 스프링 부트 Actuator와 함께 사용해야 하며, Actuator는 서비스의 상태, 데이터 통계를 제공하고 hystrix.stream 노드를 통해 모니터링 요청 데이터를 가져올 수 있으며, 시각화된 모니터링 인터페이스를 제공합니다.

  • Maven 프로젝트 생성, pom.xml
<dependencies>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    <version>2.0.2.RELEASE</version>
  </dependency>

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

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    <version>2.0.7.RELEASE</version>
  </dependency>

  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    <version>2.0.2.RELEASE</version>
  </dependency>

  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
    <version>2.0.2.RELEASE</version>
  </dependency>
</dependencies>
  • 설정 파일 application.yml 생성
server:
  port: 8060
spring:
  application:
    name: hystrix-monitor
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true
feign:
  hystrix:
    enabled: true
management:
  endpoints:
    web:
      exposure:
        include: 'hystrix.stream'
  • 시작 클래스 생성
package com.example.cloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
@EnableCircuitBreaker
@EnableHystrixDashboard
public class HystrixMonitorApplication {
    public static void main(String[] args) {
        SpringApplication.run(HystrixMonitorApplication.class, args);
    }
}

주석 설명:

  • @EnableCircuitBreaker: 데이터 모니터링 활성화 선언

  • @EnableHystrixDashboard: 시각화 데이터 모니터링 활성화 선언

  • 핸들러

package com.example.cloud.controller;

import com.example.cloud.entity.User;
import com.example.cloud.feign.FeignDataServiceClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collection;

@RestController
@RequestMapping("/monitor")
public class HystrixHandler {
    @Autowired
    private FeignDataServiceClient feignDataServiceClient;

    @GetMapping("/findAll")
    public Collection<User> findAll() {
        return feignDataServiceClient.findAll();
    }

    @GetMapping("/index")
    public String index() {
        return feignDataServiceClient.index();
    }
}
  • 시작 성공 후 http://localhost:8060/actuator/hystrix.stream에 접속하여 요청 데이터 모니터링 가능
  • http://localhost:8060/hystrix에 접속하여 시각화 모니터링 인터페이스 확인, 모니터링할 주소 노드를 입력하면 해당 노드의 시각화 데이터 모니터링을 볼 수 있습니다.

스프링 클라우드 설정 센터

스프링 클라우드 Config는 서버를 통해 여러 클라이언트에 설정 서비스를 제공합니다. 스프링 클라우드 Config는 설정 파일을 로컬에 저장할 수도 있고 원격 Git 저장소에 저장할 수도 있습니다. Config Server를 생성하여 모든 설정 파일을 관리합니다.

로컬 파일 시스템

  • Maven 프로젝트 생성, pom.xml
<dependencies>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-server</artifactId>
    <version>2.0.2.RELEASE</version>
  </dependency>
</dependencies>
  • application.yml 생성
server:
  port: 8762
spring:
  application:
    name: local-config-server
  profiles:
    active: native
  cloud:
    config:
      server:
        native:
          search-locations: classpath:/shared

주석 설명

  • profiles.active: 설정 파일 가져오기 방식

  • cloud.config.server.native.search-locations: 로컬 설정 파일 저장 경로

  • resources 경로에 shared 폴더 생성하고 해당 경로에 configclient-dev.yml 파일 생성

server:
  port: 8070
foo: foo 버전 1
  • 시작 클래스 생성
package com.example.cloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@SpringBootApplication
@EnableConfigServer
public class LocalConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(LocalConfigServerApplication.class, args);
    }
}

주석 설명

  • @EnableConfigServer: 설정 센터 선언

로컬 설정 센터의 설정 파일을 읽는 클라이언트 생성

  • Maven 프로젝트 생성, pom.xml
<dependencies>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
    <version>2.0.2.RELEASE</version>
  </dependency>
</dependencies>
  • bootstrap.yml 생성, 로컬 설정 센터 읽기 관련 정보 구성
spring:
  application:
    name: config-client
  profiles:
    active: dev
  cloud:
    config:
      uri: http://localhost:8762
      fail-fast: true

주석 설명

  • cloud.config.uri: 로컬 Config Server 접속 경로
  • cloud.config.fail-fast: 클라이언트가 Config Server 가져오기 정상 여부를 우선적으로 판단하도록 설정

spring.application.namespring.profiles.active를 결합하여 대상 설정 파일 이름 configclient-dev.yml을 만들고 Config Server에서 해당 파일을 찾습니다.

  • 시작 클래스 생성
package com.example.cloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class LocalConfigClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(LocalConfigClientApplication.class, args);
    }
}
  • 핸들러
package com.example.cloud.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/local")
public class LocalConfigHandler {

    @Value("${server.port}")
    private String port;

    @Value("${foo}")
    private String foo;

    @GetMapping("/index")
    public String index() {
        return this.port + "-" + this.foo;
    }
}

스프링 클라우드 Config 원격 설정

  • 설정 파일 생성 및 GitHub에 업로드
server:
  port: 8070
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
spring:
  application:
    name: config-client
  • Config Server 생성, 새 Maven 프로젝트 생성, pom.xml
<dependencies>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-server</artifactId>
    <version>2.0.2.RELEASE</version>
  </dependency>
</dependencies>
  • 설정 파일 application.yml 생성
server:
  port: 8888
spring:
  application:
    name: remote-config-server
  cloud:
    config:
      server:
        git:
          uri: https://github.com/username/springcloud-config.git
          searchPaths: config
          username: your-username
          password: your-password
      label: master
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
  • 시작 클래스 생성
package com.example.cloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@SpringBootApplication
@EnableConfigServer
public class RemoteConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(RemoteConfigServerApplication.class, args);
    }
}

Config Client 생성

  • Maven 프로젝트 생성, pom.xml
<dependencies>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
    <version>2.0.2.RELEASE</version>
  </dependency>

  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    <version>2.0.2.RELEASE</version>
  </dependency>
</dependencies>
  • bootstrap.yml 생성
spring:
  cloud:
    config:
      name: config-client
      label: master
      discovery:
        enabled: true
        service-id: remote-config-server
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

주석 설명

  • spring.cloud.config.name: 현재 서비스가 Eureka Server에 등록된 이름, 원격 저장소의 설정 파일 이름과 대응

  • spring.cloud.config.label: Git Repository의 분기

  • spring.cloud.config.discovery.enabled: Config 서비스 발견 지원 활성화 여부

  • spring.cloud.config.discovery.service-id: 설정 센터가 Eureka Server에 등록된 이름

  • 시작 클래스 생성

package com.example.cloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class RemoteConfigClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(RemoteConfigClientApplication.class, args);
    }
}
  • 핸들러
package com.example.cloud.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/hello")
public class HelloHandler {

    @Value("${server.port}")
    private String port;

    @GetMapping("/index")
    public String index() {
        return this.port;
    }
}

서비스 추적

스프링 클라우드 Zipkin

Zipkin은 분산 시스템에서 요청 데이터를 수집하고 추적할 수 있는 구성 요소로, 개발자가 각 마이크로서비스에서 요청이 소요된 시간 등을 더 직관적으로 모니터링할 수 있도록 합니다. Zipkin: Zipkin Server, Zipkin Client.

Zipkin Server 생성

  • Maven 프로젝트 생성, pom.xml
<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>io.zipkin.java</groupId>
    <artifactId>zipkin-server</artifactId>
    <version>2.9.4</version>
  </dependency>
  <dependency>
    <groupId>io.zipkin.java</groupId>
    <artifactId>zipkin-autoconfigure-ui</artifactId>
    <version>2.9.4</version>
  </dependency>
</dependencies>
  • 설정 파일 application.yml 생성
server:
  port: 9090
  • 시작 클래스 생성
package com.example.cloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import zipkin.server.internal.EnableZipkinServer;

@SpringBootApplication
@EnableZipkinServer
public class ZipkinServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZipkinServerApplication.class, args);
    }
}

주석 설명

  • @EnableZipkinServer: Zipkin Server 시작 선언

Zipkin Client 생성

  • Maven 프로젝트 생성, pom.xml
<dependencies>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
    <version>2.0.2.RELEASE</version>
  </dependency>
</dependencies>
  • 설정 파일 application.yml 생성
server:
  port: 8090
spring:
  application:
    name: zipkin-client
  sleuth:
    web:
      client:
        enabled: true
    sampler:
      probability: 1.0
  zipkin:
    base-url: http://localhost:9090/
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

속성 설명

  • spring.sleuth.web.client.enabled: 요청 추적 활성화 설정

  • spring.sleuth.sampler.probability: 샘플링 비율 설정, 기본값은 1.0

  • spring.zipkin.base-url: Zipkin Server 주소

  • 시작 클래스 생성

package com.example.cloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ZipkinClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZipkinClientApplication.class, args);
    }
}
  • 핸들러
package com.example.cloud.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/trace")
public class ZipkinHandler {

    @Value("${server.port}")
    private String port;

    @GetMapping("/index")
    public String index() {
        return this.port;
    }
}

태그: spring-cloud microservices Eureka zuul hystrix

6월 8일 20:00에 게시됨