[교재 EffectiveJava] 아이템 15. 클래스와 멤버의 접근 권한을 최소화하라

반응형
728x90
반응형

정보은닉

컴포넌트를 설계를 잘하기 위해서는 클래스 내부 데이터와 내부 구현 정보를 외부 컴포넌트로부터 잘 숨겨야한다. 잘 설계된 컴포넌트는 모든 내부 구현을 완벽히 숨겨, 구현과 API를 깔끔히 분리한다. 오직 API 를 통해서만 다른 컴포넌트와 소통하며 서로의 내부 동작에는 전혀 개의치 않는다. 이것을 정보은닉 또는 캡슐화라고 한다.

 

 

정보은닉의 장점

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

인터페이스를 개발하면 이에 맞춰서 개발하고, 구현하면된다. 인터페이스를 사용하는 쪽과 지원하는 쪽이 동시에 개발이 가능하다.

2. 시스템 관리 비용을 낮춘다. 각 컴포넌트를 더 빨리 파악하여 디버깅할 수 있다. 다른 컴포넌트로 교체하는 부담도 적다.

인터페이스 기준으로 캡슐화가 잘 되어있다면, 코드 파악이 쉽다. 인터페이서 위주로 코드를 살펴보면 훨씬 빠르게 파악이 가능하여 디버깅도 편하고 수정 개발도 부담이 적다.

3. 정보 은닉 자체가 성능을 높여주지는 않지만, 성능 최적화에 도움을 준다. 다른 컴포넌트에 영향 없이 해당 컴포넌트만 최적화할 수 있다.

캡슐화가 잘 되어있으면 성능의 병목화 부분을 빠르게 캐치할 수 있다.

4. 소프트웨어 재사용성을 높인다. 독자적으로 동작할 수 있는 컴포넌트라면 어느 곳에서도 유용하게 쓰일 확률이 높다.

어떤 컴포넌트가 다른 프로젝트에도 사용될 수 있는 모듈이라면 해당 모듈을 그대로 가져다 사용 가능하다.

5. 큰 시스템을 제작하는 난이도를 낮춰준다. 시스템 전체가 아직 완성되지 않은 상태에서도 개별 컴포넌트의 동작을 검증할 수 있다.

쪼개서 한 모듈씩 파악하여 큰 시스템을 쉽게 파악할 수 있다.

 

 

접근제한의 필요성

모든 클래스와 멤버의 접근성을 가능한 한 좁혀야한다. 가장 바깥의 클래스인 톱레벨 클래스와 인터페이스에 부여할 수 있는 접근 수준은 pacakage-private, public 두가지다. public 으로 선언된 클래스나 인터페이스는 공개 API가 되고 package-private(default) 로 선언하면 해당 패키지 안에서만 사용 가능하다. 패키지 외부에서 사용할 이유가 없다면 package-private(default)로 선언하자. 그러면 이들은 내부 구현이 되어, 언제든 수정할 수 있게된다. 이전에 얘기했던 정보은닉의 장점을 누릴 수 있다.

 

public 선언된 클래스들은 API가 되므로 하위 호환을 위해 영원히 관리해줘야만 한다. public API는 어디에나 노출이 되어있기 때문이다. 예를들어 한번 공개된 API도 수정을 해야한다면 버전 관리가 필요하다.

 

그리고 public 일 필요가 없는 클래스의 접근 수준을 package-private 톱레벨 클래스로 좁히는 일이 중요하다. 톱래벨 클래스는 public, package-private만 선언 가능하다.

public class Test {}
class Test {}

 public 클래스는 그 패키지의 API인 반면, package-private 톱레벨 클래스는 내부 구현에 속하기 때문이다. 

 

 

중첩클래스

한 클래스에서만 사용하는 package-private 톱레벨 클래스나 인터페이스는 이를 사용하는 클래스 안에 private stiatic 으로 중첩시키자. 중첩된 클래스는 바깥 클래스 하나에서만 접근이 가능하다.

class DefaultMemberService implements MemberService {

    private String name;

    private static class PrivateStaticClass {

    }
}

▶ 왜 private static 클래스일까?

inner class는 외부 클래스를 항상 참조한다. static class는 inner class지만, 외부 클래스에 대한 참조가 없다.

class DefaultMemberService implements MemberService {

    private String name;

    private static class PrivateStaticClass {

    }

    /**
     * private 내부 클래스는 외부 클래스의 멤버 변수에 바로 접근할 수 있음
     * 내부적으로 외부 클래스 객체의 참조를 가지고 있기 때문에
     */
    private class PrivateClass {
        void doPrint() {
            System.out.println(name);
        }
    }

