이펙티브 자바 실전 강의 교재¶
9장 — 일반적인 프로그래밍 원칙¶
대상: Java/Spring 백엔드 입문~중급 수강생 형식: 개념 설명 → 비유 → 현업 예제 → 따라하기(실습) → 함정 → 체크리스트 → 퀴즈 전제 환경: Java 17+, Spring Boot 3.x
0. 이 장을 시작하기 전에¶
0.1 학습 목표¶
- 변수·반복·라이브러리 같은 일상의 선택에서 표준에 맞는 길을 든다.
float/double·String·박싱 타입의 유혹과 함정을 분간한다.- 추상 타입(인터페이스)으로 의존하고, 리플렉션은 마지막 수단으로 둔다.
- "최적화는 측정 뒤"라는 규율을 익힌다.
- Java 커뮤니티의 명명 규칙을 따른다.
0.2 큰 그림 — "기본기를 의식적으로"¶
12개 아이템을 4개 묶음으로 보면 외울 부담이 줄어듭니다.
[ 변수·반복 ] [ 표준 라이브러리·정밀도 ]
57 지역변수 최소화 59 라이브러리를 익혀라 ⭐
58 for-each 우선 60 정확한 답엔 BigDecimal ⭐
61 박싱 회피
62 문자열 남용 회피
63 문자열 연결 비용
[ 추상화·런타임 ] [ 최적화·명명 ]
64 인터페이스로 참조 ⭐ 67 최적화는 측정 뒤 ⭐
65 리플렉션은 최후 68 명명 규칙
66 네이티브는 신중
비유 — 9장은 "주방의 기본기"입니다.
칼 잡는 법, 도마 잡는 법, 정확히 계량하는 습관 — 화려하지 않지만, 이게 안 되면 다른 기술이 다 깨집니다.
0.3 현업에서 왜 중요한가¶
- 9장의 함정은 CR에서 가장 자주 잡히는 항목들입니다. 박싱 한 줄,
+=문자열 연결 한 줄,float금액 한 줄이 운영 사고로 이어진 사례가 흔합니다. - 표준 라이브러리·인터페이스 참조 습관이 안 들면 테스트 가능성과 교체 가능성이 모두 떨어집니다.
아이템 57. 지역변수의 범위를 최소화하라¶
한 줄 요약¶
지역변수는 처음 사용하는 곳에서 선언하고, 초기값과 함께 선언하라. 범위가 좁을수록 가독성·버그 가능성·재사용 실수 모두 줄어든다.
비유 — "작업대를 매번 정리"¶
요리 끝나고 도마·칼을 그대로 두고 다음 요리로 가면 섞입니다. 한 요리에 쓰는 도구는 그 작업대 안에서만.
안티패턴¶
권장¶
for (int i = 0, n = list.size(); i < n; i++) { // ✅ 루프 변수는 루프 안
Element e = list.get(i);
process(e);
}
// ✅ try-with-resources도 변수 범위 최소화의 한 형태
try (var conn = dataSource.getConnection()) { ... }
함정¶
forvswhile: 비슷한 두 루프를 복붙하다while의 변수가 다음 루프로 새는 경우가 잦다 →for우선.final/var: 초기화와 동시에 선언하면final이 자연스러워 가독성이 향상된다.
체크리스트¶
- 메서드 상단에 변수 선언이 몰려 있지 않은가
- 변수가 처음 사용되는 줄에서 초기화하며 선언되었는가
-
while루프의 변수가 옆 루프로 새고 있지 않은가
아이템 58. 전통적인 for 문보다는 for-each 문을 사용하라¶
한 줄 요약¶
인덱스·반복자 노출이 필요 없다면 for-each가 짧고 안전하다. 인덱스 실수·반복자 오용을 컴파일러가 막아준다.
안티패턴¶
// ❌
for (Iterator<Element> it = c.iterator(); it.hasNext();) {
Element e = it.next();
...
}
for (int i = 0; i < list.size(); i++) { // size() 매번 호출
...
}
권장¶
for-each가 못 하는 3가지 (전통 for 유지)¶
- 요소 제거 —
Iterator.remove()필요 - 요소 교체 —
ListIterator.set()필요 - 여러 컬렉션 평행 순회 — 인덱스 공유 필요
// 다중 컬렉션 평행 순회 — 전통 for 또는 zip 패턴
for (int i = 0; i < names.size(); i++) {
pair(names.get(i), ages.get(i));
}
함정 — 중첩 for-each의 흔한 버그¶
// ❌ 외부 it을 내부에서 재사용 의도
for (Suit s : suits) {
for (Rank r : ranks) {
deck.add(new Card(s, r)); // s는 외부의 한 값으로 고정 — OK
}
}
// 그런데 옛 스타일 Iterator 중첩에서는 NoSuchElementException 자주 발생 (그래서 for-each 권장)
체크리스트¶
- 인덱스가 정말 필요한 경우만 전통 for인가
- 제거·교체·평행 순회가 아니면 for-each인가
-
list.size()를 매 반복마다 호출하지 않는가
아이템 59. 라이브러리를 익히고 사용하라¶
한 줄 요약¶
표준 라이브러리에 이미 있는 기능을 다시 짜지 마라. 검증·성능·유지보수에서 진다.
비유 — "전동 믹서 vs 거품기"¶
전동 믹서(java.util.*/java.util.concurrent)가 있는데 거품기로 30분 손으로 젓는 사람은 결과도 늦고 일관성도 떨어집니다.
사례 — 랜덤 정수 생성¶
// ❌ — 직관적으로 짜다 보면 음수·균등성 버그
static Random rnd = new Random();
static int random(int n) { return Math.abs(rnd.nextInt()) % n; } // 음수·편향
// ✅ — 표준 API 한 줄
ThreadLocalRandom.current().nextInt(n);
new Random().ints(1, 0, n).findFirst().getAsInt();
알아두면 좋은 표준 라이브러리¶
| 영역 | 추천 |
|---|---|
| 컬렉션 | java.util (Stream, Collectors, Map.of, List.copyOf) |
| 동시성 | java.util.concurrent (ConcurrentHashMap, ExecutorService, CompletableFuture) |
| 시간 | java.time (Instant, LocalDate, Duration, ZonedDateTime) |
| 입출력 | java.nio.file (Files.readAllLines, Files.walk) |
| 정규식 | java.util.regex (Pattern.compile — 상수 캐싱) |
| HTTP | java.net.http.HttpClient (Java 11+) |
함정¶
- 표준 라이브러리는 매 릴리스마다 진화. Java 11/17/21 신기능을 의식적으로 익혀야 한다.
- 버전 호환. Java 17 기준으로 짜되, 팀이 Java 8이면 막 다른 표준에 의존하면 안 됨.
체크리스트¶
- 직접 만들기 전 1분이라도
java.util/java.util.concurrent/java.time확인했는가 -
ThreadLocalRandom,List.of,Map.copyOf,Files.lines같은 신생 표준에 익숙한가
아이템 60. 정확한 답이 필요하다면 float와 double은 피하라¶
한 줄 요약¶
금액·금융·통화·정확한 소수에 float/double은 금지. BigDecimal, int, long 사용.
비유 — "근사값 자 vs 디지털 캘리퍼"¶
float/double은 자로 대충 잰 값(이진 부동소수점)이라 측정 자체가 부정확합니다.
함정 사례¶
double total = 1.03 - 0.42; // 결과: 0.6100000000000001
System.out.println(0.1 + 0.2 == 0.3); // false
double dollars = 1.00;
int items = 0;
for (double price = 0.10; dollars >= price; price += 0.10) {
dollars -= price;
items++;
}
// 기대: 4개, 실제: 3개 (반올림 오차)
해법¶
// 정확한 답 — BigDecimal
BigDecimal total = new BigDecimal("1.03").subtract(new BigDecimal("0.42"));
// 0.61
// 또는 — int/long, 단위를 가장 작은 단위로 (센트, 원)
int dollarsInCents = 100;
int priceInCents = 10;
함정¶
new BigDecimal(0.1)— double을 받는 생성자는 부정확. 반드시String생성자new BigDecimal("0.1").- 나눗셈에 스케일 안 지정하면
ArithmeticException(무한 소수).divide(b, scale, RoundingMode)사용. compareTo사용 —equals는 스케일 다르면 false (1.0 vs 1.00).
Spring/JPA 현업 메모¶
- JPA 엔티티의 금액 필드는
BigDecimal. 컬럼은DECIMAL(p, s).DOUBLE/FLOAT컬럼 절대 금지. - 화폐는 가능하면
Money값 객체로 캡슐화 (concept-design-patterns 참고).
체크리스트¶
- 금액·정확한 소수에
float/double사용이 없는가 -
BigDecimal은 String 생성자로 만드는가 - DB 컬럼이
DECIMAL(p, s)인가 - 비교는
compareTo인가
아이템 61. 박싱된 기본 타입보다는 기본 타입을 사용하라¶
한 줄 요약¶
성능·NPE·동일성(==) 이슈가 있는 박싱 타입(Integer/Long/Boolean)보다 기본 타입(int/long/boolean) 우선.
비유 — "원본 종이 vs 비닐 케이스"¶
기본 타입은 그 자체로 종이, 박싱은 비닐 케이스에 넣은 것 — 매번 케이스 쓰고 꺼내는 비용이 들고, 케이스가 빈(null) 경우도 생깁니다.
세 가지 함정¶
// ❌ 함정 1 — == 비교
Integer a = 127, b = 127;
System.out.println(a == b); // true (캐시)
Integer c = 128, d = 128;
System.out.println(c == d); // false (캐시 밖)
// ❌ 함정 2 — auto-unboxing NPE
Map<String, Integer> map = ...;
int count = map.get("missing"); // NPE — map.get은 null 반환
// ❌ 함정 3 — 박싱 폭발
Long sum = 0L;
for (long i = 0; i < 1_000_000_000L; i++) {
sum += i; // 매 반복마다 새 Long 객체 생성
}
권장¶
int a = 127, b = 127;
if (a == b) ...; // ✅ 기본 타입 비교
long sum = 0L;
for (long i = 0; i < 1_000_000_000L; i++) sum += i; // ✅
박싱이 필요한 경우 (어쩔 수 없음)¶
- 컬렉션·제네릭 타입 인자:
List<Integer>(Java는List<int>못 함) - null로 "없음"을 표현해야 하는 도메인 필드:
Integer score(시험 안 본 학생은null) - 리플렉션 메서드 호출 매개변수
이 경우에도 비교는 equals 또는 compareTo, NPE 주의.
체크리스트¶
- 핫패스 루프 변수가 박싱 타입인가
-
Map.get()결과를 바로 기본 타입에 대입하지 않았는가 - 박싱 타입
==비교가 없는가
아이템 62. 다른 타입이 적절하다면 문자열 사용을 피하라¶
한 줄 요약¶
문자열은 "아무거나" 담을 수 있어 편하지만, 타입 안전성·검증·도메인 의미를 잃는다. enum·전용 클래스·Optional 등 적절한 타입을 써라.
안티패턴¶
// ❌ — 모든 도메인 값을 String으로
public Order createOrder(String userId, String status, String paymentMethod) { ... }
// 문제
createOrder("u1", "PAID", "CARD");
createOrder("PAID", "u1", "CARD"); // 인자 순서가 뒤바뀌어도 컴파일 통과
권장¶
public Order createOrder(UserId userId, OrderStatus status, PaymentMethod method) { ... }
public record UserId(String value) {
public UserId { Objects.requireNonNull(value); }
}
public enum OrderStatus { DRAFT, PAID, REFUNDED }
public enum PaymentMethod { CARD, BANK_TRANSFER, MOBILE }
문자열로 표현하면 안 되는 것¶
| 잘못 | 올바름 |
|---|---|
String status |
enum OrderStatus |
String userId |
record UserId(String) (식별자 타입) |
String date |
LocalDate |
String price |
BigDecimal 또는 Money |
String 키=값 키=값 (혼합 키) |
전용 클래스/Map |
Spring/JPA 현업 예제¶
// ❌ — String 상태 컬럼
@Column private String status;
// ✅ — enum + JPA 매핑
@Enumerated(EnumType.STRING)
@Column private OrderStatus status;
EnumType.STRING 필수(ORDINAL은 enum 순서 바뀌면 데이터 깨짐 — 6장 아이템 35).
체크리스트¶
- 도메인 상태/타입을
String으로 들고 있지 않은가 - 식별자·금액·날짜에 전용 타입(record/enum/
java.time)을 썼는가
아이템 63. 문자열 연결은 느리니 주의하라¶
한 줄 요약¶
반복적 += 문자열 연결은 O(n²). StringBuilder 또는 String.join/Collectors.joining.
비유 — "복사 종이 vs 라벨 기계"¶
+=는 매번 종이를 통째로 다시 복사하는 격. StringBuilder는 라벨을 한 통에 누적해 마지막에 한 번에 출력.
함정 — O(n²)¶
// ❌ — n이 크면 폭발
String s = "";
for (int i = 0; i < n; i++) {
s += items.get(i); // 매 반복마다 새 String + 복사
}
해법¶
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) sb.append(items.get(i));
String s = sb.toString();
// 더 짧게 — Java 8+
String s = items.stream().collect(Collectors.joining());
String s2 = String.join(", ", items);
예외 — 단순 1~2회 연결은 OK¶
String greeting = "Hello, " + name + "!"; 는 컴파일러가 StringBuilder로 변환. 문제는 루프 안.
Java 9+ — StringBuilder vs StringConcatFactory (invokedynamic)¶
단순 + 연결은 JDK 9부터 invokedynamic 기반으로 더 최적화되었지만, 루프 안 누적은 여전히 StringBuilder가 정답.
체크리스트¶
- 루프 안
+=문자열 연결이 없는가 -
String.join/Collectors.joining을 적극 활용하는가 - 빌더 초기 용량(
new StringBuilder(estimatedSize))을 고려하는가
아이템 64. 객체는 인터페이스를 사용해 참조하라 ⭐¶
한 줄 요약¶
변수·매개변수·반환 타입은 인터페이스 타입으로 선언하라. 구현 교체 자유 + 테스트 자유.
안티패턴¶
권장¶
이제 구현을 LinkedList/TreeMap/ConcurrentHashMap으로 바꾸려면 한 줄만 변경.
적용 영역¶
- 지역 변수, 매개변수, 반환 타입, 필드(가능한 한)
- Spring DI: 인터페이스로 의존, 구현은 주입
// ✅ — 인터페이스에 의존
public class OrderService {
private final OrderRepository repository; // 인터페이스
public OrderService(OrderRepository r) { this.repository = r; }
}
테스트에서 Mock 구현 주입 가능 (concept-spring-core DI 원리).
예외 — 구체 타입이 필요한 경우¶
- 인터페이스가 없는 값 클래스:
String,BigDecimal,LocalDate - 클래스 기반 프레임워크:
Random,HttpServletRequest(구체 타입이 자체로 표준) - 구체 타입의 특별 기능이 필요:
LinkedHashMap의 삽입 순서,LinkedList의 deque
Spring/JPA 현업 예제¶
// JpaRepository<T, ID>가 곧 인터페이스 참조
public interface UserRepository extends JpaRepository<User, Long> { ... }
// 서비스에서
@Service
public class UserService {
private final UserRepository repo; // 인터페이스
}
체크리스트¶
- 컬렉션·맵을 인터페이스(
List/Map/Set) 타입으로 선언했는가 - 서비스 의존이 인터페이스인가
- 구체 타입을 써야 한다면 정당한 이유가 있는가
아이템 65. 리플렉션보다는 인터페이스를 사용하라¶
한 줄 요약¶
리플렉션은 컴파일 타입 검사·성능·예외 처리 모두 잃는다. 인스턴스 생성은 리플렉션, 호출은 인터페이스로.
안티패턴 — 전체 리플렉션¶
// ❌ — 컴파일러가 아무것도 못 잡음
Class<?> cls = Class.forName("java.util.HashSet");
Object set = cls.getDeclaredConstructor().newInstance();
Method m = cls.getMethod("add", Object.class);
m.invoke(set, "hello");
권장 — 생성만 리플렉션, 사용은 인터페이스¶
// ✅
@SuppressWarnings("unchecked")
Set<String> set = (Set<String>) Class.forName(className).getDeclaredConstructor().newInstance();
set.add("hello"); // 일반 메서드 호출 — 컴파일 검사·성능 정상
리플렉션이 정당한 영역¶
- 프레임워크: Spring, JPA, Jackson이 사용자 클래스에 접근할 때
- 플러그인: 런타임에 동적 로딩
- 개발 도구: IDE, 디버거
→ 여러분이 매일 짜는 비즈니스 코드에는 거의 등장하지 않아야 정상.
함정¶
- 예외 4종:
ClassNotFoundException,NoSuchMethodException,IllegalAccessException,InvocationTargetException— 다 처리 필요. - 성능: 일반 호출 대비 수~수십 배 느림.
- JVM 모듈 시스템(Java 9+): 모듈이 막혀 있으면
setAccessible(true)도 실패.
체크리스트¶
- 비즈니스 코드에 리플렉션이 있는가? 다른 길이 없는가
- 생성만 리플렉션이고 사용은 인터페이스인가
- 리플렉션 예외를 모두 처리했는가
아이템 66. 네이티브 메서드는 신중히 사용하라¶
한 줄 요약¶
JNI(C/C++ 호출)는 이식성·안전성·디버깅 모두 잃는다. 거의 모든 경우 자바 라이브러리로 충분.
정당한 경우¶
- OS·하드웨어 저수준 API (그래픽, 시리얼 포트 등)
- 고성능 라이브러리 래퍼 (BLAS, OpenSSL 등)
- 레거시 시스템 통합
함정¶
- 메모리 안전성 상실 (네이티브 영역의 버그는 JVM 통째로 죽임)
- 보안 (
SecurityManager우회) - 디버깅 어려움 (스택이 자바·네이티브 경계 넘음)
- 호출 오버헤드 (잦은 호출이면 자바보다 느릴 수도)
Java 21+ — Foreign Function & Memory API¶
JNI를 대체하는 새 표준이 안정화 중. 새 프로젝트에서 네이티브 호출이 필요하면 FFM을 우선 검토.
체크리스트¶
- 네이티브 호출 외에 길이 정말 없는가
- 자바 표준 라이브러리·JNA·FFM 검토했는가
- 네이티브 호출 횟수를 최소화하도록 묶었는가
아이템 67. 최적화는 신중히 하라 ⭐¶
한 줄 요약¶
측정 없이 최적화하지 마라. 좋은 설계는 그 자체로 충분히 빠르다.
"Premature optimization is the root of all evil." — Donald Knuth
비유 — "병목이 어디 있는지 모르면서 가속 페달"¶
차가 안 가는 이유가 엔진인지 타이어인지 모르고 RPM부터 올리는 것과 같다. 진단(측정)이 먼저.
3대 원칙¶
- 빠른 프로그램을 만들기보다 좋은 프로그램을 만들라 — 좋은 설계가 보통 빠르다.
- 성능을 제한하는 결정을 피하라 — 공개 API·데이터 모델은 한 번 굳으면 못 바꾼다.
- 각 최적화 전후로 성능을 측정하라 — JMH, JFR, async-profiler.
안티패턴¶
권장 워크플로¶
- 깨끗하게 짠다
- 성능 문제가 보고된다 (프로파일링 데이터, 사용자 불만)
- 측정한다 (JMH로 마이크로벤치, async-profiler로 핫스팟)
- 진짜 병목만 고친다
- 다시 측정한다 (예상과 다를 때가 흔하다)
Spring/JPA 현업 메모¶
- N+1 쿼리 — 가장 흔한 진짜 성능 사고. JPA 로그 켜고 측정.
- 인덱스 미사용 —
EXPLAIN으로 확인. - HTTP 응답 시간의 90%는 보통 DB·네트워크. JVM 마이크로 최적화는 그 다음.
체크리스트¶
- 최적화 전후로 측정 데이터가 있는가
- 가독성 희생할 가치가 있을 만큼 핵심 핫패스인가
- 공개 API·DB 스키마처럼 미래 비용이 큰 결정을 신중히 했는가
아이템 68. 일반적으로 통용되는 명명 규칙을 따르라¶
한 줄 요약¶
자바 커뮤니티 표준 명명 규칙을 따르라 — 읽는 사람의 추측이 사라진다.
표 한 장으로¶
| 종류 | 규칙 | 예시 |
|---|---|---|
| 패키지 | 모두 소문자, 도메인 역순 | com.example.order |
| 클래스/인터페이스 | PascalCase | OrderService, Comparable |
| 메서드/필드 | camelCase | findById, userName |
| 상수 | UPPER_SNAKE_CASE | MAX_RETRIES, DEFAULT_PORT |
| 타입 매개변수 | 단일 대문자 | T (Type), E (Element), K/V (Key/Value), R (Result), X (Exception) |
| 지역변수 | 짧은 camelCase | i, name, result |
문법적 규칙 (메서드 의미)¶
| 동작 | 메서드 이름 |
|---|---|
| 값 반환 (변환) | to*, as* (toString, asList) |
| 값 반환 (조회) | get* / find* |
| boolean 반환 | is* / has* / can* |
| 변환 + 새 인스턴스 생성 | from, of, valueOf, instance, getInstance |
| 단일 매개변수 → 같은 타입 | negate, reverse (인스턴스 메서드) |
안티패턴¶
// ❌
public class order_service { ... }
public List<order> Find_By_user(String USERID) { ... }
public boolean checkValid() { return ...; } // bool 반환은 is* 권장
권장¶
public class OrderService { ... }
public List<Order> findByUser(String userId) { ... }
public boolean isValid() { return ...; }
체크리스트¶
- 패키지 소문자·역순 도메인
- 클래스 PascalCase, 메서드·필드 camelCase, 상수 UPPER_SNAKE
- boolean 반환은
is*/has*/can* - 정적 팩터리는
of/from/valueOf등 표준 이름 (Item 1과 연결)
9장 종합 정리¶
한눈에 보는 결정 가이드¶
| 상황 | 선택 |
|---|---|
| 변수 선언 | 사용 직전 + 초기화(57) |
| 컬렉션·배열 순회 | for-each(58) — 제거·교체·평행은 전통 for |
| 새 기능 필요 | 표준 라이브러리 먼저(59) |
| 정확한 답 | BigDecimal/int/long(60) — float/double 금지 |
| 핫패스 숫자 | 기본 타입(61) — 박싱 회피 |
| 도메인 값 | enum·전용 record(62) — 무지성 String 금지 |
| 문자열 누적 | StringBuilder/String.join(63) |
| 객체 참조 | 인터페이스 타입(64) |
| 동적 호출 | 인터페이스 우선, 리플렉션은 최후(65) |
| 최적화 | 측정 후 + 좋은 설계 먼저(67) |
| 명명 | 표준 규칙(68) |
종합 체크리스트 (코드 리뷰용)¶
- 메서드 상단에 변수 선언 몰림 없음
- 컬렉션 순회 기본이 for-each
- 금액·정확한 소수에 float/double 없음, BigDecimal은 String 생성자
- 핫패스에 박싱·
+=문자열 연결 없음 - 도메인 상태가 String이 아니라 enum
- 변수·매개변수·반환 타입이 인터페이스
- 비즈니스 코드에 리플렉션 없음
- 미세 최적화 대신 N+1·인덱스 점검부터
- 명명 규칙·boolean is/has 준수
종합 퀴즈¶
Q1. new BigDecimal(0.1)이 부정확한 이유는?
A. double 매개변수 생성자는 인자로 받은 double 값을 그대로 이진 부동소수점으로 해석한다. 0.1은 이진으로 정확히 표현이 안 되므로 이미 부정확한 값을 그대로 옮긴다. new BigDecimal("0.1") 처럼 String 생성자를 써야 한다.
Q2. 박싱 타입을 ==로 비교하면 왜 위험한가?
A. Integer.valueOf가 -128~127 캐시를 쓰므로 작은 값은 같은 객체 참조라 ==가 우연히 true가 되지만, 그 범위를 벗어나면 false가 된다. 박싱 타입은 항상 equals 또는 compareTo로 비교.
Q3. 변수 타입을 인터페이스로 선언하는 가장 큰 이점은?
A. 구현 교체 자유 — 성능·테스트·요구사항에 따라 구현(HashMap → ConcurrentHashMap)을 바꿔도 호출 코드 변경이 한 줄. Spring DI에서 mock 주입도 같은 원리.
Q4. Knuth의 "premature optimization is the root of all evil"이 9장에서 의미하는 바는?
A. 측정 없이 미세 최적화에 가독성·구조를 희생하면, 실제 병목은 다른 곳에 있을 가능성이 높다. 좋은 설계 + 측정 후 진짜 핫스팟만 최적화한다. 공개 API·데이터 모델 결정은 미래 비용이 크니 신중히.
Q5. JPA에서 enum 컬럼은 왜 EnumType.STRING이 안전한가?
A. 기본값 ORDINAL은 enum 정의 순서(0,1,2...)를 DB에 저장하므로, 나중에 enum 상수의 순서가 바뀌면 기존 데이터가 다른 의미로 해석된다. STRING은 이름으로 저장되어 순서 변경에 안전 (6장 Item 35와 9장 Item 62 결합).
다음 장 예고 — 10장: 예외¶
예외는 언제 던지고, 누가 처리하며, 어떻게 문서화하는가 — 9개 아이템(Item 69~77). 검사 예외 vs 비검사 예외의 사용 기준, 표준 예외, 추상화 수준 맞추기, 실패 원자성, "예외를 무시하지 마라"까지. Spring의 @ControllerAdvice·@Transactional 롤백 정책과 직결됩니다.
이어서 만들까요? (10장으로 진행 / 11장 동시성으로 점프 / 지금까지 만든 장들을 통합 교재로 묶기)