클래스와 인터페이스


클래스와 인터페이스를 쓰기 편하고, 견고하며 유연하게 만드는 방법을 알아보자.


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

어설프게 설계된 컴포넌트와 잘 설계된 컴포넌트의 가장 큰 차이는?
클래스 내부 데이터와 내부 구현 정보를 외부 컴포넌트로부터 얼마나 잘 숨겼느냐다.

잘 설계된 컴포넌트는 모든 내부 구현을 완벽히 숨겨 구현과 API를 깔끔히 분리한다.
이는 정보은닉, 혹은 캡슐화라고 하는 개념이기도 하다.


정보 은닉의 장점

1) 시스템 개발 속도를 높인다.
여러 컴포넌트를 병렬로 개발할 수 있기 때문이다.


2) 시스템 관리 비용을 낮춘다.
각 컴포넌트를 빠르게 파악할 수 있고 교체 부담이 감소한다.

3) 정보 은닉 자체가 성능을 높여주지는 않지만 성능 최적에 도움이 된다.
완성된 시스템을 프로파일링해 최적화할 컴포넌트를 정해
다른 컴포넌트에 영향을 주지 않고 해당 컴포넌트만 최적화할 수 있기 때문이다.

4) 소프트웨어 재사용성을 높인다.

5) 큰 시스템을 제적하는 난이도를 낮춘다.
시스템 전체가 완성되지 않아도 개별 컴포넌트의 동작을 검증할 수 있기 때문이다.


정보 은닉을 위해 자바가 제공하는 다양한 장치 중 접근 제어 메커니즘의 기본 원칙은
모든 클래스와 멤버의 접근성을 가능한 한 좁혀야 한다는 것이다.

(가장 바깥) 탑레벨 클래스와 인터페이스에 부여할 수 있는 접근 수준은
package-private와 public이다.


한 클래스에서만 사용하는 package-private 탑레벨 클래스나 인터페이스는
이를 사용하는 클래스 안에 private static으로 중첩시켜보자.
탑레벨로 두면 같은 패키지의 모든 클래스가 접근할 수 있지만 private static으로 중첩시키면
바깥 클래스 하나에서만 접근할 수 있다.

이보다 더 중요한 것은 public일 필요가 없는 클래스의 접근 수준을 package-private 탑레벨 클래스로 좁히는 일이다.
public 클래스는 그 패키지의 API인 반면, package-private 탑레벨 클래스는 내부 구현에 속하기 때문이다.


접근 제어자

멤버(필드, 메서드, 중첩 클래스, 중첩 인터페이스)에 부여할 수 있는 접근 수준은 네 가지다.


주의할 점




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

class Point {
    public double x;
    public double y;
}

이런 클래스는 데이터 필드에 직접 접근할 수 있으니 캡슐화의 이점을 제공하지 못한다.

이를 private으로 바꾸고 public 접근자를 추가한다.

private double x;

public double getX() { return x; }

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

package-private 클래스 혹은 private 중첩 클래스라면 데이터 필드를 노출한다 해도 하등의 문제가 없다.
클라이언트 코드가 이 클래스 내부 표현에 묶이긴 하나,
클라이언트도 어차피 이 클래스를 포함하는 패키지 안에서 동작하는 코드일 뿐이다.
따라서 패키지 바깥 코드는 전혀 손대지 않고도 데이터 표현 방식을 바꿀 수 있다.

public 클래스의 필드가 불변이라면 직접 노출할 때의 단점이 조금은 줄어들지만 좋은 생각은 아니다.
API를 변경하지 않고는 표현 방식을 바꿀 수 없고
필드를 읽을 때 부수 작업을 수행할 수 없다는 단점이 여전하다.
단, 불변식은 보장됨

ex. 불변 필드를 노출한 public 클래스

public final class Time {

    public final int hour;
    
    . . .
    
}




17. 변경 가능성을 최소화하라.

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

자바 플랫폼 라이브러리에도 다양한 불변 클래스가 있다.


불변 클래스를 만드는 5가지 규칙

  1. 객체의 상태를 변경하는 메서드를 제공하지 않는다.

  2. 클래스를 확장할 수 없도록 한다.

  3. 모든 필드를 final로 선언한다.

  4. 모든 필드를 private으로 선언한다.

  5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.


