이펙티브 자바 실전 강의 교재¶
4장 — 클래스와 인터페이스¶
대상: Java/Spring 백엔드 입문~중급 수강생 형식: 개념 설명 → 비유 → 현업 예제 → 따라하기(실습) → 함정 → 체크리스트 → 퀴즈 전제 환경: Java 17+, Spring Boot 3.x
0. 이 장을 시작하기 전에¶
0.1 학습 목표¶
- 캡슐화(접근 권한 최소화)로 "바꿔도 안전한" 클래스를 만든다.
- 불변 클래스와 컴포지션을 설계의 기본기로 익혀, 깨지기 쉬운 상속을 피한다.
- 인터페이스 우선 사고로, 구현을 갈아끼울 수 있는 유연한 타입을 설계한다.
0.2 큰 그림 — "좋은 타입 설계"의 4갈래¶
[ 숨겨라(캡슐화) ] [ 굳혀라(불변) ] [ 조립하라(관계) ] [ 약속하라(추상) ]
15 접근 최소화 17 변경 최소화 18 컴포지션 20 인터페이스 우선
16 접근자 메서드 19 상속 설계/금지 21 구현측 고려
24 멤버 클래스 static 23 계층구조 22 타입 정의용
25 한 파일 한 클래스
비유 — 클래스는 "잘 만든 가전제품"입니다.
- 내부 회로(필드)는 케이스로 숨기고(15·16), 사용자는 버튼(public 메서드)만 누른다.
- 한 번 설계가 굳으면 안심하고 쓸 수 있는 불변(17) 부품이 좋다.
- 기능 확장은 부품을 조립(18)해서 하지, 제품을 통째로 개조(상속)하지 않는다.
- 부품끼리는 규격(인터페이스)(20)으로 연결해 호환·교체가 쉽다.
0.3 현업에서 왜 중요한가¶
- 도메인 모델을 먼저 잡는 설계에서, 불변 값 객체 + 컴포지션은 거의 기본 문법입니다.
- Spring의
Repository/Service인터페이스, 데코레이터(HttpServletRequestWrapper), record DTO가 모두 이 장의 규칙으로 만들어져 있습니다.
아이템 15. 클래스와 멤버의 접근 권한을 최소화하라¶
한 줄 요약¶
모든 클래스·멤버의 접근성을 가능한 한 좁혀라. 캡슐화는 "바꿔도 깨지지 않는 자유"를 준다.
비유 — "집의 방문(房門)"¶
모든 방문을 활짝 열어두면 누구나 들어와 가구를 옮깁니다(외부가 내부에 의존). 필요한 방만, 필요한 사람에게만 엽니다. 그래야 집 내부를 마음껏 리모델링할 수 있습니다.
핵심 원칙¶
- 클래스의 공개 API를 제외한 모든 멤버는
private으로. - 같은 패키지가 접근해야 하면
package-private(default)까지만 풀어준다. - public이 된 멤버는 영원히 책임져야 하는 약속이 된다.
함정: public static final 배열¶
// ✅ 불변 리스트로 감싸거나, 복사본을 반환
private static final Thing[] VALUES = { ... };
public static final List<Thing> VALUES_LIST =
Collections.unmodifiableList(Arrays.asList(VALUES));
현업 예제 — Java 9+ 모듈, 그리고 일상적 캡슐화¶
실무에서는 필드를 무심코 public/protected로 열어두면, 나중에 그 필드를 리팩터링할 때 외부 코드가 모두 깨집니다. "일단 private, 필요할 때만 연다"가 안전합니다.
체크리스트¶
- 외부에서 정말 써야 하는 멤버만 public인가?
-
protected는 상속 계약의 일부가 됨을 알고 쓰는가? - public static final 필드가 가변 객체(배열/컬렉션)를 가리키지 않는가?
퀴즈¶
Q. public static final이 붙은 배열이 위험한 이유는?
A. final은 참조가 고정될 뿐, 배열 내용은 외부에서 수정할 수 있습니다. 사실상 가변 공개 상태라 캡슐화가 깨집니다.
아이템 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라¶
한 줄 요약¶
public 클래스의 필드는 직접 노출하지 말고 접근자(getter)로 감싸라. 그래야 내부 표현을 바꿀 자유가 생긴다.
비유 — "은행 창구"¶
잔액을 손님이 장부에서 직접 지웠다 쓰면 사고가 납니다. 입출금 창구(메서드)를 통하게 하면, 은행은 내부 장부 방식을 바꿔도 손님 경험은 그대로입니다.
문제 vs 해법¶
// ❌ 캡슐화 없음: x, y의 표현을 영영 못 바꿈
public class Point { public double x; public double y; }
// ✅ 접근자: 내부 표현을 바꿔도 API 유지, 검증/부수효과 삽입 가능
public class Point {
private double x, y;
public double getX() { return x; }
public double getY() { return y; }
}
예외¶
package-private클래스나private중첩 클래스라면 필드를 직접 노출해도 무방(외부에 안 보임).
현업 메모 — record¶
Java 16+의 record는 불변 데이터 운반에 최적입니다. 필드는 private final이고 접근자가 자동 생성됩니다(이름은 getX()가 아니라 x()).
퀴즈¶
Q. private 중첩 클래스는 왜 필드 직접 노출이 허용되는가?
A. 외부 패키지에서 보이지 않아 API가 아닌 내부 구현 세부사항이므로, 표현을 바꿀 때 깨질 외부 코드가 없기 때문입니다.
아이템 17. 변경 가능성을 최소화하라 (불변 클래스) ⭐핵심¶
한 줄 요약¶
불변 클래스를 기본값으로 삼아라. 단순하고, 스레드 안전하며, 안심하고 공유·캐싱할 수 있다.
비유 — "새겨진 비석"¶
한 번 글자를 새긴 비석(불변)은 누가 지나가도 내용이 안 바뀝니다. 그래서 여러 사람이 동시에 읽어도(멀티스레드) 안전하고, 복사해 둘 필요도 없습니다. 반대로 화이트보드(가변)는 누군가 지울 수 있어 늘 불안합니다.
불변 클래스를 만드는 5규칙¶
- 상태를 변경하는 메서드(setter)를 제공하지 않는다.
- 클래스를 확장할 수 없게 한다(
final또는 private 생성자 + 정적 팩터리). - 모든 필드를
final로 만든다. - 모든 필드를
private으로 만든다. - 가변 컴포넌트를 외부가 접근하지 못하게 한다(방어적 복사, 아이템 50).
public final class Money {
private final long amount;
private final String currency;
public Money(long amount, String currency) {
this.amount = amount;
this.currency = currency;
}
// 변경이 아니라 '새 객체'를 반환 (함수형 접근)
public Money plus(Money other) {
if (!currency.equals(other.currency))
throw new IllegalArgumentException("통화 불일치");
return new Money(this.amount + other.amount, currency);
}
}
불변의 장점¶
- 스레드 안전: 동기화 없이 공유 가능(아이템 78·82와 직결).
- 자유로운 공유·캐싱: 같은 값은 인스턴스를 재사용(아이템 6, 예:
BigInteger.ZERO). - 실패 원자성(아이템 76)을 공짜로 얻음 — 상태가 안 바뀌니 실패해도 깨질 게 없음.
단점과 완화¶
- 값마다 별도 객체가 필요 → 다단계 연산에서 중간 객체가 많이 생김.
- 완화: 다단계 연산용 가변 동반 클래스(예:
String의StringBuilder).
현업 예제 — 불변 DTO/값 객체¶
- JDK:
String,LocalDate,BigDecimal,Integer는 모두 불변. - 도메인:
금액,사업자번호,좌표,기간(Period)같은 값 객체는 불변으로. - DTO: 요청/응답 DTO를
record로 만들면 불변이 보장돼 동시성·예측가능성이 좋아집니다.
실무 지침: "되도록 불변, 꼭 필요한 부분만 가변." 가변이 필요하면 변경 가능한 필드를 최소화하세요.
따라하기 (실습 17-A)¶
- 가변
Money(setter 보유)를 만들고, 두 스레드가 동시에 더하기를 해서 값이 깨지는 것을 재현한다. - 불변
Money(plus가 새 객체 반환)로 바꿔 같은 테스트가 안전해지는 것을 확인한다.
체크리스트¶
- setter를 습관적으로 만들고 있지 않은가?
- 값 객체/DTO를
record나final+final 필드로 만들었는가? - 가변 필드(배열/컬렉션/Date)를 방어적 복사 없이 노출하지 않는가?
퀴즈¶
Q. 불변 객체가 멀티스레드에서 동기화 없이 안전한 이유는?
A. 생성 이후 상태가 절대 바뀌지 않으므로, 여러 스레드가 동시에 읽어도 경쟁 상태(race condition)가 발생할 여지가 없기 때문입니다.
아이템 18. 상속보다는 컴포지션을 사용하라 ⭐⭐핵심¶
한 줄 요약¶
구현 상속(extends)은 캡슐화를 깨고 깨지기 쉽다. 대부분의 경우 컴포지션(has-a)과 전달(forwarding)이 정답이다.
비유 — "개조 vs 조립"¶
상속은 부모 기계를 통째로 물려받아 개조하는 것입니다. 부모가 내부 동작을 바꾸면(다음 버전에서) 내 개조가 망가집니다. 컴포지션은 부품을 레고처럼 조립해, 부품이 바뀌어도 내 조립품의 인터페이스는 그대로입니다.
문제: 깨지기 쉬운 상속 (fragile base class)¶
HashSet을 상속해 "추가된 원소 개수"를 세려고 하면, addAll이 내부적으로 add를 호출하는 바람에 중복 카운트가 됩니다. 상속은 상위 클래스의 구현 세부사항에 의존하기 때문입니다.
// ❌ 상속: 상위 구현에 종속 → addAll이 add를 호출하면 카운트가 2배
public class InstrumentedHashSet<E> extends HashSet<E> { ... }
해법: 컴포지션 + 전달(데코레이터 패턴)¶
// 래퍼: Set을 '포함'하고, 메서드는 위임(forward)
public class InstrumentedSet<E> implements Set<E> {
private final Set<E> s; // 컴포지션(has-a)
private int addCount = 0;
public InstrumentedSet(Set<E> s) { this.s = s; }
@Override public boolean add(E e) {
addCount++;
return s.add(e); // 전달(forwarding)
}
@Override public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return s.addAll(c);
}
public int getAddCount() { return addCount; }
// 나머지 Set 메서드도 s에 위임 (전달 클래스로 분리하면 재사용 가능)
}
이제 어떤 Set 구현이 와도(HashSet, TreeSet, ...) 감쌀 수 있고, 상위 구현 변경에 영향받지 않습니다.
언제 상속이 정당한가¶
- 순수한 is-a 관계이고, 상위 클래스가 상속을 위해 설계·문서화되었을 때만(아이템 19).
- 그 외에는 컴포지션을 의심하라. "B가 A의 일종인가?"에 자신 있게 "예"가 아니면 상속하지 말 것.
현업 예제¶
- Java I/O의
BufferedReader(new FileReader(...))— 전형적 데코레이터(컴포지션). - 서블릿
HttpServletRequestWrapper— 요청을 감싸 기능 추가. - Spring
BeanPostProcessor/프록시 — 상속이 아닌 위임/래핑으로 기능 확장.
도메인 모델링 메모: "주문은 결제의 일종이다(상속)"가 아니라 "주문은 결제를 가진다(컴포지션)"가 대개 맞습니다. is-a를 남발하면 모델이 경직됩니다.
따라하기 (실습 18-A)¶
HashSet을 상속한 카운팅 Set을 만들고addAll([a,b,c])후 카운트가 6이 되는 버그를 재현한다.- 컴포지션+전달 방식으로 바꿔 카운트가 3으로 정확해지는 것을 확인한다.
TreeSet을 감싸도 동작하는지 확인(상속이었다면 불가능).
체크리스트¶
- extends를 쓰기 전에 "정말 is-a인가?"를 자문했는가?
- 상위 클래스의 구현 변경에 내 코드가 깨질 위험은 없는가?
- 기능 추가가 "감싸기(wrapping)"로 표현되는가? → 컴포지션 신호
퀴즈¶
Q. InstrumentedHashSet의 addAll 카운트가 2배가 되는 근본 원인은?
A. HashSet.addAll이 내부적으로 add를 반복 호출하는데, 하위 클래스가 add도 재정의해 카운트를 올리기 때문입니다. 즉 상위 클래스의 구현 세부사항에 의존한 탓이며, 컴포지션은 이 의존을 끊습니다.
아이템 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라¶
한 줄 요약¶
상속을 허용하려면 재정의 가능 메서드의 자기사용(self-use) 패턴을 문서화해야 한다. 그럴 자신이 없으면 final로 상속을 금지하라.
비유 — "사용 설명서 없는 개조 허가"¶
남에게 내 기계를 개조하게 허락하려면, 내부 부품이 서로 어떻게 맞물리는지(어떤 메서드가 어떤 메서드를 부르는지) 설명서를 줘야 합니다. 설명서 없이 개조를 허락하면 사고가 납니다.
핵심 규칙¶
- 재정의 가능한 메서드를 생성자에서 호출하지 마라. 하위 클래스 필드가 초기화되기 전에 불려 NPE/오작동을 유발한다.
- 상속용으로 설계하지 않은 클래스는
final로 막거나, 생성자를 막고 정적 팩터리를 제공한다.
현업 메모¶
실무에서 라이브러리/공용 모듈 클래스는 기본적으로 final 또는 인터페이스 + 컴포지션을 권장합니다. 확장점이 필요하면 명시적 확장 지점(템플릿 메서드, 콜백, 전략 주입)으로 제공하세요.
퀴즈¶
Q. 생성자에서 재정의 가능 메서드를 호출하면 안 되는 이유는?
A. 상위 클래스 생성자가 하위 클래스 생성자보다 먼저 실행되므로, 하위 클래스의 필드가 초기화되기 전에 재정의된 메서드가 호출되어 예기치 않은 동작이나 NPE가 발생합니다.
아이템 20. 추상 클래스보다는 인터페이스를 우선하라 ⭐¶
한 줄 요약¶
인터페이스를 우선하라. 다중 구현이 가능하고, 믹스인·계층 없는 타입 조합에 유연하다.
비유 — "자격증 vs 가문(家門)"¶
- 추상 클래스 상속 = 단일 가문에 속함(부모는 하나뿐).
- 인터페이스 구현 = 여러 자격증을 동시에 보유(
Comparable+Serializable+Runnable).
기존 클래스에도 인터페이스는 나중에 추가하기 쉽지만, 추상 클래스를 끼워넣으려면 상속 계층을 갈아엎어야 합니다.
인터페이스의 강점¶
- 믹스인(mixin) 정의 가능: "주된 타입 외에 선택적 능력"을 더함(
Comparable). - 계층 없는 타입 조합 가능(
SingerSongwriter=Singer+Songwriter). - 디폴트 메서드로 공통 구현 제공 가능(Java 8+).
추상 골격 구현(skeletal implementation)¶
인터페이스 + 추상 골격 클래스를 함께 제공하면 두 이점을 모두 취합니다. (예: Collection 인터페이스 + AbstractCollection)
- 인터페이스로 타입을 정의하고,
AbstractXxx로 구현 부담을 덜어줌.
현업 예제 — Spring의 인터페이스 우선 설계¶
XxxRepository,XxxService를 인터페이스로 정의하고 구현을 주입 → 테스트 시 가짜 구현 교체(아이템 5).List(인터페이스)로 참조하고 구현은ArrayList/LinkedList로 교체(아이템 64).
public interface ContractRepository { // 타입(계약) 정의
Optional<Contract> findById(Long id);
}
// 운영: JpaContractRepository, 테스트: InMemoryContractRepository
체크리스트¶
- 타입을 정의할 때 추상 클래스부터 떠올리고 있지 않은가? → 인터페이스 우선
- 여러 능력을 조합해야 하는가? → 인터페이스(다중 구현)
- 공통 구현이 필요하면 디폴트 메서드 또는 골격 구현 클래스로 분리했는가?
퀴즈¶
Q. 인터페이스가 추상 클래스보다 "기존 클래스에 끼워넣기" 쉬운 이유는?
A. 자바는 단일 상속만 허용하므로 추상 클래스를 끼우려면 상속 계층을 바꿔야 합니다. 반면 인터페이스는 여러 개를 추가로 구현할 수 있어 기존 클래스에 implements만 더하면 됩니다.
아이템 21. 인터페이스는 구현하는 쪽을 생각해 설계하라¶
한 줄 요약¶
디폴트 메서드를 함부로 추가하면 기존 구현체를 조용히 깨뜨릴 수 있다. 인터페이스는 신중히 설계하고, 가능하면 릴리스 전에 굳혀라.
비유 — "이미 배포된 계약서 수정"¶
모두가 서명한 계약서(인터페이스)에 나중에 조항(디폴트 메서드)을 끼워넣으면, 그 조항을 못 지키는 기존 서명자(구현체)가 위반 상태가 됩니다.
핵심¶
- 디폴트 메서드는 모든 기존 구현체에 자동으로 주입되지만, 그 구현이 모든 구현체에서 옳다는 보장은 없다(예: 동기화/불변식을 깨는 디폴트 구현).
- 인터페이스를 설계할 때 여러 방식으로 구현해 보며 결함을 미리 발견하라.
퀴즈¶
Q. 디폴트 메서드 추가가 "런타임 오류"로 이어질 수 있는 이유는?
A. 컴파일은 통과하지만, 기존 구현체가 가정하던 불변식이나 스레드 안전성을 디폴트 구현이 위반하면 실행 중에야 문제가 드러나기 때문입니다.
아이템 22. 인터페이스는 타입을 정의하는 용도로만 사용하라¶
한 줄 요약¶
인터페이스는 "이 타입은 무엇을 할 수 있다"를 정의하는 것. 상수 보관함(상수 인터페이스)으로 쓰지 마라.
비유 — "간판 vs 창고"¶
인터페이스는 가게의 간판(어떤 가게인지)이지, 잡동사니를 쌓아두는 창고가 아닙니다.
안티패턴: 상수 인터페이스¶
// ❌ 구현 세부사항(상수)이 API로 새어나가고, 구현하면 네임스페이스가 오염됨
public interface PhysicalConstants {
double AVOGADROS_NUMBER = 6.022e23;
}
대안¶
- 상수가 특정 클래스에 강하게 묶이면 그 클래스 안에
public static final로. - 독립적이면 상수 유틸리티 클래스(아이템 4) 또는
enum(아이템 34).
public final class PhysicalConstants {
private PhysicalConstants() {}
public static final double AVOGADROS_NUMBER = 6.022e23;
}
퀴즈¶
Q. 상수 인터페이스를 구현하면 무엇이 오염되는가?
A. 해당 클래스의 네임스페이스가 인터페이스의 상수들로 오염되고, 내부 구현 세부사항이던 상수가 외부 API로 노출됩니다.
아이템 23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라¶
한 줄 요약¶
하나의 클래스에 type 필드로 종류를 구분하는 태그 클래스는 안티패턴. 상속/다형성 계층구조로 풀어라.
비유 — "스위스 만능 칼 vs 전용 도구"¶
모든 기능을 한 몸에 욱여넣은 태그 클래스는, 안 쓰는 필드와 거대한 switch로 너덜너덜해집니다. 용도별 전용 도구(서브클래스)로 나누는 편이 깔끔합니다.
문제 → 해법¶
// ❌ 태그 클래스: 도형 종류를 필드로 구분, switch 범벅
class Figure {
enum Shape { RECTANGLE, CIRCLE }
final Shape shape;
double length, width; // 사각형용
double radius; // 원용 — 한쪽은 항상 낭비
double area() {
switch (shape) { case RECTANGLE: return length*width;
case CIRCLE: return Math.PI*radius*radius;
default: throw new AssertionError(); }
}
}
// ✅ 계층구조: 각자 자기 데이터만, 다형성으로 area()
abstract class Figure { abstract double area(); }
class Rectangle extends Figure {
final double length, width;
Rectangle(double l, double w){ length=l; width=w; }
double area(){ return length*width; }
}
class Circle extends Figure {
final double radius;
Circle(double r){ radius=r; }
double area(){ return Math.PI*radius*radius; }
}
현업 메모¶
결제수단, 알림채널, 할인정책처럼 "종류에 따라 동작이 다른" 도메인은 태그+switch 대신 인터페이스 + 다형성(또는 sealed 계층)으로 모델링하면 새 종류 추가가 안전합니다.
퀴즈¶
Q. 태그 클래스의 대표적 결함을 하나만 들어라.
A. 쓰지 않는 필드까지 항상 메모리에 존재하고(메모리 낭비), 종류가 늘 때마다 switch를 빠짐없이 고쳐야 해 확장에 취약하다(다형성으로 풀면 추가만 하면 됨). 그 외 가독성 저하, 컴파일 타임 타입 안전성 상실 등도 있습니다.
아이템 24. 멤버 클래스는 되도록 static으로 만들라¶
한 줄 요약¶
중첩 클래스가 바깥 인스턴스를 참조할 필요가 없다면 static으로 선언하라. 안 그러면 숨은 참조로 메모리 누수가 생긴다.
비유 — "보이지 않는 탯줄"¶
비정적(inner) 멤버 클래스는 바깥 인스턴스로 향하는 보이지 않는 참조(탯줄)를 갖습니다. 이 탯줄 때문에 바깥 객체가 GC되지 못해(아이템 7) 누수가 납니다. 탯줄이 필요 없으면 끊으세요(static).
네 가지 중첩 클래스¶
| 종류 | 바깥 참조 | 용도 |
|---|---|---|
| 정적 멤버 클래스 | 없음 | 가장 일반적, 헬퍼/빌더 등 |
| (비정적) 멤버 클래스 | 있음 | 어댑터 등 바깥 인스턴스가 꼭 필요할 때만 |
| 익명 클래스 | 상황에 따라 | 즉석 구현(람다로 대체 권장) |
| 지역 클래스 | 상황에 따라 | 드묾 |
예제¶
public class Outer {
// ✅ 바깥 인스턴스가 필요 없으면 static
public static class Builder { /* ... */ } // 아이템 2의 빌더가 대표 사례
// 비정적은 바깥 인스턴스가 꼭 필요한 경우에만
}
현업 메모¶
빌더, 비교자, 엔트리 같은 헬퍼 중첩 클래스는 거의 항상 static입니다. Map.Entry도 정적 멤버 클래스죠. 무심코 static을 빼면 컬렉션에 담긴 엔트리들이 바깥 Map을 붙잡아 누수가 납니다.
퀴즈¶
Q. 비정적 멤버 클래스가 메모리 누수를 유발할 수 있는 이유는?
A. 바깥 인스턴스로의 숨은 참조를 갖기 때문에, 멤버 클래스 인스턴스가 살아 있는 한 바깥 인스턴스도 GC되지 못합니다. 바깥 참조가 불필요하면 static으로 선언해 이 참조를 없애야 합니다.
아이템 25. 톱레벨 클래스는 한 파일에 하나만 담으라¶
한 줄 요약¶
한 소스 파일에 톱레벨 클래스를 여러 개 두지 마라. 컴파일 순서에 따라 동작이 달라지는 위험이 있다.
비유 — "같은 이름의 두 간판"¶
한 건물에 같은 이름의 가게가 둘이면, 손님이 어느 집에 들어갈지 입구 순서에 따라 달라집니다 — 예측 불가능합니다.
핵심¶
- 한 파일에 같은 클래스가 중복 정의되면, javac에 넘기는 파일 순서에 따라 어떤 정의가 쓰일지 달라져 빌드가 비결정적이 된다.
- 부차적 톱레벨 클래스가 꼭 필요하면 정적 멤버 클래스(아이템 24)로 넣어라.
퀴즈¶
Q. 한 파일에 톱레벨 클래스를 여러 개 두면 생기는 최악의 증상은?
A. 컴파일 순서에 따라 어느 정의가 채택되는지 달라져, 같은 코드가 환경마다 다르게 동작하거나 빌드가 실패하는 비결정적 빌드가 됩니다.
4장 종합 정리¶
한눈에 보는 결정 가이드¶
| 상황 | 선택 |
|---|---|
| 멤버 공개 범위 | 최소화(15), 필드는 접근자로(16) |
| 데이터 클래스 설계 | 불변 우선(17), record 활용 |
| 기능 확장 | 컴포지션(18), 상속은 is-a + 문서화(19)일 때만 |
| 타입 정의 | 인터페이스 우선(20), 상수 보관 금지(22) |
| 종류별 동작 분기 | 태그 클래스 금지 → 다형성 계층(23) |
| 중첩 클래스 | 바깥 참조 불필요하면 static(24) |
| 파일 구성 | 한 파일 한 톱레벨 클래스(25) |
종합 체크리스트 (코드 리뷰용)¶
- 필드/메서드 접근성을 습관적으로 최소화했는가
- 값 객체·DTO를 불변(record/final)으로 설계했는가
-
extends를 쓰기 전에 컴포지션을 먼저 검토했는가 - 타입은 인터페이스로 정의하고 구현을 주입했는가
- 헬퍼 중첩 클래스에
static을 빠뜨리지 않았는가
종합 퀴즈¶
Q1. 이 장 전체를 한 문장으로 요약하면?
A. 숨기고(캡슐화), 굳히고(불변), 조립하고(컴포지션), 약속(인터페이스)으로 설계하라 — 즉 변경에 강하고 교체가 쉬운 타입을 만들라.
Q2. "상속보다 컴포지션"을 도메인 모델로 옮기면 어떤 질문을 던져야 하는가?
A. "B는 A의 일종인가(is-a)?"에 확신이 없으면, "B는 A를 가지는가(has-a)?"로 바꿔 컴포지션으로 설계한다.
Q3. record가 4장의 어떤 아이템들을 동시에 만족시키는가?
A. 불변(17), 접근자 제공(16), 캡슐화(15)를 한 번에 충족합니다(필드 private final + 접근자 자동 + 불변).
다음 장 예고 — 5장: 제네릭¶
로 타입 회피, 비검사 경고 제거, 한정적 와일드카드(PECS), 타입 안전 이종 컨테이너 등 타입 안전성의 핵심을 다룹니다. 컬렉션·DTO·유틸을 다루는 현업 코드의 컴파일 경고를 없애는 실전 장입니다.