백엔드 시스템 설계 면접: 메모리 최적화부터 고성능 주문 처리까지

1. 메모리 누수 분석 실전 사례

1.1 문제 상황 인식

실시간 데이터 동기화 서비스 운영 중 OutOfMemoryError가 반복 발생했습니다. 서비스는 다수의 노드에서 수집된 로그 데이터를 집계하여 실시간 대시보드에 전송하는 역할을 했으며, 힙 메모리 8GB 할당环境下에서 장시간 구동 후 메모리 고갈이 발생했습니다.

1.2 덤프 파일 생성 및 분석 절차

JVM 파라미터에 -XX:+HeapDumpOnOutOfMemoryError와 -XX:HeapDumpPath=/var/log/app.hprof를 설정하여 OOM 발생 시 자동으로 덤프 파일이 생성되도록 구성했습니다. 생성된 덤프 파일은 힙 사용량에 따라 7.5GB 규모였습니다.

Memory Analyzer Tool(메모리 분석 도구)을 활용한 분석 단계는 다음과 같습니다:

  • 히스토그램 뷰: 객체 유형별 메모리 점유율을 확인하여 java.util.HashMap$Node[]와 byte[] 배열이 전체 메모리의 78%를 차지하는 것을 발견
  • 도미네이터 트리: LogAggregator 클래스의 인스턴스 하나가 6.2GB를 직접/간접으로 참조하고 있음을 확인
  • LogAggregator 객체의 필드를 검사하니 ConcurrentHashMap에 수백만 개의 로그 이벤트가 쌓여 있었습니다. TTL(Time-To-Live) 설정이 누락되어 오래된 데이터가 정리되지 않았습니다.

1.3 근본 원인 및 해결 방안

G1 GC의年轻代与老年代比例는 동적으로 조정됩니다. -XX:G1NewSizePercent=5와 -XX:G1MaxNewSizePercent=60 기본값을 사용했으나, 메모리 누수로 인해 Old Gen이 급격히 증가했습니다.

해결책은 로그 데이터에 5분 TTL을 적용하고, 주기적으로 만료된 항목을 제거하는 스케줄러를 추가하는 것이었습니다. 또한 메모리 사용량 모니터링을 강화하여 70% 임계점 도달 시 알림을 발생시키도록 했습니다.

2. MySQL 복합 쿼리 최적화

2.1 최고점수 학생 조회 문제

다음과 같은 성적 테이블이 있습니다:

CREATE TABLE 성적표 (
    학생ID INT PRIMARY KEY,
    과목명 VARCHAR(50),
    점수 DECIMAL(5,2)
);

각 과목별 최고점을 받은 학생의 정보를 조회하는 쿼리는 다음과 같습니다:

SELECT a.학생ID, a.과목명, a.점수
FROM 성적표 a
INNER JOIN (
    SELECT 과목명, MAX(점수) as 최고점
    FROM 성적표
    GROUP BY 과목명
) b ON a.과목명 = b.과목명 AND a.점수 = b.최고점;

2.2 GROUP BY와 ORDER BY 실행 순서

SELECT 과목명, MAX(점수) as 최고점 FROM 성적표 GROUP BY 과목명 ORDER BY 최고점 DESC; 쿼리에서 ORDER BY는 GROUP BY 집계 완료 후 결과 집합을 정렬합니다. 실행 순서는 FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY입니다.

3. 초고성능 주문 처리 시스템 설계

3.1 마이크로서비스 분리 전략

주문 처리 시스템을 마이크로서비스로 분리할 때 적용한 원칙은 다음과 같습니다:

  • 도메인 중심 설계: 재고(Inventory), 주문(Order), 결제(Payment), 알림(Notification)으로 경계 컨텍스트 분리
  • 데이터베이스 분리: 각 서비스는 독립적인 데이터 저장소를 소유. 재고 서비스는 Redis, 주문 서비스는 MySQL 사용
  • API 게이트웨이: 외부 요청을 라우팅하고 인증/인가 처리
  • 이벤트 기반 통신: 주문 생성 시 Kafka를 통해 재시도 가능한 비동기 이벤트 발행

