티스토리 뷰

JAVA

기타

세댕댕이 2022. 7. 24. 16:14

* 이펙티브 자바 2/E를 읽고 공부하기 위해 기록한 게시글입니다.

시간 관계상 모든 규칙들을 정독하기에는 곤란한 점이 많아 중요도가 좀 떨어져 보이고 하는것들은 빨리빨리 넘기기로 함.

 

17. 계승을 위한 설계와 문서를 갖추거나, 그럴 수 없다면 계승을 금지하라

계승을 위한 설계와 문서를 갖춘다는 것은 무슨 뜻일까?

- 메서드를 재정의할 때 무슨 일이 생기는지를 정확하게 문서화 해야한다. 재정의 가능 메서드를 내부적으로 어떻게 사용하는지를 반드시 남기라는 뜻.

-> public이나 protecterd로 선언된 모든 메서드와 생성자에 대해, 어떤 재정의 가능 메서드를 어떤 순서로 호출하는지, 호출 결과가 어떤 영향을 미치는지 문서로 남기라는 것. 

(재정의 가능하다는 것은 public 또는 protected로 선언된 비-final 메서드임을 뜻함)

 

 

 "좋은 API 문서는 메서드가 하는 일이 무엇인지를 명시해야지 그 일을 어떻게 하는지 명시해서는 안된다" 라는 원칙을 위반할 수도 있다. 이는 계승이 캡슐화 원칙을 침해하기 때문에 어쩔 수 없이 발생하는 결과일 뿐이다. 안전한 하위 클래스를 만들기 위해서는 문서에 구현 세부사항을 기술해야한다. 

 

* 계승을 위해 설계한 클래스를 테스트할 수 있는 유일한 방법은 하위 클래스를 직접 만들어 보는 것 뿐이다.

* 계승을 위한 클래스를 설계하는 것은 클래스에 상당한 제약을 가하게 된다. 

* 계승을 허용하기 위해서 생성자는 직접적이건 간접적이건 재정의 가능 메서드를 호출해서는 안된다.

- 상위 클래스의 생성자는 하위 클래스의 생성자보다 먼저 실행되기 때문에 하위 클래스에서 재정의한 메서드는 하위 클래스 생성자가 실행되기 전에 호출되게 된다. 재정의한 메서드가 하위 클래스 생성자가 초기화 해주는 결과에 의존할 경우 그 메서드는 의도와는 다르게 실행되게 된다.

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

public class Sub extends Super {
    private final Date date;
    public Sub() { this.date = new Date();}
    
    @Override
    public void overrideMe() { System.out.println(date);}
}

public class EffectiveJava {
    public static void main(String[] args) throws IOException {
        Sub sub = new Sub();
        sub.overrideMe();
    }
}

결과

new Sub()으로 객체를 생성할 때 Super의 생성자가 먼저 호출된다. (super 생성자를 생략하면 자동으로 기본 생성자를 호출)

-> super의 생성자에서는 재정의 가능 메서드인 overrideMe 메서드 호출. 하위 클래스에서 재정의된 메서드가 호출된다.

-> 하위 클래스의 생성자가 호출되기 전에 메서드가 먼저 호출되었기 때문에 date 변수가 초기화 되지 않았음. 그렇기에 null을 출력하게 된다.

 

 

(결론)

계승에 맞도록 설계하고 문서화하지 않은 클래스에 대한 하위 클래스를 만들지 마라.

* 표준 인터페이스를 구현하고 있지 않은 클래스들은 데코레이터 패턴같은 대안이 없기 때문에 계승(상속)을 사용해야 할 수도 있다. 그 경우에는 클래스 내부적으로 재정의 가능 메서드를 사용하는 경우를 완전히 제거하면 안전하게 사용할 수 있다. 그렇게 하면 메서드를 재정의 해도 다른 메서드에는 영향이 없을 것이기 때문.

= 상위 클래스는 다른 public 내부 메서드를 호출하도록 만들지 말고 독립적으로 만들어라

 

 

19. 인터페이스는 자료형을 정의할 때만 사용하라

인터페이스를 구현하는 클래스를 만들게 되면. 그 인터페이스는 해당 클래스의 객체를 참조할 수 있는 자료형 역할을 하게 된다. 

