객체를 선언할 때는 클래스가 아닌 인터페이스로 선언한다

등록일: 2014. 08. 12

자바 코딩, 이럴 땐 이렇게: PMD로 배우는 올바른 자바 코딩 방법

  • 배병선 지음
  • 426쪽
  • 28,000원
  • 2014년 05월 28일

간혹 자바의 컬렉션을 사용할 때 List, Set, Map과 같은 인터페이스가 아닌 ArrayList, Vector, HashMap, HashTable과 같은 구현체로 선언할 때가 있는데 이는 객체의 결합도 측면에서 매우 비효율적인 방식이다.

예를 들어, Vector와 ArrayList는 List라는 인터페이스를 바탕으로 같은 기능을 구현하지만 Vector는 자바 1.0부터 있던 클래스로서 다중 스레드 환경에서 스레드 동기화를 바탕으로 구현된 클래스이고 ArrayList는 자바 1.2부터 포함된 클래스로서 스레드 동기화를 제공하지 않아 다중 스레드 환경에서 스레드 안전성(thread-safety)을 보장할 수 없지만 Vector에 비해 빠른 성능을 보이는 것이 특징이다. 즉, 이 두 객체는 외부에 보이는 모양은 같지만 전혀 다른 알맹이를 갖고 있으며, ArrayList<String> list = new Vector<String>();와 같이 서로 간의 직접적인 사용은 불가능하다.

예제 10.1.1과 같이 ArrayList로 선언한 변수는 ArrayList를 위한 ArrayListMaker를, Vector로 선언한 변수는 Vector를 위한 VectorMaker로 각각 사용할 수밖에 없고, ArrayList가 없으면 다른 한쪽인 ArrayListMaker가 의미 없을 정도로 서로 종속성이 매우 강한 1:1 연결을 보인다. 또한 만약 List 인터페이스에서 파생된 다른 구현체인 Stack이나 LinkedList를 위한 클래스를 구현하려면 별도의 클래스를 구현해야 한다. 결과적으로 변수의 자료형으로 구현체를 사용하는 것은 객체와 객체 간의 연결을 매우 강하게 유지하게 한다.

예제 10.1.1 Vector와 ArrayList 객체를 위한 클래스를 각각 구현한 예

ArrayListMaker

package com.software.coupling.problem.list;

import java.util.ArrayList;

public class ArrayListMaker {

    public ArrayList<String> add(ArrayList<String> arrayList) {
        arrayList.add("t");
        arrayList.add("e");
        arrayList.add("s");
        arrayList.add("t");

        return arrayList;
    }
}

VectorMaker

package com.software.coupling.problem.list;

import java.util.Vector;

public class VectorMaker {

    public Vector<String> add(Vector<String> vector) {
        vector.add("t");
        vector.add("e");
        vector.add("s");
        vector.add("t");

        return vector;
    }
}

LoosingCouplingExample

import java.util.ArrayList;
import java.util.Vector;

public class LoosingCouplingExample {

    public static void main(String[] args) {
        VectorMaker vectorMaker = new VectorMaker();
        ArrayListMaker arrayListMaker = new ArrayListMaker();

        ArrayList<String> arrayList = new ArrayList<String>();
        arrayList = arrayListMaker.make(arrayList);

        Vector<String> vector = new Vector<String>();
        vector = vectorMaker.make(vector);
    }
}

package com.software.design.problem;

import java.util.ArrayList;
import java.util.Vector;

public class LoosingCouplingExample {

    public static void main(String[] args) {
        ArrayList<String> list1 = makeVector();
    }

    public static Vector<String> makeVector() {
        return new Vector<String>();
    }
}

문제점 진단

PMD에서는 상세 구현 객체보다 인터페이스로 변수를 선언하기를 권고하며, 변수를 구현체로 선언할 경우 이를 LooseCoupling 룰로 진단하고 결합도를 낮추도록 경고한다. 그림 10.1.1은 구현체로 선언된 변수를 진단한 결과다.

figure10-1-1
그림 10.1.1 LooseCoupling 룰로 인터페이스가 아닌 구현체로 선언된 변수를 진단한 결과

해결 방안

