관리 메뉴

생각해보기

이펙티브 자바 -3- 본문

자바

이펙티브 자바 -3-

정한_s 2022. 1. 3. 22:10

클래스와 인터페이스

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

잘 설계된 컴포넌트 내부 데이터와 내부 구현 정보를 외부 컴포넌트로부터 잘 숨긴 것이다. 구현과 API를 분리해서 오직 API를 통해서만 다른 컴포넌트와 소통한다. 이는 정보은닉, 캡슐화라고 하며 캡슐화를 통해 각 컴포넌트들은 내부 구현을 신경 쓰지 않는다. 

 

캡슐화의 기본 원칙은 간단하다. 모든 클래스와 멤버의 접근성을 가능한 좁혀야 한다. 패키지 외부에서 쓸 이유가 없다면 접근자를 private으로 선언한다. 그러면 이들은 API가 아닌 내부 구현이 되어 언제든지 수정할 수 있다.

 

 public 클래스의 인스턴스 필드는 되도록 public이 아니어야 한다. public으로 가변 객체를 선언하게 되면, 그 필드와 관련된 모든 것은 불변식을 보장할 수 없게 된다 또한 public 가변 필드를 갖는 클래스는 일반적으로 스레드 안전하지 않다

 

길이가 0이 아닌 배열은 모두 변경이 가능하다. 따라서 클래스에서 public static final 배열 필드를 두거나 이 필드를 반환하는 접근자 메더르를 제공해서는 안된다

// 보안 허점이 숨어 있다
public static final Thing[] VALUES = { ... };

// 해결방법 1 : 배열을 private으로 만들고 public 불변리스트 추가
private static final Thing[] VALUES = { ... };
public static final List<Thing> VALUES_COPY = Collections.unmodifiableList(Arrays.asList(VALUES));

// 해결방법 2 : 배열을 private으로 만들고 복사본을 반환하는 public 메서드 추가
private static final Thing[] VALUES = { ... };
public static final Thing[] values(){
	return PRIVATE_VALUES.clone();
}

 

정리

프로그램 요소의 접근성은 가능한 최소한으로 하라. 꼭 필요한 것만 최소한 public API로 설계하자. public 클래스는 public static final 필드 외에는 어떠한 public 필드도 가져서는 안된다. public static final 필드가 참조하는 객체가 불변인지 확인하라.

 

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

간단한 캡슐화 예시(getter, setter)

// 안좋은 클래스 설계 
// 데이터 필드에 직접 접근할 수 있다
class Point{
	public double x;
    public double y;
}

// 더 나은 설계
class Point{
    private double x;
    private double y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() {
        return x;
    }

    public void setX(double x) {
        this.x = x;
    }

    public double getY() {
        return y;
    }

    public void setY(double y) {
        this.y = y;
    }
}

 

 

패키지 바깥에서 접근할 수 있는 클래스라면 접근자를 제공함으로써 클래스 내부 표현 방식을 언제든 바꿀 수 있는 유연성을 얻을 수 있다. 필드를 공개하면 이를 사용하는 클라이언트가 생겨날 것이므로 내부 표현 방식을 마음대로 바꿀 수 없게 된다.

 

정리

public 클래스는 절대 가변 필드를 직접 노출해서는 안 된다.

 

변경 가능성을 최소화 하라

불변 클래스란 간단히 말해 그 인스턴의 내부 값을 수정할 수 없는 클래스다. 불변 클래스는 가변 클래스보다 설계하고 구현하고 사용하기 쉬우며, 오류가 생길 여지도 적고 훨씬 안전하다.

 

클래스를 불변으로 만들기 위한 다섯가지 규칙

  • 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다
  • 클래스를 확장할 없도록 한다 - 하위 클래스에서 부주의 하게 객체의 상태를 변경하는 것을 막아준다. 간단한 예로 클래스에 final로 선언하는 방법이 있다
  • 모든 필드를 final로 선언한다 
  • 모든 필드를 private으로 선언한다 - 필드가 참조하는 가변 객체를 클라이언트가 직접 접근해 수정하는 일을 막아준다.
  • 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다 - 클래스에 가변 객체를 참조하는 필드가 하나라도 있다면 클라이언트에서 그 객체의 참조를 얻을 수 없도록 해야 한다. 생성자, 접근자, readObject 메서드 모두에서 방어적 복사를 수행하라.

