[Nest] [클린코드 5편] Interceptor와 Filter로 반환 값 통일하기(응답 속도 측정까지)
[Nest] [클린코드 5편] Interceptor와 Filter로 반환 값 통일하기(응답 속도 측정까지)
[Nest] [클린코드 4편] Service와 Repository 분리하고 의존성 주입하기(DI) [Nest] [클린코드 4편] Service와 Repository 분리하고 의존성 주입하기(DI)[Nest] [클린코드 3편] DTO와 전역 pipe로 요청 데이터 깔끔하게
blog.juyear.dev
이전 글 읽으러 가기!
👋 소개
안녕하세요! 대학생 개발자 주이어입니다.
오늘은 Nest 클린코드 6편을 적게 되었는데요.
5편까지는 기본적인 기능들 위주로 정리를 했다면, 6편부터는 조금 고급 기능들을 정리해보려고 합니다.
그 첫번째가 바로 "Application Layer(Use Cases)로 Service 정리하기" 입니다.
관련 내용으로는 Service의 책임 분리와 비즈니스 흐름 관리가 있습니다.
그럼 반로 본론으로 들어가도록 하겠습니다.
❓ Application Layer란?
Application Layer는 비즈니스 규칙을 직접 구현하지 않고, 도메인 레이어의 Service들을 조합해 하나의 비즈니스 흐름을 만드는 레이어 입니다.
예를 들어, 특정 상품을 결제하는 요청이 들어왔다고 생각해봅시다.
만약 Application Layer를 사용하지 않고, 단순히 하나의 Service에서 이 요청을 처리한다면,
- 주문 생성
- 제품 조회
- 재고 차감
- 결제 요청
- 주문 상태 변경
등등 하나의 Service에서 담당해야하는 역할의 범위가 너무 넓어지고, 여러 도메인의 Service들을 직접 주입받아 사용하게 됨으로써 역할의 경계도 모호해집니다.
즉, 결제 요청을 처리하는 Order Service가 Product Service, Stock Service, Payment Service 등 다양한 Service에 의존하게 되는 것입니다.
이렇게 될 경우, Order Service를 모듈 단위로 테스트하기 어려워지고, 유지보수에도 문제가 발생할 수 있으며, 비즈니스 흐름을 파악하기도 어려워집니다.
이러한 문제를 해결하기 위해서 사용되는 것이 Application Layer입니다.
Application Layer는 위와 같이 복잡한 처리 과정을 하나의 비즈니스 흐름으로 만들어 관리할 수 있도록 도와줍니다.
기존에는 Order Service안에서 다른 Service를 직접 가져와 사용했다면,
이제는 Order Service는 주문 생성과 관련된 역할만 담당하고,
Application Layer에서 여러 Service들을 가져와 흐름을 만들어주는 것이죠.
이러한 Application Layer는 중요한 특징이 한 가지 있습니다.
어찌 보면 당연한 얘기이지만, 비즈니스 흐름만 당담해야한다는 것입니다.
즉, Application Layer에서는 비즈니스 규칙을 정의하지 않습니다. 흐름을 판단하고 제어하는 역할만 합니다.
+) Orchestrator는 Application Layer의 하위 개념으로, Service들을 가져와 하나의 흐름을 만드는 패턴을 부르는 명칭입니다.
이 글에서는 Orchestrator와 더 가까운 글이라고 보시면 될 것 같습니다.
(Use Cases, Application Service, Orchestrator 모두 혼용되어 비슷한 의미로 사용된다고 들었습니다.)
⚙️ Service 준비하기
이해하기 쉽도록 위에서 설명했던 결제 요청 예시를 이용해서 Service들을 구현해보도록 하겠습니다.
Service 각각의 코드는 그렇게 중요한 부분이 아니기 때문에 코드랑 로직만 간단하게 설명하도록 하겠습니다.
Product Service
import { Injectable } from '@nestjs/common';
@Injectable()
export class ProductService {
private products = [
{ id: 1, name: '노트북', price: 1200000 },
{ id: 2, name: '모니터', price: 450000 },
];
getProduct(id: number) {
const product = this.products.find((p) => p.id === id);
if (!product) throw new Error('Invalid Product ID');
return product;
}
}
Product Service에는 getProduct 함수가 있으며, id로 제품을 찾아 반환하는 로직을 담당합니다.
(비즈니스 흐름을 테스트하기 위한 코드이기 때문에 데이터의 경우 Mock 데이터를 생성해서 사용하였고, 따로 Repository로 분리하지 않았습니다.)
Stock Service
import { Injectable } from '@nestjs/common';
@Injectable()
export class StockService {
private stocks = [
{ id: 1, quantity: 300 },
{ id: 2, quantity: 2 },
];
increaseStock(id: number, add: number) {
const stock = this.stocks.find((s) => s.id === id);
if (!stock) {
throw new Error('Invalid stock ID');
}
stock.quantity += add;
}
decreaseStock(id: number, sub: number) {
const stock = this.stocks.find((s) => s.id === id);
if (!stock) {
throw new Error('Invalid stock ID');
} else if (stock.quantity < sub) {
throw new Error('Invalid stock');
}
stock.quantity -= sub;
}
}
Stock Service에는 increaseStock과 decreaseStock 함수가 존재하며, 각각 재고를 증가시키거나 감소시키는 로직을 담당합니다.
Payment Service
import { Injectable } from '@nestjs/common';
@Injectable()
export class PaymentService {
private cards = [{ cardNumber: '12345678', cvc: '222', password: '3333' }];
verifyCard(cardNumber: string, cvc: string, password: string) {
const card = this.cards.find((c) => c.cardNumber === cardNumber);
if (!card || card.cvc !== cvc || card.password !== password)
throw new Error('Invalid Card');
return true;
}
}
Payment Service에는 verifyCard 함수가 존재하며, 카드를 검증하는 로직을 담당합니다.
실제 결제 요청에서 직접 카드 번호를 받아와 검증하는 경우는 없지만(일반적으로 결제 API 사용), 테스트를 위해 추가하였습니다.
Order Service
import { Injectable } from '@nestjs/common';
export interface Order {
userID: number;
id: number;
productID: number;
quantity: number;
price: number;
status: string;
}
@Injectable()
export class OrderService {
private orders: Order[] = [];
createOrder(
userID: number,
productID: number,
quantity: number,
price: number,
status: string,
) {
this.orders.push({
userID,
id: this.orders.length + 1,
productID,
quantity,
price,
status,
});
return this.orders[this.orders.length - 1];
}
}
마지막으로 Order Service에는 createOrder 함수가 존재하며, 주문서를 생성하는 역할을 합니다.
이렇게 Service들이 각각의 역할만 담당하도록 하여 구현을 마쳤습니다.
이제 비즈니스 흐름을 구현할 예정이지만, 만약 없었다면, Order Service의 코드는 매우 길고 복잡해졌을 것입니다.
⚙️ 비즈니스 흐름 구현하기
이제 준비가 다 끝났으니 결제 요청을 처리하는 비즈니스 흐름을 구현해보도록 하겠습니다.

