728x90

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

 

인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 한다.

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

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

 

이 지침에 맞지 않는 예로 소위 상수 인터페이스라는 것이 있다.

상수 인터페이스란 메서드 없이, 상수를 뜻하는 static final 필드로만 가득 찬 인터페이스를 말한다.

그리고 이 상수들을 사용하려는 클래스에서는 정규화된 이름(qualified name)을 쓰는 걸 피하고자 그 인터페이스를 구현한다.

 

public interface Physicalconstants {    
    // 아보가드로 수 (1/몰)    
    static final double AVOGADROS_NUMBERS = 6.022_140_857e23;
    // 볼츠만 상수 (J/K)    
    static final double BLOTZMANN_CONSTANT = 1.380_648_52e-23;    
    // 전자 질량 (kg)    
    static final double ELECTRON_MASS = 9.109_383_56e-31;
}

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

클래스 내부에서 사용하는 상수는 외부 인터페이스가 아니라 내부 구현에 해당된다.

따라서 상수 인터페이스를 구현하는 것은 이 내부 구현을 클래스의 API로 노출하는 행위다.

 

해결방법

 

특정 클래스나 인터페이스와 강하게 연관된 상수라면 그 클래스나 인터페이스 자체에 추가해야한다.

열거 타입으로 나타내기 적합한 상수라면 열거 타입으로 만들어 공개하고, 아니면 인스턴스화할 수 없는 유틸리티 클래스에 담아 공개하면 된다.

 

public class PhysicalConstants {
    
    private PhysicalConstants() { } // 인스턴스화 방지
    
    // 아보가드로 수 (1/몰)
    public static final double AVOGADROS_NUMBER = 6.022_140_857e23;

    // 볼츠만 상수 (J/K)
    public static final double boltzmann_constant = 1.380_648_52e-23;

    // 전자 질량 (kg)
    public static final double ELECTRON_MASS = 9.109_383_56e-31;
}
728x90
728x90

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

 

클래스, 인터페이스의 차이

클래스 : 추상 클래스를 상속받는 클래스는 반드시 추상 클래스의 하위 클래스가 된다.

인터페이스 : 어떤 클래스를 상속하든 하위 타입이 아닌 같은 타입으로 취급된다.

 

 

인터페이스는 믹스인(mixin) 정의에 안성맞춤이다

 

기존 클래스에 손쉽게 인터페이스를 구현할 수 있다.

반면, 기존 클래스에 추상 클래스를 끼워 넣기는 어렵다. 계층구조상 상위<->하위의 구조를 가지므로 클래스 계층구조에 혼란을 야기한다.

이러한 이유로, 인터페이스는 믹스인 정의에 안성맞춤이다.

 

믹스인(mixin)

대상 타입의 주된 기능에 선택적 기능을 혼합하는 것. 예를들어 Comparable은 자신을 구현한 클래스의 인스턴스끼리는 순서를 정할 수 있다

고 선택적 기능을 혼합하는 믹스인이다.

 

 

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

 

클래스는 구체 클래스가 추상 클래스의 하위 계층 클래스가 되지만, 인터페이스는 같은 타입으로 취급된다.

 

가수(Singer)와 작곡가(Songwriter) 인터페이스가 있을 때, 작곡과 노래를 모두 하는 싱어송라이터(SingerSongwriter)는

Singer와 Songwriter 인터페이스를 모두 확장하여 새로운 인터페이스로 정의할 수 있다. (+ 새로운 메서드를 추가할 수도 있다)

 

// 가수
public interface Singer {
    void sing();
}

// 작곡가
public interface SongWriter {
    void compose();
}

// 싱어송라이터 (가수 + 작곡)
public interface SingerSongwriter extends Singer, SongWriter {
    // 새로운 메서드를 추가할 수도 있다
    void strum();
    void actSensitive();
}

 

디폴트 메소드에는 제약이 있다

제약 사항

  1. Object 메소드인 equals와 hashcode를 디폴트 메소드로 제공 안함.
  2. 인터페이스는 인스턴스 필드를 가질 수 없고 public이 아닌 정적 메소드를 가질 수 없음.
  3. 본인이 만든 인터페이스가 아니면 디폴트 메소드를 추가할 수 없음.

 

골격 구현(skeletal implementation) 활용

