본문 바로가기

백엔드 개발

일체형 아키텍처(monolithic architecture) 분리하기(Hystrix 도입기) - 2

반응형

일체형 아키텍처 분리하기(Hystrix 도입기) - 2

Circuit breaker(Hystrix) 도입기

개인 프로젝트를 MSA로 변환하면서 의존성을 가지는 컴포넌트에 대해서 Hystrix라는 라이브러리로 Circuit breaker patten을 적용했다. 그런데 필자가 이 패턴에 대해서 이해가 낮아서 시행착오를 겪었다. 그 과정을 이 글에서 소개해보고자 한다.

Hystrix와의 잘못된 만남

처음 histrix를 유저 로그 기록하는 서버에 도입했다. 유저가 특정 컨텐츠를 검색하면 accessToken과 검색 조건을 유저 로그 서버가 받아서 accessToken은 인증 서버로 보내서 인증 후 User Id를 응답받는 부분이 있었기 때문이다.
Hystrix의 존재를 알고 있고 Spring에 도입하는 설정도 간편하므로 큰 고려없이 도입했다.
유저 아이디의 경우 요청이 실패하면 보내줄 값이 마땅치 않으므로 fallback method는 구현하지 않고 그냥 ExceptionHandler로 Hystirx가 발생시키는 HttpStatusCodeException과 TimeoutException을 처리해줬다.

@Service
@NoArgsConstructor
@Slf4j
public class AuthService {
    private RestTemplate restTemplate;
    private Environment env;

    @Autowired
    public AuthService(RestTemplate restTemplate, Environment env) {
        this.restTemplate = restTemplate;
        this.env = env;
    }

    @HystrixCommand()
    public Long getUserId(String accessToken) {
        String apiuri = env.getProperty("withkid.api.uri");
        String apiPort = env.getProperty("withkid.api.userId.port");

        UriComponents uri = UriComponentsBuilder.fromUriString(apiuri).path("/userId").port(apiPort).build();

        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", accessToken);
        HttpEntity<String> entity = new HttpEntity<String>("param", headers);
        ResponseEntity<Long> res = restTemplate.exchange(uri.toUriString(), HttpMethod.GET, entity, Long.class);
        return res.getBody();
    }
}

위의 클래스가 AuthService라는 인증 서버에 요청을 보내는 부분이고 fallback 메소드를 구현하지 않았다.

@Slf4j
@ControllerAdvice
@RestController
public class ExceptionController {
    @ExceptionHandler({HttpStatusCodeException.class})
    public ResponseEntity<ErrorResponse> requestFailHandler(Exception e) {
        ErrorResponse body = ErrorResponse.builder().name(HttpStatusCodeException.class.getSimpleName())
                .msg("잘못된 요청으로 인한 실패").status(HttpStatus.NOT_FOUND).build();
        log.error("restTemplate의 요청이 실패함: msg : {}", e.getMessage());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body);
    }

    @ExceptionHandler({ConnectException.class})
    public ResponseEntity<ErrorResponse> connectFailHandler(Exception e) {
        ErrorResponse body = ErrorResponse.builder().name(ConnectException.class.getSimpleName())
                .msg("의존하는 서비스 컨포넌트의 연결이 실패했습니다.").status(HttpStatus.NOT_FOUND).build();
        log.error("restTemplate의 요청이 실패함: msg : {}", e.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body);
    }

    @ExceptionHandler({TimeoutException.class})
    public ResponseEntity<ErrorResponse> timeOutHandler(Exception e) {
        ErrorResponse body = ErrorResponse.builder().name(TimeoutException.class.getSimpleName())
                .msg("타임 아웃이 발생했습니다.").status(HttpStatus.REQUEST_TIMEOUT).build();
        log.error("restTemplate의 요청이 실패함: msg : {}", e.getMessage());
        return ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT).body(body);
    }
}

위는 ExceptionHandler인데 Hystrix에서 반환하는 HttpStatusCodeException과 TimeoutException을 처리해주고 있다.

위와 같이 적용했을 때는 꽤 뿌듯했다. 외부 서비스와의 통신에서 발생하는 에러들을 한번에 ExceptionHandler로 처리해줄 수 있기 때문이다. 그리고 트렌디한 기술을 사용한다는 스스로 대견함도 있었다.
그러나 얕은 생각으로 도입한 라이브러리는 내게 큰 고민을 안겨줬다.

얕은 생각 깊은 고민

다른 부분에서 Hystrix를 사용할 일이 있었기 때문이다. 바로 리소스 서버에 요청을 보내는 부분이었다. 이 때는 fallback method를 도입해야 했는데 개념에 혼란이 왔다.
왜 Hystirx를 사용하는지 의문이 생겼기 때문이다. 왜냐하면 이떄까지는 그저 Exception 처리를 쉽게해주고 Exception이 누적되면 Circuit Open해서 의존하는 서비스에 전혀 요청이 안 가게 해주는 용도로 알고 있었다. 정확히 어떤 상황이 장애 전파인지 이해하지를 못하고 있었다.

