coding-style | domain-driven-design | refactoring | clean-code | software-design

언어 독립적인 도메인 중심 코딩 원칙과 실천법 (Domain-Centric Coding Principles)

DDD·Clean Code·Refactoring의 핵심을 통합한 도메인 중심 코딩 원칙 12가지와 조직·프로세스 수준에서 반복되는 안티패턴과 실패 패턴을 정리합니다.

Mimul
MimulJanuary 29, 2026 · 25 min read · Last Updated:

LLM을 활용해 Java, Rust, TypeScript, Python, Go 등 여러 언어로 동시에 작업하다 보면 언어별 스타일 가이드만으로는 부족하다는 것을 느끼게 된다. AI가 코드를 생성할 때 “중첩을 줄여라”, “이름을 명확히 해라” 수준이 아니라, 왜 그렇게 해야 하는지 근거가 있는 원칙이 필요하다. 이 원칙은 Domain-Driven Design(DDD), Clean Code, Code Complete, Refactoring의 핵심 내용을 통합하여, 비즈니스 도메인을 최우선으로 삼으면서도 읽기 쉽고 유지보수성이 높으며 결함이 적은 코드를 만드는 것을 목표로 한다. AI가 코드를 생성하고 수정하는 빈도가 높아질수록, 코드가 얼마나 명확하고 의도적으로 작성되었는지가 AI의 추론 품질과 직결되기 때문에 이 원칙들은 AI 시대에 더욱 중요해진다.

Core Philosophy

코드의 존재 목적은 도메인 지식을 안전하게 보존하고 발전시키는 것이다. 소프트웨어의 본질은 문법의 우아함, 프레임워크 활용, 또는 기술적 기교가 아니라, 비즈니스/도메인 행동을 명확하게 인코딩하여 인간(현재와 미래)이 안전하게 이해하고 수정하고 확장할 수 있게 하는 데 있다.

최우선 고려 대상:

  • Domain Clarity (도메인 명확성)
  • Change Safety (변경 안전성)
  • Explicit Intent (명시적 의도)
  • Evolutionary Design (진화 가능한 설계)
  • Cognitive Load Reduction (인지 부하 감소)

최우선으로 고려하지 말아야 할 것:

  • Clever abstractions (영리하지만 불필요하게 복잡한 추상화)
  • Minimal lines of code (코드 라인 수를 과도하게 줄이려는 시도)
  • Framework purity (프레임워크의 규칙과 관례를 가장 우선시하는 설계)
  • Premature reuse (아직 필요하지 않은 시점에서의 조급한 재사용)
  • Theoretical elegance (실제 실용성보다 이론적·미학적 우아함을 추구하는 것)

1. Domain First (도메인 우선)

  • 코드 구조는 테이블, API, 프레임워크가 아닌 도메인 개념과 비즈니스 행동을 따라야 한다. 비즈니스 행동은 상태와 가까운 곳에 존재해야 한다.
  • 도메인 전문가와 개발자가 사용하는 동일한 용어를 코드, 테스트, 문서, 커밋 메시지 전반에 일관되게 사용한다.
  • 기술적 레이어가 아닌 비즈니스 capability와 변화 축(change axis) 을 기준으로 경계를 나눈다.
  • 비즈니스 규칙은 반드시 도메인 모델 내부에 명시적으로 표현되어야 한다. 중요한 비즈니스 규칙은 Controller, SQL, UI, Framework Annotation, 설정 파일 등에 숨기지 않는다.

2. Layered Architecture & Boundaries (계층 아키텍처와 경계)

  • Domain Layer는 Application, Infrastructure, Presentation Layer로부터 철저히 보호된다.
  • 외부 의존성(DB, API, 라이브러리)은 Adapter/Port로 격리한다.
  • Domain Model은 기술적 세부사항에 오염되지 않아야 한다.

3. Explicit & Intentional Code (명시적이고 의도적인 코드)

  • 코드는 단순히 “어떻게(How)“가 아니라 왜(Why) 그렇게 동작하는지를 설명할 수 있어야 한다. 좋은 코드는 의도, 제약사항, 불변조건(invariant), 트레이드오프를 드러낸다.
  • 잘못된 상태를 매번 검증하는 대신, 애초에 잘못된 상태를 표현할 수 없게 모델링한다. 명확한 타입, 제한된 상태 전이, 생성 시 검증 등을 고려한다.
  • 명시성은 가독성과 AI 추론 품질의 핵심이다. 흐름이 직관적으로 드러나고(explicit flow), 상태 전환이 추적 가능하며(visible state transition), 코드를 읽었을 때 동작이 예측 가능한(predictable behavior) 코드를 추구한다.
  • 가까운 코드만 읽고도 동작을 이해할 수 있어야 한다. 국부적으로 이해 가능하고 숨겨진 흐름이 적으며 컨텍스트 복원 비용이 낮다. AI는 전체 프로젝트를 모두 읽지 못하기 때문에 이 원칙은 AI 시대에 특히 중요하다.