결제 요청의 경우 Order 도메인과 관련된 작업이기 때문에, 위와 같은 구조를 선택하였습니다.
이제 본격적으로 코드를 한번 보도록 하겠습니다.
Order Orchestrator(Use Case)
import { Injectable } from '@nestjs/common';
import { PaymentService } from 'src/payment/payment.service';
import { ProductService } from 'src/product/product.service';
import { StockService } from 'src/stock/stock.service';
import { OrderService } from '../order.service';
@Injectable()
export class OrderOrchestrator {
constructor(
private readonly productService: ProductService,
private readonly stockService: StockService,
private readonly paymentService: PaymentService,
private readonly orderService: OrderService,
) {}
placeOrder(
userID: number,
productID: number,
quantity: number,
cardInfo: { cardNumber: string; cvc: string; password: string },
) {
const product = this.productService.getProduct(productID);
const totalPrice = product.price * quantity;
this.stockService.decreaseStock(productID, quantity);
try {
this.paymentService.verifyCard(
cardInfo.cardNumber,
cardInfo.cvc,
cardInfo.password,
);
return this.orderService.createOrder(
userID,
productID,
quantity,
totalPrice,
'COMPLETED',
);
} catch (err) {
this.stockService.increaseStock(productID, quantity);
throw new Error(`Payment Error - ${err}`);
}
}
}
위 코드가 바로 비즈니스 흐름을 관리하는 OrderOrchestrator(Use Case) 코드입니다.
비즈니스 흐름을 보여주는 placeOrder 함수 부분만 확인을 해보면,
- Product Service : 제품을 가져옴
- Stock Service : 재고를 차감함
- Payment Service : 카드를 검증함
- Order Service : 주문서를 생성함
- +) Stock Service : 오류가 발생할 경우, 차감한 재고를 다시 추가함 (보상 트랜잭션)
위와 같은 흐름으로 각각의 Service가 어떤 순서로 어떤 역할을 수행하는지 한눈에 확인할 수 있습니다.
그 만큼 비즈니스 흐름을 쉽게 파악할 수 있다는 것이죠.
또한 Service가 서로 다른 Service와 경계를 침범하지 않고, 직접 주입받지도 않아 역할 분리와 책임 경계가 명확해졌습니다.
+) 현재 예시에서는 단순화를 위해 Orchestrator에서 총 가격(totalprice)을 계산하고 있지만, 가격 정책이 존재하는 서비스거나 상황에 따라 도메인 서비스로 분리하는 것이 좋습니다.
Order Module
import { Module } from '@nestjs/common';
import { OrderService } from './order.service';
import { OrderOrchestrator } from './orchestrator/order-orchestrator';
import { ProductModule } from 'src/product/product.module';
import { StockModule } from 'src/stock/stock.module';
import { PaymentModule } from 'src/payment/payment.module';
import { OrderController } from './order.controller';
import { AuthModule } from 'src/auth/auth.module';
@Module({
imports: [AuthModule, ProductModule, StockModule, PaymentModule],
controllers: [OrderController],
providers: [OrderService, OrderOrchestrator],
})
export class OrderModule {}
비즈니스 흐름을 만들기 위해서는 당연히 여러 Module에 의존을 해야하고, 위 코드는 Order도메인의 module 설정 입니다.
특별한 점은 없지만, OrderOrchestrator도 providers로 등록해주셔야 한다는 정도만 보시면 될 것 같습니다.
[Nest] [클린코드 4편] Service와 Repository 분리하고 의존성 주입하기(DI)
[Nest] [클린코드 4편] Service와 Repository 분리하고 의존성 주입하기(DI)
[Nest] [클린코드 3편] DTO와 전역 pipe로 요청 데이터 깔끔하게 검증하기 [Nest] [클린코드 3편] DTO와 전역 pipe로 요청 데이터 깔끔하게 검증하기[Nest] [클린코드 2편] Guard와 Decorator로 요청 처리 깔끔하
blog.juyear.dev
module 설정 및 의존성 주입과 관련된 부분은 윗 글을 참고해주시길 바랍니다.
Order Controller
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
import { OrderOrchestrator } from './orchestrator/order-orchestrator';
@Controller('order')
export class OrderController {
constructor(
private readonly orderOrchestrator: OrderOrchestrator,
) {}
@UseGuards(JwtAuthGuard)
@Post('/buy')
buy(
@Body()
body: {
userID: number;
productID: number;
quantity: number;
cardInfo: { cardNumber: string; cvc: string; password: string };
},
) {
return this.orderOrchestrator.placeOrder(
body.userID,
body.productID,
body.quantity,
body.cardInfo,
);
}
}
기존에는 Controller에서 Service로 로직 처리 요청을 보냈지만,
이제는 Orchestrator로 보내주시면 됩니다.
Orchestrator에서 Service들을 가지고 로직을 처리해줄거니까 말이죠.
(Post /buy에서는 userID, productID, quantity, cardInfo를 매개변수로 받습니다.)
✅ 비즈니스 흐름 테스트하기
이제 비즈니스 흐름까지 구현을 했으니 결제 요청을 보냈을 때, 주문서가 정상적으로 생성되어 반환이 되는지 확인해보도록 하겠습니다.