    /**
     * 실행시켜보면 PrivateClass 내부에 외부 클래스 참조가 있는 것을 확인할 수 있음
     */
    public static void main(String[] args) {
        Arrays.stream(PrivateClass.class.getDeclaredFields()).forEach(System.out::println);
    }

}

 

 

접근제한자

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

접근제한자 설명
private 멤버를 선언한 톱레벨 클래스에서만 접근할 수 있다.
package-private 멤버가 소속된 패키지 안의 모든 클래스에서 접근할 수 있다.
접근제한자를 명시하지 않았을때 기본적으로 적용된다.
(단, 인터페이스의 멤버는 기본적으로 public이 적용된다.)
protected package-private의 접근 범위를 포함하며, 이 멤버를 선언한 클래스의 하위 클래스에서도 접근할 수 있다.
public 모든 곳에서 접근할 수 있다.

private와 package-private 멤버는 모두 해당 클래스의 구현에 해당하므로 보통은 공개 API에 영향을 주지 않는다. 단, Serializable 을 구현한 클래스에서는 그 필드들도 의도치않게 공개 API가 될수도 있다. public 클래스에서는 멤버의 접근 수준을 package-private 에서 protected 로 바꾸는 순간 그 멤버에 접근할 수 있는 대상 범위가 엄청나게 넓어짐과 동시에 영원히 지원되어야한다. 따라서 protected 멤버의 수는 적을수록 좋다.

 

 

규칙

멤버 접근성을 좁히지 못하게 방해하는 제약이 있다. 상위 클래스의 메서드를 재정의할때는 그 접근 수준을 상위 클래스에서보다 좁게 설정할 수가 없다. 이 제약은 상위 클래스의 인스턴스는 하위 클래스의 인스턴스로 대체해 사용할 수 있어야 한다는 규칙 때문에 필요하다. (리스코프 치환 원칙)

 

클래스가 인터페이스를 구현하는건 이 규칙의 특별한 예로 볼수 있고, 이때 클래스는 인터페이스가 정의한 모든 메서드를 public 으로 선언해야한다.

 

 

접근범위 넓히기

코드를 테스트하려는 목적으로 클래스, 인터페이스, 멤버의 접근 범위를 넓히려고 한다면 적당한 수준까지만 넓혀라. 예를들어 클래스의 private 멤버를 package-private 까지 풀어주는것은 허용될 수 있지만 그 이상은 안된다. 그 이상을 수행하기 보다는 테스트 코드를 테스트 대상과 같은 패키지에 둬서 접근할 수 있게 해라.

 

 

public 필드 선언하지 않기

public 클래스의 인스턴스 필드는 되도록 public이 아니여야한다. public 으로 선언된다면 그 필드에 담을 수 있는 값을 제한할 힘을 잃게된다. 필드의 불변성을 보장할 수 없게된다. public 가변 필드를 갖는 클래스는 일반적으로 스레드 안전하지 않다. 심지어 필드가 final 이면서 불변 객체를 참조하더라도 문제는 여전하다. 내부 구현을 바꾸고 싶어도 그 public 필드를 없애는 방식으로는 리팩터링이 불가능하다. 

 

만약 꼭 필요한 구성요소로써의 상수라면 public static final 필드로는 선언해도 괜찮다.

 

 

배열의 접근제한

예외적으로 길이가 0이 아닌 배열은 모두 변경이 가능하다. 클래스에서 public static final 배열 필드를 두거나 이 필드를 반환하는 접근자 메서드를 제공하면 안된다. 이런 필드나 접근자를 제공한다면 클라이언트에서 그 배열의 내용을 수정할 수 있다.

 

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

 

위 문제의 해결방안은 2가지가 있다.

1. 앞 코드의 public 배열을 private 로 만들고 public 불변 리스트를 추가한다.

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

 

2. 배열을 private 로 만들고 그 복사본을 반환하는 public 메서드를 추가한다.

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

 

예제 바로가기

https://devfunny.tistory.com/541

 

[교재 EffectiveJava] 아이템 15-예제. static final array 변경하지 못하도록 설정하기

길이가 0 이상일 경우 변경 가능한 public static final array public class ArrayTest { public static final int[] VALUES = {1, 2, 3, 4}; } 값을 변경해보자 public class Main { public static void main(Str..

devfunny.tistory.com

 

 

모듈 시스템

자바 9에서 추가된 모듈 시스템으로 인해 암묵적 접근 수준이 추가되었다. 모듈은 자신에 속하는 패키지 중 공개할 것들을 선언한다. public, protected 멤버라도 공개하지 않았다면 모듈 외부에서는 접근할 수 없다. 모듈 시스템을 활용하면 클래스를 외부에 공개하지 않으면서도 같은 모듈을 이루는 패키지 사이에서는 자유롭게 공유할 수 있다.

 

 

 

반응형

Designed by JB FACTORY