Skip to content

Discovery, Resilience4j에 대하여 #5

Description

@guswns3371

MSA (Discovery, Resilience4j)

MSA로 프로젝트를 전환하면서 가장 골치 아팠던 게 바로 '서비스 간 통신'과 '장애 처리'였다.
단순히 "Feign 써서 호출하면 되는 거 아냐?"라고 생각했다가, 트래픽이 몰리고 서비스 하나가 죽었을 때 전체가 마비되는 걸 경험하고 나서야 정신이 번쩍 들었다.

이번에 프로젝트를 진행하며 깊게 파보았던 Service Discovery의 개념부터, 우리 서비스를 지켜줄 Resilience4j 적용기까지 공부한 내용을 정리해 둔다.


1. Eureka vs Discovery: 뭘 써야 할까?

처음엔 무지성으로 @EnableEurekaClient를 붙였는데, 레퍼런스를 찾다 보니 @EnableDiscoveryClient를 더 권장한다는 글이 많았다. 무슨 차이인가 싶어 찾아보니 추상화의 차이였다.

Service Discovery: 서비스를 찾는 '개념'이자 인터페이스.
Eureka: 그 개념을 구현한 넷플릭스의 구현체.

결론:
@EnableDiscoveryClient를 쓰는 게 맞다. 그래야 나중에 인프라를 K8s(Kubernetes)로 옮기거나 Consul 같은 다른 툴로 갈아탈 때 코드 수정을 안 해도 된다. (요즘은 K8s를 쓰면 아예 코드에서 Discovery 관련 설정을 빼고, K8s Service 기능에 위임하는 게 트렌드라고 한다. 이건 나중에 더 공부해보는 걸로...)


2. Circuit Breaker는 '누가' 달아야 할까?

Gateway에도 Resilience4j가 있고, 내부 마이크로서비스(Feign)에도 Resilience4j가 있어서 헷갈렸다.
공부해 보니 핵심은 "호출하는 놈이 방패를 들어야 한다"는 것.

  1. Gateway: 외부에서 들어오는 요청을 받아서 내부 서비스로 던지는 역할. 내부 서비스가 죽었을 때 Gateway까지 죽지 않으려면 여기에 서킷 브레이커가 필수다. (주의: Gateway는 WebFlux 기반이라 reactor-resilience4j 라이브러리를 써야 함!)
  2. Service A (Feign): 서비스 B를 호출하는 입장. B가 응답을 안 주면 A의 스레드도 다 잠기게 된다. 내 서비스(A)를 지키기 위해 Feign Client에도 서킷 브레이커 설정(feign.circuitbreaker.enabled=true)을 꼭 켜야 한다.

3. Fallback: 에러 페이지 대신 빈 리스트라도 주자

서킷이 열렸을 때(Open) 그냥 500 에러를 뱉으면 사용자 경험이 최악이다.
Feign을 쓸 때 FallbackFactory를 적용해 봤는데, 이게 진짜 물건이다.

단순 fallback 클래스는 왜 실패했는지 알 수가 없는데,
fallbackFactory는 Exception(원인)을 파라미터로 받을 수 있다.

// 이렇게 하면 로그에 원인을 남길 수 있어서 디버깅할 때 천국임
log.error("상품 서비스 호출 실패. 원인: {}", cause.getMessage());
return Collections.emptyList(); // 일단 빈 화면이라도 보여줘서 메인 페이지는 살리자

조회(GET) API는 이렇게 빈 값이라도 내려주는 게 국룰인 듯하다.


4. Timeout과 Retry: 디테일이 생명

설정하다가 제일 헷갈렸던 게 TimeLimiter(Resilience4j)와 ConnectTimeout(HTTP Client)의 관계였다.
결론은 "논리적인 타임아웃(TimeLimiter)을 더 짧게 잡자"는 것.

네트워크가 끊어질 때까지 기다리는 것보다, 내가 정한 시간(예: 2초) 내에 답 없으면 그냥 서킷 브레이커가 끊어버리고 Fallback을 실행하는 게 훨씬 깔끔하다.

Retry(재시도) 주의사항
"실패하면 다시 시도하면 되지!"라고 단순히 생각했다가 큰일 날 뻔했다.

POST/PUT 요청: 결제 요청 같은 건 재시도하다가 중복 결제 될 수 있으니 절대 금지.
Retry Storm: 재시도 간격 없이 막 쏘면 죽어가는 서버 확인사살 하는 꼴이다. Exponential Backoff 설정으로 1초 -> 2초 -> 4초... 이렇게 점잖게 재시도하도록 설정해야 한다.


마치며

MSA는 단순히 "서비스를 쪼개는 것"이 끝이 아니었다.
"하나가 죽어도 다 같이 죽지 않게 격리(Bulkhead)하고, 우아하게 대처(Fallback)하는 것"이 진짜 MSA의 핵심인 것 같다.

앞으로는 기능 구현도 중요하지만, "이거 외부 서비스가 죽으면 어떻게 되지?"를 먼저 고민하는 습관을 들여야겠다.

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentation

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions