콘텐츠로 이동

Clean Architecture

분류: Layer 9 - 아키텍처 & 설계 패턴

Clean Architecture는 비즈니스 규칙을 데이터베이스, 웹 프레임워크, 메시지큐, 외부 API 같은 바깥 기술로부터 독립시키기 위해 소스 코드 의존성이 항상 안쪽 정책을 향하게 만드는 아키텍처 패턴이다. Robert C. Martin이 여러 관심사 분리 아이디어를 정리하며 널리 알렸고, Hexagonal Architecture, Onion Architecture, BCE(Boundary-Control-Entity) 같은 역사적 이름은 뒤의 비교 절에서 다시 본다.

여기서 “독립”은 프레임워크를 쓰지 말자는 뜻이 아니다. NestJS, TypeORM, Redis를 마음껏 쓰되, 도메인 규칙과 유스케이스 코드가 그 이름을 import하지 않게 경계를 세우자는 뜻이다.

선수지식: design-principles.mdx (변경 비용, 결합도/응집도, SOLID, ADR). 자주 함께 읽는 토픽: ddd-basics.mdx.

초기 백엔드 프로젝트는 보통 Controller -> Service -> Repository -> DB 흐름으로 빠르게 만든다. 문제는 시간이 지나면서 이 호출 흐름이 그대로 코드 의존성이 된다는 점이다.

예를 들어 주문 생성 로직이 다음 세 가지를 동시에 알고 있다고 해보자.

// 좋지 않은 신호: 하나의 유스케이스가 바깥 기술 이름을 직접 안다.
import { Request } from "express";
import { Repository } from "typeorm";
import { Redis } from "ioredis";

처음에는 편하다. 하지만 결제 정책이 바뀌거나, TypeORM에서 Prisma로 바꾸거나, HTTP 대신 메시지큐로 같은 주문 생성 로직을 호출하려는 순간 비즈니스 규칙까지 함께 수정해야 한다. 단위 테스트도 DB, cache client, HTTP request mock을 준비해야 해서 느려진다.

Clean Architecture의 목표는 “레이어를 예쁘게 나누기”가 아니다. 목표는 변경 이유가 다른 코드를 서로 덜 알게 해서 변경 파급을 제한하는 것이다. 주문 정책은 주문 정책 때문에 바뀌고, 저장소 구현은 저장소 기술 때문에 바뀌어야 한다.

2.5 선행 기술의 한계 — Clean Architecture 등장 이유

섹션 제목: “2.5 선행 기술의 한계 — Clean Architecture 등장 이유”

전통적인 3-tier 구조는 런타임 흐름을 설명하는 데는 좋다. 요청이 Controller로 들어오고, Service가 처리하고, Repository가 DB를 호출한다. 하지만 런타임 호출 방향과 소스 코드 의존 방향을 그대로 같게 두면 안쪽 정책이 바깥 기술을 알기 쉽다.

전통적인 의존 방향이 굳어진 경우:
Controller
-> OrderService
-> TypeormOrderRepository
-> Express Request
-> Redis client

이 구조에서 “PostgreSQL에서 MySQL로 바꾼다”는 결정은 저장소 구현 교체로 끝나지 않는다. OrderService가 TypeORM의 Repository, entity decorator, query option을 알고 있으면 유스케이스와 테스트까지 흔들린다.

Clean Architecture는 이 문제를 의존성 규칙(Dependency Rule) 하나로 다룬다. 바깥 기술은 안쪽 정책을 알아도 되지만, 안쪽 정책은 바깥 기술의 이름을 알면 안 된다.

의존 방향을 뒤집은 경우:
OrderController -> CreateOrderUseCase -> OrderRepository(포트)
TypeormOrderRepository ----------------------^ 구현

호출은 여전히 Controller에서 시작해 DB까지 내려간다. 그러나 소스 코드 import는 안쪽 정책을 향한다. CreateOrderUseCaseTypeormOrderRepository가 아니라 OrderRepository라는 약속만 안다. TypeORM 구현체는 그 약속을 바깥에서 구현한다.