4. Readability as Primary Quality (가독성을 최우선 품질로)

  • 읽기 쉬운 코드는 유지보수 비용, 버그 발생률, 리뷰 비용, AI hallucination 가능성을 낮춘다.
  • 관련된 로직은 가까이 배치하여 cognitive jump를 최소화한다. 응집도 높은 모듈(cohesive module), 얕은 계층 구조(shallow structure), 명시적 의존성(explicit dependency)을 추구한다.
  • 이름은 의도, 역할, 제약, 비즈니스 의미, side effect 가능성을 드러내야 한다.
  • 주석은 코드 메커니즘이 아니라, “왜 그렇게 설계했는지”, “어떤 tradeoff가 있는지”, “어떤 제약이 존재하는지”를 설명해야 한다.

5. Complexity Control & Simplicity (복잡도 제어와 단순성)

  • 복잡성은 변경 비용, 버그 가능성, 리뷰 난이도, AI 추론 실패를 모두 증가시킨다. 복잡성을 지속적으로 억제하고 단순함을 추구한다.
  • 복잡성은 상태(state) -> 결합도(coupling) -> 분기(branching) -> 코드량(code volume) 순으로 줄인다.
  • 모든 추상화는 유지보수 비용을 가진다. 추상화는 오직 인지 부하를 줄이고 반복되는 개념을 안정화하며 변경 비용을 낮출 때만 한다.
  • 복잡한 orchestration보다 단순하고 명시적인 흐름을 선호한다. 분기 처리가 명시적이고(explicit branching), 실행 흐름이 예측 가능하며, 데이터 흐름이 명확하게 추적 가능한 코드가 기준이다.
  • 잘못된 추상화는 복잡성을 시스템 전체로 전파한다. 작은 지역 중복(local duplication)은 종종 조급한 추상화(premature abstraction), 변경이 잦은 불안정한 로직을 여러 곳에서 공유하는 설계(unstable shared logic), 시기상조인 재사용(forced reuse) 보다 안전하다.

6. Changeability & Refactoring (변경 용이성과 리팩토링)

  • 가장 중요한 것은 “완벽한 설계”가 아니라 안전하게 진화 가능한 구조이다.
  • Refactoring은 특별한 이벤트가 아니라 일상적인 개발 활동이다. 코드를 만질 때마다 naming 개선, ambiguity 제거, dead code 제거, 책임 분리, 흐름 단순화를 수행한다.
  • 행동은 유지한 채 구조만 개선한다.
  • 작은 단계로 안전하게 리팩토링하고, 테스트로 보호한다.

7. Consistency & Predictability (일관성과 예측 가능성)

  • 개인의 취향보다 팀 전체의 일관성이 더 중요하다. 일관성은 이해 속도, 리뷰 품질, 유지보수성, AI retrieval quality를 크게 향상시킨다.
  • 비슷한 문제는 비슷한 방식으로 해결한다. 예측 가능한 구조는 cognitive load, 탐색 비용, AI hallucination risk를 줄인다.
  • 기계적으로 검사 가능한 것은 자동화한다. formatter, linter, static analysis, automated review는 자동화하고 인간은 도메인 이해, 설계 판단, correctness, tradeoff에 집중해야 한다.

8. Exception Handling (예외 처리)

