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

김동훈·2023년 6월 2일
1

Effective-java

목록 보기
5/14
post-thumbnail

이번 item15는 정보 은닉을 올바르게 구현하기 위해 접근 제한자를 제대로 활용하는 방법에 대해 이야기 하고있다. 책에 적힌 모든 글자를 받아들이지는 못했지만, 이는 나중에 추가할 예정이고 이번 게시글에서는 이해한 내용을 바탕으로 작성할 것 이다.


접근 제한자를 제대로 활용하는 것이 정보 은닉의 핵심이다.
이에 대한 원칙은 간단하게는 모든 클래스와 멤버의 접근성을 가능한 한 좁혀야 한다.이다. 이 책에서 뿐만 아니라 평상시에도 익히 들어온 말이다.

먼저 접근 제한자 4가지에 대해 알아보자.

private

멤버를 선언한 톱레벨 클래스에서만 접근할 수 있다.

package-private

멤버가 소속된 패키지 안의 모든 클래스에서 접근할 수 있다. 접근 제한자를 명시하지 않았을 때 적용되는 패키지 접근 수준이다.(단, 인터페이스의 멤버는 기본적으로 public이다.)

protected

package-private의 접근 범위를 포함하며, 이 멤버를 선언한 클래스의 하위 클래스에서도 접근할 수 있다.

public

모든 곳에서 접근할 수 있다.

책에서 중요한 말을 해주는데 바로 권한을 풀어주는 일을 자주 하게 된다면 여러분 시스템에서 컴포넌트를 더 분해해야 하는 것은 아닌지 다시 고민해보자. 이다. 이후에 말할 내용과도 관련이 있는 말이기도하다.

이 책의 p.98을 보면 public클래스의 protected멤버는 공개API 라는 말이 나온다. 위에서 말한 protected의 접근 수준에 대한 설명을 보면 외부 패키지에서 접근이 불가능함을 알 수 있다. 공개 API라 함은 public과 같이 어디서나 접근 가능한것 아닌가?

그런데 왜 공개 API라고 하는걸까?

왜 public클래스의 protected멤버는 공개 API인걸까?

우선 내 생각은 이러하다.
결론부터 말하자면, public class를 상속받고 있는 어떤 class가 있다면, 이 클래스의 인스턴스를 통해 상위클래스의 protected멤버에 접근이 가능하기 때문이다.
그리고, 상위 클래스가 public class이기 때문에 외부 패키지에서도 인스턴스 생성이 가능하다. 따라서 외부 패키지에서 생성된 인스턴스를 통해 protected멤버에 접근이 가능해지므로 이를 보고 공개 API라고 말하지 않았나 싶다.
간단한 예시 코드를 통해 이를 확인해보자.

package item15;

public class item15 {
    public int publicX=1;
    private int privateY=2;
    int defaultZ=3;
    protected int protectW=4;
    
    protected  void star(){
        System.out.println(toString());
        System.out.println("star");
    }
    
    @Override
    public String toString() {
        return "item15{" +
                "publicX=" + publicX +
                ", privateY=" + privateY +
                ", defaultZ=" + defaultZ +
                ", protectW=" + protectW +
                '}';
    }

}

public class인 item15가 있을 때, 이를 상속받고있는 클래스를 구현해보자.

package item15test;

import item15.item15;

public class main extends item15{
    
    public static void main(String[] args) {
    	item15 item15 = new main();
        item15.star(); // 불가능
        main item151 = (main) item15;
        item151.star();

        main main = new main();
        main.star();
    }
}

여기서 주의해야 할 부분은 main 인스턴스를 생성하는 방법이다. main의 인스턴스를 생성할 때 main main = new main();처럼 인스턴스를 생성하면 당연히 protected인 star()메소드를 호출 할 수 있다. 하지만 item15 item15 = new main(); 과 같이 인스턴스를 생성할 때는 불가능하다. 왜 안될까? 하고 고민해보았는데, 이 내용은 좀 더 탐구해보고 삽질기에 써보도록 하겠다.

