한 줄 정의
컴포넌트를 기술적 역할(표현, 비즈니스, 영속성, 데이터베이스)에 따라 수평 계층으로 나누어 조직화하는, 가장 흔하고 단순한 아키텍처 스타일입니다.
쉽게 말하면
회사 조직도와 비슷합니다.
고객 응대(표현) → 실무 처리(비즈니스) → 서류 보관(영속성) → 금고(데이터베이스)처럼, 각 층이 자기 역할만 하고 아래층에 일을 넘기는 구조입니다.
위층은 아래층이 내부적으로 어떻게 일하는지 몰라도 되고, 아래층은 위층이 왜 요청했는지 알 필요가 없습니다.
Spring의 Controller → Service → Repository 구조가 바로 이것이며, 아무 생각 없이 코딩을 시작하면 자연스럽게 이 형태가 됩니다.
그래서 우발적 아키텍처(Accidental Architecture) 라는 안티패턴의 대표적 결과물이기도 합니다.
왜 중요한가?
다른 모든 아키텍처 스타일을 이해하기 위한 기준선(baseline) 이기 때문입니다.
마이크로서비스, 이벤트 기반, 파이프라인 등 대부분의 아키텍처 논의는 “계층형의 어떤 한계를 어떻게 극복하는가”에서 출발합니다.
계층형의 강점(관심사 분리, 단순성, 저렴한 비용)과 약점(기술 분할로 인한 도메인 변경 어려움, 확장성 부족, 싱크홀 안티패턴)을 정확히 알아야 언제 이 스타일을 선택하고, 언제 벗어나야 하는지 판단할 수 있습니다.
또한 대부분의 개발자가 커리어에서 가장 먼저, 가장 오래 접하는 아키텍처이므로 그 트레이드오프를 의식적으로 이해하지 못하면 습관적으로 이 구조에 갇히게 됩니다.
핵심 내용
토폴로지
컴포넌트들은 논리적인 수평 계층들로 조직화됩니다.
각 계층(layer)은 애플리케이션 내에서 특정한 역할을 수행합니다.
계층의 수와 유형에 특별한 제약은 없지만, 대부분의 계층형 아키텍처는 네 가지 표준 계층으로 구성됩니다.
flowchart TD A["표현 계층 (Presentation)"] --> B["비즈니스 계층 (Business)"] B --> C["영속성 계층 (Persistence)"] C --> D["데이터베이스 계층 (Database)"]
일부 아키텍처는 비즈니스 계층과 영속성 계층을 하나로 통합하기도 합니다.
특히 비즈니스 계층의 컴포넌트들에 영속성 관련 로직(SQL이나 HSQL 등)이 포함되어 있는 경우가 그렇습니다.
소규모 애플리케이션에서는 계층을 세 개만 둘 수도 있고, 반대로 더 크고 복잡한 비즈니스 애플리케이션은 계층을 다섯 개 이상 두기도 합니다.
물리적 토폴로지 변형
물리적 배포 관점에서 세 가지 변형이 있습니다.
첫 번째 변형은 표현, 비즈니스, 영속성 계층을 단일 배포 단위로 통합한 것입니다.
이 변형에서 데이터베이스 계층은 일반적으로 별도의 외부 물리적 데이터베이스(또는 파일 시스템)로 존재합니다.
둘째 변형은 표현 계층을 별도의 배포 단위로서 물리적으로 분리하고, 비즈니스 계층과 영속성 계층을 또 다른 배포 단위로 결합한 것입니다.
이 변형에서도 데이터베이스 계층은 일반적으로 외부 데이터베이스 또는 파일 시스템을 통해 물리적으로 분리됩니다.
셋째 변형은 데이터베이스까지 네 가지 표준 계층을 모두 단일 배포 단위로 결합한 것입니다.
이 변형은 데이터베이스를 애플리케이션에 내장하거나 인메모리 데이터베이스를 사용하는 소규모 애플리케이션(이를테면 모바일 기기용 애플리케이션)에 유용할 수 있습니다.
많은 온프레미스(사내 구축형) 제품이 이 셋째 변형을 이용해서 구축되고 고객에게 전달됩니다.
계층 간 격리
각 계층은 닫혀 있을 수도 있고 열려 있을 수도 있습니다.
닫힌 계층(closed layer) 은 주어진 요청이 건너뛸 수 없는 계층을 말합니다.
모든 계층이 닫힌 계층인 아키텍처의 경우, 한 요청이 최상위 계층에서 최하위 계층으로 이동하는 과정에서 그 어떤 계층도 건너뛸 수 없습니다.
한 계층에서 나온 요청은 반드시 그 아래 계층으로 가야 합니다.
flowchart TD subgraph "닫힌 계층 아키텍처" direction TB R["Request"] --> P P["표현 계층"] -- "닫힘" --> B["비즈니스 계층"] B -- "닫힘" --> S["영속성 계층"] S -- "닫힘" --> D["데이터베이스 계층"] end
왜 닫혀야 하는가?
단순 조회 요청이라면 비즈니스·영속성 계층을 건너뛰고 DB에 직접 접근하는 게 더 빠를 수 있습니다(2000년대 초반의 추월 차선 조회(Fast-Lane Reader) 패턴). 이처럼 건너뛸 수 있는 계층을 열린 계층(opened layer) 이라고 합니다.
하지만 계층을 열면 계층 간 격리(layers of isolation) 가 깨집니다. 핵심은 이것입니다:
표현 계층이 영속성 계층에 직접 접근할 수 있다면, 영속성 계층의 변경이 비즈니스 계층과 표현 계층 둘 다 에 전파됩니다. 의존 방향이 꼬이면서 결합도가 급격히 높아지고, 아키텍처가 깨지기 쉬워집니다.
닫힌 계층은 “반드시 바로 아래 계층만 호출한다”는 규칙을 강제합니다. 이 규칙 덕분에 각 계층은 인접 계층의 계약(인터페이스)만 알면 되고, 내부 구현 변경이 다른 계층으로 전파되지 않습니다.
Tip
Spring MVC의 Controller → Service → Repository 구조가 전형적인 닫힌 계층 패턴입니다. Controller에서 Repository를 직접 호출하면 당장은 편하지만, Service 계층의 트랜잭션 관리·비즈니스 검증을 우회하게 되어 나중에 버그 추적이 어려워집니다. “계층을 닫는다”는 건 결국 변경의 파급 범위를 한 단계로 제한하겠다 는 설계 의도입니다.
계층의 추가
모든 계층이 닫혀야 하는 것은 아닙니다.
공유 유틸리티(날짜/문자열 처리, 감사(auditing), 로깅 등)를 비즈니스 계층 안에 두면 문제가 생깁니다. 표현 계층이 비즈니스 계층에 접근할 수 있으므로 이 공유 객체에도 자연스럽게 접근할 수 있기 때문입니다.
해결책은 공유 객체를 별도의 서비스 계층 으로 분리하고, 이 계층을 열린 계층 으로 두는 것입니다.
flowchart TD P["표현 컴포넌트"] -- "닫힘" --> B["비즈니스 컴포넌트"] P -. "✕ 직접 접근 불가" .-> SV B -- "닫힘" --> SV["공유 컴포넌트<br/>(서비스 계층)"] B -- "열림 (건너뛰기 가능)" --> PS["영속성 계층"] SV --> PS PS -- "닫힘" --> DB["데이터베이스"]
이 구조의 핵심은 두 가지입니다:
- 비즈니스 계층이 닫혀 있으므로 표현 계층은 서비스 계층에 직접 접근할 수 없습니다. 접근 제한이 아키텍처 자체에서 강제됩니다.
- 서비스 계층이 열려 있으므로 비즈니스 계층은 공유 객체가 필요 없을 때 서비스 계층을 건너뛰고 영속성 계층에 직접 접근할 수 있습니다.
열림/닫힘은 반드시 문서화해야 합니다
어떤 계층이 열려 있고 닫혀 있는지, 그리고 왜 그런 결정을 내렸는지 문서화하지 않으면 개발자들이 임의로 계층을 건너뛰게 되고, 결국 강하게 결합된 아키텍처가 만들어집니다.
아키텍처 싱크홀 안티패턴
아키텍처 싱크홀(Architecture Sinkhole) 은 요청이 각 계층을 통과하면서 아무런 비즈니스 로직도 수행하지 않고 그대로 전달만 되는 현상입니다.
flowchart LR P["표현"] -- "그대로 전달" --> B["비즈니스"] B -- "그대로 전달" --> R["규칙"] R -- "그대로 전달" --> PS["영속성"] PS -- "단순 SQL" --> DB["DB"] DB -. "로직 없이 그대로 반환" .-> P
예를 들어 고객의 이름과 주소를 단순 조회하는 요청이 표현 → 비즈니스 → 규칙 → 영속성 → DB를 모두 거치지만, 집계·계산·변환 없이 데이터가 그대로 왕복합니다. 각 계층을 통과할 때마다 불필요한 객체가 생성되어 메모리와 성능을 낭비합니다.
얼마나 발생하면 문제인가?
80-20 규칙 으로 판단합니다:
| 싱크홀 비율 | 판단 |
|---|---|
| 20% 이하 | 정상 — 닫힌 계층의 대가로 수용 가능합니다 |
| 80% 이상 | 계층형 아키텍처가 해당 도메인에 부적합하다는 강한 신호입니다 |
대응 방법
모든 계층을 열린 계층으로 만들어 불필요한 통과를 줄일 수 있지만, 계층 간 격리가 약해져 변경 관리 비용이 증가합니다. 결국 싱크홀이 지배적이라면 계층형 아키텍처 자체를 재검토해야 합니다.
Tip
Spring에서 Service가 Repository를 단순 위임만 하는 메서드(
findById그대로 반환 등)가 많다면 싱크홀 징후입니다. 그렇다고 Controller에서 Repository를 직접 호출하면 격리가 깨지니, 이런 단순 CRUD 비율이 높은 도메인이라면 계층형보다 CQRS의 Query 측을 단순화하는 등 아키텍처 선택 자체를 재고하는 게 맞습니다.
데이터 토폴로지
단일 모놀리스 데이터베이스와 함께 하나의 모놀리스 시스템을 형성합니다. 영속성 계층은 주로 ORM을 통해 객체 위계구조와 관계형 DB 사이의 매핑을 담당합니다.
클라우드 고려 사항
기술적 분할 방식이므로 계층 단위로 분리 배포하는 것은 가능합니다. 하지만 요청이 모든 계층을 거치는 특성상, 온프레미스↔클라우드 간 통신 지연이 누적되어 성능 문제를 일으킬 수 있습니다.
일반적인 위험
모놀리스 배포 단위이므로 내결합성(fault tolerance) 이 없습니다. 한 부분의 메모리 부족 오류가 애플리케이션 전체를 죽일 수 있고, 평균 복구 시간(MTTR) 도 깁니다(대규모 앱은 재시작에 15분 이상 소요).
거버넌스
가장 잘 지원되는 아키텍처 스타일입니다. ArchUnit 같은 도구로 계층 간 접근 규칙을 코드로 강제할 수 있습니다:
layeredArchitecture()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Persistence").definedBy("..persistence..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service")패키지 이름으로 계층을 정의하고, 계층 간 허용되는 통신 방향을 선언적으로 관리합니다. 열린/닫힌 계층의 거버넌스를 자동화할 수 있다는 점이 큰 장점입니다.
팀 토폴로지 고려 사항
어떤 팀 구성에서도 사용할 수 있지만, 팀 유형별로 특히 잘 맞는 지점이 있습니다:
| 팀 유형 | 궁합 이유 |
|---|---|
| 스트림 정렬 팀 | 모든 계층을 관통하는 하나의 흐름을 팀이 소유하므로 독립적으로 작업 가능합니다 |
| 활성화 팀 | 기술적 관심사별로 분리되어 있어 특정 계층에서만 실험할 수 있습니다 (예: 표현 계층에서 새 UI 라이브러리 도입) |
| 난해한 하위시스템 팀 | 특정 계층(예: 영속성)에만 접근하여 다른 계층에 영향 없이 작업 가능합니다 |
| 플랫폼 팀 | 높은 모듈성 덕분에 다양한 도구를 활용할 수 있지만, 모놀리스가 커질수록 DB 연결·메모리·성능 등의 제약이 누적됩니다 |
아키텍처 특성 등급표
| 아키텍처 특성 | 별점 |
|---|---|
| 전반적인 비용 | $ (저렴) |
| 분할 방식 | 기술적 |
| 퀀텀 개수 | 1 |
| 단순성 | ★★★★★ |
| 모듈성 | ★ |
| 유지보수성 | ★ |
| 테스트성 | ★★ |
| 배포성 | ★ |
| 진화성 | ★ |
| 반응성 | ★★★ |
| 확장성 | ★ |
| 탄력성 | ★ |
| 내결합성 | ★ |
강점
단순성(★5)과 저비용 이 최대 강점입니다. 분산 아키텍처보다 복잡하지 않고 이해하기 쉽습니다.
약점
- 배포성(★1): 코드 3줄을 바꿔도 전체를 재배포해야 하며, 관련 없는 변경이 함께 묶여 배포 위험이 증가합니다
- 테스트성(★2): 계층 단위로 mock/stub 대체가 가능해서 최악은 아닙니다
- 탄력성·확장성(★1): 퀀텀이 항상 1이므로 특정 기능만 확장하기 어렵습니다. 멀티스레딩 등으로 가능은 하지만 이 아키텍처와 잘 맞지 않습니다
- 반응성(★3): 캐싱·멀티스레딩으로 높일 수 있지만, 싱크홀 안티패턴과 닫힌 계층 통과 비용이 발목을 잡습니다
모든 특성은 동적입니다
처음에는 좋아 보이지만, 코드베이스가 커질수록 모든 특성이 나빠집니다.
언제 사용하면 좋은가
- 작고 단순한 애플리케이션이나 웹사이트
- 예산과 시간이 매우 제한된 상황
- 아직 적합한 아키텍처를 결정하지 못했지만 일단 개발을 시작해야 할 때
- 타당성(feasibility) 이 최우선인 조직 — “일단 제공하고 나중에 재작성”이 합리적인 경우
나중에 다른 아키텍처로 전환할 가능성이 있다면, 상속 트리를 얕게 유지하고 모듈성을 확보해 두는 것이 좋습니다.
사용하지 말아야 할 때
규모가 커지면 유지보수성·테스트성·배포성이 급격히 나빠지므로, 대규모 시스템에서는 모듈성이 더 좋은 아키텍처 스타일을 선택해야 합니다.
예시와 용례
계층형 아키텍처는 소프트웨어에만 국한되지 않습니다. 관심사의 분리 가 중요한 모든 시스템에서 발견됩니다:
- 운영체제: 하드웨어 → 커널 → 시스템 호출 인터페이스 → 사용자 계층
- 네트워크: OSI 7계층 모델, TCP/IP 프로토콜 스택
정리
- 가장 흔하고 단순한 아키텍처 스타일로, 비용이 저렴하고 익숙하지만 “무작정 코딩”으로 이어지기 쉬운 우발적 아키텍처의 위험이 있습니다.
- 기술적 분할 아키텍처이므로 도메인 변경 시 모든 계층에 영향이 퍼지며, DDD와 맞지 않습니다.
- 계층 간 격리 가 핵심 개념입니다. 닫힌 계층은 결합도를 낮추고 변경 영향을 차단하며, 열린 계층은 성능과 유연성을 위해 선택적으로 사용합니다.
- 아키텍처 싱크홀 안티패턴 에 주의해야 합니다. 요청이 로직 없이 계층을 통과만 하는 비율이 80%를 넘으면 이 스타일이 적합하지 않다는 신호입니다.
- 단순성(★5)이 최대 강점이고, 모듈성/확장성/탄력성/내결합성(★1)이 최대 약점입니다. 코드베이스가 커질수록 모든 특성이 나빠지는 동적 성질을 갖습니다.
- 작고 단순한 앱, 예산/시간 제약이 큰 상황, 타당성(feasibility) 우선 조직에 적합합니다. 대규모 시스템에서는 피해야 합니다.
내 생각
- 스프링 기반 웹 애플리케이션은 자연스럽게 Controller → Service → Repository 계층 구조를 따르므로, 사실상 대부분의 자바 백엔드 프로젝트가 계층형 아키텍처로 시작합니다. “우발적 아키텍처”라는 표현이 정확히 들어맞는 이유입니다.
- 싱크홀 안티패턴은 실무에서 정말 자주 발생합니다. 단순 CRUD가 대부분인 관리자 페이지에서 Service 레이어가 Repository를 단순 위임만 하는 경우가 대표적입니다. 이럴 때 80-20 규칙으로 판단하는 것이 실용적인 기준입니다.
- 모놀리스에서 마이크로서비스로 전환할 때 계층형 아키텍처의 “기술적 분할”이 가장 큰 장벽이 됩니다. 도메인별로 코드가 모든 계층에 흩어져 있어서 도메인 단위로 분리해내기가 어렵기 때문입니다.
관련 개념
출처
- 소프트웨어 아키텍처 The Basics (Mark Richards, Neal Ford) — Chapter 10