Architecture & Design/Software Architecture

Principles - 03. 패러다임의 함의

걷기, 운전, 기차 — 세 패러다임이 사고를 제약하는 법

목적지에 가는 방법은 세 가지가 있다. 걸어간다(절차형) — 순서대로 한 발씩, 경로를 직접 결정한다. 운전한다(객체지향) — 자동차라는 상태(속도·연료)와 행동(가속·제동)이 함께하는 도구를 조종한다. 기차를 탄다(함수형) — 정해진 선로 위를 달리며, 내가 조종하지 않고 정해진 역에서 내린다.

세 방법은 이동이라는 같은 목표를 이루지만, 사고 방식이 다르다. 걷기는 절차(순서)에 집중하고, 운전은 객체(상태+행동)에 집중하며, 기차는 함수(입력→출력, 상태 없음)에 집중한다. 프로그래밍 패러다임도 같다 — 절차형·객체지향·함수형은 같은 문제를 다른 사고로 푼다.

절차형 — 순서가 곧 프로그램

절차형(procedural)은 데이터와 함수가 분리되어 있다. 데이터를 만들고, 함수에 데이터를 넘겨 처리한다.

// 절차형 — 데이터와 처리가 분리
double[] prices = {1000, 2000, 3000};
double total = calculateTotal(prices);   // 데이터를 함수에 넘김
applyDiscount(total, 0.1);               // 결과를 다시 함수에 넘김
printReceipt(total);

절차형은 단순하고 직관적이다. 작은 프로그램이나 스크립트에서는 가장 읽기 쉽다. 하지만 데이터가 많아지면 "누가 이 데이터를 바꿨는가"를 추적하기 어렵다 — 모든 함수가 모든 데이터에 접근할 수 있으므로.

객체지향 — 데이터와 행동이 함께

객체지향(OO)은 데이터(상태)와 행동(메서드)을 하나의 객체로 묶는다. Order 객체가 자신의 데이터(total, status)를 갖고 자신의 행동(cancel, confirm)을 스스로 수행한다.

// 객체지향 — 데이터와 행동이 객체 안에
Order order = new Order(items);
order.applyDiscount(0.1);   // 객체가 자기 상태를 변경
order.confirm();
// total, status는 order 안에 — 외부에서 직접 못 건드림

OO의 세 기둥은 캡슐화(상태 숨김), 다형성(인터페이스로 교체), 상속(코드 재사용)이다. 하지만 상속은 종종 남용된다 — 코드 재사용을 위해 깊은 상속 트리를 만들면 결합이 증가하고, LSP 위반이 발생한다(01번에서 본 Rectangle/Square 문제). 현대 담론은 상속보다 조합(composition)을 선호한다 — OrderDiscountPolicy를 상속하는 게 아니라 주입받아 쓰는 식.

함수형 — 상태를 바꾸지 않는다

함수형(FP)은 불변(immutable) 데이터와 순수 함수(pure function)를 기본으로 한다. 같은 입력에 항상 같은 출력을 내고, 부수 효과(side effect)가 없다.

// 함수형 — 불변, 부수 효과 없음
Money total = Money.ZERO;
Money discounted = items.stream()
    .map(item -> item.price().multiply(item.qty()))
    .reduce(Money.ZERO, Money::add);
// total은 바뀌지 않음 → 새 값을 반환할 뿐
// 같은 items를 넣으면 항상 같은 discounted가 나옴

FP의 장점은 테스트 용이성과 동시성 안전성이다. 순수 함수는 상태에 의존하지 않으므로 테스트하기 쉽고, 불변 데이터는 동시에 여러 스레드가 읽어도 안전하다. 단점은 상태가 필요한 도메인(은행 계좌, 주문 처리)에서 모든 것을 불변으로 처리하면 코드가 장황해질 수 있다.

패러다임이 사고를 제약한다

패러다임 중심 강점 약점
절차형 순서(절차) 단순, 직관적 데이터 추적 어려움 (전역 상태)
객체지향 객체(상태+행동) 캡슐화, 다형성 상속 남용, 결합 증가
함수형 함수(입력→출력) 불변, 동시성 안전, 테스트 용이 상태 처리 장황

패러다임은 "무엇을 할 수 있는가"가 아니라 "어떻게 생각하는가"를 정한다. 절차형은 "어떤 순서로 처리할까"를 묻고, OO는 "누가 무슨 책임을 질까"를 묻고, FP는 "어떤 입력이 어떤 출력을 내는가"를 묻는다.

