리팩터링 실전 강의 교재¶
11장 — API 리팩터링¶
대상: Java/Spring 백엔드 입문~중급 수강생 형식: 개념 → 비유 → Before/After → 절차 → 함정 → 체크리스트 → 퀴즈 전제 환경: Java 17+, Spring Boot 3.x
0. 이 장을 시작하기 전에¶
0.1 학습 목표¶
- 공개 API의 계약(시그니처·매개변수·반환·예외) 을 다듬는 13가지.
- ★ CQS(질의-변경 분리, 11.1) — 동시성·테스트·예측 가능성의 기반.
- 생성자·플래그·예외 처리의 표준 변환.
- "한 번 공개되면 영원하다" 의 책임감.
0.2 큰 그림 — 6 묶음¶
[ CQS·매개변수 ] [ 변경 형태 ] [ 호출 형태 ]
11.1 질의-변경 분리 (CQS) ★ 11.4 객체 통째로 넘기기 11.9 함수 → 명령
11.2 함수 매개변수화 11.5 매개변수 → 질의 11.10 명령 → 함수 (반대)
11.3 플래그 인수 제거 11.6 질의 → 매개변수 (반대)
11.7 세터 제거
[ 생성·반환·예외 ]
11.8 생성자 → 팩터리
11.11 수정된 값 반환
11.12 오류 코드 → 예외
11.13 예외 → 사전 확인
비유 — API는 "식당 메뉴판"입니다.
한 번 인쇄해 손님이 외운 메뉴는 함부로 못 바꿈. 메뉴 이름·옵션·세트 구성·가격 단위 — 처음 설계가 영원.
0.3 현업에서 왜 중요한가¶
- Controller·Service·Repository의 public 메서드 = 회사의 자산.
- Effective Java 51·52·53·56과 거의 1:1 대응.
- API 변경 한 번에 호출자 N+1 PR — 마이그레이션 비용 큼.
11.1 질의 함수와 변경 함수 분리하기 (Separate Query from Modifier) ★¶
한 줄 정의¶
값을 반환하는 함수는 부작용을 가지면 안 된다 (CQS — Command-Query Separation).
// Before — getter인데 부작용
public String getTotalOutstandingAndSendBill() {
double total = customer.totalOutstanding();
sendBillEmail(customer); // ❌ 부작용
return total;
}
// After — 분리
public double totalOutstanding() {
return customer.totalOutstanding();
}
public void sendBill() {
sendBillEmail(customer);
}
동기¶
- 호출 순서·횟수가 결과에 영향 없음 → 추론·테스트·캐싱 모두 쉬워짐.
- 부작용 있는 메서드는 명령 (
sendBill), 없는 메서드는 질의 (totalOutstanding) — 이름으로 구분.
함정¶
- "현재 값을 가져오면서 카운터 증가" 같은 통계용 부작용도 위험. 진짜 조회와 분리.
오브젝트 연결¶
6.04 "명령-쿼리 분리 원칙" 과 같은 메시지.
11.2 함수 매개변수화하기 (Parameterize Function)¶
한 줄 정의¶
비슷한 일을 하는 여러 함수를 매개변수화한 하나 로.
// Before
double tenPercentRaise(Employee e) { return e.salary * 1.10; }
double fivePercentRaise(Employee e) { return e.salary * 1.05; }
// After
double raise(Employee e, double rate) { return e.salary * (1 + rate); }
동기¶
- 중복 제거.
- 의도가 매개변수로 명시.
함정¶
- 매개변수가 너무 다양해지면 함수 책임이 흐려짐 — 매개변수 객체(6.8) 고려.
11.3 플래그 인수 제거하기 (Remove Flag Argument)¶
한 줄 정의¶
boolean 또는 enum 플래그가 함수 동작을 갈라놓는다면 별도 함수 로.
// Before
shipOrder(order, true); // true가 뭐?
shipOrder(order, false);
// After
shipOrderRegular(order);
shipOrderPriority(order);
동기¶
- 호출부에서 의도가 한 번에 보임.
- 함수 본문이 단순해짐.
Effective Java 연결¶
Item 51(boolean 매개변수보다는 enum, 또는 분리).
함정¶
- 플래그가 3개 이상 조합되면 함수 폭발 — 그땐 매개변수 객체나 빌더 패턴 고려.
11.4 객체 통째로 넘기기 (Preserve Whole Object)¶
한 줄 정의¶
객체의 여러 필드를 따로 넘기지 말고 객체 자체 를 넘김.
// Before
if (room.withinPlan(plan.low, plan.high)) { ... }
// After
if (room.withinPlan(plan)) { ... }
동기¶
- 매개변수 줄어듦.
- 호출 후 객체에 새 필드가 추가되어도 시그니처 안 바뀜.
- 그 객체 안의 행동 활용 가능.
함정¶
- 결합도 증가 — 받는 함수가 객체 전체를 알게 됨. 받는 쪽이 일부만 필요하면 그대로.
11.5 매개변수를 질의 함수로 바꾸기 (Replace Parameter with Query)¶
한 줄 정의¶
매개변수가 함수 안에서 다른 매개변수로 유도 가능 하면 제거.
// Before
double finalPrice(double basePrice, double discountLevel) { ... }
finalPrice(basePrice, basePrice > 1000 ? 2 : 1); // 호출자가 매번 계산
// After
double finalPrice(double basePrice) {
int discountLevel = basePrice > 1000 ? 2 : 1;
// ...
}
동기¶
- 호출 단순.
- 계산 로직 한 곳.
함정¶
- 호출자마다 다른 값이 필요해질 가능성이 있으면 매개변수 유지.
11.6 질의 함수를 매개변수로 바꾸기 (Replace Query with Parameter)¶
한 줄 정의¶
함수 안의 불순한 의존(전역 상태·외부 시스템 조회) 을 매개변수로 빼냄. 11.5의 반대.
// Before — 함수 안에서 전역 상태 조회
double targetTemperature(HeatingPlan plan) {
int currentTemp = thermostat.currentTemperature(); // 외부 의존
return plan.adjustTo(currentTemp);
}
// After — 의존을 매개변수로
double targetTemperature(HeatingPlan plan, int currentTemp) {
return plan.adjustTo(currentTemp);
}
동기¶
- 함수가 순수 해짐 — 테스트·재사용 쉬움.
- 의존 주입(DI) 의 한 형태.
11.7 세터 제거하기 (Remove Setter)¶
한 줄 정의¶
생성 후 변경되면 안 되는 필드의 setter는 제거.
// Before
public class Person {
private String name;
public void setName(String name) { this.name = name; }
}
// After
public class Person {
private final String name;
public Person(String name) { this.name = name; }
public String name() { return name; }
}
동기¶
- 불변성 → 동시성 안전, 추론 쉬움.
- "필드는 생성 시점에만 결정" 이라는 의도가 코드에 명시.
Effective Java 연결¶
Item 17(변경 가능성 최소화).
Spring/JPA 현업¶
JPA 엔티티의 setter 남용은 사고 단골. 변경이 필요한 행동만 메서드로, 나머지는 setter 제거.
// ❌ — 무지성 @Setter
@Setter
public class Order {
private OrderStatus status;
}
order.setStatus(OrderStatus.CANCELLED); // 검증·이벤트 없음
// ✅
public class Order {
private OrderStatus status;
public void cancel() {
if (status != OrderStatus.PAID) throw new IllegalStateException();
this.status = OrderStatus.CANCELLED;
registerEvent(new OrderCancelledEvent(this));
}
}
11.8 생성자를 팩터리 함수로 바꾸기 (Replace Constructor with Factory Function)¶
한 줄 정의¶
new Foo(...) 대신 이름 있는 정적 메서드 Foo.of(...).
// Before
Employee e = new Employee("Sam", "Engineer", 5);
// After
Employee e = Employee.engineer("Sam", 5);
Employee m = Employee.manager("Sam", 5);
동기¶
- 의도가 이름으로 드러남.
- 서브타입 반환 가능 (
Employee타입으로 받지만 실제Engineer). - 생성 비용 캐싱·재사용 (예:
Boolean.valueOf).
Effective Java 연결¶
Item 1(정적 팩터리). 동일 권고.
11.9 함수를 명령으로 바꾸기 (Replace Function with Command)¶
한 줄 정의¶
복잡한 함수를 상태를 가진 객체(Command) 로 승격.
// Before — 매개변수 많고 본문이 긴 함수
double score(Candidate candidate, MedicalExam exam, ScoringGuide guide) {
// 100줄
}
// After — Command 객체
public class Scorer {
private final Candidate candidate;
private final MedicalExam exam;
private final ScoringGuide guide;
public Scorer(Candidate c, MedicalExam e, ScoringGuide g) { ... }
public double execute() {
// 100줄을 단계 메서드로 분해
scoreBasic();
scoreModifiers();
return total;
}
private void scoreBasic() { ... }
private void scoreModifiers() { ... }
}
동기¶
- 큰 함수를 작은 private 메서드 로 분해 가능 (필드 = 상태 공유).
- GoF Command 패턴.
- 실행 큐·재시도·취소 가능.
함정¶
- 단순 함수까지 Command화는 과함. 본문이 길고 단계 분해가 필요할 때.
11.10 명령을 함수로 바꾸기 (Replace Command with Function)¶
한 줄 정의¶
복잡도가 사라진 Command를 다시 단순 함수로. 11.9의 반대.
11.11 수정된 값 반환하기 (Return Modified Value)¶
한 줄 정의¶
값을 변경하는 함수가 변경된 값을 반환 하게 → 호출자가 의도 인지.
// Before
double total;
calculateAdjustments(orders, total); // total이 바뀜? 안 바뀜?
// After
double total = calculateAdjustments(orders); // 명확
동기¶
- 가변 매개변수보다 반환값으로 새 값 이 의도 명확.
- 함수형 스타일 일관.
11.12 오류 코드를 예외로 바꾸기 (Replace Error Code with Exception)¶
한 줄 정의¶
함수가 -1 같은 오류 코드 를 반환하면 예외로 전환.
// Before
int code = doSomething();
if (code == -1) { /* 오류 처리 */ }
else if (code == -2) { /* 다른 오류 */ }
// After
try {
doSomething();
} catch (NetworkException e) { ... }
catch (ValidationException e) { ... }
동기¶
- 오류 코드는 무시 가능 — 호출자가 검사 잊으면 사고.
- 예외는 타입 시스템이 강제 (checked) 또는 로그·디버깅 정보 풍부 (stack trace).
Effective Java 연결¶
Item 70(checked vs unchecked).
11.13 예외를 사전확인으로 바꾸기 (Replace Exception with Precheck)¶
한 줄 정의¶
예측 가능한 조건 으로 발생하는 예외는 미리 체크. 11.12의 반대.
// Before
try {
return list.get(i);
} catch (IndexOutOfBoundsException e) {
return null;
}
// After
if (i < list.size()) return list.get(i);
return null;
동기¶
- 예외는 진짜 예외 상황만 (entity-effective-java Item 69).
- 사전 확인이 가능한 조건에 예외는 비싸고 의도 흐림.
11.12 vs 11.13 — 균형¶
| 11.12 | 11.13 |
|---|---|
| 예측 어려운 오류 (네트워크 끊김) | 예측 가능한 조건 (인덱스 범위) |
| 정상 흐름과 분리 가치 | 사전 검사가 명료 |
| → 예외 | → if-check |
11장 종합 정리¶
한눈에 보는 결정 가이드¶
| 상황 | 선택 |
|---|---|
| 함수가 값을 반환하면서 부작용 | CQS 분리(11.1) ★ |
| 비슷한 함수 여러 개 | 함수 매개변수화(11.2) |
| boolean 인자가 동작 분기 | 플래그 제거(11.3) |
| 같은 객체의 여러 필드 전달 | 객체 통째로(11.4) |
| 매개변수가 유도 가능 | 매개변수 → 질의(11.5) |
| 함수 안 외부 의존 | 질의 → 매개변수(11.6) |
| 생성 후 변경 X 필드의 setter | 세터 제거(11.7) |
| 생성 의도가 이름으로 드러나야 | 생성자 → 팩터리(11.8) |
| 큰 함수를 단계 분해 | 함수 → 명령(11.9) |
| 함수가 매개변수를 변경 | 수정된 값 반환(11.11) |
| 함수가 오류 코드 반환 | 예외로 변환(11.12) |
| 예측 가능한 조건의 예외 | 사전 확인으로(11.13) |
종합 체크리스트¶
- getter처럼 보이는 함수가 부작용을 가지고 있지 않은가
- boolean 매개변수가 함수 동작을 갈라놓고 있는가
- JPA 엔티티의 모든 필드에 setter가 노출되어 있는가
-
new Order(...)대신Order.of(...)가 더 의도적인가 - 오류를 -1 같은 코드로 반환하는 메서드가 있는가
- try/catch가 정상 흐름 제어로 쓰이고 있지 않은가
종합 퀴즈¶
Q1. CQS(11.1)가 ★ 인 이유?
A. 부작용 없는 질의 = 호출 순서·횟수가 결과에 영향 없음 → 추론·테스트·캐싱·동시성 모두 자유. 부작용 있는 명령은 명확히 따로. 동시성·예측 가능성의 기반.
Q2. JPA 엔티티에 무지성 @Setter 가 위험한 이유?
A. setter는 모든 필드 변경을 허용 → 도메인 불변식·상태 전이 규칙을 우회. 결제 안 한 주문이 갑자기 setStatus(PAID) 가 가능. 변경은 의미 있는 메서드(cancel·confirm·pay) 로만, setter는 제거. 11.7의 가장 흔한 사례.
Q3. 11.12와 11.13이 둘 다 있는 이유?
A. 균형. 예측 어려운 오류 → 예외, 예측 가능한 조건 → 사전 확인. 양극단 모두 안티패턴 (모든 걸 try-catch, 모든 걸 if-check). 상황에 맞춰 양방향 도구.
Q4. 11.8 (생성자 → 팩터리) 와 Effective Java Item 1의 관계?
A. 같은 권고의 다른 표현. Effective Java Item 1이 "왜·언제" 라면, 11.8은 "어떻게 변환" 의 절차. 의도가 이름으로 드러남(of/from/valueOf), 서브타입 반환, 캐싱·재사용 등 동일.
다음 장 예고 — 12장: 상속 다루기¶
상속은 양날의 검 — 잘 쓰면 강력, 잘못 쓰면 LSP 위반·취약한 기반 클래스. 11가지 기법으로 메서드·필드를 위·아래로 옮기고, 슈퍼클래스를 추출하고, 마지막 ★★ 서브클래스를 위임으로 바꾸기 까지. Effective Java Item 18·오브젝트 11장과 직결.