본문으로 건너뛰기

Functions, Objects and the Principles I Follow

Alex Kondovalexkondov.com ↗

Functions, Objects and the Principles I Follow

프레임워크가 없는 도메인 로직 영역에서는 아무것도 손을 잡아주지 않는다. 기본 원칙을 따르면 자연스럽게 좋은 위치에 도달한다 — 체스에서 기물 원칙을 따르듯이.

배경: 왜 원칙이 필요한가

  • React 앱이나 REST API는 프레임워크가 구조를 잡아주지만, 비즈니스 도메인 로직은 아무 가이드도 없음
  • 디자인 패턴과 베스트 프랙티스 자료들은 특정 맥락에 묶여 있어 범용 규칙으로 변환하려면 상상력이 필요
  • 저자는 알려진 패턴이 없는 구현을 탐색할 때 몇 가지 근본적인 프로그래밍 원칙에 의존

핵심 원칙들

1. 복잡성을 능동적으로 줄여라 (Actively Reduce Complexity)

  • 복잡성은 소프트웨어의 모든 문제의 근원으로 꼽힘
  • 복잡한 코드는 이해하는 데 더 많은 시간, 유지보수에 더 많은 노력이 필요 → 버그의 온상
  • 가장 좋은 방법은 복잡성이 처음부터 커지지 않도록 하는 것
반복(Repetition) vs 추상화(Abstraction) 트레이드오프
구분 반복적인 코드 잘못된 추상화
복잡도 낮음 (단순히 성가심) 높음
유지보수 관리 가능 어려움
저자 선호 추상화가 명백할 때만 제거 피함
  • 추상화가 painfully obvious할 때만 만들어야 함
  • 잘못된 추상화보다 중복 코드 관리가 훨씬 쉬움
  • OOP의 계층(hierarchy)와 함수형의 모나드(monad) 대신 함수와 객체라는 단순한 구성요소에 집중

2. 팩토리 함수 (Factory Functions)

클래스 대신 팩토리 함수를 쓰는 이유
  • 대부분의 앱은 도메인 엔티티(user, article, invoice 등)를 다루며, 역사적으로 클래스로 구현해왔음
  • 저자는 클로저 기반 팩토리 함수를 선호

클래스의 문제점:

  • 더 길고, 보일러플레이트가 많음
  • 상속(Inheritance)을 조장 → 설계에 부정적 영향
  • JavaScript의 this 키워드 문제

팩토리 함수의 장점:

  • 클로저가 더 이해하기 쉬움
  • this 관련 잠재적 문제 없음
  • 마이크로서비스 시대에 도메인 일부만 다루는 앱에서는 상속이 거의 필요 없음
팩토리 함수 설계 원칙
  • 객체 하나를 단일 파라미터로 받음 (파라미터가 많아지는 경향이 있으므로)
  • 내부 상태(private values)에 접근하는 함수만 엔티티에 붙임
  • 그 외 연산은 엔티티를 인자로 받아 퍼블릭 값으로만 동작하는 단순 함수로 분리
상속 대신 컴포지션 (Composition over Inheritance)
  • 다른 엔티티 간 로직 재사용 시 컴포지션 활용
  • 평탄한 멘탈 모델 → 클래스 간 점프 없이 엔티티의 기능을 파악 가능

3. 자료구조에 집중하라 (Focus on Data Structures)

  • 복잡성의 가장 큰 원천 중 하나는 사용하는 자료구조
  • 데이터는 모든 앱의 근간 → 잘못 다루면 치명적
잘못된 자료구조 선택의 비용
  • map이 적합한 곳에 array를 쓰면 → 부족한 기능을 보완하기 위한 더 복잡한 로직 필요
  • 자료구조의 복잡성은 격리되지 않고 **앱 전체로 누수(leak)**됨
