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
728x90

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

 

 

상속 : 한 클래스가 다른 클래스의 속성과 메서드를 확장 혹은 재정의할 수 있도록 해주는 매커니즘

// 부모 클래스: 동물
class Animal {
    public void eat() {
        System.out.println("이 동물은 먹는다.");
    }
}

// 자식 클래스: 개
class Dog extends Animal {
    public void bark() {
        System.out.println("개는 짖는다.");
    }
}

 

컴포지션 : 하나의 클래스가 다른 클래스의 인스턴스를 포함하여, 그 인스턴스의 메서드를 활용하는 방식

// 독립된 기능을 가진 클래스: 소리 생성기
class SoundMaker {
    public void makeSound(String sound) {
        System.out.println(sound);
    }
}

// 개 클래스에서 소리 생성기를 사용
class Dog {
    private SoundMaker soundMaker;

    public Dog() {
        this.soundMaker = new SoundMaker();
    }

    public void bark() {
        soundMaker.makeSound("멍멍!");
    }
}

 

 

상속은 코드를 재사용하는 강력한 수단이지만, 항상 최선은 아니다.

잘못 사용하면 오류를 내기 쉬운 소프트웨어를 만들게 된다. 

상속의 문제점 :

  • 강한 결합 : 부모 클래스의 내부 변경이 자식 클래스에 영향을 줄 수 있어 유연성이 저하된다.
  • 캡슐화 위반 : 자식 클래스가 부모 클래스의 구현 세부 사항에 의존하게되면, 캡슐화가 약화된다.
  • 재사용성 저하 : 특정 구현에 강하게 결합된 상속 구조는 새로운 상황에 재사용하기 어렵다.

 

이러한 문제를 모두 피해가기 위해 기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하는게 좋다.

 

기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 뜻에서 이런 설계를 컴포지션(compostition : 구성) 이라고 한다.

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

 

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

 

컴포지션과 전달(위임)의 장점 : 

  • 낮은 결합도 : 객체간의 결합도를 낮춰, 유연성과 확장성을 향상시킨다.
  • 캡슐화 강화 : 내부 구현을 숨기고, 필요한 인터페이스만 노출시켜 캡슐화를 강화.
  • 재사용성 향상 : 구성 요소를 쉽게 교체하거나 재사용할 수 있어, 다양한 상황에 맞게 시스템을 조정할 수 있다.

 

결론

  • 상속은 강력하지만 캡슐화를 해친다.
  • 상속은 상위 클래스와 하위 클래스가 순수한 is-a 관계일 때만 써야 한다.
  • is-a 관계일 때도 하위 클래스의 패키지가 상위 클래스와 다르고, 상위 클래스가 확장을 고려해 설계되지 않았다면 여전히 문제가 될 수 있다.
  • 이를 해결하려면 컴포지션과 전달을 사용해야 한다.
  • 래퍼 클래스는 하위 클래스보다 견고하고 강력하다.

 

 

728x90
728x90

변경 가능성을 최소화하라

불변 클래스

  • 인스턴스의 내부 값을 수정할 수 없는 클래스이다.
  • 인스턴스의 저장된 정보가 객체가 파괴되기 전까지 바뀌지 않는다.
  • 대표적으로 String, Boolean, Integer, Float, Long 등등이 있다.
  • Immutable Class들은 heap영역에서 변경불가능 한 것이지 재할당을 못하는 것은 아니다.

사용 이유

  • 설계, 구현, 사용이 쉽다.
  • thread-safe하고 에러를 만들 가능성이 적다.

 

클래스를 불변으로 만들 시 지켜야 할 규칙

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

  • setter나 필드의 정보를 변경하는 메서드를 제공하지 않는다

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

  • 객체의 상태를 변하게 만드는 사태를 막아준다. 상속을 막는 대표적인 방법은 클래스를 final로 선언하는 것이다.

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

  • 필드의 수정을 막겠다는 설계자의 의도를 드러내는 방법이다.
  • 생성된 인스턴스를 동기화 없이 다른 스레드로 건네도 문제없이 동작하게끔 보장하여 준다.

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

  • 필드가 참조하는 가변 객체를 클라이언트에서 직접 접근하여 수정하는 일을 막아 준다.

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

  • 클래스에 가변 객체를 참조하는 필드가 하나라도 있다면 클라이언트에서 인스턴스 내에 가변 객체의 참조를 얻을 수 없게 해야한다.
  • 생성자, 접근자(getter), readObject 메서드 모두에서 방어적 복사를 수행한다.

 

 