예외는 허용되지 않은 상태가 발생했을 때 반응하는 수단이다. 잘못 다루면 디버깅과 시스템 신뢰성 양쪽에 영향을 준다.

  • 에러는 침묵하지 않는다. 의미 없는 기본값으로 덮어씌우거나 조용히 무시하는 것은 문제를 숨기는 심각한 반(反)패턴이다.
  • assert는 프로덕션 입력 검증 수단이 아니다. 개발자 불변식(invariant)을 선언하는 용도로 개발/테스트 환경에서만 사용한다. Assertion·Validation·Domain Error의 구분은 10번을 참고한다.
  • 예외를 던질 때는 반드시 제대로 된 Exception 객체를 사용한다. 단순 문자열이나 primitive를 throw하면 스택 트레이스가 사라져 디버깅이 극도로 어려워진다.
  • 타입 캐스팅(as, downcasting, cast())은 런타임 검사를 동반해야 한다. 컴파일러가 허용했다고 안전하다고 착각하지 않는다.
  • switch/match 문에는 항상 default 케이스를 포함한다. 새 enum 값이나 케이스가 추가될 때 조용히 실패하는 버그를 방지한다.
  • 예외는 진짜 예외적인 상황에만 사용한다. 예측 가능한 비즈니스 분기는 조건문으로 처리한다. 예외를 흐름 제어에 사용하는 것은 성능 저하와 코드 가독성 저하를 동시에 초래한다.

9. Performance & Resource Safety (성능과 리소스 안전성)

  • 성능 최적화는 측정 기반이어야 한다. 실제 병목을 파악한 후 해당 부분만 선택적으로 개선한다. 직감 기반 최적화는 대부분 불필요한 복잡성만 증가시킨다.
  • 알고리즘 복잡도는 신중하게 선택하고, O(n²) 이상은 명확한 이유가 있어야 한다. N+1 문제, 불필요한 데이터 조회(SELECT *), 긴 트랜잭션은 적극적으로 피한다.
  • 공유 가변 상태(shared mutable state)는 최대한 피한다. 동시성 문제(race condition, deadlock 등)와 테스트 신뢰성 저하를 유발한다.
  • 획득한 모든 리소스(DB 커넥션, 파일, 소켓 등)는 반드시 즉시 해제하여 리소스 누수를 방지한다.
  • 캐시는 “성능 향상 기능”이 아니라 데이터 일관성 시스템으로 취급한다. cache invalidation은 가장 어려운 문제 중 하나다.

10. Defensive Programming & Boundary Validation (방어적 프로그래밍과 경계 검증)

  • Trust Boundary(외부 입력 지점)에서만 철저히 검증한다. 내부 레이어에서 동일한 검증을 반복하지 않는다.
  • Assertion(프로그래머 가정), Validation(외부 입력), Domain Error(비즈니스 실패)를 명확히 구분한다.
  • Happy Path를 쉽게 읽을 수 있게 하고, 에러 처리 로직은 분리한다.
  • Null 대신 명시적인 Optionality(Option<T>, Maybe<T>) 또는 Domain-specific 타입을 선호한다.
  • Invalid state는 타입 수준에서 존재할 수 없도록 설계한다. 런타임 방어 코드를 여러 겹 쌓는 것보다 그런 상태 자체가 생성되지 않게 만드는 것이 낫다.

11. Testing (테스트)

  • 테스트 코드도 생산 코드와 동일한 수준으로 깨끗하고 읽기 쉽게 작성한다.
  • 테스트 이름과 데이터는 테스트하는 비즈니스 동작을 명확히 드러낸다.
  • 가능한 한 TDD를 활용한다.
  • 레거시 코드에는 Characterization Test를 적극 활용한다.

12. Change Process (작업 흐름)

설계, 코드 리뷰, 리팩토링 판단을 할 때마다 **“이번 결정에서 가장 중요한 평가 기준은 무엇인가?”**를 먼저 명확히 선언하고, 어떤 가치를 우선하며 어떤 가치를 양보하는지 트레이드오프를 명시적으로 기록한다.

기본 작업 흐름

  1. 변경할 도메인 의도와 영향을 충분히 이해한다.
  2. Preparatory Refactoring으로 구조 개선
  3. 최소한의 기능 변경 수행
  4. 테스트 추가/갱신
  5. Boy Scout Rule 적용하여 전체 정리
  6. 리뷰에서 가독성, 중복, 복잡도를 중점 확인

의사결정 평가축 (8개)

