📖
Aaron's TECH BOOK
  • Intro
    • About me
  • Lecture
    • Kubernetes
      • Begin Kubernetes
    • Kafka
      • Begin Kafka
    • Kotlin
      • TDD, Clean Code Preview
      • woowa Kotlin
    • Java
      • Multithread Concurrency
      • The Java
    • Toby
      • Toby Spring 6
      • Toby Spring Boot
    • MSA
      • 01.Micro Service
      • 02.DDD 설계
      • 03.DDD 구현
      • 04.EDA 구현
    • Spring Boot
    • Spring Batch
    • Spring Core Advanced
    • Spring DB Part II
    • Spring DB Part I
    • JPA API and Performance Optimization
    • JPA Web Application
    • JPA Programming Basic
    • Spring MVC Part 2
      • 01.Thymeleaf
      • 02.ETC
      • 03.Validation
      • 04.Login
      • 05.Exception
    • Spring MVC Part 1
      • 01.Servlet
      • 02.MVC
    • Http
      • 01.Basic
      • 02.Method
      • 03.Header
    • Spring Core
    • Study
      • Concurrency issues
      • First Come First Served
      • Performance Test
      • TDD
      • IntelliJ
  • Book
    • Kafka Streams in Action
      • 01.카프카 스트림즈
      • 02.카프카 스트림즈 개발
      • 03.카프카 스트림즈 관리
    • Effective Kotlin
      • 01.좋은 코드
      • 02.코드 설계
      • 03.효율성
    • 이벤트 소싱과 MSA
      • 01.도메인 주도 설계
      • 02.객체지향 설계 원칙
      • 03-04.이벤트 소싱
      • 05.마이크로서비스 협업
      • 06.결과적 일관성
      • 07.CQRS
      • 08.UI
      • 09.클라우드 환경
    • 몽고DB 완벽 가이드
      • I. 몽고DB 시작
      • II. 몽고DB 개발
    • Kotlin Cookbook
      • 코틀린 기초
      • 코틀린 기능
      • ETC
    • Kotlin in Action
      • 함수/클래스/객체/인터페이스
      • 람다와 타입
      • 오버로딩과 고차 함수
      • 제네릭스, 애노테이션, 리플렉션
    • Kent Beck Tidy First?
    • 대규모 시스템 설계 기초
      • 01.사용자 수에 따른 규모 확장성
      • 02.개략적인 규모 추정
      • 03.시스템 설계 공략법
      • 04.처리율 제한 장치 설계
      • 05.안정 해시 설계
      • 06.키-값 저장소 설계
      • 07.유일 ID 생성기 설계
      • 08.URL 단축기 설계
      • 09.웹 크롤러 설계
      • 10.알림 시스템 설계
      • 11.뉴스 피드 시스템 설계
      • 12.채팅 시스템 설계
      • 13.검색어 자동완성 시스템
      • 14.유튜브 설계
      • 15.구글 드라이브 설계
      • 16.배움은 계속된다
    • 실용주의 프로그래머📖
    • GoF Design Patterns
    • 도메인 주도 개발 시작하기
      • 01.도메인 모델 시작하기
      • 02.아키텍처 개요
      • 03.애그리거트
      • 04.리포지터리와 모델 구현
      • 05.Spring Data JPA를 이용한 조회 기능
      • 06.응용 서비스와 표현 영역
      • 07.도메인 서비스
      • 08.애그리거트 트랜잭션 관리
      • 09.도메인 모델과 바운디드 컨텍스트
      • 10.이벤트
      • 11.CQRS
    • Effective Java 3/E
      • 객체, 공통 메서드
      • 클래스, 인터페이스, 제네릭
    • 소프트웨어 장인
    • 함께 자라기
    • Modern Java In Action
      • 01.기초
      • 02.함수형 데이터 처리
      • 03.스트림과 람다를 이용한 효과적 프로그래밍
      • 04.매일 자바와 함께
    • Refactoring
      • 01.리펙터링 첫 번째 예시
      • 02.리펙터링 원칙
      • 03.코드에서 나는 악취
      • 06.기본적인 리펙터링
      • 07.캡슐화
      • 08.기능 이동
      • 09.데이터 조직화
      • 10.조건부 로직 간소화
      • 11.API 리팩터링
      • 12.상속 다루기
    • 객체지향의 사실과 오해
      • 01.협력하는 객체들의 공동체
      • 02.이상한 나라의 객체
      • 03.타입과 추상화
      • 04.역할, 책임, 협력
      • 05.책임과 메시지
      • 06.객체 지도
      • 07.함께 모으기
      • 부록.추상화 기법
    • Clean Code
    • 자바 ORM 표준 JPA 프로그래밍
