ASM 모듈의 역할과 기존 구조 문제점

1. ASM 모듈이란?

  • ASM(Agent State Manager) 모듈은 상담원의 상태를 관리하는 핵심 컴포넌트
    • 상담원 상태 관리: 로그인/로그아웃, READY(상담 가능), NOT_READY(휴식/이석) 등 관리
    • 상태 변경 이벤트 처리: 상태 변경 이벤트를 수신하고 처리하여 다른 시스템에 기준 데이터 제공
    • 기준 데이터 제공: 상담 가능 여부 판단, 상담 배분(Routing) 엔진이 상담원을 선정시 최우선 판단 기준
  • 상담 시스템에서 상담원의 상태는 모든 로직의 출발점에 해당됨
  • ASM 모듈은 타 모듈 및 외부 시스템 연동 진입점 역할을 함

2. 기존 로그인 흐름

  • 상담원이 상담 애플리케이션에 접속하면, 채널별로 로그인 상태를 확인함
    • 전화 상담 로그인: CTI 시스템을 통해 전화 상담 가능 여부 확인
    • 멀티미디어 상담 로그인: ASM 모듈을 통해 멀티미디어 상담 가능 여부 확인
  • 아래 구조에서 멀티미디어 상담의 정상 동작 여부는 ASM 모듈의 상태와 응답에 강하게 의존하고 있음

image.png

3. 기존 구조의 문제점

  • (1) DB 중심의 강함 결합 구조
    • 기존 시스템은 모든 상태 변경이 발생할 때마다 DB에 직접 Write하고, 상담 인입 시마다 DB를 Read하여 상태를 확인하는 구조
      • 상담원 상태 변경시 → 즉시 DB 반영
      • 상담 가능 여부 판단 → DB 조회를 통해 상태 확인
      • 상담 배분 모듈 → 상담원 선정을 위해 DB 조회 수행
      • ASM 모듈 정상 여부 판단 → DB 헬스체크 결과에 의존
    • 즉, 상담원의 상태의 변경, 조회, 모듈 정상 여부 판단이 모두 DB 연결 상태에 강하게 결합된 구조
  • (2) DB 장애가 곧 서비스 장애로 확산
    • 이 구조에서 부분 장애를 전체 장애로 확대시키는 단일 장애 지점(SPOF)이 존재하게 됨
    • DB 연결이 불안정해지는 순간 발생하는 문제
      1. 연쇄 장애: DB 네트워크에 미세한 순단만 발생해도 ASM 모듈의 헬스체크가 실패
      2. 서비스 마비: ASM이 비정상이 되면 상담원 앱은 강제 로그아웃 처리되며, DB가 복구될 때까지 상담사는 로그인을 할 수 없는 ‘전면 업무 중단’ 상태에 빠짐
      3. 확장성 저하: 상담 인입이 몰릴수록 상태 조회 쿼리가 급증하여 DB 부하를 가중시킴

개선 전략: 캐시를 통한 장애 격리

DB 장애 상황에서도 상담원이 최소한의 상담 업무는 지속할 수 있도록 변경하고자 함

1. 개선 목표

  • 부분 장애가 전체 장애로 확대되는 구조적 한계 개선
  • DB가 일시적으로 불안정해도 상담원 상태 관리와 상담 배분은 계속 가능해야 함
  • DB 장애 상황에서도 시스템이 스스로 버틸 수 있도록 하는 것이 목표

2. 왜 인메모리인가?

  • 상담원 상태 데이터는 다음과 같은 특성이 있음
    • 전체 상담원 수는 수천 명 단위로, 메모리에 충분히 적재 가능
    • 조회(Read) 빈도가 쓰기(Write)보다 압도적으로 높음
      • 상담원 상태 변경 시점: 출퇴근시, 휴식/복귀, 일정 시간 상담 수락 불가시 상태 전환 등
      • 상담원 인입시 모든 상담원 상태 조회 (콜이 많을 수록 빈도 증가)
    • 복잡한 조인이나 검색보다는 Key-Value 형태의 접근이 주를 이룸
  • 상담원 상태를 인메모리로 관리시 장점
    • 조회 성능 향상
    • DB가 장애 상황에서도 상담원 상태 변경 및 조회 유지 가능

캐시 디자인 패턴

