리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

개요

서로 다른 대상을 한꺼번에 다루는 코드를 각각 별개 모듈로 나누는 방업

모듈에만 집중해 다른 모듈을 신경 필요가 없어짐

 

동작을 연이은 단계로 쪼개기

입력 값이 처리 로직에 적합하지 않은 경우, 전처리로 입력값을 처리에 적합한 값으로 바꾼다.

이후 로직을 수행한다.

 

경우 대표적인 예시가 컴파일러다.

언어에 맞는 문법 텍스트를 입력으로 받는다.

텍스트를 토큰화 한다. 토큰은 파싱한다. 구문 트리를 만든다. 최적화 등의 이유로 구문 트리를 변환한다.

마지막으로 바이트 코드(목적코드) 생성한다.

단계는 자신의 단계만 신경 쓰면 된다.

 

예시

상품 결제 금액 계산

//상품 결제 금액 계산 코드
function priceOrder(product, quantity, shippingMethod){
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
    const shippingPerCase = (basePrice > shippingMethod.discountThreshold )
        ?shippingMethod.discountedFee : shippingMethod.feePerCase;
    const shippingCost = quantity * shippingPerCase;
    const price = basePrice - discount + shippingCost;
    return price;
}
//배송비 계산 부분 함수로 추출
function priceOrder(product, quantity, shippingMethod){
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
    const price = applyShipping(basePrice, shippingMethod, quantity, discount);
    return price;
}
function applyShipping(basePrice, shippingMethod, quantity, discount) {
    const shippingPerCase = (basePrice > shippingMethod.discountThreshold)
        ? shippingMethod.discountedFee : shippingMethod.feePerCase;
    const shippingCost = quantity * shippingPerCase;
    const price = basePrice - discount + shippingCost;
    return price;
}


//단계 간 주고받을 중간 데이터 구조
function priceOrder(product, quantity, shippingMethod){
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
    const priceData = {};
    const price = applyShipping(priceData, basePrice, shippingMethod, quantity, discount);
    return price;
}
function applyShipping(priceData, basePrice, shippingMethod, quantity, discount) {
    const shippingPerCase = (basePrice > shippingMethod.discountThreshold)
        ? shippingMethod.discountedFee : shippingMethod.feePerCase;
    const shippingCost = quantity * shippingPerCase;
    const price = basePrice - discount + shippingCost;
    return price;
}


//중간 데이터 구조에 데이터를 넣고 인자를 하나씩 줄여간다.
function priceOrder(product, quantity, shippingMethod){
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
    const priceData = {basePrice:basePrice};
    const price = applyShipping(priceData, shippingMethod, quantity, discount);
    return price;
}
function applyShipping(priceData, shippingMethod, quantity, discount) {
    const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)
        ? shippingMethod.discountedFee : shippingMethod.feePerCase;
    const shippingCost = quantity * shippingPerCase;
    const price = priceData.basePrice - discount + shippingCost;
    return price;
}


//과정을 반복하며, 중간 데이터 구조 완성
function priceOrder(product, quantity, shippingMethod){
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
    const priceData = {basePrice:basePrice , quantity : quantity, discount : discount};
    const price = applyShipping(priceData, shippingMethod);
    return price;
}
function applyShipping(priceData, shippingMethod) {
    const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)
        ? shippingMethod.discountedFee : shippingMethod.feePerCase;
    const shippingCost = priceData.quantity * shippingPerCase;
    const price = priceData.basePrice - priceData.discount + shippingCost;
    return price;
}


function priceOrder(product, quantity, shippingMethod){
    const priceData = calculatePricingData(product, quantity);
    const price = applyShipping(priceData, shippingMethod);
    return price;
}
//첫 번째 단계 처리 함수
function calculatePricingData(product, quantity) {
    const basePrice = product.basePrice * quantity;
    const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
    const priceData = { basePrice: basePrice, quantity: quantity, discount: discount };
    return priceData;
}
//두 번째 단계 처리 함수
function applyShipping(priceData, shippingMethod) {
    const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)
        ? shippingMethod.discountedFee : shippingMethod.feePerCase;
    const shippingCost = priceData.quantity * shippingPerCase;
    const price = priceData.basePrice - priceData.discount + shippingCost;
    return price;
}


