Skip to main content

04. 클래스와 인터페이스

추상화의 기본 단위인 클래스와 인터페이스는 자바 언어의 심장과 같다.

클래스와 인터페이스를 적절히 사용하여 변경에 유연한 객체지향스러운 코드를 만들어보자.

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

item 15

당연한 이야기이다. 객체지향에서 항상 이야기하는 정보은닉은 괜히 하라고 하는 것이 아니다.

클래스 내부를 감추면 감출 수록 클래스는 하나의 역할과 책임을 가질 확률이 높다.

이렇게 정보를 은닉함으로써 다른 객체간의 종속성을 줄여준다.

자바에서 역시 이와 관련된 기능을 접근제한자로 제공해준다.

이 접근 제한자를 제대로 사용하는 원칙은 딱 하나이다.

모든 클래스와 멤버의 접근성을 가능한 좁혀라이다.

그리고 주의해야할 점이 하나 더 있는데, 클래스에서 public static final 배열 필드를 두거나 이 필드를 반환하는 접근자 메서드를 제공하지는 말자.

클라이언트가 해당 배열의 내용을 수정할 수 있게 되니까 말이다.

이를 회피하기 위해 해당 배열의 접근제한자를 priavte로 바꾸고 복사본을 만드는 방식이다.

자바 9에서 모듈 시스템이라는 개념이 도입되면서 조금 더 직관적으로 접근을 제한할 수 있게 되었다.

모듈은 자신이 속하는 패키지 중 공개해야할 것들을 modue-info.java 파일에 선언한다.

해당 패키지를 공개하지 않으면 외부에 공개되지 않는다.

모듈 시스템을 활용하면 클래스를 외부에 공개하지 않으면서 모듈을 이루는 패키지 사이에서는 자유롭게 공유하는 것이 가능하다.

결론적으로 클래스의 접근제한은 빡시게(?) 가져가는 편이 좋다.

클래스 내부의 변경을 최소화하고 메소드를 통해서만 접근할 수 있게 해두자.

이는 DDD에서도 중요한 개념으로 등장한다.

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

item 16

public 접근제어자 클래스라면 패키지 바깥에서 접근자를 제공함으로써 클래스 내부 표현 방식을 언제든 바꿀 수 있는 유연성을 제공하는 편이 더 낫다.

그러나 package-private 클래스나 private 중첩 클래스라면 데이터 필드를 노출한다 해도 하등 문제가 없고, 오히려 데이터 필드를 노출하는 편이 더 낫다.

public 클래스는 절대 가변 필드를 직접 노출해서는 안된다. 불변 필드라면 덜 위험하지만, 여전히 노출하지 않는 편이 대다수 경우에서 더 낫다.

변경 가능성을 최소화하라#

item 17

클래스를 불변으로 만들어 변경을 최소화하는 것이 좋다.

불변 클래스는 가변 클래스보다 설계하고 구현, 사용하기가 쉽고 오류가 생길 여지도 적으며 안전하다.

불변 클래스를 만들기 위한 규약은 아래와 같다.

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

객체가 불변한 것의 가장 큰 장점은 Thread Safe 하다는 점이다.

불변 객체는 방어적 복사도 필요없다는 결론으로 이어진다.

아무리 복사해봐야 원본과 똑같으니 복사의 의미가 없다. 그러니 불변클래스는 clone 메소드나 복사 생성자를 제공하지 않는 편이 좋다.

불변 객체는 자유롭게 공유할 수 있음은 물론이요, 불변 객체끼리는 내부 데이터를 공유할 수도 있다.

이런 불변 클래스에도 단점은 있으니, 값이 다르면 반드시 독립된 객체로 만들어야한다는 점이다.

값의 가짓수가 많은 경우라면 일일이 인스턴스로 만들어줘야하므로 비용이 꽤 비쌀 것이다.

불변 클래스는 이름 그대로 상속을 받지 못하게 해야하는데, final 클래스로 만드는 것 보다 더 유연한 방법이 있다.

