3장 함수
1. 📌 핵심 개념 정리
✅ 요약하기
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
문 중첩을 최소화하여 가독성을 높였다.
2. 한 가지만 해라!
"함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다." 함수는 지정된 이름 아래에서 추상화 수준이 하나인 단계만 수행해야 하며, 의미 있는 이름으로 다른 함수를 추출할 수 없을 때까지 작게 만들어야 한다.
- 추상화는 필요한 정보만 남기고 불필요한 세부사항을 숨기는 개념이며, "무엇을 할 것인가?"(What)에 집중하고 "어떻게 처리할 것인가?"(How)는 숨기는 원칙이다.
3. 함수당 추상화 수준은 하나로!
함수가 한 가지 일만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다. 코드는 위에서 아래로 책처럼 자연스럽게 읽혀야 한다.
- 개선 전
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.");
}
}
각 단계를 함수로 분리하여 코드 흐름을 책처럼 자연스럽게 읽히도록 개선했다.
4. Switch 문
본질적으로 N
가지를 처리하는 switch
문을 완전히 피할 수는 없지만, 다형성을 이용하여 각 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);
}
}
}
- 개선 후
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
문은 추상 팩토리에 의해 숨겨지고 객체를 생성하는 부분에만 사용된다.
5. 서술적인 이름을 사용하라!
이름이 길더라도 괜찮다. 길고 서술적인 이름이 짧고 모호한 이름보다 좋으며, 서술적인 주석보다도 효과적이다. 함수 이름은 함수 기능을 명확히 표현해야 하며, 모듈 내에서 함수 이름은 같은 문구, 명사, 동사 형태를 일관성 있게 사용해야 한다.
- 좋은 예시:
includeSetupAndTeardownPages
,includeSetupPages
,includeSuiteSetupPage
6. 함수 인수
함수의 인수는 적을수록 좋다. 이상적인 인수 개수는 0개(무항)이며, 단항, 이항 함수 순으로 좋다. 3개 이상의 인수는 가능한 피하는 것이 좋다.
- 함수 인수에
boolean
값을 넘기는 것은 피해야 한다. - 인수가 많아진다면 독자적인
class
로 묶는 것을 고려해야 한다. - 함수 이름에 인수 이름을 추가하면 함수 의도를 더 명확하게 할 수 있다.
assertEquals(expected, actual)
보다assertExpectedEqualsActual(expected, actual)
이 더 명확하다.
7. 오류 코드보다 예외를 사용하라!
오류 코드를 반환하는 대신 예외를 사용하여 오류 처리 코드를 분리하고 코드를 깔끔하게 유지해야 한다. try-catch
블록을 사용하여 오류를 처리하고, try-catch
블록은 별도의 함수로 분리하는 것이 좋다.
public void delete(Page page) {
try {
deletePage(page);
} catch (Exception e) {
log.error(e);
}
}
8. 반복하지 마라
중복은 소프트웨어의 모든 악의 근원이다. 중복을 제거하기 위해 다양한 원칙과 기법 (관계형 데이터베이스 정규화, 구조적 프로그래밍, AOP, COP 등)이 등장했다.
9. 구조적 프로그래밍
함수는 return
문이 하나여야 하며, 루프 안에서 break
나 continue
를 사용하지 않고 goto
문은 절대 사용해서는 안 된다.
10. 함수를 어떻게 짜죠?
처음부터 완벽한 함수를 작성하기는 어렵다. 초안은 길고 복잡할 수 있지만, 점진적으로 리팩토링하고 다듬어 나가면서 깨끗한 함수를 만들어야 한다. 단위 테스트를 작성하면서 함수를 개선해 나가는 것이 좋은 방법이다.
2. 🤔 이해가 어려운 부분
🔍 질문하기
1. 개념 또는 원칙의 이름: AOP, COP
- 어려웠던 부분: AOP, COP란 무엇인가? 중복 제거에 어떻게 활용되는가?
- 궁금한 점: AOP와 COP의 정확한 개념과 중복 제거와의 연관성
이해한 점:
펼치기/접기
AOP (Aspect-Oriented Programming, 관점 지향 프로그래밍)
AOP는 핵심 비즈니스 로직(Core Logic)과 부가적인 관심사(Cross-cutting Concerns)를 분리하여 코드의 재사용성을 높이고 유지보수를 쉽게 만드는 프로그래밍 패러다임이다.
-
AOP의 주요 개념
- Aspect(애스펙트): 부가적인 기능(로깅, 트랜잭션 관리 등)을 모듈화한 것.
- Join Point(조인 포인트): 애스펙트가 적용될 수 있는 지점(메서드 실행, 예외 발생 등).
- Pointcut(포인트컷): 애스펙트를 적용할 특정 조건을 정의.
- Advice(어드바이스): 애스펙트가 실행될 구체적인 동작(메서드 실행 전/후, 예외 발생 시 등).
- Weaving(위빙): 핵심 코드와 애스펙트를 결합하는 과정.
-
장점
- 중복 코드 제거 (로깅, 트랜잭션 관리 같은 부가적인 기능을 분리 가능)
- 핵심 로직과 부가 기능 분리하여 유지보수 용이
COP (Context-Oriented Programming, 문맥 지향 프로그래밍)
COP는 실행되는 문맥(Context)에 따라 동적으로 프로그램의 동작을 변경하는 프로그래밍 기법이다. AOP가 주로 부가적인 기능을 모듈화하는 데 집중하는 반면, COP는 동적인 문맥(Context)에 따라 동작을 변경하는 데 초점을 맞춘다.
-
COP의 주요 개념
- Context(문맥): 애플리케이션의 현재 상태(사용자의 위치, 시간, 디바이스 종류 등).
- Layer(레이어): 특정 문맥에서 활성화될 동작을 정의.
- Activation(활성화): 특정 문맥이 충족될 때 적절한 레이어를 활성화하여 동작을 변경.
-
장점
- 사용자 환경(시간, 장소, 디바이스 등)에 따라 동적으로 동작 변경 가능
- 애플리케이션의 유연성을 높이고, 사용자 맞춤형 서비스 구현에 유리
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. 함수에 기능 하나씩
- 추상화: 함수의 지정된 이름 아래 추상화 수준이 하나인 단계
- 궁금한 점: 여기서 추상화란 정확히 무엇을 의미하는가?
이해한 점:
펼치기/접기
추상화 수준이 하나라는 것은 한 함수가 너무 낮은 수준의 작업(세부적인 로직)과 높은 수준의 작업(큰 개념)을 동시에 처리하면 안 된다는 의미이다.
- 개선 전
public void processOrder() {
// 주문 처리 (높은 수준)
System.out.println("주문을 처리합니다.");
// 데이터베이스에서 주문 정보 가져오기 (낮은 수준)
Order order = database.getOrderById(1);
// 결제 처리 (낮은 수준)
PaymentProcessor processor = new PaymentProcessor();
processor.processPayment(order);
// 이메일 전송 (낮은 수준)
EmailService emailService = new EmailService();
emailService.sendOrderConfirmation(order);
}
- 개선 후
public void processOrder() {
System.out.println("주문을 처리합니다.");
Order order = getOrder();
processPayment(order);
sendConfirmationEmail(order);
}
private Order getOrder() {
return database.getOrderById(1);
}
private void processPayment(Order order) {
PaymentProcessor processor = new PaymentProcessor();
processor.processPayment(order);
}
private void sendConfirmationEmail(Order order) {
EmailService emailService = new EmailService();
emailService.sendOrderConfirmation(order);
}
3. 📚 참고 사항
🔍 질문하기
1. 모르는 단어
- FitNesse: 사용자 스토리 기반의 테스트를 지원하는 오픈 소스 웹 애플리케이션이다. 애자일 개발 프로세스에서 사용자 및 개발자 협력하여 소프트웨어 요구 사항을 검증하는 데 사용된다.
- 출력 인수: 프로그램에서 결과를 화면, 콘솔, 로그 등으로 출력하는 부분이다. 출력 코드는 프로그램이 사용자나 다른 시스템에 정보를 제공하는 중요한 역할을 한다.
📢 논의하기
1. 가독성 있는 함수 작성법
- 주제: 추상화 패턴으로 함수를 작성 시 가독성 있는 함수 작성법
- 설명: 복잡한 비즈니스 로직을 추상화 패턴을 사용하여 작성할 때, 코드를 따라가기 어려운 경우가 있다. 가독성 있는 함수를 작성하기 위해 어떻게 해야 할까? 한 클래스 파일에는 한 가지 기능만 담겨야 하는가?
2. switch
문과 if/else
- 주제:
switch
문과if/else
중 어떤 것을 써야 하는가? - 설명: 저자는
switch
문보다 단일 블록이나 함수를 사용하는 것을 선호한다고 한다. 그렇다면 우리는 어떤 것을 써야 할까?
3. 부수 효과 최소화
- 주제: 부수 효과 최소화
- 설명: "시간적인 결합이나 순서 종속성을 초래한다"는 의미가 무엇인가? 함수는 외부 상태를 변경하지 않도록 하여 예측 가능한 결과를 보장해야 한다는 뜻인가?
No Comments