조슈아 블로크의 Effective Java3의 15번째 아이템은 정보 은닉(Information Hiding)에 관한 내용을 다룬다.
외부에서 알 필요 없는 내부 구현을 숨기고, 오직 필요한 API만 노출해라
기본적으로 은닉화는 무결성, 안정성, 보안적인 측면에서 이점이 있다.
이 책에서는 조금 더 자세한 장점들을 나열하였다.
이처럼 여러가지 장점이 있지만 정리하자면 은닉화로 인한 각각의 개별 컴포넌트가 독립적으로 이루어져 생기는 장점들로 이해하면 될 것 같다.
기본 원칙은 간단하다. 모든 클래스와 멤버의 접근성을 가능한 한 좁혀야 한다. 즉, 소프트웨어가 올바로 동작하는 한 항상 가장 낮은 접근 수준을 부여해야 한다.
최상위 클래스와 인터페이스는
public또는package-private으로 접근수준을 부여할 수 있는데,public으로 선언하면 공개 API가 되어 외부에서 사용되기 때문에 계속 관리해줘야 하고 마음대로 수정하기가 어렵다. 반면,package-private으로 선언하면 API가 아닌 내부 구현이 되어 언제든 수정이 가능하다.
어떤 헬퍼 클래스나 작은 인터페이스가 오직 한 클래스에서만 사용됨
그런데 그것을 package-private 최상위로 만들어 두면, 패키지 내의 다른 클래스들도 접근할 수 있게 됨
// 파일: MyHelper.java
class MyHelper { // package-private 최상위 클래스
void help() { ... }
}
// 파일: MyMain.java
public class MyMain {
private MyHelper helper = new MyHelper();
}
여기서 MyHelper는 사실 MyMain에서만 쓰는데, 같은 패키지 안의 다른 클래스도 접근 가능
즉, 접근 범위가 필요 이상으로 넓음
해결 방법 — private static 중첩 클래스
사용 범위를 한 클래스 내부로 제한
private static 으로 중첩시켜 외부에서 접근 불가능하게 함
public class MyMain {
private final MyHelper helper = new MyHelper();
// 🔒 오직 MyMain 안에서만 쓰이는 내부 클래스
private static class MyHelper {
void help() {
System.out.println("Helping...");
}
}
}
접근제한자 설정 과정 :
공개 API 설계 => 그 외의 모든 멤버를private으로 설정 => 같은 패키지의 다른 클래스가 접근해야 하는 멤버에 한하여package-private으로 풀어줌 => 이 때 권한을 풀어주는 일을 자주 하게 된다면 시스템에서 컴포넌트를 더 분해해야 하는 것은 아닌지 고민해보자.
protected 멤버의 수는 적을 수록 좋다.
public클래스의protected멤버는 공개 API이므로 영원히 지원돼야 한다. 또한 내부 동작 방식을 API 문서에 적어 사용자에게 공개해야 할 수도 있어서protected멤버의 수는 적을수록 좋다.
// 라이브러리 코드
package lib;
public class Animal {
protected void eat() {
System.out.println("Animal eats");
}
}
// 외부 사용자 코드
package user;
import lib.Animal;
public class Dog extends Animal {
@Override
protected void eat() { // 상속받아 재정의 가능
System.out.println("Dog eats");
}
}
여기서 Animal.eat()은 외부에서 상속 경로를 통해 접근 가능하므로,
이미 공개 API 역할을 하게 된다.
멤버 접근성을 좁히지 못하게 방해하는 제약.
상위 클래스의 메서드를 재정의할 때는 그 접근 수준을 상위 클래스에서보다 좁게 설정 할 수 없다.
테스트만을 위해 클래스, 인터페이스, 멤버를 공개 API로 만들어서는 안 된다.
예:
// src/main/java/com/example/MyService.java
package com.example;
class MyService {
int calculate(int x) { // package-private
return x * 2;
}
}
// src/test/java/com/example/MyServiceTest.java
package com.example;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class MyServiceTest {
@Test
void testCalculate() {
MyService service = new MyService();
assertEquals(4, service.calculate(2)); // 접근 가능
}
}
이렇게 하면 굳이 public으로 바꾸지 않아도 테스트 가능
내부 구현은 여전히 패키지 외부에는 감춰짐
public 클래스의 필드가 가변 객체를 참조하거나, final이 아닌 인스턴스 필드를 public으로 선언하면 그 필드에 담을 수 있는 값을 제한할 힘을 잃게 된다.
public class Person {
public String name; // final 아님, public
public List<String> hobbies; // 가변 객체 참조, public
}
Person p = new Person();
p.name = "철수"; // 외부에서 변경 가능
p.hobbies = new ArrayList<>(); // 아예 새로운 리스트로 변경 가능
여기서 문제는:
name이 final이 아님 + public
→ 외부에서 마음대로 변경 가능 (p.name = "영희";)
hobbies가 List(가변 객체) + public
→ 외부에서 리스트 내용을 마음대로 수정 가능 (p.hobbies.add("축구"))
이렇게 되면 클래스 안에서 아무리 값 검증 로직을 넣어도 우회 가능
원래는 필드를 private으로 감추고 setter나 메서드를 통해 값을 변경하게 하면 검증 로직을 넣을 수 있다.
public void setName(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("이름은 비어있을 수 없습니다.");
}
this.name = name;
}
하지만 public 필드로 열어버리면:
누가 언제 어떤 값으로 바꿀지 모름
잘못된 값이 들어가도 막을 수 없음
가변 객체일 경우 내부 구조까지 무단 변경 가능
또한,
public 가변 필드를 갖는 클래스는 일반적으로 스레드 안전하지 않다.
스레드 안전하게 필드를 변경하려면, 보통 다음처럼 락을 걸거나 동기화를 해야 한다.
increment() 메서드 안에서만 값을 변경하도록 하고, 락(synchronized)을 걸어 동시성 문제를 방지함.public class Counter {
private int count;
public synchronized void increment() {
count++;
}
}
하지만 필드가 public이면?
Counter c = new Counter();
c.count++; // 외부에서 직접 변경 → 락 없음 → 경쟁 조건 발생
public static final필드는 Java에서 흔히 상수(constant) 를 정의할 때 쓰는 형태이다.
1. 각 키워드의 의미
public
어디서나 접근 가능
다른 패키지나 클래스에서도 이 필드 사용 가능
static
클래스 변수
인스턴스(객체)마다 따로 생기는 게 아니라, 클래스에 하나만 존재
객체를 만들지 않고 클래스명.필드명 형태로 접근 가능
final
한 번 값이 정해지면 변경 불가
초기화 시 반드시 값을 지정해야 하고, 이후 재할당 불가능
2. 결합해서 의미
public static final = "어디서든 접근 가능한, 클래스에 하나만 존재하며, 변경할 수 없는 값"
즉, 모든 곳에서 공유하는 상수를 만드는 데 쓰임.
관례상 이런 상수의 이름은 대문자 알파벳으로 쓰며, 각 단어 사이에 밑줄(_)을 넣는다.
public class MathConstants {
public static final double PI = 3.141592653589793;
public static final int MAX_USERS = 100;
}
3. 주의사항
클래스에서 배열로 상수를 사용할 때가 있다.
public class Colors {
public static final String[] BASIC_COLORS = {"RED", "GREEN", "BLUE"};
}
이런 경우
final이라 배열 변경이 안될 거라고 생각을 할 수 있지만, 배열의 주소값이 변경이 되지 않는거지 배열의 내용 자체는 변경이 가능하여 배열의 내용을 제한할 힘을 잃게 된다.
해결방법은 배열을 private으로 우선 바꾼 후 public 불변 리스트를 추가하거나 그 배열의 복사본을 반환하는 public 메서드를 추가하는 방법(방어적 복사)방법이 있다.
public class Example {
private static final String[] NAMES = {"철수", "영희"};
public static final List<String> NAMES_LIST =
Collections.unmodifiableList(Arrays.asList(NAMES));
}
public class Example {
private static final String[] NAMES = {"철수", "영희"};
public static String[] getNames() {
return NAMES.clone(); // 새로운 배열 반환 → 원본 보호
}
}
모듈 시스템
Java 9 이후 모듈(패키지들의 묶음) 개념 추가
.java 소스에서 module-info.java 파일로 정의
모듈 간에 어떤 패키지를 외부에 공개(export) 할지 선택 가능
module com.example.myapp {
exports com.example.service; // 이 패키지만 외부 모듈에서 사용 가능
}
두 가지 암묵적 접근 수준
public이더라도, 그 클래스가 속한 패키지가 모듈에서 exports 되지 않았다면 외부 모듈에서는 접근 불가, 같은 모듈 안에서는 exports 여부와 상관없이 접근 가능 (일종의 protected)
export한 경우 외부 모듈에서 사용 가능 (일종의 public)
은닉화를 개념적으로는 이해했었는데 이렇게 디테일한 부분이 있는줄 처음 알았다. 여러 내용이 나왔지만 결론은 최대한 접근수준을 최소화하는게 포인트 인 것 같다. 추후 프로젝트 설계할 때 적용해보고싶다.