테스트는 Postman으로 진행하였고,
요청 데이터로는 userID, productID(1: 노트북), quantity(구매수량),cardInfo(카드정보)를 넣어주었습니다.
결과 부분을 보면, 가격과 주문 상태가 포함된 주문서가 잘 반환되는 것을 확인할 수 있습니다.

수량이 부족해질 때 까지 지속적으로 결제를 하면, 결국 Error가 반환되는 것을 확인할 수 있습니다.
이외에도 잘못된 제품 ID, 잘못된 카드 정보, 존재하지 않는 유저 ID 등 여러 테스트에도 의도한대로 잘 작동하였습니다.
🛠️ 폴더 구조

😊 마무리
이렇게 오늘 Application Layer에 대해서 정리하고 실습까지 진행해보았습니다.
오늘 정리한 부분은 어떻게 보면 6편까지 진행하면서 실무와 가장 큰 관련이 있는 부분이었지 않나라는 생각이 듭니다.
Application Layer는 Service가 비대해지는 것을 막아주고, 기능 별 테스트, 유지보수 등에 꼭 필요한 개념 중 하나이니,
여러분들도 한번씩 실습 해보시길 바라며, 개념은 꼭 기억해두시길 바랍니다.
물론 Application Layer가 무조건 사용해야하는, 무조건 좋은 을 의미하는 것은 아닙니다.
상황에 따라, 필요에 따라 사용하는 개념이며, 단순 CRUD 중심 도메인에서는 Service를 바로 호출하는 것이 일반적입니다.
다음에는 조금 더 복잡하고 어려운 개념인 Transaction을 Nest에서 구현하고 사용하는 방법에 대해서 정리해볼 예정입니다.
그럼 지금까지 읽어주셔서 감사드리며, 다음에는 더 유익한 글로 찾아오도록 하겠습니다.
KYT CODING COMMUNITY Discord 서버에 가입하세요!
Discord에서 KYT CODING COMMUNITY 커뮤니티를 확인하세요. 27명과 어울리며 무료 음성 및 텍스트 채팅을 즐기세요.
discord.com
KYT CODING COMMUNITY 가입하기!
'[Nest]' 카테고리의 다른 글
| [Nest] [클린코드 5편] Interceptor와 Filter로 반환 값 통일하기(응답 속도 측정까지) (1) | 2026.01.02 |
|---|---|
| [Nest] [클린코드 4편] Service와 Repository 분리하고 의존성 주입하기(DI) (1) | 2025.12.31 |
| [Nest] [클린코드 3편] DTO와 전역 pipe로 요청 데이터 깔끔하게 검증하기 (1) | 2025.11.17 |
| [Nest] [클린코드 2편] Guard와 Decorator로 요청 처리 깔끔하게 구현하기 (1) | 2025.11.07 |
| [Nest] [클린코드 1편] JWT 발급 및 인증 구조 깔끔하게 구현하기 (Postman API 테스트) (1) | 2025.11.03 |