Powered by GitBook
On this page
  • 기본적인 리펙터링
  • 리팩터링 전에 테스트 구축하기
  • Chapter 6. 기본적인 리팩터링
  • 함수 추출하기
  • 함수 인라인하기
  • 변수 추출하기
  • 변수 인라인하기
  • 함수 선언 바꾸기
  • 변수 캡슐화하기
  • 변수 이름 바꾸기
  • 매개변수 객체 만들기
  • 여러 함수를 클래스로 묶기
  • 여러 함수를 변환 함수로 묶기
  • 단계 쪼개기
  1. Book
  2. Refactoring

06.기본적인 리펙터링

기본적인 리펙터링

리팩터링 전에 테스트 구축하기

  • 모든 테스트를 완전히 자동화하고 그 결과까지 스스로 검사하게 만들자.

  • 실패해야 할 상황에서는 반드시 실패하게 만들자.

  • 문제가 생길 가능성이 있는 경계 조건을 생각해보고 그 부분을 집중적으로 테스트하자.

  • 어차피 모든 버그를 잡아낼 수는 없다고 생각하여 테스트를 작성하지 않는다면 대다수의 버그를 잡을 수 있는 기회를 날리는 셈이다.

  • 너무 많은 테스트는 의욕을 떨어지게 만들 수 있다. 따라서 위험한 부분에 집중하자. 처리 과정이 복잡하거나 함수에서 오류가 생길만한 부분을 찾아보자.

  • 버그 리포트를 받으면 가장 먼저 그 버그를 드러내는 단위 테스트부터 작성하자.

  • 누군가 결함을 심으면 테스트가 발견할 수 있다는 믿음을 기준으로 테스트 코드를 작성하자.

Chapter 6. 기본적인 리팩터링

함수 추출하기

목적과 구현을 분리하는 방식

  • 반대 리팩터링 : 함수 인라인하기

개요

Before

function printOwing(invoice) {
    printBanner();

    let outstanding = calculatorOutstanding();

    // 세부 사항 출력
    console.log(`고객명: ${invoice.customer}`);
    console.log(`채무액: ${outstanding}`);
}

After

function printOwing(invoice) {
    printBanner();
    let outstanding = calculatorOutstanding();
    printDetails(outstanding);

    function printDetails(outstanding) {
        console.log(`고객명: ${invoice.customer}`);
        console.log(`채무액: ${outstanding}`);
    }
}

절차

  1. 함수를 새로 만들고 목적(무엇을 하는지)을 잘 드러내는 이름 붙이기.

  2. 추출할 코드를 새 함수에 복사

  3. 참조하는 변수는 매개변수로 전달

    • 원본 함수의 지역 변수

    • 추출한 함수의 유효범위를 벗어나는 변수

  4. 추출한 코드 부분을 새로 만든 함수를 호출하는 문으로 수정

  5. 테스트

  6. 유사한 코드 확인

함수 인라인하기

함수 본문이 이름만큼 명확한 경우, 함수 코드를 이름만큼 깔끔하게 리팩터링할 경우

  • 반대 리팩터링 : 함수 추출하기

