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

+ Recent posts