본문 바로가기
  • 개발하는 곰돌이
Programming Language/Kotlin & Java

[Java] HashMap vs Hashtable(feat. ConcurrentHashMap)

by 개발하는 곰돌이 2023. 3. 27.

목차

    서론

    스터디를 진행하던 도중 Hashtable에 대한 이야기가 조금 나왔었다. 사실 이전까진 Hashtable이라는 것이 있다는 것만 알고 Map의 구현체로 대부분 HashMap을, 아주 가끔 TreeMap을 사용했는데, Hashtable은 HashMap과 비슷하게 Key - Value 쌍의 Map 구현체인 것은 동일하지만 세부적인 내용에서 차이가 있는 구조라고 한다. 이 글에서는 HashMap과 Hashtable의 차이에 대해 조금 다뤄보려고 한다.

    HashMap

    HashMap은 자바 1.2에서 등장한 Map 인터페이스의 구현체이다. 기본적으로 HashMap은 동기화를 지원하지 않기 때문에 다중 스레드 환경에서는 동시성 이슈가 발생할 수 있다. 이러한 이유로 인해 HashMap에서는 연산 도중 Map이 변경되었을 때를 대비하여 각종 메소드에서 ConcurrentModificationException를 던져서, 오동작을 하는 대신 아예 실패하도록 fail-fast가 구현되어 있다. 하지만 이것이 데이터 무결성을 보장해주진 못하기 때문에 이 예외에 의존하는 프로그램을 작성하면 안 된다고 한다.

    메소드 실행 중에 modCount가 중간에 변경되면 ConcurrentModificationException을 발생시키도록 작성된 HashMap의 forEach()

    하지만 이렇게 비동기적으로 작동한다는 점으로 인해 단일 스레드 환경에서는 HashMap이 훨씬 좋은 성능을 내기 때문에 널리 사용된다고 한다.

     

    별 의미는 없겠지만 HashMap은 Key값에 null을 사용할 수도 있다.

    Hashtable

    Hashtable은 자바 1.0에서 등장한 Map 인터페이스의 구현체이다. 일단 Map의 구현체이고 Hash를 사용한다는 점에서 HashMap과 유사하지만 가장 큰 차이점은 거의 모든 메소드에 synchronized가 붙어서 동기적으로 작동한다는 점이다.

    synchronized가 붙어서 동기적으로 작동하는 Hashtable의 get()

    이렇게 단순 삽입, 제거, 조회 등을 포함한 거의 모든 메소드에 synchronized가 붙다보니 뭘 하려고만 하면 스레드 간에 잠금을 걸고 푸는 것을 반복하여 Hashtable은 연산 속도가 느린 편이라고 한다.

     

    또한, HashMap과는 달리 Key값에 null을 사용하려고 하면 NullPointerException을 발생시킨다.

     

    그런데 HashMap은 동기화를 지원하지 않으니 결국 스레드 안정성이 필요한 환경에서는 성능이 매우 떨어진다고는 하지만 Hashtable을 사용해야 하는 것이 아닌가? 하는 의문이 생긴다.

    SynchronizedMap

    SynchronizedMap은 HashMap을 포함하여 다양한 Map 구현체들에 스레드 안정성을 부여하기 위해 만들어진, Collections 클래스에 내부적으로 구현되어 있는 클래스이다. SynchronizedMap은 아래와 같이 선언하여 사용할 수 있다.

    Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());

    여기서 synchronizedMap()의 파라미터는 HashMap이 아닌 다른 Map 구현체가 와도 상관없다.

     

    하지만 이 방법은 두 가지 문제가 있는데, 첫째로는 감싸지지 않은 원본 Map에 모종의 방법으로 접근하게 되면 스레드 안정성을 보장할 수 없게 된다. SynchronizedMap은 아래와 같이 구성되어 있다.

    Collections 클래스 내부에 정의된 SynchronizedMap의 구조.

    이를 보면 SynchronizedMap은 파라미터로 받은 Map을 단순 참조로 받아 감싸고, 연산을 수행할 때마다 스스로를 잠그고 참조한 Map의 연산을 수행하는 것을 볼 수 있다. 즉, Map 자체에는 스레드 안정성이 보장되지 않는다! 이로 인해 원본 Map에 변화가 일어나면 그 변화에 영향을 받아 스레드 안정성을 보장할 수 없다. 따라서, 이 방법으로 스레드 안정성을 보장하고싶다면 기존에 선언된 Map 객체를 감싸는 것이 아니라 외부에서 접근할 수 없는 새로운 Map을 생성하여 감싸야 할 것이다.

    SynchronizedMap으로 감싼 원본 Map에 변화가 생기면 SynchronizedMap도 영향을 받는다.

    또한 이 방법은 우선 Map을 선언하고 그것을 다시 감싸야한다는 점에서 자원 낭비가 발생한다. 이러한 문제점들을 해결하기 위해 ConcurrentHashMap를 사용할 수 있다.

    ConcurrentHashMap

    ConcurrentHashMap은 자바 1.5에서 등장한 ConcurrentMap의 구현체이다. 이 ConcurrentMap 인터페이스는 Map을 상속받아 작성되었기 때문에 크게 보면 Map의 구현체라고도 볼 수 있다.

     

    ConcurrentHashMap은 Hashtable과 달리 동기화가 굳이 필요하지 않은 메소드에는 아예 synchronized를 사용하지 않고, 동기화가 필요한 메소드에는 무작정 synchronized를 거는 것이 아니라 꼭 필요한 부분에만 사용하여 Hashtable에 비해 성능이 크게 향상되었다.

    synchronized를 사용하지 않은 ConcurrentHashMap의 get().
    동기화가 필요한 경우에도 조건문으로 분기를 둬서 필요한 경우에만 스레드 간 잠금을 건다.

    결론

    Hashtable은 굉장히 오래된 클래스인 만큼 자바를 만든 개발자들도 사용하지 않는 것을 권하고 있다. 스레드 안정성이 필요없는 환경이라면 HashMap을 사용하는 것이 훨씬 이득이고, 스레드 안정성이 필요한 상황이라도 Hashtable 대신 ConcurrentHashMap을 사용하는 것이 좋을 것이다.

    Hashtable 문서에서도 Hashtable 대신 다른 구현체를 사용할 것을 권장하고 있다.

    댓글