바로, 모든 생성자를 private 혹은 package-private으로 만들고 첫 장에 배운 public 정적 팩터리 메서드를 제공하는 방법이다.

이러한 정적 팩터리 메서드를 제공하면 인스턴스 캐싱 기능을 추가해 성능을 끌어올리는 것도 가능할 것이다.

또한 setter를 만드는 것에 특별한 주의가 필요하다.

이는 DDD에서도 언급되는 부분인데, 클래스는 꼭 필요한 경우가 아니라면 불변인 편이 좋다.

불변 클래스는 장점이 더 많고, 단점이라곤 특정 상황에서의 성능 저하밖에 없다.

모든 클래스를 불변으로 만드는 것은 불가능하지만, 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분은 최소화하는 편이 좋다.

생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야한다.

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

item 18

어떻게 보면 당연한 이야기이다.

상속은 자바에서 잘못 만들어진 기능이며 이를 통해 클래스를 확장해 나가는 것은 설계를 더 어렵게 만든다.

상속은 캡슐화를 깨뜨리는 주범이다.

상위 클래스에 의존적일 수 밖에 없기 때문에 상위 클래스가 바뀌면 하위 클래스의 동작에 문제가 생길 수 있다.

이러한 상속 구조를 피해갈 수 있는 묘안이 바로 컴포지션이다.

이 컴포지션은 기존 클래스가 새로운 클래스를 자신의 구성요소로 사용하는 설계기법을 의미한다.

컴포지션은 기존 클래스의 결함을 숨기는 새로운 API를 설계해볼 수 있지만, 상속은 상위 클래스의 API를 결함까지도 승계하기 떄문에 변경을 더 어렵게 만든다.

상속은 가급적 상위, 하위가 순수한 is-a 관계일 때만 사용하는 편이 좋다.

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

item 19

상속용 클래스에서 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용할 수 있는지에 대한 문서를 반드시 남겨야한다.

아이러니하게도 클래스를 안전하게 상속하게 하기 위해선 굳이 설명할 필요도 없었던 내부 구현 방식을 자세하게 설명해야만한다.

클래스의 내부 동작 과정 중간에 끼어들 수 있는 hook을 잘 선별하여 protected 메서드 형태로 제공하여 공개하는 것도 좋은 방법이다.

그리고 상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다.

이를 어기면 의도치않은 동작이 실행될 수 있다.

상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로, 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 더 먼저 호출되어버린다.

그런데 거기서 재정의 메서드가 하위 클래스의 생성자에서 초기화되는 값에 의존되어버린다면 이는 동작이 이상하게 될 것이다.

클래스를 상속용으로 설계하는 것에는 엄청난 노력과 비용이 들며, 클래스 자체에 제약도 상당하다.

고로 이러한 문제를 피하기 위해 상속용이 아닌 클래스는 상속을 금지시켜버리는 편이 더 낫다.

코틀린의 모든 기본 클래스는 final 클래스이다.

이는 기존 자바의 클래스 상속을 피하기 위함으로 보인다.

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

item 20

자바가 제공하는 다중 구현 메커니즘은 인터페이스와 추상 클래스. 두 가지이다.

자바 8부터는 인터페이스가 디폴트 메서드를 제공하면서 두 메커니즘 모두 인스턴스 메서드를 구현 형태로 제공할 수 있게 되었다.

자바는 단일 상속만 지원하다보니 추상 클래스는 반드시 상속을 해야한다는 점에서 약점이 된다.

인터페이스는 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해줄 수 있다.

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

가령 Signer라는 인터페이스와, Songwriter라는 인터페이스가 있으면, 이 두 인터페이스를 상속받는 인터페이스는 싱어송 라이터가 될 것이다.

이처럼 인터페이스를 조합하여 유연한 설계를 가져갈 수 있는 것이 인터페이스의 큰 장점이다.

한편, 인터페이스와 추상 골격 구현 클래스를 함께 제공하는 식으로 인터페이스와 추상 클래스의 장점을 모두 취하는 방법도 존재한다.