개요

Before

function raiting(aDriver) { return moreThanFiveLateDeliveries(aDriver) ? 2 : 1; }
function moreThanFiveLateDeliveries(aDriver) { return aDriver.numberOfLaterDeliveries > 5; }

After

function raiting(aDriver) {  return aDriver.numberOfLaterDeliveries > 5 ? 2 : 1; }

절차

  1. 다형 메서드인지 확인

    • 서브클래스에서 오버라이드하는 메서드는 인라인 금지

  2. 인라인할 함수를 호출하는 곳을 모두 찾기

  3. 각 호출문을 함수 본문으로 교체

  4. 하나씩 교체할 때마다 테스트

  5. 함수 정의 삭제

변수 추출하기

표현식이 복잡하여 이해하기 어려울 경우 지역 변수를 활용하여 표현식을 관리

  • 디버거 breakpoint를 지정하거나 상태 출력 문장을 추가하면 디버깅에 도움이 된다.

  • 문맥을 고려하여 현재 선언된 함수보다 더 넓은 문맥에서까지의 의미가 된다면 함수로 추출 권장

  • 반대 리팩터링 : 변수 인라인하기

개요

Before

function price(order) {
    // 가격(price) = 기본 가격 - 수량 할인 + 배송비
    return (
        order.quantity * order.itemPrice -
        Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
        Math.min(order.quantity * order.itemPrice * 0.1, 0.05)
    );
}

After

function price(order) {
    const basePrice = order.quantity * order.itemPrice;
    const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
    const shipping = Math.min(basePrice * 0.1, 100);

    return basePrice - quantityDiscount + shipping;
}

절차

  1. 추출할 표현식에 사이드이펙트가 없는지 확인

  2. 불변 변수를 하나 선언 후 이름을 붙일 표현식의 복제본 대입해보기

    • 함수가 클래스 전체에 영향을 줄 떄는 변수가 아닌 메서드로 추출

  3. 원본 표현식을 새로 만든 변수로 교체

  4. 테스트

  5. 표현식을 여러 곳에서 사용할 경우 각각을 새로 만든 변수로 교체

변수 인라인하기

변수명이 원래 표현식과 다를 바 없을 때

  • 반대 리팩터링 : 변수 추출하기

개요

Before

((anOrder) => {
    let basePrice = anOrder.basePrice;
    return basePrice > 1000;
})(anOrder);

After

((anOrder) => {
    return anOrder.basePrice > 1000;
})(anOrder);

절차

  1. 인라인할 표현식에 사이드 이펙트가 없는지 확인

  2. 상수인지 확인하고 상수로 수정 후 테스트

    • 이 경우 변수에 값이 단 한 번만 대입되는지 확인 가능

  3. 변수를 표현식으로 교체

  4. 테스트

  5. 변수를 사용하는 부분 모두 교체 시까지 이 과정 반복

  6. 변수 선언문과 대입문 지우기

  7. 테스트

함수 선언 바꾸기

호출문만 보고도 무슨 일을 하는지 파악할 수 있도록 함수 이름을 좋게 만들자.

  • 주석을 이용해 함수의 목적을 설명해보면 좋은 이름이 떠오를 것이다.

다른 이름 : 함수 이름 바꾸기, 매개변수 바꾸기

개요

간단한 절차

// Before
function circum(radius) {}

// After
function circumference(radius) {}

마이그레이션 절차

// Before
function circum(radius) {
    return 2 * Math.PI * radius;
}

// After
function circum(radius) {
    return circumference(radius);
}

function circumference(radius) {
    return 2 * Math.PI * radius;
}

절차

함수 이름 바꾸기, 매개변수 바꾸기에 모두 적용

마이그레이션 절차의 복잡도에 따라 간단한 절차와 마이그레이션 절차로 구분지어 따름

