기능 이동
- 8.1 함수 옮기기
- 8.2 필드 옮기기
- 8.3 문장을 함수로 옮기기
- 8.4 문장을 호출한 곳으로 옮기기
- 8.5 인라인 코드를 함수 호출로 바꾸기
- 8.6 문장 슬라이드하기
- 8.7 반복문 쪼개기
- 8.8 반복문을 파이프라인으로 바꾸기
- 8.9 죽은 코드 제거하기
"좋은 소프트웨어 설계의 핵심은 모듈성이고, 모듈성이 좋으려면 관련 있는 것이 함께 있어야 한다." 8장은 코드의 요소를 적절한 위치로 옮기는 기법들을 다뤄. 코드가 처음부터 제 자리에 있기는 어렵거든. 리팩터링을 통해 계속 옮겨주는 거야.
**함수 옮기기(Move Function)**는 함수를 다른 모듈로 옮기는 거야. 어떤 함수가 자기가 속한 모듈보다 다른 모듈의 요소를 더 많이 참조한다면, 그 함수는 다른 모듈에 있어야 해. 판단 기준은 **"이 함수가 어디에 있을 때 가장 자연스러운가"**야. 함수가 호출하는 것들이 모여 있는 곳이 더 적절한 위치지. 결정이 어려우면 일단 옮겨보고 이전보다 나은지 판단해. 리팩터링은 언제든 되돌릴 수 있으니까.
**필드 옮기기(Move Field)**는 더 근본적인 리팩터링이야. "데이터 구조는 프로그램의 토대다." 토대가 흔들리면 그 위에 세운 모든 게 불안정해져. 한 레코드의 필드를 다룰 때 다른 레코드의 필드를 항상 함께 가져와야 하거나, 한 레코드를 변경할 때 다른 레코드의 필드도 같이 변경해야 하면 필드를 옮겨야 하는 신호야. 데이터 구조를 바꾸는 건 다른 리팩터링보다 영향 범위가 크지만, 잘못된 데이터 구조를 방치하면 나중에 고치기가 기하급수적으로 어려워져.
**문장을 함수로 옮기기(Move Statements into Function)**는 특정 함수를 호출할 때마다 그 앞이나 뒤에 항상 같은 코드가 반복된다면, 그 코드를 함수 안으로 넣는 거야.
// Before
result.push(`<p>제목: ${person.photo.title}</p>`);
result.concat(photoData(person.photo));
// After
result.concat(photoData(person.photo));
function photoData(aPhoto) {
return [`<p>제목: ${aPhoto.title}</p>`, `<p>위치: ${aPhoto.location}</p>`, ...];
}
나중에 이 관계가 달라지면 반대 기법인 **문장을 호출한 곳으로 옮기기(Move Statements to Callers)**로 되돌려. 기능이 확장되면서 "항상 같이 수행되던 동작"이 "어떤 곳에서는 다르게 수행되어야 하는 동작"으로 바뀌는 경우가 있거든. 변경이 작으면 문장 하나만 빼면 되지만, 경계가 모호하면 함수 인라인하기로 일단 전부 펼친 다음 다시 함수 추출하기로 재구성하는 게 나을 수도 있어.
**인라인 코드를 함수 호출로 바꾸기(Replace Inline Code with Function Call)**는 이미 존재하는 함수의 동작과 동일한 코드가 있으면 함수 호출로 바꾸는 거야.
// Before
let appliesToMass = false;
for (const s of states) {
if (s === "MA") appliesToMass = true;
}
// After
appliesToMass = states.includes("MA");
바퀴를 다시 발명하지 마라는 거지. **문장 슬라이드하기(Slide Statements)**는 관련 있는 코드를 가까이 모으는 거야. 변수 선언은 처음 사용하는 곳 근처에, 관련 있는 연산은 서로 옆에. 코드가 모여 있으면 함수 추출하기 같은 다른 리팩터링을 적용하기 쉬워져. 다만 문장을 옮길 때 부작용을 조심해야 해. 다른 코드가 참조하는 값을 변경하는 문장이 있으면 순서를 바꿨을 때 동작이 달라질 수 있거든.
**반복문 쪼개기(Split Loop)**는 하나의 반복문이 두 가지 이상의 일을 하고 있으면 각각의 반복문으로 쪼개는 거야. "반복문을 두 번 도는 게 비효율적이지 않나?"라고 생각할 수 있는데, 대부분의 경우 성능 차이는 무시할 수 있어. "최적화와 리팩터링은 별개의 단계다." 반복문을 쪼개면 각 반복문이 하나의 일만 하니까 함수 추출하기로 더 쉽게 분리할 수 있지.
**반복문을 파이프라인으로 바꾸기(Replace Loop with Pipeline)**는 반복문 대신 filter, map, reduce 같은 컬렉션 파이프라인을 쓰는 거야.
// Before
const names = [];
for (const i of input) {
if (i.job === "programmer")
names.push(i.name);
}
// After
const names = input
.filter(i => i.job === "programmer")
.map(i => i.name);
파이프라인으로 바꾸면 "필터링하고 → 변환한다"는 흐름이 한눈에 보여. 반복문은 변수의 상태 변화를 추적해야 하지만, 파이프라인은 데이터의 흐름을 따라가면 돼.
마지막으로 죽은 코드 제거하기(Remove Dead Code). "쓰이지 않는 코드가 있으면 소프트웨어의 동작을 이해하려 할 때 혼란을 준다." "나중에 필요할지 모르니까 남겨둬야 하지 않나?" 버전 관리 시스템이 있으니까 괜찮아. 정말 필요하면 히스토리에서 꺼내면 돼. 주석 처리해두는 것도 좋지 않아. 그냥 지워.
정리
8장 읽고 기억할 거 세 가지:
- "데이터 구조가 잘못되면 모든 게 복잡해진다." 필드 옮기기는 어렵지만 그래서 더 중요해
- "반복문을 쪼개고 파이프라인으로 바꿔라." 성능 걱정은 나중에, 명확성이 먼저야
- "죽은 코드는 지워라." 버전 관리가 있으니까 미련 없이 삭제하면 돼