ConcurrentHashMap과 동기화(Synchronization) 메커니즘의 성능 차이

지난 포스팅에서는 시스템 자원 낭비를 막고 서버의 안정성을 지키는 스레드 풀(Thread Pool)의 적정 사이즈 산정법에 대해 알아보았습니다. 여러 개의 스레드를 효율적으로 띄우는 것까지 성공했다면, 그다음 직면하게 되는 문제는 ‘여러 스레드가 동시에 동일한 데이터(공유 자원)에 접근할 때 어떻게 안전하게 처리할 것인가’입니다.

Java에서 Key-Value 형태로 데이터를 저장할 때 가장 많이 사용하는 자료구조는 HashMap입니다. 하지만 HashMap은 스레드 안전성(Thread-safe)을 보장하지 않기 때문에 멀티 스레드 환경에서 그대로 사용하면 데이터가 덮어씌워 지거나 무한 루프에 빠지는 치명적인 오류가 발생합니다. 이번 글에서는 이를 해결하기 위한 자바의 동기화 메커니즘과, 성능의 끝판왕이라 불리는 ConcurrentHashMap에 대해 깊이 있게 분석해 보겠습니다.

1. 전통적인 동기화의 함정: Hashtable과 SynchronizedMap

과거 자바 초기 버전에서는 스레드 안전성을 확보하기 위해 Hashtable을 사용하거나 Collections.synchronizedMap()을 통해 맵을 감싸서 사용했습니다.

이러한 전통적인 방식들의 특징은 ‘메서드 전체에 암묵적인 락(Lock)을 건다’는 점입니다. 즉, A라는 스레드가 데이터를 읽거나 쓰기 위해 맵에 접근하면, 맵 전체(객체 자체)에 자물쇠가 채워집니다. 이 경우 B, C, D 스레드는 A가 작업을 마치고 락을 풀 때까지 아무것도 하지 못하고 대기(BLOCKED) 상태에 빠지게 됩니다. 데이터 무결성은 완벽하게 보장되지만, 트래픽이 몰리는 웹 서버 환경에서는 병목 현상이 극심해져 애플리케이션의 처리량(Throughput)이 수직 하락하는 원인이 됩니다.

2. 성능의 게임 체인저: ConcurrentHashMap과 락 스트라이핑

이러한 성능 문제를 획기적으로 해결하기 위해 Java 5부터 java.util.concurrent 패키지에 ConcurrentHashMap이 도입되었습니다.

ConcurrentHashMap의 핵심 아이디어는 ‘전체 락’이 아닌 ‘부분 락(Lock Striping)’을 사용한다는 것입니다. 데이터를 저장하는 공간을 여러 개의 세그먼트(Segment) 또는 버킷(Bucket)으로 쪼개고, 특정 데이터가 저장되는 해당 버킷에만 락을 겁니다.

  • Java 8 이후의 진화: Java 8부터는 성능이 더욱 개선되어, 각 노드(Node)별로 독립적인 락을 사용합니다. 새로운 데이터를 추가(Write)할 때는 값이 비어있다면 락 없이 CAS(Compare-And-Swap) 알고리즘을 사용해 하드웨어 수준에서 원자성을 보장하며 매우 빠르게 삽입합니다. 이미 값이 있는 경우에만 synchronized 블록을 최소화하여 해당 노드에만 락을 겁니다.
  • 읽기(Read) 성능 극대화: 가장 놀라운 점은 검색(Get) 작업에는 아예 락을 사용하지 않는다는 것입니다. volatile 키워드를 활용해 항상 메모리에서 가장 최근에 업데이트된 값을 읽어오도록 설계되어 있어, 수많은 스레드가 동시에 데이터를 읽어도 병목이 전혀 발생하지 않습니다.

3. 실무 활용 사례: 데이터베이스 조회 결과의 동시성 캐싱

실제 엔터프라이즈 환경에서 이 구조가 어떻게 빛을 발하는지 예를 들어보겠습니다. 대량의 데이터베이스 쿼리 결과 리스트를 조회하여 JSON 파일 포맷으로 변환하고, 이 변환된 JSON 데이터를 메모리에 임시로 캐싱해두는 서버 로직이 있다고 가정해 보겠습니다.

수백 명의 사용자가 동시에 이 API를 호출할 때(수백 개의 스레드가 동시에 캐시에 접근할 때), 캐시 저장소로 일반 Hashtable을 사용한다면 단 한 명만이 캐시를 읽거나 쓸 수 있습니다. 반면 캐시 저장소를 ConcurrentHashMap으로 구현한다면, 스레드들이 서로 다른 Key(예: 서로 다른 유저 ID)를 조회하고 쓰는 한 락 경합이 거의 발생하지 않아 압도적으로 빠른 응답 속도를 보여줍니다.

결론: 무분별한 락을 버리고 병렬성을 취하라

멀티 스레드 프로그래밍에서 ‘스레드 안전성’과 ‘성능’은 보통 반비례 관계에 있습니다. 하지만 ConcurrentHashMap은 영리한 자료구조 설계를 통해 두 마리 토끼를 모두 잡은 자바 언어의 걸작입니다. 실무에서 캐시 로직이나 전역 상태를 관리하는 Map이 필요하다면 주저 없이 ConcurrentHashMap을 선택해야 합니다.

지금까지 스레드 간의 자원 공유 문제를 다루었다면, 다음 포스팅에서는 대기 시간을 극단적으로 줄여주는 현대적인 프로그래밍 패러다임, ‘비동기 프로그래밍(CompletableFuture)을 활용한 서버 응답 속도 극대화’에 대해 본격적으로 파헤쳐 보겠습니다.

댓글 남기기