불변 객체의 장점

  • 불변 객체는 단순하다 - 불변 객체는 생성되는 시점의 상태를 파괴될 때 까지 그대로 간직한다. 따라서 복잡한 상태를 가지지 않는다
  • 불변 객체는 근본적으로 스레드 안전하여 따로 동기화할 필요가 없다 - 불변 객체는 상태가 바뀌지 않으므로 동시에 사용해도 절대 훼손되지 않는다. 따라서 불변 객체는 안심하고 공유할 수 있다 
  • 불변 객체는 자유롭게 공유할 수 있음, 불변 객체끼리는 내부 데이터를 공유할 수 있다. 즉, 배열과 같은 가변 객체도 불변 객체 끼리는 원본 인스턴스와 공유해도 된다.
  • 객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많다 - 값이 바뀌지 않는 구성요소들로 이루어진 객체라면 구조가 아무리 복잡해도 불변식을 유지하기 때문에 수월하다
  • 불변 객체는 그 자체로 실패 원자성을 제공한다 - 상태가 절대 변하지 않으니 잠깐이라도 불일치 상태에 빠질일 이 없다( 실패 원자성 : 메서드에서 예외가 발생한 후에도 그 객체는 예외전과 같아야 한다)

불변 클래스에도 단점이 있다. 값이 다르면 반드시 독립된 객체로 만들어야한다. 값의 가짓수가 많다면 이들을 모두 만드는데 큰 비용을 치러야 한다.

 

불변 클래스를 만드는 방법에서 클래스에 final을 붙이는 것 외에도, 모든 생성자를 private으로 만들고 public 정적 팩터를 제공하는 방법이 있다. 정적 팩토리 방식은 다수의 구현 클래스를 활용한 유연성을 제공한다.

 

정리

게터가 있다고 해서 무조건 세터를 만들지 말자. 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다. 만약 불변으로 만들수 없는 클래스라면 변경할 수 있는 부분을 최소한으로 줄여라. 다른 합당한 이유가 없다면 모든 필드는 private final이어야 한다. 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.

 

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

상속은 코드를 재사용하는 강력한 수단이지만, 최선은 아니다. 상속(클래스가 다른 클래스를 확장하는 상속)에서 메서드 호출과 달리 상속은 캡슐화를 깨트린다. 다르게 말하면, 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있다. 

 

이러한 문제를 피해가는 방법은 기존의 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하자. 기존 클래스가 새로운 클래스의 구성요소로 쓰이는 설계를 컴포지션이라 한다.

 

새로운 클래스는 기존 클래스의 내부 구현 방식의 영향에서 벗어나며, 심지어 기존 클래스에 새로운 메서드가 추가되어도 전혀 영향받지 않는다

 

새로운 클래스는 기존의 인스턴스를 감싸고 있으므로 래퍼 클래스라 하며 기존 클래스에 기능을 덧씌운다는 뜻으로 데코레이션 패턴이라고 한다. 컴포지션과 전달의 조합은 넓은 의미로 위임이라고 부른다. 단, 래퍼 객체가 내부 객체에 자시 자신의 참조를 넘기는 경우만 위임에 해당 된다.

 

상속은 반드시 하위 클래스가 상위 클래스의 '진짜' 하위 타입인 상황에서만 쓰여야 한다. 다르게 말하면 클래스 B가 클래스 A와 is - a 관계(B는 A인가) 일 때만 A를 상속해야 한다

 

정리

상속은 강력하지만 캡슐화를 해친다는 문제가 있다. 상속은 상위 클래스와 하위 클래스가 순수한 is-a 관계일 때만 써야한다. 상속의 취약점을 피하려면 상속 대신 컴포지션과 전달을 사용하는 것이 좋다.

 

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

상속은 캡슐화를 해친다. 따라서 상속용 클래스는 재정의 할 수 있는 메서드들을 어떻게 이용하는지 문서로 남겨야 한다. 

상속에서 접근제한자를 protected 이상으로 놓을 경우 앞으로 이 필드와 메서드에 대해 책임져야 한다. 따라서 상속용으로 설계한 클래스는 배포 전에 하위 클래스로 만들어 검증하자. 

 

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

class Super{
    public Super(){
        overrideMe();
    }
    public void overrideMe(){

    }
}
final class Sub extends Super{
    private final String instant;

    public Sub(String instant) {
        this.instant = instant;
    }

    @Override
    public void overrideMe() {
        System.out.println(instant);
    }
}

 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자 보다 먼저 호출된다. 따라서 의도대로 동작 하지 않는다.

 

clone과 readObject 모두 직접적으로 간접적으로든 재정의 가능 메서드를 호출해서는 안된다. 어느 쪽이든 프로그램 오작동으로 이어 질 수 있다.

 

클래스를 상속용으로 설계하려면 상당한 노력이 필요하다, 따라서 상속용 으로 설계하지 않는 클래스는 상속을 금지하라.

 

정리

상속용 클래스를 설계하는 것은 노력이 필요하다. 클래스 내부에서 스스로 어떻게 사용하는지 문서로 남겨야 한다. 그렇지 않으면 하위 클래스가 오작동 할 수 있다. 또한 상속용이 아니라면 상속을 금지하는 것도 좋은 방법이다

 

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