상속하지 못하게 하는 방법

자신을 상속하지 못하게 하는 가장 쉬운 방법은 final 클래스로 선언하는 것이지만, 더 유연한 방법이 있다.
모든 생성자를 private 혹은 package-private 으로 만들고 public 정적 팩토리를 제공하는 방법이다.
package-private 구현 클래스를 원하는 만큼 만들어 활용할 수 있으니 훨씬 유연하다.




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

상속은 코드를 재사용하는 강력한 수단이지만, 항상 최선은 아니다.
(지금 이야기하는 ‘상속’은 클래스가 다른 클래스를 확장하는 구현 상속을 말한다.)

메서드 호출과 달리 상속은 캡슐화를 깨뜨린다.
→ 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있다.


상속의 문제점

(1) 자신의 다른 부분을 사용하는 ‘자기 사용(self-use)’ 여부는 해당 클래스의 내부 구현 방식에 해당하며,
다음 릴리스에서도 유지될지는 알 수 없다.

(2) 상위 클래스의 메서드 동작을 다시 구현하는 방식은 어렵고,
시간이 더 들며 오류를 내거나 성능을 떨어뜨릴 수도 있다.

(3) 다음 릴리스에서 상위 클래스에 새로운 메서드를 추가한다고 가정하면,
특정 조건을 만족해야만 한다면 모든 메서드를 재정의해 필요한 검증을 추가해야 한다.
하지만 이 방식은 상위 클래스에 또 다른 추가 메서드가 만들어지기 전까지만 통한다.

위 문제점은 모두 메서드 재정의가 원인이다.
위 문제를 모두 피해가는 방법이 있는데 기존 클래스를 확장하는 대신,
새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하는 것이다.

이러한 설계를 컴포지션(composition)이라 한다.
새 클래스의 인스턴스 메서드들은 (private 필드로 참조하는) 기존 클래스의 대응하는 메소드를 호출해 그 결과를 반환한다.
이 방식을 전달(forwarding)이라 한다.
새로운 클래스는 기존 클래스의 내부 구현 방식의 영향에서 벗어나며,
기존 클래스에 새로운 메서드가 추가되더라도 전혀 영향을 받지 않는다.


예) InstrumentedSet 컴포지션과 전달방식으로 구현

// 래퍼 클래스 
public clas InstrumentedSet<E> extends ForwardingSet<E> {
    public InstrumentedSet(Set<E> s) { super(s); } 
}

// 재사용할 수 있는 전달 클래스 
public class ForwardingSet<E> implements Set<E> {
    private final Set<E> s;
    public ForwardingSet(Set<E> s) {
        this.s = s;
    }
}

다른 Set 인스턴스를 감싸고 있다는 뜻에서 InstrumentedSet 같은 클래스를 래퍼 클래스라고 하며,
Set에 기능을 덧씌운다는 뜻에서 데코레이터 패턴이라고 한다.

래퍼 클래스는 단점이 거의 없다.
래퍼 클래스가 콜백(Callback) 프레임워크와는 어울리지 않는다는 점만 주의하면 된다.
(콜백 프레임워크: 자기 자신의 참조를 다른 객체에 넘겨서 다음 호출 때 사용하도록 한다.)

내부 객체는 자신을 감싸고 있는 래퍼의 존재를 모르니 대신 자신(this)의 참조를 넘기고,
콜백 때는 래퍼가 아닌 내부 객체를 호출하게 된다. 이를 SELF 문제라고 한다.
실전에서 재사용할 수 있는 전달 클래스를 인터페이스당 하나씩만 만들어주면 원하는 기능을 덧씌우는 전달 클래스들을 쉽게 구현할 수 있다.

상속은 반드시 하위 클래스가 상위 클래스의 ‘진짜’ 하위 타입인 상황에서만 쓰여야 한다.
컴포지션을 써야 할 상황에서 상속을 사용하는 건 내부 구현을 불필요하게 노출하는 꼴이다.
API가 내부 구현에 묶이고 그 클래스의 성능도 영원히 제한된다.
또한 클라이언트가 노출된 내부에 직접 접근하 수도 있다.




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

