머리말
객체지향 프로그래밍에서의 가장 중요한 클래스와 인터페이스 Java에서는 클래스와 인터페이스를 어떤식으로 사용 권장 하는지 알아보는 시간을 갖도록 하겠습니다.
클래스와 맴버의 접근 권한을 최소화하라.
소프트웨어가 정상 동작하는 이상 항상 가장 낮은 접근 수준을 부여하라.
구현과 API를 깔끔하게 분리하자 오직 API를 통해서만 다른 컴포넌트와 소통하며, 내부 동작 방식에는 절때 개의치 말아야 한다. 정보은닉, 캡슐화는 소프트웨어 설계에 되는 원리이다.
클래스는 private-package와 public이 있는데 API를 제공해야하는 객체라면 public, 패키지 내에서 사용되는 클래스라면 private-package를 사용하라 API를 공개하는순간 내부 구현이 아니므로 관리의 대상이 된다.
private : 맴버를 선언한 톱 클래스에서만 접근할 수 있다.
package-private : 맴버가 소속된 패키지 안의 모든 클래스에서 접근 할 수 있다.
protected : package-private를 포함하며, 이 맴버를 선언한 클래스의 하위 클래스에서도 접근 할 수 있다.
public : 모든곳에서 접근가능하다.
- 맴버 변수는 가능하면 외부로부터 접근 불가능하고 불변으로 만들어라. 만약에 외부에 노출되어야 하는 상황이라면 반드시 불변 객체로 외부에 노출 시켜야한다.
- 단지 코드를 테스트할 목적으로 private맴버를 public으로 바꾸지말자, 같은 패지키 내에서 공유할 수 있는 package-private까지 풀어주는 것은 허용 할 수 있다.
- public 클래스의 인스턴스는 되도록 public이 아니어야한다. ( 일반적으로 스레드에 안전하지 못하게 된다 )
- public static final 배열필드를 두거나 필드를 반환하는 접근자 메서드를 제공해서는 안된다.
모듈은 패키지들의 묶음인데 자바 9에서부터 모듈 시스템 개념이 도입되었다. 관례상 module-info.java 파일에 패지지를 공개 할지 말지 선언한다.
핵심정리
<aside> ✏️ 프로그램 요소 접근성은 가능한 최소한으로 하라. 꼭 필요한 것만 골라 최소한의 public API를 설계하자. 그 외에는 클래스, 인터페이스, 맴버가 의도치 않게 API로 공개 되는 일이 없도록 해야한다. public 클래스는 상수용 public static final 필드 외에는 어떠한 public 필드도 가져서는 안된다. public static final 필드가 불변인지도 확인하라.
</aside>
public 클래스에서는 public 필드가 아닌 접근자 메서드를 활용하라
public 클래스는 절때 가변 필드를 직접 노출해서는 안 된다. 불편 필드라면 노출해도 덜 위험하지만 완전히 안심할 수 없다. 하지만 package-private 클래스나 private 클래스는 중첩 클래스에서는 종종 필드를 노출하는 편이 나을때도 있다.
변경 가능성을 최소화하라
- 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.
- 클래스를 확장 할 수 없도록 한다.
- 모든 필드를 final로 선언한다.
- 모든 필드를 private로 선언한다.
- 자신 외에는 내부의 가변 컴포넌트에 접근 할 수 없게 한다.
자신의 인스턴스를 수정하지않고 새로운 인스턴스를 만들어 반환하는 것처럼 피연산자 자체는 그대로인 프로그래밍 패턴을 함수형 프로그래밍이라고 한다.
불변 객체는 근본적으로 스레드에 안전하여 따로 동기화 할 필요가 없다. 불변 객체는 자유롭게 공유할 수 있음은 물론, 불변 객체끼리는 내부 데이터를 공유할 수 있다.
불변클래스의 단점이라고 한다면 많은 필드를 갖고 있다면 이것을 초기화하는 리소스가 많이 투입된다면 시공간적 비용을 치뤄야한다. 만들고자하는 객체가 많은 중간단계있는 객체들을 생성하며 만든다면 중간에 있던 객체는 버려지게되고 성능이 더 불거진다.
클래스를 불변으로 만드는 방법은 final 키워드를 사용하거나, 생성자를 private로 바꾸고 정적팩토리 메서드로 생성자를 제공하는법이다.
상속보다는 컴포지션을 사용하라
상속을 사용하게 될경우 하위 클래스는 상위 클래스에 종속 되어 유연성이 떨어지게된다. 또한 상위 클래스의 변경에 하위클래스는 원하지 않는 동작을 할 수 있게되고, 오버라이딩을 제공하는 클래스를 상위 클래스 내부에서 호출할경우 하위 클래스에서는 원하지 않는 동작을 일으키게 된다. 또한 하위 클래스는 여러개의 상위 클래스를 갖을 수 없기때문에 하나의 타입에 한정이 된다.
- 전달메서드(forwarding method) 방식을 활용하여 상속보다는 컴포지션을 활용하여 문제를 해결하라.
- 상속을 사용할 경우 하위 클래스기 is-a 일 경우에만 상속을 한다. 그 외에는 상속을 권장하지 않는다.
상속을 고려해 설계하고 문서화하라. 그렇지 않다면 상속을 금지하라
상속을 사용하기위해서는 하위 메서드가 상위 메서드를 호출할때, 해당 메서드가 “어떻게” 동작하는지 파악해야 한다. 그러나 좋은 API를 문서를 작성하는것은 **“어떻게”보다 “무엇”**을 표현하는것이 더 바람직하다.
상속 메서드들이 어떻게 동작하는지 모두 문서화하고, 그렇게 하지 않는다면 하위 클래스에서 해당 메서드를 사용할경우 어떻게 동작 될지 모르니 프로그램 오류가 발생한다.
- 클래스의 내부동작에 끼어 들 수 있는 혹(hook)을 잘 선별하여 protected 메서드 형태로 공개해야 할 수 도 있다.
- 상속용 클래스에서 생성자에서 절대로 재정의 가능 메서드를 호출해서는 안된다.
- 상속용 클래스를 만들게 될경우 반드시 하위클래스를 3개 이상만들어보고 테스트하라.
- clone과 readObject 모두 직접적으로든 간접적으로는 재정의 가능 메서드를 호출 해서는 안된다.
- 물론 toString, hashCode, equals도 모두 해당하는 내용이다.
- 상속을 금지시키기 위해서는 클래스의 final 키워드를 활용하거나 생성자를 private로 두고 정적팩토리메서드 패턴으로 해결하자.
추상 클래스보다는 인터페이스를 우선하자
방금전에서 알 수 있듯이, 추상클래스를 만들고 상속하는 과정은 매우 복잡하고 힘들다. 따라서 추상 클래스보다는 가능한 인터페이스를 우선시 사용하자.
- 인터페이스는 믹스인(mixin) 정의에 안성맞춤이다.
- 인터페이스는 계층구조가 없는 타입 프레임워크를 잘 만들 수 있다.
- 자바에서 제공하는 인터페이스의 default키워드를 잘 활용하자.
- default키워드를 이용할 수 없는경우에는 골격구현클래스(AbstractInterface) 클래스를 만들어 제공하자.
- 인터페이스는 맴버 필드를 가질 수 없으므로 default 키워드를 가지고 확장하기 어려울 때가 있다. 이럴때 에는 골격구현 클래스를 활용하여 문제를 해결하자
- 가능한 한 디폴드 메서드를 제공하고 그 상황이 불가피한 경우만 골격구현클래스를 이용하자
인터페이스를 구현하는 쪽을 생각해 설계하라
생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴드 메서드를 작성하는방법은 어렵다. 인터페이스를 설계할때에는 세심한 주의를 기울여야한다. 특히 디폴드 메서드를 구현할때에 더욱.
인터페이스는 타입을 정의하는 용도로만 사용하라
static final double AVOGADROS_NUMBER = 6.022 … 와 같이 static final 필드로 가득찬 인터페이스를 사용하지 말아라. 상수 인터페이스 안티패턴은 인터페이스를 잘못 사용한 예이다. 클래스를 활용하여 만들자.
태그 달린 클래스보다 클래스의 계층구조을 활용하라
태그 달린 클래스는 복잡하고 오류를 내기 쉽고, 비효율적이다. 태그 달린 클래스는 클래스의 계층구조를 어설프게 흉내낸 아류일 뿐이다. 따라서 계층구조를 적극 활용하여 사용하고, 기존클래스가 태그달린 클래스가 있다면 계층구조 클래스로 리팩터링 하자.
맴버 클래스는 되도록 static 으로 만들라
정적 중첩클래스와 비정적 중첩클래스의 큰 차이는 정적 중첩클래스는 바깥 클래스의 인스턴스를 참조 못하지만, 비정적 클래스는 바깥클래스의 인스턴스를 참조 할 수 있다. 따라서 참조하지 않는경우라면 맴버 클래스에서 바깥 인스턴스에 접근 할 일이 없다면 무조건 static을 붙혀서 정적 맴버 클래스로 만들자**( 단일책임 원칙 )**
- 비정적 중첩 클래스 ( inner class ) : 바깥 클래스의 인스턴스 참조 가능, 가능한 package-private나 private로 선언하자.
- 정적 중첩 클래스 ( static inner class ) : 바깥 클래스의 인스턴스 참조 불가능, 가능한 pakcage-private나 private로 선언하자.
- 익명 클래스 : 메소드 내에서 사용하는 클래스로 인터페이스나 추상메서드가 존재할때 사용하자.
- 지역 클래스 : 메소드 내에 사용하는 클래스로 인터페이스가 추상메서드가 없을때 사용하자.
톱레벨 클래스는 한 파일에 하나만 담으라
톱 레벨 클래스를 여러개를 구현할 경우, 컴파일 시점에 다른 파일과 충돌이 일어날 가능성이 있으므로 가능한 한 파일에 담아라
회고
실무를 볼때에 네이티브 코드를 C#에게 API 형식으로 제공해주는 업무를 하고 있는데 위의 해당하는 내용들이 잘 지켜지지 않은것 같아서 다음에 API를 설계할때에는 위의 내용을 잘지켜서 깔끔한 코드를 만들도록 노력해야겠습니다. 물론 C++와 Java의 언어적 특성이 달라서 위의 내용을 적용하는것에 한계가 있지만, 필자가 이야기한 의도를 생각하며 적용할 것입니다. 클래스와 인터페이스 4장을 읽으면서 제 생각을 정리하자면 아래와 같습니다.
- 클래스 접근자 : 가능한 한 private 또는 package-private를 사용해야한다**. public을 사용하는 순간 관리 대상이 됨**으로 변경이 어려워진다.
- 맴버 접근자 : 클래스의 맴버는 가능한 한 private으로 사용한다. public으로 사용할경우 클라이언트가 해당 맴버를 접근 할 수 있고, 내부구현을 바꿀 수 없으므로 유연성이 떨어지는것은 물론이고, 의도치않은 동작을 할 수 있다. 또한 final를 사용하여 불변으로 상태로 관리하자. 불변인 객체는 보통 스레드에 안전하며 불변이 아닐경우 클라이언트가 의도치 않은 결과값을 얻을 수 있다.
- 인터페이스 vs 추상클래스 : 인터페이스를 더 우선 사용하고, 어쩔 수 없이 추상클래스를 사용해야할 경우 재정의가능한(오버라이딩) 메서드들에 한에서는 모두 “어떻게” 동작하는지에 대한 API 문서화를 한다.
- 상속 vs 컴포지션 : 상속보다는 컴포지션을 활용한다. 위에서 이야기한 내용과 중첩되는 이야기로 상속을 사용하게된다면 추상클래스를 이용하는것이고, 컴포지션을 활용한다면 인터페이스를 이용하는것이기때문에 위의 내용과 동일하다고 생각합니다.
- 비정적 중첩클래스 vs 정적 중첩클래스 : 바깥 클래스의 인스턴스를 참조하지않는다면 비 정적클래스를 가능한 한 사용하고, 그렇지 않다면 정적클래스를 사용하라. 정적으로 생성 할경우 자동으로 단일 책임 원칙이 지켜진다.
필자의 의도 :
- 클래스의 내부 구현은 최대한 감추고, 공개 API를 최소화 한다.
- 클래스 간의 의존성을 분리해야한다. 상속을 사용할경우 상위 클래스에 의존적이다.
'이팩티브 자바' 카테고리의 다른 글
열거 타입과 에너테이션 (0) | 2024.03.17 |
---|---|
제네릭 (0) | 2024.03.17 |
모든 객체의 공통 메서드 (0) | 2024.03.17 |
객체 생성과 파괴 (0) | 2024.03.10 |