인터페이스로 타입을 정의하고, 필요하면 디폴트 메서드도 지원해준다.

이렇게 골격만 잡아두고 확장을 자유롭게 열어둔다. 이것이 디자인패턴 중 하나인 바로 템플릿 메서드 패턴이다.

관례상 인터페이스 이름이 Harry라면, 골격 구현 클래스의 이름은 AbstractHarry로 짓는 것이 관례라고 한다.

대표적으로 컬렉션 프레임워크의 AbstractCollection, AbstractList 등이 있다.

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

item 21

자바 8에서는 핵심 컬렉션 인터페이스들에 다수의 디폴트 메서드가 추가되었는데 이는 모두 람다를 활용하기 위해서이다.

디폴트 메서드는 기존 구현체에 런타임 오류를 일으킬 수 있다.

때문에 기존 인터페이스에 새로운 디폴트 메서드를 추가하는 일은 가급적 피하는 편이 더 좋다.

디폴트 메서드를 통해 설계가 더욱 편리해졌지만, 여전히 세심한 주의를 기울여야한다.

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

item 22

클래스가 어떤 인터페이스를 구현한다는 것은 자신의 인스턴스로 무엇을 할 수 있는지를 클라이언트에게 얘기해주는 용도이다.

인터페이스는 오직 이 용도로만 사용해야 한다.

이 지침에 맞지 않는 예로는 상수 인터페이스라는 것이 있는데, 상수 인터페이스란 메서드 없이, 상수를 뜻하는 static final 필드로만 가득 찬 인터페이스를 말한다.

상수 인터페이스 안티패턴은 인터페이스를 잘못 사용한 예이다.

인터페이스는 타입을 정의하는 용도로만 사용하자. 상수 공개용 수단으로 사용해서는 안된다!

태그 달린 클래스보다는 클래스 계층구조를 활용하자#

item 23

태그 달린 클래스란 클래스 내부에서 enum을 사용하여 분류를 하는 클래스를 의미한다.

이 태그 클래스는 클래스 하나 안에 여러 구현이 혼합되어있어 가독성을 해치게 된다.

한 마디로, 태그 달린 클래스는 장황하며 오류를 내기 쉽고 비효율적이니 쓰지 말자.

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

item 24

중첩 클래스는 다른 클래스 안에 정의된 클래스를 의미한다.

정적 멤버 클래스와 비정적 멤버 클래스의 구문상 차이는 static 유무이지만 의미상 차이는 꽤 크다.

비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결된다.

멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여 정적 멤버 클래스로 만들어주자.

중첩 클래스에는 총 네 가지가 존재한다.

메서드 밖에서도 사용해야하고 메서드 안에 정의하기에는 너무 길다면 멤버 클래스로 만든다.

멤버 클래스의 인스턴스 각각이 바깥 인스턴스를 참조한다면 비정적을,

참조할 필요가 없다면 정적으로 만들자.

중첩 클래스가 한 메서드 안에서만 쓰이면서 그 인스턴스를 생성하는 지점이 단 한곳이고 해당 타입으로 쓰기에 적합한 클래스나 인터페이스가 이미 있다면 익명 클래스로 만들고,

그렇지 않으면 지역 클래스로 만들자.

톱 레벨 클래스는 한 파일에 하나만 담으라#

item 25

소스 파일 하나에 톱 레벨 클래스를 여러 개 선언해도 자바 컴파일러는 불평하지 않는다.

하지만 딱히 이득이 없기 때문에 가급적이면 톱레벨 클래스는 한 파일에 하나만 넣는 편이 좋다.

(코틀린은 .Kt 파일을 지원하면서 이러한 제약을 없앴다.)

소스 파일 하나에는 반드시 하나의 톱레벨 클래스(또는 톱 레벨 인터페이스)를 담는 것이 자바의 관례이다.

Reference#

스크린샷 2021-04-16 오후 4 24 23

이펙티브 자바 Effective Java 3/E

저자 : 조슈아 블로크

Last updated on