Spring Cloud: Netflix Hystrix - 프로젝트설정

2020. 1. 11. 23:31Spring Micro Services/Netflix Hystrix

반응형

Netflix Hystrix

예를 들어 앞선 주문서비스를 보면 사용자가 주문 요청을 하면 주문서비스에서 주문정보를 받고 결제서비스, 상품재고 서비스를 호출하여 결제 및
상품의 재고 처리를 하도록 서비스를 분리하였다. 하지만 만약에 결제 서비스에 장애가 발생하는 경우 이에 대한 처리 프로세스가 없었다. 이러한 장애
처리를 위해 Netflix의 Hystrix를 통하여 장애 인지 및 FallBack 구현을 통하여 장애에 대한 대응 로직을 처리 할 수 있다. 

그리고 Hystrix DashBoard를 통하여서 서비스 상태를 확인 할 수도 있다.

프로젝트설정
앞선 예제와 동일하게 메이븐 모듈 프로젝트를 생성한다.
아래의 순서대로 프로젝트를 생성한다.

  • discovery-service: Eureka Server
  • hystrix-dashboard: Hystrix DashBoard
  • order-service
  • payment-service
  • product-service

프로젝트작성
전체 적인 소스는 GitHub를 참고 하도록 하자.

서비스별로 중요한 부분만 살펴보도록 하자.

discovery-service

Eureka Server 로서 제공되는 서비스 등록


01. application.yml
server:
  port: ${PORT:8761}                
 
eureka:
  client:
    registerWithEureka: false       
    fetchRegistry: false
  server:
    waitTimeInMsWhenSyncEmpty: 0    

- 서비스 포트 8761로 설정



02. DiscoveryApplication.java
package com.roopy.services.discovery;

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

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

- @SpringBootApplication, @EnableEurekaServer 어노테이션 설정



hystrix-dashboard

서비스 모니터링을 위한 DashBoard 화면 제공 및 환경 설정


01. application.yml - 개별서비스 모니터링시
spring:  
  application:
    name: hystrix-turbine
    
server:
  port: ${PORT:9090}