1. Cache 목적

  • 성능 향상 및 부하 감소
  • 캐시는 application과 영속적 데이터저장소 사이에서 임시 저장소 역할
  • 일반적인 시스템에서 캐시는 Read 성능을 극대화하는 용도로 사용됨
  • 그러나 Write 요청이 많은 시스템에서의 캐시는 쓰기 버퍼 역할을 담당하기도 함
    • 쓰기 요청 건마다 개별 처리하는 것이 아니라
    • 캐시에 버퍼링된 쓰기 요청을 적당히 모아 한 번에 처리하는 것
    • 네트워크 오버헤드를 상당 부분 감소시킬 수 있는 장점 있음
    • Write-Behind 패턴 구조

2. Cache Aside 패턴 (Lazy Loading, 지연로딩 패턴)

image.png

  • 가장 많이 사용되는 캐시 패턴
  • 해당 데이터를 Application이 필요로 할 때 캐시에 로딩되므로 지연 로딩으로 불림
  • 캐시와 영속적 데이터 저장소는 병렬로 구성되며, 애플리케이션은 양쪽 소스에 직접 연결됨
  • 애플리케이션이 데이터를 읽어가는 과정은 2-Phase로 동작함
    • 우선 원하는 데이터가 캐싱되어 있는지 체크한 후에 값이 있다면 그 값을 반환
    • 없다면 데이터 저장소에 가서 원하는 값을 읽어옴
    • 가져온 값은 캐시에 저장한 후 애플리케이션에 반환
장점 • 캐시 미스(Cache Miss, 캐시에 데이터가 없다는 뜻)가 치명적이지 않음
⇒ 데이터 저장소에서 읽어와 적재하면 됨
• 클라이언트가 요청한 데이터에 한해서 캐싱하므로 캐시 히트율이 높음
◦ 리소스를 효율적으로 사용할 수 있음
◦ 파레토의 법칙에 따라, 전체 데이터의 20% 리소스로도 80% 정도의 요청을 캐싱할 수 있음
• 구조가 단순하여 기존 코드에 손쉽게 적용할 수 있음
단점 • 항상 2단계로 체크해야 하므로 읽기 과정이 복잡해짐
• 캐시 미스가 발생해야 데이터 저장소에서 데이터를 읽어오므로, 캐싱된 데이터가 최신이 아닐 수 있음
• Cold Start 상태에서 성능을 발휘하기 위해서는 Warm-Up 시간이 필요함
◦ ColdStart 란? 캐싱된 데이터가 없는 상태로 시스템이 시작되는 경우 (↔ Warm Start )
◦ ColdStart 상황에서 접속이 대량으로 발생할 경우,
여러 곳에서 한꺼번에 데이터 저장소의 원본 데이터를 요청하는 Thundering Herd 문제가 발생함

3. Read-Through 패턴 (Inline Cache 패턴)

%ED%94%8C%EB%9E%AB%ED%8F%BC%EC%85%80_2020_08_-_Google_Slides.png

  • 캐시는 Application과 DB 사이에 위치하며, Application은 캐시에만 연결됨
  • Application과 DB의 연결선 상에 Cache가 위치한다고 하여 인라인 캐시로 불림
  • 캐시의 적재 및 갱신은 비동기적으로 이루어짐
  • 주기적으로 배치 담당 Application이 데이터 저장소의 데이터를 읽어와 캐시에 적재함
  • Read-Through 패턴은 전체 데이터를 캐싱하는 일괄 처리 작업 특성상, 대량 데이터를 캐싱하기는 어려움
    • 주로 다음과 같이 한정적 범위를 가지는 데이터를 캐싱하는 데 사용됨
      • 국가별 환율 데이터
      • 현재 노출 중인 공지 목록
      • 어제까지의 누적 방문자 수
      • 모바일 앱의 메인 페이지 콘텐츠 데이터
장점 • 캐시 적재 요청을 단일화할 수 있기 때문에, Cold Start 상황에서도 큰 문제가 발생하지 않음
• Application은 캐시를 최종 데이터 저장소처럼 인식하므로, 데이터를 읽어오는 로직을 단순화할 수 있음
단점 • 주기적으로 캐시와 DB 사이를 동기화할 배치 어플리케이션을 만들고 관리해야 함
• 캐시 히트율과 상관없이 모든 데이터를 캐싱하기 때문에, 캐싱된 데이터가 반드시 읽힌다는 보장이 없음
⇒ 캐시 히트율이 낮아질 수 있음
• DB에 저장되어 있더라도 캐시 공간 부족으로 일부 데이터가 캐싱되지 못하면 Applicaion도 참조 불가

