우당탕탕

Java 동시성 컬렉션 ConcurrentHashMap 실제 사용하며 겪은 문제들 본문

언어/Java

Java 동시성 컬렉션 ConcurrentHashMap 실제 사용하며 겪은 문제들

모찌모찝 2026. 7. 23. 10:03

Java로 백엔드 개발을 하다 보면 하나쯤은 꼭 만나게 되는 게 동시성 문제인데요, 저도 ConcurrentHashMap을 쓰면서 생각보다 여러 삽질을 했어요. 특히 실제 코드에 적용하면서 어느 상황에서 유용한지, 그리고 다른 동시성 컬렉션과 어떻게 다른지도 헷갈렸거든요.

이번 글에서는 제가 직접 경험한 ConcurrentHashMap 사용기와, 다른 사람들이 겪은 사례를 비교하며 어떤 경우에 더 적합한지, 그리고 코드 예제와 함께 실제 문제 해결 과정을 자세히 풀어볼게요.

개발 환경 / 버전 정보

제 프로젝트는 Java 17 기반이고, 스프링 부트는 Spring Boot 3.2를 사용 중입니다. 여러 동시성 컬렉션 중 ConcurrentHashMap에 집중해서 비교했어요.

Java 동시성 컬렉션 ConcurrentHashMap 실전 사용기 관련 이미지

Java 동시성 컬렉션 ConcurrentHashMap 실전 사용기 관련 정보

ConcurrentHashMap 이렇게 쓰니까 좋더라고요

사실 이 부분이 제가 직접 사용하며 가장 만족했던 점인데요. ConcurrentHashMap은 멀티스레드 환경에서 락 병목 없이 성능을 낼 수 있어요. 저는 웹 서비스에서 사용자 세션이나 캐시용 데이터를 관리할 때 썼는데, synchronized 블록을 쓸 때보다 훨씬 부하가 적더라고요.

기본적인 사용 방법은 HashMap과 거의 같아서 진입 장벽도 낮았어요.

// ConcurrentHashMap 선언과 기본 사용 예
import java.util.concurrent.ConcurrentHashMap;

public class CacheExample {
    private ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();

    public void putData(String key, String value) {
        cache.put(key, value);
    }

    public String getData(String key) {
        return cache.get(key);
    }

    public void removeData(String key) {
        cache.remove(key);
    }
}

단순히 넣고 꺼내는 기본 기능에는 전혀 무리가 없었고, 동시 수정에도 안전해서 바로바로 최신 값이 반영되는 걸 체감했어요.

Java 동시성 컬렉션 ConcurrentHashMap 실전 사용기 직접 정리한 자료

Java 동시성 컬렉션 ConcurrentHashMap 실전 사용기 관련 정보

다른 동시성 컬렉션과 비교해보니

그런데 ConcurrentHashMap 쓰면서 다른 컬렉션과 헷갈리는 부분이 분명 있었어요. 예를 들어 Collections.synchronizedMapConcurrentSkipListMap 같은 녀석들이죠.

정리해보면 아래처럼 특성과 쓰임새가 조금씩 다르더라고요.

  • ConcurrentHashMap : 고성능, 락 분할(lock striping) 기법으로 동시 접근에 유리, 키의 순서 유지 안 됨
  • Collections.synchronizedMap : 간단하게 동기화 처리 가능하지만 모든 메서드에 락 걸어 성능 저하 우려
  • ConcurrentSkipListMap : 정렬된 키 값을 유지해야 할 때, 스킵 리스트 기반, ConcurrentHashMap보다 느림

저도 처음에는 synchronizedMap으로 시작했는데, 트래픽 높은 서비스에서 성능 병목이 명확히 느껴져서 ConcurrentHashMap으로 바꿨더니 훨씬 나아졌습니다.

Java 동시성 컬렉션 ConcurrentHashMap 실전 사용기 참고 사진

Java 동시성 컬렉션 ConcurrentHashMap 실전 사용기 관련 정보

실제 코드에서 막혔던 부분과 해결법