간단한 절차

  1. 함수 본문에 제거 대상 매개변수를 참조하는 곳이 없는지 확인

  2. 메서드 선언 형태 변경

  3. 기존 메서드 선언을 참조하는 부분을 바뀐 형태로 수정

  4. 테스트

마이그레이션 절차

(간단한 절차 적용 문제 발생 시)

  1. 함수 본문 리팩터링

    • 함수/변수 추출 등

  2. 함수 본문에서 새로운 함수로 추출

  3. 추출한 함수에 매개변수 추가 시 간단한 절차를 따라 추가

  4. 어서션을 추가하여 새로 추가한 매개변수를 실제 사용하는지 확인 (javascript)

  5. 테스트

  6. 기존 함수 인라인 처리

  7. 임시 이름을 붙인 새 함수를 원래 이름으로 수정

  8. 테스트

변수 캡슐화하기

자주 사용하는 가변 데이터에 대한 결합도가 높아지는 막기 위해, 데이터의 유효범위가 넓을수록 캡슐화하자.

  • 데이터 변경 전 검증이나 변경 후 추가 로직을 쉽게 끼워넣을 수 있다.

개요

Before

let defaultOwner = { firstName: '마틴', lastName: '파울러'};

After

let defaultOwnerData = { firstName: '마틴', lastName: '파울러' };
export function defaultOwner() { return defaultOwnerData; }
export function setDefaultOwner(arg) { defaultOwnerData = arg; }

절차

  1. 변수 접근/갱신을 전담하는 Getter/Setter 만들기

  2. 정적 검사 수행

  3. 변수를 직접 참조하던 부분을 적절한 캡슐화 함수 호출로 변경 (테스트 병행)

  4. 변수의 접근 범위 제한

  5. 테스트

  6. 변수 값이 레코드일 경우 레코드 캡슐화 적용 고려

캡슐화할 데이터를 사용하는 방식과 어떻게 변경할지에 따라 캡슐화의 구체적인 대상과 방법을 정하자.

let defaultOwnerData = { firstName: '마틴', lastName: '파울러' };
export function defaultOwner() { return new Person(defaultOwnerData); }
export function setDefaultOwner(arg) { defaultOwnerData = arg; }

class Person {
    constructor(data) {
        this._lastName = data.lastName;
        this._firstName = data.firstName;
    }

    get lastName() { return this._lastName; }

    get firstName() { return this._firstName; }
}

변수 이름 바꾸기

변수는 프로그래머가 하려는 일에 관해 많은 것을 설명해준다.

개요

Before

let a = height * width;

After

let area = height * width;

절차

  1. 폭넓게 쓰이는 변수라면 변수 캡슐화 고려하기

  2. 변경할 변수를 참조하는 곳을 하나씩 변경하기

    • 해당 함수에서만 유효한 변수

  3. 테스트

매개변수 객체 만들기

데이터 뭉치를 데이터 구조로 묶으면 데이터 사이의 관계가 명확해진다.

  • +, 매개변수 수 감소, 원소 참조의 일관성

  • 데이터 구조를 클래스로 만들어두면 관련 동작들을 해당 클래스로 옮길 수 있다.

개요

Before

function amountInvoiced(startDate, endDate) {}
function amountReceived(startDate, endDate) {}
function amountOverdue(startDate, endDate) {}

After

function amountInvoiced(aDateRange) {}
function amountReceived(aDateRange) {}
function amountOverdue(aDateRange) {}

절차

  1. 데이터 구조 만들기

  2. 함수 선언 바꾸기로 매개변수에 새 데이터 구조 추가

  3. 함수 호출 시 인스턴스 수정

  4. 기존 매개변수를 사용하던 코드를 새 데이터 구조를 사용하도록 수정

  5. 기존 매개변수 제거

  6. 각 단계별 테스트

여러 함수를 클래스로 묶기

클래스로 묶으면 함수들이 공유하는 공통 환경을 더 명확하게 표현

