[Effective Java] 4장 클래스와 인터페이스 (2부)

정리
아이템 20. 일반적으로 다중 구현용 타입으로는 인터페이스가 가장 적합하다. 복잡한 인터페이스라면 구현하는 수고를 덜어주는 골격 구현을 함께 제공하는 방법을 꼭 고려해보자. 골격 구현은 '가능한' 한 인터페이스의 디폴트 메서드로 제공하여 그 인터페이스를 구현한 모든 곳에서 활용하도록 하는 것이 좋다. '가능한'이라고 한 이유는, 인터페이스에 걸려있는 구현상의 제약 때문에 골격 구현을 추상 클래스로 제공하는 경우가 더 흔하기 때문이다.
아이템 22. 인터페이스는 타입을 정의하는 용도로만 사용해야 한다. 상수 공개용 수단으로 사용하지말자.
아이템 23. 태그 달린 클래스를 써야하는 상황은 거의 없다. 새로운 클래스를 작성하는 데 태그 필드가 등장한다면 태그를 없애고 계층구조로 대체하는 방법을 생각해보자. 기존클래스가 태그 필드를 사용하고 있다면 계층구조로 리팩터링하는 걸 고민해보자
아이템 24. 중첩 클래스에는 4가지가 있으면 각각의 쓰임이 다르다. 메서드 밖에서도 사용해야 하거나 메서드 안에 정의하기엔 너무 길다면 멤버 클래스로 만든다. 멤버 클래스의 인스턴스 각각이 바깥 인스턴스를 참조한다면 비정적으로, 그렇지 않으면 정적으로 만들자. 중첩 클래스가 한 메서드 안에서만 쓰이면서 그 인스턴스를 생성하는 지점이 단 한 곳이고 해당 타입으로 쓰기에 적합한 클래스나 인터페이스가 이미 있다면 익명 클래스로 만들고, 그렇지 않으면 지역 클래스로 만들자.
아이템 25. 소스 파일 하나에는 반드시 Top레벨 클래스를 하나만 담자. 소스 파일을 어떤 순서로 컴파일하든 바이너리 파일이나 프로그램의 동작이 달라지는 일은 없을 것이다.