public final class Complex {

    private final double re;
    private final double im;

    public Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }

    public double realPart() {
        return re;
    }

    public double imaginaryPart() {
        return im;
    }

    public Complex plus(Complex c) {
        return new Complex(re + c.re,im + c.im);
    }

    public Complex minus(Complex c) {
        return new Complex(re - c.re,im - c.im);
    }

    public Complex times(Complex c) {
        return new Complex(re * c.re - im * c.im,re * c.im + im + c.re);
    }

    public Complex dividedBy(Complex c) {
        double tmp = c.re * c.re + c.im * c.im;
        return new Complex((re * c.re + im * c.im) / tmp,
                (im * c.re - re + c.im) / tmp);
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }

        if (!(o instanceof Complex)) {
            return false;
        }

        Complex c = (Complex) o;
        return Double.compare(c.re, re) == 0
                && Double.compare(c.im, im) == 0;
    }

    @Override
    public int hashCode() {
        return 31 * Double.hashCode(re) + Double.hashCode(im);
    }

    @Override
    public String toString() {
        return "(" + re + " + " + im + "i)";
    }

}

위 코드는 불변 복소수 클래스이다.

 

  • 실수부와 허수부 값을 반환하는 접근자(realPart, imaginaryPart)와 사칙연산 메서드(plus, minusm times, dividedBy)를 정의하였다.
  • 이 사칙연산 매서드들은 인스턴스 자신은 수정하지 않고 새로운 Complex 인스턴스를 만들어 반환한다.
  • 이처럼 피연산자에 함수를 적용해 그 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴을 함수형 프로그래밍이라 한다.
  • 이처럼 함수형 프로그래밍 기법을 적용하면 코드의 불변이 영역이 되는 비율이 높아져 안전합니다.불변 객체의 장점
    불변 객체는 근본적으로 스레드 안전하여 따로 동기화 할 필요가 없다
  • 불변 객체는 안심하고 공유 할 수 있다
  • 불변 객체는 방어적 복사본이 필요없다
  • 불변 객체는 자유롭게 공유할 수 있음은 물론, 불변 객체끼리는 내부 데이터를 공유할 수 있다.
  • 불변 객체를 key로 하면 이점이 많다.
    • Map의 key
    • Set의 원소
  • 불변 객체는 그 자체로 실패 원자성을 제공한다

 

결론

  • 모든 클래스를 불면으로 만들수는 없다.
  • 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄여야 한다.
    다른 합당한 이유가 없다면 모든 필드는 private final 이어야 한다.
    생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
728x90
728x90

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

 

public 클래스의 경우

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

위처럼 인스턴스 필드만을 모아놓는 경우있다.

 

문제는 바로 저 필드만 딱 있을때 발생한다.

모두 public으로 선언되어 있기 때문에 다른 컴포넌트에서 해당 클래스 데이터 필드에 직접 접근이 가능하다.

캡슐화(Encapsulation)가 전혀 안되기 때문에 많은 단점과 위험성이 존재한다.

 

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 double getY() { 
  		return y; 
  	}

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

정석적인 수정 방법이다.

 

getter와 setter를 구현하여 private필드에 접근할 수 있게 되었다.

 

 

 

private-package, private 클래스의 경우

 

이 경우엔 데이터 필드를 노출하던 안하던 별로 상관이 없다.

이때는 그냥 본분을 다해 그 클래스가 구현하고자 하는 기능만 잘 구현해내면 된다.

실제로 이것은 클래스 선언 면에서나, 클라이언트 코드 면에서나 접근자보다 훨씬 깔끔해진다.

 

중첩된 private 클래스의 경우는 더 쉽다.

어차피 그 클래스를 사용할 수 있는건 바로 바깥 클래스 하나이기 때문에 클라이언트가 될 수 있는 대상이 훨씬 좁다.

내부 표현 수정이 더욱 용이하다.

 

 

만약 public 클래스의 필드가 불변이라면?

 

불변이라면야 직접 노출하는 거에 대해서 단점이 줄어들긴 한다.

하지만 다른 문제는 여전히 존재한다.

내부 표현 방식을 바꿀수 없고, 필드를 읽을 때 부수 작업도 수행할 수 없다.

 

public final class Time {
    private static final int HOURS_PER_DAY    = 24;
    private static final int MINUTES_PER_HOUR = 60;

    public final int hour;
    public final int minute;

