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가 있어서 헷갈렸다.
공부해 보니 핵심은 "호출하는 놈이 방패를 들어야 한다"는 것.
- Gateway: 외부에서 들어오는 요청을 받아서 내부 서비스로 던지는 역할. 내부 서비스가 죽었을 때 Gateway까지 죽지 않으려면 여기에 서킷 브레이커가 필수다. (주의: Gateway는 WebFlux 기반이라
reactor-resilience4j 라이브러리를 써야 함!)
- 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의 핵심인 것 같다.
앞으로는 기능 구현도 중요하지만, "이거 외부 서비스가 죽으면 어떻게 되지?"를 먼저 고민하는 습관을 들여야겠다.
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가 있어서 헷갈렸다.
공부해 보니 핵심은 "호출하는 놈이 방패를 들어야 한다"는 것.
reactor-resilience4j라이브러리를 써야 함!)feign.circuitbreaker.enabled=true)을 꼭 켜야 한다.3. Fallback: 에러 페이지 대신 빈 리스트라도 주자
서킷이 열렸을 때(Open) 그냥 500 에러를 뱉으면 사용자 경험이 최악이다.
Feign을 쓸 때
FallbackFactory를 적용해 봤는데, 이게 진짜 물건이다.단순
fallback클래스는 왜 실패했는지 알 수가 없는데,fallbackFactory는 Exception(원인)을 파라미터로 받을 수 있다.조회(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의 핵심인 것 같다.
앞으로는 기능 구현도 중요하지만, "이거 외부 서비스가 죽으면 어떻게 되지?"를 먼저 고민하는 습관을 들여야겠다.