그냥 요청 받은 그 파일에서 모든 걸 수정할 수도 있지만
그러면 나중에 코드 관리하기 힘들어서 쪼개서 만든다

Controller => Service => Repository 꼴
// src/database.provider.ts
import { ConfigService } from '@nestjs/config';
import * as mysql from 'mysql2/promise';
// 의존성 주입(DI)에 사용할 우리만의 고유 이름표(토큰)
export const DATABASE_CONNECTION = 'DATABASE_CONNECTION';
export const databaseProviders = [
{
provide: DATABASE_CONNECTION,
inject: [ConfigService],
useFactory: (configService: ConfigService) => {
return mysql.createPool({
host: configService.get<string>('DB_HOST', 'localhost'),
port: configService.get<number>('DB_PORT', 3306),
user: configService.get<string>('DB_USER', 'root'),
password: configService.get<string>('DB_PASSWORD', ''),
database: configService.get<string>('DB_NAME', 'study'),
waitForConnections: true, // 선로가 꽉 차면 에러 대신 대기
connectionLimit: 10, // 미리 뚫어둘 핫라인(커넥션) 개수 10개
queueLimit: 0,
});
},
},
];
configService.get('타겟', '기본 값')
.env에서 설정이 가능
app.module.ts
ㄴcontrollers로 BookController, RentalsController
ㄴproviders로 databaseProviders, BookService, BookRepository, RentalsService, RentalsRepository
ㄴㄴ BookController → BookRepository → BookService
ㄴㄴ RentalsController → RentalsRepository → RentalsService
꼴로 만들었다.
app.module.ts
import { Module } from '@nestjs/common';
import { createObserveModule } from '@nestjs/observe';
import { AppController } from './app.controller.js';
import { AppService } from './app.service.js';
import { ConfigModule } from '@nestjs/config'; //.env 알아먹게
import { databaseProviders } from './database.provider.js';
import { BookController } from './book.controller.js';
import { BookService } from './book.service.js';
import { BookRepository } from './book.repository.js';
import { RentalsController } from './rentals.controller.js';
import { RentalsService } from './rentals.service.js';
import { RentalsRepository } from './rentals.repository.js';
export const { ObserveModule, ObserveInstrument } = createObserveModule();
@Module({
imports: [
// Distributed tracing, auto-correlated logs, request/job metrics, error
// telemetry, alarms, and more — out of the box. Sign up at https://observe.nestjs.com
ObserveModule.forRoot({
appKey: 'YOUR_APP_KEY',
appSecret: 'YOUR_APP_SECRET',
serviceId: 'study',
}),
ConfigModule.forRoot({
isGlobal: true,
})
],
controllers: [
AppController,
BookController,
RentalsController,
],
providers: [
...databaseProviders, // db 커넥션 풀
AppService,
BookService,
BookRepository,
RentalsService,
RentalsRepository,
],
exports: [
...databaseProviders
]
})
export class AppModule {}
database.provider.ts
// src/database.provider.ts
import { ConfigService } from '@nestjs/config';
import * as mysql from 'mysql2/promise';
// 의존성 주입(DI)에 사용할 우리만의 고유 이름표(토큰)
export const DATABASE_CONNECTION = 'DATABASE_CONNECTION';
export const databaseProviders = [
{
provide: DATABASE_CONNECTION,
inject: [ConfigService],
useFactory: (configService: ConfigService) => {
return mysql.createPool({
host: configService.get<string>('DB_HOST', ''),
port: configService.get<number>('DB_PORT', 3360),
user: configService.get<string>('DB_USER', ''),
password: configService.get<string>('DB_PASSWORD', ''),
database: configService.get<string>('DB_NAME', ''), //뒤에 건 기본 값들
waitForConnections: true, // 선로가 꽉 차면 에러 대신 대기
connectionLimit: 10, // 미리 뚫어둘 핫라인(커넥션) 개수 10개
queueLimit: 0,
});
},
},
];
book.controller.ts
// src/book.controller.ts
import { Controller, Get, Param, Patch } from '@nestjs/common';
import { BookService } from './book.service.js';
import { Body, Post } from '@nestjs/common';
@Controller('books') // 이 컨트롤러로 들어오는 기본 주소: /books
export class BookController {
// 주방장(BookService)을 주입받습니다.
constructor(private readonly bookService: BookService) {}
// HTTP GET 방식으로 /books 요청이 들어왔을 때 실행되는 핸들러
@Get()
async getBooks(): Promise<any> {
return await this.bookService.getAllBooks();
}
@Get('category/:categoryID')
async findBooksByCategoryID(@Param('categoryID') categoryID: number): Promise<any> {
return await this.bookService.findBooksByCategoryID(categoryID);
}
@Post()
async createBook(@Body() body: Record<string, any>): Promise<string> {
return await this.bookService.createBook(body);
}
}
book.repository.ts
// src/book.repository.ts
import { Injectable, Inject } from '@nestjs/common';
import type { Pool } from 'mysql2/promise';
import { DATABASE_CONNECTION } from './database.provider.js';
//ex) 레시피가 있음
@Injectable() // NestJS 컨테이너에 "나 주입 가능한 부품이야!"라고 등록
export class BookRepository {
constructor(
// 2단계에서 우리가 등록해둔 DB 커넥션 풀(DATABASE_CONNECTION)을 가져옵니다.
@Inject(DATABASE_CONNECTION) private readonly pool: Pool, //안전하게 readonly
/* 재료의 위치를 가리키는 스티커 */
) {}
async findAll(): Promise<any> {
const sql = 'SELECT * FROM book';
// pool.query()는 [조회된 행들, 메타데이터 필드들] 형태의 배열을 돌려줍니다.
// 우리는 실제 행 데이터만 필요하므로 구조 분해 할당으로 [rows]만 쏙 꺼냅니다.
const [rows] = await this.pool.query(sql);
return rows;
}
async findBooksByCategoryID(categoryID: number): Promise<any> {
const sql = 'SELECT * FROM book WHERE category_id = ?;';
const [rows] = await this.pool.execute(sql, [
categoryID,
]);
return rows;
}
async create(body: Record<string, any>): Promise<any> {
const sql = 'INSERT INTO book (category_id, title, description, is_available) VALUES (?, ?, ?, true)';
// 두 번째 인자로 넘긴 배열이 ? 자리에 순서대로 안전하게 바인딩됩니다.
const [result] = await this.pool.execute(sql, [
body.categoryId,
body.title,
body.description,
]);
return result;
}
}
/*
새 Promise 생성
- const p = new Promise((resolve, reject) => { ... resolve(value) 또는 reject(error) ... })
then으로 결과 받기
- p.then(value => { ... return값 } ).catch(err => { ... })
async 함수는 항상 Promise를 반환합니다.
함수 내부에서 await로 Promise가 해결될 때까지 기다립니다.
- async function getData() { const data = await fetchSomething(); return data; }
- getData().then(d => console.log(d)).catch(e => console.error(e));
*/
/*
constructor
class Person {
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
처럼 new Person('길동', 20) 처럼
*/
book.service.ts
// src/book.service.ts
import { Injectable } from '@nestjs/common';
import { BookRepository } from './book.repository.js';
@Injectable()
export class BookService {
// 창고지기(BookRepository)를 주입받습니다.
constructor(private readonly bookRepository: BookRepository) {}
async getAllBooks(): Promise<any> {
return await this.bookRepository.findAll();
}
async findBooksByCategoryID(categoryID: number): Promise<any> {
return await this.bookRepository.findBooksByCategoryID(categoryID);
}
// book.service.ts에 추가
async createBook(body: Record<string, any>): Promise<string> {
await this.bookRepository.create(body);
return '도서 등록이 완료되었습니다!';
}
}
rentals.controller.ts
// src/book.controller.ts
import { Controller, Param, Patch } from '@nestjs/common';
import { RentalsService } from './rentals.service.js';
import { Body, Post } from '@nestjs/common';
@Controller('rentals')
export class RentalsController {
constructor(private readonly rentalsService: RentalsService) {}
@Post()
async rentBook(@Body() body: Record<string, any>): Promise<string> {
return await this.rentalsService.rentBook(body);
}
@Patch(':rentalID/return')
async returnBook(@Param('rentalID') rentalID:number): Promise<string> {
return await this.rentalsService.returnBook(rentalID);
}
}
Get 안 써서 그냥 뺌
rentals.repository.ts
import { Injectable, Inject } from '@nestjs/common';
import type { Pool } from 'mysql2/promise';
import { DATABASE_CONNECTION } from './database.provider.js';
@Injectable()
export class RentalsRepository {
constructor(
@Inject(DATABASE_CONNECTION) private readonly pool: Pool,
) {}
async rentBook(body: Record<string, any>): Promise<any> {
const sql = 'INSERT INTO rental (user_id, book_id, rented_at, due_at) VALUES (?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 7 DAY))';
const [result] = await this.pool.execute(sql, [
body.userId,
body.bookId,
]);
return result;
}
async returnBook(rentalID: number): Promise<any> {
const sql = 'UPDATE rental SET returned_at = NOW() WHERE rental_id = ?';
const [result] = await this.pool.execute(sql, [
rentalID
]);
return result;
}
}
rentals.service.ts
// src/book.service.ts
import { Injectable } from '@nestjs/common';
import { RentalsRepository } from './rentals.repository.js';
@Injectable()
export class RentalsService {
constructor(private readonly rentalsRepository: RentalsRepository) {}
async rentBook(body: Record<string, any>): Promise<string> {
await this.rentalsRepository.rentBook(body);
return '정상적으로 대여했습니다.'
}
async returnBook(rentalID: number): Promise<string> {
await this.rentalsRepository.returnBook(rentalID);
return '정상적으로 반납했습니다.'
}
}
기본