이 구조는 마이크로서비스를 무리하게 먼저 쪼개기 전에도 유용하다. Martin Fowler는 성공한 마이크로서비스 사례가 대개 커진 모놀리스를 나눈 경우였고, 처음부터 잘게 쪼갠 시스템은 운영과 경계 설정에서 어려움을 겪는 경우가 많다고 설명한다. Clean Architecture는 배포 단위를 쪼개기 전에 모듈형 모놀리스 안에서 경계를 먼저 연습하게 해준다.

Clean Architecture의 철학은 세 문장으로 잡을 수 있다.

  1. 정책은 세부사항보다 안쪽에 둔다. 할인 규칙, 주문 상태 전이, 재고 예약 규칙은 DB 테이블이나 HTTP 형식보다 오래 간다.
  2. 세부사항은 바깥에서 갈아 끼운다. TypeORM, Prisma, S3, Redis, NestJS controller는 선택 가능한 구현이다.
  3. 경계는 import 방향으로 검증한다. “우리는 레이어를 나눴다”가 아니라 “domainapplicationinfrastructure를 import하지 않는다”로 확인한다.

이 관점은 design-principles.mdx의 DIP(Dependency Inversion Principle, 의존성 역전 원칙)를 아키텍처 전체에 확대한 것이다. 고수준 정책이 저수준 구현을 직접 의존하지 않고, 둘 다 추상화에 의존한다.

아래 용어는 뒤에서 계속 나온다. 이름보다 “어느 방향을 알아도 되는가”를 먼저 잡는 것이 중요하다.

Clean Architecture 기본 용어

Entity

도메인 규칙을 가진 핵심 객체다. DB row가 아니라 주문 상태 전이, 금액 검증처럼 업무 규칙을 품은 객체다.

예: `Order.confirm()`은 상품이 없으면 확정할 수 없다는 규칙을 가진다.

Use Case / Interactor

사용자의 한 가지 의도를 수행하는 애플리케이션 흐름이다. 여러 Entity와 Repository를 조합하지만 HTTP나 ORM은 몰라야 한다.

예: `CreateOrderUseCase`, `CancelOrderUseCase`.

Interface Adapter

바깥 형식과 안쪽 형식을 번역하는 코드다. Controller, Presenter, DTO mapper, Repository 구현체가 여기에 놓인다.

예: HTTP DTO를 Use Case command로 바꾸거나 DB schema를 Domain entity로 바꾼다.

Framework / Driver

NestJS, TypeORM, Redis, AWS SDK처럼 실제 실행을 담당하는 바깥 기술이다.

바꿀 가능성이 있거나 테스트에서 대체해야 할 세부사항이다.

Port / Adapter

Port는 안쪽이 필요로 하는 약속이고, Adapter는 그 약속을 바깥 기술로 구현한 코드다.

`OrderRepository`는 port, `TypeormOrderRepository`는 adapter다.

DTO

Data Transfer Object. 레이어 경계를 지나가는 데이터 운반 형식이다. 규칙을 가진 Entity와 다르다.

HTTP 요청 DTO, 응답 DTO, DB schema DTO는 도메인 Entity와 분리한다.

Repository interface

도메인 또는 유스케이스가 저장소에 기대하는 행위를 표현한 인터페이스다.

`save(order)`, `findById(id)`처럼 업무 언어로 쓴다. SQL query option을 노출하지 않는다.

Adapter는 역할 이름이고, 폴더명은 팀 규칙에 따라 달라질 수 있다. 어떤 팀은 Controller와 DTO mapper를 interface-adapters/에 두고, TypeORM 구현체는 infrastructure/에 둔다. 중요한 것은 폴더명이 아니라 바깥 형식을 안쪽 모델로 번역한다는 책임이다.

의존성 규칙은 다음 한 문장이다.