실전 사례
  • 비동기 함수 의존 관계 문제: 자료구조 변경이 핵심 해결책 — 그래프 대신 트리 사용으로 막대한 노력 절감
  • 턴제 카드 게임 엔진 구현: 자료구조 연구에 많은 시간 투자 → 상태 머신(State Machine) 채택
    • 미리 정의된 상태(states)와 상태를 전환하는 액션(actions)의 개념이 턴제 게임의 본질과 완벽하게 일치

자료구조 선택은 애플리케이션 설계에서 가장 임팩트 있는 단일 결정이 될 수 있다.


4. 애플리케이션 구조 (Application Structure)

  • 모듈과 함수의 설계·API만큼이나 폴더/파일 구성 방식도 중요
  • 디렉토리와 내용물을 한 눈에 보는 것만으로 앱의 목적을 고수준에서 파악할 수 있어야 함
구조화 원칙
  • 기술적 책임(technical responsibility)으로 그룹핑하는 방식 비선호
    • 예: /controllers, /services, /models 같은 레이어 기반 구조
  • 함께 동작하는 함수·모듈은 함께 위치 시켜야 함
    • 코드의 의존 관계를 자연스럽게 반영하는 구조
    • 코드베이스를 보는 것만으로 모듈 간 상호작용을 파악 가능

5. 책임과 인터페이스 (Responsibilities and Interfaces)

SOLID 원칙에 대한 입장
  • OOP를 싫어하지만 SOLID 원칙은 추상적 수준에서 가치 있다고 인정
  • 클린 코드 임의 규칙보다 구조적 관점에서 더 유용
  • 단, 교조적으로(dogmatically) 따르면 안 됨
단일 책임 원칙 (SRP)
  • 핵심적으로 따르는 원칙 — 함수/모듈이 해야 할 것 이상을 하면 복잡성 증가
  • 주의: 길이(length)를 책임(responsibility)으로 착각하지 말 것
구분 지침
분리 기준 논리적으로 완결된 작업 단위 — 길이가 아님
긴 함수 하나의 작업을 완전히 수행한다면 OK
나쁜 분리 미적 기준으로 쪼개서 잘못 설계된 작은 함수들 생성
이름 짓기 논리적으로 완결된 작업을 분리해야 좋은 이름을 붙일 수 있음

함수는 자연스럽게 분리되어야 한다, 길이 때문이 아니라.

의존성 역전 원칙 (DIP)
  • 구체(concretion)가 아닌 추상(abstraction)에 의존
  • 적용 방식: 의존성 주입(Dependency Injection) 과 결합
    • 함수 내에서 특정 라이브러리/객체에 직접 의존하지 말고, 파라미터로 받아서 인터페이스로 계약을 명시
  • 효과:
    • 코드베이스 디커플링
    • 테스트 용이성 향상
    • transport / domain / data 레이어 분리 → 도메인 로직 격리

단, 비즈니스 로직과 엔티티는 코드베이스에서 더 dominant하게 위치하고 tight coupling이어도 괜찮다고 봄


6. 단순함은 쉽지 않다 (Simple Is Not Easy)

  • 단순성에 대한 집중 ≠ 낮은 노력의 코딩
  • 단순한 구현을 만드는 것은 복잡한 것보다 훨씬 더 많은 에너지와 집중력 필요
  • 우리는 “복잡성과 함께 파워가 올라간다”는 믿음 때문에 **오버엔지니어링에 사전 경향(predisposed)**이 있음
왜 단순성을 추구하는가
  • 복잡하게 만드는 건 언제든 가능하다 — 반대는 불가능
  • 단순성은 선택지를 열어둔다 (keeps your options open)
  • 소프트웨어 설계는 목표 달성을 위한 수단 → 단순성이 최선의 목표

결론: 체스 원칙처럼

  • 같은 체스 게임은 두 번 없고, 동일한 코드베이스도 없음
  • 그럼에도 원칙은 효과가 있기에 따름 — 나이트 먼저, 센터 장악, 킹 안전
  • 함수, 객체, 단순성에 대한 집중 — 이것만으로도 소프트웨어 엔지니어로서 멀리 갈 수 있다