설계 사례 — 주문 취소를 세 패러다임으로

같은 "주문 취소"를 세 패러다임으로 구현하면, 사고 방식의 차이가 선명해진다.

flowchart LR
    subgraph P["절차형"]
        PD["orderData (Map)"] --> PF["cancelOrder(orderData)"]
    end
    subgraph O["객체지향"]
        OO["Order 객체"] --> OM["order.cancel()"]
    end
    subgraph F["함수형"]
        FO["Order (불변)"] --> FF["cancel(order) → 새 Order"]
    end
// 절차형 — 데이터와 처리가 분리
Map<String, Object> orderData = repo.find(id);
if (!"PENDING".equals(orderData.get("status")))
    throw new IllegalStateException();
orderData.put("status", "CANCELLED");
repo.save(orderData);

// 객체지향 — 객체가 자기 상태를 변경
Order order = repo.find(id);
order.cancel();    // 객체가 규칙을 지킴 — 캡슐화
repo.save(order);

// 함수형 — 불변, 새 객체 반환
Order order = repo.find(id);
Order cancelled = Order.cancelled(order);   // order는 안 바뀜, 새 객체 반환
repo.save(cancelled);

절차형은 데이터를 직접 조작하므로 규칙("대기 중만 취소 가능")이 코드 여기저기 흩어진다. 객체지향은 객체가 규칙을 캡슐화한다. 함수형은 원본을 보존하고 새 객체를 반환한다 — 동시성에는 안전하지만, 상태 변경이 잦은 도메인에서는 매번 새 객체를 만드는 오버헤드가 있다.

현장에서 언제 무엇을 쓸까

세 패러다임을 실무에서 선택하는 기준이다. "OO vs FP"를 종교 논쟁으로 만들지 말고, 각 층의 요구에 맞는 도구를 고른다.

적합한 패러다임 이유
도메인 모델 (Order, Customer) OO 상태(잔액, 주문 상태)와 행동(취소, 입금)이 함께 묶여야 — 캡슐화
데이터 변환 (조회 → 응답 매핑) FP 입력만으로 출력, 상태 없음 — map/filter/reduce로 간결
인프라 설정·초기화 절차형 순차적 실행이 자연스러움 — "DB 연결 → 마이그레이션 → 서버 시작"
동시성·병렬 처리 FP 불변 데이터는 스레드 안전 — 공유 상태 없음

한 시스템에서 세 패러다임이 공존한다

// 도메인 (OO) — 상태와 행동이 함께
public class Order {
    private OrderStatus status;
    private Money total;
    public void cancel() {           // 상태를 변경하는 행동
        if (status != PENDING) throw new IllegalStateException();
        status = CANCELLED;
    }
}

// 데이터 변환 (FP) — 불변, 부수 효과 없음
public List<OrderResponse> toResponses(List<Order> orders) {
    return orders.stream()                                   // 불변 리스트에서
        .filter(o -> o.getStatus() == PLACED)                // 필터 (순수 함수)
        .map(this::toResponse)                               // 변환 (순수 함수)
        .toList();                                           // 새 리스트 (원본 안 건드림)
}

// 인프라 (절차형) — 순차적 설정
public void bootstrap() {
    Database.connect(url);        // 1. 연결
    Migration.run();              // 2. 마이그레이션
    Server.start(8080);           // 3. 시작
}

도메인은 상태 변경이 본질이므로 OO이 자연스럽고, 데이터 변환은 상태가 필요 없으므로 FP가 간결하며, 초기화는 순차 실행이 명확하므로 절차형이 읽기 쉽다. 한 언어(Java) 안에서 세 패러다임을 층마다 다르게 쓰는 것 — 이것이 "다 패러다임"의 실천이다. 한 패러다임으로 전부 해결하려 하지 말고, 각 층에 맞는 도구를 고르는 것이 실용적이다.

다 패러다임 시대

현대 언어(Java, Kotlin, Scala, TypeScript)는 세 패러다임을 섞어 쓴다. 도메인 객체는 OO(상태+행동 캡슐화)로 짜고, 데이터 변환은 FP(stream, map, reduce)로 처리하고, 인프라 설정은 절차형(순차적 초기화)으로 한다. 하나의 패러다임으로 전부 해결하려는 것보다, 각 층에 맞는 패러다임을 고르는 것이 실용적이다.


참고