김주엽
1. 📌 핵심 개념 정리
✅ 요약하기
-
작게 만들어라!
함수를 만드는 첫번째 규칙은 작게 만들기다.
일반적으로 함수는 20줄 이하로 작성하고if/else
,while
,for
문 등에 들어가는 블록은 1줄이어야 한다.
또한 들여쓰기 수준은 1단, 2단을 넘어서면 안 된다.- 개선 전
public class OrderProcessor { public void processOrder(Order order) { if (order != null) { if (order.isPaid()) { if (order.isShipped()) { System.out.println("이미 배송된 주문입니다."); } else { order.ship(); System.out.println("주문이 배송되었습니다."); } } else { System.out.println("결제되지 않은 주문입니다."); } } else { System.out.println("잘못된 주문입니다."); } } }
위의 코드는 함수의 길이가 길고 들여쓰기 수준이 깊다.
- 개선 후
public class OrderProcessor { public void processOrder(Order order) { if (order == null) { handleInvalidOrder(); return; } if (!order.isPaid()) { handleUnpaidOrder(); return; } processShipping(order); } private void handleInvalidOrder() { System.out.println("잘못된 주문입니다."); } private void handleUnpaidOrder() { System.out.println("결제되지 않은 주문입니다."); } private void processShipping(Order order) { if (order.isShipped()) { System.out.println("이미 배송된 주문입니다."); return; } order.ship(); System.out.println("주문이 배송되었습니다."); } }
코드 개선 후 들여쓰기 제한을 준수하여
if
문 중첩을 최소화하고 가독성을 향상시켰다.
-
한 가지만 해라!
"함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다."- 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행해야 한다.
추상화
란 필요한 정보만 남기고 불필요한 세부사항을 숨기는 개념이다.- "무엇을 할 것인가?"(What)만 신경 쓰고 "어떻게 처리할 것인가?"(How)는 감춘다는 원칙
- 단순히 다른 표현이 아닌 의미 있는 이름으로 다른 함수를 추출할 수 있다면
그 함수는 여러 작업을 하는 함수이기에 분리가 필요하다.
- 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행해야 한다.
-
함수당 추상화 수준은 하나로!
함수가 한 가지 일만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.
또한 코드는 위에서 아래로 책처럼 읽혀야 좋다.- 개선 전
public class OrderProcessor { private double getPrice(String item) { return switch (item) { case "Apple" -> 1.0; case "Banana" -> 0.5; case "Orange" -> 0.75; default -> 0.0; }; } public void processOrder() { System.out.println("Processing order..."); List<String> items = fetchItems(); System.out.println("Items to pack: " + items); for (String item : items) { System.out.println("Packing: " + item); } System.out.println("Calculating total price..."); double total = 0; for (String item : items) { total += getPrice(item); } System.out.println("Total price: $" + total); System.out.println("Generating invoice..."); String invoice = "Invoice: " + items + " | Total: $" + total; System.out.println(invoice); System.out.println("Order processed."); } private List<String> fetchItems() { return List.of("Apple", "Banana", "Orange"); } }
한 함수 안에 너무 많은 세부 구현이 섞여 있어서 코드를 위에서 아래로 자연스럽게 읽기 어렵다.
- 개선 후
public class OrderProcessor { public void processOrder() { printStartMessage(); List<String> items = fetchItems(); packItems(items); double total = calculateTotalPrice(items); generateInvoice(items, total); printEndMessage(); } private void printStartMessage() { System.out.println("Processing order..."); } private List<String> fetchItems() { return List.of("Apple", "Banana", "Orange"); } private void packItems(List<String> items) { System.out.println("Items to pack: " + items); for (String item : items) { System.out.println("Packing: " + item); } } private double calculateTotalPrice(List<String> items) { System.out.println("Calculating total price..."); double total = 0; for (String item : items) { total += getPrice(item); } System.out.println("Total price: $" + total); return total; } private double getPrice(String item) { return switch (item) { case "Apple" -> 1.0; case "Banana" -> 0.5; case "Orange" -> 0.75; default -> 0.0; }; } private void generateInvoice(List<String> items, double total) { System.out.println("Generating invoice..."); String invoice = "Invoice: " + items + " | Total: $" + total; System.out.println(invoice); } private void printEndMessage() { System.out.println("Order processed."); } }
각 단계를 함수로 분리하여 책처럼 자연스럽게 읽히도록 개선했다.
-
Switch 문
본질적으로N
가지를 처리하는switch
문을 완전히 피하는 방법은 없다.
하지만 각switch
문을 다형성을 이용해서 숨기고 반복하지 않게 구현하는 방법은 있다.- 개선 전
public class Payroll { public Money calculatePay(Employee e) throws InvalidEmployeeType { switch (e.type) { case COMMISSIONED: return calculateCommissionedPay(e); case HOURLY: return calculateHourlyPay(e); case SALARIED: return calculateSalariedPay(e); default: throw new InvalidEmployeeType(e.type); } } }
위의 코드는 여러 문제가 존재한다.
- 함수가 길고 새 직원 유형을 추가하려면 코드가 더 길어질 위험이 있다.
- 한 가지 작업만을 수행하지 않는다.
SRP
원칙을 위반한다.- 급여를 계산해야 하는 함수가 직원을 구분하는 역할까지 하고 있다.
OCP
원칙을 위반한다- 새로운 직원 유형을 추가하려면 기존 코드를 수정해야 한다.
- 구조가 동일한 함수가 추가될 수 있는 위험이 존재한다.
isPayDay(Employee e, Date date)
,deliverPay(Employee e, Money pay)
와 같은 메서드를 추가하면
각 함수 내부에서 직원 유형별로switch
문을 사용하게 될 위험이 있다.
- 개선 후
public abstract class Employee { public abstract boolean isPayday(); public abstract Money calculatePay(); public abstract void deliverPay(Money pay); } public interface EmployeeFactory { public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType; } public class EmployeeFactoryImpl implements EmployeeFactory { public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType { switch (r.type) { case COMMISSIONED: return new CommissionedEmployee(r); case HOURLY: return new HourlyEmployee(r); case SALARIED: return new SalariedEmployee(r); default: throw new InvalidEmployeeType(r.type); } } }
코드 개선 후
switch
문은 추상 팩토리에 의해 숨겨지고 객체를 생성하는 부분에만 사용된다.
- 서술적인 이름을 사용하라!
- 이름이 길어도 괜찮다. 길고 서술적인 이름이 짧고 어려운 이름보다 좋고 서술적인 주석보다도 좋다.
- 여러 단어가 쉽게 읽히는 명명법을 사용하고 함수 기능을 잘 표현하는 이름을 선택한다.
- 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.
- 좋은 예:
includeSetupAndTeardownPages
,includeSetupPages
,includeSuiteSetupPage
- 좋은 예:
- 함수 인수
- 함수의 인수 개수는 짧을수록 좋다.
- 함수 인수에
bool
값을 넘기는 것은 피하자. - 함수 인수의 개수가 3개, 4개 이상은 가능한 피하는 게 좋다.
- 인수의 개수가 길어진다면 독자적인
class
로 선언할 수 있는지 확인하자.
- 인수의 개수가 길어진다면 독자적인
- 함수 이름에 인수 이름을 추가하면 좋다.
assertEquals(expected, actual)
보다assertExpectedEqualsActual(expected, actual)
이 더 좋다.
- 오류 코드보다 예외를 사용하라!
if (deletePage(page) == E_OK)
같은 오류 코드를 작성하는 것을 피하고 별도의try-catch
함수를 작성한다.public void delete(Page page) { try { deletePage(page); } catch (Exception e) { log.error(e); } }
2. 🤔 이해가 어려운 부분
🔍 질문하기
책을 읽으며 이해하기 어려웠던 개념이나 명확하지 않았던 내용을 정리합니다.
- 함수당 추상화 수준은 하나로!
-
어려웠던 부분
책에서 말하는 추상화 수준에 대해 잘 이해가 안갔다. -
궁금한 점
추상화 수준이 "높다, 낮다, 중간이다" 라는 의미가 대체 무엇인가? -
이해한 점
펼치기/접기
1. 고수준(High-level) 코드란?
✔ 핵심 개념만 보여주는 코드
- "이 코드가 무엇을 하는지"를 설명하는 큰 그림(전체 흐름) 을 보여줌.
- 세부 구현을 감추고, 전체 로직을 명확히 이해할 수 있게 함.
- 주로 함수 호출이나 비즈니스 로직(업무 흐름) 에 해당함.
💡 예제
const placeOrder = ({ order }) => { validateAvailability(order); // 주문 가능 여부 확인 (고수준) // (여기서 중간 및 저수준 작업 진행) shipOrder(order); // 주문 배송 처리 (고수준) };
✔ 왜 고수준인가?
validateAvailability(order)
→ "주문이 가능한지 확인"shipOrder(order)
→ "주문을 배송한다"- 세부 구현이 아닌 "이 코드가 무엇을 하는지"를 표현하는 큰 개념 이다.
- 내부 구현이 감춰져 있어서, "어떻게 하는지"는 신경 쓸 필요 없음.
2. 저수준(Low-level) 코드란?
✔ 실제로 동작하는 상세 코드
- 세부적인 계산, 데이터 변환, API 호출 같은 작업을 직접 수행하는 코드
- "어떻게 작동하는지"를 설명하는 코드
- 구현 세부사항이 많아서 가독성이 떨어질 수 있음.
💡 예제
const total = order.items.reduce( (item, totalAcc) => totalAcc + item.unitPrice * item.units, 0, );
✔ 왜 저수준인가?
reduce()
를 사용해 주문 항목의 총 가격을 계산하는 세부적인 연산 코드- "어떤 방식으로 가격을 계산하는지"에 집중한 구체적인 구현 코드
3. 중간수준(Mid-level) 코드란?
✔ 고수준과 저수준을 연결하는 코드
- 고수준 코드와 저수준 코드 사이에서 데이터를 가공하고, 각 단계를 연결하는 역할을 함.
- 하나의 개념을 실행하는데 필요한 여러 작업을 모아서 처리하는 코드.
💡 예제
const invoiceInfo = getInvoiceInfo(order); const request = new PaymentService.Request({ total, invoiceInfo, }); const response = PaymentService.pay(request); sendInvoice(response.invoice);
✔ 왜 중간수준인가?
getInvoiceInfo(order)
→ 주문 정보를 가져옴.PaymentService.Request({ total, invoiceInfo })
→ 결제 요청 객체를 생성함.PaymentService.pay(request)
→ 결제 요청을 보냄.sendInvoice(response.invoice)
→ 결제 후 인보이스(청구서)를 보냄.
✅ 이들은 개별적으로 보면 특정 기능을 처리하는 코드이지만, 전체 주문 처리 과정에서 보면 중간 과정(연결 역할)을 담당하고 있음!
정리: 코드의 추상화 수준 이해하기
수준 특징 예제 고수준 (High-level) - "무엇을 하는지" 설명
- 전체 흐름을 보여줌
- 세부 구현을 감춤validateAvailability(order);
shipOrder(order);
중간수준 (Mid-level) - 고수준과 저수준을 연결함
- 여러 작업을 모아서 처리함const invoiceInfo = getInvoiceInfo(order);
const response = PaymentService.pay(request);
저수준 (Low-level) - "어떻게 동작하는지" 설명
- 세부적인 계산 및 구현const total = order.items.reduce(...);
-
3. 📚 참고 사항
📢 논의하기
- 관련 자료 공유
- 논의하고 싶은 주제
- 주제
switch
문과if/else
중 어떤 것을 써야 하는가? - 설명
저자는switch
문을 작성하는 것보다 단일 블록이나 함수를 사용하는 것을 선호한다고 한다.
그렇다면 우리는 어떤 것을 써야 할까?
- 주제
No Comments