eureka:
  client:
    serviceUrl:
      defaultZone: ${EUREKA_URL:http://localhost:8761/eureka/}  

turbine:
  aggregator:
    clusterConfig: ORDER-SERVICE,PAYMENT-SERVICE,PRODUCT-SERVICE
  appConfig: order-service,payment-service,product-service


02. application.yml - 전체서비스 모니터링시
spring:  
  application:
    name: hystrix-turbine
    
server:
  port: ${PORT:9090}

eureka:
  client:
    serviceUrl:
      defaultZone: ${EUREKA_URL:http://localhost:8761/eureka/}  

turbine:
  appConfig: order-service,payment-service,product-service
  clusterNameExpression: "'default'"


03. HystrixApplication.java 
package com.roopy.services.hystrix;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
import org.springframework.cloud.netflix.turbine.EnableTurbine;

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

- @SpringBootApplication, @EnableDiscoveryClient, @EnableHystrixDashboard, @EnableTurbine 어노테이션 설정

  

order-service

주문서비스에서 사용자가 주문요청을 하였는데 만약 결제서비스에 장애가 생겨서 정상적인 주문처리가 안되었을 경우 사용자에게는 주문이 실패하였다는 내용만 알려주면 되겠지만 

서비스 관점에서 봤을때는 사용자의 주문정보를 저장 해두어야 할 것이다. 따라서 걸제서비스 오류발생시 FallBack 메소드에는 주문상태로 임시로 저장하는 로직을 추가한다.
그리고 상품서비스 오류 발생시 FallBack 메소드에는 단순 사용자에게 전달할 메세지를 설정한다.


01. application.yml
server:
  port: ${PORT:8060}
  
eureka:
  client:
    serviceUrl:
      defaultZone: ${EUREKA_URL:http://localhost:8761/eureka/}

spring:  
  application:
    name: order-service
    
  datasource:
    hikari:
      connection-test-query: SELECT 1
      minimum-idle: 1
      maximum-pool-size: 5
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://localhost:5432/postgres
    username: postgres
    password: admin!@34
   
  jpa:
    database-platform: org.hibernate.dialect.PostgreSQLDialect
    show-sql: true
    properties:
      hibernate: 
        format_sql: true
        temp.use_jdbc_metadata_defaults: false
    hibernate:
      naming:
        implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl         
        
management:
  endpoints:
    web:
      exposure:
        include: "*"  


02. OrderApplication.java
package com.roopy.services.order;

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

@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public class OrderApplication {
	
	public static void main(String[] args) {
		SpringApplication.run(OrderApplication.class, args);
	}
	
	@LoadBalanced
	@Bean
	public RestTemplate getRestTemplate() {
		return new RestTemplate();
	}
	
}

- @SpringBootApplication, @EnableDiscoveryClient, @EnableCircuitBreaker 어노테이션 설정



03. OrderController.java
package com.roopy.services.order.controller;

import java.util.HashMap;
import java.util.LinkedHashMap;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.http.HttpStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.roopy.services.order.domain.Order;
import com.roopy.services.order.helper.IDGeneratorHelper;
import com.roopy.services.order.service.IOrderService;
import com.roopy.services.order.service.IPaymentService;
import com.roopy.services.order.service.IProductService;

@RestController
public class OrderController {
	
	@Autowired
	private IOrderService orderService;
	
	@Autowired
	private IPaymentService paymentService;
	
	@Autowired
	private IProductService productService;
	
	@Autowired
	private IDGeneratorHelper idGenerator;

	@RequestMapping(value = "/order", method = RequestMethod.POST)
	public HashMap<String,Object> order(HttpServletRequest request, HttpServletResponse response,
			@RequestBody Order order) throws Exception {
		
		// 주문결과
		HashMap<String,Object> retObj = new LinkedHashMap<String,Object>();
		
		// 주문ID생성
		String orderId = idGenerator.getOrderId();
		
		// 주문ID설정
		order.setOrderId(orderId);
		
		// 주문날짜설정
		order.setOrderDtm(orderId.substring(1));
		
		// 결제처리결과	
		HashMap<String,Object> paymentRetObj = paymentService.payment(order);
		
		// 결제처리결과코드
		int paymentResultCode = (int) paymentRetObj.get("code");
		
		// 주문정보저장
		if (paymentResultCode == 200) {
			// 상품재고수량 업데이트
			HashMap<String,Object> productRetObj = productService.updateProductQty(order);
			
			// 상품재고수량처리 결과 코드
			int productStockQtyUpdateResultCode = (int) productRetObj.get("code");
			
			if (productStockQtyUpdateResultCode == -1) {
				// 결제처리시 오류가 발생하더라도 이력을 남기기 위해서 주문대기상태로 저장한다.
				orderService.save(order);
				
				retObj.put("code", -1);
				retObj.put("msg", productRetObj.get("msg"));
				retObj.put("data", order);
				
				return retObj;
			}
			
			// 결제처리, 상품재고수량 업데이트 처리가 정상적으로 된경우 주문상태코드를 완료 변경 처리
			order.setOrderStatus("C");
			
			// 주문정보저장
			orderService.save(order);
		}
		else {
			// 결제처리시 오류가 발생하더라도 이력을 남기기 위해서 주문대기상태로 저장한다.
			orderService.save(order);
			
			retObj.put("code", -1);
			retObj.put("msg", paymentRetObj.get("msg"));
			retObj.put("data", order);
			
			return retObj;
		}
		
		// 최종주문정보 조회
		retObj.put("code", HttpStatus.SC_OK);
		retObj.put("msg", "[" + order.getOrderId() + "] 주문처리가 정상적으로 처리 되었습니다.");
		
		return retObj;
	}
	
	@RequestMapping(value = "/order/{orderId}", method = RequestMethod.POST)
	public Order findOrder(HttpServletRequest request, HttpServletResponse response, @PathVariable String orderId,
			@RequestParam HashMap<String, String> param) throws Exception {
		
		return orderService.find(orderId);
	}
}

- 서비스 호출 부분은 서비스 클래스에 호출 한다.



04. PaymentServiceImpl
package com.roopy.services.order.service.impl;

import java.util.HashMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.roopy.services.order.domain.Order;
import com.roopy.services.order.service.IPaymentService;

@Service
public class PaymentServiceImpl implements IPaymentService {
	
	private static final Logger LOGGER = LoggerFactory.getLogger(PaymentServiceImpl.class);
	
	@Autowired
	private RestTemplate restTemplate;
	
	@Override
	@HystrixCommand(commandKey = "order-service.payment", fallbackMethod = "paymentFallBack")
	public HashMap<String,Object> payment(Order order) throws Exception {
		HashMap<String,Object> retObj = new HashMap<String,Object>();
		
		HttpEntity<Order> ordRequest = new HttpEntity<>(order);
		ResponseEntity<String> response =  null;
		
		try {
			response = restTemplate.postForEntity("http://payment-service/payment", ordRequest, String.class);
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		retObj.put("code", response.getStatusCodeValue());
		retObj.put("data", response.getBody());
		
		return retObj;
	}
	
	/**
	 * 결재 오류 시 호출되는 메소드
	 * 
	 * @param order
	 * @return
	 */
	public HashMap<String,Object> paymentFallBack(Order order) {
		
		LOGGER.error("[" + order.getOrderId() + "] 주문 결제 서비스 처리 중 오류가 발생하였습니다.");
		
		HashMap<String,Object> retObj = new HashMap<String,Object>();
		
		retObj.put("code", -1);
		retObj.put("data", order);
		retObj.put("msg", "[" + order.getOrderId() + "] 주문 결제 서비스 처리 중 오류가 발생하였습니다.");
		
		return retObj;
	}

}

- payment 메소드의 @HystrixCommand를 설정하여서 장애 발생 처리를 할 수 있다.

  장애가 발생 하였을 시 정의된 속성중에 fallbackMethod에 선언한 메소드가 호출된다.
  위에서 말한대로 장애가 발생할 시 paymentFallBack에서는 사용자의 기본주문정보를 임시상태로 저장한다.


05. ProductServiceImpl
package com.roopy.services.order.service.impl;

import java.util.HashMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.roopy.services.order.domain.Order;
import com.roopy.services.order.service.IProductService;

@Service
public class ProductServiceImpl implements IProductService {
	
	private static final Logger LOGGER = LoggerFactory.getLogger(PaymentServiceImpl.class);
	
	@Autowired
	private RestTemplate restTemplate;

	@Override
	@HystrixCommand(commandKey = "order-service.updateProductQty", fallbackMethod = "productFallBack")
	public HashMap<String,Object> updateProductQty(Order order) throws Exception {
		HashMap<String,Object> retObj = new HashMap<String,Object>();
		
		HttpEntity<Order> ordRequest = new HttpEntity<>(order);
		ResponseEntity<Void> response =  null;
		
		try {
			response = restTemplate.postForEntity("http://product-service/products", ordRequest, Void.class);
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		retObj.put("code", response.getStatusCodeValue());
		retObj.put("data", order);
		
		return retObj;
	}
	
	/**
	 * 결재 오류 시 호출되는 메소드
	 * 
	 * @param order
	 * @return
	 */
	public HashMap<String,Object> productFallBack(Order order) {
		
		LOGGER.error("[" + order.getOrderId() + "] 상품재고 수량 업데이트 중 오류가 발생하였습니다.");
		
		HashMap<String,Object> retObj = new HashMap<String,Object>();
		
		retObj.put("code", -1);
		retObj.put("data", order);
		retObj.put("msg", "[" + order.getOrderId() + "] 상품재고 수량 업데이트 중 오류가 발생하였습니다.");
		
		return retObj;
	}

}

- 04.PaymentServiceImpl과 동일하게 처리된다.



payment-service

01. application.yml

server:
  port: ${PORT:8070}
  
eureka:
  client:
    serviceUrl:
      defaultZone: ${EUREKA_URL:http://localhost:8761/eureka/}

spring:  
  application:
    name: payment-service
    
  datasource:
    hikari:
      connection-test-query: SELECT 1
      minimum-idle: 1
      maximum-pool-size: 5
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://localhost:5432/postgres
    username: postgres
    password: admin!@34
   
  jpa:
    database-platform: org.hibernate.dialect.PostgreSQLDialect
    show-sql: true
    properties:
      hibernate: 
        format_sql: true
        temp.use_jdbc_metadata_defaults: false
    hibernate:
      naming:
        implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl     

management:
  endpoints:
    web:
      exposure:
        include: "*"  


02. PaymentApplication.java

package com.roopy.services.payment;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

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

- @SpringBootApplication, @EnableDiscoveryClient, @EnableCircuitBreaker 어노테이션 설정



product-service

01. application.yml

server:
  port: ${PORT:8090}
  
eureka:
  client:
    serviceUrl:
      defaultZone: ${EUREKA_URL:http://localhost:8761/eureka/}

spring:  
  application:
    name: product-service
        
  datasource:
    hikari:
      connection-test-query: SELECT 1
      minimum-idle: 1
      maximum-pool-size: 5
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://localhost:5432/postgres
    username: postgres
    password: admin!@34
   
  jpa:
    database-platform: org.hibernate.dialect.PostgreSQLDialect
    show-sql: true
    properties:
      hibernate: 
        format_sql: true
        temp.use_jdbc_metadata_defaults: false
    hibernate:
      naming:
        implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl     

management:
  endpoints:
    web:
      exposure:
        include: "*"  


02. ProductApplication.java

package com.roopy.services.product;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

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

- @SpringBootApplication, @EnableDiscoveryClient, @EnableCircuitBreaker 어노테이션 설정



설정확인

01. Eureka Server
모든 서비스 구동 후 http://localhost:8761 접속 한다.

- Application 영역에 4개의 서비스가 보이면 정상


02. Hystrix Dashboard

- 위와 같은 화면이 로딩 되면 정상



참고사이트

반응형

'Spring Micro Services > Netflix Hystrix' 카테고리의 다른 글

Spring Cloud: Netflix Hystrix - 테스트  (0) 2020.01.11