내일배움캠프

[내일배움캠프] DB 복제 지연, 메모리 릭, 캐시 압력 및 설정 버전 관리

munsik22 2026. 6. 5. 14:24

🧩 DB 복제 지연 (Replication Lag)

무슨 문제인가?

읽기/쓰기 DB를 분리한 구조에서, 쓰기 DB(마스터)에 데이터를 저장한 직후 읽기 DB(레플리카)에서 조회하면 방금 저장한 데이터가 없을 수 있다.

[앱] --쓰기--> [마스터 DB]
                    |
               (복제 지연)
                    ↓
              [레플리카 DB] <--읽기-- [앱] ← 이전 데이터가 반환됨

예를 들어 회원가입 직후 프로필 페이지로 리다이렉트했는데 "존재하지 않는 사용자"가 뜨는 버그가 이 패턴에서 자주 발생할 수 있다.

해결 전략

복제 지연 자체를 없앨 수는 없다. 애플리케이션 레이어에서 이를 인식하고 설계해야 한다.

전략 설명 적합한 상황
Read-after-Write 쓰기 직후의 읽기는 마스터에서 직접 수행 사용자 본인의 데이터 조회
Lag Compensating 쓰기 후 잠시 대기 후 재시도 일관성 요구가 낮은 경우
Semi-sync Replication 복제 확인 후 쓰기 완료 처리 복제 지연 자체를 줄이고 싶을 때
캐시 갱신 쓰기 후 캐시를 즉시 업데이트하고 이후 읽기는 캐시에서 제공 읽기가 매우 빈번한 경우
CQRS 쓰기/읽기 모델을 아키텍처 수준에서 분리 복잡한 도메인, 대규모 시스템

🧩 메모리 릭 (Memory Leak)

무슨 문제인가?

더 이상 필요하지 않은 메모리를 해제하지 않아 메모리 사용량이 계속 증가하는 현상이다. JVM 기반의 Spring 애플리케이션에서 자주 발생하는 원인은 다음과 같다.

  • 이벤트 리스너 등록 후 미해제
  • 전역 변수에 대용량 객체 참조 유지
  • 캐시 객체의 무한 증가
  • ThreadLocal 미정리 ← 가장 흔한 함정

ThreadLocal 메모리 릭 패턴

스레드 풀을 사용하는 환경에서 ThreadLocal을 사용하고 remove()를 호출하지 않으면, 스레드가 재사용될 때 이전 스레드의 데이터가 그대로 남아있는다.

// 위험한 코드
executorService.submit(() -> {
    threadLocal.set("someValue");
    // threadLocal.remove() 호출 없이 작업 종료
    // → 스레드가 풀에 반환된 후에도 메모리 점유 지속
});

// 안전한 코드
executorService.submit(() -> {
    try {
        threadLocal.set("someValue");
        // ... 처리 로직
    } finally {
        threadLocal.remove(); // 항상 finally에서 정리
    }
});

테스트 환경에서는 메모리 릭이 에러로 드러나지 않고, 장기 운영 후 메모리 사용량이 점진적으로 증가하는 형태로만 나타난다.

Spring에서 주의할 점

  • 싱글톤 빈 내부에 컬렉션을 필드로 두고 계속 추가만 하면 릭이 된다.
  • @Autowired 순환 참조는 의도치 않은 객체 생존을 일으킬 수 있다.
  • 요청 스코프 빈을 ThreadLocal로 처리할 때는 요청 종료 시 반드시 정리한다.

🧩 캐시 압력 (Cache Pressure)

무슨 문제인가?

캐시가 가득 차서 새로운 데이터를 저장하지 못하거나, 자주 쓰이는 데이터가 밀려나는 상황이다. 사용자별 페이징 데이터를 캐시에 통째로 저장하는 패턴이 대표적인 원인이다.

// 안티패턴: 페이지 전체 데이터를 키로 저장
{
  "user123_page_1": { "data": [ {...item1}, {...item2}, ... ] },
  "user456_page_1": { "data": [ {...item1}, {...item2}, ... ] }
}