내용
아이템 20. 추상 클래스보다는 인터페이스를 우선하라
자바에서 제공하는 다중 구현 메커니즘은 인터페이스와 추상 클래스가 있다. 자바 8부터 인터페이스도 디폴트 메서드(default method)를 제공할 수 있게 되어 이제는 두 메커니즘 모두 인스턴스 메서드를 구현 형태로 제공할 수 있다.
추상클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야한다. 추상 클래스 방식은 새로운 타입을 정의하는 데 제약이 생긴다. 반면, 인터페이스가 선언한 메서드를 모두 정의하고 그 일반 규약을 잘 지킨 클래스라면 다른 어떤 클래스를 상속했든 같은 타입으로 취급된다.
기존의 클래스 위에 새로운 추상 클래스를 끼워 넣는 일은 어렵다. 하지만 인터페이스는 기존 클래스에도 쉽게 구현해 넣을 수 있다.
인터페이스는 믹스인(mixin) 정의에 안성맞춤이다. 믹스인이란, 클래스가 구현할 수 있는 타입으로 믹스인을 구현한 클래스에 원래 '주된 타입'외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 준다. 대상 타입의 주된 기능에 선택적 기능을 혼합한다고 해서 믹스인이라 부른다.
인터페이스는 계층구조가 없는 타입 프레임워크를 만들 수 있다. 그렇기 때문에 유연성을 줄 수 있다. 같은 구조를 클래스로 만들기 위해서 모든 조합 전부를 클래스에 정의해야 하는데 이 과정에서  매개변수 타입만 다른 메서드들을 수없이 많이 가진 거대한 클래스가 . 이런 것을 조합 폭발(combinatorial explosion)이라 부른다.
래퍼 클래스 관용구와 함께 사용한다면 인터페이스는 기능을 향상시키는 안전하고 강력한 수단이 된다. 타입을 추상 클래스로 정의해두면 그 타입에 기능을 추가하는 방법은 상속뿐이다. 상속으로 만든 클래스는 래퍼 클래스보다 활용도가 떨어지고 깨지기가 더 쉽다.
인터페이스의 디폴트 메서드를 구현할 수 있고, 구현을 했다면 @implSpec 자바독 태그를 붙여서 상속하려는 사람을 위해 문서화를 해야 한다.
디폴트 메서드에도 제약이 있다. 많은 인터페이스가 equals hashCode같은 Object의 인터페이스를 정의하고 있지만 디폴트 메서드로 제공해서는 안 된다. 또한 인터페이스는 인스턴스 필드를 가질 수 없고public이 아닌 정적 멤버도 가질 수 없다. (private 정적 메서드는 예외) 사용자가 만들지 않은 인터페이스에는 디폴트 메서드를 추가할 수 없다.
인터페이스와 추상 골격 구현(skeletal implementation) 클래스를 함께 제공하는 방식으로 인터페이스와 추상 클래스의 장점을 모두 취하는 방법도 있다. 골격 클래스는 나머지 메서드들까지 구현한다. 이렇게 단순히 골격 구현을 확장하는 것만으로 이 인터페이스를 구현하는 데 필요한 일이 대부분 완료된다. 이것 패턴이 바로 템플릿 메서드 패턴이다.
관례상 인터페이스 이름이 Interface라면 그 골격 구현 클래스의 이름은 AbstractInterface로 짓는다.
골격 구현을 확장하는 것으로 인터페이스 구현이 거의 끝나지만, 꼭 이렇게 해야 하는 것은 아니다. 구조상골격 구현을 확장하지 못하는 처지라면 인터페이스를 직접 구현해야 한다. 구조상 골격 구현을 확장하지 못하는 처지라면 인터페이스를 직접 구현해야 한다. 이런 경우라도 인터페이스가 직접 제공하는 디폴트 메서드의 이점을 여전히 누릴 수 있다.
인터페이스를 구현한 클래스에서 해당 골격 구현을 확장한 private 내부 클래스를 정의하고 각 메서드 호출을 내부 클래스의 인스턴스에 전달하는 것이다. 아이템 18에서 다룬 래퍼 클래스와 비슷한 이 방식은 시뮬레이트한 다중 상속(simulated multiple inheritance)이라 하며, 다중 상속의 많은 장점을 제공하는 동시에 단점은 피하게 해준다.
골격 구현 작성하는 방법은 다음과 같다.
1. 인터페이스를 잘 살펴 다른 메서드들의 구현에 사용되는 기반 메서드들을 선정한다. 이 기반 메서드들은 골격 구현에서는 추상 메서드가 될 것이다.
2. 기반 메서드들을 사용해 직접 구현할 수 있는 메서드를 모두 디폴트 메서드로 제공한다.
3. 만약 인터페이스의 메서드 모두 기반 메서드와 디폴트가 된다면 골격 구현 클래스를 별도로 만들 이유는 없다. 기반 메서드와 디폴트 메서드로 만들지 못한 메서드가 남아 있다면, 이 인터페이스를 구현하는 골격 구현 클래스를 하나 만들어 남은 메서드들을 작성해 넣는다. 골격 구현 클래스에는 필요하면 public이 아닌 필드와 메서드를 추가해도 된다.
예로 Map.Entry인터페이스가 있다. getKey, getValue는 기반 메서드이면, 선택적으로 setValue도 포함할 수 있다. 이 인터페이스는 equals hashCode의 동작 방식도 정의했다. Object 메서드들은 디폴트 메서드로 제공해서는 안되므로, 해당 메서드는 모두 골격 구현 클래스에 구현한다.
아이템 19에서 언급한 것처럼 설계 및 문서화 지침을 모두 따라야 한다.
단순 구현(simple implementation)은 골격 구현의 작은 변종으로, AbstractMap.SimpleEntry가 좋은 예이다. 단순 구현도 골격 구현과 같이 상속을 위해 인터페이스를 구현한 것이지만, 추상 클래스가 아니라 저마다 다르다. 쉽게 말해 동작하는 가장 단순한 구현이다. 필요에 따라 혹장해도 된다.
아이템 21. 인터페이스는 구현하는 쪽을 생각해 설계하라
● 자바 8 전에는 기존 구현체를 깨뜨리지 않고는 인터페이스에 메서드를 추가할 방법이 없었다. 자바 8에 와서 기존 인터페이스에 메서드를 추가할 수 있도록 디폴트 메서드를 소개했지만, 위험이 사라진 것은 아니다.
● 디폴트 메서드를 선언하면, 그 인터페이스를 구현한 후 디폴트 메서드를 재정의하지 않은 모든 클래스에서 디폴트 구현이 쓰이게 된다. 하지만 모든 기존 구현체들과 매끄럽게 연동되리라는 보장은 없다.
● 자바 8에서는 핵심 컬렉션 인터페이스들에 다수의 디폴트 메서드가 추가되었다. 주로 람다를 활용하기 위해서이다. 대부분의 자바 라이브러리의 디폴트 메서드를 잘 동작하지만 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기란 어려운 법이다.
● 대표적으로 문제가 되는 예는 org.apache.commons.collections4.collection.Synchronizedollection이다. 클라이언트가 제공한 객체로 락을 거는 능력을 추가로 제공한다. 모든 메서드에서 주어진 락 객체로 동기화한 후 내부 컬렉션 객체에 기능을 위임하는 래퍼 클래스다. removeIf 메서드를 재정의하지 않았기 때문에(현재 재정의 되어 있음) 그냥 사용할 경우 예기치 못한 결과로 이어질 수 있다.
● 자바 플랫폼 라이브러리에서도 이런 문제를 예방하기 위해 일련의 조치를 취했다. 구현한 인터페이스의 디폴트 메서드를 재정의하고, 다른 메서드에서는 디폴트 메서드를 호출하기 전에 필요한 작업을 수행하도록 했다. 하지만 자바 플랫폼에 속하지 않은 제3의 기존 컬렉션 구현체들은 이런 언어 차원의 인터페이스 변화에 맞춰 수정될 기회가 없었으면, 그중 일부는 여전히 수정되지 않고 있다.
● 디폴트 메서드는 컴파일에 성공하더라도 기존 구현체에 런타임 오류를 일으킬 수 있다. 자바 8은 컬렉션 인터페이스에 꽤 많은 디폴트 메서드를 추가했고, 그 결과 기존에 짜여진 많은 자바 코드가 영향을 받은 것으로 알려졌다.
● 기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 경우가 아니면 피해야한다. 추가하려는 디폴트 메서드가 기존 구현체들과 충돌하지는 않는지 확인해야한다. 반면 새로운 인터페이스를 만드는 경우라면 표준적인 메서드 구현을 제공하는 데 유용한 수단이며, 그 인터페이스를 더 쉽게 구현해 활용할 수 있게 해준다.
● 인터페이스를 설계할 때는 세심한 주의를 기울여야한다. 릴리스한 후라도 결함을 수정하는 게 가능한 경우도 있겠지만 절대 그 가능성에 기대서는 안된다.
아이템 22. 인터페이스는 타입을 정의하는 용도로만 사용하라
● 인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 한다. 인터페이스는 오직 이 용도로만 사용해야 한다.
● 상수 인터페이스는 대표적으로 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 하지 않는다. 이는 인터페이스를 잘못 사용한 예다. 클래스 내부에서 사용하는 상수는 외부 인터페이스가 아니라 내부 구현에 해당한다. 상수 인터페이스를 구현하는 것은 이 내부 구현을 클래스 API로 노출하는 행위다.
● 상수를 공개할 목적이라면 몇가지 고려를 해야 한다.
1. 특정 클래스나 인터페이스와 강하게 연관된 상수라면 그 클래스나 인터페이스에 자체에 추가해야 한다.
2. 열거 타입으로 나타내기 적합한 상수라면 열거 타입으로 만들어 공개하면 된다.
3. 열거 타입으로 나타내기 힘들다면 인스턴스화할 수 없는 유틸리티 클래스에 담아 공개하자.
● 유틸리티 클래스에 정의된 상수를 클라이언트에서 사용하려면 클래스 이름까지 함께 명시해야 한다. 유틸리티 클래스의 상수를 빈번히 사용한다면 정적 임포트(static import)하여 클래스 이름은 생략할 수 있다.
아이템 23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라
● 태그 달린 클래스는 장황하고 오류를 내기 쉽고, 비효율적이다.
1. 열거 타입 선언, 태그 필드, switch문 등 쓸데없는 코드가 많다.
2. 여러 구현이 한 클래스에 혼합돼 있어서 가독성도 나쁘다.
3. 다른 의미를 위한 코드도 있어서 메모리도 많이 사용한다. 필드들을 final로 선언하려면 해당 의미에 쓰이지 않는 필드들까지 생성자에서 초기화해야 한다.
4. 다른 의미를 추가하려면 코드를 수정해야 한다.
● 태그 달린 클래스를 클래스 계층구조로 바꾸는 방법이다.
1. 계층구조의 root가 될 추상 클래스를 정의하고, 태그 값에 따라 동작이 달라지는 메서드들을 루트 클래스의 추상 메서드로 선언한다.
2. 태그 값에 상관없이 동작이 일정한 메서드들을 루트 클래스에 일반 메서드로 추가한다. 모든 하위 클래스에서 공통으로 사용하는 데이터 필드들도 전부 루트 클래스로 올린다.
3. 루트 클래스를 확장한 구체 클래스를 의미별로 하나씩 정의한다.
● 계층구조 클래스로 바꾸면 여러가지 장점이 있다.
1. 각 의미를 독립된 클래스에 담아 관련 없던 데이터 필드를 모두 제거하고 남은 필드는 final로 선언되어 있다.
2. 컴파일러가 남은 필드를 초기화하고 추상 메서드를 모두 구현했지 확인한다.
3. 루트 클래스를 코드를 건드리지 않고도 다른 프로그래머들이 독립적으로 계층구조를 확장하고 함께 사용할 수 있다.
4. 타입 사이의 자연스러운 계층 관계를 반영할 수 있어서 유연성은 물론 컴파일타임 타입 검사 능력을 높여준다.
이번 아이템의 예에서는 접근자 메서드 없이 필드를 직접 노출했다. 공개할 클래스라면 이렇게 설계0000000000하지 않는 것이 좋다.
아이템 24. 멤버 클래스는 되도록 static으로 만들라
● 중첩 클래스의 종류는 정적 멤버 클래스, (비정적) 멤버 클래스, 익명 클래스, 지역 클래스 4가지가 있다. 정적 멤버 클래스 이외에는 내부 클래스(inner class)에 해당한다.
● 정적 멤버 클래스는 다른 클래스 안에 선언되고, 바깥 클래스의 private 멤버에도 접근할 수 있다는 점만 제외하고 일반 클래스와 똑같다. 다른 정적 멤버와 똑 같은 접근 규칙을 적용받는다. (private으로 선언하면 바깥 클래스에서만 사용할 수 있다.)
정적 멤버 클래스와 비정적 멤버 클래스의 구문상 차이는 단지 static이 붙어 있고 없고 뿐이지만, 의미상 차이는 크다. 비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결된다. 그래서 비정적 멤버 클래스의 인스턴스 메서드에서 정규화된 this를 사용해 바깥 인스턴스의 메서드를 호출하거나 바깥 인스턴스의 참조를 가져올 수 있다. , 중첩 클래스의 인스턴스가 바깥 인스턴스와 독립적으로 존재할 수 있다면 정적 멤버 클래스로 만들고, 비정적 멤버 클래스는 바깥 인스턴스 없이는 생성할 수 없다.
비정적 멤버 클래스의 인스턴스 안에 만들어져 메모리공간을 차지하며, 생성 시간도 더 걸린다. 어댑터를 정의할 대 자주 쓰인다. 어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게 하는 뷰로 사용하는 것이다.
● 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들자. Static을 생략하면 바깥 인스턴스로의 숨은 외부 참조를 갖게 된다. 이 참조를 저장하려면 시간과 공간이 소비되고 가비지 컬렉션이 수거하지 않는다면 메모리 누수로 이어진다.
● 멤버 클래스가 공개된 클래스의 public 이나 protected 멤버라면 static 여부에 따라 하위 호환이 깨질 수 있다.
● 익명 클래스
1. 이름이 없다. 또한 바깥 클래스의 멤버도 아니다.
2. 멤버와 달리, 쓰이는 시점에서 선언과 동시에 인스턴스가 만들어지고 코드의 어디서든 만들 수 있다.
3. 오직 비정적인 문맥에서 사용될 때만 바깥 클래스의 인스턴스를 참조할 수 있다.
4. 상수 표현을 위해 초기화된 final 기본타입과 문자열 필드만 가질 수 있다.
5. 선언한 지점에서만 인스턴스를 만들 수 있고, instanceof 검사나 클래스의 이름이 필요한 작업은 수행할 수 없다.
6. 여러 인터페이스를 구현할 수 없고, 인터페이스를 구현하는 동시에 다른 클래스를 상속할 수도 없다.
7. 익명 클래스를 사용하는 클라이언트는 그 익명 클래스가 상위 타입에서 상속한 멤버 외에는 호출할 수 없다.
● 지역 클래스
1. 지역 클래스는 지역변수를 선언할 수 있는 곳이면 실질적으로 어디서든 선언할 수 잇고, 유효 범위도 같다.
2. 멤버 클래스처럼 이름이 있고 반복해서 사용할 수 있다.
3. 익명 클래스처럼 비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있으며, 정적 멤버는 가질 수 없다.
아이템 25. Top레벨 클래스는 한 파일에 하나만 담으라
소스 파일 하나에 Top 레벨 클래스를 여러 개 선언할 수 있다. 하지만 아무런 득이 없을 뿐더러 심각한 위험을 감수해야하는 행위이다. 한 클래스를 여러 가지로 정의할 수 있으며, 그중 어느 것을 사용할지는 어느 소스파일을 먼저 컴파일하냐에 따라 달라지기 때문이다.
● 만약 굳이 여러 Top레벨 클래스들을 한 파일에 담고 싶다면 정적 멤버 클래스를 사용하는 방법을 사용할 수 있다. 다른 클래스에 딸린 부차적인 클래스라면 정적 멤버 클래스로 만드는 쪽이 일반적으로 더 나은 방법이다.