소스 코드 의존성은 반드시 안쪽의 고수준 정책을 향해야 한다.

레이어를 동심원으로 보면 안쪽일수록 오래 가는 정책이고, 바깥일수록 교체 가능한 세부사항이다.

[바깥] Frameworks & Drivers
NestJS, TypeORM, Prisma, Redis, AWS SDK
Interface Adapters
Controller, Repository 구현체, DTO mapper
Application / Use Cases
CreateOrderUseCase, CancelOrderUseCase
[안쪽] Entities / Domain
Order, Money, OrderStatus, 도메인 규칙

실제로 확인해야 할 것은 폴더 이름이 아니라 import 방향이다.

위치import 허용import 금지
domain/같은 domain 내부NestJS, TypeORM, Express, Redis, application, infra
application/domain, application port/interfacecontroller, TypeORM 구현체, HTTP request/response
interface adapterapplication, domain, DTO mapperdomain을 오염시키는 framework type 전달
infrastructure/application port, domain, framework없음. 바깥 기술을 실제로 아는 위치
module/composition모든 구현체 조립비즈니스 규칙 작성

여기서 중요한 오해가 있다. 호출 방향과 의존 방향은 다를 수 있다. 런타임에는 Controller가 Use Case를 호출하고 Use Case가 Repository를 호출한다. 하지만 소스 코드에서는 Use Case가 TypeormOrderRepository를 import하지 않는다. Use Case는 OrderRepository라는 port만 알고, TypeORM adapter가 그 port를 구현한다.

NestJS에 적용할 때는 “NestJS 모듈을 쓰지 말라”가 아니라, NestJS와 TypeORM의 decorator가 안쪽 레이어로 새지 않게 하는 것이 핵심이다.

src/orders/
domain/
order.ts # Entity. NestJS/TypeORM import 없음
order-repository.ts # Repository interface(port)
application/
create-order.use-case.ts # 유스케이스. port에만 의존
interface-adapters/
orders.controller.ts # HTTP DTO -> command 변환
infrastructure/
typeorm-order.repository.ts
order.orm-entity.ts # TypeORM decorator는 여기까지만
orders.module.ts # DI 조립부(composition root)

6.1 Entity: DB row가 아니라 규칙을 가진 객체

섹션 제목: “6.1 Entity: DB row가 아니라 규칙을 가진 객체”

Entity는 식별자와 생명주기를 가진 도메인 객체다. TypeORM의 @Entity()가 붙은 class와 이름이 같아서 헷갈리지만, Clean Architecture에서의 Entity는 DB mapping class가 아니다.

domain/order.ts
export class Order {
private status: "draft" | "confirmed" = "draft";
constructor(
readonly id: string,
private readonly items: string[],
) {}
confirm(): void {
if (this.items.length === 0) {
throw new Error("상품 없는 주문은 확정할 수 없다");
}
this.status = "confirmed";
}
}

이 파일에는 @Entity, @Column, @Injectable, Repository 같은 decorator나 framework type이 없어야 한다. 그래야 주문 확정 규칙을 DB 없이 테스트할 수 있다.

6.2 Repository interface: 안쪽이 필요로 하는 저장소 약속

섹션 제목: “6.2 Repository interface: 안쪽이 필요로 하는 저장소 약속”

Repository interface는 저장 기술을 숨기는 약속이다. 이름은 DB가 아니라 도메인 언어로 작성한다.

domain/order-repository.ts
import { Order } from "./order";
export interface OrderRepository {
findById(id: string): Promise<Order | null>;
save(order: Order): Promise<void>;
}

좋은 port는 findById, save처럼 유스케이스가 필요로 하는 행위를 말한다. 나쁜 port는 createQueryBuilder, where, leftJoinAndSelect처럼 특정 ORM의 어휘를 안쪽으로 끌고 온다.

6.3 Use Case: 사용자 의도를 수행하는 흐름