평가축주요 근거 및 출처설명
목적 적합성Value-Focused Thinking (Ralph L. Keeney, 1992)결정의 출발점인 “진짜 가치와 목적”을 최우선으로 삼아야 한다는 Keeney의 핵심 사상
제약 적합성Requirements Engineering (기능/비기능 요구사항), Systems EngineeringFunctional & Non-functional requirements를 체계적으로 고려하는 전통적 접근
실현 가능성INCOSE Systems Engineering Handbook의 Feasibility Analysis기술적·경제적·운영적·스케줄 feasibility를 기술적·조직적 관점으로 압축
품질 영향ISO/IEC 25019:2023 (Product Quality Model)Maintainability, Reliability, Security, Performance 등 소프트웨어 품질 국제 표준
시간 효과Technical Debt (Ward Cunningham, 1992)단기 vs 장기 관점에서의 비용·가치 트레이드오프
위험 및 불확실성Decision Analysis / Decision Quality의 Uncertainty 평가불확실성에 대한 명시적 체크
무결성Architecture Decision Records (ADR, Michael Nygard)아키텍처 결정의 일관성과 장기적 무결성을 유지하기 위한 기록 수단
합의 가능성Decision Quality의 Commitment to Action + Stakeholder Buy-in좋은 결정이라도 실행 의지와 합의가 없으면 실패한다

원칙을 어기는 코드와 의사결정은 특정 패턴으로 반복된다. 아래에는 코드 레벨에서 즉시 피해야 할 Anti-Pattern과, 조직·프로세스 수준에서 누적되는 실패 패턴을 나누어 정리한다.

금지하는 Anti-Patterns

코드 레벨에서 강력히 금지하는 Anti-Patterns

  • Anemic Domain Model — 데이터만 있고 행동(비즈니스 규칙)이 거의 없는 객체
  • DB-First / UI-First Domain Modeling — 데이터베이스 구조나 화면 설계에서 도메인 모델을 역으로 유도하는 것
  • Technical Naming Dominance — 기술적 이름이 도메인 개념을 압도하는 것
  • God Class / God Object — 하나의 클래스가 너무 많은 책임을 가져 이해와 유지보수가 매우 어려워지는 클래스
  • Primitive Obsession — 도메인 개념을 String, int, boolean 등의 기본 타입으로만 표현하는 것
  • Feature Envy — 메서드가 자신의 데이터보다 다른 클래스의 데이터에 과도하게 의존하는 코드
  • Long-lived Duplicated Logic & Deep Nesting — 중복 로직 방치와 과도하게 깊은 코드 중첩
  • Shared Mutable State — 공유 가변 상태로 인한 암묵적 의존과 예측 불가능한 버그
  • Silent Failure — 예외를 잡아서 무시하거나 의미 없는 기본값으로 대체하는 행위
  • Magic Numbers & Magic Strings — 의미 없는 리터럴 값을 직접 코드에 사용하는 것
  • Excessive Null Checking / Null Propagation Hell — Null을 기본값으로 남용하고 과도한 null 체크가 난무하는 설계
  • Giant Files / Oversized Modules — 하나의 파일이나 모듈이 지나치게 커져 책임이 모호해지는 것
  • Utility Dumping Grounds — 의미 없는 Utils, Helper, Common 클래스에 로직을 마구 dump하는 행위

설계 / 아키텍처 레벨 Anti-Patterns

  • Big-bang Rewrite — 점진적 리팩토링 대신 한 번에 대규모 재작성을 시도하는 것
  • Speculative Abstraction — 실제 필요하지 않은 미래를 위한 과도한 추상화
  • Excessive Layering / Meaningless Layering — 불필요하게 많은 레이어로 인한 복잡도 증가
  • Hidden Side Effects — 숨겨진 부수효과 (action at a distance)
  • Cyclic Dependencies — 순환 의존성
  • Deep Inheritance — 과도한 상속 계층 구조
  • Framework-driven Design — 도메인보다 프레임워크 구조에 맞추는 설계
  • Meta-programming Abuse — 과도한 메타프로그래밍과 리플렉션 남용
  • Inconsistent Naming — 일관성 없는 명명 규칙
  • Context-heavy Architecture — 지나치게 많은 컨텍스트를 요구하는 복잡한 아키텍처
  • Prompt-dependent Code Understanding — AI 프롬프트에 의존해야만 이해 가능한 코드

실패 패턴들

다음은 코드, 조직, 프로세스 수준에서 반복되는 실패 패턴들이다. 위 Anti-Pattern이 코드 레벨의 즉각적 문제라면, 아래는 팀과 프로젝트 단위에서 누적되는 구조적 실패다.

1. 바퀴의 재발명

  • 인증(Spring Security, JWT 등), 로깅(Logback, Log4j, SLF4J, Winston, Serilog), 캐시(Redis, Ehcache, Caffeine, Guava Cache), 날짜, 알림, 국제화, 페이징, 유효성 검사, 상태관리 등은 라이브러리가 있는데 자체적으로 구현하는 것.
  • DB 제약 조건으로 충분히 해결할 수 있는 것을 애플리케이션 코드에서 if문으로 검증하는 것.