Circuit breaker(Hystrix)의 용도는 무엇인가?

우선 아래의 두 글에 정리가 잘 되어 있으니 정독을 권한다.
자세한 설명은 하지 않고 실제 예를 들면서 언급하고자 한다.
조대협의 블로그
기록은 재산이다

    @Bean
    public RestTemplate restTemplate(){
        HttpComponentsClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory();
        httpRequestFactory.setConnectTimeout(5000);
        httpRequestFactory.setReadTimeout(5000);
        return new RestTemplate(httpRequestFactory);
    }

요즘은 RestTemplate를 이용해서 http 요청을 보낸다. 위는 실제 개인프로젝트에서 사용하는 스프링 빈이다. ReadTimeOut과 ConnectTimeout 값을 설정하고 있다.
사실 이 부분 때문에 헷갈렸다. RestTemplate에서 적정한 시간 안에 TimeOutException을 반환해주면 장애 전파는 안 일어나는 것이 아닌가?하는 의문점이 있어서이다.
그러나 Hystrix는 전체 요청 대비 일정 비율이 실패한다면 Circuit Open 되어서 해당하는 서비스에 대해서는 아예 요청이 가지 않고 fallback method를 실행한다.
spring boot를 사용할 때 일반적으로 내장 톰캣을 사용하므로 하나의 요청에 대해서 하나의 스레드가 블록킹되는 것은 사실이다.
대량의 요청을 받는 서버에서 5초간 다량의 스레드가 점유되면서 요청들이 밀리며 장애 전파가 생길 수 있는 것이다. ( 비동기적인 처리에서도 마찬가지라고 생각한다. 결국 1 thread라도 여러 작업들을 스케줄링해야하기 때문이다. )
그리고 hystrix에서는 요청에 대해서 얼마나 장애가 발생했는지 모니터링할 수 있는 툴도 제공하고 있으므로 RestTemplate의 설정만 믿기보다는 훨씬 안정적인 운영을 할 수 있다.

결론은 났다. AuthService에 대해서도 fallback method를 도입해야한다.

    @HystrixCommand(fallbackMethod = "getInvalidID")
    public Long getUserId(String accessToken) {
        String apiuri = env.getProperty("withkid.api.uri");
        String apiPort = env.getProperty("withkid.api.userId.port");

        UriComponents uri = UriComponentsBuilder.fromUriString(apiuri).path("/userId").port(apiPort).build();

        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", accessToken);
        HttpEntity<String> entity = new HttpEntity<String>("param", headers);
        ResponseEntity<Long> res = restTemplate.exchange(uri.toUriString(), HttpMethod.GET, entity, Long.class);
        return res.getBody();
    }

    public Long getInvalidID(String accessToken) {
        return -1L;
    }

위와 같이 fallback method에서는 실존할 수 없는 ID값인 -1L을 반환해준다. 그리고 -1L을 반환받는 경우에는 AuthServerUnavailableException을 반환하도록 할 것이다.
authService.getUserId(accessToken)을 호출하는 부분이 많으므로 AOP로 처리해줄 것이다.
유의할 점은 fallback method가 아니라 HystrixCommand가 적용된 메소드에 대해서 포인트컷 표현식을 설정해줘야한다는 것이다.

@Aspect
@Component
public class userAspect {

    @Around("execution(public Long tk.withkid.userlog.service.AuthService.getUserId(*))")
    public Object throwAuthException(ProceedingJoinPoint point) throws Throwable {
        Object result = point.proceed();
        if (result.equals(-1L)) {
            throw new AuthServerUnavailableException("인증에 실패했습니다.");
        }
        return result;
    }
}

마치며

고백하자면 장애전파에 대응한다는 것을 잘 못 이해해서 다른 컴포넌트가 죽어도 무조건 정상적으로 작동하도록 보여야한다는 생각을 가지기도 했었다. 그래서 간단한 작업이 너무 오래걸리고 머리가 너무 아팠다.
의존하는 컴포넌트의 불능 상태에서 한 컴포넌트의 장애가 전체 시스템 혹은 장애가 일어난 컴포넌트에 의존하는 컴포넌트의 불능을 방지 해주는 것이 Hystrix의 주 목적이고 fallback method라는 것도 한시적인 방편이라는 것을 깨달았다.
최악의 상황을 피하고 시스템이 최소한의 조건에서 돌아가고 있다면 엔지니어가 빨리 복구를 하면 되는 것이다.
MSA라는 것은 클라우드 네이티브의 다른 말이기도 해서 빠르게 시스템 복구를 할 수 있을 것이라고 생각한다.

ps

아직 모르는 게 많습니다. 제 개념과 생각에 오류가 있다면 댓글로 지적해주시면 고치겠습니다.
시간내서 제 글을 읽어주셔서 감사합니다.

반응형