클래스와 인터페이스

Effective JAVA

6 minute read

ITEM15: 클래스와 멤버의 접근 권한을 최소화하라

  • 사실 두말하면 잔소리다.
  • 정보 은닉, 캡슐화는 소프트웨어 설계의 근간이 되는 원리이다.
  • public 등 API는 하위 호환을 위해 영원히 관리해줘야 한다.

특히나 자바의 경우에는

  • 패키지 외부에서 쓸 이유가 없다면 package-private으로 선언하자.
  • 한 클래스에서만 사용하는 package-private 클래스나 인터페이스를 private static으로 중첩해서 사용하자. 이 경우 바깥 클래스 하나에서만 접근할 수 있다.
  • public일 필요가 없는 클래스의 접근 수준을 package-private로 좁혀두자.
  • Serializable을 구현한 클래스에서는 그 필드들도 의도치 않게 공개 API가 될 수 있다.
  • Overriding을 할때는 상위 클래스에서의 접근 수준보다 좁게 지정할 수 없다. 이는 리스코프 치환법칙이다.
  • 해당 클래스가 추상 개념을 완성하는데 꼭 필요한 구성요소로서의 상수라면 public static final 필드로 공개해도 좋다. 이 경우 대문자와 언더스코어(_)를 통해 명명하고 값은 반드시 값이나 불변 객체이어야 한다.
  • 길이가 0이 아닌 배열은 참조이므로 변경 가능하다. 주의해야 한다.