3. Write-Through 패턴

image.png

  • 캐시는 Application과 DB 사이에 위치하지만, 쓰기 시점에 캐시가 적재됨
  • Application은 저장할 데이터를 먼저 캐시에 기록한 후 이어서 데이터 저장소에도 업데이트함
    • 해당 과정은 동기적으로 처리되기 때문에 쓰기 속도가 느려짐
    • 처리 과정
      • 캐시에 데이터를 저장함. 이미 있는 경우에는 업데이트함
      • DB에 데이터를 추가하거나 업데이트함
      • 처리 결과를 애플리케이션에 응답함
  • 동일한 데이터에 대한 읽기는 상대적으로 빠르게 동작함
  • 읽을 때가 아니라 쓸 때 캐싱하는 방식이므로 캐싱된 데이터를 최신으로 유지할 수 있음
  • 다만 캐시에 쓰여진 대부분의 데이터가 반드시 읽힌다는 보장은 없음 (즉, 캐시 히트율이 낮아질 수 있음)
장점 • 쓰기 처리 시에 캐싱되므로, 캐시를 최신 데이터로 유지할 수 있다.
단점 • 쓰기 지연이 발생함
• 캐싱된 대부분의 데이터가 반드시 읽힌다는 보장이 없음 ( 즉, 리소스 낭비를 야기함)

4. Write-Behind 패턴 (Write-Back)

image.png

  • Write-Through의 쓰기 성능 문제에 대한 대안으로 사용되는 패턴
  • 데이터를 먼저 캐시에 기록하고, 결과를 먼저 애플리케이션에 반환함
  • 캐시에 저장된 데이터는 이후 별도의 비동기 프로세스를 이용하여 데이터 저장소에 업데이트함
    • 즉, 이 패턴에서 캐시는 일종의 쓰기 버퍼 역할을 함
    • 더티 캐시(Dirty Cache)
      • 캐시에만 저장되고 아직 데이터 저장소에 반영되지 않은 것을 말함 (Dirty Block 이라고도 함)
      • Dirty Cache는 비동기 처리 애플리케이션을 통해 데이터 저장소에 동기화하기 때문에 배치 처리를 담당할 별도의 애플리케이션이 필요 함
      • Read-Through 패턴과 유하사하지만 배치 처리 프로세스의 흐름 방향이 반대임
  • 쓰기 요청을 한 번에 모아서 처리하므로 네트워크 오버헤드를 감소시킬 수 있음
    • 쓰기 성능이 향상됨 = 쓰기가 많은 작업에 적합한 패턴
    • 예를 들어 로깅이나 이벤트 수집, Compaction 처리가 필요한 이벤트 소싱 데이터 등에 적용하기 좋음
  • 처리 과정
    • 애플리케이션이 캐시에 데이터를 추가하거나 업데이트함
    • 캐시 레벨에서 기록이 끝나면 결과를 반환함
    • 이후 캐시에서 비동기적으로 데이터 저장소에 업데이트함
장점 • 네트워크 오버헤드를 감소시켜 쓰기 성능을 향상시킬 수 있음
단점 • 다른 캐시 전략에 비해 구현이 어렵다.
• 캐시에 업데이트된 후 아직 데이터 저장소에 업데이트 되기 전 더티 캐시 가 다운되면 데이터 손실이 발생함

해결 방안 1: Write-Behind 패턴과 Redis 사용

  • 캐시 디자인 패턴 중 가능한 대안
    • DB 장애 상황에서도 ASM 모듈이 정상 동작하기 위해서는 캐시에 우선적으로 데이터를 저장해야 함
    • Write-Through 패턴Write-Behind 패턴 사용 가능
    • 두 패턴의 차이는 영구 데이터 저장소에 반영 시점 차이
  • Write-Behind 패턴과 Redis 사용 고려
    • Write-Behind 패턴
      • 캐시에 먼저 쓰고 DB에 비동기 반영하므로 DB 의존도를 가장 최소화
      • DB 장애 발생 후 정상화시, 영구저장소에 적재되지 않은 데이터를 비동기적으로 적재할 수 있음
    • Redis 사용
      • 여러 서버에서 상담원 상태를 일관되기 관리 가능
      • 서버 재시작/배포 시에도 상태 유실 위험 감소
      • TTL 활용하여 운영 종료시 자동 로그아웃 처리도 용이함

