개요
서로 다른 두 대상을 한꺼번에 다루는 코드를 각각 별개 모듈로 나누는 방업
각 모듈에만 집중해 다른 모듈을 신경 쓸 필요가 없어짐
동작을 연이은 두 단계로 쪼개기
입력 값이 처리 로직에 적합하지 않은 경우, 전처리로 입력값을 처리에 적합한 값으로 바꾼다.
이후 로직을 수행한다.
위 경우 대표적인 예시가 컴파일러다.
언어에 맞는 문법 텍스트를 입력으로 받는다.
텍스트를 토큰화 한다. 토큰은 파싱한다. 구문 트리를 만든다. 최적화 등의 이유로 구문 트리를 변환한다.
마지막으로 바이트 코드(목적코드)를 생성한다.
각 단계는 자신의 단계만 신경 쓰면 된다.
예시
상품 결제 금액 계산
//상품 결제 금액 계산 코드 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); } |
명령줄 프로그램 쪼개기를 할 때 두 번째 단계에 해당하는 코드를 독립 함수로 추출한 후 중간 데이터 구조를 구조체로 쓸지, 변환기를 쓸지는 상관없다.
핵심은 단계를 명확히 분리하는 것
'IT책, 강의 > 리팩터링' 카테고리의 다른 글
07 - 캡슐화 - 컬렉션 캡슐화하기 (0) | 2023.07.26 |
---|---|
07 - 캡슐화 - 레코드 캡슐화하기 (0) | 2023.07.25 |
06 - 기본적인 리펙터링 - 여러 함수를 변환 함수로 묶기 (0) | 2023.07.23 |
06 - 기본적인 리펙터링 - 여러 함수를 클래스로 묶기 (0) | 2023.07.22 |
06 - 기본적인 리펙터링 - 변수 이름 바꾸기 (0) | 2023.07.21 |