3.2 성능 병목 지점 분석

로컬 환경(16GB RAM, 8코어 CPU)에서 JMeter로 부하 테스트를 수행한 결과, 2000TPS에서 병목이 발생했습니다. 원인은 다음과 같았습니다:

  • 단일 Redis 인스턴스의 연결 수 제한
  • 톰캣 스레드 풀의 최대 스레드 수(200개) 한계
  • 동기적인 재고 확인 로직으로 인한 대기 시간 증가

3.3 확장성 개선 방안

TPS를 4000 이상으로 향상시키기 위한 최적화 전략:

// Redis 클러스터링 적용
RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration();
clusterConfig.addClusterNode(new RedisNode("node1", 6379));
clusterConfig.addClusterNode(new RedisNode("node2", 6380));

// 비동기 재고 차감
@Async
public CompletableFuture<Boolean> decreaseStockAsync(String productId, int quantity) {
    return CompletableFuture.supplyAsync(() -> 
        redisTemplate.opsForValue().decrement(productId, quantity) >= 0
    );
}

추가로 주문 서비스를 3개 인스턴스로 수평 확장하고, 로드 밸런서를 통해 트래픽을 분산했습니다. DB 커넥션 풀은 HikariCP로 교체하여 연결 효율성을 개선했습니다.

4. 경량 RPC 프레임워크 구현 경험

4.1 프로토콜 설계 선택

Netty를 활용해 커스텀 바이너리 프로토콜을 구현했습니다. HTTP 대신 커스텀 프로토콜을 선택한 이유는:

  • 헤더 오버헤드 최소화(8바이트 고정 헤더)
  • 바이트 단위 제어로 직렬화/역직렬화 속도 향상
  • 네트워크 트래픽 감소(평균 30% 절감)
  • 특정 유스케이스에 맞춘 흐름 제어 구현

4.2 직렬화 방식 비교

구현 시도한 직렬화 방식은 다음과 같습니다:

  • Protobuf: 스키마 기반, 최소 페이로드, 하지만 유연성 부족
  • Kryo: Java 객체 직접 지원, 높은 속도, 버전 호환성 문제
  • MsgPack: JSON과 호환, 이진 형식, 균형잡힌 선택지
  • 자동 생성 직렬화: 애너테이션 프로세서로 컴파일 타임에 직렬화 코드 생성

5. 효율적인 기술 학습 방법론

5.1 목표 기반 학습 프로세스

새로운 기술을 습득할 때는 반드시 해결해야 할 실제 문제가 있을 때만 접근합니다. 학습 흐름은 다음과 같습니다:

  1. 문제 정의: 현재 시스템의 한계점 명확히 파악
  2. POC 제작: 공식 문서와 GitHub 예제를 참고하여 3일 이내에 작동하는 프로토타입 구현
  3. 통합: 실제 코드베이스에 적용하면서 엣지 케이스 처리
  4. 비교 분석: 기존 솔루션과 성능/복잡도/유지보수성 비교
  5. 문서화: 팀 위키에 적용 사례와 주의사항 정리

5.2 학습 리소스 활용

정보 출처는 다음과 같이 계층화하여 활용합니다:

  • 초급: Baeldung, 김영한 님의 스프링 강의, 회사 내부 테크블로그
  • 중급: Official Documentation, RFC 문서, OpenJDK 소스 코드
  • 고급: InfoQ 발표 자료, QCon 세션, 직접 디버깅 및 바이트코드 분석

특히 GPT-4는 개념 설명과 코드 리뷰에 활용하되, 항상 공식 문서와 교차 검증합니다.

태그: java OOM Memory Analyzer Tool G1 GC MySQL

9월 25일 14:55에 게시됨