CleanCoders - Function Structure

develkkm·2025년 10월 17일

CleanCoders

목록 보기
3/6

함수 구조

함수는 작고 명확해야 한다. 하지만 “작다”는 것만으로는 충분하지 않다.
함수가 어떤 구조로 작성되었는가, 인자와 반환, 예외와 제어 흐름을 어떻게 다루는가가 읽히는 코드와 유지보수 가능한 코드를 가르는 핵심이 된다.


인자 (Arguments)

인자는 적을수록 좋다

함수의 인자는 복잡도를 결정짓는다.
3개 이하가 이상적이며, 그 이상이 필요하다면 설계를 의심해야 한다.

void createUser(String name, int age, String address, boolean isAdmin);

→ 인자가 4개 이상이라면 “이 함수가 너무 많은 일을 하는 건 아닐까?”를 고민해야 한다.

해결책 ① — 파라미터 객체 사용

여러 인자가 논리적으로 하나의 의미를 가진다면, 객체로 묶어라.

class DateRange {
    private LocalDate from;
    private LocalDate to;
}

int getInvoicesWithin(DateRange range);

→ 인자 개수를 줄이면서 코드의 의미도 명확해진다.

해결책 ② — 빌더 패턴 활용

생성자에 인자가 많다면 Builder 패턴을 사용하라.

User user = new User.Builder("Kim")
        .age(25)
        .email("kim@example.com")
        .build();

필수 인자는 생성자에서 받고, 선택 인자는 setter 메서드로 체이닝한다.


불린(Boolean) 인자 피하기

불린 인자는 두 가지 일을 한 함수에 섞는 신호다.
함수는 한 가지 일만 해야 하므로, 불린 값으로 분기하는 대신 함수를 분리해야 한다.

// 나쁜 예시
void printReport(boolean detailed);

// 좋은 예시
void printSummaryReport();
void printDetailedReport();

이렇게 하면 함수 이름만 보고도 어떤 역할인지 명확히 알 수 있다.


CQS (Command–Query Separation)

CQS 원칙은 함수를 크게 두 종류로 구분한다.

종류역할상태 변경반환값
Command시스템 상태를 변경OX
Query상태 조회XO

하나의 함수가 둘을 동시에 수행하면 의도가 흐려진다.

// 나쁜 예시
int withdraw(int amount) {
    balance -= amount;
    return balance;
}

// 좋은 예시
void withdraw(int amount) { balance -= amount; }
int getBalance() { return balance; }

이 원칙을 지키면 함수의 부작용을 명확히 구분할 수 있다.


Tell, Don’t Ask

객체의 상태를 꺼내서 조건문으로 처리하지 말고,
객체에게 해야 할 일을 직접 시켜라.

// 나쁜 예시
if (order.isPaid()) {
    order.sendEmail();
}

// 좋은 예시
order.notifyIfPaid();

함수는 “상태를 묻는 존재”가 아니라,
“행동을 요청받는 주체”가 되어야 한다.


Law of Demeter (데메테르의 법칙)

“친한 객체에게만 말하라.”

함수는 다른 객체의 내부에까지 관여하지 않아야 한다.
즉, 메서드 체이닝(obj.getA().getB().getC())은 피해야 한다.

// 나쁜 예시
order.getCustomer().getAddress().getZipCode();

// 좋은 예시
order.getZipCode();

Order가 내부적으로 Customer를 알고 있다면,
그 세부 사항은 Order가 직접 처리하도록 해야 한다.
이렇게 하면 결합도가 낮아지고, 변경 영향이 줄어든다.


Early Return (조기 리턴)

복잡한 중첩은 함수의 흐름을 흐리게 만든다.
조기 리턴을 활용해 단계를 단순하게 만들어라.

// 나쁜 예시
void validateUser(User user) {
    if (user != null) {
        if (user.isActive()) {
            if (user.hasPermission()) {
                process();
            }
        }
    }
}

// 좋은 예시
void validateUser(User user) {
    if (user == null) return;
    if (!user.isActive()) return;
    if (!user.hasPermission()) return;

    process();
}

단, 루프 중간에서의 return은 피해야 한다.
흐름을 따라가기 어려워지고 디버깅이 복잡해진다.


예외 처리 (Error Handling)

예외 처리도 함수의 구조적 일관성을 깨뜨리기 쉬운 부분이다.

원칙 1 — try 블록은 한 가지 일만 해야 한다

try 블록 내부에 여러 줄의 코드를 두면
정상 흐름과 예외 흐름이 섞이게 된다.

try {
    processPayment();  // 한 문장만
} catch (PaymentFailedException e) {
    logError(e);
}

원칙 2 — 예외는 예외적으로

조건문으로 처리 가능한 상황을 예외로 던지지 마라.
예외는 정말 복구 불가능한 상황에서만 사용한다.

원칙 3 — 구체적인 예외 클래스 정의

불필요하게 많은 예외를 생성하지 말고,
특별한 경우는 특별한 클래스로 분리하라.

class Stack {
    static class ZeroSizeStack extends RuntimeException { }
}

원칙 4 — Null은 오류가 아닐 수도 있다

  • Null is not an error: 에러 신호로 null을 쓰지 마라.
  • Null is a value: “값이 없음”을 의미한다면 null 자체가 유효할 수 있다.

Null 여부를 계속 검사하기보다는, 단위 테스트로 보장하거나
Null Object 패턴으로 대체하는 편이 낫다.

class NullCustomer extends Customer {
    @Override
    public void sendPromotion() { /* 아무 일도 하지 않음 */ }
}

Switch 대신 다형성

Switch문은 객체지향의 적이다.
새로운 조건이 추가될 때마다 기존 코드를 수정해야 하기 때문이다.

// 나쁜 예시
switch (shape.getType()) {
    case CIRCLE -> drawCircle();
    case RECTANGLE -> drawRectangle();
}

→ 다형성으로 바꿔라.

interface Shape { void draw(); }

class Circle implements Shape {
    public void draw() { ... }
}

class Rectangle implements Shape {
    public void draw() { ... }
}

이제 새로운 도형이 추가되어도 기존 코드를 건드릴 필요가 없다.


함수의 배치와 Stepdown Rule

함수는 위에서 아래로 읽히는 흐름을 가져야 한다.
이때 public 메서드는 위에, private 메서드는 아래에 배치한다.

public class PaymentService {

    // 의도를 드러내는 공개 메서드
    public void pay() {
        if (isAvailable()) processPayment();
    }

    // 세부 동작을 수행하는 내부 메서드
    private boolean isAvailable() { ... }
    private void processPayment() { ... }
}

이런 구조를 Stepdown Rule이라 부른다 —
코드가 한 문서처럼 위에서 아래로 자연스럽게 읽히게 하는 규칙이다.


정리

항목핵심 원칙
인자3개 이하, 빌더·객체로 단순화
Boolean 인자두 함수로 분리
CQSCommand와 Query를 분리
Tell, Don’t Ask객체에 시켜라
Law of Demeter메서드 체이닝 지양
Early Return중첩 최소화, 단 루프 내 리턴은 피함
예외try는 한 문장, 예외는 구체적, Null Object
Switch다형성으로 대체
Stepdown Rulepublic 위, private 아래, 위→아래로 읽히는 흐름
profile
알던것을 더 확실하게

0개의 댓글