[JAVA] API 에서 객체 반환시 주의

Joshua Bloch 의 Effective Java (3rd Edition) 50 장에 바람직한 메서드 작성법과 관련해서 아래와 같은 내용이 나온다.

  • Make defensive copies of the object-type method arguments
  • Return defensive copies of mutable internal fields

오브젝트 형의 메서드 인자를 복제해서 사용하는 이유는, 메서드 호출자가 메서드에 객체를 넘긴 후 레퍼런스를 통해 해당 객체를 변경할 가능성이 있기 때문이다. 반대로 메서드에서 mutable class 타입의 객체를 반환하는 경우, 반환값을 받는 곳에서 해당객체를 변조할 수 있다.

매개변수로든 반환값으로든, 하나의 메서드 안에서 사용하는 객체의 레퍼런스값이 외부에 노출되면, 외부에서 해당 객체를 바꿔치기하거나 객체의 내부값을 변경할 수 있다. 단, 객체가 immutable class 타입인 경우엔 객체의 내부값을 변경할 순 없겠다.

Public API 를 작성하는 경우 오브젝트 형 반환에 특히 유의해야 할 것 같다. 아래의 예시를 생각해보자.

Capture1

 

Post 는 생성 후 setter 메서드를 통해 내부값을 변경할 수 있기 때문에 mutable class 이다. PostHolder 의 경우 모든 Post 의 상태를 메모리에 들고 있는 역할을 수행하며, Spring Component 어노테이션을 통해 인스턴스가 한 개만 생성되도록 정의하였다; PostHolder 의 cache 는 Post Id와 Post 를 맵핑하는데, 하나 이상의 쓰레드에서 cache 를 접근하거나 put/remove 등을 수행해도 크게 문제가 되지 않도록 ConcurrentHashMap 으로 구현하였다.

ConcurrentHashMap 은 put 이나 remove 등의 경우 내부적으로 lock 을 걸어주기 때문에 사용자가 별도로 synchronized 블록을 작성할 필요가 없다. 그러나 조회하는 성격의 API 의 경우 자체적으로 lock 을 걸지 않는다고 JAVA API 문서에 명시되어 있다.

Service 클래스의 updateVisitCountsBy 메서드를 두 개의 쓰레드에서 동시에 호출했다고 가정하자 (User A 와 B 에 대해 각각). 만일 두 User 가 둘 다 읽은 Post 가 있다면 해당 Post 의 visitCount 는 2 만큼 증가해야 한다. ConcurrentHashMap.values() 로 접근하기 때문에 두 쓰레드가 동시에 Post 객체들에 접근 가능하고, 운이 나쁘면 다음과 같은 상황이 벌어질 수 있다.

  1. Thread A 가 Post 의 visitCount 값을 읽는다 (0)
  2. Thread B 가 같은 Post 의 visitCount 값을 읽는다 (0)
  3. Thread A 가 같은 Post 의 visitCount 값을 업데이트한다 (0 + 1)
  4. Thread B 가 같은 Post 의 visitCount 값을 업데이트한다 (0 + 1)

따라서 Service 클래스의 updateVisitCountsBy 메서드는 thread-safe 하지 않다. 가공의 예이지만 실제 현장에서 맡닥뜨린 적이 종종 있다. 그럼 이런 상황을 어떻게 극복해야 할까.

  1. 반환할 Post 객체(들)을 복제하여 반환한다: 객체가 얼마나 복잡하고 몇개나 반환해야 되는지에 따라 이 방법은 비효율적일 수도 있다.
  2. 애초에 이곳 저곳에서 참조할 목적으로 PostHolder 를 만들었으니, 객체는 그대로 반환하되 Post 클래스를 immutable 하게 만든다: 아래의 예시에서 Post 는 완전히 immutable 은 아니지만 같은 패키지에서만 내부값을 변경할 수 있도록 접근을 제한하였다. 다른 패키지에 있는 Service 는 PostHolder 를 통해 Post 의 상태값을 변경해야 하며, PostHolder 는 ConcurrentHashMap.computeIfPresent() API 를 사용하여 thread-safe 하게 visitCount 를 업데이트 하도록 구현하였다.

    Capture2-1Capture2-2

  3. Post 클래스가 프로젝트 여기저기 널리 쓰여서 내맘대로 immutable 하게 변경하기 어려운 경우도 있을 것이다. 적어도 PostHolder 의 getAllPosts() API 주석에 경고의 문구를 남기자: 여러 쓰레드에서 동시에 접근하는 것을 허용하기 때문에 가급적 이 API 로 조회한 객체의 내부값을 직접 변경하지 말자고.