{% hint style="info" %} 정보 은닉, 혹은 캡슐화라고 하는 이 개념은 소프트웨어 설계의 근간이 되는 원리다 {% endhint %}
어설프게 설계된 컴포넌트와 잘 설계된 컴포넌트의 가장 큰 차이는 바로 클래스 내부 데이터와 내부 구현 정보를 외부 컴포넌트로부터 얼마나 잘 숨겼느냐다.
- 여러 컴포넌트를
병렬
로 개발할 수 있기 때문에시스템 개발 속도를 높인다 - 각 컴포넌트를 더 빨리 파악하여 디버깅할 수 있고, 다른 컴포넌트로 교체하는 부담도 적기 때문에 시스템 관리 비용을 낮춘다.
- 정보 은닉 자체가 성능을 높여주지는 않지만, 성능 최적화에 도움을 준다. 완성된 시스템을 프로파일링해 최적화할 컴포넌트를 정한 다음, 다른 컴포넌트에 영향을 주지 않고 해당
컴포넌트만
최적화할 수 있기 때문 이다. - 소프트웨어 재사용성을 높인다.
- 큰 시스템을 제작하는 난이도를 낮춰준다. 시스템 전체가 아직 완성되지 않은 상태에서도 개별 컴포넌트의 동작을 검증할 수 있기 때문이다.
{% hint style="info" %} 모든 클래스와 멤버의 접근성을 가능한 한 좁혀야 한다. 달리 말하면, 소프트웨어가 올바로 동작하는 한 항상 가장 낮은 접근 수준을 부여해야 한다는 뜻이다 {% endhint %}
public일 필요가 없는 클래스의 접근 수준을 packageprivate 톱레벨 클래스로 좁히는 일이 가장 중요하다.
그러면 이들은 API가 아닌 내부 구현이 되어 언제든 수정할 수 있다. 즉, 클라이언트에 아무런 피해 없이 다음 릴리스에서 수정, 교체, 제거할 수 있다. 반면, public으로 선언
한다면 API가 되므로 하위 호환을 위해 영원히 관리해줘야만 한다.
톱 레벨로 두면 같은 패키지의 모든 클래스가 접근할 수 있지만, private static으 로 중첩시키면 바깥 클래스 하나에서만 접근할 수 있다.
접근 범위 private < package-private < protected < public
- private: 멤버를 선언한 톱레벨 클래스에서만 접근
- package-private: 멤버가 소속된 패키지 안의 모든 클래스에서 접근, 접근 제한자를 명시하지 않았을 때 적용되는 패키지 접근 수준이다.(단, 인터페이스의 멤버는 기본적으로 public이 적용된다).
- protected : package-private의 접근 범위를 포함하며, 이 멤버를 선언한 클래스의 하위 클래스에서도 접근할 수 있다.
- public: 모든 곳에서 접근할 수 있다.
{% hint style="warning" %} 상위 클래스의 메서드를 재정의할 때는 그 접근 수준을 상위 클래스에서보다 좁게 설정 할 수 없다는 것이다 {% endhint %}
리스코프 치환 원칙은 OOP의 SOLID 원칙 중 하나로, 하위 클래스는 상위 클래스의 객체로서도 정상적으로 동작해야 한다는 원칙이다. 즉, 상위 클래스의 인스턴스가 있는 곳에서는 언제든 하위 클래스의 인스턴스를 대체할 수 있어야 한다는 의미이다.
이를 위해 하위 클래스는 상위 클래스의 메서드를 재정의할 때 상위 클래스의 메서드보다 더 좁은 접근 제어자(예: protected
나 private
)를 사용할 수 없다.
클래스가 인터페이스를 구현할 때는 그 인터페이스에서 정의한 모든 메서드를 반드시 public
으로 선언해야 한다. 왜냐하면 인터페이스는 기본적으로 공용 API를 정의하는 것이므로, 해당 메서드를 외부에서 접근할 수 있도록 public
접근 제어자를 설정해야 하기 때문이다.
interface MyInterface {
void doSomething(); // 인터페이스에서는 기본적으로 public 메서드를 정의
}
class MyClass implements MyInterface {
// 반드시 public으로 재정의해야 함
@Override
public void doSomething() {
System.out.println("MyClass doing something");
}
}
가변 객체를 참조하는 필드나
final
이 아닌 인스턴스 필드를public
으로 선언하면 발생하는 문제
public 클래스의 인스턴스 필드는 되도록 public이 아니어야 한다. . 필드를 public
으로 노출할 경우 해당 필드에 어떤 값이 들어올지 제어할 수 없게 되며, 클래스가 의도한 불변식을 깨뜨릴 수 있다. 특히 스레드 안전성이 문제가 될 수 있는데, 필드가 수정될 때 필요한 작업(락 획득 등)을 처리할 수 없기 때문이다.
심지어 필드가 final
이라 하더라도, 불변 객체가 아닌 가변 객체
를 참조한다면 여전히 문제가 남는다. 예를 들어 public static final
배열 필드는 참조는 불변이지만 배열의 내용은 변경될 수 있기 때문에, 클라이언트에서 배열의 값을 수정할 수 있는 문제가 발생할 수 있다.
public static final
필드는 모든 객체에 대해 공유되고 변경 불가능한 필드이다.public
: 이 필드에 모든 클래스에서 접근할 수 있다.static
: 이 필드는 클래스 차원에서 존재하며, 객체마다 따로 생성되지 않는다.final
: 이 필드는 한 번 초기화된 후 절대 값이 변경되지 않는다.
기본적으로 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
로 선언해도 안전하다. 왜냐하면 객체의 상태가 절대 바뀌지 않기 때문이다.
만약 가변 객체(예: 배열, 리스트, 사용자 정의 클래스)를 public static final
로 선언하면 어떤 문제가 생길까?
public static final int[] NUMBERS = {1, 2, 3}; // 가변 객체 배열
- 이 경우, 참조 자체는
final
이기 때문에NUMBERS
변수 자체를 다른 배열로 바꿀 수는 없지만, 배열의 내용은 수정할 수 있다. - 즉, 클라이언트(외부에서 이 필드에 접근하는 코드)가
NUMBERS[0] = 100;
같은 방식으로 배열의 값을 변경할 수 있다는 뜻이다. 이로 인해 예기치 않은 문제가 발생할 수 있다.
- 불변 리스트로 변환: 배열을
private
으로 만들고 불변 리스트를 제공하는 방법
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES =
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
- 방어적 복사(Defensive Copy): 배열을
private
으로 만든 후 복사본을 반환하는 메서드를 제공한다.
private static final Thing[] PRIVATE_VALUES = { ... };
public static final Thing[] values() {
return PRIVATE_VALUES.clone(); // 방어적 복사
}
바 9에서 모듈 시스템이 도입되면서 암묵적 접근 수준이 추가되었다. 모듈은 패키지들의 묶음이며, 모듈 시스템을 통해 모듈 외부에서 접근 가능한 패키지를 제한할 수 있다. 모듈 내부의 public
이나 protected
멤버는 모듈 외부에서는 접근할 수 없지만, 같은 모듈 내에서는 여전히 접근이 가능하다. 이러한 모듈 시스템을 적절히 활용하면 클래스를 외부에 공개하지 않으면서도 모듈 내부에서는 자유롭게 공유할 수 있다.
{% hint style="danger" %} 모듈 시스템은 강력하지만, 현재는 모듈 개념이 널리 받아들여지지 않았으므로 꼭 필요한 경우가 아니라면 당분간은 사용하지 않는 것이 좋다. 특히 JAR 파일을 클래스패스에 두면 모듈 시스템의 보호가 깨질 수 있다. {% endhint %}
모듈 시스템을 이용하여 패키지 사이의 접근을 제어하는 예시이다. 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 필드가 참조하는 객체가
불변
인지 확인하라.