모듈 시스템

  • Java9부터 도입되었다.
  • 모듈은 패키지의 묶음이다. 패키지 중 공개(export)할 것들을 (관례상 module-info.java 파일에 선언한다.
  • 접근 지정자와 상관없이 위 파일에 공개되지 않은 패키지는 외부에서 접근할 수 없다.
  • JDK 자체가 이를 적극 활용하였다. 자바 라이브러리에서 공개하지 않은 패키지들은 외부에서 접근할 수 없다.

ITEM16: public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라

  • 오히려 너무 당연한 말이라서 public으로 써도 좋을만한 사례만 정리한다.
  • package-private 클래스 혹은 private 중첩 클래스라면 데이터 필드를 노출한다해도 문제가 없다.
  • 그 클래스가 표현하려는 추상 개념만 올바르게 표현해주면 된다.
  • 클라이언트도 어차피 이 클래스를 포함하는 패키지 안에서만 동작하는 코드일 뿐이기 때문이다.
  • 이는 접근자 방식보다 깔끔하다.

ITEM17: 변경 가능성을 최소화하라

  • 불변 클래스란 그 인스턴스의 내부 값을 수정할 수 없는 클래스이다.
  • 아래 다섯가지 규칙을 따르면 불변 클래스라 할 수 있다.

객체의 상태를 변경하는 메소드를 제공하지 않는다.
클래스를 확장할 수 없도록 한다
모든 필드를 final로 선언한다.
모든 필드를 private으로 선언한다.
자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.

불변 객체의 예

public final class Complex {  
	private final double re;  
	private final double im;  
  
	// 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.  
	public Complex(double re, double im) {  
		this.re = re;  
		this.im = im;  
	}  
  
	public Complex plus(Complex c) {  
		return new Complex(re + c.re, im + c.im);  
	}  
}  

불변 객체의 장점

  • 불변 객체는 단순하다.
  • 근본적으로 스레드에 안전하여 따로 동기화할 필요가 없다. 따라서 안심하고 공유할 수 있다.
  • 정적 팩토리 메소드 등을 통해 인스턴스를 중복 생성하지 않도록 캐싱할 수 있다.

불변 객체의 단점

  • 단점으로는 원하는 객체를 완성하기까지 단계가 많고, 그 중간 단계에서 만들어진 객체들이 모두 버려진다면 성능 문제가 있을 수 있다는 것이다. 대표적인 예가 String과 같은 클래스이다.
  • 이런 경우 가변 동반 클래스(companion class)를 package-private으로 두곤 한다.
  • String의 경우 가변 동반 클래스로 StringBuilder를 두고 있다.

함수형 프로그래밍

  • 피연산자에 함수를 적용해 그 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴을 말한다.
  • 위 코드에서 plusnew를 통해 새로운 객체를 반환한다. 따라서 내부 상태가 변하지 않으므로 함수형 프로그래밍이다.

ITEM18: 상속보다는 컴포지션을 사용하라

  • 이번도 너무 당연한 이야기다. C++에서도 상속은 friend 다음으로 강력한 결합이다.
  • 메소드 호출과는 달리 상속은 캡슐화를 깨뜨린다.
  • 상속은 결함까지도 상속함을 주의한다. 또한 어떻게 overriding될지 모르기에 미래의 동작을 예측할 수 없다.
  • 기존 클래스를 새로운 클래스의 구성요소로 쓴다고 하여 이러한 설계를 컴포지션(composition)이라 한다.
  • 기존 클래스의 API를 호출하는 것을 forwarding, 그 호출하는 메소드를 forwarding method라고 부른다.
  • 상속은 반드시 하위 클래스가 상위 클래스의 진짜 하위 타입인 상황(is-a)에서만 쓰여야 한다.

SELF 문제

  • 컴포지션은 거의 단점이 없지만 Callback 형태의 구조에서 취약점이 있다.
  • 내부 객체는 자신을 감싸고 있는 wrapper의 존재를 모르므로 자신(this)의 참조를 넘기게 되고, 콜백 때 wrapper가 아닌 자신이 호출되게 된다. 이러한 문제를 SELF 문제라 하며 주의해야 한다.

ITEM19: 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라

  • 상속은 ITEM18에서 보듯 위험한 기술이지만 그만큼 강력한 기술이기도 하다.
  • 사용한다면 protected 메소드를 최소화하는게 좋지만, 너무 적게 노출해서 상속으로 얻는 이점마저 없애지 않도록 주의해야 한다.
  • 상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는 것이 유일하다.
  • 상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메소드를 호출해서는 안된다. 상위 클래스의 생성자가 먼저 호출되고, 어떻게 동작할지 하위 클래스에 달려 있어 장담할 수 없다.
  • Cloneable이나 Serializable 인터페이스를 구현한 클래스는 상속을 허용하면 일반적으로 좋지 않다.

상속을 금지하는 방법

  • 클래스를 final로 선언하면 상속을 금지할 수 있다.
  • 모든 생성자를 private이나 package-private으로 선언하고 public 정적 팩토리를 만들어주는 방법도 있다.

ITEM20: 추상 클래스보다는 인터페이스를 우선하라

  • 인터페이스가 선언된 메소드를 모두 정의하면 다른 어떤 클래스를 상속했든 같은 타입을 취급된다.

인터페이스는 믹스인(mixin) 정의에 안성맞춤이다.

  • 믹스인을 구현한 클래스에 원래의 ‘주된’ 타입 외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 준다.
  • 예컨대 Comparable은자신을 구현한 클래스의 인스턴스들끼리는 순서를 정할 수 있다고 선언하는 믹스인 인터페이스이다.
  • 이처럼 대상 타입의 주된 기능에 선택적 기능을 혼합한다고 해서 믹스인이라고 부른다.

인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다.

public interface SingerSongwriter extends Singer, Songwriter {  
	AudioClip strum();  
	void actSensitive();  
}  
  • 위와 같은 구조를 클래스로 만들려면 가능한 조합의 전부를 각각의 클래스로 정의한 고도비만 계층구조가 만들어질 것이다.
  • 속성이 n개라면 지원해야 할 조합의 수는 2^n개나 된다. 흔히 조합 폭발(combinatorial explosion)이라 부르는 현상이다.

추상 골격 구현(skeletal implementation)

  • 인터페이스와 추상 클래스의 장점을 모두 취하는 방법이다.
  • 단순히 골격 구현을 확장하는 것만으로 이 인터페이스를 구현하는데 필요한 일이 대부분 완료된다. 바로 템플릿 메소드 패턴이다.
  • 관례상 인터페이스 이름이 Interface라면 그 골격 구현 클래스의 이름은 AbstractInterface로 짓는다.
static List<Integer> intArrayAsList(int[] a) {  
	Objects.requireNonNull(a);  
  
	return new AbstractList<>() {  
		@Override  
		public Integer get(int i) {  
			return a[i];  
		}  
  
		@Override  
		public int size() {  
			return a.length;  
		}  
	}  
}  

주의사항

  • equals, hashCode와 같은 Object의 메소드는 디폴트 메소드로 제공하면 안된다.

ITEM21: 인터페이스는 구현하는 쪽을 생각해 설계하라

디폴트 메소드(default method)

  • 자바8 이전에는 기존 구현체를 깨뜨리지 않고는 인터페이스에 메소드를 추가할 방법이 없었다.
  • 디폴트 메소드를 선언하면 그 인터페이스를 구현한 후 디폴트 메소드를 재정의하지 않은 모든 클래스에서 디폴트 구현이 쓰이게 된다.
  • 하지만 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메소드를 작성하기란 어려운 법이다.
  • 다중 상속이 가능하므로 AbstractClass와는 차별성이 있다.

주의사항

  • 기존 인터페이스에 디폴트 메소드로 새 메소드를 추가하는 일은 꼭 필요한 경우가 아니면 피해야 한다. 추가하려는 디폴트 메소드가 기존 구현체들과 충돌하지는 않을지 심사숙고해야한다.
  • 새로운 인터페이스를 만드는 경우라면 표준적인 메소드 구현을 제공하는데 매우 유용한 수단이며, 그 인터페이스를 더 쉽게 구현해 활용할 수 있게끔 해준다.

ITEM22: 인터페이스는 타입을 정의하는 용도로만 사용하라

  • 인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 한다.
  • 달리 말해 클래스가 어떤 인터페이스를 구현한다는 것은 자신의 인스턴스로 무엇을 할 수 있는지를 클라이언트에 얘기해주는 것이다.
  • 인터페이스는 오직 이 용도로만 사용해야 한다.
  • 상수 인터페이스 안티패턴은 인터페이스를 잘못 사용한 예이다. enum을 사용하자.

ITEM23: 태그 달린 클래스보다는 클래스 계층구조를 활용하라

  • 태그달린 클래스에는 단점이 한가득이다. 우선 열거 타입 선언, 태그 필드, switch 문 등 쓸데없는 코드가 많다.
  • 태그달린 클래스는 클래스 계층구조를 어설프게 흉내낸 아류일 뿐이다.

ITEM24: 멤버 클래스는 되도록 static으로 만들어라

  • 중첩 클래스(nested class)란 다른 클래스 안에 정의된 클래스를 말한다.
  • 중첩 클래스는 자신을 감싼 바깥 클래스에서만 쓰여야 하며, 그 외에 쓰임새가 있다면 탑레벨 클래스로 만들어야 한다.
  • 중첩 클래스의 종류는 정적 멤버 클래스, 비정적 멤버 클래스, 익명 클래스, 지역 클래스 이렇게 네 가지이다.

정적 멤버 클래스

  • 다른 클래스 안에 선언되고, 바깥 클래스의 private 멤버에도 접근할 수 있다는 점만 제외하고는 일반 클래스와 똑같다.
  • 흔히 바깥 클래스와 함께 쓰일 때만 유용한 public 도우미 클래스로 쓰인다.
  • Operation 열거 타입은 Calculator 클래스의 public 정적 멤버 클래스가 되어야 한다. (Calculator.Operation.MINUS)
  • 개념상 중첩 클래스의 인스턴스가 바깥 인스턴스와 독립적으로 존재할 수 있다면 정적 멤버 클래스로 만들어야 한다. 비정적 멤버 클래스는 바깥 인스턴스없이는 생성할 수 없기 때문이다.
  • 반대로 말하면 바깥 인스턴스를 참조해야한다면 비정적 멤버 클래스를 사용해야 한다.

비정적 멤버 클래스

  • 바깥 클래스의 인스턴스 메소드에서 비정적 멤버 클래스의 생성자를 호출할 때 자동으로 만들어지는게 보통이다.
  • 직접 수동으로 만들때는 직접 바깥 인스턴스의 클래스.new MemberClass(args)를 호출한다.
User user = new User();  
Address address = user.new Address();  
  • 반복자(Iterator)의 구현은 비정적 멤버 클래스의 흔한 쓰임이다.
public class MySet<E> extends AbstractSet<E> {  
	  
	@Override  
	public Iterator<E> iterator() {  
		return new MyIterator();  
	}  
  
	private class MyIterator implements Iterator<E> {  
		  
	}  
}  
  • 멤버 클래스가에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들어야 한다.
  • 자원낭비 이외에도 가비지 컬렉션이 바깥 클래스의 인스턴스를 수거하지 못하는 문제가 발생할 수 있다!

익명 클래스

  • 익명 클래스는 표현식 중간에 등장하므로 10줄 이하로 짧지 않으면 가독성이 떨어진다.
  • 자바8 이후로는 람다가 익명 클래스의 역할을 대체하게 되었다.
Interface Name {  
	public void call();  
}  
  
Name name = new Name() {  
	private int age;  
  
	@Override  
	public void call() {  
		  
	}  
}  

ITEM25: 탑레벨 클래스는 한 파일에 하나만 담아라

  • 소스 파일 하나에 탑레벨 클래스(inner class가 아닌)를 여러개 선언하더라도 자바 컴파일러는 불평하지 않는다.
  • 탑레벨 클래스는 한 파일에 하나만 담아라.
  • 굳이 여러 탑레벨 클래스를 한 파일에 담고 싶다면 정적 멤버 클래스를 사용하는 방법을 고민해볼 수 있다.