Skip to main content

3장 함수

1. 📌 핵심 개념 정리

✅ 좋은 함수란 무엇인가?

  • 함수를 작게 만들어라
    – 함수는 가능한 한 작고 단순해야 한다.
  • 한 가지 기능만 하도록 설계하라
    – 함수는 하나의 작업만 수행해야 하며, 여러 작업을 한 함수에 담으면 안 된다.
  • 함수의 추상화 수준을 일관되게 유지하라
    – 한 함수 내의 모든 문장은 같은 추상화 수준(예: 고수준, 중간수준, 저수준)을 가져야 한다.
  • 의도를 분명히 드러내는 함수명을 사용하라
    – 의미 있는, 서술적인 함수명을 사용하여 코드의 목적을 쉽게 파악할 수 있게 한다.
  • Switch문은 다형성을 활용하여 최소화하라
    – Switch문과 같은 조건 분기는 다형성(혹은 추상 팩토리 패턴)으로 감추어 코드의 개방-폐쇄 원칙(OCP)을 따르도록 한다.
  • 인수 개수를 최소화하라
    – 이상적인 함수 인수는 0개이며, 인수가 많아지면 가독성과 테스트가 어려워진다.
  • 오류 코드보다 예외를 사용하라
    – 오류 처리는 if-else 분기를 통해 코드와 혼합하기보다는 예외(Exception) 처리로 분리한다.
  • 부수 효과를 피하라
    – 함수가 외부 상태를 예상치 못하게 변경하지 않도록 작성하여, 시간적 결합이나 순서 종속성을 줄인다.
  • 명령과 조회를 분리하라
    – 함수는 상태를 변경하는 명령(Command) 또는 값을 반환하는 조회(Query) 중 하나만 수행해야 한다.
  • 중복을 제거하라
    – 중복 코드는 유지보수를 어렵게 하므로, 공통 기능은 모듈화(AOP, COP 등)하여 제거한다.
  • 구조적 프로그래밍을 준수하라
    – 함수는 입구와 출구가 하나여야 하며, return문은 한 번만 사용하는 것이 좋다.

2. ✅ 함수 작성 원칙

1. 함수를 작게 만들어라!

  • 목표: 함수는 가능한 한 작고 단순해야 한다.
  • 원칙:
    • 들여쓰기 수준은 1~2단계로 유지한다.
    • 중첩 구조(예: if/else, while 등)는 최소화한다.
    • 각 블록은 한 줄로 작성하도록 한다.

2. 한 가지 작업만 수행하도록 하라!

  • 목표: 함수는 단 하나의 책임만 가지도록 설계한다.
  • 원칙:
    • 함수 이름 아래에 한 가지 추상화 단계만 존재해야 한다.
    • 의미 있는 이름으로 여러 작업을 분리할 수 있다면, 분리해야 한다.
