서버

NestJS 철학 이해하기 1 : Layered Architecture

행복한 시지프 - 서버 2026. 2. 1. 16:16

 

들어가며

서버 개발을 하면서, Layered Architecture 가 자주 쓰입니다. Spring, NestJS 가 기본적으로 강제하고 있는 방식입니다. 이 아키텍쳐를 그대로 따르는 것이 팀 개발에서 중요하겠지만, 그 전에 이것이 왜 태동했는지 이해해야 합니다. 그것을 이해하지 않으면, 형태만 똑같을 뿐, 본질적인 가치를 훼손할 수 있습니다. 그래서 이번에 Layered Architecture 가 왜 생겼는지, 본질적인 철학은 무엇인지 이해해보려고 합니다. 그리고 그것을 기반으로 NestJS 를 이해해 볼 것입니다.

 

Layered Architecture 이전

Layered Architecture 가 없는 시절, 또는 지금 express 로 개발을 한다면 무슨 일이 벌어졌을까요?

 

1. 관심사의 분리가 되지 않는다.

 

변경에 용이한 소프트웨어를 만들기 위해서, 관심사의 분리는 필수적입니다. 응집도가 높고, 결합도가 낮은 소프트웨어를 만들어야 하죠. 한 파일에 있던 코드를 여러 개의 클래스로 나누고, 파일과 폴더를 분리하여 관심사 분리를 해내면 됩니다.

그렇게 관심사의 분리를 하면 되겠지만, 또 문제가 있습니다. 상황마다, 사람마다 관심사 분리 기준이 다른다는 것입니다. 아키텍쳐는 일견 예술의 세계이기 때문에, 각자의 방식이 다를 것입니다.

 

2. 관심사를 분리하더라도, 상황마다/사람마다 그 기준이 다르다.

사람마다 생각하는 기준이 다른건 정말 고통스럽습니다. 누군가는 이 로직을 Business Layer 에 넣자고 하고, 누군가는 Data Layer 에 넣자고 합니다. 이걸 하나하나 조율해가는 과정은 쉽지 않습니다. 이 과정에서, 보편적인 상황에서, 가장 적절하게 관심사의 분리가 지켜지는, 하나의 구조를 만듭니다.

 

그 중 하나가 Layered Architecture 입니다. 기준을 통일시켜서, 대규모 소프트웨어를 만들면서 모두가 똑같이 관심사를 분리하며 협업할 수 있도록 합니다.

 

Layered Architecture 란?

하나의 소프트웨어를 여러개의 Layer 로 분리하는 것입니다.

개념적 계층으로 Layer 를 분리하면 다음과 같습니다.

Presentation Layer → Business Layer → Data Layer → Domain

 

 

일반적으로 서버 개발자들에게 익숙한 방식으로, 구현 계층으로 Layer 를 분리하면 다음과 같습니다.

Controller → Service → Repository → Entity

 

여기서 화살표의 방향이 중요합니다. 화살표는 의존성을 말합니다. 의존성 방향은 한쪽으로만 흐르고, 그 방향은 “안정된 의존성 원칙 (Stable Dependencies Principle)” 을 따릅니다. 의존성이 무엇인지, 안정성이란 무엇인지 살펴보겠습니다.

 

의존성이란?

의존성은 사용관계를 의미합니다.

  • A → B
    • = A 는 B 에 의존한다.
    • = A 는 B 를 사용한다.
class Foo {
	method1() {
		const bar = new Bar()
		bar.do1()
	}
}

Foo 는 Bar 에 의존한다고 합니다.

 

안정성이란?

안정성은 변경하기 어려운 정도입니다.

 

로버트 마틴은 불안정성 지표를 제안합니다.

Ce (Efferent Coupling) = Fan-out = 나가는 의존성 (내가 의존하는 것)
Ca (Afferent Coupling) = Fan-in = 들어오는 의존성 (나를 의존하는 것)

I = Ce / (Ca + Ce)
I = 0: 완전히 안정
I = 1: 완전히 불안정

 

 

즉, 내가 의존하는 것(Ce)이 많아질수록 불안정적이고, 나를 의존하는 것(Ca)이 많아질수록 안정적이라는 것입니다. 예시 코드를 보시죠

// 불안정적인 Foo : Foo 가 의존하는 코드는 Bar, Baz 이다

class Foo {
	method1() {
		const bar = new Bar()
		const baz = new Baz()
		
		bar.do()
		baz.do()	
	}
}