상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 사용하는지 문서로 남겨야 한다.
내부 매커니즘을 문서로 남기는 것만이 상속을 위한 설계의 전부는 아니다.
효율적인 하위 클래스를 만들 수 있게 하려면 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여
protected 메서드 형태로 공개해야 할 수도 있다.

Q. 상속용 클래스를 설계할 때 어떤 메서드를 protected로 노출해야 할지는 어떻게 결정할까?
A. 직접 하위 클래스를 만들어보는 것이 유일하다.

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


예) 생성자가 재정의 가능 메서드를 호출

public class Super {
    public Super() {
        overrideMe();
    }
    
    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);
    }
   
}

Sub 클래스 객체를 생성하고 overrideMe() 메서드를 호출하면
instant를 두 번 출력할 것으로 기대하지만 첫번째는 null을 출력한다.
상위 클래스의 생성자는 하위 클래스의 생성자가 인스턴스 필드를 초기화하기도 전에 overrideMe를 호출하기 때문이다.
(private, final, static 메서드는 재정의가 불가능하기 때문에 생성자에서 안심하고 호출해도 된다.)

Cloneable과 Serializable 인터페이스는 상속용 설계의 어려움을 한층 더해준다.
상속용 클래스에서 Cloneable이나 Serializable을 구현할지 정해야 한다면, 제약은 생성자와 비슷하다.
즉 clone과 readObject 모두 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 된다.
하위 클래스의 상태가 미완성인데 재정의한 메서드부터 호출하게 된다.

Serializable을 구현한 상속용 클래스가 readResolve나 writeReplace 메서드를 갖는다면
이 메서드들은 private이 아닌 protected로 선언해야 한다.
private으로 선언하면 하위 클래스에서 무시되기 때문이다.

이 문제를 해결하는 가장 좋은 방법은 상속용으로 설계하지 않은 클래스는 상속을 금지하는 것이다.
(1) 클래스를 final로 선언
(2) 모든 생성자를 private이나 package-private로 선언하고 public 정적 팩토리로 만든다.




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

자바가 제공하는 다중 구현 매커니즘은 인터페이스추상 클래스다.
자바 8부터 인터페이스도 디폴트 메서드를 제공할 수 있게 되어 두 메커니즘 모두 인스턴스 메서드를 구현 형태로 제공할 수 있다.
둘의 가장 큰 차이는 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다는 점이다.
자바는 단일 상속만 지원하므로 추상 클래스 방식은 새로운 타입을 정의하는 데 커다란 제약을 갖고 있는 것이다.

반면 기존 클래스 위에 새로운 추상 클래스를 끼워넣기는 어려운 게 일반적이다.
두 클래스가 같은 추상 클래스를 확장하길 원한다면, 그 추상 클래스는 계층구조상 두 클래스의 공통 조상이어야 한다.
이 방식은 클래스 계층구조에 혼란을 일으킨다.


인터페이스는 믹스인(mixin) 정의에 안성맞춤이다.
믹스인이란 클래스가 구현할 수 있는 타입으로 믹스인을 구현한 클래스에
원래의 ‘주된 타입’ 외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 준다.
예) Comparable은 자신을 구현한 클래스의 인스턴스들끼리는 순서를 정할 수 있다고 선언하는 믹스인 인터페이스이다.

추상 클래스로는 믹스인을 정의할 수 없다.
이유는 앞서와 같이, 기존 클래스에 덧씌울 수 없기 때문이다.
클래스는 두 부모를 둘 수 없고 클래스 계층구조에는 믹스인을 삽입하기에 합리적인 위치가 없기 때문이다.

mixin에 대해 더 알아보기 : https://hyerin6.github.io/2021-06-21/mixin/


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

public interface Singer {
    AudioClip sing(Song s);
}

public interface SongWriter {
    Song compose(int chartPosition);
}

