ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 4장 - 클래스와 인터페이스
    📕 book/이펙티브 자바 2022. 11. 1. 17:15

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


    어설프게 설계된 컴포넌트, 잘 설계된 컴포넌트의 가장 큰 차이점

    • 클래스 내부 데이터와 내부 구현 정보를 외부 컴포넌트로부터 얼마나 잘 숨겼느냐

    잘 설계된 컴포넌트는 모든 내부 구현을 완벽히 숨겨, 구현과 API를 깔끔히 분리한다.

    • 오직 API를 통해서만 다른 컴포넌트와 소통
    • 서로의 내부 동작 방식에는 전혀 개의치 않는다.

    정보 은닉, 캡슐화

    장점

    • 시스템 개발 속도를 높인다
    • 시스템 관리 비용을 낮춘다
    • 성능최적화에 도움을 준다.
    • 소프트웨어 재사용성을 높인다
    • 큰 시스템을 제작하는 난이도를 낮춘다.

    접근 제어 메커니즘

    자바는 정보 은닉을 위한 다양한 장치를 제공, 그중 하나가 접근 제어 메커니즘이다.

    • 각 요소의 접근성은 그 요소가 선언된 위치와 접근 제한자로 정해진다.

    기본 원칙

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

    접근 수준

    • private : 멤버를 선언한 톱 레벨 클래스에서만 접근 가능
    • package-private: 멤버가 소속된 패키지 안의 모든 클래스에서 접근할 수 있다.
      • 접근 제한자를 명시하지 않았을 때 적용되는 패키지 접근 수준
    • protected : package-private 접근 범위를 포함하여, 이 멤버를 선언한 클래스의 하위 클래스에서도 접근할 수 있다.
    • public : 모든 곳에서 접근할 수 있다.
    1. 클래스의 공개 API를 세심히 설계한 후, 그 외의 모든 멤버는 private로 만든다
    2. 오직 같은 패키지의 다른 클래스가 접근해야 하는 멤버에 한해 package-private으로 풀어준다
    3. 권한을 풀어주는 일을 자주하면 시스템에서 컴포넌트를 더 분해해야 하는 것은 아닌지 다시 고민

    리스코프 치환 원칙

    상위 클래스의 인스턴스는 하위 클래스의 인스턴스로 대체해 사용할 수 있어야 한다는 규칙

    이를 지키기 위해

    상위 클래스의 메서드를 재정의할 때는 그 접근 수준을 상위 클래스에서보다 좁게 설정할 수 없다.

    public 클래스의 인스턴스 필드는 되도록 public이 아니어야 한다.

    • 불변식을 보장할 수 없게 된다.

    public 가변 필드를 갖는 클래스는 일반적으로 스레드 안전하지 않다.

    • 내부 구현을 바꾸고 싶어도 그 public 필드를 없애는 방식으로는 리팩터링할 수 없게 된다.

    클래스에서 public static final 배열 필드를 두거나 이 필드를 반환하는 접근자 메서드를 제공해서는 안된다.

    • 길이가 0이 아닌 배열은 모두 변경이 가능..!

     

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


    철저한 객체 지향 프로그래머는 필드들을 모두 private으로 바꾸고 public 접근자(getter)를 추가한다.

    패키지 바깥에서 접근할 수 있는 클래스라면 접근자를 제공하여 클래스 내부 표현 방식을 언제든 바꿀 수 있는 유연성을 얻는다.

    정리

    💡 public 클래스는 절대 가변 필드를 직접 노출해서는 안 된다.
    불변 필드라면 노출해도 덜 위험하지만 완전히 안심할 수는 없다.
    하지만 package-private 클래스나 private 중첩 클래스에서는 종종 필드를 노출하는 편이 나을 때도 있다.

     

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


    불변 클래스

    인스턴스의 내부 값을 수정할 수 없는 클래스

    • 불변 인스턴스에 간직된 정보는 고정되어 객체가 파괴되는 순간까지 절대 달라지지 않는다.

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

    클래스를 불변으로 만들기 위해 따를 5가지 규칙

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

    불변 객체는 근본적으로 스레드 안전하다 → 따로 동기화할 필요 없다.

    • 불변 객체에 대해서는 그 어떤 스레드도 다른 스레드에 영향을 줄 수 없으니
    • 불변 객체는 안심하고 공유할 수 있다.

    불변 클래스는 자주 사용되는 인스턴스를 캐싱하여 같은 인스턴스를 중복 생성하지 않게 해주는 정적 팩터리를 제공할 수 있다.

    ⇒ 메모리 사용량과 가비지 컬렉션 비용이 줄어든다.

    불변 객체는 자유롭게 공유할 수 있다.

    → 불변 객체끼리는 내부 데이터를 공유할 수 있다.

    객체를 만들 때 다른 불변 객체를 구성요소로 사용하면 이점이 많다.

    ⇒ 불변 객체는 그 자체로 실패 원자성을 제공

    • 상태가 절대 변하지 않으니 잠깐이라도 불일치 상태에 빠질 가능성이 없다.

    실패 원자성

    • 메소드에서 예외가 발생한 후에도 그 객체는 여전히(메소드 호출전과 같은) 유효한 상태여야 한다는 성질

    단점

    • 값이 다르면 반드시 독립된 객체로 만들어야 한다.

    클래스가 불변임을 보장하려면 자신을 상속하지 못하게 해야 한다.

    • 가장 쉬운 방법 : final 클래스로 선언
    • 모든 생성자를 private 혹은 package-private로 만들고 public 정적 팩터리를 제공하는 방법

    불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자.

    • 다른 합당한 이유가 없다면 모든 필드는 private final 이어야 한다.
    • 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.

     

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


    메소드 호출과 달리 상속은 캡슐화를 깨뜨린다.

    • 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있다.

    이러한 이유로 상위 클래스 설계자가 확장을 충분히 고려하고 문서화도 제대로 해두지 않으면 하위 클래스는 상위 클래스의 변화에 발맞춰 수정되어야 한다.

     

    상속 문제 예시

    상속 관계에 놓은 두 클래스에서 메서드를 재정의 할 경우 문제 발생

    • 하위 클래스에서 새로운 메서드를 만들었는데 이후 상위 클래스에서 동일한 메서를 추가 → 문제

    상위 클래스가 새로운 메서드를 추가

    • 하위 클래스가 깨짐

    해결

    기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조

    컴포지션(compositon, 구성) 이라고 한다.

    새 클래스의 인스턴스 메서드들은 기존 클래스의 대응하는 메서드를 호출해 그 결과를 반환

    • 이 방식을 전달(forwarding)이라 하며, 새 클래스의 메서드들을 전달 메서드라고 한다.

    컴포지션과 전달의 조합은 넓은 의미로 위임이라고 부른다. 단, 엄밀히 따지면 래퍼 객체가 내부 객체에 자기 자신의 참조를 넘기는 경우만 위임에 해당한다.

    래퍼 클래스는 단점이 거의 없다.

    • 한 가지, 래퍼 클래스가 콜백 프레임워크와는 어울리지 않는다는 점만 주의하면 된다.

    상속은 반드시 하위 클래스가 상위 클래스의 진짜 하위 타입인 상황에서만 쓰여야 한다.

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


    메서드를 재정의하면 어떤 일이 일어나는지를 정확히 정리하여 문서로 남겨야 한다.

     

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

    API 문서의 메서드 설명 끝에서 종종 “implementation Requirements” 로 시작하는 절이 있다.

    • 그 메서드의 내부 동작 방식을 설명하는 곳.

    널리 사용할 클래스를 상속용으로 설계한다면 문서화한 내용 사용 패턴과, protected 메서드와 필드를 구현하면서 선택한 결정에 영원히 책임져야 함을 인식해야 한다.

    → 그러니 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다.

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

    • 프로그램이 오작동
    • 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되어 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출된다.

    상속을 금지하는 방법 두가지

    1. 둘 중 더 쉬운쪽은 클래스를 final로 선언
    2. 모든 생성자를 private이나 package-private으로 선언하고 public 정적 팩터리를 만드는 방법

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


    추상 클래스와 인터페이스

    차이

    • 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다.
    • 인터페이스가 선언한 메서드를 모두 정의하고 그 일반 규약을 지킨 클래스라면 다른 어떤 클래스를 상속했든 같은 타입으로 취급

    인터페이스 장점

    믹스인(mixin) 정의에 안성맞춤

    믹스인

    • 클래스가 구현할 수 있는 타입
    • 믹스인을 구현한 클래스에 원래의 ‘주된 타입’ 외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 준다.

    → 대상 타입의 주된 기능에 선택적 기능을 ‘혼합’

    계층 구조가 없는 타입 프레임워크를 만들 수 있다.

    래퍼클래스 관용구와 함께 사용하면 인터페이스는 기능을 향상시키는 안전하고 강력한 수단이 된다.

    인터페이스의 메서드 중 구현방법이 명확한 것이 있다면, 디폴트 메서드로 제공할 수 있다.

    디폴트 메서드제약

    • equals, hashCode 등은 디폴트 메서드로 제공해서는 안된다.
    • 인스턴스 필드를 가질 수 없고 public이 아닌 정적 멤버도 가질 수 없다(priave 정적 메서드 예외)
    • 직접 만들지 않은 인터페이스에는 디폴트 메서드를 추가할 수 없다.

    → 인터페이스와 추상 골격 구현(skeletal implementation) 클래스를 함께 제공하는 식으로 인터페이스와 추상 클래스의 장점을 모두 취하는 방법도 있다.

    템플릿 메서드 패턴

    • 인터페이스로는 타입을 정의하고 필요하면 디폴트 메서드 몇개도 함께 제공
    • 골격 구현 클래스는 나머지 메서드들까지 구현
      • 추상 클래스처럼 구현을 도와주는 동시에 타입을 정의할 때 따라오는 심각한 제약에서 자유롭다.

    시뮬레이트한 다중 상속

    • 인터페이스를 구현한 클래스에서 해당 골격 구현을 확장한 private 내부 클래스를 정의하고, 각 메서드 호출을 내부 클래스의 인스턴스에 전달

     

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


    생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하긴 어렵다.

    • 디폴트 메서드는 컴파일에 성공하더라도 기존 구현체에 런타임 오류를 일으킬 수 있다.

     

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


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

    상수 인터페이스 등 용도에 맞지 않게 사용하지 말자

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


    두 가지 이상의 의미를 표현할 수 있고, 그 중 현재 표현하는 의미를 태그 값으로 알려주는 클래스가 있다.

    태그 달린 클래스는 열거 타입 선언, 태그 필드, switch 문 등으로 장황하고, 오류를 내기 쉽고, 비효율적이다.

    태그달린 클래스를 클래스 계층구조로 바꾸는 방법

    • 계층 구조의 루트가 될 추상 클래스를 정의
    • 태그 값에 따라 동작이 달라지는 메서드들을 루트 클래스의 추상 메서드로 선언
    • 태그 값과 상관없이 동작이 일정한 메서드들을 루트 클래스에 일반 메서드로 추가
    • 모든 클래스에서 공통으로 사용하는 데이터 필드도 루트 클래스로 올린다
    • 루트 클래스를 확장한 구체 클래스를 의미별로 하나씩 정의한다.

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


    중첩 클래스 (nested class)

    다른 클래스안에 정의된 클래스를 말한다.

    • 자신을 감싼 바깥 클래스에서만 쓰여야 한다.
    • 그 외의 쓰임새가 있다면 톱레벨 클래스로 만들어야 한다.

    종류

    • 정적 멤버 클래스
    • (비정적) 멤버 클래스
    • 익명 클래스
    • 지역 클래스

    정적 멤버 클래스

    다른 클래스 안에 선언되고, 바깥 클래스의 private 멤버에도 접근할 수 있다는 점 외에 일반 클래스와 동일하다

    (비정적) 멤버 클래스

    • 바깥 클래스의 인스턴스와 암묵적으로 연결된다. 바깥 인스턴스의 메서드를 호출하거나 참조를 가져올 수 있다.
    • 개념상 중첩 클래스의 인스턴스가 바깥 인스턴스와 독립적으로 존재할 수 있다면 정적 멤버 클래스로 만들어야 한다.
    • 바깥 인스턴스로의 숨은 외부 참조를 갖게 되므로, 시간과 공간이 소비된다. 가비지 컬렉션이 바깥 클래스의 인스턴스를 수거하는 메모리 누수가 생길 수도 있다.

    ⇒ 어댑터를 정의할 때 자주 쓰인다.

    익명 클래스

    • 이름이 없다.
    • 바깥 클래스의 멤버도 아니다
    • 쓰이는 시점에 선언과 동시에 인스턴스가 만들어 진다.

    지역 클래스

    • 지역변수를 선언할 수 있는 곳이면 실질적으로 어디서든 선언할 수 있고 유효 범위도 지역변수와 같다

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


    컴파일러에 어느 소스 파일을 먼저 건네느냐에 따라 동작이 달라지므로 되도록 톱레벨 클래스들을 서로 다른 소스파일로 분리하자.

    ⇒ 굳이 같은 파일에 담고 싶다면 정적 멤버 클래스를 고려하자.

    참고 자료

    http://www.yes24.com/Product/Goods/65551284

    '📕 book > 이펙티브 자바' 카테고리의 다른 글

    7장 - 람다와 스트림  (0) 2022.11.07
    6장 - 열거타입과 애너테이션  (0) 2022.11.04
    5장 - 제네릭  (0) 2022.11.01
    3장 - 모든 객체의 공통 메서드  (0) 2022.10.26
    2장 - 객체 생성과 파괴  (0) 2022.10.25

    댓글

Designed by Tistory.