리팩터링 실전 강의 교재¶
6장 — 기본적인 리팩터링¶
대상: Java/Spring 백엔드 입문~중급 수강생 형식: 개념 → 비유 → Before/After → 절차 → 함정 → 체크리스트 → 퀴즈 전제 환경: Java 17+, Spring Boot 3.x, IntelliJ
0. 이 장을 시작하기 전에¶
0.1 학습 목표¶
- 모든 리팩터링의 기본기 11개 를 손에 익힌다.
- ★ 5개(함수 추출·변수 추출·함수 선언 바꾸기·매개변수 객체·단계 쪼개기)는 무의식적으로 적용 가능 수준까지.
- 각 기법의 절차(작은 단계) 를 따르는 습관 — 어디서 멈춰도 동작 보존.
0.2 큰 그림 — "분해와 명명"¶
[ 분해 ] [ 명명 ] [ 묶기 ]
6.1 함수 추출 ★ 6.5 함수 선언 바꾸기 ★ 6.8 매개변수 객체
6.2 함수 인라인 (반대) 6.7 변수 이름 바꾸기 6.9 여러 함수 → 클래스
6.3 변수 추출 ★ 6.6 변수 캡슐화 6.10 여러 함수 → 변환 함수
6.4 변수 인라인 (반대) 6.11 단계 쪼개기 ★
비유 — 6장은 "주방의 칼·도마·계량컵"입니다.
모든 리팩터링은 결국 분해(작게)·명명(이름)·묶기(응집)의 조합. 화려한 기법(다형성·합성)도 이 기본기 위에 얹어지는 것.
0.3 현업에서 왜 중요한가¶
- 코드 리뷰의 80%는 함수 추출·이름 바꾸기·매개변수 정리 가 처방.
- IDE 단축키 매핑이 가장 잘 되어 있어 자동화 가성비 폭증.
- 7~12장 카탈로그는 거의 모두 6장 위에 쌓임.
6.1 함수 추출하기 (Extract Function) ★¶
한 줄 정의¶
긴 함수의 한 단락을 의도가 드러나는 이름의 함수 로 분리.
비유 — "레시피에서 부분 동작 떼어내기"¶
긴 레시피 "양파를 까서, 다지고, 기름에 볶고, 토마토를 넣고, 끓이고, 간을 본다" 를 "양파 볶기 → 토마토 졸이기 → 간 보기" 세 함수로.
Before / After¶
// Before
public void printOwing(Invoice invoice) {
printBanner();
double outstanding = 0;
for (Order o : invoice.orders) outstanding += o.amount; // ← 의도 단락
System.out.println("name: " + invoice.customer); // ← 의도 단락
System.out.println("amount: " + outstanding);
}
// After
public void printOwing(Invoice invoice) {
printBanner();
double outstanding = calculateOutstanding(invoice);
printDetails(invoice, outstanding);
}
private double calculateOutstanding(Invoice invoice) {
double result = 0;
for (Order o : invoice.orders) result += o.amount;
return result;
}
private void printDetails(Invoice invoice, double outstanding) {
System.out.println("name: " + invoice.customer);
System.out.println("amount: " + outstanding);
}
동기¶
- 의도와 구현 분리 — 함수 이름이 "무엇" 을, 본문이 "어떻게" 를.
- 같은 동작 재사용 가능해짐.
- 테스트하기 쉬워짐 — 작은 함수가 단위.
언제 추출하나¶
- "이 코드가 무슨 일을 하는지 한 줄 주석으로 설명할 수 있다" → 그 주석을 함수 이름으로.
- 한 함수 안에 빈 줄로 구분된 블록이 2개 이상.
절차¶
- 새 함수 만들고 의도를 드러내는 이름 부여.
- 추출할 코드를 새 함수로 옮김.
- 옮긴 코드가 쓰는 지역 변수를 매개변수로 추가.
- 옮긴 코드 자리에 새 함수 호출 삽입.
- 테스트.
함정¶
- 매개변수가 7개+ 되면 → 추출 잘못 잘랐거나, 매개변수 객체(6.8)가 필요한 신호.
- 부작용을 가진 함수 를 별생각 없이 추출하면 호출 순서가 바뀌어 사고. 11.1(CQS)과 같이 고려.
IDE¶
- IntelliJ:
Ctrl/Cmd + Alt + M— Extract Method. 자동으로 매개변수·반환값 분석.
6.2 함수 인라인하기 (Inline Function)¶
한 줄 정의¶
과한 분해·성의 없는 함수 를 본문으로 다시 합치기. 6.1의 반대.
Before / After¶
// Before — 함수가 너무 작아 본문보다 호출이 길다
boolean moreThanFiveLateDeliveries(Driver d) {
return d.numberOfLateDeliveries > 5;
}
boolean isInExperienced(Driver d) {
return moreThanFiveLateDeliveries(d); // 위임만
}
// After
boolean isInExperienced(Driver d) {
return d.numberOfLateDeliveries > 5;
}
동기¶
- 추출했는데 의미가 안 살아남 (성의 없는 요소 — 악취 3.14).
- 위임만 하는 중간 함수가 가독성 해침.
절차¶
- 다형성 메서드가 아닌지 확인 (오버라이드되면 인라인 X).
- 호출 지점들 찾기.
- 각 호출 지점에 함수 본문을 채워 넣음.
- 원본 함수 제거.
IDE¶
- IntelliJ:
Ctrl/Cmd + Alt + N— Inline Method.
6.3 변수 추출하기 (Extract Variable) ★¶
한 줄 정의¶
복잡한 식의 일부를 의도가 드러나는 변수 로 분리.
Before / After¶
// Before
double price = quantity * itemPrice
- Math.max(0, quantity - 500) * itemPrice * 0.05
+ Math.min(quantity * itemPrice * 0.1, 100);
// After
double basePrice = quantity * itemPrice;
double quantityDiscount = Math.max(0, quantity - 500) * itemPrice * 0.05;
double shipping = Math.min(basePrice * 0.1, 100);
double price = basePrice - quantityDiscount + shipping;
동기¶
- 디버거에서 중간 값 확인 가능.
- 의도가 식이 아니라 변수 이름 으로 드러남.
- 6.1 함수 추출의 전 단계 — 변수가 충분히 모이면 함수로.
절차¶
- 추출할 식의 부작용 여부 확인 (없어야 안전).
- 변경 불가 변수(
final또는var)로 새 변수 선언, 추출할 식 대입. - 원래 식을 새 변수로 교체.
- 테스트.
IDE¶
- IntelliJ:
Ctrl/Cmd + Alt + V— Extract Variable.
6.4 변수 인라인하기 (Inline Variable)¶
한 줄 정의¶
이름이 의미를 보태지 않는 변수를 식으로 직접 대체. 6.3의 반대.
// Before
double basePrice = anOrder.basePrice(); // 그저 한 번 쓰는 임시
return basePrice > 1000;
// After
return anOrder.basePrice() > 1000;
언제¶
- 변수가 식보다 의미를 추가하지 않을 때.
- 다른 리팩터링(예: 함수 추출) 의 전 단계로 본문을 잠시 단순화할 때.
6.5 함수 선언 바꾸기 (Change Function Declaration) ★¶
한 줄 정의¶
함수의 이름·매개변수·반환 을 바꿔 계약을 정정.
동기¶
- 이름이 의도를 못 드러냄 (악취 3.1 기이한 이름).
- 매개변수 순서·타입이 부적절.
- 매개변수 추가·제거.
Before / After (이름 변경)¶
// Before
void circum(double radius) { ... } // circum이 뭐?
// After
void circumference(double radius) { ... }
Before / After (매개변수 추가 — 안전 절차)¶
이름·시그니처 변경은 호출자가 많을수록 위험. 안전 절차 두 가지.
간단 절차 (호출자 적음, IDE 도움 가능)¶
- 새 시그니처로 한 번에 변경.
- 모든 호출자 갱신.
- 테스트.
마이그레이션 절차 (호출자 많음, 라이브러리 API)¶
- 새 함수 만듦 (목표 시그니처).
- 원본 함수를 새 함수로 위임 하게 변경.
- 호출자들을 하나씩 새 함수로 이전.
- 모든 호출이 새 함수로 옮겨졌으면 원본 제거.
Spring 현업¶
// 1단계
@GetMapping("/orders")
public List<Order> orders(@RequestParam String userId) { ... } // 원본
// 2단계 — 새 함수 추가
@GetMapping("/v2/orders")
public List<Order> ordersByUser(UserId userId) { ... } // 목표
// 3단계 — 호출자 마이그레이션 + 4단계 원본 deprecate/제거
@Deprecated(since = "...") 와 결합하면 안전 진행.
함정¶
- 라이브러리 공개 API는 한 번 바뀌면 사용자에 폭탄. 마이그레이션 절차 필수.
- 동적 참조(SpEL·JSON 키)는 자동 Rename이 못 잡음.
IDE¶
- IntelliJ:
Ctrl/Cmd + F6— Change Signature.
6.6 변수 캡슐화하기 (Encapsulate Variable)¶
한 줄 정의¶
직접 접근하는 변수를 getter/setter 또는 불변 인터페이스 뒤로 숨김.
// Before
public class Config {
public static int port = 8080; // 누구나 변경 가능
}
// After
public class Config {
private static int port = 8080;
public static int port() { return port; }
public static void setPort(int p) {
if (p < 1 || p > 65535) throw new IllegalArgumentException();
port = p;
}
}
동기¶
- 전역 데이터 (악취 3.5) 처방의 첫 단계.
- 변경 추적·검증·로깅 가능해짐.
- 나중에 가변→불변 전환의 발판.
절차¶
- 변수에 접근하는 함수(get/set) 만들기.
- 모든 외부 참조를 함수 호출로 변경.
- 변수의 가시성 제한 (
private). - 테스트.
6.7 변수 이름 바꾸기 (Rename Variable)¶
한 줄 정의¶
이름이 의도를 못 드러내면 즉시 바꿔라.
IDE¶
- IntelliJ:
Shift + F6— Rename. 전체 코드베이스 일괄 변경, 동적 참조 경고까지.
함정¶
- 변수가 여러 의미 로 쓰이고 있으면 이름 바꾸기만으로는 부족 — 변수 쪼개기(9.1) 필요.
- 짧은 임시 변수(
i,n) 는 그대로 둬도 좋음 — 짧은 범위에선 가독성 손해 없음.
6.8 매개변수 객체 만들기 (Introduce Parameter Object)¶
한 줄 정의¶
함께 다니는 매개변수 묶음(데이터 뭉치 — 악취 3.10) 을 하나의 객체/record 로.
// Before
boolean amountInRange(int amount, int min, int max);
// After
boolean amountInRange(int amount, Range range);
public record Range(int min, int max) {
public Range {
if (min > max) throw new IllegalArgumentException();
}
public boolean includes(int v) { return min <= v && v <= max; }
}
동기¶
- 매개변수 4개+ (악취 3.4) 처방.
- 묶음 자체에 행동 을 부여 가능 (
Range.includes). - 도메인 개념을 코드에 명시.
절차¶
- 후보 record/클래스 만들기.
- 함수 선언 바꾸기(6.5) — 매개변수 자리에 새 타입.
- 호출자들을 새 객체 생성으로 변경.
- 함수 본문에서 개별 인자 대신 객체 메서드 사용.
- 묶음을 쓰는 다른 함수들도 단계적으로 옮김.
Java 17 record와 궁합¶
record는 자동 equals/hashCode/toString/생성자 → 6.8의 최적 도구.
6.9 여러 함수를 클래스로 묶기 (Combine Functions into Class)¶
한 줄 정의¶
같은 데이터를 공유하며 함께 변하는 함수들을 한 클래스로.
// Before — 함수 3개가 같은 데이터에 작용
double baseCharge(Reading r) { ... }
double taxableCharge(Reading r) { ... }
double calcAmount(Reading r) { ... }
// After
public class Bill {
private final Reading reading;
public Bill(Reading r) { this.reading = r; }
public double baseCharge() { ... }
public double taxableCharge() { ... }
public double calcAmount() { ... }
}
동기¶
- 함께 변하는 함수 = 함께 있는 게 자연스러움 (응집도).
- 매개변수 반복 사라짐.
- 데이터 클래스 (악취 3.22) 에 행동을 끌어오는 처방.
절차¶
- 공통 데이터를 받는 새 클래스 만들기.
- 함수들을 차례로 옮기기 (함수 옮기기 — 8.1).
- 각 함수의 데이터 매개변수를 클래스 필드로 대체.
- 테스트.
6.10 여러 함수를 변환 함수로 묶기 (Combine Functions into Transform)¶
한 줄 정의¶
원본 데이터를 받아 파생 정보를 채워 반환 하는 변환 함수로 묶기.
// Before — 같은 reading에서 여러 계산을 흩어 호출
double base = baseCharge(reading);
double tax = taxableCharge(reading);
double amount = calcAmount(reading);
// After
EnrichedReading enriched = enrich(reading);
double base = enriched.baseCharge();
double tax = enriched.taxableCharge();
6.9 vs 6.10¶
| 6.9 클래스 묶기 | 6.10 변환 함수 묶기 |
|---|---|
| 함께 변하는 행동 | 함께 계산되는 파생값 |
| 원본 데이터 가변 가능 | 원본 데이터 불변 가정 (함수형) |
| OO 친화 | 함수형/파이프라인 친화 |
동기¶
- 같은 파생값이 여러 곳에서 따로 계산 → 변환 함수 한 곳에 모으기.
- 계산 결과 일관성 보장.
6.11 단계 쪼개기 (Split Phase) ★¶
한 줄 정의¶
서로 다른 두 가지 일을 하는 함수를 명확한 두 단계 로 분리.
Before / After¶
// Before — 파싱 + 계산 + 출력이 한 함수
String orderSummary(String csv) {
// CSV 파싱
String[] tokens = csv.split(",");
String name = tokens[0];
int qty = Integer.parseInt(tokens[1]);
int price = Integer.parseInt(tokens[2]);
// 계산
int total = qty * price;
// 출력
return name + ": " + qty + "개 × " + price + "원 = " + total + "원";
}
// After — 2단계 분리
OrderData parse(String csv) {
String[] t = csv.split(",");
return new OrderData(t[0], Integer.parseInt(t[1]), Integer.parseInt(t[2]));
}
String render(OrderData o) {
int total = o.qty() * o.price();
return o.name() + ": " + o.qty() + "개 × " + o.price() + "원 = " + total + "원";
}
동기¶
- 뒤엉킨 변경 (악취 3.7) 처방 — 파싱 형식 변경 / 출력 형식 변경이 한 함수에 묶이지 않음.
- 1장의
statement()→htmlStatement()가 정확히 이 리팩터링. - 단계가 분리되면 각 단계 단독 테스트 가능.
절차¶
- 두 번째 단계에 해당하는 코드를 함수 추출(6.1).
- 첫 단계가 두 번째 단계에 넘길 중간 데이터 구조 정의.
- 첫 단계가 그 구조를 만들도록 변경.
- 두 번째 단계는 그 구조만 받게 변경.
Spring 현업¶
// Before — Controller 안에 파싱·검증·비즈니스·DTO 변환 다 섞임
@PostMapping
public OrderResponse create(@RequestBody Map<String, Object> raw) {
// 파싱·검증·서비스 호출·응답 변환 한 함수에
}
// After — 입력 DTO → 도메인 → 응답 DTO 3단계
@PostMapping
public OrderResponse create(@Valid @RequestBody CreateOrderRequest req) {
Order order = service.create(req.toCommand());
return OrderResponse.from(order);
}
6장 종합 정리¶
한눈에 보는 결정 가이드¶
| 상황 | 선택 |
|---|---|
| 긴 함수에 의도 단락이 보임 | 함수 추출(6.1) ★ |
| 함수가 위임만 함 | 함수 인라인(6.2) |
| 복잡한 식의 의미가 안 드러남 | 변수 추출(6.3) ★ |
| 임시 변수가 의미를 못 보탬 | 변수 인라인(6.4) |
| 이름·시그니처가 잘못됨 | 함수 선언 바꾸기(6.5) ★ |
| 전역·공용 변수 | 변수 캡슐화(6.6) |
| 변수 이름이 의도 못 드러냄 | 변수 이름 바꾸기(6.7) |
| 매개변수 묶음·4개+ | 매개변수 객체(6.8) |
| 같은 데이터에 작용하는 함수들 | 여러 함수를 클래스로(6.9) |
| 같은 파생값이 여러 곳에서 계산 | 여러 함수를 변환 함수로(6.10) |
| 함수가 두 단계로 분리 가능 | 단계 쪼개기(6.11) ★ |
종합 체크리스트 (코드 리뷰용)¶
- 의도가 한 줄로 설명 가능한 단락 → 함수 추출
- 위임만 하는 함수 → 인라인 검토
- 디버거에서 보고 싶은 중간 값 → 변수 추출
- 이름이 의도를 정확히 드러냄
- 매개변수 4개+ → 매개변수 객체
- 같이 변하는 함수들 → 한 클래스로
- 한 함수에 파싱·계산·포맷이 섞임 → 단계 쪼개기
종합 퀴즈¶
Q1. 함수 추출(6.1)이 "모든 리팩터링의 기본기"라 불리는 이유?
A. 다른 모든 리팩터링이 함수 추출의 결과 위에 쌓이기 때문. 추출이 없으면 옮길(8.1)·당길/내릴(12.1·12.4)·다형성으로 바꿀(10.4) 단위가 없다. 함수가 작아져야 모든 카탈로그가 적용 가능.
Q2. 함수 추출과 함수 인라인이 둘 다 카탈로그에 있는 이유?
A. 문맥에 따라 어제 옳던 추출이 오늘 과한 분해(악취 3.14)가 되기 때문. 양방향 도구로 가져야 상황에 맞게 적용 가능. 5장의 "쌍" 원칙.
Q3. 매개변수 4개+가 단순한 가독성 문제가 아닌 이유?
A. 매개변수 묶음 자체가 이름 없는 도메인 개념 인 경우가 많기 때문 (예: start/end → Period). 객체화 = 도메인 발견. 그래서 6.8은 가독성을 넘어 모델링 활동.
Q4. 단계 쪼개기(6.11)가 1장의 핵심 리팩터링인 이유?
A. statement() 가 "계산" 과 "출력" 을 한 함수에 묶고 있어, HTML 출력 추가가 거대 복붙을 부르는 상황이었음. 단계 쪼개기로 계산 데이터 구조를 만들어 두면 HTML 출력은 한 함수 추가만으로 끝. 새 기능 추가 비용이 한 자릿수로 줄어드는 리팩터링.
다음 장 예고 — 7장: 캡슐화¶
기본 리팩터링(6장)이 손에 익었으니, OO의 핵심인 캡슐화 로 갑니다. 레코드/컬렉션을 어떻게 노출 안 하고 행동만 외부에 드러내는지, 기본형을 객체로 승격해 도메인을 풍부하게 만드는 법, 클래스 추출 로 거대 클래스를 자르는 절차, 위임 숨기기/중개자 제거 의 균형까지.