위와 같은 가수 인터페이스와 작곡가 인터페이스가 있다.
우리 주변에 작곡도 하는 가수가 제법있는데,
타입을 인터페이스로 정의하면 가수 클래스가 Singer와 Songwriter 모두를 구현해도 문제가 없다.
Singer와 Songwriter 모두를 확장하고 새로운 메서드까지 추가한 제 3의 인터페이스를 정의할 수도 있다.

// Singer와 Songwriter 모두 구현 
public class People implements Singer, SongWriter {
    @Override
    public void Sing(String s) {

    }
    @Override
    public void Compose(int chartPosition) {

    }
}

// Singer와 Songwriter 모두 확장, 새로운 메서드까지 추가한 제 3의 인터페이스 정의 
public interface SingerSongWriter extends Singer, Songwriter {
        AudioClip strum();
        void actSensitive();
}


위와 같은 구조를 클래스로 만들려면?

public abstract class Singer {
    abstract void sing(String s);
}

public abstract class SongWriter {
    abstract void compose(int chartPosition);
}

public abstract class SingerSongWriter {
    abstract void strum();
    abstract void actSensitive();
    abstract void Compose(int chartPosition);
    abstract void sing(String s);
}

가능한 조합 전부를 각각의 클래스로 정의한 고도비만 계층구조가 만들어질 것이다.
추상 클래스로 만들었기 때문에 Singer 클래스와 SongWriter 클래스를 둘다 상속할 수 없어
다음과 같은 SingerSongWriter라는 또 다른 추상 클래스를 만들어서 클래스 계층을 표현할 수 밖에 없다.
매개변수 타입만 다른 메서드들을 수없이 많이 가진 거대한 클래스를 낳을 수 있다.
이는 조합 폭발(combinato-rial explosion)이라고 부르는 현상이다.


래퍼 클래스 관용구와 함께 사용하면 인터페이스는 기능을 향상 시키는 안전하고 강력한 수단이 된다.
타입을 추상 클래스로 정의해두면 그 타입에 기능을 추가하는 방법은 상속뿐이다.
상속해서 만든 클래스는 래퍼 클래스보다 활용도가 떨어지고 깨지기는 더 쉽다.

인터페이스 메서드 중 구현 방법이 명백한 것이 있다면, 그 구현을 디폴트 메서드로 제공할 수 있다.
그런데 디폴트 메서드에도 제약은 있다.

1) Object의 equals, hashcode 같은 메서드는 디폴트 메서드로 제공해서는 안된다.

2) public이 아닌 정적 멤버도 가질 수 없다.

3) 본인이 만들지 않은 인터페이스에는 디폴트 메서드를 추가할 수 없다.


인터페이스와 추상 골격 구현 (skeletal implementation) 클래스를 함께 제공하는 식으로
인터페이스와 추상 클래스의 장점을 가질 수 있다.
인터페이스로 타입 정의, 필요한 디폴트 메서드 구현
추상 골격 구현 클래스는 나머지 메서드까지 구현
이렇게 골격 구현을 확장하는 것만으로 이 인터페이스를 구현하는데 필요한 일이 대부분 완료된다.
이는 템플릿 메서드 패턴과 같다.

예) 컬렉션 프레임워크의 AbstractList, AbstractSet 클래스
두 추상 클래스는 각각 List, Set 인터페이스의 추상 골격 구현 클래스이다.


추상 골격 구현 클래스는 아래 게시물에서 더 알아보자.
https://hyerin6.github.io/2021-06-20/skeletal_implementation/

위 게시물에서 예제로 인터페이스의 디폴트 메소드를 사용하지 않고 추상 골격 구현 클래스를 만들어 중복을 제거했다.
그런데 Vending을 구현하는 구현 클래스가 VendingManuFacturer라는 제조사 클래스를 상속받아야해서
추상 골격 구현을 확장하지 못하는 상황일 땐 어떻게 해야할까?

public class VendingManuFacturer {
    public void printManufacturerName() { . . . }
}

// 상속받아야 하는 클래스를 구현체가 상속받는다.
public class SnackVending extends VendingManufacturer implements Vending {
    InnerAbstractVending innerAbstractVending = new InnerAbstractVending();

    @Override
    public void start() {
        innerAbstractVending.start();
    }

    @Override
    public void chooseProduct() {
        innerAbstractVending.chooseProduct();
    }