이 문제를 해결하는 방안은 각 변수의 선언을 구현체가 아닌 인터페이스로 선언하는 것이다. 인터페이스란 객체의 형식을 정하는 하나의 명세서(spec)와 같은 역할을 한다. 따라서 변수를 인터페이스로 선언하면 객체의 내부 구현 구조와 상관없이 해당 인터페이스의 형태로 구현한 모든 구현 객체를 변수에 지정할 수 있다. 즉, 객체와 객체 간의 직접적인 연결이 아닌 객체의 형태와 객체라는 조금 더 간접적이고 유연한 연결관계를 형성할 수 있다. 인터페이스의 사용을 다음과 같이 비유할 수 있다.

소나타는 승용차다. – 참
포터는 화물차다. – 참
봉고는 승합차다. – 참
소나타는 화물차다 – 거짓
소나타는 포터다 – 거짓
포터는 승합차다 – 거짓
포터는 봉고다 – 거짓

자동차라는 큰 범주에 승용차, 화물차 그리고 승합차 등은 포함될 수 있지만 자동차라는 형태를 구현한 승용차, 화물차 그리고 승합차 모두는 각기 다른 고유 속성과 기능을 가지고 있으므로 서로 같은 객체가 될 수는 없다. 이를 List 인터페이스와 그 하위 구현체인 ArrayList, Vector, LinkedList, Stack으로 바꾸면

ArrayList는 List다 - 참
Vector는 List다 – 참
LinkedList는 List다 – 참
Stack은 List다 – 참
ArrayList는 Vector다 - 거짓
LinkedList는 Stack이다 – 거짓

위와 같이 모든 객체가 List 인터페이스라는 형태를 취하고 있지만 고유의 속성과 기능을 포함하므로 서로 다른 구현체다. 따라서 변수를 선언할 때 고유한 특성을 갖는 구현체가 아닌 인터페이스로 선언하면 해당 변수를 단일한 구현 객체의 형태로 제한되지 않아서 같은 인터페이스를 바탕으로 구현한 모든 객체가 호환되어 코드의 유연성이 높아진다. 예제 10.1.2는 예제 10.1.1의 구현체를 바탕으로 작성한 코드를 인터페이스를 바탕으로 리팩터링한 코드다.

ListMaker.java

package com.software.coupling.solution.list;

import java.util.List;

public class ListMaker {

    /**
    * 메서드의 인자와 반환 값의 자료형을 List 인터페이스로 선언함으로써
    * List 인터페이스를 바탕으로 구현한 모든 구현 객체는
    * 이 메서드를 활용할 수 있다.
    * @param list
    * @return
    */
    public List<String> make(List<String> list) {
        list.add("t");
        list.add("e");
        list.add("s");
        list.add("t");

        return list;
    }
}

LoosingCouplingExample

package com.software.coupling.solution.list;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;
import java.util.Vector;

public class LoosingCouplingExample {

    public static void main(String[] args) {
        ListMaker listMaker = new ListMaker();
        /*
        * List 인터페이스로 변수를 선언해
        * List 인터페이스를 바탕으로 구현한
        * 이 변수는 List 인터페이스를 바탕으로
        * 구현한 모든 객체를 받아들일 수 있다.
        */
        List<String> list = new ArrayList<String>();
        list = listMaker.make(list);

        list = new Vector<String>();
        list = listMaker.make(list);

        list = new LinkedList<String>();
        list = listMaker.make(list);

        list = new Stack<String>();
        list = listMaker.make(list);
    }
}
익명 내부 클래스를 이용한 List 초기화는 비효율적이다

간혹 List를 초기화하기 위해 아래 예제와 같이 ArrayList를 확장해 익명 내부 클래스(anonymous inner class)로 초기화하는 소스코드를 볼 수 있다. 언뜻 보면 ArrayList를 선언한 이후 add 메서드를 이용해 List를 초기화하는 것보다 효율적으로 보이지만 사실 이 방식은 성능상 매우 비효율적이고 컴파일 시 불필요한 내부 클래스 파일(.class)을 만들어낸다

// 익명 내부 클래스를 이용한 List 초기화 방법
List<String> list = new ArrayList<String>() {{ 
    add("Hello");
    add("World!");
}};

// 일반적인 List의 초기화 방법
List<String> list2 = new ArrayList<String>();
list2.add("Hello");
list2.add("World!");