public void processOrder() {
    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. 함수의 추상화 수준을 일관되게 유지하라!

  • 목표: 함수 내 모든 문장이 같은 추상화 수준을 가져야 한다.
  • 원칙:
    • 상위 함수는 '무엇을' 하는지 설명하고, 하위 함수는 '어떻게' 하는지 세부 구현한다.
    • 내려가기 규칙(Top-Down Reading): 코드는 위에서 아래로 읽을 때, 추상화 단계가 한 단계씩 내려가야 한다.

4. Switch 문을 피하라!

  • 목표: Switch문은 여러 작업을 한꺼번에 처리하므로 피하거나 감춘다.
  • 원칙:
    • 다형성을 활용해 Switch문을 캡슐화한다.
    • OCP(개방-폐쇄 원칙)를 위반하지 않도록 한다.
public abstract class Employee {
    public abstract boolean isPayday();
    public abstract Money calculatePay();
    public abstract void deliverPay(Money pay);
}

5. 서술적인 함수명을 사용하라!

  • 목표: 함수명만 보고도 그 기능을 쉽게 이해할 수 있어야 한다.
  • 원칙:
    • 의미를 충분히 전달하는 긴 이름도 괜찮다.
    • 동사 및 명사를 조합해 일관된 문체로 작성한다.
// 나쁜 예
void d(); // 의미 불명확

// 좋은 예
void deleteUserById(int userId);

6. 함수 인수 개수를 최소화하라!

  • 목표: 인수 개수가 적을수록 함수가 단순해지고 테스트하기 쉽다.
  • 원칙:
    • 이상적인 함수는 인수가 0개(무항)이다.
    • 1개(단항)는 변환이나 상태 확인에 사용한다.
    • 2개(이항)는 비교나 두 값 처리를 위한 최소한의 경우.
    • 3개 이상이면 객체로 묶어 전달하는 방법을 고려한다.
// 개선 전
Circle makeCircle(double x, double y, double radius);

// 개선 후
Circle makeCircle(Point center, double radius);

7. 부수 효과를 일으키지 마라!

  • 목표: 함수가 외부 상태를 예상치 못하게 변경하지 않도록 한다.
  • 원칙:
    • 부수 효과(Side Effect)는 함수가 외부 변수나 시스템 상태를 변경하는 것을 의미한다.
    • 부수 효과로 인해 시간적 결합(Temporal Coupling)이나 순서 종속(Execution Order Dependency)이 발생할 수 있다.
    • 출력 인수(Out Parameter)를 피하고, 결과는 반환값으로 처리한다.

8. 명령과 조회를 분리하라!

  • 목표: 함수는 한 가지 역할(상태 변경 또는 값 반환)만 수행해야 한다.
  • 원칙:
    • 명령(Command) 함수는 상태를 변경하고, 조회(Query) 함수는 값을 반환해야 한다.
    • 둘을 혼합하면 코드의 예측 가능성이 떨어진다.
// 명령과 조회를 섞은 나쁜 예
if (deleteUser(userId)) {
    System.out.println("삭제 성공");
}

// 개선된 코드
boolean isDeleted = deleteUser(userId);
if (isDeleted) {
    System.out.println("삭제 성공");
}

9. 오류 코드보다 예외를 사용하라!

  • 목표: 오류 처리 코드를 본 로직과 분리하여 가독성을 높인다.
  • 원칙:
    • if-else문으로 오류 코드를 반환하는 방식은 명령-조회 분리 원칙에 어긋난다.
    • 예외(Exception)를 사용해 오류 상황을 처리하면 코드가 더 깔끔해진다.
// 나쁜 예 - 오류 코드 사용
int processPayment(Order order) {
    if (order.isPaid()) {
        return -1; // 이미 결제됨
    }
    return 0; // 정상 처리
}

// 좋은 예 - 예외 사용
void processPayment(Order order) throws PaymentAlreadyProcessedException {
    if (order.isPaid()) {
        throw new PaymentAlreadyProcessedException("이미 결제되었습니다.");
    }
}

10. 중복을 제거하라!

  • 목표: 중복된 코드는 유지보수를 어렵게 하므로 제거해야 한다.
  • 원칙:
    • 공통 기능은 모듈화하여 재사용성을 높인다.
    • AOP(관점 지향 프로그래밍) 또는 COP(컴포넌트 지향 프로그래밍)를 활용해 중복을 제거할 수 있다.
@Aspect
public class LoggingAspect {
    @Before("execution(* com.example.service.*.*(..))")
    public void logMethodCall(JoinPoint joinPoint) {
        System.out.println("Method called: " + joinPoint.getSignature());
    }
}

11. 구조적 프로그래밍을 준수하라!

  • 목표: 함수의 입구와 출구를 명확하게 하여 제어 흐름을 단순화한다.
  • 원칙:
    • 함수는 한 번만 return하는 것이 이상적이다.
    • 중첩 if-else문을 줄이고, early return을 사용해 가독성을 높인다.
// 나쁜 예
public boolean isValidUser(User user) {
    if (user != null) {
        if (user.isActive()) {
            return true;
        }
    }
    return false;
}

// 좋은 예
public boolean isValidUser(User user) {
    if (user == null) return false;
    return user.isActive();
}

3. 🤔 이해가 어려운 부분

A. 함수 내 추상화 수준

  • 문제: 한 함수 내에서 고수준(What을 설명)과 저수준(How를 구현) 코드가 섞이면 읽기 어려워진다.
  • 정의:
    • 고수준 코드 (High-level):
      – 전체 흐름이나 큰 개념을 설명하는 코드
      – 예: validateAvailability(order);, shipOrder(order);
    • 저수준 코드 (Low-level):
      – 세부적인 연산, 계산, API 호출 등 구체적인 구현을 다루는 코드
      – 예: total = order.items.reduce(...);
    • 중간수준 코드 (Mid-level):
      – 고수준과 저수준 사이의 다리 역할을 하며, 여러 작업을 묶어 처리하는 코드
      – 예: 결제 요청 객체를 생성하고, 결제 서비스를 호출하는 부분
  • 요약: 한 함수는 동일한 추상화 수준의 코드로 구성되어야 하며, 상위 함수는 ‘무엇을 할 것인가’를, 하위 함수는 ‘어떻게 할 것인가’를 담당해야 한다.

B. 반복하지 마라! – AOP와 COP

  • AOP (Aspect-Oriented Programming, 관점 지향 프로그래밍)
    • 정의:
      – 핵심 기능(Core Concern)과 부가적 관심사(Cross-cutting Concern: 예. 로깅, 보안, 트랜잭션 관리)를 분리하여 모듈화하는 기법
    • 장점:
      – 중복 코드 제거
      – 재사용성과 유지보수성 향상
  • COP (Component-Oriented Programming, 컴포넌트 지향 프로그래밍)
    • 정의:
      – 프로그램을 독립적인 컴포넌트(Component) 단위로 구성하여 각 컴포넌트가 자신의 역할에 집중하도록 하는 방식
      – (일부 문헌에서는 COP를 '문맥 지향 프로그래밍'으로 정의하기도 한다.)
    • 장점:
      – 컴포넌트 간 결합도를 낮추어 유지보수 및 확장이 용이함
  • 요약:
    – AOP와 COP는 공통 기능의 중복을 제거하고 코드의 재사용성을 높이기 위한 기법이다.

C. 모르는 단어

  • FitNesse:
    – 사용자 스토리 기반의 테스트를 지원하는 오픈 소스 웹 애플리케이션
    – 애자일 개발 프로세스에서 사용자와 개발자가 협력하여 소프트웨어 요구 사항을 검증하는 데 사용됨
  • 출력 인수 (Out Parameter):
    – 함수가 결과를 반환하기 위해 인수를 수정하는 방식
    – 객체 지향 언어에서는 출력 인수 대신 객체 자신의 상태를 변경하거나, 반환값으로 처리하는 것이 권장됨

4. 📢 논의할 주제

  • 부수 효과 최소화
    – 함수가 외부 상태를 변경함으로써 발생하는 시간적 결합과 순서 종속성 문제를 어떻게 해결할 수 있을까?

  • 플래그 인수를 사용하지 않고 여러 기능 처리하기
    – 플래그 인수를 사용하면 함수가 여러 작업을 암시하므로, 이를 대체할 수 있는 설계 방법은 무엇일까?

  • 객체지향적인 방식으로 Switch문 리팩토링
    – 다형성을 활용해 조건 분기를 제거하고, 확장성이 높은 설계를 어떻게 구현할 수 있을까?

  • 함수 내 추상화 수준 명확히 구분하기
    – 고수준, 중간수준, 저수준 코드를 어떻게 효과적으로 구분하여 작성할 수 있을까?

  • 서술적인 함수명 선정
    – 함수명을 통해 함수의 의도를 보다 명확히 전달하기 위한 좋은 사례와 방법은 무엇일까?

  • AOP와 COP의 실제 적용 사례 및 장단점
    – 두 기법을 실제 프로젝트에 적용할 때의 효과와 한계는 무엇인지 논의해보자.

  • 테스트 도구와 함수 설계의 관계
    – FitNesse와 같은 도구가 테스트 주도 개발(TDD)이나 함수 설계에 어떤 영향을 미치는지 토론해보자.


5. 📚 참고 사항

내려가기 규칙
추상화 수준
switch와 if/else 중 어떤 것을 써야 하는가?