[코드리뷰]java.util.ArrayList같은 객체를 인터페이스로 참조하기

2015-03-09 09:48

메서드 시그니처와 로컬변수 참조에서 모두 ArrayList같은 구현 클래스로 참조하기보다는 가능하다면 java.util.List같은 인터페이스로 참조하는것을 권장할만합니다. 즉 아래와 같은 메서드 시그니처보다는

public ArrayList<Track> findMyTracks() {

}

아래와 같이 바꾸는것이 바람직합니다.

public List<Track> findMyTracks() {

}

Effective java 2nd Edition의 Item 52에서 권장하는 항목입니다. ( http://jtechies.blogspot.kr/2012/07/item-52-refer-to-objects-by-their.html 참조 ). Effective Java의 저자 Joshua Bloch은 Java Collection Framework의 설계자입니다. java.util.Collection의 소스코드를 보면 그가 @author 항목에 표시되어 있습니다.

이 지침에 아주 민감한 개발자도 있으니 더욱 염두에 두어야합니다.

List xxx = new ArrayList 도 아닌 그냥 ArrayList xxx = new ArrayList… 오랜만에 코드를 보다 살의를 느낌. ( http://www.okjsp.net/seq/262881 중에서)

사용처에 따라서 중요한 정도

이 지침은 모든 영역에서 중요하지만, 우선 순위를 따진다면 아래와 같습니다.

  1. 공통 프레임워크성 코드의 메서드 시그니처
  2. 비지니스 코드의 메서드 시그니처
  3. Local 변수의 참조 타입

한 예로 Spring framework의 RowMapperResultSetExtractor는 내부적으로 LinkedList를 사용하다가 ArrayList로 교체한 적이 있습니다. 만약 LinkedList라는 구현 클래스가 외부에 노출되어 있었다면 이런 변경은 하위 호환성을 깨지 않고는 하기 어려웠습니다.

  • RowMapperResultSetExtractor 2.0.1 -> [RowMapperResultSetExtractor 2.0.2]( http://grepcode.com/file/repo1.maven.org/maven2/org.springframework/spring/2.0.2/org/springframework/jdbc/core/RowMapperResultSetExtractor.java/)

공통 프레임워크성 코드에서는 java.util.List처럼 Collection 요소뿐만 아니라 프레임워크 구성요소를 interface로 선언하는 설계는 더욱 깊이 고민해야합니다. 여러 프로젝트에서 참조하는 모듈이라면 인터페이스 참조를 하지 않고 구현 클래스를 변경한다면 하위호환성을 깨게 됩니다.

비지니스 코드의 메서드 시그니처에서도 이 지침을 의식해야합니다.

무엇보다 Arrays.asList()로 생성한 객체를 비니지스 코드의 메서드 파라미터로 넘기거나, Collections.emtpyList()로 생성한 객체를 return타입으로 넘길 때등 JDK의 여러 유틸리키성 클래스를 활용하는데에도 인터페이스 참조가 유리합니다.

또는 구현클래스를 교체하는 변경비용도 적어집니다. 비니지스 코드에서 ArrayList를 LinkedList로 교체할일은 거의 없지않냐고 생각하실분도 있을수도 있습니다. 그러나 오랫동안 유지하면서 꾸준히 개선할 시스템에는 생각보다 그런 일이 많이 발생합니다. 예를 들면 아래와 같습니다.

  • JDK 1.4를 쓰던 시스템에서 JDK 버전을 올리면서 Vector -> ArrayList로 교체
  • 기본 JDK의 Collection을 보다 개선된 Guava나 GS-Collections 같은 라이브러리를 도입
  • JDK7 -> JD8로 업그레이드하면서 기존의 for, if문으로 되어 있었던 필터링 처리를 Stream과 람다를 이용해서 바꾸고, 마지막 return은 'Stream.collect(Collectors.toList());'으로 하고 싶을 때

java Collection framework의 인터페이스들은 JDK8의 Stream 객체에서도 변환될수 있고, Guava나 GS Collection, Totally lazy같은 라이브러리 뿐만이 아니라 Groovy, Scala 등의 다른 JVM 언어에서도 지원하고 있습니다. ( https://github.com/benelog/lambda-resort 의 예제 참조). 따라서 JVM 버전업이나 다른 라이브러리나 JVM 언어를 도입할 때도 기존의 코드가 인터페이스를 참조하고 있는 것이 유리합니다.

이 전의 코드의 메서드 시그니처에서 구체적인 클래스에 의존하고 있었다면 위와 같은 변경에 비용이 커집니다. IDE에서 find, replace-all로 고치면 되지 않느냐고 생각할수도 있지만, 수정할 범위가 커지면 그것도 부담이 되는 작업입니다. 프로젝트가 jar파일로 빌드되어서 여러 곳에 배포되는 경우라면 더욱 그런 수정작업에 실수와 오류의 가능성이 높습니다.

다만 비지니스 코드 ( *Service, *BO, *DAO) 자체를 인터페이스로 선언하고 구현클래스를 따로 둘지는 (UserService, UserServiceImpl 처럼) 프로젝트에서 추구하는 바에 따라서 선택할수 있다고 봅니다.

마지막으로 Local 변수를 참조할때도 아래와 같이 인터페이스 참조로하는것이 좋습니다.

Map<String, String> idToName = new LinkedHashMap<>();

객체의 scope가 짧은 때는 인터페이스로 참조하지 않아도 변경비용에는 차이가 없지만, 그래도 인터페이스를 쓰면 아래와 같은 장점이 있습니다.

  • 향후 메서드 추출, 클래스 추출 리팩토링에도 자연스럽게 interface가 반영됨.
  • 특정 메서드를 사용할 때 interface에 정의된 것인지, 클래스 레벨에서만 지원하는지 의식할 수 있습니다. 그래서 특정 구현체에 대한 의존정도를 더 섬세하게 제어할 수 있습니다.

지침의 예외

Effective Java의 Item 52에서도 이 지침에 예외가 있음을 아래와 같이 밝히고 있습니다.

  1. String, BigInteger와 같은 'value class'의 참조
  2. 적절히 참조할 수 있는 인터페이스가 없을 때
  3. 인터페이스 수준에서 적절한 메서드가 제공되지 않을 때

3번은 LinkedList.addFirst()를 예로 들수 있습니다.

참고로, Android에서는 한 때 성능 최적화를 위해서 인터페이스 참조를 하지 말라지는 지침이 Google의 가이드에 있기도 했습니다. 지금은 공식가이드에서는 사라지고, 근래의 단말에서는 전체 성능에 비해서는 미묘한 차이라서 성능보다는 설계 유연성을 추구할 수도 있습니다. 그래도 Android 프로젝트에서는 해당 프로젝트에서 추구하는 바와 관레를 따라서 판단하는것이 좋습니다.

0개의 의견 from SLiPP

의견 추가하기

연관태그

← 목록으로