본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 22. · 2 Views
속도 제한과 벌크헤드 완벽 가이드
마이크로서비스 환경에서 시스템을 안정적으로 운영하기 위한 필수 패턴인 속도 제한과 벌크헤드를 실무 중심으로 알아봅니다. Resilience4j를 활용한 구체적인 구현 방법과 실전 팁을 제공합니다.
목차
1. @RateLimiter 사용
신입 개발자 김개발 씨가 처음으로 맡은 API 서버가 갑자기 느려지기 시작했습니다. 모니터링을 확인해보니 특정 사용자가 1초에 수천 건의 요청을 보내고 있었습니다.
팀장님께서 조용히 말씀하셨습니다. "속도 제한이 필요한 상황이네요."
@RateLimiter는 일정 시간 동안 허용되는 요청 횟수를 제한하는 어노테이션입니다. 마치 놀이공원 입구에서 한 번에 들어갈 수 있는 사람 수를 제한하는 것처럼, API 호출 횟수를 조절합니다.
이를 통해 서버 과부하를 방지하고 모든 사용자에게 공평한 서비스를 제공할 수 있습니다.
다음 코드를 살펴봅시다.
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/products")
public class ProductController {
// 초당 10개의 요청만 허용
@RateLimiter(name = "productService", fallbackMethod = "rateLimitFallback")
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
// 상품 조회 로직
return productService.findById(id);
}
// 속도 제한 초과 시 호출되는 메서드
private Product rateLimitFallback(Long id, Exception e) {
return Product.cached(id); // 캐시된 데이터 반환
}
}
김개발 씨는 입사 2개월 차 백엔드 개발자입니다. 회사의 전자상거래 플랫폼에서 상품 조회 API를 담당하고 있는데, 오늘 아침 갑자기 서버 응답이 느려졌다는 알림을 받았습니다.
로그를 확인해보니 특정 IP 주소에서 1초에 3,000건이 넘는 요청을 보내고 있었습니다. 아마도 크롤링 봇이나 잘못 작성된 스크립트인 것 같습니다.
이 때문에 다른 정상적인 사용자들까지 서비스 이용에 불편을 겪고 있었습니다. 팀의 시니어 개발자 박아키 씨가 코드를 보더니 말했습니다.
"API에 속도 제한이 없네요. RateLimiter를 적용해야겠어요." RateLimiter란 무엇일까요? 쉽게 비유하자면, RateLimiter는 마치 은행 창구와 같습니다.
은행에는 여러 창구가 있지만, 한 사람이 모든 창구를 독점할 수는 없습니다. 한 명당 하루에 처리할 수 있는 업무 건수가 정해져 있는 것처럼, RateLimiter도 특정 시간 동안 처리할 수 있는 요청 수를 제한합니다.
이를 통해 소수의 사용자나 악의적인 봇이 모든 서버 자원을 독점하는 것을 방지할 수 있습니다. 왜 속도 제한이 필요한가요? 속도 제한이 없던 시절에는 어땠을까요?
개발자들은 DDoS 공격이나 잘못된 클라이언트 코드로 인한 과도한 요청을 막을 방법이 없었습니다. 서버는 들어오는 모든 요청을 처리하려다 과부하 상태에 빠졌고, 결국 정상적인 사용자들도 서비스를 이용할 수 없게 되었습니다.
더 큰 문제는 비용이었습니다. 클라우드 환경에서는 처리하는 요청 수에 따라 비용이 발생하는데, 불필요한 요청까지 모두 처리하다 보면 예상치 못한 비용 폭탄을 맞을 수 있었습니다.
RateLimiter의 등장 바로 이런 문제를 해결하기 위해 RateLimiter 패턴이 등장했습니다. RateLimiter를 사용하면 API별로 서로 다른 속도 제한을 설정할 수 있습니다.
예를 들어 조회 API는 초당 100건, 쓰기 API는 초당 10건으로 제한하는 식입니다. 또한 제한을 초과했을 때 어떻게 대응할지도 우아하게 정의할 수 있습니다.
무엇보다 간단한 어노테이션 하나로 적용할 수 있다는 큰 이점이 있습니다. 코드 분석 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 @RateLimiter(name = "productService") 어노테이션을 보면 이 메서드에 속도 제한을 적용한다는 것을 알 수 있습니다. name 속성으로 설정 파일에서 정의한 속도 제한 정책을 참조합니다.
다음으로 fallbackMethod = "rateLimitFallback" 부분에서는 속도 제한을 초과했을 때 호출할 대체 메서드를 지정합니다. 이렇게 하면 사용자에게 단순히 에러를 반환하는 대신, 캐시된 데이터를 제공하는 등 더 나은 사용자 경험을 제공할 수 있습니다.
마지막으로 fallback 메서드는 원본 메서드와 동일한 파라미터를 받고, 추가로 Exception을 받습니다. 여기서는 캐시된 상품 정보를 반환하여 사용자가 최소한의 정보라도 볼 수 있도록 했습니다.
실무 활용 사례 실제 현업에서는 어떻게 활용할까요? 대형 전자상거래 플랫폼에서는 블랙프라이데이 같은 특별 세일 기간에 트래픽이 평소의 10배 이상 증가합니다.
이때 RateLimiter를 활용하면 서버가 감당할 수 있는 수준으로 요청을 제한하면서도, 제한된 사용자에게는 캐시된 데이터를 제공하여 서비스 중단을 방지할 수 있습니다. 또한 외부 API를 호출하는 경우, 상대방 서버의 속도 제한 정책에 맞춰 우리 쪽에서도 요청을 제한함으로써 API 차단을 방지할 수 있습니다.
설정 파일 작성 RateLimiter를 사용하려면 application.yml에 설정이 필요합니다. 초당 10개의 요청을 허용하고, 대기 시간은 0으로 설정하면 제한을 초과하는 즉시 fallback이 실행됩니다.
limitRefreshPeriod는 제한이 갱신되는 주기를 의미합니다. 주의사항 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 너무 엄격한 제한을 설정하는 것입니다. 예를 들어 초당 1개로 설정하면 정상적인 사용자도 불편을 겪을 수 있습니다.
따라서 실제 트래픽 패턴을 분석하여 적절한 값을 설정해야 합니다. 또한 fallback 메서드의 시그니처가 원본 메서드와 정확히 일치해야 합니다.
파라미터가 다르면 런타임에 메서드를 찾을 수 없다는 에러가 발생합니다. 정리 다시 김개발 씨의 이야기로 돌아가 봅시다.
박아키 씨의 조언에 따라 RateLimiter를 적용한 김개발 씨는 모니터링 화면을 확인했습니다. 과도한 요청은 적절히 차단되고, 정상 사용자들은 다시 빠른 응답을 받기 시작했습니다.
RateLimiter를 제대로 이해하면 안정적인 API 서비스를 구축할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 실제 트래픽 패턴을 먼저 분석한 후 적절한 제한 값을 설정하세요
- fallback 메서드에서는 캐시나 기본값을 반환하여 사용자 경험을 유지하세요
- 프로덕션 환경에서는 속도 제한 발생 횟수를 모니터링하여 정책을 조정하세요
2. 초당 요청 제한
김개발 씨가 RateLimiter를 적용하고 나서 며칠 후, 팀장님께서 질문하셨습니다. "현재 설정이 초당 몇 건으로 되어 있죠?" 김개발 씨는 설정 파일을 보면서 생각했습니다.
적절한 제한 값은 어떻게 정해야 할까요?
**초당 요청 제한(Requests Per Second)**은 1초 동안 허용할 최대 요청 수를 의미합니다. 마치 고속도로 톨게이트가 1분에 처리할 수 있는 차량 대수를 정하는 것처럼, API가 안정적으로 처리할 수 있는 최대 처리량을 설정합니다.
이 값은 서버 성능과 비즈니스 요구사항을 모두 고려하여 결정해야 합니다.
다음 코드를 살펴봅시다.
resilience4j:
ratelimiter:
instances:
productService:
# 초당 10개의 요청 허용
limitForPeriod: 10
# 1초 단위로 제한 갱신
limitRefreshPeriod: 1s
# 대기 시간 없음 (즉시 실패)
timeoutDuration: 0s
paymentService:
# 결제는 더 엄격하게 초당 5개만 허용
limitForPeriod: 5
limitRefreshPeriod: 1s
timeoutDuration: 100ms # 100ms까지 대기
김개발 씨는 RateLimiter를 적용한 후 안정적으로 서비스가 운영되는 것을 보며 뿌듯해했습니다. 하지만 곧 새로운 고민이 생겼습니다.
과연 초당 10개가 적절한 값일까요? 어느 날 사내 카페에서 박아키 씨를 만났습니다.
"선배님, 속도 제한 값을 어떻게 정하는 게 좋을까요? 지금은 그냥 10으로 해뒀는데, 뭔가 기준이 있을 것 같아서요." 박아키 씨는 커피를 한 모금 마시고는 말했습니다.
"좋은 질문이네요. 속도 제한 값을 정하는 건 과학이자 예술이에요." 초당 요청 제한이란 무엇인가요? 초당 요청 제한은 말 그대로 1초 동안 허용되는 요청의 최대 개수입니다.
쉽게 비유하자면, 이것은 마치 패스트푸드점의 주문 처리 능력과 같습니다. 매장에서 1분에 10개의 주문을 처리할 수 있다면, 그 이상의 주문을 받으면 대기 시간이 길어지거나 서비스 품질이 떨어집니다.
API도 마찬가지입니다. 서버가 초당 100개의 요청을 안정적으로 처리할 수 있다면, 그 이상을 허용하면 응답 시간이 느려지고 최악의 경우 서버가 다운될 수 있습니다.
왜 서비스마다 다른 값을 설정할까요? 모든 API에 동일한 제한을 적용하면 안 될까요? 그렇지 않습니다.
각 API는 서로 다른 특성을 가지고 있기 때문입니다. 상품 조회 같은 읽기 작업은 데이터베이스에 부담을 덜 주므로 높은 처리량을 허용할 수 있습니다.
반면 결제 처리 같은 쓰기 작업은 데이터 정합성이 중요하고 외부 시스템과의 연동도 있어 더 보수적으로 제한해야 합니다. 더 큰 문제는 비용입니다.
외부 API를 호출하는 경우, 호출 횟수에 따라 비용이 청구되므로 적절한 제한이 필수입니다. 적절한 값을 찾는 방법 그렇다면 어떻게 적절한 값을 찾을 수 있을까요?
먼저 부하 테스트를 실시합니다. JMeter나 Gatling 같은 도구로 점진적으로 부하를 증가시키며 서버가 안정적으로 처리할 수 있는 최대 처리량을 측정합니다.
예를 들어 응답 시간이 200ms를 넘지 않는 선에서 초당 150개를 처리할 수 있다면, 안전 마진을 고려하여 80% 수준인 120개로 설정합니다. 다음으로 실제 트래픽 패턴을 분석합니다.
모니터링 도구로 평균 트래픽과 피크 트래픽을 확인합니다. 평소에는 초당 50개지만 점심시간에는 200개까지 증가한다면, 이를 고려한 값을 설정해야 합니다.
무엇보다 비즈니스 요구사항을 고려해야 합니다. 실시간 재고 확인은 빠른 응답이 중요하므로 높은 처리량을 허용하고, 회원가입은 보안 검증이 중요하므로 낮은 처리량으로 설정하는 식입니다.
설정 파일 분석 위의 설정 파일을 자세히 살펴보겠습니다. limitForPeriod: 10은 한 번의 갱신 주기 동안 허용되는 요청 수입니다.
limitRefreshPeriod: 1s와 함께 사용되면 초당 10개를 의미합니다. timeoutDuration은 제한에 도달했을 때 얼마나 대기할지를 정합니다.
0으로 설정하면 즉시 실패하고, 100ms로 설정하면 100밀리초 동안 대기하다가 슬롯이 생기면 처리합니다. 결제 서비스는 더 보수적으로 초당 5개만 허용하고, 100ms까지 대기 시간을 주었습니다.
이는 결제가 중요한 작업이므로 조금 기다려서라도 처리하는 것이 낫다는 판단입니다. 실무에서의 활용 실제 프로젝트에서는 어떻게 사용할까요?
대형 소셜 미디어 플랫폼을 예로 들어봅시다. 피드 조회는 초당 1000개, 게시물 작성은 초당 10개, 좋아요는 초당 100개로 설정합니다.
각 기능의 비즈니스 중요도와 시스템 부하를 고려한 결과입니다. 또한 사용자 등급에 따라 다른 제한을 적용할 수도 있습니다.
일반 사용자는 초당 10개, 프리미엄 사용자는 초당 100개를 허용하는 식입니다. 동적 조정 고정된 값만 사용해야 할까요?
아닙니다. 서버의 현재 부하 상태에 따라 동적으로 조정할 수도 있습니다.
CPU 사용률이 80%를 넘으면 제한을 더 엄격하게 하고, 50% 이하면 완화하는 방식입니다. Kubernetes 같은 오케스트레이션 툴과 함께 사용하면, 트래픽 증가 시 Pod를 자동으로 확장하고 그에 맞춰 전체 처리량도 증가시킬 수 있습니다.
주의사항 초보 개발자들이 흔히 하는 실수는 너무 낮게 설정하는 것입니다. 보안을 위해 너무 엄격하게 제한하면 정상적인 사용자도 불편을 겪습니다.
예를 들어 이미지가 많은 페이지에서는 한 번에 여러 개의 API 요청이 발생하는데, 제한이 너무 낮으면 일부 이미지가 로딩되지 않을 수 있습니다. 반대로 너무 높게 설정하면 제한의 의미가 없어집니다.
서버가 초당 100개를 처리할 수 있는데 1000개로 설정하면, 과부하를 막을 수 없습니다. 정리 박아키 씨의 설명을 들은 김개발 씨는 부하 테스트를 실시하고, 실제 트래픽을 분석하여 적절한 값을 찾아냈습니다.
상품 조회는 초당 100개, 결제는 초당 10개로 설정했습니다. 적절한 속도 제한 값을 설정하면 서버를 보호하면서도 사용자에게 좋은 경험을 제공할 수 있습니다.
실전 팁
💡 - 부하 테스트로 서버의 최대 처리량을 측정한 후 80% 수준으로 설정하세요
- 읽기 작업은 넉넉하게, 쓰기 작업은 보수적으로 설정하세요
- 프로덕션 환경에서 지속적으로 모니터링하며 값을 조정하세요
3. SemaphoreBulkhead
어느 날 김개발 씨의 API 서버에서 이상한 현상이 발생했습니다. 외부 결제 시스템이 느려지자 다른 모든 API까지 응답이 느려졌습니다.
박아키 씨가 말했습니다. "벌크헤드 패턴이 필요한 상황이네요.
Semaphore부터 알아볼까요?"
SemaphoreBulkhead는 동시에 실행할 수 있는 요청의 최대 개수를 제한하는 패턴입니다. 마치 주차장의 주차 공간처럼, 정해진 수의 슬롯만 제공하여 한 기능이 모든 자원을 독점하는 것을 방지합니다.
세마포어 방식은 별도의 스레드 풀 없이 현재 스레드에서 실행되므로 가볍고 빠릅니다.
다음 코드를 살펴봅시다.
import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
// 동시에 최대 5개의 결제 요청만 처리
@Bulkhead(name = "paymentBulkhead",
type = Bulkhead.Type.SEMAPHORE,
fallbackMethod = "paymentFallback")
public PaymentResult processPayment(PaymentRequest request) {
// 외부 결제 시스템 호출 (느릴 수 있음)
return externalPaymentApi.charge(request);
}
// 동시 실행 제한 초과 시 호출
private PaymentResult paymentFallback(PaymentRequest request, Exception e) {
// 대기열에 추가하거나 나중에 재시도하도록 안내
return PaymentResult.queued(request);
}
}
김개발 씨의 전자상거래 플랫폼은 순조롭게 운영되고 있었습니다. 하지만 어느 금요일 저녁, 갑자기 모니터링 알람이 울렸습니다.
모든 API의 응답 시간이 급격히 증가하고 있었습니다. 로그를 확인해보니 외부 결제 시스템의 응답이 평소 200ms에서 10초로 느려졌습니다.
문제는 그것뿐만이 아니었습니다. 결제와 전혀 관련 없는 상품 조회 API까지 느려진 것입니다.
팀 채팅방에 급히 올라온 김개발 씨의 메시지를 본 박아키 씨가 답했습니다. "전형적인 리소스 고갈 문제네요.
벌크헤드가 없어서 그래요." SemaphoreBulkhead란 무엇인가요? 벌크헤드라는 이름은 배의 구조에서 유래했습니다. 큰 배에는 여러 개의 격벽(bulkhead)이 있어서 한 구역에 물이 차더라도 다른 구역은 안전합니다.
타이타닉호가 침몰한 이유 중 하나가 격벽이 충분하지 않았기 때문입니다. 마찬가지로 마이크로서비스에서도 한 기능의 문제가 전체 시스템으로 번지지 않도록 자원을 격리해야 합니다.
SemaphoreBulkhead는 이를 세마포어 방식으로 구현한 것입니다. 세마포어는 컴퓨터 과학의 고전적인 동기화 기법입니다.
정해진 수의 허가증(permit)을 발급하고, 작업을 시작할 때 허가증을 받아가고 끝나면 반환합니다. 허가증이 모두 소진되면 새로운 작업은 대기하거나 거절됩니다.
왜 벌크헤드가 필요한가요? 벌크헤드 없이 운영하면 어떤 문제가 생길까요? 김개발 씨의 서버는 100개의 스레드로 모든 요청을 처리했습니다.
결제 API가 느려지자 결제를 기다리는 스레드가 점점 쌓이기 시작했고, 결국 100개 스레드가 모두 결제 대기 상태에 빠졌습니다. 이때 상품 조회 요청이 들어와도 처리할 스레드가 없었습니다.
결제 문제가 전체 시스템의 마비로 이어진 것입니다. 이런 현상을 **리소스 고갈(resource exhaustion)**이라고 합니다.
더 큰 문제는 복구가 어렵다는 점입니다. 외부 시스템이 정상화되어도 쌓인 요청들을 처리하느라 한참 동안 시스템이 불안정했습니다.
SemaphoreBulkhead의 작동 원리 어떻게 이 문제를 해결할까요? SemaphoreBulkhead를 적용하면 결제 처리에 사용할 수 있는 동시 실행 수를 제한합니다.
예를 들어 최대 5개로 설정하면, 6번째 결제 요청부터는 즉시 거절되거나 fallback으로 처리됩니다. 이렇게 하면 결제가 아무리 느려져도 최대 5개의 스레드만 영향을 받습니다.
나머지 95개의 스레드는 상품 조회나 다른 작업을 정상적으로 처리할 수 있습니다. 코드 상세 분석 위 코드를 자세히 살펴봅시다.
@Bulkhead(name = "paymentBulkhead", type = Bulkhead.Type.SEMAPHORE) 어노테이션으로 세마포어 방식의 벌크헤드를 적용합니다. name으로 설정 파일의 정책을 참조하고, type으로 세마포어 방식임을 명시합니다.
fallbackMethod는 동시 실행 제한을 초과했을 때 호출될 메서드입니다. 여기서는 결제를 대기열에 추가하고 나중에 처리하도록 안내합니다.
실제 결제 로직에서는 외부 시스템을 호출하는데, 이 작업이 느려지더라도 전체 시스템에는 영향을 주지 않습니다. 설정 파일 application.yml에는 다음과 같이 설정합니다.
yaml resilience4j: bulkhead: instances: paymentBulkhead: maxConcurrentCalls: 5 # 최대 5개 동시 실행 maxWaitDuration: 100ms # 100ms까지 대기 maxConcurrentCalls가 핵심입니다. 이 값만큼만 동시에 실행할 수 있습니다.
maxWaitDuration은 슬롯이 생길 때까지 대기할 시간입니다. 실무 활용 사례 실제로 어떻게 활용할까요?
Netflix는 각 마이크로서비스 간 호출에 벌크헤드를 적용합니다. 추천 시스템이 느려져도 영상 재생은 정상 작동하도록 자원을 격리한 것입니다.
금융 시스템에서는 핵심 거래 기능과 부가 기능을 벌크헤드로 분리합니다. 이벤트 조회가 느려져도 실제 송금은 영향받지 않도록 합니다.
Semaphore vs ThreadPool 두 가지 벌크헤드 방식이 있는데 언제 어떤 것을 쓸까요? Semaphore는 가볍고 빠릅니다.
별도의 스레드 풀을 생성하지 않고 현재 스레드에서 바로 실행됩니다. 대부분의 경우 이것으로 충분합니다.
하지만 호출되는 라이브러리가 스레드를 블로킹하거나, timeout을 지원하지 않는다면 ThreadPool 방식을 사용해야 합니다. 이는 다음 섹션에서 다루겠습니다.
주의사항 초보자들이 자주 하는 실수가 있습니다. 너무 낮은 값을 설정하면 정상적인 부하에서도 자주 거절됩니다.
너무 높게 설정하면 격리 효과가 없어집니다. 실제 동시 사용자 수와 응답 시간을 고려하여 적절한 값을 찾아야 합니다.
또한 fallback 메서드가 원본 메서드보다 무거우면 안 됩니다. fallback은 빠르게 대체 응답을 제공하는 것이 목적입니다.
정리 박아키 씨의 조언에 따라 김개발 씨는 결제 서비스에 SemaphoreBulkhead를 적용했습니다. 다음 번 외부 시스템이 느려졌을 때, 결제만 영향받고 다른 기능은 정상 작동했습니다.
벌크헤드 패턴으로 시스템의 장애 전파를 막고 안정성을 크게 향상시킬 수 있습니다.
실전 팁
💡 - 외부 시스템 호출이나 느린 작업에는 반드시 벌크헤드를 적용하세요
- 동시 사용자 수의 20-30% 수준으로 설정하는 것이 일반적입니다
- 모니터링으로 거절률을 추적하여 값을 조정하세요
4. ThreadPoolBulkhead
SemaphoreBulkhead를 적용한 후, 김개발 씨는 새로운 문제를 발견했습니다. 외부 API가 응답하지 않으면 타임아웃까지 스레드가 계속 블로킹되는 것이었습니다.
박아키 씨가 말했습니다. "이럴 때는 ThreadPool 방식이 필요해요."
ThreadPoolBulkhead는 별도의 스레드 풀을 생성하여 특정 작업을 완전히 격리하는 방식입니다. 마치 전용 작업장을 따로 만드는 것처럼, 메인 스레드 풀과 분리된 공간에서 위험한 작업을 실행합니다.
타임아웃과 함께 사용하면 응답하지 않는 외부 시스템으로부터 완벽히 보호할 수 있습니다.
다음 코드를 살펴봅시다.
import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class ExternalApiService {
// 별도 스레드 풀에서 실행 (비동기)
@Bulkhead(name = "externalApi",
type = Bulkhead.Type.THREADPOOL,
fallbackMethod = "externalApiFallback")
public CompletableFuture<ApiResponse> callExternalApi(String request) {
return CompletableFuture.supplyAsync(() -> {
// 외부 API 호출 (타임아웃이 없을 수도 있음)
return slowExternalApi.getData(request);
});
}
// 격리된 스레드 풀이 가득 차면 호출
private CompletableFuture<ApiResponse> externalApiFallback(
String request, Exception e) {
return CompletableFuture.completedFuture(
ApiResponse.cached(request) // 캐시된 데이터 반환
);
}
}
김개발 씨는 SemaphoreBulkhead를 적용하고 안심하고 있었습니다. 하지만 며칠 후 새로운 문제가 발생했습니다.
외부 날씨 정보 API를 호출하는 기능을 추가했는데, 이 API가 가끔 응답을 전혀 하지 않았습니다. 타임아웃도 없는 오래된 라이브러리였습니다.
SemaphoreBulkhead를 적용했지만, 허용된 5개의 요청이 모두 무한정 대기 상태에 빠지는 문제가 있었습니다. 박아키 씨에게 다시 도움을 청했습니다.
"Semaphore 방식은 현재 스레드에서 실행되기 때문에, 블로킹되면 어쩔 수 없어요. ThreadPool 방식을 써야 해요." ThreadPoolBulkhead란 무엇인가요? ThreadPoolBulkhead는 벌크헤드의 또 다른 구현 방식입니다.
쉽게 비유하자면, 이것은 마치 위험한 화학 실험을 위한 별도의 실험실과 같습니다. 메인 건물(메인 스레드 풀)과 완전히 분리된 공간(전용 스레드 풀)에서 위험한 작업을 수행합니다.
실험실에서 무슨 일이 일어나든 메인 건물은 안전합니다. 스레드 풀도 마찬가지입니다.
전용 스레드 풀의 스레드가 모두 블로킹되어도 메인 스레드 풀은 영향받지 않습니다. Semaphore와 무엇이 다른가요? 두 방식의 핵심 차이는 무엇일까요?
Semaphore 방식은 현재 스레드에서 바로 실행됩니다. 요청을 받은 Tomcat 스레드가 직접 작업을 수행합니다.
가볍고 빠르지만, 작업이 블로킹되면 그 스레드도 함께 블로킹됩니다. ThreadPool 방식은 별도의 스레드 풀에 작업을 위임합니다.
요청을 받은 Tomcat 스레드는 작업을 전용 스레드 풀에 넘기고 바로 반환됩니다. 비동기로 처리되며, CompletableFuture를 통해 결과를 받습니다.
따라서 ThreadPool 방식은 오버헤드가 있지만, 블로킹 작업이나 신뢰할 수 없는 외부 시스템을 호출할 때 훨씬 안전합니다. 언제 ThreadPool을 사용해야 할까요? 어떤 상황에서 ThreadPool 방식을 선택해야 할까요?
첫째, 타임아웃이 없는 외부 라이브러리를 사용할 때입니다. 오래된 HTTP 클라이언트나 레거시 시스템 연동 라이브러리가 그 예입니다.
둘째, 응답 시간이 매우 불규칙한 API를 호출할 때입니다. 보통은 100ms인데 가끔 10초씩 걸리는 경우, 전용 스레드 풀로 격리하는 것이 안전합니다.
셋째, CPU 집약적인 작업을 수행할 때입니다. 이미지 처리나 복잡한 계산을 메인 스레드 풀에서 하면 다른 요청이 느려지므로 분리합니다.
코드 상세 분석 코드를 한 줄씩 살펴보겠습니다. type = Bulkhead.Type.THREADPOOL로 ThreadPool 방식임을 명시합니다.
이제 Resilience4j가 자동으로 전용 스레드 풀을 생성합니다. 반환 타입이 CompletableFuture<ApiResponse>인 것에 주목하세요.
ThreadPool 방식은 항상 비동기로 동작하므로 CompletableFuture를 사용해야 합니다. CompletableFuture.supplyAsync() 안에서 실제 외부 API를 호출합니다.
이 코드는 전용 스레드 풀의 스레드에서 실행됩니다. fallback 메서드도 CompletableFuture를 반환합니다.
캐시된 데이터를 즉시 반환하는 완료된 Future를 생성합니다. 설정 파일 작성 application.yml에는 다음과 같이 설정합니다.
yaml resilience4j: thread-pool-bulkhead: instances: externalApi: maxThreadPoolSize: 5 # 최대 5개 스레드 coreThreadPoolSize: 3 # 기본 3개 스레드 유지 queueCapacity: 10 # 대기 큐 크기 10 keepAliveDuration: 20ms # 유휴 스레드 유지 시간 maxThreadPoolSize는 최대 스레드 수입니다. queueCapacity는 모든 스레드가 바쁠 때 대기할 수 있는 큐 크기입니다.
스레드 풀이 가득 차고 큐도 가득 차면 새로운 요청은 즉시 거절되어 fallback이 실행됩니다. 비동기 처리 패턴 CompletableFuture를 어떻게 활용할까요?
컨트롤러에서는 다음과 같이 사용합니다. java @GetMapping("/weather") public CompletableFuture<WeatherResponse> getWeather(@RequestParam String city) { return externalApiService.callExternalApi(city) .thenApply(apiResponse -> WeatherResponse.from(apiResponse)) .exceptionally(ex -> WeatherResponse.error()); } Spring WebFlux나 Spring MVC의 비동기 처리 기능과 함께 사용하면, 적은 수의 스레드로 많은 요청을 처리할 수 있습니다.
실무 활용 사례 실제 프로젝트에서는 어떻게 사용할까요? 대형 이커머스 플랫폼에서는 상품 추천 AI 모델을 ThreadPool 벌크헤드로 격리합니다.
AI 모델이 느려지거나 멈춰도 주문 처리는 계속됩니다. 금융 시스템에서는 외부 신용평가 API 호출을 전용 스레드 풀로 분리합니다.
외부 시스템 장애가 핵심 뱅킹 기능에 영향을 주지 않도록 합니다. 모니터링 ThreadPool 벌크헤드는 어떻게 모니터링할까요?
Resilience4j는 메트릭을 제공합니다. 큐 사용률, 활성 스레드 수, 거절된 요청 수 등을 Prometheus나 Micrometer로 수집할 수 있습니다.
큐 사용률이 80%를 지속적으로 넘으면 스레드 풀 크기나 큐 크기를 늘려야 한다는 신호입니다. 주의사항 ThreadPool 방식을 사용할 때 주의할 점이 있습니다.
스레드 풀을 너무 많이 만들면 컨텍스트 스위칭 오버헤드가 증가합니다. 각 외부 시스템마다 하나씩 만드는 것이 일반적입니다.
또한 CompletableFuture를 잘못 사용하면 예외가 제대로 처리되지 않을 수 있습니다. exceptionally나 handle로 예외 처리를 명시적으로 해야 합니다.
정리 박아키 씨의 조언대로 김개발 씨는 신뢰할 수 없는 외부 API 호출에 ThreadPoolBulkhead를 적용했습니다. 이제 외부 시스템이 응답하지 않아도 전용 스레드 풀만 영향받고, 메인 서비스는 정상 작동했습니다.
ThreadPool 벌크헤드로 위험한 작업을 완전히 격리하여 시스템의 안정성을 한 단계 높일 수 있습니다.
실전 팁
💡 - 타임아웃이 없거나 신뢰할 수 없는 외부 시스템 호출에 사용하세요
- 스레드 풀 크기는 외부 시스템의 처리 능력을 고려하여 설정하세요
- CompletableFuture 예외 처리를 명시적으로 작성하세요
5. 동시 요청 제한
김개발 씨의 시스템은 이제 꽤 견고해졌습니다. 하지만 신규 이벤트 페이지를 오픈하는 날, 순간적으로 수천 명이 몰리면서 데이터베이스 커넥션 풀이 고갈되었습니다.
박아키 씨가 말했습니다. "동시 요청 수를 제한해야 해요."
동시 요청 제한은 특정 시점에 동시에 처리할 수 있는 요청의 최대 개수를 제한하는 것입니다. RateLimiter가 시간당 횟수를 제한한다면, 동시 요청 제한은 바로 지금 처리 중인 요청 수를 제한합니다.
데이터베이스 커넥션, 메모리, CPU 같은 한정된 자원을 보호하는 데 효과적입니다.
다음 코드를 살펴봅시다.
import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import org.springframework.stereotype.Service;
@Service
public class EventService {
// 동시에 최대 20개의 이벤트 응모만 처리
@Bulkhead(name = "eventRegistration",
type = Bulkhead.Type.SEMAPHORE,
fallbackMethod = "registrationFallback")
public RegistrationResult registerForEvent(Long userId, Long eventId) {
// DB 커넥션 사용, 외부 API 호출 등 리소스 집약적인 작업
validateUser(userId);
checkEventCapacity(eventId);
createRegistration(userId, eventId);
sendConfirmationEmail(userId);
return RegistrationResult.success();
}
// 동시 처리 한계 도달 시
private RegistrationResult registrationFallback(
Long userId, Long eventId, Exception e) {
// 대기 큐에 추가하거나 잠시 후 재시도 안내
waitingQueue.add(userId, eventId);
return RegistrationResult.queued();
}
}
김개발 씨의 회사에서 인기 아이돌 굿즈 증정 이벤트를 진행하기로 했습니다. 마케팅팀은 SNS에 대대적으로 홍보했고, 오픈 시간에 맞춰 수만 명의 팬들이 대기하고 있었습니다.
오후 3시 정각, 이벤트 페이지가 열렸습니다. 그 순간 초당 5,000건의 이벤트 응모 요청이 쏟아졌습니다.
30초 후, 시스템이 응답하지 않기 시작했습니다. 데이터베이스 커넥션 풀이 고갈되었고, 메모리도 부족해졌습니다.
김개발 씨는 식은땀을 흘리며 로그를 확인했습니다. 동시 요청 제한이란 무엇인가요? 동시 요청 제한과 RateLimiter는 어떻게 다를까요?
RateLimiter는 시간 기반입니다. 초당 100개를 허용한다면, 1초 동안 총 100개까지 처리합니다.
요청이 1ms 만에 끝나든 1초가 걸리든 상관없이 개수만 셉니다. 동시 요청 제한은 현재 진행 중인 요청의 개수를 제한합니다.
쉽게 비유하자면, 식당의 좌석 수와 같습니다. 동시에 20명까지만 식사할 수 있고, 한 명이 나가야 다음 사람이 들어올 수 있습니다.
사람마다 식사 시간이 다르듯, 요청마다 처리 시간이 다릅니다. 어떤 요청은 10ms, 어떤 요청은 1초가 걸릴 수 있습니다.
동시 요청 제한은 지금 이 순간 처리 중인 요청 수를 관리합니다. 왜 필요한가요? 동시 요청 제한이 없으면 어떤 문제가 생길까요?
가장 흔한 문제는 데이터베이스 커넥션 풀 고갈입니다. 일반적으로 애플리케이션은 10-50개의 DB 커넥션을 풀로 관리합니다.
동시에 100개의 요청이 들어오면 50개는 커넥션을 얻지 못해 대기합니다. 대기 중인 스레드가 쌓이면 메모리 부족이 발생합니다.
각 스레드는 1-2MB의 스택 메모리를 사용하므로, 수천 개가 쌓이면 수 GB의 메모리가 소진됩니다. 더 심각한 것은 연쇄 장애입니다.
데이터베이스가 느려지면 애플리케이션이 느려지고, 로드밸런서가 헬스체크 실패로 인스턴스를 제외하면서 전체 시스템이 무너집니다. 어떻게 작동하나요? 동시 요청 제한은 어떤 원리로 작동할까요?
내부적으로 세마포어(Semaphore) 를 사용합니다. 설정된 개수만큼의 허가증(permit)을 발급합니다.
요청이 들어오면 허가증을 하나 가져가고, 작업이 끝나면 반환합니다. 모든 허가증이 소진되면 새로운 요청은 세 가지 방법으로 처리됩니다.
첫째, 즉시 거절하고 fallback을 실행합니다. 둘째, 일정 시간 대기하다가 허가증을 얻으면 처리합니다.
셋째, 대기 큐에 추가하여 나중에 처리합니다. 코드 분석 위 코드를 자세히 살펴보겠습니다.
@Bulkhead(name = "eventRegistration", type = Bulkhead.Type.SEMAPHORE) 로 세마포어 기반 동시 실행 제한을 적용합니다. 이것이 바로 동시 요청 제한입니다.
메서드 안에서는 여러 리소스 집약적인 작업을 수행합니다. 사용자 검증으로 DB 조회를 하고, 이벤트 정원 확인으로 또 DB 조회를 하고, 등록 정보를 DB에 쓰고, 이메일을 발송합니다.
이런 작업들이 동시에 수천 개 실행되면 시스템이 버티지 못합니다. 그래서 동시에 20개까지만 허용하는 것입니다.
fallback에서는 대기 큐에 추가합니다. 사용자에게는 "대기 중입니다.
곧 처리됩니다"라는 메시지를 보여주고, 백그라운드에서 순차적으로 처리할 수 있습니다. 설정 값 정하기 적절한 동시 요청 제한 값은 어떻게 정할까요?
가장 중요한 기준은 데이터베이스 커넥션 풀 크기입니다. 커넥션 풀이 20개라면 동시 요청도 20개 이하로 설정해야 합니다.
그래야 모든 요청이 대기 없이 커넥션을 얻을 수 있습니다. 다음으로 메모리와 CPU를 고려합니다.
각 요청이 평균 10MB의 메모리를 사용한다면, 2GB 힙 메모리에서는 200개 정도가 한계입니다. 마지막으로 응답 시간 SLA를 고려합니다.
평균 응답 시간을 500ms로 유지하려면, 처리 시간이 1초인 작업의 동시 실행 수를 제한해야 합니다. 실무 활용 사례 실제 현업에서는 어떻게 활용할까요?
대형 게임 회사는 랭킹 조회 API에 동시 요청 제한을 적용합니다. 랭킹 계산이 복잡하고 메모리를 많이 사용하므로, 동시에 10개까지만 처리하고 나머지는 캐시된 데이터를 반환합니다.
금융 앱에서는 계좌 이체에 동시 요청 제한을 적용합니다. 정확한 잔액 계산과 거래 기록을 위해 동시 처리 수를 제한하여 데이터 정합성을 보장합니다.
RateLimiter와 함께 사용하기 두 가지를 함께 사용하면 더 효과적입니다. RateLimiter로 초당 100개를 제한하고, Bulkhead로 동시 20개를 제한하면 이중 방어가 됩니다.
순간적인 버스트 트래픽은 RateLimiter가 막고, 지속적인 높은 부하는 Bulkhead가 막습니다. yaml resilience4j: ratelimiter: instances: eventRegistration: limitForPeriod: 100 # 초당 100개 bulkhead: instances: eventRegistration: maxConcurrentCalls: 20 # 동시 20개 주의사항 초보자들이 흔히 하는 실수가 있습니다.
너무 낮게 설정하면 정상 트래픽에서도 자주 거절됩니다. 평소 트래픽의 2배 정도는 감당할 수 있도록 여유를 두어야 합니다.
반대로 너무 높게 설정하면 제한의 의미가 없습니다. 시스템이 감당할 수 있는 최대치를 부하 테스트로 측정하고, 그보다 낮게 설정해야 합니다.
정리 김개발 씨는 이벤트 응모 API에 동시 요청 제한을 적용했습니다. 다음 이벤트에서는 수만 명이 몰려도 시스템이 안정적으로 작동했습니다.
처리되지 못한 사용자는 대기 큐에서 순차적으로 처리되었습니다. 동시 요청 제한으로 한정된 자원을 보호하고, 순간적인 트래픽 폭증에서도 시스템을 안정적으로 유지할 수 있습니다.
실전 팁
💡 - 데이터베이스 커넥션 풀 크기를 기준으로 설정하세요
- RateLimiter와 함께 사용하여 이중 방어하세요
- 거절된 요청은 대기 큐나 캐시로 우아하게 처리하세요
6. 리소스 격리
어느 날 김개발 씨는 전체 시스템 아키텍처를 보며 깨달았습니다. 각 API에 벌크헤드를 적용했지만, 더 높은 수준의 격리가 필요하다는 것을요.
박아키 씨가 미소 지으며 말했습니다. "이제 리소스 격리의 진짜 의미를 이해하는군요."
리소스 격리(Resource Isolation) 는 서로 다른 기능이나 서비스가 독립적인 자원을 사용하도록 분리하는 아키텍처 패턴입니다. 마치 아파트 각 호실이 독립적인 전기와 수도를 사용하는 것처럼, 한 서비스의 장애가 다른 서비스로 전파되지 않도록 완벽히 격리합니다.
이는 마이크로서비스 아키텍처의 핵심 원칙입니다.
다음 코드를 살펴봅시다.
// 서비스별로 별도의 DB 커넥션 풀 사용
@Configuration
public class DataSourceConfig {
// 주문 서비스 전용 커넥션 풀
@Bean(name = "orderDataSource")
public DataSource orderDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/order_db");
config.setMaximumPoolSize(20); // 주문용 20개
config.setPoolName("OrderPool");
return new HikariDataSource(config);
}
// 상품 조회 전용 커넥션 풀
@Bean(name = "productDataSource")
public DataSource productDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/product_db");
config.setMaximumPoolSize(50); // 조회용 50개
config.setPoolName("ProductPool");
return new HikariDataSource(config);
}
}
김개발 씨의 전자상거래 플랫폼은 이제 꽤 성숙한 시스템이 되었습니다. RateLimiter와 Bulkhead를 곳곳에 적용하여 안정성도 크게 향상되었습니다.
하지만 어느 날 밤, 배치 작업이 잘못되어 주문 테이블을 전체 스캔하는 쿼리가 실행되었습니다. 데이터베이스 커넥션 풀 20개가 모두 이 작업에 묶였고, 상품 조회 같은 다른 기능도 덩달아 느려졌습니다.
다음 날 아침, 사후 분석 회의에서 박아키 씨가 말했습니다. "벌크헤드로 애플리케이션 레벨은 격리했지만, 데이터베이스 레벨 격리가 없었네요.
진짜 리소스 격리가 필요합니다." 리소스 격리란 무엇인가요? 리소스 격리는 단순히 동시 실행 수를 제한하는 것을 넘어섭니다. 쉽게 비유하자면, 이것은 마치 아파트 건물 구조와 같습니다.
각 호실은 독립적인 전기, 수도, 가스를 사용합니다. 101호에서 전기를 과다 사용해도 102호는 영향받지 않습니다.
왜냐하면 물리적으로 분리된 자원을 사용하기 때문입니다. 소프트웨어에서도 마찬가지입니다.
완벽한 격리란 데이터베이스 커넥션, 스레드 풀, 메모리, 네트워크 등 모든 자원을 서비스별로 분리하는 것을 의미합니다. 왜 완벽한 격리가 필요한가요? 애플리케이션 레벨 벌크헤드만으로는 부족할까요?
그렇습니다. 벌크헤드는 스레드 레벨에서 격리하지만, 데이터베이스나 캐시 같은 공유 자원은 여전히 모두가 함께 사용합니다.
예를 들어 주문 서비스와 상품 조회 서비스가 같은 DB 커넥션 풀을 공유한다고 가정해봅시다. 주문 서비스에 버그가 있어서 커넥션을 반환하지 않으면, 상품 조회도 커넥션을 얻지 못해 실패합니다.
더 심각한 것은 데이터베이스 자체의 부하입니다. 무거운 쿼리가 DB CPU를 100% 사용하면, 가벼운 쿼리도 느려집니다.
애플리케이션에서 아무리 격리해도 소용없습니다. 완벽한 격리의 수준들 리소스 격리는 여러 수준에서 적용할 수 있습니다.
Level 1: 애플리케이션 레벨 격리 ThreadPoolBulkhead로 서비스별 스레드 풀을 분리합니다. 앞서 배운 내용입니다.
Level 2: 데이터베이스 커넥션 풀 격리 서비스별로 별도의 커넥션 풀을 만듭니다. 주문용 20개, 상품 조회용 50개를 독립적으로 관리합니다.
코드 예제가 바로 이것입니다. Level 3: 데이터베이스 인스턴스 격리 아예 서비스별로 다른 DB 인스턴스를 사용합니다.
주문은 order_db, 상품은 product_db를 사용합니다. 비용은 들지만 완벽한 격리가 가능합니다.
Level 4: 물리적 인프라 격리 서비스별로 다른 서버나 클러스터를 사용합니다. Kubernetes에서는 Namespace나 Node를 분리하여 CPU, 메모리를 격리합니다.
코드 분석 위 코드를 자세히 살펴보겠습니다. @Bean(name = "orderDataSource")로 주문 서비스 전용 데이터소스를 생성합니다.
이것은 독립적인 HikariCP 커넥션 풀입니다. setMaximumPoolSize(20)으로 주문용으로 20개의 커넥션을 할당합니다.
이 20개는 주문 서비스만 사용할 수 있습니다. 마찬가지로 상품 조회용으로 50개의 커넥션을 가진 별도의 풀을 만듭니다.
조회는 쓰기보다 트래픽이 많으므로 더 큰 풀을 할당했습니다. 이제 주문 서비스가 모든 커넥션을 점유해도 상품 조회는 자신의 50개 커넥션으로 정상 작동합니다.
실무에서의 적용 실제 프로젝트에서는 어떻게 적용할까요? Netflix는 각 마이크로서비스가 독립적인 DB를 사용합니다.
추천 서비스, 사용자 서비스, 영상 서비스가 모두 별도의 Cassandra 클러스터를 가집니다. Amazon도 마찬가지입니다.
장바구니, 주문, 상품, 결제 서비스가 각각 독립적인 데이터 저장소를 사용합니다. 한 서비스가 폭발적으로 성장해도 다른 서비스는 영향받지 않습니다.
캐시와 메시지 큐 격리 데이터베이스뿐 아니라 다른 자원도 격리해야 합니다. Redis 캐시도 서비스별로 분리할 수 있습니다.
한 서비스가 캐시를 잘못 사용하여 메모리를 가득 채워도 다른 서비스의 캐시는 안전합니다. 메시지 큐도 마찬가지입니다.
Kafka에서는 서비스별로 다른 Topic과 Consumer Group을 사용하여 격리합니다. 비용과 복잡도의 트레이드오프 완벽한 격리는 비용이 듭니다.
DB 인스턴스를 여러 개 운영하면 인프라 비용이 증가합니다. 관리해야 할 컴포넌트도 많아져 운영 복잡도가 올라갑니다.
따라서 핵심 서비스와 부가 서비스를 구분하여 적용합니다. 주문과 결제는 완벽히 격리하고, 관리자 기능은 공유 자원을 사용하는 식입니다.
모니터링과 관찰 격리된 자원을 어떻게 모니터링할까요? 각 커넥션 풀의 사용률, 대기 시간, 타임아웃 발생 횟수를 개별적으로 추적합니다.
하나의 풀에서 문제가 생겨도 다른 풀은 정상임을 확인할 수 있습니다. Grafana 대시보드에 서비스별로 패널을 나누어 표시하면, 어떤 서비스가 자원을 많이 사용하는지 한눈에 파악할 수 있습니다.
주의사항 리소스 격리를 적용할 때 주의할 점이 있습니다. 너무 세밀하게 나누면 오히려 자원 낭비가 될 수 있습니다.
10개 서비스가 각각 20개 커넥션을 가지면 총 200개인데, 평소에는 50개만 사용한다면 150개가 낭비됩니다. 또한 격리가 완벽하지 않을 수 있습니다.
같은 DB 인스턴스를 사용하면 커넥션은 격리되어도 CPU와 디스크 I/O는 공유됩니다. 정리 김개발 씨는 주문과 상품 조회의 커넥션 풀을 분리했습니다.
다음 번 배치 작업 문제가 생겼을 때, 주문 처리만 영향받고 상품 조회는 정상 작동했습니다. 박아키 씨가 말했습니다.
"이제 진짜 resilient한 시스템을 만드는 법을 이해했네요. 축하합니다." 리소스 격리로 장애 전파를 원천 차단하고, 진정으로 독립적인 마이크로서비스 아키텍처를 구축할 수 있습니다.
실전 팁
💡 - 핵심 서비스부터 우선적으로 격리하세요
- 커넥션 풀 격리부터 시작하여 점진적으로 DB 인스턴스 격리로 발전시키세요
- 비용과 안정성의 균형을 고려하여 격리 수준을 결정하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Istio 설치와 구성 완벽 가이드
Kubernetes 환경에서 Istio 서비스 메시를 설치하고 구성하는 방법을 초급 개발자도 쉽게 이해할 수 있도록 실무 스토리와 비유로 풀어낸 가이드입니다. istioctl 설치부터 사이드카 주입까지 단계별로 학습합니다.
서비스 메시 완벽 가이드
마이크로서비스 간 통신을 안전하고 효율적으로 관리하는 서비스 메시의 핵심 개념부터 실전 도입까지, 초급 개발자를 위한 완벽한 입문서입니다. Istio와 Linkerd 비교, 사이드카 패턴, 실무 적용 노하우를 담았습니다.
Helm 마이크로서비스 패키징 완벽 가이드
Kubernetes 환경에서 마이크로서비스를 효율적으로 패키징하고 배포하는 Helm의 핵심 기능을 실무 중심으로 학습합니다. Chart 생성부터 릴리스 관리까지 체계적으로 다룹니다.
관찰 가능한 마이크로서비스 완벽 가이드
마이크로서비스 환경에서 시스템의 상태를 실시간으로 관찰하고 모니터링하는 방법을 배웁니다. Resilience4j, Zipkin, Prometheus, Grafana, EFK 스택을 활용하여 안정적이고 관찰 가능한 시스템을 구축하는 실전 가이드입니다.
EFK 스택 로깅 완벽 가이드
마이크로서비스 환경에서 로그를 효과적으로 수집하고 분석하는 EFK 스택(Elasticsearch, Fluentd, Kibana)의 핵심 개념과 실전 활용법을 초급 개발자도 쉽게 이해할 수 있도록 정리한 가이드입니다.