본문 바로가기
프로그래밍/Spring Cloud

[MSA] Spring Cloud Hystrix - 실습편

by 사바라다 2020. 1. 8.

안녕하세요. 저번 포스팅에 이어서 오늘도 Hystrix를 이어서 알아보도록 하겠습니다. 이번 시간에는 저번 시간의 내용을 코드를 통해 구현해보는 시간을 가지도록 하겠습니다.

의존성

gradle 기준으로 저는 2.1.0.RELEASE를 사용합니다.

build.gradle 파일에 아래와 같이 주입합니다.

    implementation("org.springframework.cloud:spring-cloud-starter-netflix-hystrix:2.1.0.RELEASE")

Hello Histrix

가장 먼저 "Hello Histrix"를 찍어보도록 하겠습니다.

// HystrixCommand는 annotation을 import 하지않도록 주의하자.
import com.netflix.hystrix.HystrixCommand;  
import com.netflix.hystrix.HystrixCommandGroupKey;

public class HelloHystrix extends HystrixCommand<String> {

    private String name;

    public HelloHystrix(String name) {
        // HystrixCommandGroupKey : Hystrix 통계에서 사용하는 구별 Key
        super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
        this.name = name;
    }

    @Override
    protected String run() throws Exception {
        System.out.println("Thread.currentThread().getName() = " + Thread.currentThread().getName());
        return "Hello " + name;
    }
}