    @Override
    public void stop() {
        innerAbstractVending.stop();
    }

    @Override
    public void process() {
        printManufacturerName();
        innerAbstractVending.process();
    }

    private class InnerAbstractVending extends AbstractVending {
        @Override
        public void chooseProduct() {
            System.out.println("choose product");
            System.out.println("chocolate");
            System.out.println("cracker");
        }
    }
}

인터페이스를 구현한 클래스에서 해당 골격 구현을 확장한 private 내부 클래스를 정의하고
각 메소드 호출을 내부 클래스의 인스턴스에 전달하여 골격 구현 클래스를 우회적으로 이용하는 방식을
시뮬레이트한 다중 상속(simulated multiple inheritance)이라고 한다.


단순 구현(simple implementation)

단순 구현은 골격 구현의 작은 변종이다.
단순 구현도 골격 구현과 같이 상속을 위해 인터페이스를 구현한 것이지만,
추상 클래스가 아니란 점이 다르다.
이러한 단순 구현은 그대로 써도 되고 필요에 맞게 확장해도 된다.
예) AbstractMap.SimpleEntry




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

자바 8전에 기존 구현체를 깨뜨리지 않고는 인터페이스에 메서드를 추가할 방법이 없었다.
자바 8에서 기존 인터페이스에 메서드를 추가할 수 있도록 디폴트 메서드를 소개했지만,
위험이 완전히 사라진 것은 아니다.

디폴트 메서드를 선언하면 그 인터페이스를 구현한 후
디폴트 메서드를 재정의하지 않은 모든 클래스에서 디폴트 구현이 쓰이게 된다.
그러나 모든 기존 구현체들과 매끄럽게 연동되리라는 보장은 없다.
디폴트 메서드는 구현 클래스에 대해 아무것도 모른 채 합의 없이 삽입될 뿐이다.

자바 8에서 핵심 컬렉션 인터페이스들에 다수의 디폴트 메서드가 추가되었다.
주로 람다를 활용하기 위해서다.
하지만 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기란 어렵다.

자바 8에서 추가된 Collection 인터페이스 추가된 removeIf 메서드

반복자를 이용해 순회하면서 각 원소를 인수로 넣어 predicate를 호출해서,
predicate가 true를 반환하면 iterator의 remove() 메서드를 호출해 원소를 제거 한다.

default boolean removeIf(Predivate<? super E> filter) {
    Objects.requireNonNull(filter);
    boolean result = false;
    for ( Iterator<E> it = iterator(); it.hashNext(); ) {
        if (filter.test(it.next())) {
            it.remove();
            result = true;
        }
    }
    return result;
}

이렇게 Collection에 새롭게 들어간 디폴드 메서는 과연 모든 Collection에 대해 정상작동 할까?


apache가 만든 SynchronizedCollection

이 클래스는 스레드 안정성을 위해서 동기화가 되어있는 List의 wrapper 클래스이다.
다음과 같이 코드를 보면 동기화 코드가 있는걸 확인할 수 있다.

 @Override
    public boolean add(final E object) {
        synchronized (lock) {
            return decorated().add(object);
        }
    }

그러나 이 클래스는 removeIf 메서드를 재정의하지 않는다.
즉 자바8과 함께 쓴다면 스레드 안전성을 갖지 못한다.

removeIf의 구현은 동기화에 대해 아무것도 모르기 때문에 락 객체를 사용할 수 없다.
SynchronizedCollection 인스턴스를 여러 스레드가 공유하는 환경에서 한 스레드가 removeIf를 호출하면
ConcurrentModificationException이 발생하거나 다른 예기치 못한 결과로 이루어 질 수 있다.

JDK에서 이런 문제를 예방하기 위해 구현 클래스에서 removeIf() 디폴트 메서드를 재정의했다.

Collections.synchronizedCollection 클래스의 removeIf()

@Override
public boolean removeIf(Predicate<? super E> filter) {
    synchronized (mutex) {
        return c.removeIf(filter);
    }
}

하지만 JDK에 속하지 않은 제 3의 기존 컬렉션 구현체들은 이런 언어 차원의 인터페이스에 맞춰 수정될 기회가 없다.