다시 안정성의 정의를 보면, 변경하기 어려운 정도 입니다. 내가 아무렇게나 구현을 변경해도, Bar, Baz 에 영향을 주지 않습니다. 그러니까, 불안정적이라고 합니다.

 

안정한 모듈 예시를 보죠. Bar 의 do 메서드가 변경되면, Foo 와 Foo2 는 영향을 받습니다. Bar 의 변경이 다른 2개의 모듈에 영향을 주게 됩니다. Foo 에 의존하는 다른 모듈이 있다면, 거기까지 파급을 미칠 수 있겠죠. 즉, 파급이 많아지니까, 변경하기가 더 어려운 모듈이 됩니다.

// 안정적인 Bar : Foo, Foo2 가 Bar 에 의존한다. 

class Foo {
	method1() {
		const bar = new Bar()
		const baz = new Baz()
		
		bar.do()
		baz.do()	
	}
}

class Foo2 {
	method1() {
		const bar = new Bar()
		
		bar.do()
	}
}

안정된 의존성 원칙 (Stable Dependencies Principle)

그래서 로버트 마틴은 이 원칙을 제안합니다.

  • 불안정적 모듈은 안정적 모듈에 의존해야 한다.
  • 자주 변경되는 것이 변경되지 않는 모듈에 의존해야 합니다.

위 코드 예시로 보면, Bar 가 자주 변경되지 않는, 안정적인 모듈이어야 하고, Foo 가 자주 변경되어도 괜찮은, 불안정적 모듈이어야 합니다.

UI/Presentation 부분은 불안정적이니, 여기에 의존하는 것은 없어야 합니다. UI 가 다른 것을 사용하도록 해야 합니다.

 

반대로 Domain 은 안정적이니, 다른 것들이 Domain 에 의존하게 설계해야 합니다. Domain 이 다른 모듈을 많이 사용하게 하면, 변경에 유연하지 않습니다.

 

다시 Layered Architecture 로 돌아옵시다. 레이어의 분리와 의존성의 방향이 중요하다고 했습니다. 이제부터 각 레이어가 어떻게 분리되는지 살펴볼게요.

 

각 Layer 의 역할

각 Layer 는 어떤 역할을 할까요? Layer 는 변경 사유 별로 분리합니다. SOLID 의 SRP 에 따라, 하나의 변경 사유만 가지도록 하고, 로직을 응집합니다.

Presentation Layer

  • 사용자와의 상호작용
  • 입력, 출력

Business Layer

  • 애플리케이션 규칙/비즈니스 로직 구현
  • 사용자 기준의 작업

Data Layer

  • 데이터 저장, 조회

Domain

  • 핵심 비즈니스 개념
  • 비즈니스 제약사항

 

NestJS 에서의 구현된 Layered Architecture

NestJS 에서 Layered Architecture 측면의 핵심 개념이 4가지가 있습니다.

Module

  • 모듈은 DI 컨테이너의 범위를 명시합니다. NestJS 의 핵심은 DI 인데요. DI 를 추상화하여 쉽게 사용할 수 있도록 하는 것이 바로 모듈입니다.
  • 모듈을 import 하거나 export 하여 외부와 소통할 수 있도록 합니다.

Controller

  • Presentation Layer 를 담당합니다. HTTP 요청을 받아서, 응답하는 계층입니다.

Provider

  • Business Layer 와 Data Layer 를 아울러 Provider 로 묶습니다. 구체적으로 Service, Repository 라는 이름으로 다뤄집니다.
  • Provider 말 그대로 제공자이고, 의존성 주입(DI)의 대상입니다.
  • 그런 의미에서 @Injectable 데코레이터로 Provider 라는 것을 명시합니다.

Entity

  • Domain Layer 를 담당합니다.
  • 데이터 타입/관계를 정의합니다.

마치며

NestJS 를 잘 이해하기 위해서, Layered Architecture 에서 시작하여, 의존성, 안정성까지 살펴보고, NestJS 에 적용된 모습까지 살펴보았습니다. 이전에 Layered Architecture 를 그대로 따라만 하다보니, 의문이 드는 것이 많았습니다. 이 로직은 어디 레이어로 가야하는 걸까? Service 와 Repository 는 왜 Provider 이며, Provider 는 왜 @Injectable 이 붙을까? 이번에 근원을 학습하며 의문을 해소해보았고, NestJS 의 철학과 아키텍쳐에 대한 이해가 훨씬 높아졌습니다. NestJS 를 쓰더라도, 또는 간단하게 express, hono 를 쓰더라도 필요에 따라 더 나은 아키텍쳐를 적용할 수 있겠다는 자신감이 생겼습니다.