그런데 여기서 많이들 틀리는 게 ConcurrentHashMap을 완전 무결한 동시성 해결책으로 오해하는 경우입니다. 저도 한 번은 이런 상황에서 고생했었는데요, 바로 computeIfAbsent와 같은 원자적 연산을 제대로 이해하지 못해서 생긴 문제였어요.

예를 들어 캐시에 복잡한 객체를 넣는 로직에서, 아래처럼 기존에 없으면 새로 계산해서 넣는 코드를 썼다가 의도치 않은 중복 생성이 발생했어요.

// 문제 코드: get-then-put 방식, 멀티스레드에서 중복 객체 생성 가능

Object value = cache.get(key);
if (value == null) {
    value = createNewObject();
    cache.put(key, value);
}

이 부분이 생각보다 치명적이었는데, ConcurrentHashMap도 get과 put은 각각 원자적이어도 이 조합은 원자적이지 않기 때문이에요.

해결책은 computeIfAbsent 같은 메서드를 쓰는 건데, 이걸 써주면 내부에서 락을 걸어서 단 한 번만 계산하도록 보장해줘요.

// 올바른 코드
Object value = cache.computeIfAbsent(key, k -> createNewObject());

이 방법을 쓴 뒤로는 중복 객체 생성 문제는 깔끔하게 해결됐습니다.

실무에서 비교해본 동시성 문제 사례

제가 일하는 회사에서는 대용량 사용자 데이터 캐싱 시스템을 운영 중인데, 동시성 컬렉션 선택을 두고 내부에서도 의견이 분분했어요. 참고로 다른 회사에서 들은 이야기들도 좀 섞어보겠습니다.

가장 대표적인 고민은 아래 3가지였는데요.

컬렉션 장점 단점 실무 적용 예
ConcurrentHashMap 높은 동시성 성능, 락 경합 최소화 키 순서 유지 안 됨 사용자 세션 캐시, 실시간 조회 캐시
Collections.synchronizedMap 쉽게 적용 가능, 코드 변경 부담 적음 전체 락으로 인한 성능 저하 심함 소규모 트래픽, 관리용 데이터
ConcurrentSkipListMap 키 정렬 보장, 탐색 성능 좋음 동시성 성능은 CHM보다 떨어짐 랭킹 시스템, 정렬된 캐시

결국 저희 팀은 트래픽과 요구 기능에 따라 상황별로 선택하기로 했는데, 여러분도 비슷한 고민을 하게 될 텐데요. 무턱대고 성능 높다는 이유로 ConcurrentHashMap만 쓰는 게 능사는 아니더라고요.

마무리하며, 자주 묻는 질문들

Q. ConcurrentHashMap은 완전한 동시성 보장인가요?

A. 멀티스레드 환경에서 get/put 같은 기본 연산은 안전하지만, 여러 연산을 조합하면 원자적이지 않을 수 있어요. 그래서 computeIfAbsent 같은 원자적 메서드를 꼭 써야 합니다.

Q. ConcurrentHashMap에서 null 값을 넣을 수 있나요?

A. 못 넣습니다. NullPointerException이 발생해서, null 키나 값은 허용하지 않으니 주의하세요.

Q. 동시성 컬렉션 중 어떤 걸 선택해야 할지 감이 안 와요

A. 성능과 기능 요구사항에 따라 달라요. 순서가 중요한지, 트래픽은 얼마나 되는지, 그리고 기존 코드와의 호환성도 고려해야죠. 일반적인 캐시나 매핑엔 ConcurrentHashMap 추천합니다.

ConcurrentHashMap을 쓰면서 겪은 시행착오를 솔직히 나누다 보니, 자칫 복잡하고 난해할 수 있는 동시성 컬렉션 선택과 사용법이 조금은 덜 막연해졌길 바랍니다. 여러 사례 비교와 실전 경험이 합쳐져서 여러분도 적합한 컬렉션을 고르는 데 도움이 됐으면 좋겠네요.

Comments