인터페이스로는 타입을 정의하고, 골격 구현 클래스는 나머지 메서드를 구현함으로써 인터페이스를 구현할 때 공통된 부분을 추상클래스로 해결할 수 있다. 이를 템플릿 메서드 패턴이라 한다.

 

public interface Character {
    void create();
    void hunt();
    void levelUp();
    void process();
}

Character라는 인터페이스가 존재한다.

해당 인터페이스는 생성(create), 사냥(hunt), 레벨업(levelUp)의 메서드를 가지며,

생성 - 사냥 - 레벨업을 순서대로 수행하는 process라는 메서드를 가진다.

 

// Warrior
public class Warrior implements Character {

    @Override
    public void create() {
        System.out.println("create");
    }

    @Override
    public void hunt() {
        System.out.println("warrior hunt");
    }

    @Override
    public void levelUp() {
        System.out.println("levelup");
    }

    @Override
    public void process() {
        create();
        hunt();
        levelUp();
        System.out.println("============");
    }
}

 

// Archer
public class Archer implements Character {

    @Override
    public void create() {
        System.out.println("create");
    }

    @Override
    public void hunt() {
        System.out.println("archer hunt");
    }

    @Override
    public void levelUp() {
        System.out.println("levelup");
    }

    @Override
    public void process() {
        create();
        hunt();
        levelUp();
        System.out.println("============");
    }
}

 Warrior와 Archer 클래스를 자세히 보면, hunt 메서드를 제외하고는 모두 동일한 동작을 수행한다.

이럴 때 골격 구현 클래스를 구현하여 공통된 메서드는 골격 구현 클래스에서 구현하고, 공통되지 않은 메서드에 대해서만 상속 받은 클래스에서 구현하도록 할 수 있다.

 

// AbstractCharacter
public abstract class AbstractCharacter implements Character {
    @Override
    public void create() {
        System.out.println("Character create");
    }

    @Override
    public void levelUp() {
        System.out.println("Character levelUp");
    }

    @Override
    public void process() {
        create();
        hunt();
        levelUp();
        System.out.println("============");
    }
}

 


해당 골격 구현 클래스를 Warrior와 Archer 클래스에 적용한다.

 

public class Warrior extends AbstractCharacter {

    @Override
    public void hunt() {
        System.out.println("warrior hunt");
    }
}

 

public class Archer extends AbstractCharacter {

    @Override
    public void hunt() {
        System.out.println("archer hunt");
    }
}

 

728x90
728x90

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

 

클래스의 API로 공개된 메소드에서 클래스 자신의 또 다른 메서드를 호출할 수 있다.

그런데 마침 호출되는 메서드가 재정의 가능 메서드라면 그 사실을 호출하는 메서드의 API 설명에 적시해야 한다.

덧붙여서 어떤 순서로 호출하는지, 각각의 호출 결과가 이어지는 처리에 어떤 영향을 주는지도 담아야 한다.

 

즉 재정의 가능 메소드를 호출할 수 있는 모든 상황을 문서로 남겨야 한다.

대표적인 예로 @implSpec를 들 수 있다. Implementation Requirements로 그 메서드의 내부 동작 방식을 설명하는 어노테이션이다.

 

    /**
     ...
     * @implSpec
     * This implementation iterates over the collection looking for the
     * specified element.  If it finds the element, it removes the element
     * from the collection using the iterator's remove method.
     *
     * <p>Note that this implementation throws an
     * {@code UnsupportedOperationException} if the iterator returned by this
     * collection's iterator method does not implement the {@code remove}
     * method and this collection contains the specified object.
        ...
     */
    public boolean remove(Object o) {
      ...
    }

 

설명에 따르면 iterator 메서드를 재정의하면 remove 메서드의 동작에 영향을 준다고 한다.

 

 

Protected 메서드 형태로 공개하라

효율적인 하위 클래스를 큰 어려움 없이 만들 수 있게 하려면 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected메서드 형태로 공개해야 할 수도 있다.

드물게는 protected 필드로 공개해야할 수도 있다.

 

clear 메서드가 removeRange를 호출한다.

 

이 메서드가 존재하는 이유는 단지 하위 클래스에서 부분리스트의 clear메서드를 고성능으로 만들기 쉽게 하기 위해서이다. 