섹션 제목: “6.3 Use Case: 사용자 의도를 수행하는 흐름”

Use Case는 한 가지 사용자 의도를 실행한다. Entity의 규칙을 호출하고, 필요한 저장소 port를 사용하며, 외부 기술은 모른다.

application/create-order.use-case.ts
import { Order } from "../domain/order";
import { OrderRepository } from "../domain/order-repository";
export class CreateOrderUseCase {
constructor(private readonly orders: OrderRepository) {}
async execute(input: { orderId: string; items: string[] }): Promise<void> {
const order = new Order(input.orderId, input.items);
order.confirm();
await this.orders.save(order);
}
}

이 예제에는 @Injectable()도 붙이지 않았다. 실무에서는 편의상 application class에 Nest decorator를 허용하는 팀도 있지만, strict한 Clean Architecture를 연습할 때는 NestJS 조립을 바깥 module에 몰아두는 편이 의존 방향을 더 선명하게 만든다.

6.4 Adapter: 바깥 형식을 안쪽 형식으로 번역

섹션 제목: “6.4 Adapter: 바깥 형식을 안쪽 형식으로 번역”

Controller는 HTTP 요청을 받는 interface adapter다. DTO(Data Transfer Object)는 요청/응답 데이터를 운반하는 형식이다. DTO는 직렬화와 validation에는 좋지만, 주문 확정 규칙을 넣는 위치가 아니다.

interface-adapters/orders.controller.ts
@Controller("/orders")
export class OrdersController {
constructor(private readonly createOrder: CreateOrderUseCase) {}
@Post()
async create(@Body() dto: CreateOrderRequestDto) {
await this.createOrder.execute({
orderId: dto.orderId,
items: dto.items,
});
return { ok: true };
}
}

TypeORM 구현체도 adapter다. DB schema와 domain entity 사이를 변환한다.

infrastructure/typeorm-order.repository.ts
export class TypeormOrderRepository implements OrderRepository {
constructor(private readonly repo: Repository<OrderOrmEntity>) {}
async findById(id: string): Promise<Order | null> {
const row = await this.repo.findOneBy({ id });
return row ? row.toDomain() : null;
}
async save(order: Order): Promise<void> {
await this.repo.save(OrderOrmEntity.fromDomain(order));
}
}

OrderOrmEntity에는 TypeORM decorator가 있어도 된다. 다만 그 class가 domain/이나 application/으로 들어오면 경계가 깨진다.

6.5 DI 조립부: 구현체 선택은 가장 바깥에서

섹션 제목: “6.5 DI 조립부: 구현체 선택은 가장 바깥에서”

TypeScript interface는 런타임에 사라진다. 그래서 NestJS DI 컨테이너는 OrderRepository interface만 보고 구현체를 주입할 수 없다. 해결은 symbol이나 문자열 token을 조립부에서 사용하는 것이다. OrderRepository라는 port 자체는 안쪽 정책의 약속이고, token은 NestJS가 런타임에 구현체를 찾기 위한 조립용 이름이다.

application/order-repository.token.ts
export const ORDER_REPOSITORY = Symbol("ORDER_REPOSITORY");
orders.module.ts
@Module({
imports: [TypeOrmModule.forFeature([OrderOrmEntity])],
controllers: [OrdersController],
providers: [
TypeormOrderRepository,
{
provide: ORDER_REPOSITORY,
useExisting: TypeormOrderRepository,
},
{
provide: CreateOrderUseCase,
useFactory: (orders: OrderRepository) => new CreateOrderUseCase(orders),
inject: [ORDER_REPOSITORY],
},
],
})
export class OrdersModule {}

이 구조에서 Prisma로 바꾸려면 PrismaOrderRepository를 만들고 ORDER_REPOSITORY 매핑만 바꾼다. Order, OrderRepository, CreateOrderUseCase는 그대로 둔다.

7. 손으로 확인하는 import 방향 테스트

섹션 제목: “7. 손으로 확인하는 import 방향 테스트”

