Java & Spring

이펙티브 자바 - 아이템 21: 인터페이스는 구현하는 쪽을 생각해 설계하라

밍 끄적 2023. 5. 8. 15:51
728x90

Default Method

자바8에서, 기존의 구현체를 깨뜨리지 않고, 비교적 안전하게 인터페이스에 메소드를 추가하는 방법

기존에는 메서드 하나를 추가하려면 해당 인터페이스를 구현하는 모든 클래스에서는 해당 메서드를 모두 구현해줘야 했다. 하지만, 디폴트 메서드를 이용하면 인터페이스의 기분 구현을 그대로 상속하므로 인터페이스에 자유롭게 새로운 메서드를 추가할 수 있게 된다. 호환성을 유지하면서 API를 바꿀 수 있는 것이다.

java8의 java.util.Collection 인터페이스

java7의 Collection 인터페이스는 원래 removeIf 메소드가 없었다.

Collection (Java Platform SE 7 )

 

Collection (Java Platform SE 7 )

Compares the specified object with this collection for equality. While the Collection interface adds no stipulations to the general contract for the Object.equals, programmers who implement the Collection interface "directly" (in other words, create a clas

docs.oracle.com

 

하지만, java8로 업그레이드 되면서 default method를 쓸 수 있게 되었다.

Collection 인터페이스도 주어진 조건에 true를 반환하는 boolean 함수(Predicate)를 인자로 받아, 함수의 결과가 true인 element를 삭제하는 removeIf 메소드를 추가하고 싶었고, 이를 default method로 삽입하여, 이 Collection 인터페이스를 구현하는 클래스에 재정의 없이도 사용할 수 있도록 만들었다.

Collection (Java Platform SE 8 )

 

Collection (Java Platform SE 8 )

Compares the specified object with this collection for equality. While the Collection interface adds no stipulations to the general contract for the Object.equals, programmers who implement the Collection interface "directly" (in other words, create a clas

docs.oracle.com

문제점

생각할 수 있는 모든 상황에서 불변식을 해치지 않는 default 메소드를 작성하기는 어렵다.

default 메소드만 있으면, 구현체에서 재정의 없이 default 메소드에 구현된 로직에 따라 수행할 텐데, 만약 그 로직이 구현체의 내부 로직과 적합하지 않다면, 불변식을 해치게 되는 것이다.

Default 메소드의 문제점
: 아파치 커먼즈 라이브러리의 SynchronizedCollection

아파치 커먼즈 라이브러리와, java.util.Collection의 Synchronized Collection 클래스

아파치 커먼즈 라이브러리는 자바 플랫폼 라이브러리에 속하지 않는, 제3의 라이브러리이다. 아파치 커먼즈 라이브러리는 SynchronizedCollection 클래스를 가지고 있는데, 이는 java.util.Collection을 구현하고 있다.

SynchronizedCollection (Apache Commons Collections 4.4 API)

 

SynchronizedCollection (Apache Commons Collections 4.4 API)

Decorates another Collection to synchronize its behaviour for a multi-threaded environment. Iterators must be manually synchronized: synchronized (coll) { Iterator it = coll.iterator(); // do stuff with iterator } This class is Serializable from Commons Co

commons.apache.org

...
public class SynchronizedCollection<E> implements Collection<E>, Serializable {
...

이러한 아파치 라이브러리의 SynchronizedCollection 클래스는 java.util.Collections.SynchronizedCollection 클래스와 비슷하다.

java.util.Collections.SynchronizedCollection

두 synchronizedCollection 클래스는 비슷한 역할을 수행하고 있다. 여기서는, 이 synchronizedCollection이 어떤 기능을 수행하는지 소개하지 위해, java.util.Collections.SynchronizedCollection을 소개한다.

java.util.Collections.SynchronizedCollection 클래스는 스레드로부터 안전한 컬렉션을 제공하기 위해 사용되는 래퍼(Wrapper) 클래스이다.

여러 개의 스레드가 동시에 동일한 컬렉션을 수정하려고 할 때 예기치 않은 동작이 발생할 수 있다. 이를 해결하기 위해 Java는 java.util.Collections 클래스에 여러 스레드에서 안전하게 사용할 수 있는 래퍼 클래스인 java.util.Collections.SynchronizedCollection 클래스를 제공한다.

 

여러 개의 스레드가 동시에 동일한 컬렉션을 수정할 때, Thread-safe하지 않은 예제는 아래와 같다.

import java.util.ArrayList;
import java.util.Collection;

public class NonThreadSafeCollectionExample {
  public static void main(String[] args) {
    // 스레드로부터 안전하지 않은 컬렉션 생성
    Collection<Integer> collection = new ArrayList<>();

    // 1,000개의 요소를 추가하는 스레드 생성
    Thread addThread1 = new Thread(() -> {
      for (int i = 0; i < 1000; i++) {
        collection.add(i);
      }
    });

    // 1,000개의 요소를 추가하는 스레드 생성
    Thread addThread2 = new Thread(() -> {
      for (int i = 1000; i < 2000; i++) {
        collection.add(i);
      }
    });

    // 스레드 시작
    addThread1.start();
    addThread2.start();

    try {
      // 스레드 종료 대기
      addThread1.join();
      addThread2.join();

      // 컬렉션 크기 출력 (예상: 2000 이하)
      System.out.println("컬렉션 크기: " + collection.size());
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

실행 결과, 두 개의 스레드는 각각 1,000개의 요소를 컬렉션에 추가하여 2000의 결과를 출력해야하지만, 1887이라는 올바르지 않은 실행 결과를 출력했다.

스레드가 동시에 컬렉션을 수정하려고 할 때, 스레드 간에 경쟁 조건이 발생한 것이다. 한 스레드가 컬렉션의 크기를 확인하고 요소를 추가하기 전에 다른 스레드가 이미 요소를 추가하여 크기가 변경되었고, 이로 인해 예기치 않은 결과가 발생한 것이다.

 

아래는 위의 예제와 같은 상황을 방지하기 위해, java.util.Collections.SynchronizedCollection클래스를 사용한 예제이다.

import java.util.*;

public class ThreadSafeCollectionExample {
  public static void main(String[] args) {
    // 스레드로부터 안전한 컬렉션 생성
    Collection<Integer> synchronizedCollection = Collections.synchronizedCollection(new ArrayList<>());

    // 1,000개의 요소를 추가하는 스레드 생성
    Thread addThread1 = new Thread(() -> {
      for (int i = 0; i < 1000; i++) {
        synchronizedCollection.add(i);
      }
    });

    // 1,000개의 요소를 추가하는 스레드 생성
    Thread addThread2 = new Thread(() -> {
      for (int i = 1000; i < 2000; i++) {
        synchronizedCollection.add(i);
      }
    });

    // 스레드 시작
    addThread1.start();
    addThread2.start();

    try {
      // 스레드 종료 대기
      addThread1.join();
      addThread2.join();

      // 컬렉션 크기 출력 (예상: 2000)
      System.out.println("컬렉션 크기: " + synchronizedCollection.size());
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

Collections.synchronizedCollection을 사용하여 스레드로부터 안전한 컬렉션 객체를 생성한다. 그리고 두 개의 스레드를 생성하여 각각 1,000개의 요소를 컬렉션에 추가한다. addThread1은 0부터 999까지의 요소를 추가하고, addThread2는 1,000부터 1,999까지의 요소를 추가한다.

스레드가 요소를 추가하는 동안, synchronizedCollection은 내부적으로 동기화되어 다중 스레드 간의 안전한 접근을 보장한다.
마지막으로, addThread1과 addThread2의 종료를 기다리고, 컬렉션의 크기를 출력한다.

 

출력 결과는 예상대로 2,000이었다.

문제점

이러한 java.util.Collections.SynchronizedCollection 클래스는 자바 플랫폼 라이브러리에 속하므로, 디폴트 메소드가 추가됨에 따라 함께, 재정의한 메소드를 릴리즈 했기 때문에, 이상이 없었다.

 

하지만, org.apache.commons.collections4.collection.SynchronizedCollection 클래스는 이에 바로 대응할 수 없어,
4.3 버전에는 removeIf 메소드를 재정의하지 못했기 때문에, default 메소드의 로직을 수행하게되었다.

따라서, 4.3 버전에서 removeIf 메소드를 수행할 경우, 동기화되지 못해 ConcurrentModificationException이 발생했다.

 

아래 github의 코드를 확인해보면, 4.3 버전의 SynchronizedCollection 클래스는, removeIf는 재정의되지 않았음을 알 수 있다.

commons-collections/SynchronizedCollection.java at collections-4.3 · apache/commons-collections

 

GitHub - apache/commons-collections: Apache Commons Collections

Apache Commons Collections. Contribute to apache/commons-collections development by creating an account on GitHub.

github.com

 

책에서는 아직 재정의하지 않고 있다고 했지만, 현재 4.4 버전에서 removeIf를 재정의하여 릴리즈에 해결되었다.

commons-collections/SynchronizedCollection.java at master · apache/commons-collections

 

GitHub - apache/commons-collections: Apache Commons Collections

Apache Commons Collections. Contribute to apache/commons-collections development by creating an account on GitHub.

github.com

꼭 필요한 경우가 아니면 Default 메소드를 추가하는 것은 피하자

Default 메소드는, 기존 메서드를 제거하거나 수정하는 용도가 아니다.

Default 메소드로 인해 기존 클라이언트를 망가뜨릴 수 있다.

 

따라서, 인터페이스를 설계 할 때는 여전히 세심한 주의를 기울여야 한다.

이를 검증하기 위해 서로 다른 방식으로 최소 세 가지의 구현체를 만들어 보자.

 

인터페이스를 릴리즈한 후라도 결함을 수정하는 게 가능한 경우도 있지만, 이를 보험삼아서는 안된다.

728x90

'Java & Spring' 카테고리의 다른 글

Interceptor  (0) 2023.05.23
Filter  (0) 2023.05.16
Spring AOP - Proxy, Dynamic Proxy  (0) 2023.05.03
이펙티브 자바 - 아이템 18 : 상속보다는 컴포지션을 사용하라  (0) 2023.04.24
Spring AOP - AOP 기본 개념  (0) 2023.04.19