본문 바로가기

Java

(JAVA) 인터페이스는 구현하는 쪽을 생각해 설계하라

이번장은 자바 8의 대표적인 특징 중 하나인 디폴트 메서드를 예시로 들면서 해당 메서드를 사용할 때의 유의사항에 대해서 설명을 한다.
결론은 양날의 검이니 상황에 맞게 잘 사용하자는 것이다.

 

자바 8 이전에는 기존에 정의된 인터페이스가 있다면 해당 인터페이스에 메서드를 추가하는 것은 불가능에 가까운 일이었다.

하나 자바 8 이후부터는 디폴트 메서드라는 기능이 생겨서 위에서 언급했던 불편함을 어느 정도 해소시켜주었다.

 

 

대표적인 예는 아래의 List의 sort 메서드이다. 

이와 같이 자바 라이브러리에 포함된 디폴트 메서드는 코드 품질이 높고 대부분의 상황에서 무리 없이 사용할 수 있다.

 

그렇다면 이제 인터페이스에 메서드를 추가하는 문제점은 완전히 해결된 것인가?

아니다!

 

아래 두가지의 문제점이 존재한다.

  1. 디퐅르 메서드를 재 정의하지 않은 (사용을 원하지 않는) 모든 클래스에서 디폴트 메서드의 구현을 사용할 수 있다.
    • 이전에 언급하였다. Vector, Stack과 같은 맥락
  2. 모든 기존 구현체들과 매끄러운 연동을 보장할 수 없다.

왜 이런 문제점이 생긴 것일까?

이유는 서두에 언급했듯이 자바 8 이전 버전에서는 인터페이스에 메서드가 추가될 것이라는 상상은 할 수가 없었고 자바 8 이후부터 디폴트 메서드가 생겼지만 해당 버전 이전에 만들어졌던 모든 내용을 커버하기는 힘들었기 때문이라고 말할 수 있다.

 

https://m.blog.naver.com/PostView.nhn?blogId=sol9501&logNo=70104141478&proxyReferer=undefined

 

|소켓 프로그래밍| 멀티 스레드(스레드 동기화) ③

|소켓 프로그래밍| 멀티 스레드(스레드 동기화) ③ * 이번 포스팅은 멀티스레드를 사용하는 경우 두 개 이...

blog.naver.com

 

아래의 코드는 10000개의 스레드에서 하나의 필드를 참조하는 아주 위험한 코드이다.

해당 코드를 안정적으로 사용하기 위해서는 동기화가 시급해 보인다.

/**
 * Project : FourTeamEffectiveJavaStudy
 *
 * @author : jwdeveloper
 * @comment :
 * Time : 10:19 오후
 */
public class SynchronizedCollectionEx {
    static Collection treeSet = new TreeSet();

    public static void main(String[] args) {
        Thread[] t = new Thread[10000];
        for (int i = 0; i < 10000; i++) {
            t[i] = new Thread(new WriterThread());
            t[i].start();
        }
    }
}
class WriterThread implements Runnable{

    @Override
    public void run() {
        SynchronizedCollectionEx.treeSet.add(Math.random() * 10);
        SynchronizedCollectionEx.treeSet.remove(Math.random() * 10);
    }
}

 

이와 같은 문제는 아래와 같이 synchronizedCollection를 사용하면 동기화 문제가 간단히 해결된다.

/**
 * Project : FourTeamEffectiveJavaStudy
 *
 * @author : jwdeveloper
 * @comment :
 * Time : 10:29 오후
 */
public class SynchronizedCollectionEx2 {
    static Collection treeSet = new TreeSet();
    static Collection synchronizedSet = Collections.synchronizedCollection(treeSet);

    public static void main(String[] args) {
        Thread[] thread = new Thread[10000];
        
        for (int i = 0; i < 10000; i++) {
            thread[i] = new Thread(new WriterThread2());
            thread[i].start();
        }
    }
    
}
class WriterThread2 implements Runnable{

    @Override
    public void run() {
        SynchronizedCollectionEx.treeSet.add(Math.random() * 10);
        SynchronizedCollectionEx.treeSet.remove(Math.random() * 10);
    }
    
}

 

"아파치의  synchronizedCollection  버전은 (컬렉션 대신) 클라이언트가 제공한 객체로 락을 거는 능력을 추가로 제공한다. 즉, 모든 메서드에서 주어진 락 객체로 동기화한 후 내부 컬렉션 객체에 기능을 위임하는 래퍼 클래스(Item 18)이다."
왜 이렇게 장황하게 설명하였을까?

이유는 synchronizedCollection이 removeIf를 재정의하고 있지 않기 때문이다.

 

기존의 removeIf는 동기화에 대해서 고려를 하지 않았기 때문에 멀티스레드 환경에서 removeIf를 호출한다면 동기화 관련 이슈가 발생할 수 있다.

 


결론은?

디폴트 메서드는 (컴파일에 성공하더라도) 기존 구현체에 런타임 오류를 일으킬 수 있다. 

 

기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 일이 아니라면 피해야 한다. 또한 추가하려는 디폴트 메서드가 기존 구현체들과 충돌하지 않을지 심사숙고해야 한다.

 

 

따라서 디폴트 메서드라는 도구가 생겼더라도 인터페이스 설계 시 여전히 세심한 주의가 필요하다. 또한 새로운 인터페이스라면 릴리스 전 반드시 테스트를 거쳐야 한다. 

 

인터페이스를 릴리스한 후라도 결함을 수정하는 게 가능한 경우도 있겠지만, 절대 그 가능성에 기대서는 안된다.