사용자 수 × 페이지 수만큼 캐시 항목이 폭발적으로 증가하고, 히트율은 낮아지는 최악의 조합이다.

해결: 캐시를 분리하자

페이지 ID 리스트와 아이템 상세 데이터를 분리해서 캐싱하면 중복 저장을 줄이고 재사용성을 높일 수 있다.

// 페이징 캐시: 어떤 아이템이 있는지만 기록
{ "user123_page_1": ["item1", "item2", "item3"] }

// 아이템 캐시: 실제 데이터는 공유
{ "item1": { "id": "item1", "name": "Item One", ... } }

조회 시 ① 페이지 ID 리스트 → ② 각 ID로 아이템 상세 조회 → ③ 조합하여 응답

다른 해결 전략

  • TTL 설정: 오래된 캐시가 자동 만료되어 공간 확보
  • 최대 캐시 크기 제한: LRU 등의 정책으로 오래된 항목 자동 제거
  • 분산 캐시(Redis): 단일 서버 메모리 한계 극복
  • 캐시 우회: 세분화된 요청은 캐시 없이 DB에서 직접 조회

캐시 백오프(Backoff) 주의

캐시가 일시적으로 비활성화되면 모든 트래픽이 DB로 쏟아지는 캐시 스탬피드(Cache Stampede) 가 발생할 수 있다. 캐시 우회 전략을 쓸 때는 DB 부하 급증 상황을 항상 염두에 두어야 한다.

🧩 설정 버전 관리 (Configuration Versioning)

무슨 개념인가?

애플리케이션 설정에 버전(또는 타임스탬프)을 부여해서, 시간 순으로 이력을 관리하는 방법이다. 항상 최신 버전의 설정을 사용하되, 문제 발생 시 이전 버전으로 즉시 롤백할 수 있다.

Spring + JPA 구현 예시

@Entity
public class CoreSetting {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(columnDefinition = "jsonb")
    private String configData;

    private LocalDateTime version;  // 이 버전 기준으로 정렬

    @CreationTimestamp
    private LocalDateTime createdAt;
}
// 최신 설정 조회: version 내림차순으로 첫 번째 항목
@Query(value = "SELECT * FROM CoreSetting ORDER BY version DESC LIMIT 1", nativeQuery = true)
Optional<CoreSetting> findLatestCoreSetting();

새 설정을 저장할 때 기존 행을 수정하지 않고 새 행을 추가하는 것이 핵심이다. 덮어쓰면 이력이 사라진다.

장단점 요약

장점

  • 장애 시 이전 설정으로 즉시 롤백 가능
  • 설정 변경 이력 추적으로 원인 분석 용이
  • 운영 배포 전 테스트 환경에서 사전 검증 가능

단점

  • 버전이 쌓일수록 저장 공간 증가
  • 관리 복잡도 상승 (정기적인 오래된 버전 정리 필요)
  • 잘못된 버전 롤백 시 인적 오류 가능성

여담으로 설정 행에 description 컬럼을 추가해두면 "왜 이 설정으로 바꿨는지"를 기록할 수 있어 나중에 이력을 추적하기 훨씬 편하다.

🧩 정리

주제 핵심 교훈
DB 복제 지연 지연은 제거할 수 없다. 애플리케이션이 이를 인식하고 설계해야 한다.
메모리 릭 ThreadLocal은 반드시 finally에서 remove(). 테스트에서는 릭이 보이지 않는다.
캐시 압력 전체 데이터를 캐시하지 말고, ID 리스트와 상세 데이터를 분리해서 캐시하자.
설정 버전 관리 설정을 덮어쓰지 말고 버전별로 누적 저장하면 롤백과 이력 추적이 쉬워진다.

'내일배움캠프' 카테고리의 다른 글

[내일배움캠프] RAG  (0) 2026.06.10
[내일배움캠프] Spring AI  (0) 2026.06.09
[내일배움캠프] DB Lock  (0) 2026.06.04
[내일배움캠프] 시큐어 코딩  (0) 2026.06.04
[내일배움캠프] 모니터링 시스템  (0) 2026.06.02