피드시스템 안정성 향상기 파해치기
당근 피드시스템 안정성 향상기를 읽고 — 마이크로서비스인데 왜 연쇄장애가 날까?
참고한 아티클: 당근 피드시스템 안정성 향상기: 프로파일링부터 서킷브레이커까지
개선 사항 들어가기 전 가졌던 의문
피드시스템이 복잡한 의존성 구조라는 건 알겠는데, 왜 그중 하나라도 문제가 생기면 연쇄적으로 장애가 발생하는 구조일까?
마이크로서비스라면 느슨한 결합이어야 할 텐데, 왜 하나가 죽으면 같이 죽는 구조로 되어 있을까?
내 질문에 대한 답:
결론부터 정리하면, 연쇄장애의 본질은 이거였다.
배포 단위의 느슨한 결합 ≠ 런타임의 느슨한 결합
마이크로서비스로 쪼개면 개발과 배포는 분리된다. 하지만 피드를 만들 때는 여러 서비스를 동기로 호출하고 응답을 기다린다.
여기서 연쇄장애가 발생하는 실제 메커니즘:
- 추천 서비스(없어도 되는 선택 서비스)가 느려진다
- 피드 서버가 그 응답을 타임아웃까지 기다리며 자원을 점유한다
- 요청이 계속 쌓이면 피드 서버의 자원이 고갈된다
- 정작 멀쩡한 필수 서비스 요청까지 처리하지 못하고 전체가 다운된다
즉, 없어도 되는 선택 서비스 하나가 자원을 빨아먹어 전체를 죽이는 것이 진짜 문제였다.
메모리 동적 할당 최적화
왜 연결리스트를 안 썼을까?
처음엔 “재할당+복사가 싫으면 연결리스트를 쓰면 되지 않나?” 싶었다. 하지만 이 방법은 더 나쁘다.
이 최적화의 목적 자체가 할당 횟수를 줄여 GC 부담을 낮추기다. 그런데 연결리스트는:
- 원소 하나당 노드 객체를 매번 힙에 새로 할당 → 할당 횟수가 오히려 폭증
즉 “할당 줄이기”라는 목표를 정면으로 반대한다. 그래서 정답은 배열을 쓰되 초기 capacity를 지정해 재할당 자체를 없애는 것.
세 최적화의 공통된 뿌리
개선 사항의 원인이 사실상 하나다.
무분별한 객체 생성 → 메모리·GC 부담
장애 격리: 서킷브레이커(Circuit Breaker) 패턴
처음 가졌던 “왜 하나 죽으면 같이 죽지?”의 해답이 바로 이 패턴이다.
Spring에서 적용한다면?
처음엔 “빈이 상태를 들고 있다가 실패하면 상태를 바꾸고, 호출 시 그 상태를 확인하는 방식일까?”라고 생각했다. 방향은 비슷하지만 실제 표준 구현은 더 깔끔하다.
찾아본 방법
Spring Cloud Circuit Breaker 기준:
@CircuitBreaker애너테이션을 메서드에 붙이면 AOP 프록시가 호출을 감싸서 슬라이딩 윈도우로 실패율을 집계하고, 임계 초과 시 호출을 차단한다.- 즉 “빈 자신이 상태를 들고 호출 측이 확인”하는 게 아니라, 빈을 감싼 프록시/데코레이터가 가로채는 형태다.
1
2
3
4
5
6
7
8
9
10
@CircuitBreaker(name = "userProfile", fallbackMethod = "fallback")
public List<Feed> getProfileFeeds(Long userId) {
/*
...
*/
}
public List<Feed> fallback(Long userId, Throwable t) {
return latestFeeds(userId); // 추천 대신 최신순
}
모니터링을 해본적이 있나?
나는 부끄럽지만, 아직까지 모니터링을 제대로 해본 적이 없었다.
그래서 아티클을 읽을 때 이 부분이 가장 공감이 안갔다.
이 부분에 대해서는 한 달 뒤의 나에게 넘겨야 겠다.
한 달 뒤의 내가 보면?
100% 이해했다고는 못 하겠지만, 다른 서비스에 요청할 때의 주의사항과 동기적으로 영향이 갈 수밖에 없는 구조를 이해한 것이 가장 큰 수확이었다.
또한, 한 달 뒤의 내가 이 아티클과 정리 내용을 본다면 어떤 생각이 들지 너무 궁금했다.