Clean Architecture는 말보다 검색으로 확인하는 편이 낫다. 다음 검사는 완벽한 정적 분석은 아니지만, 경계가 새는지 빠르게 보여준다.

Terminal window
rg 'from "(@nestjs|typeorm|express|ioredis)|from ".*/(infrastructure|interface-adapters|presentation)' src/orders/domain src/orders/application

기대 결과:

  • domain/에서는 결과가 0건이어야 한다.
  • application/에서는 domain/과 application port import만 보여야 한다.
  • TypeORM decorator, NestJS controller/request type, Redis client가 잡히면 안쪽 정책이 바깥 기술을 알게 된 것이다.

DI token 누락도 자주 발생한다.

Terminal window
rg 'ORDER_REPOSITORY|OrderRepository' src/orders

확인할 점은 세 가지다.

  1. Use Case가 OrderRepository interface에 의존한다.
  2. TypeORM adapter가 implements OrderRepository를 선언한다.
  3. orders.module.ts가 runtime token(ORDER_REPOSITORY)을 실제 구현체에 매핑한다.

테스트에서도 같은 원리가 드러난다. CreateOrderUseCase 테스트가 TypeORM connection 없이 fake repository만으로 돌면 경계가 잘 선 것이다. 반대로 유스케이스 테스트에 TestingModule, TypeOrmModule, DB container가 필요하면 application layer가 framework에 묶였을 가능성이 높다.

let savedOrder: Order | undefined;
const fakeOrders: OrderRepository = {
async findById() {
return null;
},
async save(order) {
savedOrder = order;
},
};
await new CreateOrderUseCase(fakeOrders).execute({
orderId: "order-1",
items: ["book"],
});

이 테스트가 NestJS module 없이도 돌아간다면 Use Case가 framework가 아니라 port에 의존하고 있다는 뜻이다.

8. Worked Example: PostgreSQL에서 MySQL로 바꾸는 경우

섹션 제목: “8. Worked Example: PostgreSQL에서 MySQL로 바꾸는 경우”

같은 변경을 두 구조에서 비교해보자.

의존성 규칙 위반:
CreateOrderUseCase
-> TypeormOrderRepository
-> OrderOrmEntity
-> PostgreSQL column option
PostgreSQL -> MySQL 전환:
UseCase 수정
도메인 테스트 수정
Controller fixture 수정
Repository query option 수정
의존성 규칙 준수:
CreateOrderUseCase
-> OrderRepository(port)
TypeormOrderRepository
-> OrderRepository 구현
-> OrderOrmEntity 사용
PostgreSQL -> MySQL 전환:
MysqlOrderRepository 추가
module provider 매핑 변경
adapter 통합 테스트 수정
UseCase와 Domain 테스트는 유지

수치 감각으로 보면 차이가 더 분명하다. 주문 정책 테스트 40개가 모두 DB container를 띄우면 테스트 한 번에 수십 초가 걸릴 수 있다. 반대로 domain/use case 테스트 40개가 fake repository로 1초 안에 돌고, DB adapter 통합 테스트 5개만 따로 돌면 변경 피드백이 훨씬 빠르다. Clean Architecture의 가치는 폴더 구조가 아니라 이런 피드백 경로 단축에서 나온다.

Clean Architecture는 강력하지만, 모든 프로젝트의 기본값은 아니다. 경계가 실제 변경 비용을 줄일 때만 투자 가치가 있다.

과소 설계는 도메인 규칙이 바깥 기술을 직접 아는 상태다.

// 실패 신호: 정책 코드가 ORM row를 직접 조작한다.
if (orderEntity.status === "PENDING") {
await this.repo.update(orderEntity.id, { status: "CONFIRMED" });
}

이 코드는 간단해 보이지만 PENDING -> CONFIRMED 전이가 왜 가능한지 Entity가 알지 못한다. 나중에 “상품이 없으면 확정 불가”, “결제 승인 전 확정 불가” 같은 규칙이 생기면 여러 Service와 query update에 흩어진다.