removeRange메서드가 없는 경우, 하위 클래스에서 clear메서드를 호출하면 제거할 원소 수의 제곱에 비례해 성능이 느려지거나 부분리스트의 매커니즘을 밑바닥 부터 새로 구현해야 했을 것이다.

 

상속용 클래스를 설계할 때 어떤 메서드를 protected로 노출해야하는지 정답은 없다. 심사숙고해서 잘 예측해본 다음, 실제 하위클래스를 만들어 시험해보는 것이 최선이다. 

protected 메서드 하나하나가 내부 구현에 해당하므로 그 수는 가능한 한 적어야 한다. 반면, 너무 적게 노출해서 상속으로 얻는 이점마저 없애지 않도록 주의해야한다.

 

상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는 것이 '유일'하다.  

꼭 필요한 protected 맴버를 놓쳤다면 하위 클래스를 작성할 때 그 빈자리가 확연하게 드러난다. 반대로 하위 클래스를 여러 개 만들 때까지 전혀 쓰이지않는 protected 맴버는 사실 private이었어야 할 가능성이 크다.

 

 

 

상속용 클래스 생성자는 직접/간접적 재정의 가능 메서드 호출 금지

상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다. 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 호출된다. 이때 재정의한 메서드가 하위 클래스의 생성자에서 초기화하는 값에 의존한다면 의도대로 동작하지 않을 것이다.

public class Super {
    //잘못된 예 - 생성자가 재정의가능 메서드를 호출한다.
    public Super() {
    	overridMe();
    }
    
    public void overrideMe(){}
}

public final class Sub extends Super {
    // 초기화되지 않은 final 필드, 생성자에서 초기화한다.
    private final Instant instant;
    
    Sub() {
    	instant = instant.now();
    }
    // 재정의 가능 메서드, 상위 클래스의 생성자가 호출한다.
    @Override
    public void overrideMe(){
    	System.out.println(instant);
    }
    
    public static void main(String[] args){
    	Sub sub = new Sub();
        sub.overrideMe();
    }
}

 

 

 

Cloneable과 Serializable 인터페이스를 상속용 클래스에 적용하지 마라

clone과 readObject는 생성자와 비슷한 효과를 낸다(객체를 만든다)  상속용 클래스에서 Cloneable이나 Serializable을 구현할지 정해야 한다면, clone과 readObject 모두 직접적,/간접적으로든 재정의 가능 메서드를 호출해서는 안된다.

 

  • readObject의 경우 하위 클래스의 상태가 미쳐 역직렬화 되기 전에 재정의한 메서드부터 호출된다.
  • clone은 하위 클래스의 clone메서드가 복제본 상태를 올바른 상태로 수정하기 전에 재정의한 메서드를 호출한다. clone이 잘못되면 더 큰문제는 깊은 복사가 이뤄지지 않아 원본객체에 타격을 줄 수 있다. 

 

Serializable을 구현한 상속용 클래스가 readResolve나 writeReplace 메서드를 갖는다면 이 메서드들은 private가 아닌 protected로 선언해야 한다. private로 선언하면 하위 클래스에서 무시되기 때문이다. 이 역시 상속을 허용하기 위해 내부 구현을 클래스 API로 공개하는 예중 하나이다. 인터페이스를 하위 클래스에서 구현하게 하는 방법도 있다.

 

 

클래스를 상속용으로 설계하려면 엄청난 노력이 들고 그 클래스에 안기는 제약도 크다. 여기에 대한 가장 좋은 방법은 상속용으로 설계하지 않은 클래스는 상속을 금지하는 것이다. 가장 쉬운 방법은 클래스를 final로 선언하는것, 두 번 쨰는 모든 생성자를 private나 package private로 선언하고 public한 정적 팩터리를 만들어주는 방법. 정적 팩터리 방법은 내부에서 다양한 하위 클래스를 만들어 쓸 수 있는 유연성을 준다.

 

상속의 대안으로는 인터페이스, 래퍼 클래스 패턴등이 있다. 상속을 금지하더라도 개발에 큰 어려움이 없을 것이다. 다만, 구체 클래스가 표준 인터페이스를 구현하지 않았는데 상속을 금지하면 사용하기 불편해진다. 이런 경우에도 상속을 꼭 허용하겠다면 클래스 내부에서는 재정의 가능 메서드를 사용하지않게 만들고 이 사실을 문서로 남기는 것이다. 

728x90

+ Recent posts