    public Time(int hour, int minute) {
        if (hour < 0 || hour >= HOURS_PER_DAY)
            throw new IllegalArgumentException("Hour: " + hour);
        if (minute < 0 || minute >= MINUTES_PER_HOUR)
            throw new IllegalArgumentException("Min: " + minute);
        this.hour = hour;
        this.minute = minute;
    }

}

 

728x90
728x90

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

 

정보 은닉의 장점

  • 여러 컴포넌트를 병렬로 개발 가능 == 시스템 개발 속도 향상
  • 컴포넌트가 작게 나눠져 있기 때문에 디버깅도 쉽고 교체도 쉬움 == 시스템 관리 비용 낮춤
  • 컴포넌트간 결합도가 낮기 때문에 특정 컴포넌트를 최적화하기 쉬움 == 성능 최적화에 도움
  • 외부 의존성이 적은 컴포넌트 == 소프트웨어 재사용성 향상
  • 단위 테스트가 쉬워짐 == 큰 시스템을 제작하는 난이도 하락

 

정보 은닉 기본원칙

 

  • private - 멤버를 선언한 톱레벨 클래스에서만 접근 가능.
  • package-private - 해당 패키지 안의 모든 클래스에서 접근 가능.
    (default 접근 수준, 인터페이스는 public이 default)
  • protected - private-package의 범위를 포함하고, 해당 클래스의 하위 클래스에서도 접근 가능.
  • public - 모든 곳에서 접근 가능.

모든 클래스와 멤버의 접근성을 가능한 한 좁힌다 (public 지양)
접근성 : 요소가 선언된 위치 + 접근 제한자 (private, protected, public)

 

그냥 모든 클래스와 멤버의 접근성을 가능한 한 최대로 좁힌다.
항상 제일 낮은 수준의 접근성을 부여해야 한다는 것이다.

소프트웨어가 동작하기만 하면 된다.

 

 

톱레벨 클래스와 인터페이스

 

public으로 선언할 경우

  • 클래스가 공개 API가 됨.
  • 클라이언트와의 호환을 위해 릴리즈마다 계속해서 관리를 해줘야 함.

package-private 으로 선언할 경우

  • 클래스가 내부 구현이 됨.
  • 릴리즈마다 클라이언트에 상관없이 언제든 수정, 교체, 제거가 가능.

API로 쓸거면 public, 그게 아니면 package-private로 선언하면 된다.

 

 

한 클래스에서만 사용하는 package-private 클래스나 인터페이스의 경우

 

패키지 내에서 클래스나 인터페이스의 사용처가 단 한곳일 때는, 그 사용처에서 private static으로 구현한다.

 

 

멤버 (필드, 메서드, 중첩 클래스, 중첩 인터페이스)

 

멤버에 부여할 수 있는 접근 수준은 private, package-private, protected, public로 총 네가지이다.

멤버는 접근 수준 종류가 많아 구별하기 어려워 보일수 있지만 단순하다.

 

  1. 일단 모든 멤버를 private으로 만든다.
  2. 오직 같은 패키지의 다른 클래스가 접근해야 하는 멤버에 한해 package-private으로 풀어준다.


만약 위와 같은 과정에서 권한을 풀어주는 일이 많다면, 컴포넌트를 더 분해해야 하는 것이 아닌가 고민해봐야 한다.

 

 

protected는 적을수록 좋다.

 

protected는 package-private에 비해 접근 가능 범위가 엄청나게 넓다.

public 클래스의 protected 멤버는 공개 API이기 때문에 영원히 지원되어야 한다.

그래서 protected는 적을수록 좋다.

 

 

상위 클래스를 재정의하는 경우

 

 

앞에서 멤버의 접근성을 최대한 작게 하라고 말한 바가 있다.

그런데 인터페이스를 구현하는 클래스의 경우엔 불가능하다.

 

상위 클래스의 인스턴스는 하위 클래스의 인스턴스로 대체하여 사용할 수 있어야 한다. (리스코프 치환 규칙)

실제로 이를 어기면 하위 클래스를 컴파일 할 때 오류가 난다.

 

그렇기에 하위 클래스는 상위 클래스에서보다 접근 수준을 좁게 설정할 수 없다.

클래스가 인터페이스를 구현할 경우, 인터페이스가 정의한 모든 메서드들을 public으로 선언해야만 한다.

 

 

 

코드를 테스트 해야할 경우

 

 

코드를 테스트 하기 위해 클래스, 인터페이스, 멤버의 접근 범위를 넓히려 할 때가 있다.