```java
public class HelloHystrixTest {
    @Test
    public void unitTest() {
        System.out.println("Thread.currentThread().getName() = " + Thread.currentThread().getName());
        assertThat(new HelloHystrix("Hystrix").execute(), is("Hello Hystrix"));
    }
}

위와 같이 코드를 작성 한 후 테스트를 돌려 확인해보도록 하겠습니다. 테스트 코드는 아래와 같이 작성하였습니다. 이렇게 코드를 테스트 했을 때 로그를 확인해보도록 하겠습니다. 일단 unitTest method에서 찍은 현재 thread의 이름은 Thread.currentThread().getName() = Test worker으로 출력되었습니다. 그렇다면 Hystrix 내부에서 찍은 로그에는 어떻게 찍혔을까요? Thread.currentThread().getName() = hystrix-ExampleGroup-1 main thread와는 다른 별도의 thread에서 돌아간 것을 알 수 있었습니다.

fallback

이번에는 오류를 발생시켜 fallback method를 발생시켜보도록 하겠습니다. fallback은 빠른 실패의 결과로 graceful shutdown을 할 수 있게 해주는 방법입니다. Exception 발생 시 호출 되는 method로 쉽게 커스터마이징 할 수 있습니다.

  • graceful shutdown : Transaction의 Rollback처럼 깔끔하게 정리되며 종료되는 것
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;

public class HelloHystrix extends HystrixCommand<String> {

    private String name;

    public HelloHystrix(String name) {
        super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup")); // Hystrix 통계에서 사용하는 구별 Key
        this.name = name;
    }

    @Override
    protected String run() throws Exception {
        System.out.println("Thread.currentThread().getName() = " + Thread.currentThread().getName());

        if(name.equals("exception")) {
            // exception 발생
            throw new RuntimeException("sabarada exception");
        }

        return "Hello " + name;
    }

    // fallback method
    @Override
    protected String getFallback() {
        return "Hello sabarada exception";
    }
}
public class HelloHystrixTest {
    @Test
    public void unitTest_fail() {
        assertThat(new HelloHystrix("exception").execute(), is("Hello sabarada exception"));
    }
}

HelloHystrix의 내용이 조금 변경 되었습니다. 먼저 getFallback이라는 method를 오버라이드해서 만듭니다. 해당 method는 RuntimeException이 발생했을때 호출 되는 fallback method입니다.

그리고 run() method를 보시면 만약 생성자로 들어온 name 변수가 exception 이면 RuntimeException을 발생시키는 것으로 만들어 두었습니다. 실행해 보면 아래와 같은 결과를 얻을 수 있습니다.

09:21:38.351 [hystrix-ExampleGroup-1] DEBUG com.netflix.hystrix.AbstractCommand - Error executing HystrixCommand.run(). Proceeding to fallback logic ...

Hystrix 내부의 DEBUG로그에 runtime에 에러가 발생했고 fallback method를 호출 한다라는 메시지가 출력되며 실제로 위의 테스트가 통과함을 알 수 있었습니다.

hystrix cache

이전 포스팅을 통해 hystrix flow를 보시면 Hystrix Instance를 실행 후 가장 먼저 확인하는게 이전 번에 동일하게 실행 된 적이 있는지 판단하여 cache hit를 확인하는 것입니다. 만약 cache에 hit한다면 로직을 실행할 필요 없이 바로 해당 결과값을 리턴하는 것입니다.

참고로 저는 아래 코드를 통해 hystrix는 ConcurrentHashMap을 cache로 사용하고 있다는 것을 알 수 있었습니다.

public class HystrixRequestCache {
    ...
private final static ConcurrentHashMap<RequestCacheKey, HystrixRequestCache> caches = new ConcurrentHashMap<RequestCacheKey, HystrixRequestCache>();
    ...
}

그러면 cache를 한번 적용해보도록 하겠습니다. 코드는 아래와 같습니다. 위의 코드와 다른점은 getCacheKey라는 method를 오버라이드하여 사용했다는 것입니다. 하지만 테스트코드에는 차이가 있습니다. 바로 HystrixRequestContext를 사용했다는 것인데요. 이 Class는 Cache 등 단일 Thread 범위가 아닐 경우 사용합니다.

import org.springframework.context.annotation.Bean;

import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandProperties;

public class HelloHystrix extends HystrixCommand<String> {

    private String name;

    public HelloHystrix(String name) {
        super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup")); // Hystrix 통계에서 사용하는 구별 Key
        this.name = name;
    }

    @Override
    protected String run() throws Exception {
        System.out.println("Thread.currentThread().getName() = " + Thread.currentThread().getName());

        System.out.println("run method");

        if(name.equals("exception")) {
            throw new RuntimeException("sabarada exception");
        }

        return "Hello " + name;
    }

    @Override
    protected String getFallback() {
        System.out.println("fallback");
        return "Hello sabarada exception";
    }

    // cache key
    @Override
    protected String getCacheKey() {
        return name;
    }
}
    @Test
    public void unitTest_cache() {
        HystrixRequestContext context = HystrixRequestContext.initializeContext();
        try {
            String exception = new HelloHystrix("Hystrix").execute();
            String exception_1 = new HelloHystrix("Hystrix").execute();
            String exception_2 = new HelloHystrix("Hystrix").execute();
            String exception_3 = new HelloHystrix("Hystrix").execute();
        } finally {
            context.shutdown();
        }
    }

아래 테스트 코드를 실행해보면 실행로그에서 run method가 1번만 찍히는 것을 확인할 수 있으며, 그래서 우리는 cache가 적용되었구나라는 것을 알 수 있었습니다.

Annotation

Hystrix를 사용하려면 이렇게 HystrixCommand를 상속 받아야만 할까요? 아닙니다. 우리의 Spring Cloud Hystrix는 Annotation으로도 해당 기능을 제공하고 있습니다.

먼저 @EnableCircuitBreaker로 @HistrixCommand의 의존성을 추가해 준 후 아래와 같이 사용할 수 있습니다.

@SpringBootApplication
@EnableCircuitBreaker
public class RestConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(RestConsumerApplication.class, args);
    }
}
@Service
public class GreetingService {
    @HystrixCommand(fallbackMethod = "defaultGreeting")
    public String getGreeting(String username) {
        return new RestTemplate()
          .getForObject("http://localhost:9090/greeting/{username}", 
          String.class, username);
    }

    private String defaultGreeting(String username) {
        return "Hello Histrix!";
    }
}

위의 getGreeting에서는 HystrixCommand 객체가 만들어지고 실행됩니다. 그리고 RuntimeException 발생 시 defaultGreeting method가 실행됩니다. 처음 저희가 보았던 Hello Histrix와 다를 게 없는 method입니다.

@HystrixCommand는 몇가지 configuration을 해줄 수 있습니다. 그것을 간단히 살펴보면 아래와 같습니다.

  • fallbackMethod - method 실패시 실행할 fallback Method
  • commandProperties
    • HystrixCommand의 상세설정, 내용은 사이트 참조
  • threadPoolPropeties
    • threadPool 설정, 내용은 사이트 참조
  • ignoreExceptions - fallback method를 실행시키지 않을 RuntimeException

마무리

오늘은 이렇게해서 Hystrix의 기본사용법을 알아보았습니다. 저는 개인적으로 이렇게 사용하는 것도 중요하지만 내부에서 어떻게 돌아가는지 아는것도 중요하다고 생각합니다. Hystrix안에서 어떤 flow가 담겨 있고 일어 나는지 궁금하신 분들은 저번포스터를 확인해주시면 됩니다. 다음시간에는 Ribbon에 대해서 한번 알아보도록 하겠습니다.

감사합니다.

참조

https://www.baeldung.com/spring-cloud-netflix-hystrix

https://github.com/Netflix/Hystrix/wiki/How-To-Use

댓글