용어정리
● 숫자 리터럴에 사용한 밑줄(_)에 주목해보자. 자바 7부터 허용되는 이 밑줄은 숫자 리터럴의 갑에는 아무런 영향을 주지 않으면서, 읽기는 훨씬 편하게 해준다. 고정소수점 수든 부동소수점 수든 5자리 이상 이라면 밑줄을 사용하는 걸 고려한다. (, 6.022_140_857e23)

● 중첩 클래스(nested class)란 다른 클래스 안에 정의된 클래스를 말한다. 중첩 클래스는 자신을 감싼 바깥 클래스에서만 쓰여야 한다. 그 외의 쓰임새가 있다면 Top 레벨 클래스로 만들어야 한다.

●  정규화된 this, 클래스명.this 형태로 바깥 클래스의 이름을 명시하는 용법을 말한다.




4장 클래스와 인터페이스 2부는 인터페이스에 대해 소개하고 있다. 인터페이스는 다중 구현용 타입으로 사용하는 것이 가장 적절하다. 상수 공개용 수단으로 인터페이스를 사용하는 경우가 있는데, 이것은 내부 구현을 클래스 API로 노출하는 행위다. 중복 클래스는 정적 멤버 클래스로 만들어서 사용하는 방향으로 고려하자. 한 소스에 Top 레벨 클래스를 여러 개 선언이 가능하지만 하지말자.

댓글

가장 많이 본 글