하지만 애초에 테스트 코드를 대상과 같은 패키지에 두면 굳이 그럴 이유가 없어진다. (package-private에 접근이 가능하므로)

 

 

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

 

 

만약 가변 객체를 참조하거나, final이 아닌 인스턴스를 public으로 선언하면, 그 필드에 담을 수 있는 값을 제한할 힘을 잃게 된다.

그 필드와 관련된 모든 것은 불변식을 보장할 수 없다는 의미이다.

 

또한, 필드가 수정될 경우 다른 작업을 할 수 없게 되므로 public 가변 필드를 갖는 클래스는 일반적으로 스레드가 안전하지 않다.

 

문제는 필드를 final로 바꾸고 불변 객체를 참조하더라도 public이면 여전히 문제가 있다는 것이다.

 

 

정적 필드의 경우

 

 

정적 필드는 앞서 인스턴스 필드에서 말했던 것과 같은 문제점을 가지게 된다.

다만 예외가 하나 있다.

 

해당 클래스가 표현하는 추상 개념을 완성하는 데 꼭 필요한 구성요소이고,
상수라면 public static final 정도로 공개해도 된다.

이런 필드는 반드시 기본 타입 값이나 불변 객체를 참조해야 한다.

만약 가변 객체를 참조한다면 앞서 말한 불이익이 모두 다시 적용된다.

 

 

길이가 0이 아닌 배열

 

 

길이가 0이 아닌 배열은 모두 변경 가능하니 주의해야 한다.

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

이를 어기면, 클라이언트에서 그 배열의 내용을 수정할 수 있게 된다.

 

안 좋은 예시

public static final Thing[] VALUES = { ... };

 

 

해결 방법 1