각 함수에 전달되는 인수를 줄여 객체 안에서의 함수 호출을 간결하게

개요

Before

function base(aReading) {}
function taxableCharge(aReading) {}
function calculateBaseChange(aReading) {}

After

class Reading {
    base() {}
    taxableCharge() {}
    calculateBaseChange() {}
}

절차

  1. 함수들이 공유하는 공통 데이터 레코드 캡슐화

  2. 공통 레코드를 사용하는 함수 각각을 새 클래스로 옮기기

    • 멤버 변수는 함수 호출문의 인수에서 제거

  3. 데이터를 조작하는 로직들은 함수로 추출하여 새 클래스로 옮기기

여러 함수를 변환 함수로 묶기

변환 함수는 원본 데이터를 입력받아서 필요 정보를 모두 두출한 후, 각 출력 데이터 필드에 넣어 반환한다.

검색과 갱신을 일관된 장소에서 처리할 수 있고, 로직 중복도 막을 수 있다.

원본 데이터가 코드 안에서 갱신될 때는 클래스로 묶기를 적용하자.

  • 데이터 일관성을 지켜야 한다면(원본 데이터 변경을 막기 위한 목적이 있다면) 클래스 묶기를 적용하자.

개요

Before

function base(aReading) {}
function taxableCharge(aReading) {}

After

function enrichReading(argReading) {
    const aReading = _.cloneDeep(argReading);
    aReading.baseCharge = base(aReading);
    aReading.taxableCharge = taxableCharge(aReading);
    // 원본 데이터는 변경면 안됨. (원본 데이터 확인 테스트 작성 필요)
    return aReading;
}

절차

  1. 입력된 값을 그대로 반환하는 변환 함수 생성

    • 깊은 복사 처리

  2. 묶을 함수의 본문 코드를 변환 함수로 옮기고, 처리 결과를 새 필드로 기록

    • 로직이 복잡하면 함수 추출 먼저 적용

  3. 테스트

  4. 나머지 관련 함수도 적용

단계 쪼개기

서로 다른 두 대상을 한꺼번에 다루는 경우 각각을 별개의 모듈로 나누자.

동작을 두 단계로 쪼개는 방법

개요

Before

const orderData = orderString.split(/\s+/);
const productPrice = priceList[orderData[0].split('-')[1]];
const orderPrice = parseInt(orderData[1]) * productPrice;

After

const orderRecord = parseOrder(order)
const orderPrice = parseInt(orderData[1]) * productPrice;

function parseOrder(aString) {
    const values = aString.split(/\s+/);
    return ({
        productID: values[0].split('-')[1]
        quantity: parseInt(values[1])
	})
}
function price(order, priceList) {
    return order.quantity * priceList[order.productID]
}

절차

  1. 두 번째 단계 처리 코드를 독립 함수로 추출

  2. 테스트

  3. 중간 데이터 구조(ex. Class)를 만들어 전 단계에서 추출한 함수의 인수로 추가

  4. 테스트

  5. 추출한 두 번째 단계 함수의 매개변수를 검토

    • 첫 번째 단계에 사용된 매개변수는 중간 데이터 구조(ex. Class)로 옮기기

  6. 첫 번째 단계 코드를 함수로 추출하면서 중간 데이터 구조 반환

Example

function priceOrder(product, quantity, shippingMethod) {
    const priceData = calculatePricingData(product, quantity);
    return applyShipping(priceData, shippingMethod, discount);
}

function calculatePricingData(product, quantity) {
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshold, 0) 
		    * product.basePrice * product.discountRate;
    return { basePrice: basePrice, quantity: quantity, discount: discount };
}

function applyShipping(priceData, shippingMethod) {
    const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold) ? 
          	 shippingMethod.discountedFee : shippingMethod.feePerCase;
    const shippingCost = priceData.quantity * shippingPerCase;
    return priceData.basePrice - priceData.discount + shippingCost;
}

Last updated 1 year ago