해결 방법은?




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

예) 상수 인터페이스
메서드 없이 상수를 뜻하는 static final 필드만 갖고 있는 인터페이스
정규화된 이름을(qualified name) 쓰는 것을 피하고자 이 인터페이스를 구현한다.

상수는 내부 구현에 해당한다.
상수 인터페이스 구현
-> 내부 구현을 클래스의 API로 노출하는 행위
-> 클라이언트 코드가 내부 구현에 해당하는 이 상수들에 종속




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

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

예) 원과 사각형을 표현할 수 있는 클래스

class Figure {
    enum Shape { RECTANGLE, CIRCLE };
    
    // 태그 필드 - 현재 모양을 나타낸다.   
    final Shape shape;
    
    // 다음 필드들은 모양이 사각형일 때만 쓰인다. (RECTANGLE)
    double length;
    double width;
    
    // 다음 필드는 모양이 원일 때만 쓰인다. (CIRCLE)   
    double radius;
    
    // 사각형 생성자   
    Figure(double length, double width) { . . . }
    
    // 원 생성자 
    Figure(double radius) { . . . }
    
    double area() {
        switch(shape) {
            . . .
        }
    }
     
}

태그 달린 클래스에는 단점이 한가득이다.

태그 달린 클래스는 장황하고, 오류를 내기 쉽고 비효율적이다.


객체 지향 언어는 타입 하나로 다양한 의미의 객체를 표현하는 훨씬 나은 수단을 제공한다.
-> 클래스 계층구조를 활용하는 서브 타이핑(subtyping) 이다.

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

(1) 가장 먼저 계층 구조의 루트(root)가 될 추상 클래스 정의
(2) 태그 값에 따라 동작이 달라지는 메서드는 추상 메서드로 선언
(3) 태그 값에 상관없는 동작이 일정한 메서드는 루트 클래스에 일반 메서드로 추가
(4) 모든 하위 클래스에서 공통으로 사용하는 데이터 필드들도 전부 루트 클래스에 올림
(5) 루트 클래스를 확장한 구체 클래스를 의미별로 하나씩 정의한다.


// Figure.java 
abstract class Figure {
    abstract double area();
}

// Circle.java 
class Circle extends Figure {
    final double radius;

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

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

// Rectangle.java
class Rectangle extends Figure {
    final double length;
    final double width;

    Rectangle(double length, double width) {
        this.length = length;
        this.width  = width;
    }
    