private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES = 
  	Collections.unmodifiableList(Array.asList(PRIVATE_VALUES);

위처럼 public 이였던 배열을 private로 변경한다.
그리고 public 불변 리스트를 추가한다.

 

해결 방법 2

private static final Thing[] PRIVATE_VALUES = { ... };
public static final Thing[] values() {
  return PRIVATE_VALUES.clone();

마찬가지로 public이였던 배열을 private으로 변경한다.
그리고 그 복사본을 반환하는 public 메서드를 하나 추가해준다. (방어적 복사)

클라이언트가 무엇을 원하냐에 따라 둘 중 하나를 선택해주면 된다.

 

 

 

암묵적 접근 수준

 

자바 9에서부터는 모듈 시스템이라는 개념이 도입되었다.
이때 두 가지의 암묵적 접근 수준이 추가되었다.

패키지가 클래스들의 묶음이라면, 모듈은 패키지들의 묶음이다.

모듈은 그 안에 속하는 패키지들 중, 공개할 것들을 명시한다. (module-info.java)
이때 공개하지 않은 패키지들은 public이던 protected던 외부에서 접근할 수가 없다.

이를 이용하여 클래스를 외부에 공개하지 않으면서 같은 모듈안에 패키지들 끼리는 자유롭게 공유가 가능하다.

 

암묵적 접근 수준이란, public이나 protected이지만 그것이 모듈 내로 한정되는 것을 말한다.

 

하지만 보통 이런 형태가 필요한 경우는 흔하지 않고,
그런 경우가 있더라도 패키지들 사이에서 클래스를 재배치하면 해결된다.

 

새롭게 등장한 모듈의 접근 수준을 적극 활용한 예시가 바로 JDK 그 자체이다.

자바 라이브러리에서 공개하지 않은 패키지들은 해당 모듈 밖에서는 절대로 접근할 수가 없다.

 

접근 보호 방식이 추가된 것 외에도 모듈 자체를 제대로 사용하려면 할 일이 많다.

 

결론은, 아직 모듈 자체를 사용하기에는 조금 이른 감이 있으므로 당분간은 사용하지 않는게 좋다.

 

 

 

Item15 정리

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

Comparable을 구현할지 고려하라

 

Comparable의 compareTo는 Object의 equals와 동일하지만 2가지 다른점이 존재한다.

  1. 동시성 비교 + 순서 비교
  2. 제네릭 형태
public static void main(String[] args)
{
    Set<String> s = new TreeSet<>(); //TreeSet (순서 정렬 + 중복제거)
    Collections.addAll(s, args);
    System.out.println(s);
}

 

Comparable을 구현한 객체는 순서가 존재함으로 Arrays.sort로 쉽게 정렬할 수 있다.

String이 Comparatable을 구현되어있으므로 가능한 코드이다.

순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현하자.

 

CompareTo메서드 규약

  1. 두 객체의 참조의 순서를 바꿔 비교해도 예상한 결과가 나와야 한다.
  2. 첫 번째가 두 번째보다 크고 두 번째가 세 번째보다 크면, 첫 번째는 세 번째보다 커야 한다
  3. 크기가 같은 객체들끼리는 어떤 객체와 비교하더라도 항상 같아야 한다.
  4. (필수X) compareTo 메서드로 수행한 동치성 테스트 결과가 equals의 결과와 같아야 한다.
    • 지키지 않을 경우, 컬렉션이 구현한 인터페이스 (Collection, Set, Map 등) 에 정의된 동작과 다르게 작동할 수 있다.
    • 정렬된 컬렉션들은 동치성을 비교할 때 equals 대신 compareTo를 사용하기 때문이다.

compareTo 메서드 작성 요령

  • Comparable은 제네릭 인터페이스이므로 타입을 확인하거나 형변환할 필요 없다.
  • 객체 참조 필드를 비교하려면 compareTo 메서드를 재귀적으로 호출이 필요하다.
  • Comparable을 구현하지 않은 필드나 표준이 아닌 순서로 비교해야 한다면 Comparator를 대신 사용한다.
  • 정수 기본 타입, 실수 기본 타입을 비교할 때는 박싱된 기본 타입 클래스들에 새로 추가된 정적 메서드인 compare를 이용한다.
  • 클래스에 핵심 필드가 여러 개라면 가장 핵심적인 필드부터 비교한다.

Comparator

자바 8에서는 Comparator 인터페이스가 비교자 생성 메서드를 제공하여 메서드 연쇄 방식으로 비교자를 생성할 수 있다.

이를 활용하여 Comparable 인터페이스의 compareTo 메서드를 구현 가능하다.

728x90
728x90

clone 재정의는 주의해서 진행하라

clone이란

인스턴스를 복제하는 메서드이다.

이 메서드를 사용하려면 반드시 Cloneable 인터페이스를 implements 해야한다.

 

Cloneable 인터페이스는 Object protected 메소드인 clone의 동작 방식을 결정한다. 

Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환하며, 그렇지 않은 클래스의 인스턴스에서 호출하면 CloneNotSupportedException(언체크 예외)을 던진다.

 

 

clone 메소드의 허술한 일반 규약

x.clone() != x

불변 객체라면 복제한 객체와 원본 객체가 서로 다른 객체이기 때문에 이다. 

가변 객체이지만 clone() 메소드를 오버라이딩하여 복제 시에도 같은 참조를 공유하도록 구현한 경우에는 거짓이 된다.

 

x.clone().getClass() == x.getClass()

clone 메서드가 원본 객체(x)와 동일한 클래스 타입의 객체를 반환해야 한다.

 

x.clone().equals(x)

복제된 객체와 원본 객체가 같은 내용을 가지고 있어도 참조가 다를 수 있기 때문에 거짓이 될 수 있다.

 

super.clone 대신 생성자를 호출할 때 문제

public class Person implements Cloneable 
{
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // clone() 메소드 오버라이딩
    @Override
    public Object clone() throws CloneNotSupportedException {
        return new Person(this.name, this.age);  // 생성자를 호출하여 객체를 생성
    }

    // getter, setter 생략
}



public class Student extends Person 
{
    private String school;

    public Student(String name, int age, String school) {
        super(name, age);
        this.school = school;
    }

    // clone() 메소드 오버라이딩
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();  // 오류 발생!
    }

    // getter, setter 생략
}

 

위 코드는 Student 클래스에서 super.clone()메소드를 호출하면 Person 클래스의 생성자를 호출해서 객체를 생성한다.

이 경우 복제된 객체는 Person 클래스의 객체이다.

Student 클래스의 객체가 아니므로 Student클래스에서 추가한 필드나 메소드는 복제되지 않는다.

 

 

 

불변 클래스는 clone 메소드를 제공하지 않는 것이 좋다.

 

복제를 통한 객체의 변경이 필요하지 않고, 불변 클래스의 특성에 어긋나는 결과를 가져올 수 있기 때문에 제공하지 않는 것이 좋다.

 

CloneNotSupportedException 예외 전환

@Override
public Object clone()  
{
    try
    {
        return super.clone();
    }
    catch (CloneNotSupportedException e)
    {
        throw new AssertionError();
    }
}

CloneNotSupportedException 예외가 발생하는 경우는 Cloneable 인터페이스를 implements 하지 않은 상태에서 clone 메서드를 호출할때 뿐이다.

사실 개발자 입장에서 clone 메서드를 사용한다면 Cloneable 인터페이스를 implements 하는 것을 알고있기에 CloneNotSupportedException 예외 상황은 발생 불가능한 예외라고 할 수 있다.

그런데 이 예외는 Checked Exception이며 예외처리를 필수로 해야한다.

발생 불가능한 예외에 대해 예외처리를 하는거다.

예외 처리를 강제하지 않도록 예상치 못한 상황이 발생했음을 나타내는 에러인 AssertionError를 사용하여 예외를 변환해주자.

 

형변환은 clone 메서드 내에서 한다.

가변 객체를 참조하는 클래스에 대해서는 clone을 사용하면 안된다.

public class Stack implements Cloneable
{
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack()
    {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e)
    {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop()
    {
        if(size == 0)
        {
            throw new EmptyStackException();
        }
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }

    private void ensureCapacity()
    {
        if(elements.length == size){
            elements = Arrays.copyOf(elements, 2* size + 1);
        }
    }

    @Override
    protected Stack clone() 
    {
        try
        {
            return (Stack) super.clone();
        }
        catch (CloneNotSupportedException e)
        {
            throw new AssertionError();
        }
    }
}

아래 Stack 클래스의 경우 elements라는 가변객체를 참조하고 있다.

Stack에 대해 clone을 하게 된다면, 복제된 인스턴스 내의 elements는 기존 elements와 동일한 주소를 갖는 elements를 참조할 것이다.

둘 중 하나의 인스턴스를 수정하면 다른 하나도 수정되어 프로그램이 이상하게 동작할 수 있다.

 

 

 

이렇듯 clone은 주의사항이 많다.하지만 객체를 복사하는 방법이 clone 메서드만 있는것이 아니다.

 

 

 

 

복사 생성자와 복사 팩터리

public Stack(Stack stack)
{
    this.elements = stack.elements.clone();
    this.size = stack.size;
}

// 복사 팩터리 메서드
public static Stack newInstance(Stack stack)
{
    return new Stack(stack);
}

이 방법을 사용하면

Clonable 인터페이스를 구현하지 않아도 되고,

clone 메서드를 재정의하지 않아도 되고, 

불필요한 예외처리를 할 필요가 없고,

형변환 할 필요도 없다.

 

배열 타입의 가변객체의 경우 똑같이 clone을 사용하곤 있지만, 앞서 언급한 여러 방면을 비교해봤을 때 clone보다 복사 생성자와 팩터리 메서드를 사용하는 방식이 이점이 많다.

728x90
728x90

toString을 항상 재정의하라

public static void main(String[] args) 
{
    Human human = new Human("대희", 20);
    System.out.println("human = " + human);
}

위 코드의 결과

human = item12.Human@7e0ea639

Process finished with exit code 0

딱 봐도 이런 결과를 바란게 아니다.

 

toString을 재정의 해야하는 이유

  • toString을 재정의함으로써 인스턴스 자체를 참조할때 인스턴스 멤버에 대한 정보를 표시할 수 있기 때문이다.
  • toString을 재정의하지 않으면 인스턴스를 출력시 클래스_이름@16진수로_표시한_해시코드를 반환할 뿐이다.
  • toString을 재정의한 클래스를 사용하는 시스템은 디버깅이 쉽다.
  • 인스턴스를 참조하는 컴포넌트가 오류 메시지를 로깅(logging)할때 자동으로 호출하는데 이때 메시지에 유용한 로그를 남길 수 있다.

toString을 재정의 할 때는 객체 스스로를 완벽히 설명하는 문자열이어야 한다.

객체가 가진 주요 정보를 모두 반환하는게 좋다.

 

String name;
int age;
String country;

/*
사람의 정보를 반환한다.
이름-나이-국적 형태로 반환된다.
*/
@Override
public String toString() {...}

 

핵심

모든 구체 클래스에서 Object의 toString을 재정의하자. 

상위 클래스에서 이미 알맞게 재정의한 경우는 예외다.

toString을 재정의한 클래스는 사용하기도 즐겁고 그 클래스를 사용한 시스템을 디버깅하기 쉽게 해준다.

toString은 해당 객체에 관한 명확하고 유용한 정보를 읽기 좋은 형태로 반환해야 한다

728x90

+ Recent posts