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

정보 은닉, 혹은 캡슐화라고 하는 이 개념은 소프트웨어 설계의 근간이 되는 원리다

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

1. 정보은닉

정보은닉의 장점

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

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

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

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

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

2. 접근지정자를 잘 이용해라

모든 클래스와 멤버의 접근성을 가능한 한 좁혀야 한다. 달리 말하면, 소프트웨어가 올바로 동작하는 한 항상 가장 낮은 접근 수준을 부여해야 한다는 뜻이다

1) 패키지 외부에서 쓸 이유가 없다면 package- private으로 선언하자

public일 필요가 없는 클래스의 접근 수준을 packageprivate 톱레벨 클래스로 좁히는 일이 가장 중요하다.

그러면 이들은 API가 아닌 내부 구현이 되어 언제든 수정할 수 있다. 즉, 클라이언트에 아무런 피해 없이 다음 릴리스에서 수정, 교체, 제거할 수 있다. 반면, public으로 선언한다면 API가 되므로 하위 호환을 위해 영원히 관리해줘야만 한다.

2) 한 클래스에서만 사용하는 package-private 톱레벨 클래스나 인터페이스는 이를 사용하는 클래스 안에 private static으로 중첩시켜보자

톱 레벨로 두면 같은 패키지의 모든 클래스가 접근할 수 있지만, private static으 로 중첩시키면 바깥 클래스 하나에서만 접근할 수 있다.

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

접근 범위 private < package-private < protected < public

  • private: 멤버를 선언한 톱레벨 클래스에서만 접근

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

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

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

3. 멤버 접근성을 좁히지 못하게 방해하는 제약

상위 클래스의 메서드를 재정의할 때는 그 접근 수준을 상위 클래스에서보다 좁게 설정 할 수 없다는 것이다

1) 리스코프 치환의 원칙

리스코프 치환 원칙은 OOP의 SOLID 원칙 중 하나로, 하위 클래스는 상위 클래스의 객체로서도 정상적으로 동작해야 한다는 원칙이다. 즉, 상위 클래스의 인스턴스가 있는 곳에서는 언제든 하위 클래스의 인스턴스를 대체할 수 있어야 한다는 의미이다.

이를 위해 하위 클래스는 상위 클래스의 메서드를 재정의할 때 상위 클래스의 메서드보다 더 좁은 접근 제어자(예: protectedprivate)를 사용할 수 없다.

2) 인터페이스 구현

클래스가 인터페이스를 구현할 때는 그 인터페이스에서 정의한 모든 메서드를 반드시 public으로 선언해야 한다. 왜냐하면 인터페이스는 기본적으로 공용 API를 정의하는 것이므로, 해당 메서드를 외부에서 접근할 수 있도록 public 접근 제어자를 설정해야 하기 때문이다.

interface MyInterface {
    void doSomething(); // 인터페이스에서는 기본적으로 public 메서드를 정의
}

class MyClass implements MyInterface {
    // 반드시 public으로 재정의해야 함
    @Override
    public void doSomething() {
        System.out.println("MyClass doing something");
    }
}

4. Public 가변 필드와 불변식 위반

1) Public 가변 필드와 불변식 위반시 스레드 안전성의 문제

가변 객체를 참조하는 필드나 final이 아닌 인스턴스 필드를 public으로 선언하면 발생하는 문제

public 클래스의 인스턴스 필드는 되도록 public이 아니어야 한다. . 필드를 public으로 노출할 경우 해당 필드에 어떤 값이 들어올지 제어할 수 없게 되며, 클래스가 의도한 불변식을 깨뜨릴 수 있다. 특히 스레드 안전성이 문제가 될 수 있는데, 필드가 수정될 때 필요한 작업(락 획득 등)을 처리할 수 없기 때문이다.

심지어 필드가 final이라 하더라도, 불변 객체가 아닌 가변 객체를 참조한다면 여전히 문제가 남는다. 예를 들어 public static final 배열 필드는 참조는 불변이지만 배열의 내용은 변경될 수 있기 때문에, 클라이언트에서 배열의 값을 수정할 수 있는 문제가 발생할 수 있다.

public static final 필드의 개념

  • public static final 필드모든 객체에 대해 공유되고 변경 불가능한 필드이다.

    • public: 이 필드에 모든 클래스에서 접근할 수 있다.

    • static: 이 필드는 클래스 차원에서 존재하며, 객체마다 따로 생성되지 않는다.

    • final: 이 필드는 한 번 초기화된 후 절대 값이 변경되지 않는다.

