생성자는 이름이 클래스 이름으로 고정되어 있습니다. new Money(1000)이 원인지 달러인지, new Point(3, 4)가 직교좌표인지 극좌표인지 알 수 없습니다. 같은 시그니처의 생성자를 두 개 둘 수도 없습니다. 정적 팩토리 메서드(static factory method)는 이 한계를 넘습니다.
| 장점 | 예 |
|---|---|
| 이름을 가질 수 있다 | Money.won(1000), Money.dollars(2) |
| 호출할 때마다 새 객체를 만들지 않아도 된다 | Boolean.valueOf(true), Integer.valueOf(127) — 캐시 반환 |
| 반환 타입의 하위 타입을 돌려줄 수 있다 | List.of()는 List 인터페이스를 반환, 실제 클래스는 숨김 |
| 입력에 따라 다른 클래스를 돌려줄 수 있다 | EnumSet.of()는 크기에 따라 RegularEnumSet 또는 JumboEnumSet |
관례적 이름: of(값을 감싸기), from(다른 타입에서 변환), valueOf(of와 같지만 캐시 가능성), getInstance/newInstance, create. 생성자를 private으로 막고 정적 팩토리만 열어 두면 "만드는 방법"을 클래스가 완전히 통제합니다.
매개변수가 4개를 넘고 그 중 일부가 선택 사항이면 생성자 오버로딩은 조합 폭발을 일으킵니다(점층적 생성자 패턴). 빌더(builder)는 필수 값은 빌더 생성자로, 선택 값은 이름 있는 메서드로 받아 build()에서 검증하고 객체를 만듭니다.
Order.builder("Kim") ← 필수 인자
.item("노트북") ← 선택, return this
.address("부산") ← 선택, return this
.build() ← 검증 후 불변 Order 생성return this가 체이닝을 가능하게 합니다. 각 setter가 자기 자신(빌더)을 돌려주므로 .을 계속 이어 쓸 수 있습니다.
| 종류 | 결정 시점 | 기준 | 예 |
|---|---|---|---|
| 오버로딩 | 컴파일 시점 | 인자의 정적 타입 | print(1) vs print("a") |
| 오버라이딩 | 런타임 | 객체의 실제 타입 | Shape s = new Circle(); s.area() → Circle의 area |
super.method() |
컴파일 시점 | 부모 클래스의 구현으로 고정 | 오버라이딩한 메서드에서 부모 동작 재사용 |
default 메서드 |
런타임 | 구현 클래스가 덮어쓰지 않으면 인터페이스 것 | Shape.describe() |
템플릿 메서드가 동작하는 원리가 바로 런타임 디스패치입니다. 부모의 generate()가 header()를 호출하면, this의 실제 타입(CsvReport)의 header()가 실행됩니다. 부모는 자식이 누구인지 몰라도 됩니다.
| 방법 | 언제 | 단점 |
|---|---|---|
null 반환 |
(권장하지 않음) | 호출자가 잊으면 NPE |
| 빈 컬렉션 반환 | 결과가 0개 이상인 목록 | 없음. 항상 이렇게 |
boolean 반환 |
성공/실패만 중요 | 실패 이유를 못 담음 |
Optional<T> 반환 |
있을 수도 없을 수도 있는 값 하나 | 필드·컬렉션 원소로는 쓰지 않음 |
| 예외 | 호출자가 복구할 수 없는 상황, 프로그래머 실수 | 흐름 제어용으로 남용하면 느리고 읽기 어려움 |
Result<T> 타입 |
실패가 정상 흐름의 일부이고 이유가 필요 | 직접 정의해야 함(sealed + record로 간단) |
"찾는데 없을 수 있다"는 Optional, "입력이 잘못됐다"는 예외 또는 Result, "목록이 비었다"는 빈 컬렉션. 이 세 가지만 지켜도 API가 명확해집니다.
객체는 주소로 전달되므로, 생성자에서 받은 리스트를 그대로 필드에 넣으면 바깥 코드가 그 리스트를 바꿀 때 내 객체의 상태도 바뀝니다. 이를 별칭 문제라 합니다. 해결책은 두 방향의 복사입니다.
바깥 → 안 (생성자/setter): this.items = new ArrayList<>(items); 방어적 복사
안 → 바깥 (getter): return Collections.unmodifiableList(items); 불변 뷰
return List.copyOf(items); 불변 복사unmodifiableList는 원본이 바뀌면 같이 바뀌는 뷰이고, List.copyOf는 그 순간의 스냅샷입니다. 둘 다 바깥에서 add하면 UnsupportedOperationException입니다.
| 메서드 | 계약 | 어기면 |
|---|---|---|
equals |
반사·대칭·추이·일관성, null과는 false |
List.contains, remove가 오동작 |
hashCode |
equals가 true면 반드시 같은 값 |
HashSet/HashMap에서 같은 값이 중복·분실 |
toString |
사람이 읽을 수 있는 표현 | 디버깅·로그가 Money@1b6d |
compareTo |
equals와 일관되게(권장), 부호가 뒤집혀야 함 |
TreeSet/sort 결과가 이상 |
record는 이 중 equals/hashCode/toString을 자동 생성합니다. 직접 클래스를 쓸 때는 Objects.equals, Objects.hash, Long.compare를 사용해 실수를 줄입니다.