2. Over Engineering

  • DBA가 없는 팀에 복잡한 샤딩 + 멀티마스터 DB 구성을 처음부터 설계하는 것.
  • 24시간 온콜 체계가 없는 팀인데 멀티 리전 고가용성 아키텍처를 도입하는 것.
  • Kubernetes나 Kafka 운영 경험이 거의 없는 팀이 본격 도입하는 것.
  • 상태가 ‘신청 → 승인 → 확정’ 정도로 단순한데 이벤트 소싱 + CQRS를 도입하는 것.
  • 하루 500건 정도의 CSV 처리를 Kafka + Consumer Group으로 구성하는 것.
  • 간단한 입력 검증(필수, 자리수, 정규식)에 Drools 같은 룰 엔진을 도입하는 것.
  • SRE 전담 인력이 없는 팀에 SLO, Error Budget, Chaos Engineering을 도입하는 것.

3. 땜방식 대응

  • 경계값 오류(Off-by-one, 빈 배열, 최대값, 최소값, 인덱스 범위 초과)를 고려하지 않은 채 배포하는 것.
  • null/nil/None 체크 안하거나 NullPointerException만 피하려고 if (x != null)을 단순 추가하는 것. 원인은 그대로 둔다.
  • 타입을 안 쓰거나, Any/unknown/Object 남발 (특히 TS, Python)
  • Controller나 Service에 도메인 로직을 직접 추가하는 것.
  • 도메인 규칙을 무시하고 화면에서만 validation을 하는 것.
  • 테스트가 깨지면 기대값을 실제 구현에 맞춰 수정하는 것.
  • 하드코딩 (URL, API 키, 설정값, 매직 넘버)

4. 준비 안된 출발

  • 요구사항이 아직 불명확한데 화면, API, DB를 먼저 만드는 것.
  • 에러 발생 시 업무 처리가 정해지지 않았는데 예외 처리 코드를 먼저 작성하는 것.
  • 권한 정책이 미정인데 UI에서만 숨기는 것.
  • 데이터 이관 방침이 결정되기 전에 이관 스크립트를 먼저 작성하는 것.
  • 성능 요구사항(건수, 응답시간)이 정해지지 않았는데 Redis 캐시를 처음부터 도입하는 것.

5. 너무 이른 추상화

  • 구현이 아직 하나밖에 없는데 Strategy 패턴을 도입하는 것.
  • 검색 조건이 name과 status 두 개뿐인데 독자 DSL을 설계하는 것.
  • 설정 항목이 거의 변하지 않을 것 같은데 미리 DB 기반 설정 관리 화면을 만드는 것.

6. 핵심 제약을 사후에 추가하는 역순 개발 관행

  • CRUD 기능을 먼저 만든 뒤, 나중에 권한 체크를 추가하는 것.
  • 데이터 갱신 기능을 만든 뒤에 감사 로그를 뒤늦게 추가하는 것.
  • 삭제 기능을 만든 후에 “이력 관리가 필요하다”는 요구를 받고 대응하는 것.
  • 동기 연동을 만든 후 재시도·멱등성(idempotency) 처리를 나중에 붙이는 것

7. “한 번 데였다고 다 금지하는” 반지성적 규칙

  • TypeScript 도입을 “타입 관리할 사람이 없다”며 거부하고 계속 JavaScript로 개발하는 것.
  • 과거에 빌드 자동화가 실패한 경험이 있다고 해서 CI/CD를 포기하고 수동 빌드·배포를 유지하는 것
  • 과거 Lambda/Stream API로 버그가 난 적이 있다고 해서, 모든 반복문을 확장 for문으로 강제하는 것.
  • Optional 사용을 금지하고, null 체크를 호출하는 쪽에 모두 떠넘기는 것.

8. 과거에 대한 과도한 배려

  • 기존 API를 깨뜨리지 않으려고 신규 API와 기존 API를 병렬로 계속 유지하는 것.
  • 오류 거부감 때문에 is_deleted, is_archived, is_suspended 같은 DB 칼럼이 계속 쌓이는 것.

Mimul

Written byMimul
Mimul is a programmer, technologist, exercise enthusiast and more.
Connect

Related ArticlesView All

Related StoriesView All