과대 설계는 경계가 필요보다 많아져 기능 추가 속도를 늦추는 상태다.

작은 CRUD/admin 앱에서 흔한 실패:
CreateNoticeController
-> CreateNoticeUseCase
-> NoticeCommand
-> NoticeFactory
-> NoticeRepository
-> NoticeMapper
-> NoticeOrmEntity
실제 규칙:
제목 필수, 본문 저장, 목록 조회

팀이 1~3명이고, 화면 대부분이 CRUD이며, 읽기 요청이 95% 이상이고, 정책 변경이 월 1회 미만이라면 완전한 Clean Architecture는 보일러플레이트가 될 수 있다. 이때는 Controller -> Service -> Repository의 경량 구조로 시작하고, 변경 압력이 생길 때 경계를 세우는 편이 낫다.

다음 신호가 2개 이상이면 Clean Architecture 경계를 도입할 가치가 커진다.

  • 최근 4주 동안 같은 유스케이스의 비즈니스 규칙이 3회 이상 바뀌었다.
  • DB 없이 1초 안에 도는 domain/use case 단위 테스트가 필요하다.
  • TypeORM, Prisma, 외부 API, 메시지큐 중 하나를 교체하거나 병행할 가능성이 분기 계획에 있다.
  • HTTP 외에 batch, message consumer, gRPC 등 다른 진입점에서 같은 유스케이스를 호출해야 한다.
  • Service가 DTO, ORM entity, cache client, domain object를 한 메서드에서 동시에 다룬다.

반대로 다음 조건에서는 경량 구조가 더 현실적이다.

  • 내부 관리자 CRUD가 대부분이고 비즈니스 규칙이 얇다.
  • 팀이 작고 도메인 언어보다 화면 요구사항 변화가 더 빠르다.
  • 저장소 기술 교체 가능성이 낮고 테스트도 adapter 수준이면 충분하다.
  • Use Case가 repository.save() 한 줄만 감싸는 pass-through가 반복된다.
AB차이점
Clean ArchitectureHexagonal (Ports & Adapters)Hexagonal은 외부 통신을 Port/Adapter로 추상화하는 표현을 강조한다. Clean Architecture는 계층 의존 규칙을 강조한다. 본질은 DIP다.
Clean ArchitectureOnion ArchitectureOnion이 먼저 제안한 도메인 중심 동심원 모델이고, Clean Architecture는 Use Case와 adapter 경계를 더 명시적으로 설명한다.
Clean Architecture전통 3-tier3-tier는 주로 런타임 데이터 흐름을 설명한다. Clean Architecture는 소스 코드 의존 방향을 고수준 정책으로 돌린다.
Use CaseServiceUse Case는 단일 사용자 의도다. Service는 여러 유스케이스를 담는 기술적 묶음이 되기 쉽다.
RepositoryDAORepository는 도메인 객체 컬렉션처럼 보이는 추상화다. DAO는 테이블 단위 CRUD에 더 가깝다.
DTOEntityDTO는 데이터 운반 형식이다. Entity는 상태와 규칙을 가진 도메인 객체다.
DIDIPDI는 객체를 주입하는 기술이고, DIP는 고수준 정책이 저수준 구현에 직접 의존하지 않는 설계 원칙이다.

긴 운영 런북보다 먼저 봐야 할 것은 실패 신호다.

실패 신호의미먼저 볼 곳
domain test에 DB/Nest TestingModule이 필요안쪽 규칙이 framework나 persistence에 묶였다domain/application import, constructor type
Use Case가 Repository<OrderOrmEntity>를 받음ORM schema가 application으로 샜다repository port의 반환 타입과 mapper 위치
Controller DTO가 Entity constructor에 그대로 들어감transport 형식과 domain 모델이 섞였다DTO -> command -> Entity 변환 위치
모든 Use Case가 한 줄 pass-through경계가 실제 규칙을 담지 못한다경량 구조로 축소하거나 규칙이 생길 때까지 보류
Repository interface에 query builder 옵션이 있음port가 특정 DB/ORM을 숨기지 못한다domain 언어 기반 메서드로 재설계
Prisma/TypeORM 교체가 Service 수정으로 번짐의존 방향이 구현체로 향한다module provider 매핑과 adapter 구현 분리

