중첩 클래스의 종류
네 가지 중첩 형태를 분류하고 정적 중첩 클래스의 바깥 인스턴스 접근 실패·캡슐화·게시글 메시지 모델을 검증합니다.
클래스 안이나 메서드 블록 안에 다른 클래스를 선언하면 사용 범위와 결합 의도를 코드 구조로 표현할 수 있습니다.
크게 바깥 인스턴스 참조가 없는 정적 중첩 클래스와, 바깥 인스턴스 또는 지역 문맥에 연결되는 내부 클래스 종류로 나뉩니다.
가까이 둔다는 이유만으로 모든 클래스를 중첩하지 않고 외부 공개 필요와 상태 공유 여부를 판단합니다.
정적 중첩 클래스의 접근 범위
Nested는 Outer 타입 안에 선언됐지만 특정 Outer 객체에 소속되지 않습니다.
다음 코드는 instanceValue를 읽을 바깥 참조가 없어 컴파일되지 않습니다.
public final class StaticNestedOuterAccessFailure {
private int instanceValue = 40;
private static final class Nested {
int read() {
return instanceValue;
}
}
}error: non-static variable instanceValue cannot be referenced from a static context정적 중첩 클래스는 바깥 타입의 static 멤버에는 접근할 수 있습니다.
특정 인스턴스 데이터가 필요하면 생성자 인수로 명시적으로 전달하거나, 정말 바깥 인스턴스와 한 수명으로 움직인다면 non-static 내부 클래스를 검토합니다.
네 가지 중첩 클래스
용어가 비슷하므로 “어디에 선언되는가”와 “무엇을 자동으로 기억하는가”를 기준으로 나눕니다.
- 정적 중첩 클래스: 클래스 멤버 위치, 바깥 인스턴스 참조 없음
- 내부 클래스: 인스턴스 멤버 위치, 생성에 바깥 인스턴스 필요
- 지역 클래스: 메서드 블록 안, 바깥 인스턴스와 사실상
final지역 값 접근 - 익명 클래스: 이름 없는 지역 클래스 형태, 선언과 인스턴스 생성을 한 표현식에서 수행
람다는 익명 클래스와 비슷하게 동작을 전달하지만 별도의 익명 클래스 인스턴스 의미와 this 해석이 다릅니다.
함수형 인터페이스를 다룰 때 뒤에서 선택 기준을 확장합니다.
Board의 정적 중첩 값
Entry가 Board 문맥에서만 의미 있지만 특정 Board 인스턴스 필드를 자동으로 읽을 필요는 없습니다.
정적 중첩 레코드로 두면 Board.Entry라는 소속을 드러내고 독립적으로 생성할 수 있습니다.
import java.util.ArrayList;
import java.util.List;
public final class StaticNestedPost {
public static void main(String[] args) {
Board board = new Board();
board.add(new Board.Entry("nested", 40));
board.add(new Board.Entry("inner", 50));
System.out.println(board.entries());
System.out.println("total=" + board.totalViewCount());
}
private static final class Board {
private final List<Entry> entries = new ArrayList<>();
void add(Entry entry) { entries.add(entry); }
List<Entry> entries() { return List.copyOf(entries); }
int totalViewCount() { return entries.stream().mapToInt(Entry::viewCount).sum(); }
static record Entry(String title, int viewCount) {
Entry {
if (title == null || title.isBlank()) throw new IllegalArgumentException("title");
if (viewCount <= 0) throw new IllegalArgumentException("viewCount");
}
}
}
}[Entry[title=nested, viewCount=40], Entry[title=inner, viewCount=50]]
total=90Entry는 바깥 entries 목록을 직접 만지지 않습니다.
검증된 값 하나를 표현하고 Board가 보관 책임을 가집니다.
최상위 파일로 꺼내 재사용할 필요가 생기면 중첩을 풀어도 됩니다.
구현 세부 타입의 은닉
NetworkMessage처럼 한 클래스 안에서만 사용하는 보조 타입은 비공개 정적 중첩로 숨길 수 있습니다.
공개 API는 String이나 역할 인터페이스처럼 소비자가 필요한 타입만 노출합니다.
public final class EncapsulatedBoardMessage {
public static void main(String[] args) {
BoardGateway network = new BoardGateway("local");
System.out.println(network.send("객체 질문 공개"));
}
private static final class BoardGateway {
private final String endpoint;
BoardGateway(String endpoint) {
this.endpoint = endpoint;
}
String send(String content) {
NetworkMessage message = new NetworkMessage(content);
return endpoint + ":" + message.payload();
}
private static final class NetworkMessage {
private final String content;
NetworkMessage(String content) {
if (content == null || content.isBlank()) throw new IllegalArgumentException("content");
this.content = content;
}
String payload() { return "[board] " + content; }
}
}
}local:[board] 객체 질문 공개main은 NetworkMessage의 존재를 몰라도 됩니다.
메시지 포맷 변경은 BoardGateway 내부 구현 변화로 제한됩니다.
중첩 클래스가 private여도 바깥 클래스와 중첩 클래스는 서로 private 멤버에 접근할 수 있지만, 책임 경계를 흐릴 정도로 남용하지 않습니다.
중첩 클래스 생성과 이름 해석
외부에서는 new Outer.Nested()로 만들며 Outer 인스턴스가 필요하지 않습니다.
바깥 클래스 본문에서는 new Nested()처럼 바깥 이름을 생략할 수 있습니다.
컴파일 결과 클래스 파일 이름에는 보통 $가 포함되지만 소스 설계는 자바 이름과 접근 제어를 기준으로 읽습니다.
public final class NestedConstruction {
public static void main(String[] args) {
Counter.Bucket first = new Counter.Bucket("java");
Counter.Bucket second = Counter.newBucket("time");
System.out.println(first.label());
System.out.println(second.label());
}
private static final class Counter {
static Bucket newBucket(String name) {
return new Bucket(name);
}
static final class Bucket {
private final String name;
Bucket(String name) { this.name = name; }
String label() { return "bucket=" + name; }
}
}
}bucket=java
bucket=time팩터리로 생성 규칙을 숨길 수도 있고 중첩 타입 자체를 공개할 수도 있습니다.
반환 타입까지 private로 숨기면 외부가 값을 받을 수 없으므로 API 경계를 함께 설계합니다.
정적 중첩 클래스 선택 기준
- 바깥 타입과 개념적으로 밀접하지만 특정 바깥 객체 상태가 필요 없는가?
- 보조 타입을 외부 패키지 이름 공간에 노출할 이유가 없는가?
- 함께 변경되는 코드가 가까이 있을 때 이해가 쉬운가?
- 파일이 지나치게 커져 독립 테스트와 탐색이 어려워지지 않는가?
“관련 있음”만으로 중첩하면 거대한 클래스가 됩니다.
독립 책임과 재사용 소비자가 생기면 최상위 타입으로 분리합니다.
연습 문제
BoardRunner 안에 성공 여부와 메시지를 가진 정적 중첩 레코드 Result를 만들고, 조회 수가 40이면 성공 결과를 반환하세요.
해설 보기
public final class StaticNestedResultExercise {
public static void main(String[] args) {
BoardRunner.Result result = BoardRunner.run("nested", 40);
System.out.println(result);
}
private static final class BoardRunner {
static Result run(String title, int viewCount) {
if (viewCount <= 0) return new Result(false, "invalid");
return new Result(true, title + "=" + viewCount);
}
static record Result(boolean success, String message) { }
}
}Result[success=true, message=nested=40]Result는 BoardRunner의 특정 인스턴스와 무관하므로 static 중첩 클래스가 맞습니다.
다른 실행기에서도 공유된다면 최상위 결과 타입을 검토합니다.