현실적 제약: Redis 사용 불가

  • 인프라 운영 부담: 고객사 인프라 정책상 미들웨어(Redis) 설치 불가한 경우 존재

  • 운영 포인트 증가 및 비용 이슈

  • 팀 내부 논의 결과

해결 방안 2: Write-Through 패턴 사용

  • Redis 없이 로컬 Map에서 관리시 발생하는 문제
    • Redis를 사용시 모든 모듈이 DB에 의존하지 않고, 인메모리 캐시를 바라보도록 변경할 예정이었음
    • Redis 사용 대신 ASM 모듈 내부에서 Map으로 상담원 상태 관리 방식 사용 결정
    • 현재 구조상 발생하는 문제
      • 타 모듈들은 ASM 이벤트를 통해서 동작하지 않고, DB의 상담원 상태 관련 테이블을 기준으로 동작
      • ASM 내부 캐시와 DB 상태가 어긋나면 실시간 일관성 붕괴 발생
    • 로컬 Map에만 저장하면 DB를 바라보는 다른 모듈들과의 정합성을 어떻게 맞출 수 있을 것인가?
      • 다른 모듈에서는 ASM 모듈의 로컬 Map의 실시간 최신 데이터를 접근 불가
      • Write-Through 기반의 이중 구조를 설계 고려
  • 설계의 핵심
    • ASM 모듈 내부의 ConcurrentHashMap이 항상 최신 상태를 유지하는 기준 데이터가 됨
    • 쓰기 시점에 AMS 모듈 내부 인메모리(Map)와 영구 데이터 저장소에 모두 저장하는 방식 사용 → 동시에 저장하므로 일관성 붕괴 최소화
    • 모든 상태 조회는 DB를 거치지 않고 메모리에서 즉시 반환 → 조회 성능 향상

왜 ConcurrentHashMap 사용?

  • 단일 JVM 환경에서 멀티스레드 접근에 대한 안정성과 고성능을 동시에 확보하기 위함

  • HashMap은 Thread-Safe하지 않음

  • Hashtable이나 SynchronizedMap은 맵 전체에 락이 걸기 때문에 성능 저하가 큼
    → 한 스레드가 작업 중이면 다른 모든 스레드가 대기해야 하는 병목 현상 발생

  • ConcurrentHashMap

  • 장애 복구 시나리오 (Self-Healing)
    • DB 장애 시에도 상담 업무를 지속하기 위한 세부 로직을 구축
    • DB 장애 중:
      • 상태 변경/조회는 메모리에서 정상 처리 (상담 업무 유지)
      • 히스토리 로그는 유실 방지를 위해 내부 인메모리 Queue에 임시 적재
    • DB 복구 시:
      • 실시간 상태: 메모리 데이터를 기준으로 DB 테이블을 재생성하여 동기화
      • 이력 데이터: Queue에 쌓인 데이터를 순차적으로 DB에 Write.
  • 현재 구조의 한계와 향후 과제
    • 현재 구조는 장애 상황에서도 상담 업무는 계속할 수 있게 한다는 목표를 달성함
    • 하지만 이 구조에서도 한계는 있음
      • 단일 서버 제약: ASM 모듈 내부 메모리를 사용하므로 다중 서버일 경우, 서버 간 상태 불일치 발생
      • 의존성 전이: 현재 상담 배분 로직이 DB Join을 사용하고 있어, 타 모듈의 DB 의존도를 완전히 끊지는 못함
    • 차후 계획
      • 타 모듈이 DB를 직접 조회하는 대신 ASM 모듈의 이벤트를 수신하거나 API를 호출하도록 변경하여 DB 의존도를 점진적으로 낮출 예정

저장소 의존성 분리

  • Repository 인터페이스 도입
    • 추후 Redis 도입이나 DB 교체에 유연하게 대응하기 위해 인터페이스로 추상화함
    • DB Repository / In-Memory Repository 각각 구현
    • 추후 Redis 도입 시에도 구조 변경 최소화
  • 설정 기반 저장소 전환
    • 설정(configuration) 파일 변경만으로 저장소를 전환할 수 있도록 구성
    • 운영 환경에 따라 적절한 저장소 선택 가능

캐시를 바라보는 관점의 변화

  • 과거 캐시 활용

  • 이번 프로젝트를 통한 발견

  • 역할 전환의 의미