자바는 단일 상속만 지원하니, 추상 클래스 방식은 커다란 제약을 안게 된다. 반면 인터페이스는 다중 상속이 가능하다. 따라서 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해 넣을 수 있다. 

 

인터페이스는 다중 상속이 가능하고 모두 구현을 해야 하므로 믹스인 정의에 안성맞춤이다. 또한 인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다. 

 

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

생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기 어렵다. 따라서 디폴트 메서드는 기존 구현체에 런타임 오류를 일으킬 수 있다. 우리는 인터페이스를 설계할 때 세심한 주의를 기울여야 한다.

 

정리

새로운 인터페이스라면 릴리즈 전에 반드시 테스트를 거쳐야한다

 

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

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

 

상수 인터페이스 안티패턴은 인터페이스를 잘못 사용한 예이다. 클래스 내부에서 사용하는 상수는 외부 인터페이스가 아니라 내부 구현에 해당한다.

 

정리

인터페이스는 타입을 정의하는 용도로만 사용해야 한다

 

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

두가지 이상의 의미를 표현할 수 있으며 여러 구현이 한 클래스의 혼합된 형태로, 현재 표현하는 의미를 주석으로 알려주는 클래스를 태그 달린 클래스라고 한다.

 

태그 달린 클래스는 여러개의 변경에 대한 책임을 가진다. 클래스는 한 변경에 책임만 가져야 한다. 태그 달린 클래스는 오류를 내기 쉽고 비효율 적이다

 

정리

태그 달린 클래스를 써야 하는 상황은 거의 없다. 클래스를 두가지 이상의 의미를 부여하지 말고 계층 구조로 만들어 하나의 책임을 가지도록 하자

 

멤버 클래스는 되도록 static으로 만들어야 한다

정적 멤버 클래스와 비정적 멤버 클래스의 구문상 차이는 단지 static이 붙어 있고 없고 차이지만, 의미상의 차이는 크다. 비정적 멤버 클래스는 바깥 인스턴스 없이는 생성할 수 없다. 따라서 바깥 인스턴스로의 숨은 외부 참조를 갖게 된다

class A {
    int a = 10;

    public void run() {
        System.out.println("Run A");
        B.run();
        C c = new C();
        c.run();
    }

    // 정적 멤버 클래스
    public static class B {
        public static void run() {
            System.out.println("Run B");
        }
    }

    // 비정적 멤버 클래스
    public class C {
        public void run() {
            // 정규화된 this를 통해 참조 가능하다.
            // 정규화된 this란 클래스명.this 형태로 이름을 명시하는 용법을 말한다.
            System.out.println("Run C: " + A.this.a);
        }
    }
}
public class Example {
    public static void main(String[] args) {
        // 정적 멤버 클래스는 이렇게 외부에서 접근 가능하다.
        A.B.run();
        A a = new A();
        a.run();
        A.C c = a.new C();
        c.run();
    }
}

멤버 클래스에서 바깥에 위치한 인스턴스에 접근할 필요가 있다면 무조건 static을 추가하여 정적 멤버 클래스로 만드는 것이 좋다. static을 생략하면 바깥 인스턴스로의 숨은 외부 참조를 갖게 되는데, 이 참조를 저장하려면 시간과 공간적인 리소스가 소비된다. 더 심각한 문제로 가비지 컬렉션이 바깥 클래스의 인스턴스를 정리하지 못할 수 있다.

 

정리

멤버 클래스의 인스턴스 각각이 바깥 인스턴스를 참조한다면 비정적으로, 그렇지 않다면 정적으로 만들어라.

 

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

소스 파일 하나에 톱레벨 클래스(소스 파일에 가장 상위 클래스)를 여러개 선언하는 경우 심각한 위험이 생길 수 있다. 이 경우 한 클래스를 여러 가지로정의 될 수 있으며 그 중 어느 것을 사용하지는 어느 소스파일을 먼저 컴파일하느냐에 따라 달라진다.

// 톱레벨 클래스가 2개
class Product{...}
class Dessert{...}

해결방법은 톱레벨 클래스들을 서로 다른 소스 파일로 분리하거나. 여러 톱레벨 클래스를 한 소스파일에 담고 싶다면 정적 멤버 클래스를 사용해라

 

정리

소스 파일 하나에는 반드시 톱레벨 클래스(혹은 톱레벨 인터페이스)를 하나만 담자

 

 

 

 

'자바' 카테고리의 다른 글

이펙티브 자바 -5-  (0) 2022.01.07
이펙티브 자바 -4-  (0) 2022.01.05
이펙티브 자바 -2-  (0) 2021.12.30
이펙티브 자바 -1-  (0) 2021.12.29
모던 자바 인 액션 -16-  (0) 2021.12.28
Comments