    @Override 
    double area() { return length * width; }
}

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

(1) 루트 클래스를 건드리지 않고 다른 개발자들이 독립적으로 계층구조 사용, 확장 가능

(2) 타입이 의미별로 존재

(3) 타입 사이의 자연스러운 계층 관계를 반영할 수 있어
유연성과 컴파일타임 타입 검사 능력이 높아짐




24. 멤버 클래스는 되도록 static으로 만들자.

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


정적 멤버 클래스

정적 멤버 클래스는 바깥 클래스의 private 멤버에도 접근할 수 있다는 점만 제외하면 일반 클래스와 똑같다.


비정적 멤버 클래스

비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결된다.


멤버 클래스에서 바깥 인스턴스에 접근할 일이 없으면 무조건 static을 붙여서 정적 멤버 클래스로 만들자.

static을 생략하면 바깥 인스턴스의 숨은 외부 참조를 갖게 된다.
이 참조를 저장하려면 시간과 공간이 소비된다.
더 심각한 문제는 가비지 컬렉션이 바깥 클래스의 인스턴스를 수거하지 못하는 메모리 누수가 생길 수 있다는 점이다.


static / non-static 객체 생성

class A {
    static class B { ... }
}

void foo() {
    A.B b = new B();
}

static 예약어가 있음으로 인해 독립적으로 생성할 수 있다.

class A {
    class B { ... }
}

void foo() {
    // ex1
    A a = new A();
    A.B b = a.new B();
    
    // ex2
    A.B b = new A().new B();
}

반드시 A 객체를 생성한 뒤 A 객체를 이용해서 생성해야 한다.
즉 비정적 내부 클래스는 바깥 클래스에 대한 참조가 필요하다는 것이다.


static / non-static 메모리 누수 가능성

비정적 내부 클래스의 경우 바깥 클래스에 대한 참조를 가지고 있기 때문에 메모리 누수가 발생할 여지가 있다.
바깥 클래스는 더 이상 사용되지 않지만 내부 클래스의 참조로 인해 GC가 수거하지 못해
바깥 클래스의 메모리 해제를 하지 못하는 경우가 발생할 수 있다.

정적 내부 클래스의 경우 바깥 클래스에 대한 참조 값을 가지고 있지 않기 때문에 메모리 누수가 발생하지 않는다.


static / non-static 사용 시기

메모리 누수가 발생할 수 있는 문제점이 있기 때문에
내부 클래스가 독립적으로 사용된다면 정적 클래스로 선언하여 사용하는 것이 좋다.


static / non-static 멤버 클래스가 public이나 protected 멤버라면?

공개된 클래스의 public이나 protected 멤버라면 정적이냐 아니냐는 두 배로 중요해진다.
멤버 클래스 역시 공개 API가 되기 때문에 향후 릴리스에서 static을 붙이면 하위 호환성이 떨어진다.


익명 클래스 (Anonymous Inner Class)

즉, 상수 표현을 위해 초기화된 final 기본 타입과 문자열 필드만 가질 수 있음

익명 클래스는 응용하는 데 제약이 많은 편이다.

// 인터페이스 사용
public interface Monster{
    String getName();
} 

public static void main(String args[]){
    Monster monster = new Monster(){
        String name;
        public String getName(){
            return name;
        }
    };
    System.out.println(monster.getName());
}


// 클래스 상속 사용 
public class Pet{
    String name = "부모 클래스";
    public String getName(){
            return name;
    }
}

public static void main(String[] args){
    Pet pet = new Pet(){
            String name = "익명 내부 클래스";
            @Override
            public String getName(){
                return name;
            }
    };
    System.out.println(pet.getName()); // 결과 : 익명 내부 클래스
}

Pet 익명 클래스는 Pet 클래스는 아니다.
생성된 인스턴스의 클래스 이름 확인 instance.getClass().getName
Pet + $ + n(생성된 몇번 째) 이런식의 클래스 이름이 나온다.
인터페이스로 선언된 익명 클래스는 현재 main에 속해있는 클래스 이름을 반환한다.
즉 자바에서는 이 서로 두개의 클래스를 같은 클래스로 보고 있지 않다는 것이다.


사용 시기


지역 클래스

네 가지 중첩 클래스 중 가장 드물게 사용된다.

지역 클래스는 지역 변수처럼 메소드 내부에 정의되는 클래스를 말한다.


핵심 정리




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

소스 파일 하나에 톱레벨 클래스를 여러 개 선언하더라도 자바 컴파일러가 불평하지는 않지만
아무런 이득 없이 위험을 감수해야 하는 행위다.

한 클래스를 여러 가지로 정의할 수 있지만 어느 소스 파일을 먼저 컴파일하냐에 따라 달라진다.

예) 집기(Utensil)와 디저트(Dessert) 클래스를 담은 Utensil.java

public class Main {
    public static void main(String[] args) {
        System.out.println(Utensil.Name + Dessert.Name);       
    }
}
class Utensil { 
    static final String NAME = "pan";
}

class Dessert { 
    static final String NAME = "cake";
}
class Utensil { 
    static final String NAME = "pot";
}

class Dessert { 
    static final String NAME = "pie";
}


이때 우연히 똑같은 두 클래스를 담은 Dessert.java 파일을 만들었다고 해보자.

컴파일러에 어느 소스 파일을 먼저 건네느냐에 따라 동작이 달라지므로 반드시 바로 잡아야 하는 문제다.

해결책은 톱레벨 클래스들을 서로 소스 파일로 분리하면 그만이다.
다른 클래스에 딸린 부차적인 클래스라면 정적 멤버 클래스를 사용하는 방법을 고민해볼 수 있다.

정적 멤버 클래스로 만들면 읽기 좋고, private으로 선언하면 접근 범위도 최소로 관리할 수 있다.