간단하게 생각해보면, main main = new main();으로 생성된 인스턴스는 상속받은 클래스인 main의 인스턴스이다. 위에서 protected 접근 제한자의 정의를 보면 하위 클래스에서 접근 가능하므로 당연하게도 가능할 것 이다. 하지만 item15 item15 = new main();으로 생성된 인스턴스는 그 타입이 item15이다. 결국 이 인스턴스는 하위 클래스인 main의 인스턴스이긴 하지만 item15타입이기 때문에, 묵시적 형변환을 통해 그냥 item15의 인스턴스랑 별 다를바 없을 것 이다. 그러므로 당연하게도 외부 패키지에서 생성된 item15의 인스턴스 이니, protected멤버에 접근이 불가능한 것이다.

다시 왜 공개 API라고 하는걸까?질문으로 돌아가면 내가 생각해본대로 실제로 외부 패키지에서 public클래스를 상속받고 있는 class는 상위 클래스의 인스턴스를 통해 문제없이 protected 멤버에 접근하고있다. 따라서 이와 같은 방법을 사용한다면 외부 패키지라도 충분히 접근 가능하므로 공개 API라고 볼 수 있을 것이다.


public 클래스의 인스턴스 필드는 되도록 public이면 안된다.
public이라면 다른 곳에서 아무렇게나 접근하여 해당 필드를 수정할 수 있게된다. 이는 그 필드와 관련된 모든 것은 불변식을 보장할 수 없게 되는 것이다.

그럼 final로 재할당을 막아버리면 해결이 되지 않을까?

이전 item에서도 보았듯이 아쉽게도 안된다.. 예를 들자면 public final로 선언된 배열이 있다. 이 배열에 대한 접근자 메서드를 통해 다시 재할당하는것은 안되겠지만, 그 내부는 충분히 변경할 수 있다. 다음 예시 코드를 보자.

public class PublicFinalArray {
    public final int array[];

    public PublicFinalArray(int[] array) {
        this.array = array;
    }

    public int[] getArray() {
        return this.array;
    }
}

아래 코드를 통해 PublicFinalArray의 내부 배열 필드를 수정할 수 있는가 테스트해보자.

        int [] array = {1,2,3,4,5};
        PublicFinalArray pfArray = new PublicFinalArray(array);
        System.out.println(pfArray);
        int[] newArray = new int[5]     ;
        pfArray.array = newArray; // final키워드로 인해 재할당 불가.
        int[] get = pfArray.getArray();
        get[0] = -1;
        get[1] = -2;
        get[2] = -3;
        get[3] = -4;
        get[4] = -5;
        System.out.println(pfArray);

이를 실행하면 수정되는것을 볼 수 있다.


이렇게 final로 재할당은 막았지만, 배열의 내부 요소들에 대한 수정은 막을 수 없다. 이 필드를 private final로 바꿔보아도 getArray()와 같은 접근자 메서드를 제공한다면 의미없다. 따라서 이 책에서는 클래스에서 public static final 배열 필드를 두거나 이 필드를 반환하는 접근자 메서드를 제공해서는 안된다.라고 말해주고 있다.

해결책으로 2가지를 알려주고있다.

불변 리스트 사용

private static final int[] PRIVATE_ARRAYS = { ... };
public static final List<Integer> ARRAYS = Collections.unmodifiableList(Arrays.asList(PRIVATE_ARRAY));

방어적 복사

private static final int[] PRIVATE_ARRAYS = { ... };
public static final int[] valuse() { return PRIVATE_ARRAYS.clone(); }

이후에는 모듈 시스템 을 말하고 있다. 맨 처음에 받아들이지 못한 글자가 바로 이에대한 것이다. 이 내용은 앞으로 공부해나가면서 차근차근 정리해서 수정해보도록 하겠다.

결론

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

effective-java스터디에서 공유하고 있는 전체 item에 대한 정리글

profile
董訓은 영어로 mentor

0개의 댓글