2026. 8. 24. 15:00ㆍServer - Architecture/장애대응
이번 내용은 각각 다른 장애처럼 보이지만 공통점이 있다. 모두 서비스가 당장 종료되지 않은 상태에서도 데이터 불일치, 응답 지연, 성능 저하를 일으키며 점차 큰 장애로 확산될 수 있다는 점이다.
DB 복제 지연 → 오래된 데이터 조회
메모리 릭 → 메모리 사용량 지속 증가
캐시 압력 → 적중률 저하와 원본 저장소 부하 증가
설정 관리 실패 → 잘못된 설정이 전체 서비스로 확산
따라서 단순한 서버 생존 여부가 아니라 데이터 최신성, 자원 사용 추세, 캐시 상태, 설정 변경 이력까지 함께 모니터링해야 한다.
1. DB 복제 구조
서비스의 조회 성능과 가용성을 높이기 위해 하나의 DB만 사용하지 않고 Primary와 Replica로 구성할 수 있다.
쓰기 요청 → Primary DB
읽기 요청 → Replica DB
Primary DB는 데이터 생성, 수정, 삭제를 담당하고 Replica DB는 Primary의 데이터를 복제해 조회 요청을 처리한다.
이 구조의 장점은 다음과 같다.
- 읽기 부하 분산
- 조회 성능 향상
- Primary DB의 부담 감소
- 장애 발생 시 복구와 전환에 활용
하지만 Primary에서 변경된 데이터가 Replica에 즉시 반영되는 것은 아니다. 대부분의 복제 방식에서는 짧은 시간이라도 지연이 발생할 수 있다.
2. DB 복제 지연이란?
DB 복제 지연(Replication Lag)은 Primary DB에 반영된 데이터가 Replica DB에 적용되기까지 시간이 걸리는 현상이다.
1. 사용자가 상품을 주문함
2. Primary DB의 재고가 10개에서 9개로 변경됨
3. Replica DB에는 아직 재고가 10개로 남아 있음
4. 사용자가 조회 API를 호출함
5. Replica에서 오래된 재고 10개를 반환함
쓰기 직후 조회했는데 변경 전 데이터가 보이는 현상이 발생할 수 있다. 이를 읽기 후 쓰기 일관성(Read-after-write consistency)이 보장되지 않는 상황이라고 볼 수 있다.
3. DB 복제 지연이 발생하는 원인
대표적인 원인은 다음과 같다.
- Primary의 쓰기 작업이 급격히 증가함
- Replica의 CPU, 메모리 또는 디스크 성능 부족
- 대량 UPDATE, DELETE 또는 DB 마이그레이션
- Primary와 Replica 사이의 네트워크 지연
- Replica에서 느린 쿼리가 실행됨
- 복제 적용 스레드의 처리 속도 부족
- 특정 트랜잭션이 너무 오랫동안 실행됨
- Replica의 스펙이 Primary보다 낮음
복제 지연은 단순히 네트워크 문제로만 발생하는 것이 아니다. Primary가 변경 로그를 전달했어도 Replica의 적용 속도가 느리면 지연이 계속 쌓일 수 있다.
4. DB 복제 지연으로 발생하는 문제
변경 직후 이전 데이터가 조회됨
사용자가 정보를 수정했지만 화면에는 수정 전 정보가 표시될 수 있다.
존재하지 않는 데이터처럼 보임
회원가입이나 주문 생성 직후 Replica에서 조회하면 아직 데이터가 복제되지 않아 404 Not Found가 반환될 수 있다.
중복 요청이 발생함
사용자는 요청이 처리되지 않았다고 생각해 같은 작업을 다시 실행할 수 있다.
주문 생성 성공
→ Replica 조회 결과에는 주문이 없음
→ 사용자가 주문 재시도
→ 중복 주문 발생 가능성
잘못된 비즈니스 판단
오래된 재고, 포인트 또는 상태 값을 기준으로 다음 작업을 수행하면 데이터 정합성 문제가 발생할 수 있다.
특히 결제, 재고, 인증 및 권한처럼 정확성이 중요한 기능은 Replica의 오래된 데이터를 기준으로 판단하지 않도록 주의해야 한다.
5. DB 복제 지연 대응 방법
쓰기 직후 Primary에서 조회
변경 직후 반드시 최신 데이터가 필요한 요청은 Primary DB에서 조회할 수 있다.
일반 조회 → Replica
쓰기 직후 조회 → Primary
모든 조회를 Primary로 보내면 읽기 분산의 장점이 사라지므로 일관성이 필요한 구간만 선택해야 한다.
일정 시간 동안 Primary로 고정
사용자가 데이터를 변경한 뒤 일정 시간 동안 해당 사용자의 조회를 Primary로 전달하는 방법도 있다.
사용자에게 결과를 직접 반환
데이터를 저장한 뒤 다시 Replica에서 조회하지 않고 저장된 결과를 응답으로 전달할 수 있다.
@Transactional
public OrderResponse createOrder(OrderRequest request) {
Order order = orderRepository.save(
Order.create(request.productId())
);
return OrderResponse.from(order);
}
비동기 흐름 고려
즉시 일관성이 필요하지 않은 기능이라면 데이터가 반영 중이라는 상태를 사용자에게 명확하게 전달할 수 있다.
처리 중 → 복제 및 후속 작업 완료 → 처리 완료
복제 지연 모니터링
다음 지표를 관찰해야 한다.
- 복제 지연 시간
- 복제되지 않은 로그의 크기
- Replica의 CPU와 디스크 I/O
- 복제 적용 처리량
- Replica 조회 오류율
- Primary와 Replica의 데이터 차이
- 장기 실행 트랜잭션
복제 지연이 일정 기준을 초과하면 Replica의 조회 트래픽을 줄이거나 Primary로 임시 전환할 수 있어야 한다.
메모리 릭
6. 메모리 릭이란?
메모리 릭(Memory Leak)은 더 이상 필요하지 않은 객체가 계속 참조되어 Garbage Collector가 메모리에서 제거하지 못하는 현상이다.
Java는 Garbage Collector가 사용하지 않는 객체를 자동으로 정리한다. 하지만 어떤 객체가 여전히 참조되고 있다면 애플리케이션에서 실제로 사용하지 않더라도 GC는 해당 객체를 제거할 수 없다.
객체 생성
→ 사용 완료
→ 참조가 남아 있음
→ GC가 살아 있는 객체로 판단
→ Heap 메모리에 계속 유지
메모리 릭이 계속되면 Heap 사용량이 점차 증가하고, 결국 OutOfMemoryError가 발생할 수 있다.
7. Java에서 메모리 릭이 발생하는 사례
정리되지 않는 컬렉션
@Component
public class RequestStorage {
private final Map<String, Object> requests =
new ConcurrentHashMap<>();
public void add(String id, Object request) {
requests.put(id, request);
}
}
계속 데이터가 추가되지만 제거되지 않는다면 Map이 객체를 참조하고 있으므로 GC가 정리할 수 없다.
제한이 없는 캐시
private final Map<Long, Product> cache =
new ConcurrentHashMap<>();
최대 크기와 만료 시간이 없는 캐시는 데이터 증가에 따라 Heap을 계속 점유할 수 있다.
Listener 및 Callback 해제 누락
이벤트 리스너를 등록한 뒤 제거하지 않으면 리스너가 관련 객체를 계속 참조할 수 있다.
ThreadLocal 정리 누락
스레드 풀의 스레드는 요청이 끝난 뒤에도 재사용된다. ThreadLocal 값을 제거하지 않으면 이전 요청의 데이터가 남을 수 있다.
try {
requestContext.set(context);
process();
} finally {
requestContext.remove();
}
자원 해제 누락
Stream, DB Connection, HTTP Response 같은 자원을 정상적으로 닫지 않으면 메모리뿐 아니라 파일 디스크립터와 커넥션도 고갈될 수 있다.
가능하면 try-with-resources를 사용하는 것이 안전하다.
8. 메모리 릭의 주요 증상
메모리 릭이 발생하면 다음과 같은 현상이 나타날 수 있다.
- Heap 사용량이 시간에 따라 계속 증가함
- GC 이후에도 메모리 사용량이 충분히 감소하지 않음
- Full GC 발생 횟수가 증가함
- GC 정지 시간이 길어짐
- 응답 시간이 점차 느려짐
- CPU 사용률이 증가함
- 서버를 재시작하면 일시적으로 정상화됨
- 일정 시간이 지나면 같은 문제가 다시 발생함
- 최종적으로 OutOfMemoryError 발생
정상적인 애플리케이션도 트래픽에 따라 메모리가 증가하고 GC 이후 감소한다.
정상적인 Heap
증가 → GC → 감소 → 증가 → GC → 감소
메모리 릭이 있으면 GC 이후의 최소 사용량이 점점 높아지는 형태가 나타난다.
메모리 릭 의심
증가 → GC → 조금 감소 → 더 증가 → GC → 조금 감소
순간적인 메모리 사용량보다 GC 이후에도 남아 있는 메모리의 장기적인 추세를 확인해야 한다.
9. 메모리 릭 분석 방법
JVM 메트릭 확인
다음 항목을 먼저 확인한다.
- Heap 사용량
- Old Generation 사용량
- GC 횟수
- Full GC 횟수
- GC 소요 시간
- 객체 할당 속도
- OutOfMemoryError 발생 여부
Heap Dump 수집
Heap Dump는 특정 시점에 Heap에 존재하는 객체와 참조 관계를 저장한 자료다.
분석할 때는 다음 내용을 확인한다.
- 어떤 클래스의 객체가 가장 많은가?
- 어떤 객체가 가장 많은 메모리를 점유하는가?
- 해당 객체를 누가 참조하고 있는가?
- GC Root까지 어떤 참조 경로가 이어지는가?
- 동일한 객체가 지속해서 증가하고 있는가?
대표적인 분석 도구로 Eclipse MAT, VisualVM 등의 도구를 사용할 수 있다.
운영 환경에서 Heap Dump를 생성하면 서버 성능과 디스크 공간에 영향을 줄 수 있으므로 충분한 공간과 영향도를 확인해야 한다.
Thread Dump와 함께 분석
스레드가 작업을 계속 쌓거나 처리하지 못하는 상황이라면 메모리 증가 원인이 작업 대기열일 수 있다.
따라서 메모리 지표, Heap Dump, Thread Dump와 요청량을 함께 비교하는 것이 좋다.
10. 메모리 릭 대응 및 예방
- 컬렉션에 최대 크기 설정
- 사용 완료된 데이터 제거
- 캐시에 만료 시간과 제거 정책 적용
- Listener와 Callback 해제
- ThreadLocal.remove() 호출
- 자원에 try-with-resources 적용
- 무제한 작업 대기열 사용 금지
- 대용량 객체를 불필요하게 메모리에 보관하지 않기
- Heap과 GC 지표 모니터링
- 부하 테스트와 장시간 실행 테스트 수행
서버 재시작은 메모리를 초기화해 일시적으로 서비스를 복구할 수 있지만 근본적인 해결은 아니다. 재시작 전후의 지표와 Heap Dump를 바탕으로 참조가 해제되지 않는 원인을 찾아야 한다.
캐시 압력
11. 캐시 압력이란?
캐시 압력(Cache Pressure)은 캐시에 저장하려는 데이터와 요청이 캐시가 감당할 수 있는 용량 또는 처리 능력을 넘어서는 상태를 의미한다.
캐시는 원본 DB나 외부 API보다 빠르게 데이터를 제공해 성능을 높인다. 하지만 캐시도 메모리와 네트워크 등 제한된 자원을 사용한다.
데이터 증가
→ 캐시 메모리 부족
→ 기존 데이터 제거 증가
→ Cache Miss 증가
→ DB 요청 증가
→ 응답 지연과 DB 장애 가능성
캐시가 부족해지면 단순히 캐시만 느려지는 것이 아니라 원본 저장소까지 함께 과부하될 수 있다.
12. 캐시 압력이 발생하는 원인
- 캐시 데이터가 예상보다 빠르게 증가함
- 최대 크기가 설정되지 않음
- TTL이 지나치게 김
- 큰 객체를 캐시에 저장함
- 한 번만 사용하는 데이터를 모두 캐싱함
- 특정 키에 요청이 집중됨
- 캐시 키가 불필요하게 세분화됨
- 만료 시점이 한꺼번에 몰림
- 트래픽이 급격히 증가함
- 캐시 노드의 메모리 또는 네트워크 부족
애플리케이션 로컬 캐시라면 JVM Heap을 함께 사용하므로 과도한 캐싱이 메모리 릭과 유사한 장애로 이어질 수도 있다.
13. 캐시 압력으로 발생하는 문제
Cache Hit Ratio 감소
필요한 데이터가 캐시에 없어서 DB나 외부 API를 다시 호출하는 비율이 증가한다.
Cache Hit Ratio
= Cache Hit / 전체 캐시 조회 수
Eviction 증가
캐시 공간이 부족해 기존 데이터가 지속해서 제거된다. 자주 사용하는 데이터까지 제거되면 캐시 효율이 급격히 떨어진다.
DB 부하 증가
캐시에서 처리하지 못한 요청이 DB로 몰려 응답 시간이 증가하고 DB 커넥션 풀이 고갈될 수 있다.
Cache Stampede
인기 데이터가 만료되는 순간 여러 요청이 동시에 DB로 향하는 현상이다.
인기 상품 캐시 만료
→ 1,000개 요청이 동시에 Cache Miss
→ 1,000개 요청이 DB 조회
→ DB 부하 급증
Hot Key
특정 캐시 키 하나에 요청이 집중돼 하나의 노드나 네트워크 구간에 과도한 부하가 발생할 수 있다.
14. 캐시 압력 대응 방법
적절한 최대 크기 설정
캐시가 무제한으로 증가하지 않도록 최대 엔트리 수나 최대 메모리를 제한한다.
로컬 캐시를 사용한다면 Caffeine과 같은 캐시에서 크기와 만료 시간을 설정할 수 있다.
Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(10))
.build();
적절한 TTL 적용
데이터 특성에 맞는 만료 시간을 설정한다.
- 자주 변경되는 데이터: 짧은 TTL
- 변경이 적은 데이터: 상대적으로 긴 TTL
- 정확성이 중요한 데이터: 캐시 적용 여부를 신중하게 결정
TTL이 너무 짧으면 Cache Miss가 증가하고, 너무 길면 오래된 데이터가 반환될 수 있다.
TTL 분산
여러 캐시가 같은 순간 만료되지 않도록 TTL에 무작위 값을 추가할 수 있다.
기본 TTL 10분
+ 0~2분의 무작위 지연
요청 병합
동일한 키에 여러 Cache Miss가 발생했을 때 하나의 요청만 DB를 조회하고 나머지는 그 결과를 기다리도록 구성할 수 있다.
캐시 워밍
배포나 캐시 재시작 후 주요 데이터를 미리 적재해 초기 요청이 모두 DB로 몰리는 것을 방지한다.
캐시할 데이터 선별
모든 데이터를 캐싱하기보다 다음 조건을 만족하는 데이터를 우선 캐싱한다.
- 반복 조회가 많음
- 원본 조회 비용이 큼
- 변경 빈도가 낮음
- 일정 수준의 오래된 값을 허용할 수 있음
15. 캐시 모니터링 지표
다음 지표를 함께 확인해야 한다.
- Cache Hit와 Miss 수
- Cache Hit Ratio
- 메모리 사용량
- 캐시 엔트리 수
- Eviction 횟수
- 만료된 키의 수
- 명령 처리 지연 시간
- 캐시 연결 수
- Hot Key
- Cache Miss 이후 DB 요청량
- 캐시 장애 시 DB가 감당할 수 있는 부하
적중률이 높더라도 특정 키에 요청이 집중되거나 캐시 응답 시간이 느릴 수 있다. 하나의 지표만으로 캐시 상태를 판단해서는 안 된다.
설정 버전 관리
16. 설정도 코드처럼 관리해야 하는 이유
애플리케이션 장애는 코드뿐 아니라 설정 변경으로도 발생한다.
예를 들면 다음과 같다.
- DB 커넥션 풀 크기를 지나치게 작게 설정
- 외부 API Timeout 제거
- 잘못된 DB 주소 입력
- 로그 레벨을 DEBUG로 변경해 디스크 사용량 급증
- 스레드 풀 크기를 과도하게 증가
- 캐시 TTL 단위를 잘못 입력
- Feature Flag를 잘못 활성화
- 운영 환경에 개발용 설정 적용
코드는 변경 이력이 남아 있어도 서버 설정을 관리자가 직접 수정하면 누가, 언제, 왜 바꿨는지 알기 어렵다. 문제가 발생해도 이전 값으로 복구하기 힘들어진다.
따라서 설정도 코드와 동일하게 버전 관리해야 한다.
17. 설정 버전 관리의 핵심 원칙
변경 이력 기록
설정 변경에는 다음 정보가 남아야 한다.
- 변경한 사람
- 변경 시각
- 변경 이유
- 변경 전후 값
- 적용 대상 환경
- 관련 작업 또는 장애 번호
환경별 설정 분리
개발, 테스트, 스테이징, 운영 환경의 설정을 구분해야 한다.
application.yml
application-local.yml
application-test.yml
application-prod.yml
공통 설정과 환경별 설정을 나누되 설정의 출처가 너무 복잡해지지 않도록 관리해야 한다.
비밀 정보 분리
다음 값은 Git 저장소에 직접 저장하면 안 된다.
- DB 비밀번호
- API Key
- 인증 토큰
- 개인키
- 암호화 키
비밀 정보는 환경 변수나 별도의 Secret 관리 시스템을 사용해야 한다.
spring:
datasource:
password: ${DB_PASSWORD}
검토와 승인
중요한 운영 설정은 한 사람이 즉시 변경하기보다 코드 리뷰나 승인 과정을 거치는 것이 안전하다.
자동화된 검증
배포 전에 다음 항목을 검증할 수 있다.
- 필수 설정 존재 여부
- 숫자 범위
- 시간 단위
- URL 형식
- 환경별 금지 값
- 서로 충돌하는 설정
- 비밀 정보 포함 여부
18. 설정 변경으로 인한 장애 예방
설정 스키마와 타입 사용
문자열 설정을 코드 곳곳에서 직접 읽기보다 타입이 있는 설정 클래스로 관리하면 잘못된 값을 빠르게 발견할 수 있다.
@ConfigurationProperties(prefix = "app.cache")
@Validated
public record CacheProperties(
@NotNull Duration ttl,
@Min(1) int maximumSize
) {
}
다음과 같이 명확한 단위도 함께 사용하는 것이 좋다.
app:
cache:
ttl: 10m
maximum-size: 10000
600처럼 단위가 불분명한 값보다 10m처럼 의미가 분명한 설정이 안전하다.
설정 변경 전 영향도 확인
DB Pool 크기 변경
→ DB 최대 연결 수에 미치는 영향
캐시 TTL 변경
→ Cache Miss와 DB 부하에 미치는 영향
Timeout 증가
→ 스레드와 커넥션 점유 시간에 미치는 영향
하나의 설정 변경이 다른 시스템 자원에 어떤 영향을 주는지 함께 확인해야 한다.
점진적으로 적용
설정을 전체 인스턴스에 한 번에 적용하지 않고 일부 인스턴스부터 적용해 지표를 확인할 수 있다.
즉시 롤백 준비
설정 변경 전에 이전 값을 기록하고 문제가 발생하면 빠르게 원래 설정으로 되돌릴 수 있어야 한다.
변경 직후 모니터링
설정을 변경한 뒤에는 다음 항목을 확인한다.
- 에러율
- 응답 시간
- CPU와 메모리
- DB 커넥션 풀
- 캐시 적중률
- 외부 API 실패율
- 주요 비즈니스 지표
19. 설정 드리프트
설정 드리프트(Configuration Drift)는 같은 역할을 하는 서버들의 설정이 서로 달라지는 현상이다.
서버 A: Timeout 1초
서버 B: Timeout 5초
서버 C: Timeout 설정 없음
이 경우 같은 요청도 어느 서버로 전달되는지에 따라 결과가 달라질 수 있다. 장애가 특정 서버에서만 발생해 원인 분석도 어려워진다.
설정 드리프트를 방지하려면 다음 방법을 사용할 수 있다.
- 설정 저장소를 단일화
- 배포 자동화
- 서버 직접 수정 제한
- 주기적인 설정 비교
- 설정 버전 또는 해시 기록
- 애플리케이션 시작 시 설정 검증
로그와 모니터링 정보에 애플리케이션 버전뿐 아니라 설정 버전도 함께 기록하면 장애 분석에 도움이 된다.
20. 네 가지 장애의 연결 관계
DB 복제 지연, 메모리 릭, 캐시 압력과 설정 오류는 독립적으로 발생할 수도 있지만 서로 영향을 주며 장애를 확대할 수 있다.
잘못된 설정으로 캐시 TTL이 너무 짧아짐
→ Cache Miss 증가
→ Primary와 Replica의 조회 부하 증가
→ DB 복제 지연 발생
→ 오래된 데이터 조회
→ 사용자 재시도 증가
→ 애플리케이션 요청 및 메모리 사용량 증가
또 다른 예시는 다음과 같다.
제한이 없는 로컬 캐시 적용
→ 캐시 엔트리 지속 증가
→ JVM Heap 압박
→ Full GC 증가
→ API 응답 지연
→ 요청 대기열 증가
→ 메모리 부족과 서비스 장애
따라서 장애를 분석할 때는 가장 먼저 보이는 증상만 원인으로 단정하지 않아야 한다.
21. 장애 대응 체크리스트
DB 복제 지연
- Primary와 Replica의 지연 시간을 확인했는가?
- 쓰기 직후 조회가 Replica로 전달되는가?
- 중요한 비즈니스 판단에 오래된 데이터를 사용하지 않는가?
- 대량 쿼리나 장기 트랜잭션이 존재하는가?
- 지연이 심할 때 Primary로 전환할 방법이 있는가?
메모리 릭
- GC 이후 Heap 사용량도 계속 증가하는가?
- 제한 없이 증가하는 컬렉션이나 캐시가 있는가?
- ThreadLocal 값을 제거하는가?
- Listener 및 자원을 정상적으로 해제하는가?
- Heap Dump와 GC 로그를 확보했는가?
캐시 압력
- 캐시에 최대 크기가 설정돼 있는가?
- 데이터별 TTL이 적절한가?
- Cache Hit Ratio와 Eviction을 모니터링하는가?
- Cache Stampede 방지 장치가 있는가?
- 캐시 장애 시 DB가 요청을 감당할 수 있는가?
설정 버전 관리
- 설정 변경 이력이 남아 있는가?
- 운영 설정 변경에 검토 절차가 있는가?
- 비밀 정보가 코드 저장소에서 분리돼 있는가?
- 설정값의 타입과 범위를 검증하는가?
- 이전 설정으로 빠르게 롤백할 수 있는가?
- 인스턴스 사이에 설정 드리프트가 없는가?
마무리
이번 학습을 통해 장애는 코드 오류뿐 아니라 데이터 복제 구조, 메모리 참조, 캐시 정책, 설정 변경에서도 발생할 수 있다는 것을 알게 되었다.
핵심 내용을 정리하면 다음과 같다.
DB 복제 지연
→ 쓰기와 읽기의 시점 차이로 오래된 데이터가 조회될 수 있다.
메모리 릭
→ 필요 없는 객체의 참조가 유지되면 GC가 메모리를 회수하지 못한다.
캐시 압력
→ 캐시 용량과 처리 한계를 넘으면 DB까지 부하가 확산될 수 있다.
설정 버전 관리
→ 설정도 코드처럼 변경 이력, 검증, 배포와 롤백이 필요하다.
이러한 장애는 순간적인 수치보다 시간에 따른 변화와 구성요소 사이의 관계를 관찰해야 원인을 찾을 수 있다.
결국 안정적인 서비스를 만들기 위해서는 코드뿐 아니라 DB 복제 상태, JVM 메모리, 캐시 효율과 설정 변경까지 하나의 운영 시스템으로 관리해야 한다.
'Server - Architecture > 장애대응' 카테고리의 다른 글
| [장애대응] 낙관적 락과 데드락 실습 정리 (0) | 2026.08.24 |
|---|---|
| [장애대응] DB Lock과 비관적 락 실습 정리 (0) | 2026.08.24 |
| [장애대응] 장애 예방 조치 정리 (1) | 2026.08.24 |
| [장애대응] 장애 복구와 후속 조치 및 사후 평가 개선 정리 (0) | 2026.08.24 |
| [장애대응] 모니터링과 장애 분석 및 진단 정리 (0) | 2026.08.24 |