2) 불변 상수의 예외적인 경우

기본적으로 public static final 필드를 사용하는 것은 가변 객체일 때 문제가 된다. 가변 객체는 상태를 바꿀 수 있는 객체를 말합니다. 예를 들어 배열, 리스트 같은 자료 구조는 가변 객체이다. 하지만 몇 가지 예외가 있다.

예외: 추상 개념을 완성하는 상수

상수는 흔히 불변 값으로, 클래스가 다루는 특정한 추상 개념을 나타낼 때는 public static final로 선언해도 문제가 없다.

public static final int MAX_VALUE = 100;

위와 같이 숫자 같은 기본 타입(Primitive Type)이나, 불변 객체(Immutable Object)를 참조할 때는 public static final을 사용해도 안전합니다. 즉, 숫자 값, 문자열 등은 변경되지 않으므로 안전하게 사용할 수 있다.

불변 객체란?

  • 불변 객체(Immutable Object)는 한 번 만들어지면 그 상태가 절대 변경되지 않는 객체이다. 예를 들어 String 객체는 불변 객체이다.

  • 이러한 불변 객체public static final로 선언해도 안전하다. 왜냐하면 객체의 상태가 절대 바뀌지 않기 때문이다.

3) 문제: 가변 객체를 참조하는 경우

만약 가변 객체(예: 배열, 리스트, 사용자 정의 클래스)를 public static final로 선언하면 어떤 문제가 생길까?

public static final int[] NUMBERS = {1, 2, 3}; // 가변 객체 배열
  • 이 경우, 참조 자체는 final이기 때문에 NUMBERS 변수 자체를 다른 배열로 바꿀 수는 없지만, 배열의 내용은 수정할 수 있다.

  • 즉, 클라이언트(외부에서 이 필드에 접근하는 코드)가 NUMBERS[0] = 100; 같은 방식으로 배열의 값을 변경할 수 있다는 뜻이다. 이로 인해 예기치 않은 문제가 발생할 수 있다.

4) 해결책

  1. 불변 리스트로 변환: 배열을 private으로 만들고 불변 리스트를 제공하는 방법

private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES = 
    Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
  1. 방어적 복사(Defensive Copy): 배열을 private으로 만든 후 복사본을 반환하는 메서드를 제공한다.

private static final Thing[] PRIVATE_VALUES = { ... };

public static final Thing[] values() {
    return PRIVATE_VALUES.clone(); // 방어적 복사
}

5. 자바 9 모듈 시스템과 암묵적 접근 수준과 사용 시 유의사항

바 9에서 모듈 시스템이 도입되면서 암묵적 접근 수준이 추가되었다. 모듈은 패키지들의 묶음이며, 모듈 시스템을 통해 모듈 외부에서 접근 가능한 패키지를 제한할 수 있다. 모듈 내부의 public이나 protected 멤버는 모듈 외부에서는 접근할 수 없지만, 같은 모듈 내에서는 여전히 접근이 가능하다. 이러한 모듈 시스템을 적절히 활용하면 클래스를 외부에 공개하지 않으면서도 모듈 내부에서는 자유롭게 공유할 수 있다.

모듈 시스템은 강력하지만, 현재는 모듈 개념이 널리 받아들여지지 않았으므로 꼭 필요한 경우가 아니라면 당분간은 사용하지 않는 것이 좋다. 특히 JAR 파일을 클래스패스에 두면 모듈 시스템의 보호가 깨질 수 있다.

예시 코드: 모듈 시스템

모듈 시스템을 이용하여 패키지 사이의 접근을 제어하는 예시이다. module-info.java 파일에 모듈이 공개할 패키지를 정의한다.

module com.example.module {
    exports com.example.package;  // 외부에 공개할 패키지
}

모듈 외부에서는 com.example.package 패키지에 접근할 수 있지만, 다른 패키지는 접근이 차단된다. 모듈을 적용하면 보다 명확하게 접근 제어를 할 수 있으나, 아직까지는 완전히 널리 사용되지 않았다.

정리

프로그램 요소의 접근성은 가능한 한 최소한으로 하라.

  • 꼭 필요한 것만 골라 최소한의 public API를 설계하자.

  • 그 외에는 클래스, 인터페이스, 멤버가 의도치 않게 API로 공개 되는 일이 없도록 해야 한다. public 클래스는 상수용 public static final 필드 외에는 어떠한 public 필드도 가져서는 안 된다.

  • public static final 필드가 참조하는 객체불변인지 확인하라.

Last updated