- 인터페이스를 구현해 클래스를 만든다는 것은 해당 클래스의 객체로 어떤 일을 할 수 있는지를 클라이언트에게 알리는 것이다. 

-> 이외의 목적으로 인터페이스를 정의하고 사용하는 것은 적절치 못하다.

 

대표적으로 이상한 인터페이스 안티패턴으로 "상수 인터페이스"가 있다. 인터페이스에 메서드도 없이 상수값만 넣어놓고 사용하는 것. 실제로 있다.

public interface ObjectStreamConstants {
    static final short STREAM_MAGIC = (short)0xaced;
    static final short STREAM_VERSION = 5;
    static final byte TC_BASE = 0x70;
    ...
}

정말 인터페이스를 이상하게 사용하고 있는 예시. 절대 따라하면 안된다.

클래스가 어떤 상수를 어떻게 사용하는지에 대한 것은 구현 세부사항에 속한다. 상수 정의를 인터페이스에 포함시키면 구현 세부사항이 클래스의 공개 API에 스며들게 된다. 

 

그럼 어떻게 해야 할까요?

1. 해당 상수가 이미 존재하는 클래스나 인터페이스에 강하게 연결되어 있을 때는 그 상수들을 해당 클래스나 인터페이스에 넣어라. Enum 타입을 사용하는 것도 좋다.

2. 객체 생성이 불가능한 유틸리티 클래스를 사용해라

public class PysicalConstants {
    private PysicalConstants() {} // 생성 불가

    public static final double AVOGADROS_NUMBER = 6.1231432423e23;
    public static final double BOLTZMANN_CONSTANT = 1.2342352e-13;
    ...
}
///
import static springbook.effectivejava.PhysicalConstants.BOLTZMANN_CONSTANT;

public class EffectiveJava {
    public static void main(String[] args) throws IOException {
        double val = BOLTZMANN_CONSTANT;
        double val2 = PhysicalConstants.AVOGADROS_NUMBER;
    }
}

유틸리티 클래스에 포함된 상수를 이용할 일이 많다면 static import 기능을 이용해 클래스 이름을 제거할 수 있다.

 

* static import 기능이 도입되게 된 중요한 계기가 바로 상수 인터페이스(constant interface)를 사용하던 일부 개발자들 때문이라 카더라..

 

(결론) 인터페이스는 목적에 맞게 사용해라

 

 

20. 태그 달린 클래스 대신 클래스 계층을 활용하라

태그 달린 클래스란 그냥 여러 기능을 한 클래스 안에 다 때려넣고 조건문을 이용해서 A일땐 이거 B일땐 저거 식으로 동작하게 만든 클래스를 의미하는 것 같다. 딱봐도 별로다. 

abstract class Figure {
    abstract double area();
}

class Circle extends Figure {
    final double radius;

    Circle(double radius){
        this.radius = radius;
    }

    double area(){
        return Math.PI * (radius * radius);
    }
}

class Ractangle extends Figure {
    final double length;
    final double width;

    Ractangle(double length, double width){
        this.length = length;
        this.width = width;
    }

    double area(){
        return length * width;
    }
}

클래스 계층을 이용해 깔끔하게 분리하는 것이 좋다.

- 태그 값(조건)에 따라 달리 동작하는 메서드들을 "추상 메서드"로 선언하는 추상 클래스를 정의한 다음 그 추상 클래스를 클래스 계층 맨 꼭대기에 둔다. 그리고 태그 값에 좌우되지 않는 메서드, 모든 기능에 공통되는 데이터 필드는 전부 이 클래스(추상 클래스)에 넣는다.

- 그 다음으로 이 추상 클래스를 구현하는 하위 클래스를 만들면 된다.

-> 단순하고 명료하며, 각각의 기능이 별도의 클래스로 구현된다. 각 클래스 안에 관련없는 필드가 존재하지 않아 응집도가 좋다. 또한 클래스 계층의 장점은 타입 체크를 하기 용이하다는 것. 

class Square extends Rectangle {
    Square(double side) {
        super(side, side);
    }
}

-> Square 클래스는 딱 봐도 Rectangle 클래스와 상속관계에 있구나 하는 것이 한눈에 들어옴.

 

(결론) 클래스 계층과 상속을 이용해 응집도 있는 개발을 하자

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함