/books
get

post

/rentals
post

patch

3계층 아키텍처 이외에 어떤 아키텍처들이 있을까?
2계층 아키텍처 (2-Tier)
N계층 / 다층 아키텍처 (Multi-/N-tier)
마이크로서비스 아키텍처
서비스 지향 아키텍처(SOA)
헥사고날 아키텍처 / 클린 아키텍처(온ion/포츠 앤 어댑터)
핵심 도메인 로직을 중심으로 포트(입출력 인터페이스)와 어댑터로 외부 의존성을 분리. 테스트 용이성, 도메인 독립성에 강점.
헥사고날 아키텍처의 아이디어는 입력과 출력을 설계의 가장자리에 배치하는 것입니다.
비즈니스 로직은 REST 또는 GraphQL API를 노출시키는지 여부에 의존해서는 안됩니다.
마찬가지로, 데이터베이스나 API 또는 단순한 CSV 파일과 같은 데이터를 어디에서 가져오는지에 의존해서는 안 됩니다.
즉, 비즈니스 로직을 설계의 중심으로 하면서,
노출시키는 영역(outputs)과 데이터를 가져오는 영역(inputs)에 의존하지 않는 디자인을 의미입니다.
출처:
[ENFJ.dev:티스토리]

이벤트 주도 아키텍처(EDA) / 스트리밍 아키텍처
비동기 이벤트 버스(Kafka 등)로 생산자-소비자 간 결합을 줄이고 확장성을 확보. 대용량 데이터 처리나 실시간 분석에 적합.
a 통신 중개가 있어서 일관성 관리 굿. replay 가능 (복구 ?)


CQRS(쓰기/읽기 분리) + 이벤트 소싱
서버리스 아키텍처
마이크로 프런트엔드
도메인 주도 설계(DDD) 기반 아키텍처
SQL Injection을 포함한 대표적인 웹 보안 공격 기법
커넥션 풀(Connection Pool)이란?
Raw SQL vs ORM
INSERT 외에 데이터를 다루는 핵심 SQL 문법