작게 진단하려면 다음 순서로 본다.

  1. domain/application/에서 바깥 기술 import가 잡히는지 검색한다.
  2. Use Case 테스트가 fake repository로 실행되는지 확인한다.
  3. Repository interface가 도메인 언어인지, ORM 언어인지 읽어본다.
  4. module/composition root에서 구현체 선택이 끝나는지 확인한다.

Clean Architecture를 적용할지 결정할 때는 다음 질문을 사용한다.

적용 판단 기준

적용 가치가 큰 경우

도메인 규칙이 자주 바뀌고, 여러 진입점이 같은 유스케이스를 호출하며, DB/외부 API 교체 가능성이 있다.

테스트를 빠르게 유지하면서 business rule을 framework 밖에서 검증해야 한다.

경량 구조가 나은 경우

작은 CRUD/admin 앱, 팀 1~3명, 읽기 중심, 규칙 변경이 적은 초기 MVP.

Use Case가 대부분 repository pass-through라면 경계 비용이 이익보다 크다.

점진 도입

처음부터 모든 폴더를 만들지 않고, 변경 압력이 큰 유스케이스부터 port와 adapter를 분리한다.

결제, 주문, 재고처럼 규칙과 외부 시스템 연결이 동시에 복잡해지는 구간.
  • Robert C. Martin, The Clean Architecture — 4계층, 의존성 규칙, DB/UI/framework 독립성.
  • NestJS, Custom providers — 비클래스 provider token, useClass/custom provider 매핑.
  • Martin Fowler, Monolith First — 마이크로서비스 경계를 처음부터 안정적으로 잡기 어렵다는 사례 기반 판단.
  • Martin Fowler, Microservice Prerequisites — 운영 준비 기준과 배포 자동화 전제.

Clean Architecture 이해 점검

  • Entity가 DB row가 아니라 도메인 규칙을 가진 객체라는 점을 설명할 수 있는가?
  • Use Case가 TypeORM 구현체가 아니라 Repository interface(port)에 의존해야 하는 이유를 설명할 수 있는가?
  • Controller DTO, Domain Entity, ORM Entity를 서로 다른 목적으로 구분할 수 있는가?
  • NestJS/TypeORM decorator가 domain/application으로 새면 어떤 테스트 문제가 생기는지 설명할 수 있는가?
  • PostgreSQL -> MySQL 교체 시 어떤 파일이 바뀌고 어떤 파일은 유지되어야 하는지 그릴 수 있는가?
  • 작은 CRUD/admin 앱에서 Clean Architecture가 과대 설계가 되는 조건을 말할 수 있는가?
  1. Clean Architecture의 핵심은 폴더명이 아니라 의존성 규칙이다. 소스 코드 의존성은 안쪽 정책을 향해야 한다.
  2. Entity는 도메인 규칙을 가진 객체이고, Use Case는 사용자 의도를 수행하며, Repository interface는 저장소 port다.
  3. NestJS와 TypeORM은 바깥 adapter와 module 조립부에서 사용한다. decorator와 ORM schema가 domain/application으로 새면 경계가 깨진다.
  4. 좋은 경계는 검색과 테스트로 확인된다. 안쪽 레이어의 framework import가 0건이고, Use Case 테스트가 fake repository로 빠르게 돌아야 한다.
  5. 작은 CRUD/admin 앱, 초기 MVP, pass-through Use Case가 많은 팀에는 경량 구조가 더 낫다. 변경 압력이 생기는 유스케이스부터 점진 도입한다.