//상수 정리
function priceOrder(product, quantity, shippingMethod){
    const priceData = calculatePricingData(product, quantity);
    return applyShipping(priceData, shippingMethod);
}
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;
}

예시 : 명령줄 프로그램 쪼개기(자바)



import java.io.File;
import java.nio.file.Paths;
import java.util.stream.Stream;
import com.fasterxml.jackson.databind.ObjectMapper;
public class TestEx {
//현재 두 가지 일을 하고 있다. 하나는 갯수 세기, 다른 하나는 인수가 -r 일 시 ready상태 갯수 세기
    static class Order{
        String status;
    }
    public static void main(String[] args) {
        try {
            if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
            String filename = args[args.length -1];
            File input = Paths.get(filename).toFile();
            ObjectMapper mapper = new ObjectMapper();
            Order[] orders = mapper.readValue(input, Order[].class);
            if(Stream.of(args).anyMatch(arg->"-r".equals(arg))) {
                System.out.println(Stream.of(orders)
                        .filter(o->"ready".equals(o.status))
                        .count());
            }else {
                System.out.println(orders.length);
            }
        } catch (Exception e) {
            System.err.println(e);
            System.exit(1);
        }
    }
}



//자바 명령줄 프로그램 테스트 시 매번 JVM 구동은 느리니
//JUnit 호출 가능한 상태로 변경
public class TestEx {
    static class Order{
        String status;
    }
    public static void main(String[] args) {
        try {
            System.out.println(run(args));
        } catch (Exception e) {
            System.err.println(e);
            System.exit(1);
        }
    }
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        String filename = args[args.length -1];
        File input = Paths.get(filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(Stream.of(args).anyMatch(arg->"-r".equals(arg))) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }
}


    //먼저, 두 번째 단계에 해당하는 코드를 메서드로 추출
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        String filename = args[args.length -1];
        return countOrders(args, filename);
    }
    private static long countOrders(String[] args, String filename)
            throws IOException {
        File input = Paths.get(filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(Stream.of(args).anyMatch(arg->"-r".equals(arg))) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    //중간 데이터 구조 추가
    private static class CommandLine{}
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine();
        String filename = args[args.length -1];
        return countOrders(commandLine, args, filename);
    }
    private static long countOrders(CommandLine commandLine, String[] args, String filename)
            throws IOException {
        File input = Paths.get(filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(Stream.of(args).anyMatch(arg->"-r".equals(arg))) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    //두 번째 단계로 가는 인수 분석하기
    private static class CommandLine{}
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine();
        String filename = args[args.length -1];
        return countOrders(commandLine, args, filename);
    }
    private static long countOrders(CommandLine commandLine, String[] args, String filename)
            throws IOException {
        File input = Paths.get(filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        //args 는 두 번째 단계에서 굳이 필요없다. 이를 위해 먼저 조건식을 변수로 추출
        boolean onlyCountReady = Stream.of(args).anyMatch(arg->"-r".equals(arg));
        if(onlyCountReady) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    private static class CommandLine{
        private boolean onlyCountReady;
    }
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine();
        String filename = args[args.length -1];
        //중간 데이토 구조로 옮김, 이후 첫번째 단계로 옮김, 이후 매개변수 제거
        commandLine.onlyCountReady = Stream.of(args).anyMatch(arg->"-r".equals(arg));
        return countOrders(commandLine, filename);
    }
    private static long countOrders(CommandLine commandLine, String filename)
            throws IOException {
        File input = Paths.get(filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(commandLine.onlyCountReady) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    //잔여 매개변수 처리 완료
    private static class CommandLine{
        private boolean onlyCountReady;
        private String filename;
    }
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine();
        commandLine.filename = args[args.length -1];
        commandLine.onlyCountReady = Stream.of(args).anyMatch(arg->"-r".equals(arg));
        return countOrders(commandLine);
    }
    private static long countOrders(CommandLine commandLine)
            throws IOException {
        File input = Paths.get(commandLine.filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(commandLine.onlyCountReady) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    //메인 작업 완료, 잔여 코드 정리(리팩터링)
    private static class CommandLine{
        private boolean onlyCountReady;
        private String filename;
    }
    static long run(String[] args) throws IOException{
        return countOrders(parseCommandLine(args));
    }
    private static CommandLine parseCommandLine(String[] args) {
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine result = new CommandLine();
        result.filename = args[args.length -1];
        result.onlyCountReady = Stream.of(args).anyMatch(arg->"-r".equals(arg));
        return result;
    }
    private static long countOrders(CommandLine commandLine)
            throws IOException {
        File input = Paths.get(commandLine.filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(commandLine.onlyCountReady) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }

예시 : 번째 단계에 변환기 사용하기(자바)

이전 예시와 다르게 데이터 구조 생성 전달이 아닌 번째 단계에 적합한 인터페이스로 바꿔주는 변환기 객체 사용

 



    //중간 데이터 구조 추가 부분부터
    private static class CommandLine{}
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine();
        String filename = args[args.length -1];
        return countOrders(commandLine, args, filename);
    }
    private static long countOrders(CommandLine commandLine, String[] args, String filename)
            throws IOException {
        File input = Paths.get(filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(Stream.of(args).anyMatch(arg->"-r".equals(arg))) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    //최상위 별도 클래스 파일로 뺏다고 가정
    //CommandLine.java
    private static class CommandLine{
        String[] args;
        public CommandLine(String[] args) {
            this.args = args;
        }
    }
    //Main.java
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine(args);
        String filename = args[args.length -1];
        return countOrders(commandLine, args, filename);
    }
    private static long countOrders(CommandLine commandLine, String[] args, String filename)
            throws IOException {
        File input = Paths.get(filename).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(Stream.of(args).anyMatch(arg->"-r".equals(arg))) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    //Main.java
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine(args);
        return countOrders(commandLine, args, filename(args));
    }
    //임시변수를 질의함수로 바꾸기
    private static String filename(String[] args) {
        return args[args.length -1];
    }


    //CommandLine.java
    private static class CommandLine{
        String[] args;
        public CommandLine(String[] args) {
            this.args = args;
        }
        //질의 함수 옮기기
        public  String filename() {
            return args[args.length -1];
        }
    }
    //Main.java
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine(args);
        return countOrders(commandLine, args, commandLine.filename());
    }


    //Main.java
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine(args);
        return countOrders(commandLine, args);
    }
    //매개변수 정리
    private static long countOrders(CommandLine commandLine, String[] args)
            throws IOException {
        File input = Paths.get(commandLine.filename()).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(Stream.of(args).anyMatch(arg->"-r".equals(arg))) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    //CommandLine.java
    private static class CommandLine{
        String[] args;
        public CommandLine(String[] args) {
            this.args = args;
        }
        public  String filename() {
            return args[args.length -1];
        }
        //함수 추출 후, 클래스로 옮김 이후, 불필요한 인자 제거
        public boolean onlyCountReady() {
            return Stream.of(args).anyMatch(arg->"-r".equals(arg));
        }
    }
    //Main.java
    static long run(String[] args) throws IOException{
        if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
        CommandLine commandLine = new CommandLine(args);
        return countOrders(commandLine);
    }
    private static long countOrders(CommandLine commandLine)
            throws IOException {
        File input = Paths.get(commandLine.filename()).toFile();
        ObjectMapper mapper = new ObjectMapper();
        Order[] orders = mapper.readValue(input, Order[].class);
        if(commandLine.onlyCountReady()) {
            return Stream.of(orders)
                    .filter(o->"ready".equals(o.status))
                    .count();
        }else {
            return orders.length;
        }
    }


    //CommandLine.java
    private static class CommandLine{
        String[] args;
        public CommandLine(String[] args) {
            //검사로직 옮기기, 리팩터링 완료
            if(args.length == 0) throw new RuntimeException("파일명을 입력하세요.");
            this.args = args;
        }
        public  String filename() {
            return args[args.length -1];
        }
        public boolean onlyCountReady() {
            return Stream.of(args).anyMatch(arg->"-r".equals(arg));
        }
    }
    //Main.java
    static long run(String[] args) throws IOException{
        CommandLine commandLine = new CommandLine(args);
        return countOrders(commandLine);
    }

 

명령줄 프로그램 쪼개기를 번째 단계에 해당하는 코드를 독립 함수로 추출한 중간 데이터 구조를 구조체로 쓸지, 변환기를 쓸지는 상관없다.

핵심은 단계를 명확히 분리하는

 

+ Recent posts