NestJS의 DI, 모듈 등 핵심 이론을 배우고, Nest CLI를 사용해 백엔드 프로젝트와 CRUD API 뼈대를 구축합니다. 이어서, TypeORM으로 MySQL DB를 연동하고 ERD를 Entity로 구현하며, DTO와 ValidationPipe를 적용해 안정적인 API 서버를 완성합니다.
Table of Content
4일차: 백엔드 프로그래밍 (NestJS)
부제: Node.js, TypeScript, NestJS 핵심 개념과 RESTful API 서버 구축
Agenda: 4일차 학습 목표 및 일정
1.
NestJS 이론 (1): 핵심 개념, 아키텍처 (DI, Module)
2.
NestJS 실습 (1): Nest CLI 환경 구축 및 프로젝트 생성 (Resource)
3.
NestJS 이론 (2): 데이터베이스와 ORM (TypeORM)
4.
NestJS 실습 (2): DB 연동, Entity/Repository, 서비스 로직 구현
5.
NestJS 이론 (3): REST API 리팩토링 (DTO, Pipe)
6.
요약 및 5일차 예고 (FastAPI AI 연동 준비)
"안녕하십니까. 4일차, 백엔드 프로그래밍 강의에 오신 것을 환영합니다."
"어제(3일차)는 우리가 '프론트엔드', 즉 'React'를 다뤘습니다. 2일차에 설계했던 UI/UX 목업을 Vite 환경에서 실제 동작하는 컴포넌트(App.tsx, ImageUpload.tsx)로 만들었죠. useState, useEffect, React Router 같은 핵심 Hook과 라이브러리도 실습했습니다."
"오늘은 4일차, **'백엔드'**의 날입니다.
3일차에 만든 React 앱은 아직 '껍데기'에 불과합니다. 실제 '탐지' 버튼을 눌러도 아무 일도 일어나지 않죠. 왜냐하면 3일차에 만든 '클라이언트'가 요청을 보낼 '서버'가 아직 없기 때문입니다."
"오늘 8시간 동안, 우리는 3일차의 React와 통신하고, 2일차에 설계한 데이터를 처리할 **'메인 API 서버'**를 **'NestJS'**로 구축합니다."
"오늘 우리가 8시간 동안 다룰 내용을 캔버스의 Agenda로 정리했습니다. 4일차는 NestJS에 완전히 집중합니다."
1.
"먼저 **'NestJS 이론 (1)'**입니다.
Node.js 개발의 '자유도'가 왜 문제(Problem)가 되는지, 그리고 '체계적인 아키텍처'를 강제하는 NestJS가 그 해답(Solution)이 되는 이유를 알아봅니다. 3일차에 배운 TypeScript가 여기서 어떻게 쓰이는지, 그리고 NestJS의 핵심 사상인 **'DI(의존성 주입)'**와 '모듈(Module)' 개념을 짚어봅니다."
2.
"다음은 **'NestJS 실습 (1)'**입니다.
3일차에 Vite로 React 프로젝트를 만들었듯이, 4일차에는 **'Nest CLI'**라는 강력한 도구로 백엔드 프로젝트 환경을 구축합니다. 특히 nest g resource라는 명령어로, 우리가 2일차에 설계한 API의 **'CRUD 뼈대'**가 10초 만에 자동으로 생성되는 것을 경험할 것입니다."
3.
"이어서 **'NestJS 이론 (2)'**입니다.
서버의 핵심은 '데이터 저장'이죠. 2일차에 우리가 그렸던 'ERD'를 어떻게 코드로 옮기는지, 즉 '데이터베이스와 ORM', 그중에서도 **'TypeORM'**의 개념을 배웁니다."
4.
"네 번째는 **'NestJS 실습 (2)'**입니다.
이론으로 배운 TypeORM을 실제 deepfake-backend 프로젝트에 적용합니다. AppModule에 MySQL DB를 연동하고, 2일차 ERD를 Entity 클래스로 구현합니다. 그리고 Service 로직을, 단순 배열이 아닌 실제 DB에 데이터를 저장(Save)하고 조회(Find)하도록 수정합니다."
5.
"다섯 번째, **'NestJS 이론 (3) 및 실습'**입니다.
API는 '기능'뿐만 아니라 '안정성'도 중요합니다. 클라이언트(React)가 엉뚱한 데이터를 보내지 못하도록, **'DTO(데이터 전송 객체)'**와 **'ValidationPipe(유효성 검사)'**를 적용하여 우리가 만든 API를 리팩토링합니다."
6.
"마지막으로 **'요약 및 5일차 예고'**입니다.
4일차에 만든 NestJS 서버는 '메인 서버'입니다. 하지만 실제 AI 모델(Python)을 돌리기엔 적합하지 않죠. 5일차에는 이 NestJS 서버가 호출할, AI 모델 전용 '마이크로서비스 서버(FastAPI)'를 구축할 준비를 합니다."
"오늘 4일차의 성과는 명확합니다. 2일차에 설계하고 3일차에 UI를 만든 우리 앱의 **'실제 두뇌(메인 서버)'**를 NestJS로 완성하고, **'실제 데이터베이스(MySQL)'**에 데이터를 읽고 쓰는 것을 Postman으로 확인하는 것입니다.
그럼, 첫 번째 모듈, 'NestJS 핵심 개념' 이론부터 시작하겠습니다."
모듈 1: NestJS 핵심 개념 (이론)
1. Why NestJS?
•
Problem: Node.js의 자유도. (Express, Koa)
◦
개발자마다 다른 아키텍처, 코드 구조.
◦
유지보수 및 협업의 어려움 발생.
•
Solution: NestJS (Opinionated Framework)
◦
'체계적인 아키텍처'를 강제하는 프레임워크.
◦
TypeScript를 기본 언어로 사용하여 '타입 안정성' 확보 (3일차 TS 경험).
◦
모듈성(Modularity): 기능을 부품(Module)처럼 관리.
◦
DI (의존성 주입): 강력한 객체지향(OOP) 설계 지원.
"자, 그럼 4일차 첫 번째 이론 모듈, **'NestJS 핵심 개념'**을 시작하겠습니다.
우리가 3일차에 React를 배웠고, 오늘부터는 백엔드로 NestJS를 배웁니다.
3일차 'React와 NestJS 모두 JavaScript(TypeScript) 기반이다'라고 말씀드렸죠.
그럼 이런 질문이 생깁니다.
'둘 다 Node.js 런타임 위에서 돌아간다면, 왜 굳이 Express나 Koa 같은 더 가볍고 자유로운 프레임워크를 쓰지 않고, NestJS라는 무거운 프레임워크를 배워야 하는가?'"
"그 답이 바로 캔버스의 첫 번째 항목, **'Why NestJS?'**에 있습니다."
"**'Problem'**을 먼저 보시죠.
**'Node.js의 자유도'**입니다.
Express나 Koa 같은 프레임워크는 '최소한의 기능(Middleware)'만 제공합니다. 이건 장점이기도 하지만, 프로젝트가 커지면 심각한 단점이 됩니다.
바로 **'개발자마다 다른 아키텍처, 다른 코드 구조'**를 갖게 되는 것이죠.
A 개발자는 'Service'라는 폴더를 만들고, B 개발자는 'Logic'이라는 폴더를 만듭니다. A 개발자는 라우터에서 모든 비즈니스 로직을 처리하고, B 개발자는 로직을 분리합니다.
"이렇게 되면 어떻게 될까요?
**'유지보수 및 협업의 어려움'**이 기하급수적으로 증가합니다.
새로운 개발자가 프로젝트에 투입되면, 그 프로젝트만의 '암묵적인 룰'을 파악하는 데만 몇 주를 낭비하게 됩니다."
"그래서 **'Solution'**으로 **'NestJS'**가 등장합니다.
NestJS는 'Opinionated Framework', 한국어로 번역하면 **'주장(의견)이 강한 프레임워크'**입니다.
이게 무슨 뜻이냐면, '개발자님, 자유롭게 하지 마시고, 우리가 수많은 대규모 프로젝트를 통해 검증한 '체계적인 아키텍처'를 강제로 따르세요.'라고 말하는 것입니다.
'라우팅은 반드시 Controller에 작성하세요.'
'비즈니스 로직은 반드시 Service 클래스에 작성하세요.'
'기능은 반드시 Module 단위로 캡슐화하세요.'
...라고 NestJS가 '강제'합니다."
"이 '강제성' 덕분에, A 개발자가 만든 NestJS 프로젝트와 B 개발자가 만든 NestJS 프로젝트의 구조가 90% 동일해집니다. 유지보수와 협업이 매우 쉬워지죠."
"NestJS가 제공하는 강력한 장점은 다음과 같습니다."
1.
"TypeScript 기본 사용: 3일차에 우리가 React를 .tsx로 작성했던 이유와 같습니다. '타입 안정성'을 확보하여 런타임 오류를 컴파일 시점에 잡습니다. 4일차의 NestJS는 이게 '선택'이 아닌 '필수'입니다."
2.
"모듈성(Modularity): '기능을 부품(Module)처럼 관리'합니다. 'User 관련 기능', 'Dectection 관련 기능'을 레고 블록처럼 캡슐화해서, 나중에 필요 없으면 그 모듈만 떼어내면 됩니다."
3.
"DI (의존성 주입): 이것이 NestJS의 핵심 철학입니다. '강력한 객체지향(OOP) 설계'를 지원합니다. 이 DI 개념은 잠시 후 '모듈 2'에서 아주 자세히 다루겠습니다."
"정리하자면,
React가 '프론트엔드' 개발의 복잡성을 '컴포넌트'로 해결했다면,
NestJS는 '백엔드' 개발의 복잡성을 '체계적인 아키텍처(모듈, DI)'로 해결하는 프레임워크입니다."
"그럼 이 NestJS를 개발할 때 사용하는 '핵심 도구'인 'NestJS CLI'에 대해 다음 장에서 알아보겠습니다."
2. NestJS CLI (Command Line Interface)
•
NestJS 개발의 '핵심 도구'.
•
npm install -g @nestjs/cli로 설치.
•
프로젝트 생성(nest new ...), 모듈/컨트롤러/서비스 자동 생성(nest g ...) 등.
•
'보일러플레이트(Boilerplate)' 코드를 자동으로 생성하여 개발 속도 향상.
"자, 앞서서 '왜' NestJS를 써야 하는지, 그 '철학'에 대해 알아봤습니다. NestJS가 '체계적인 아키텍처'를 강제한다고 했죠."
"그럼 이제 '어떻게(How)' 그 아키텍처를 쉽게 구축할 수 있는지 알아봐야 합니다."
"만약 NestJS가 'Controller는 여기, Service는 저기, Module은 여기에 만드세요'라고 말만 하고 도구를 주지 않는다면, 개발자들은 매번 손으로 dectection.controller.ts, dectection.service.ts... 이 모든 파일을 직접 만들어야 할 겁니다. 매우 번거롭겠죠."
"그래서 NestJS는 강력한 **'자동화 도구'**를 제공합니다.
이것이 바로 모듈 1의 두 번째 항목, **'NestJS CLI (Command Line Interface)'**입니다."
"NestJS CLI는 말 그대로 **'NestJS 개발의 핵심 도구'**입니다. 3일차에 Vite가 React 프로젝트 생성을 도와줬다면, NestJS에서는 CLI가 그 역할을 합니다."
"이 도구는 **'모듈 3: 실습 1'**에서 바로 사용할 것이기 때문에, 지금 '설치'까지는 미리 확인해 보겠습니다."
"3일차처럼 로컬 PC의 터미널을 열어주세요.
npm install -g @nestjs/cli 명령어를 사용하면 '글로벌(g)'로, 즉 PC 전체에 Nest CLI 도구를 설치할 수 있습니다. (이미 설치하신 분은 넘어가셔도 됩니다.)"
(설치 확인 시연)
"이 CLI가 왜 강력하냐면, 두 가지 핵심 기능을 제공하기 때문입니다."
1.
프로젝트 생성 (nest new ...):
•
nest new deepfake-backend
•
이 명령어 한 줄이면, 우리가 모듈 3에서 실습할 것처럼, main.ts, app.module.ts 등 NestJS 프로젝트에 필요한 모든 '기본 뼈대'가 자동으로 생성됩니다.
2.
자동 생성 (nest g ...):
•
g는 'generate'의 약자입니다.
•
nest g module users → users.module.ts 파일 생성
•
nest g controller users → users.controller.ts 파일 생성
•
nest g service users → users.service.ts 파일 생성
•
... 그리고 module 파일에 이 controller와 service를 알아서 '등록'까지 해줍니다.
"결론적으로, NestJS CLI는 개발자가 직접 작성해야 하는 수많은 '보일러플레이트(Boilerplate)' 코드를 자동으로 생성해 줍니다.
개발자는 '파일 만들기', 'import 하기' 같은 귀찮은 작업 대신, '실제 비즈니스 로직'에만 집중할 수 있게 되어 개발 속도가 비약적으로 향상됩니다."
"우리는 '모듈 3' 실습에서 nest g resource detection라는 명령어를 쓸 건데, 이건 방금 본 module, controller, service 생성을 한 번에 다 해주는 더 강력한 명령어입니다. 실습 때 직접 경험해 보시죠."
"자, 그럼 이 CLI가 자동으로 생성해 주는 '빌딩 블록', 즉 'Module', 'Provider(Service)', 'Controller'가 각각 무엇을 의미하는지 다음 장에서 자세히 알아보겠습니다."
3. NestJS의 빌딩 블록 (Building Blocks)
•
Modules:
◦
@Module() 데코레이터.
◦
애플리케이션의 '기능 단위' (레고 블록).
◦
(예: UserModule, DetectionsModule, AuthModule)
◦
기능에 필요한 Controllers와 Providers를 '캡슐화'함.
•
Providers (Services):
◦
@Injectable() 데코레이터.
◦
'비즈니스 로직' (실제 기능)을 처리하는 클래스.
◦
(예: DB에서 데이터 조회, 계산 수행, API 호출 로직)
◦
'컨트롤러'로부터 '주입(Inject)'되어 사용됨.
•
Controllers:
◦
@Controller() 데코레이터.
◦
'라우터(Router)' 역할.
◦
'HTTP 요청(Request)'을 수신하고, '응답(Response)'을 반환.
◦
실제 로직은 'Provider(Service)'에게 위임함.
"자, 방금 전까지 우리는 'Nest CLI'라는 '자동화 도구'가 nest new나 nest g 같은 명령어로 '보일러플레이트' 코드를 자동으로 생성해준다고 배웠습니다.
그럼, 이 CLI가 자동으로 생성해 주는 '빌딩 블록(Building Blocks)', 즉 NestJS 아키텍처를 구성하는 가장 중요한 세 가지 요소가 무엇인지 알아보겠습니다.
캔버스의 3. NestJS의 빌딩 블록 항목을 봐주세요.
바로 **Modules(모듈), Providers(프로바이더), Controllers(컨트롤러)**입니다."
"첫 번째, **Modules (모듈)**입니다.
•
@Module() 데코레이터가 붙습니다.
•
이것은 3일차 React의 '컴포넌트'와 비슷한 개념이지만, UI가 아닌 **'기능(Feature)'**을 기준으로 묶는 '캡슐' 또는 '레고 블록'입니다.
•
예를 들어, UserModule은 '사용자' 관련 기능(회원가입, 로그인)을, DetectionModule(우리가 실습할)은 '아이템' 관련 기능(탐지 요청, 결과 조회)을 모두 담는 '기능 단위'입니다.
•
이 Module은 내부에 어떤 Controller와 Provider가 한 세트인지 NestJS에게 알려주는 '설명서' 역할을 합니다."
"두 번째, Providers (프로바이더), 그리고 우리가 가장 많이 부르게 될 이름인 **Services (서비스)**입니다.
•
@Injectable() 데코레이터가 붙습니다. ('주입 가능하다'는 뜻)
•
여기가 바로 '실제 비즈니스 로직', 즉 '핵심 두뇌'가 들어가는 클래스입니다.
•
컨트롤러가 '요청'을 받으면, '그 요청을 실제로 어떻게 처리할지'가 여기에 코딩됩니다.
•
(예시) 'DB에서 데이터를 조회'하거나, '계산을 수행'하거나, '5일차에 만들 FastAPI(AI 서버)로 API를 호출'하는 등의 **'실제 일(Logic)'**은 모두 이 Service가 담당합니다.
•
이 Service는 잠시 후 배울 Controller로부터 '주입(Inject)'되어 사용됩니다."
"세 번째, **Controllers (컨트롤러)**입니다.
•
@Controller() 데코레이터가 붙습니다.
•
컨트롤러의 역할은 **'라우터(Router)', 즉 '교통 경찰'**입니다.
•
'HTTP 요청(Request)'을 **'수신'**하고, '응답(Response)'을 **'반환'**하는 것이 유일한 임무입니다.
•
(예시) POST /detection라는 요청이 오면, 컨트롤러는 그 요청을 받아서, '아, 이건 Service가 할 일이네'라고 판단합니다.
•
그리고 Service에게 **'실제 로직(일)'을 '위임(Delegate)'**합니다. 컨트롤러가 직접 DB에 접근하거나 복잡한 계산을 하지 않습니다.
•
Service가 일을 끝내고 '결과'를 돌려주면, 컨트롤러는 그 결과를 받아 '응답(Response)'을 클라이언트(React)에게 전송합니다."
"자, 그럼 여기서 의문이 하나 생기실 겁니다.
'강사님, DB랑 통신하는 Repository(저장소)는 어디 있나요?'
캔버스의 모듈 2: 계층형 아키텍처 [cite: 54-71]를 잠시 미리 보면, 분명히 Data Access Layer (Repository)라는 '창고 관리인'이 존재합니다.
그런데 왜 '기본 빌딩 블록'에는 빠져있을까요?"
"그 이유는, 강사님께서 짚어주신 대로, **NestJS에서 'Repository'는 Provider(Service)가 사용하는 '도구'**이기 때문입니다. 즉, 아키텍처상 Service에 포함되는(사용되는) 개념입니다.
•
Controller (교통 경찰) → Service (핵심 두뇌) → Repository (창고 관리인)
"그런데 우리가 모듈 4, 5에서 'TypeORM' 같은 ORM을 사용하게 되면, 이 구조는 더욱 최적화됩니다.
우리는 Repository라는 클래스 '구현체(Implementation)'를 직접 만들지 않습니다.
즉, findAll() { return 'SELECT * ...' } 같은 SQL 코드를 우리가 짤 필요가 없다는 뜻입니다.
TypeORM이 우리가 모듈 5에서 만들 **'Entity(엔터티)'**를 기반으로, find(), save(), delete() 같은 모든 DB 통신 메서드가 이미 구현된 'Repository 구현체'를 '자동으로' 생성해 줍니다.
우리는 모듈 5 실습에서 constructor(@InjectRepository(Detection) ...)이라는 코드 한 줄로, 이 '이미 완성된' Repository를 Service에 **'주입(Inject)'**받아서 사용하기만 하면 됩니다.
정리하자면, Repository는 우리가 직접 만드는 빌딩 블록이 아니라, TypeORM이 '제공'해주고, Service가 '주입'받아 사용하는, 최적화된 데이터 접근 객체입니다."
"지금까지 NestJS의 3가지 빌딩 블록(Module, Provider, Controller)과, Provider(Service)가 사용하게 될 Repository의 개념까지 알아봤습니다.
다음 '모듈 2'에서는 이 빌딩 블록들이 어떻게 '계층형 아키텍처'와 'DI(의존성 주입)'라는 패턴으로 동작하는지 더 자세히 알아보겠습니다."
모듈 2: 아키텍처 및 디자인 패턴 (이론)
1. 계층형 아키텍처 (Layered Architecture)
•
2일차에 설계한 '플로우 차트'를 NestJS가 어떻게 구현하는가?
•
[ 3-Tier 아키텍처 ]
1.
Controller Layer: (교통 경찰)
•
main.ts -> app.module.ts -> detection.controller.ts
•
@Controller('detection'), @Get(), @Post()
•
오직 '요청'과 '응답'만 처리. (Request/Response)
2.
Service Layer (Provider): (핵심 두뇌)
•
detection.service.ts
•
@Injectable()
•
*'실제 비즈니스 로직'**이 일어나는 곳.
•
(예: "아이템을 DB에 저장하라", "결과를 계산하라")
3.
Data Access Layer (Repository): (창고 관리인)
•
(실습 2에서 구현) TypeORM, detection.repository.ts
•
실제 DB와 통신(SQL Query)하는 로직.
"자, 4일차 두 번째 이론 모듈, **'아키텍처 및 디자인 패턴'**입니다.
모듈 1에서는 NestJS의 '빌딩 블록'(Module, Controller, Provider)이 각각 무엇인지 '개념'을 배웠습니다.
이제 모듈 2에서는 이 빌딩 블록들이 '어떻게 함께 작동하는지', 그 '구조(Architecture)'를 알아보겠습니다."
"첫 번째 항목은 **'계층형 아키텍처 (Layered Architecture)'**입니다.
이 구조는 여기 계신 모든 개발자분들에게 이미 익숙한, 가장 표준적인 **'3-Tier 아키텍처'**입니다.
가장 중요한 질문은 이것입니다.
'2일차에 우리가 설계한 '플로우 차트'를 NestJS가 어떻게 구현하는가?'"
"2일차에 우리는 React(클라이언트)가 API를 호출하면, 서버가 AI 모델을 호출하고 DB에 저장하는 '흐름'을 그렸습니다.
NestJS는 이 '흐름'을 처리하기 위해, 서버 내부의 역할을 **'세 개의 계층(Layer)'**으로 명확하게 분리합니다.
캔버스의 [ 3-Tier 아키텍처 ] 항목을 보시죠.
Controller, Service, Data Access Layer입니다."
"1. Controller Layer (컨트롤러 계층): (교통 경찰)
•
이 계층의 파일은 detection.controller.ts입니다.
•
@Controller('detection'), @Get(), @Post() 같은 데코레이터를 사용합니다.
•
모듈 1에서 배운 것처럼, 이 계층의 역할은 단 하나, **'교통 경찰'**입니다.
•
클라이언트(React)로부터 POST /detection 같은 'HTTP 요청(Request)'을 받아서, '어떤 서비스'가 이 일을 처리해야 하는지 '라우팅'하고,
•
서비스가 일을 마치면, 그 '결과'를 받아 클라이언트에게 '응답(Response)'해줍니다.
•
절대로 컨트롤러가 직접 계산하거나 DB에 접근하지 않습니다. 오직 '요청'과 '응답'만 처리합니다."
"2. Service Layer (Provider): (핵심 두뇌)
•
이 계층의 파일은 detection.service.ts입니다.
•
@Injectable() 데코레이터가 붙죠.
•
여기가 바로 '핵심 두뇌', 즉 **'실제 비즈니스 로직'**이 일어나는 곳입니다.
•
컨트롤러로부터 '아이템을 생성해 줘'라는 요청을 위임받으면,
•
(예시) '아이템을 DB에 저장하라', 'AI 서버를 호출해서 결과를 계산하라' 같은 **'실제 일(Logic)'**을 수행합니다."
"3. Data Access Layer (Repository): (창고 관리인)
•
이 계층은 모듈 5 실습에서 구현할 TypeORM이 담당합니다. (detection.repository.ts라는 파일을 직접 만들 수도 있지만, TypeORM은 이 과정을 자동화해줍니다.)
•
이 계층의 역할은 **'창고 관리인'**입니다.
•
Service 계층(두뇌)으로부터 '아이템 저장해 줘'라는 명령을 받으면,
•
이 계층이 **'실제 DB와 통신(SQL Query)'**하는 로직을 수행합니다.
•
'SQL 쿼리'를 직접 작성하거나, 모듈 4에서 배울 'ORM 메서드'(repository.save())를 실행하는 유일한 곳입니다."
"정리하자면, 3일차의 React가 '탐지' 버튼을 누르면,
그 요청은 NestJS의 Controller('교통 경찰')에게 먼저 가고,
Controller는 Service('핵심 두뇌')에게 일을 위임하며,
Service는 Repository('창고 관리인')를 시켜 DB에 데이터를 저장한 뒤,
그 결과를 다시 Controller를 통해 React에게 응답하는
**명확한 '단방향 흐름'**이 만들어집니다. 이것이 NestJS가 강제하는 '계층형 아키텍처'입니다."
"그럼 다음 장에서는, 이 Controller와 Service가 서로 어떻게 '느슨하게' 연결되는지, NestJS의 핵심 사상인 **'DI(의존성 주입)'**에 대해 알아보겠습니다."
2. DI (의존성 주입) 및 IoC (제어의 역전)
•
Problem: 클래스 간의 강한 결합(Coupling).
◦
Controller가 new DetectionsService()처럼 직접 객체를 생성하면,
◦
Controller는 DetectionsService에 '강하게 의존'하게 됨.
◦
테스트가 어렵고, 유지보수가 힘듦.
•
Solution (IoC): NestJS가 '제어'를 '역전'시킴.
◦
개발자가 new로 생성하는 게 아니라, NestJS 프레임워크가 객체를 생성하고 관리함.
•
How (DI):
◦
Service는 @Injectable()로 "주입 가능"하다고 선언.
◦
Module에 "이 모듈은 DetectionService를 쓴다"고 providers에 등록.
◦
Controller는 constructor(private readonly detectionService: DetectionService)처럼 '생성자'에서 '주입'을 요청함.
◦
결과: NestJS가 알아서 DetectionService 객체를 만들어 Controller에 주입함.
"자, 모듈 2의 첫 번째 주제로, 우리는 NestJS가 '계층형 아키텍처'를 사용해서 Controller(교통 경찰), Service(두뇌), Repository(창고)로 역할을 명확히 분리한다는 것을 배웠습니다."
코드에서 분리한다는 의미는 결국 파일 자체를, 파일은 또한 클래스 파일 자체를 분리한다는 말과 같습니다. 말그대로 컨트롤러 파일과 서비스 파일이 분리 된다는 것인데요.
컨트롤러 파일은 매개체 역할의 코드만 있을것이고, 서비스 파일은 비지니스 로직 즉, 연산과 관련된 내용만 있을 것 입니다. 레포지토리는 또한 데이터관련 코드만 있겠죠. 하지만 실제로 프로그램은 이렇게 따로따로 움직이지 않고 마치 우리가 달리기 할때 팔과 다리와 발가락과 무릎과 모든 부위가 나누어져있지만 서로 유기적으로 움직이면서 달린다는 행동을 취하는 것 처럼,
웹 서비스도 어떤 동작에 따라 이 모든것들이 유기적으로 연관된 부분들이 움직이게 됩니다. 하지만 코드가 분리되어 있는 점에서 어떻게 연결되어 있을까를 생각해봐야 합니다.
우선 직관적으로 단순한 방법은 이 코드들이 모두 한부위(파일)에 모여있는 것입니다. 그럼 서로 직접연결되어 있기 때문에 당연히 모두 움직이겠죠. 하지만 이러면 우리의 몸이 부위별로 나눠져있기 때문에 효율적으로 필요한 것만 움직이도록 설계되있는 것 처럼 프로그램도 필요한것만 사용하고 서로 통신되도록 구조화 시켜야 효율적이고 유지보수가 편리해집니다. 우리가 팔이 다치면 팔만 치료하듯이, 소프트웨어에도 어느부분이 오류가 있는지, 고쳐야 할 때 빠르게 찾고 교체하거나 수리 할 수 있겠죠.
이런 효율적으로 구조화하여 필요한 부분만 연결 시키는 것을 모듈화라고 하며 소프트웨어에서는 “낮은 결합도” 라고 합니다. 반대로 높은 결합도는 관련있는 다른 부분이, 직접 코드에 큰 영향(최악은 직접 포함 된 경우)을 미칠 수 밖에 없는 상태를 높은 결합도를 가지고 있다고 합니다. 좋은 영향이던 나쁜 영향이던 이 부품과 관련없는 부분은 낮은 결합도를 지향하도록 설계해야 유리하겠죠. 하지만 1개의 부품 내부를 살펴보겠습니다. 해당 부품은 해당 부품의 이름에 걸맞게 그 기능들만 수행 할 수 있도록 똘똘뭉쳐야 합니다. 네, 그 모듈은 “높은 응집도”를 가지고 있어야 한다라고 합니다. 이처럼 모듈간의 결합도는 낮추고, 모듈의 응집도를 높이는 것이 좋은 소프트웨어를 개발하는데 꼭 필요한 부분이라 할 수 있습니다.
이러한 관점에서 많은 것들을 바라 볼 수 있습니다. 저희는 AI 소프트웨어는 Python을 쓰고, 프론트엔드는 React, 백엔드는 NestJS, 데이터베이스를 MySQL을 사용하듯, 각 부분의 역할에 맞도록 전문적으로 응집도가 높은 프레임워크를 사용하고 있으며, 이 각 아키텍처들을 각 관문들을 통해서만 연결하여 낮은 결합도를 유지하고자 하는 아키텍처 설계 관점에서도 이미 이렇게 진행되고 있습니다.
한단계 확대를 해서 현재 배우고 있는 백엔드인 NestJS 내부에서만 보더라도 이전에 Controller, Service, Repository 처럼 3개의 계층으로 구분해서 구현될 것이라 했습니다. 이 또한 백엔드 내부를 각 파일의 역할을 나누어 각 역할에 집중되도록하고 그 사이를 연결하는, 서로 통신되도록 구성하는 설계 철학이 모두 담겨있는 것입니다.
이렇게 모듈화를 통해서 재활용성과 유지보수성을 확보 할 수 있고, 더 나은 코드와 다음 개발에서는 보다 빠르게 재사용할수 있는 부품들을 만들고, 또한 다른 개발자가 만든 재활용 부품을 사용하는 것이 개발 생태계가 이어질 수 있는 힘입니다.
여기까지 조금 개발과 관련된 주변이야기를 했지만, 다시 NestJS로 돌아와서 각 계층별 분리는 좋지만 서로 통신되도록 연결도 되어야합니다.
이런 연결을 의존성 주입이라고 합니다. 특히 각 모듈파일로 분리된 계층들을 “필요한 파일이(실제론 클래스)” 대상 클래스를 사용 할 수 있도록 주입하여 사용하게 됩니다.
쉽게 컨트롤러는 서비스로 데이터를 전달한다 라는 말에서 컨트롤러 클래스는 서비스 클래스를 가지고 있어야 데이터를 전달 할 수 있고, 이런 것을 컨트롤러가 서비스를 호출한다라고 합니다. 이어서 서비스도 레포지토리를 호출해야 하겠죠.
코드에서는 그 클래스의 메서드를 호출하기 위해서는 직접 그 코드에 상단에 미리 정의되어 있어야 합니다. 전혀 모르는 코드는 사용할 수 없는것이 기본인것 처럼요.
따라서 컨트롤러는 서비스의 코드 사용이 필요하기 때문에, 서비스라는 객체 모듈 자체를 가지고 있게 만듭니다. (실제 코드를 가지고 있는 것이 아닌) 이걸 어렵게는 의존성주입이라고 하며, 이런 것들을 연결해주고 서로 주입 관리해주는 부분을 DI 컨테이너라고 합니다. 이 의존성 생성 과 주입을 개발자가 직접 관리하는 것이 아니라, DI 컨테이너가 관리하기 때문에 프레임워크가 개발자의 코드를 관여하는 형태가 되면서, 이것을 IoC(제어의 역전) 이라고 합니다.
결과적으로 DI를 통해 제어의 역전이 발생했고, 이것은 낮은 결합도, 높은 응집도를 위한 설계를 진행하게 되는겁니다.
이 이론적인 부분은 처음엔 와닿지 않거나 조금 햇갈리게 느껴지실수 있지만, 용어자체가 어렵게 느껴져서 그렇다고 생각합니다. 실제로 우린 이미 제어의 역전을 계속 경험하고 있습니다. 프로그래밍 언어의 규칙을 따라야하는것, 프레임워크가 지정하는 틀대로 규칙대로 프로그램을 구성하는것, 이 행동들 자체의 목적은 빠르고 쉽게 그 가이드에 맞춰서 개발하면 좋은 프로그램을 빠르게 개발 할 수 있기 때문에 의지하게 되고 믿고 진행하는 것입니다. 따라서 제어의 역전을 통해 규칙에 따라 개발되면 좋은 품질의 소프트웨어를 신뢰도 있게 구성 할 수 있을 것입니다.
이렇게 NestJS의 의존성 주입은 각 모듈 파일간의 주입을 통해서 진행된다는 것으로 정리하겠습니다.
그 다음으로 중요한것은 해당 프레임워크의 규칙인 데코레이터(애너테이션)을 살펴보겠습니다.
주요 데코레이터 @
1. 핵심 빌딩 블록 (아키텍처)
•
@Module(): NestJS 앱의 '기능 단위'를 정의함. controllers와 providers 배열에 이 모듈이 사용할 클래스들을 등록.
•
@Injectable(): 이 클래스가 DI(의존성 주입) 시스템에 '주입 가능한 Provider'(주로 Service)임을 선언.
•
@Controller('prefix'): 이 클래스가 HTTP 요청을 처리하는 '라우터'임을 선언함. prefix로 기본 경로(예: 'detection')를 지정.
2. 컨트롤러 (라우팅 및 데이터 추출)
•
@Get(), @Post(), @Patch(), @Delete(): 각 HTTP Method(GET, POST 등) 요청을 처리할 '핸들러(메서드)'를 지정.
•
@Param('id'): URL 경로에서 파라미터(예: /detection/:id의 id 값)를 추출.
•
@Body(): 클라이언트(React)가 보낸 Request Body 전체를 DTO 객체로 받아옴.
3. 데이터베이스 (TypeORM 연동)
•
@Entity(): 해당 클래스를 DB '테이블'과 매핑함 (2일차 ERD가 코드가 되는 순간).
•
@PrimaryGeneratedColumn(): @Entity 내부에서 '기본 키(PK)' 속성을 정의 (자동 증가).
•
@Column(): 해당 속성을 DB '컬럼'과 매핑함.
•
@InjectRepository(Detection): Service의 constructor에서 Detection Entity의 Repository 객체를 '주입'받을 때 사용.
4. 데이터 유효성 검사 (DTO & Pipe)
•
@IsString(), @IsNotEmpty(): class-validator 라이브러리. ValidationPipe와 함께 DTO 클래스 속성의 유효성을 '자동으로' 검사.
모듈 3: 실습 1 - 프로젝트 환경 구축 및 API 테스트
1. 실습 목표
•
NestJS CLI로 백엔드 프로젝트 환경 구축.
•
'Resource' 명령어로 CRUD API 뼈대 자동 생성.
•
Postman으로 API 동작 테스트 (React 연동 전).
모듈 1, 2에서 우리는 NestJS가 '왜' 필요한지(자유도 문제 해결), 그리고 '어떻게' 동작하는지(DI, 계층형 아키텍처) 이론을 배웠습니다.
이제 이 이론을 코드로 직접 구현해 볼 시간입니다."
"이번 실습 목표는 캔버스에 보시는 것처럼 세 가지입니다.
1.
Nest CLI로 백엔드 프로젝트 환경을 구축합니다.
2.
'Resource' 명령어라는 Nest CLI의 핵심 기능을 사용해, 2일차에 설계한 CRUD API 뼈대를 자동으로 생성합니다.
3.
3일차 React와 연결하기 전에, Postman이라는 툴을 사용해 '이 백엔드 서버가 정말 독립적으로 동작하는지' 먼저 테스트합니다."
추가로 Postman이라는 것을 미리 설치해둡시다. 해당 프로그램은 데이터 통신을 간단하게 테스트 할 수 있는 도구로 GET요청같은 경우엔 브라우저로 손쉽게 할 수 있지만, 그 외 POST, PUT, DELETE등 다른 HTTP 메서드 요청을 테스트 할 수 있는 도구입니다.
2. Nest CLI 설치 (로컬)
•
3일차와 동일하게 로컬 터미널(VS Code 터미널) 사용.
•
(최초 1회만 실행)
•
npm install -g @nestjs/cli
1. Nest CLI 설치 (로컬)
"3일차에 React(Vite)를 설치하면서, 우리는 이미 node와 npm은 설치가 완료된 상태입니다."
"하지만 NestJS 프로젝트를 생성하고 관리하려면, Vite와 유사한 '전용 커맨드 라인 도구(CLI)'가 필요합니다.
다 같이 3일차에 사용했던 로컬 PC의 터미널을 열어주세요. (VS Code 터미널 등)"
"먼저, nest 명령어가 설치되어 있는지 확인해 보겠습니다."
Bash
nest --version
(실행)
"만약 9.x.x 같은 버전 번호가 나오면 이미 설치된 것입니다.
하지만 'command not found' (명령어를 찾을 수 없습니다)라고 나온다면, 지금 바로 **글로벌(-g)**로 Nest CLI를 설치해야 합니다."
"설치가 안 되신 분들은, 다음 명령어를 입력해 주세요. (이미 설치된 분은 안 하셔도 됩니다.)"
Bash
npm install -g @nestjs/cli
(실행)
"이것은 3일차 npm create vite와는 다르게, -g 옵션으로 nest라는 명령어를 우리 PC '전체'에 설치하는 과정입니다."
3. 새 프로젝트 생성
•
3일차 React 프로젝트(my-app)가 있는 폴더 밖으로 이동.
•
cd ..
•
nest new detect-backend (프로젝트 명)
•
(package manager는 npm 선택)
"좋습니다. 이제 nest 명령어가 준비되었습니다.
본격적으로 백엔드 서버 프로젝트를 생성해 보겠습니다."
"중요: 3일차에 만든 React 프로젝트(detect-frontend) **'안'**이 아니라, 그 **'밖'**에, 즉 React와 NestJS가 **'같은 레벨'**에 있도록 폴더를 만들어야 합니다."
"현재 터미널이 detect-frontend 폴더 안이라면, cd .. 명령어로 상위 폴더로 이동해 주세요."
"이제, nest new 명령어로 'detect-backend'라는 이름의 새 프로젝트를 생성합니다."
Bash
nest new detect-backend
(실행)
"명령어를 실행하면, NestJS가 '어떤 패키지 매니저를 사용할 것인지' 물어봅니다.
npm / yarn / pnpm이 나오죠?
우리는 3일차 React와 통일하기 위해, 키보드 화살표로 **npm**을 선택하고 엔터를 칩니다."
"(설치 대기) ... NestJS가 package.json에 필요한 모든 기본 패키지들을 npm으로 설치하고 있습니다. 3일차 npm install과 동일한 과정입니다."
4. 프로젝트 구조 분석
•
cd deepfake-backend
•
VS Code로 폴더 열기.
•
src/main.ts: 앱이 시작되는 '진입점'.
•
src/app.module.ts: 루트 모듈.
•
src/app.controller.ts: 기본 컨트롤러.
•
src/app.service.ts: 기본 서비스.
•
npm run start:dev 실행 (Hot-reload 지원). 그냥 npm run start로 해도 상관없습니다.
"(VS Code로 화면 전환)
src 폴더를 열어보세요. 3일차 Vite가 App.tsx를 만들어 준 것과는 다르게, NestJS는 우리가 모듈 1, 2에서 배운 **'아키텍처'**에 필요한 파일들을 이미 다 만들어 줬습니다."
•
src/main.ts: NestJS 애플리케이션을 '시작'시키는(Bootstrap) 진입점 파일입니다.
•
src/app.module.ts: @Module() 데코레이터가 붙은, 앱의 '루트 모듈'입니다.
•
src/app.controller.ts: @Controller()가 붙은, '루트 컨트롤러'(교통 경찰)입니다.
•
src/app.service.ts: @Injectable()이 붙은, '루트 서비스'(핵심 로직)입니다.
"이것이 바로 '주장이 강한(Opinionated)' 프레임워크입니다. 이미 구조가 다 잡혀있죠."
"자, 이 서버를 바로 실행해 보겠습니다. VS Code 터미널에서 다음 명령어를 입력하세요."
Bash
npm run start:dev
"이 명령어는 3일차 npm run dev와 똑같이, 'Hot-reload', 즉 코드가 바뀌면 서버가 자동으로 재시작되는 개발용 서버 명령어입니다."
"(로그 확인) ... 터미널에 Application is running on: http://localhost:3000 메시지가 뜨면 성공입니다.
이제 브라우저에서 http://localhost:3000 주소로 접속해 보세요.
app.service.ts가 반환하는 **'Hello World!'**가 보일 겁니다."
5. 리소스(Resource) 자동 생성
•
NestJS의 가장 강력한 기능.
•
(새 터미널) nest g resource detection
•
(g = generate)
•
(질문 1) What name would you like to use for the resource? detection
•
(질문 2) What transport layer would you like to use? REST API
•
(질문 3) Would you like to generate CRUD entry points? y (Yes)
•
결과:
◦
src/detection 폴더가 생기고,
◦
detection.module.ts, detection.controller.ts, detection.service.ts
◦
detection.entity.ts, dto/create-detection.dto.ts 등
◦
CRUD API 뼈대가 10초 만에 자동으로 완성됨.
"좋습니다. 서버가 동작하는 것을 확인했습니다.
이제 2일차에 설계한 '탐지 내역' 관련 API를 만들 차례입니다.
detection.controller.ts, detection.service.ts... 이 파일들을 우리가 직접 만들까요?"
"아닙니다. 'Nest CLI'가 이 작업을 대신해 줍니다."
-
•
(새 터미널) nest g resource detection --no-spec
•
이 명령어를 (실행)
"자, CLI가 우리에게 3가지를 물어봅니다."
1.
"What name would you like to use for the resource?"
-> detection 이라고 이미 입력되어 있습니다. 엔터를 칩니다.
2.
"What transport layer would you like to use?"
-> "어떤 통신 방식을 쓸 거냐?"는 질문입니다. 우리는 REST API를 만들 것이므로, REST API (기본값)를 선택하고 엔터를 칩니다.
3.
"Would you like to generate CRUD entry points?"
-> "CRUD(생성, 읽기, 수정, 삭제) API 뼈대를 '자동으로' 생성해 줄까?"
-> 이것이 핵심입니다. y (Yes)를 누르고 엔터를 칩니다.
"자, 이제 VS Code의 src 폴더를 주목해 주세요.
src/detection이라는 폴더가 통째로 생겼습니다!
그 안에는 detection.module.ts, detection.controller.ts, detection.service.ts는 물론, detection.entity.ts와 dto/create-detection.dto.ts까지,
2일차에 우리가 설계했던 CRUD API의 모든 뼈대(Boilerplate)가 10초 만에 자동으로 완성되었습니다.
심지어 src/app.module.ts 파일을 열어보시면, Imports 배열에 DetectionModule이 '자동으로' 등록까지 되어있습니다.
이것이 '주장이 강한(Opinionated)' 프레임워크와 'Nest CLI'가 주는 최고의 생산성입니다."
"믿기지 않죠? 그럼 '탐지 내역(detection)' API가 정말 동작하는지 테스트해 보겠습니다. 3일차 React와 아직 연결하기 전이기 때문에, **'Postman'**이라는 툴을 사용해 API가 독립적으로 동작하는지 확인해야 합니다."
"첫 번째 터미널의 npm run start:dev 서버가 계속 실행 중인지 확인하세요. (포트 3000번)"
6. Postman을 이용한 API 테스트 (★수정됨)
•
npm run start:dev 실행 (포트 3000번)
•
Postman 툴 실행.
•
(GET) http://localhost:3000/detection -> findAll() (배열 응답)
•
(POST) http://localhost:3000/detection (Body: raw/JSON) -> create()
•
(GET) http://localhost:3000/detection/1 -> findOne()
•
(3일차 React와 아직 연동 안 했지만, 백엔드 서버가 준비됨)
"좋습니다. 그럼 '탐지 내역(detection)' API가 정말 동작하는지 테스트해 보겠습니다. 3일차 React와 아직 연결하기 전이기 때문에, **'Postman'**이라는 툴을 사용해 API가 독립적으로 동작하는지 확인해야 합니다."
"첫 번째 터미널의 npm run start:dev 서버가 계속 실행 중인지 확인하세요. (포트 3000번)"
"자, 다 같이 Postman 툴을 엽니다."
"테스트 1: GET /detection (모두 조회)"
•
"Postman에서 새 탭을 엽니다."
•
"Method를 GET으로 둡니다."
•
"URL에 http://localhost:3000/detection 를 입력하고 'Send' 버튼을 누릅니다."
•
"결과(Response)를 보세요. Body에 This action returns all detection라는 문자열이 오면 성공입니다. detection.controller.ts의 findAll() 메서드가 자동으로 호출된 것입니다."
"테스트 2: POST /detection (create)"
•
"Method를 POST로, Body에 아까처럼 JSON을 넣고 'Send'"
•
"결과: This action adds a new detection 문자열이 오면 성공입니다. create()가 호출됐습니다."
"이것으로 Controller 뼈대가 살아있음을 증명했습니다."
"테스트 3: GET /detection/:id (특정 1개 조회)"
•
"Method를 다시 GET으로 변경합니다."
•
"URL 끝에 /1을 붙여 http://localhost:3000/detection/1 로 만듭니다."
•
"'Send'를 누릅니다. This action returns a #1 detection 응답이 오면 성공입니다. findOne() 메서드가 호출되었습니다."
"좋습니다. 이것으로 4일차 첫 번째 실습이 끝났습니다.
우리는 Nest CLI로 백엔드 프로젝트를 생성했고, 'Resource' 명령어로 10초 만에 API 뼈대를 만들었으며, Postman으로 이 API 엔드포인트가 '살아있음'을 증명했습니다."
"하지만 지금 이 서버는 '가짜'입니다.
Postman으로 데이터를 POST해도 반환(return)으로 지정된 문자만 응답하고 있죠. 실제 데이터베이스나 어떤 로직이 있지 않습니다.
"이제 다음 **모듈 4 (이론)**와 **모듈 5 (실습)**에서는, 이 '가짜' 서비스를 '진짜' MySQL 데이터베이스와 TypeORM으로 연결하는 작업을 진행하겠습니다."
모듈 4: Database와 ORM (이론)
RDBMS 및 SQL 기초
우선 실제 데이터베이스를 백엔드 서버와 연동하기 위해서 데이터베이스에 대해서 기본적으로 이해해야 합니다.
그 근간이 되는 관계형 데이터베이스와 SQL의 기초를 정리하는 것은 매우 중요합니다.
관계형 데이터베이스(RDBMS) 및 SQL 기초
1. 관계형 데이터베이스 (RDBMS)란?
•
정의: 데이터를 **'테이블(Table)'**이라는 정형화된 2차원 표 형태로 저장하고 관리하는 데이터베이스 시스템을 의미함. (우리가 4일차에 사용할 MySQL이 대표적임).
•
주요 구성 요소:
◦
테이블 (Table): 데이터가 실제로 저장되는 기본 단위 (엑셀 시트와 유사).
(예: 2일차에 설계한 Detection 엔터티 -> detection 테이블)
◦
행 (Row / Record): 테이블에 저장된 하나의 완전한 데이터 항목.
(예: 'ID 1번 탐지 내역'의 모든 정보)
◦
열 (Column / Attribute): 데이터의 '속성' 또는 '타입'.
(예: id, filename, filetype, createdAt)
◦
키 (Key):
▪
기본 키 (Primary Key, PK): 각 행(Row)을 '고유하게' 식별하는 값. (예: id 컬럼)
▪
외래 키 (Foreign Key, FK): 한 테이블이 다른 테이블을 '참조(연결)'하기 위해 사용하는 키. 2일차에 설계한 ERD의 **'관계(Relationship)'**를 구현하는 핵심 요소임.
2. SQL (Structured Query Language)이란?
•
정의: 이러한 RDBMS(관계형 데이터베이스)와 통신(데이터를 넣거나, 빼거나, 수정하거나, 정의)하기 위해 사용하는 '표준 질의 언어'.
•
ORM과의 관계: 모듈 4에서 배운 TypeORM 같은 ORM은, 개발자가 작성한 TypeScript 코드(예: repository.find())를, 내부적으로 이 **'SQL'**로 '번역'하여 DB에 대신 실행해 줌.
세부적인 내용을 들어가기 전에 우선 관계형데이터베이스에서 많이 사용되는 MySQL 환경 구축을 먼저하겠습니다.
이제 개발 컴퓨터에 데이터베이스를 설치했으며 백그라운드에서 3306포트로 실행되고 있습니다. 다시 데이터베이스 언어인 SQL을 세부적으로 살펴보겠습니다.
3. SQL의 3가지 주요 종류 (구분)
SQL 명령어는 그 '역할'에 따라 크게 3가지로 구분합니다.
구조 생성의 DDL, 권한과 같은 것으로 DCL, 실제 데이터를 삽입하거나 수정 삭제하는 DML로 구성됩니다.
SQL도 데이터베이스가 이해할 수 있는 문법으로 구성되어 있으며 각각 키워드들을 이해하고 있어야 합니다. 대표적인 키워드들은 다음과 같습니다.
1. DDL (Data Definition Language - 데이터 정의어)
•
역할: 데이터베이스의 '구조(Structure)'를 정의하는 언어 (테이블/DB의 '설계도'를 다룸).
•
대표 명령어:
◦
CREATE: 테이블, 데이터베이스를 생성.
◦
ALTER: 테이블 구조를 변경 (예: 컬럼 추가, 타입 변경).
◦
DROP: 테이블, 데이터베이스를 삭제.
•
(TypeORM의 synchronize: true 옵션은, detection.entity.ts 파일을 읽고 이 DDL(CREATE, ALTER)을 '자동으로' 실행해 주는 기능임)
2. DML (Data Manipulation Language - 데이터 조작어)
•
역할: 테이블 내의 '데이터(Row)'를 **조작(CRUD)**하는 언어 (가장 많이 사용됨).
•
대표 명령어 (CRUD):
◦
SELECT (Read): 데이터를 조회.
◦
INSERT (Create): 새 데이터를 삽입.
◦
UPDATE (Update): 기존 데이터를 수정.
◦
DELETE (Delete): 기존 데이터를 삭제.
•
(TypeORM의 repository.find(), repository.save(), repository.delete() 메서드가 이 DML을 대신 실행해 줌)
3. DCL (Data Control Language - 데이터 제어어)
•
역할: 데이터에 대한 '접근 권한'과 '보안'을 제어하는 언어 (주로 DB 관리자가 사용).
•
대표 명령어:
◦
GRANT: 특정 사용자에게 SELECT, INSERT 등 특정 작업의 권한을 부여.
◦
REVOKE: 사용자의 권한을 회수.
실제로 데이터베이스 SQL에 익숙해지기 위해서 기본적인 실습을 진행하겠습니다.
CREATE TABLE IF NOT EXISTS MAJOR
(
major_code varchar(100) primary key comment '주특기코드',
major_name varchar(100) not null comment '주특기명',
tutor_name varchar(100) not null comment '튜터'
);
SQL
복사
CREATE TABLE IF NOT EXISTS STUDENT
(
student_code varchar(100) primary key comment '수강생코드',
name varchar(100) not null comment '이름',
birth varchar(8) null comment '생년월일',
gender varchar(1) not null comment '성별',
phone varchar(11) null comment '전화번호',
major_code varchar(100) not null comment '주특기코드',
foreign key(major_code) references major(major_code)
);
SQL
복사
CREATE TABLE IF NOT EXISTS EXAM
(
student_code varchar(100) not null comment '수강생코드',
exam_seq int not null comment '시험주차',
score decimal(10,2) not null comment '시험점수',
result varchar(1) not null comment '합불'
);
SQL
복사
desc exam;
ALTER TABLE EXAM ADD PRIMARY KEY(student_code, exam_seq);
ALTER TABLE EXAM ADD CONSTRAINT exam_fk_student_code FOREIGN KEY(student_code) REFERENCES STUDENT(student_code);
SQL
복사
INSERT INTO MAJOR VALUES('m1', '스프링', '남병관');
INSERT INTO MAJOR VALUES('m2', '노드', '강승현');
INSERT INTO MAJOR VALUES('m3', '플라스크', '이범규');
SQL
복사
INSERT INTO STUDENT VALUES('s1', '최원빈', '20220331', 'M', '01000000001', 'm1');
INSERT INTO STUDENT VALUES('s2', '강준규', '20220501', 'M', '01000000002', 'm1')
,('s3', '김영철', '20220711', 'M', '01000000003', 'm1');
INSERT INTO STUDENT VALUES('s4', '예상기', '20220408', 'M', '01000000004', 'm3'),
('s5', '안지현', '20220921', 'F', '01000000005', 'm3');
SQL
복사
select * from student;
INSERT INTO EXAM VALUES('s1', 1, 8.5, 'P'),
('s1', 2, 9.5, 'P'),
('s1', 3, 3.5, 'F'),
('s2', 1, 8.2, 'P'),
('s2', 2, 9.5, 'P'),
('s2', 3, 7.5, 'P'),
('s3', 1, 9.3, 'P'),
('s3', 2, 5.3, 'F'),
('s3', 3, 9.9, 'P');
SQL
복사
INSERT INTO STUDENT VALUES('s0', '수강생', '20220331', 'M', '01000000005', 'm1');
UPDATE STUDENT SET major_code= 'm2' where student_code= 's0';
SQL
복사
DELETE FROM STUDENT WHERE student_code = 's0';
SQL
복사
-- 모든 STUDENT 테이블 데이터 조회
SELECT * FROM STUDENT;
-- 특정 STUDENT 데이터 조회
SELECT * FROM STUDENT WHERE STUDENT_CODE = 's1';
-- 특정 STUDENT의 이름과 전공 코드 조회
SELECT name, major_code FROM STUDENT WHERE student_code = 's1';
SQL
복사
--명시적 조인
--JOIN 키워드를 사용하여 두 테이블을 조인합니다.
--ON 절을 사용하여 조인 조건을 지정합니다 (s.major_code = m.major_code).
--가독성이 좋고, 복잡한 조인 조건을 명확하게 표현할 수 있습니다.
--INNER JOIN, LEFT JOIN, RIGHT JOIN 등 다양한 조인 방식이 있으며 디폴트값은 INNER 조인으로 교집합을 나타냅니다.
--조인 방식을 통해 원하는 데이터를 할 수 있습니다.
-- 이 쿼리는 STUDENT 테이블의 major_code와 MAJOR 테이블의 major_code가 일치하는 경우,
-- 학생의 이름, 전공 코드, 그리고 해당 전공 이름을 함께 반환
SELECT s.name, s.major_code, m.major_name
FROM STUDENT s
JOIN MAJOR m
ON s.major_code = m.major_code;
--참고로 s, m 등 축약된것은 별칭(alias)라고 불리움
SQL
복사
여기까지 간략하게 SQL들을 사용하면서 데이터베이스에 어떻게 데이터가 삽입, 조회, 수정, 삭제 되는지 살펴보았습니다.
1. Why ORM? (Object-Relational Mapping)
•
Problem: SQL과 객체지향(TS/JS)의 '패러다임 불일치'.
◦
(TS) class User { ... } (객체)
◦
(SQL) SELECT * FROM users WHERE ... (문자열 쿼리)
◦
서비스 로직이 'SQL 문자열'로 가득 차게 됨 (유지보수 어려움).
•
Solution (ORM): SQL 없이, '객체'와 '메서드'로 DB를 조작.
◦
(ORM) userRepository.find({ where: { id: 1 } })
◦
코드가 'SQL'이 아닌 'TypeScript'가 됨 (가독성, 안정성 향상).
"자, 4일차 모듈 3 실습을 모두 마쳤습니다.
Postman으로 http://localhost:3000/detection을 테스트해 보니, API가 동작하는 것은 확인했습니다.
하지만 우리는 detection.service.ts의 create() 메서드가 실제로 어떤 연산은 없는 문자만 반환하는 '가짜' 서비스죠.
"이제 모듈 4와 모듈 5에서는 이 '가짜' 서비스를 '진짜' MySQL 데이터베이스와 연동하여, 데이터가 영구적으로 저장되도록 만들 것입니다."
"DB를 연동한다고 하면, 많은 분들이 Service 로직 안에서 'SQL 쿼리'를 직접 작성하는 것을 떠올립니다.
하지만 최근 많은 기술들은 NestJS는 그렇게 하지 않습니다. 바로 **'ORM'**이라는 기술을 사용하죠."
"첫 번째 항목, **'Why ORM? (Object-Relational Mapping)'**을 보겠습니다."
ORM을 사용하려는 문제점은 명확합니다.
**'SQL과 객체지향(TS/JS)의 '패러다임 불일치'**입니다."
우선 우리는 SQL 실습을 하면서도 느꼈지만, 사소한 오타하나도 허용되지 않는 규칙적인 문법을 따라야 합니다. 하지만 개발자는 소프트웨어 개발에 집중되어야 하는데 데이터베이스 언어까지 신경써야하는 문제가 있습니다.
이런 언어적 차이점도 하나지만 개념적인 문제도 있습니다.
현재 객체 지향 프로그래밍을 진행하면서 하나의 회원은 하나의 객체로 생각해야 합니다. 하지만 데이터베이스에서는 하나의 데이터일 것이지만 하나의 객체 인스턴스에서 한줄의 문장으로 차이점을 보이게 됩니다. 실수가 발생 할 수 있는 점이기도 하면서, 개념적으로 직관적이지 않기 때문이죠 같은것을 다루는 것인데 말이죠.
•
우리가 4일차에 다루는 NestJS(TypeScript)는 **'객체(Object)'**로 생각합니다. (예시: class User { ... })
•
하지만 우리가 사용할 MySQL은 **'관계형 데이터(테이블)'**로 생각합니다.
"그래서 Service 로직을 짤 때, 객체지향의 세계에서 갑자기 '문자열'의 세계로 넘어가야 합니다."
(예시: SELECT * FROM users WHERE id = 1 ... )
이 문자로 바라본다는 것은 단순히 개발자의 생각의 문제만이 아니라. 실제 “문자열”로 컴파일러가 생각한다는 것입니다. “이 따옴표” 안에서는 어떤 오타가 나더라도 컴파일러는 알 수 없죠. 의도된 오타인지 무엇인지 생각할 필요가 없기 때문입니다. 이 말은
"이렇게 Service 파일(detection.service.ts) 안에 SQL '문자열'이 가득 차게 되면,
1.
SELECT 오타가 나도 VS Code가 못 잡아줍니다. (컴파일 오류가 아닌 런타임 오류 발생)
2.
데이터의 관계가 복잡해지면 SQL 문자열은 끔찍하게 길어집니다.
3.
혹시나 DB를 MySQL에서 PostgreSQL로 바꾸면? 이 모든 SQL '문자열'을 다 찾아서 수정해야 합니다.
"이것이 바로 '유지보수가 어려운' 코드입니다."
"그래서 **'Solution (ORM)'**이 나왔습니다.
ORM은 'Object-Relational Mapping', 즉 '객체-관계 매핑'의 약자입니다.
이름 그대로, SQL '문자열' 없이, 오직 **'객체(Object)'와 '메서드(Method)'**만으로 DB를 조작하게 해주는 '번역기'입니다."
"예시를 보시죠.
(SQL) SELECT * FROM users WHERE id = 1
이 쿼리 문자열이,
(ORM) userRepository.find({ where: { id: 1 } })
...이렇게 바뀝니다."
"이게 우리에게 주는 장점은 명확합니다."
"코드가 'SQL'이 아닌, 100% 'TypeScript'가 됩니다.find, where 같은 '메서드'를 쓰기 때문에, whre라고 오타를 치면 VS Code가 즉시 에러를 잡아줍니다.
SQL 쿼리를 몰라도, 자바스크립트/타입스크립트의 '객체'와 '메서드'만으로 DB와 통신할 수 있게 되는 것이죠.
이것이 **'가독성'**과 **'안정성'**을 비약적으로 향상시키는 ORM의 존재 이유입니다."
이 기술은 ORM이라 하며 객체와 관계형데이터베이스를 맵핑해주는 중간역할을 합니다. 각 주요 서버 프레임워크마다 ORM이 있으며 동일한 역할을 합니다.
2. TypeORM
•
NestJS가 공식 지원하는 대표적인 TypeScript ORM.
•
2일차에 설계한 'ERD'를 'Entity 클래스'로 1:1 매핑.
ORM도 Prisma, Sequelize 등 종류가 많습니다. 왜 하필 TypeORM일까요?"
"NestJS가 공식 지원하는 대표적인 TypeScript ORM이기 때문입니다.
@nestjs/typeorm이라는 전용 모듈을 제공해서, 모듈 2에서 배운 DI(의존성 주입) 패턴과 완벽하게 호환됩니다."
"그리고 가장 중요한 점은, 2일차에 우리가 설계했던 'ERD'를 기억하시나요?
TypeORM은 2일차에 설계한 'ERD'를 'Entity 클래스'로 1:1 매핑합니다."
"2일차에 그렸던 'Detection'이라는 ERD가, 모듈 5 실습에서 @Entity() 데코레이터가 붙은 Detection 클래스로 그대로 변환될 것입니다. 즉, 2일차의 설계가 4일차의 '코드'가 되는 것이죠."
"그럼, 다음 '모듈 5' 실습에서는 이 TypeORM을 사용해서 detect-backend 프로젝트와 '진짜' MySQL DB를 연동하는 작업을 시작하겠습니다."
모듈 5: 실습 2 - DB 연동 및 Entity 구현
1. 실습 목표
•
NestJS 프로젝트와 MySQL 데이터베이스 연동.
•
TypeORM 'Entity'로 구현.
•
'Service' 로직을 실제 DB 연동 로직으로 수정.
2. TypeORM 관련 패키지 설치
•
(터미널)
•
npm install @nestjs/typeorm typeorm mysql2
3. AppModule에 TypeORM 설정
•
app.module.ts 수정.
•
TypeOrmModule.forRoot({...}) 설정 추가.
◦
(host, port, username, password, database)
◦
(entities: [detection], synchronize: true) -> 'synchronize'는 개발용 (Entity 기준으로 DB 자동 변경)
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { DetectionModule } from './detection/detection.module';
// 1. TypeOrmModule 임포트
import { TypeOrmModule } from '@nestjs/typeorm';
// 2. (중요) DB와 매핑할 Entity 클래스 임포트
import { Detection } from './detection/entities/detection.entity';
@Module({
imports: [
// 3. TypeOrmModule.forRoot() 설정 추가
TypeOrmModule.forRoot({
type: 'mysql', // DB 타입
host: 'localhost', // (DB가 설치된 주소, 보통 localhost)
port: 3306, // (MySQL 기본 포트)
username: 'root', // (PC의 MySQL 아이디)
password: '1234', // (PC의 MySQL 비밀번호)
database: 'deepfake_db', // (미리 생성해둔 DB 스키마 이름)
// 4. Entity: 이 프로젝트가 어떤 Entity들을 사용하는지 등록
entities: [Detection], // ★ Detection 엔티티 등록
// 5. synchronize: true (개발용 옵션)
synchronize: true,
logging: true, // 쿼리 로깅 여부
}),
DetectionModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
SQL
복사
4. Entity 생성 (ERD -> Code)
이제 ERD에서 설계했던 코드를 작성하게 됩니다. 여기 ORM에서 인지하는 데코레이터가 추가로 붙습니다.
•
detection.entity.ts 파일 수정.
•
@Entity()
•
@PrimaryGeneratedColumn() (id)
•
@Column() (filename, filetype)
•
@CreateDateColumn() (createdAt)
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
// 1. @Entity(): 이 클래스는 DB 테이블과 매핑되는 '엔티티'임을 선언
@Entity()
export class Detection {
// 2. @PrimaryGeneratedColumn(): 'id' 컬럼이 '기본 키(PK)'이며,
// '자동 증가(Auto-increment)' 값임을 선언
@PrimaryGeneratedColumn()
id: number;
// 3. @Column(): 'filename' 컬럼임을 선언
@Column()
filename: string;
// 4. @Column(): 'filetype' 컬럼임을 선언
@Column()
filetype: string;
// 5. @CreateDateColumn(): 데이터가 '생성'될 때의 시간을 자동으로 저장
@CreateDateColumn()
createdAt: Date;
// 6. @Column(): 'isDeepfake' 컬럼임을 선언
@Column('boolean', { default: false })
isDeepfake: boolean;
// 7. @Column(): 'confidence' 컬럼임을 선언
@Column('float', { default: 0.0 })
confidence: number;
}
SQL
복사
5. Repository 패턴 적용
•
detection.module.ts 수정:
◦
imports: [TypeOrmModule.forFeature([Detection])] (이 모듈이 Detection Entity를 쓴다고 등록)
"테이블이 준비됐습니다.
이제 detection.service.ts가 이 테이블과 통신하게 만들 차례입니다."
"1단계: 모듈에 Entity 등록"
"src/detection/detection.module.ts 파일을 엽니다.
이 DetectionModule이 Detection Entity를 사용한다는 것을 NestJS에 알려줘야 합니다.
imports 배열에 TypeOrmModule.forFeature([Detection])를 추가합니다."
import { Module } from '@nestjs/common';
import { DetectionService } from './detection.service';
import { DetectionController } from './detection.controller';
// 1. TypeOrmModule, Detection 임포트
import { Detection } from './entities/detection.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
// 2. forFeature()로 이 모듈에서 사용할 Entity를 등록
imports: [TypeOrmModule.forFeature([Detection])],
controllers: [DetectionController],
providers: [DetectionService],
})
export class DetectionModule {}
SQL
복사
•
detection.service.ts 수정 (DI):
◦
@Injectable()
◦
constructor(@InjectRepository(Detection) private detectionRepository: Repository<Detection>) {}
◦
(NestJS가 detectionRepository를 '주입'해 줌)
"2단계: Service에 Repository 주입(DI)"
"드디어 모듈 2에서 배운 DI(의존성 주입)를 사용합니다.
src/detection/detection.service.ts 파일을 열어주세요."
"constructor (생성자)를 수정해서, NestJS가 자동으로 만들어준 Detection의 Repository를 '주입'받겠습니다."
import { Injectable } from '@nestjs/common';
import { CreateDetectionDto } from './dto/create-detection.dto';
import { UpdateDetectionDto } from './dto/update-detection.dto';
// 1. TypeORM 관련 모듈 임포트
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Detection } from './entities/detection.entity';
@Injectable()
export class DetectionService {
// 2. 생성자(Constructor)에서 Repository 주입 받기
constructor(
@InjectRepository(Detection) // "Detection Entity의 Repository를 주입해주세요"
private detectionRepository: Repository<Detection>, // "주입받은 것을 this.detectionRepository 변수에 할당"
) {}
create(createDetectionDto: CreateDetectionDto) {
return 'This action adds a new detection';
}
findAll() {
return `This action returns all detection`;
}
findOne(id: number) {
return `This action returns a #${id} detection`;
}
update(id: number, updateDetectionDto: UpdateDetectionDto) {
return `This action updates a #${id} detection`;
}
remove(id: number) {
return `This action removes a #${id} detection`;
}
}
SQL
복사
"이제 this.detectionRepository라는 변수를 통해 ORM이 지원하는 자동 쿼리 메서드인 find(), save() 같은 TypeORM 메서드를 쓸 수 있게 되었습니다." 한번 실제로 동작하는지 해보려고합니다.
•
create(detection) 메서드 수정:
◦
(Before) (단순 문자)
◦
(After) const newDetection = this.detectionRepository.create(detection);
◦
(After) return this.detectionRepository.save(newDetection); (실제 DB 저장)
"3단계: '가짜' 로직을 '진짜' 로직으로 수정"
"create() 메서드와 findAll() 메서드를 수정합니다."
"먼저 create()입니다.
return '...'; 이 부분을 지우고, '비동기(async)'로 변경한 뒤, repository를 사용합니다."
import { Injectable } from '@nestjs/common';
import { CreateDetectionDto } from './dto/create-detection.dto';
import { UpdateDetectionDto } from './dto/update-detection.dto';
// 1. TypeORM 관련 모듈 임포트
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Detection } from './entities/detection.entity';
@Injectable()
export class DetectionService {
// 2. 생성자(Constructor)에서 Repository 주입 받기
constructor(
@InjectRepository(Detection) // "Detection Entity의 Repository를 주입해주세요"
private detectionRepository: Repository<Detection>, // "주입받은 것을 this.detectionRepository 변수에 할당"
) {}
// 3-1. create() 메서드 수정
async create(createDetectionDto: CreateDetectionDto): Promise<Detection> {
// (Before) return 'This action adds a new detection';
// (After)
// 1. DTO로부터 Entity 인스턴스 생성
const newDetection = this.detectionRepository.create(createDetectionDto);
// 2. DB에 저장 (INSERT 쿼리가 실행됨)
return this.detectionRepository.save(newDetection);
}
findAll() {
return `This action returns all detection`;
}
findOne(id: number) {
return `This action returns a #${id} detection`;
}
update(id: number, updateDetectionDto: UpdateDetectionDto) {
return `This action updates a #${id} detection`;
}
remove(id: number) {
return `This action removes a #${id} detection`;
}
}
SQL
복사
"설명: create()는 DTO를 받아 새 Detection 객체를 '만들고', save()는 이 객체를 DB에 '저장(INSERT)'한 뒤, 저장된 객체를 '반환'합니다."
•
findAll() 메서드 수정:
◦
(Before) This action returns all detection 단순 문자 반환
◦
(After) return this.detectionRepository.find(); (실제 DB 조회)
"다음, findAll()도 해보겠습니다.
이것도 async로 바꾸고 repository를 사용합니다."
SQL
복사
6. Postman 재테스트
•
(POST) http://localhost:3000/detection -> DBeaver/DataGrip에서 DB 확인.
•
(GET) http://localhost:3000/detection -> DB 데이터가 JSON으로 반환됨.
6. Postman 재테스트
"자, 코드를 저장하고 서버(npm run start:dev)가 재시작되는 것을 확인합니다.
이제 '진짜' DB가 연결되었습니다. Postman으로 다시 테스트해 보죠."
이전과 같이 조회를 하면 이제 문자가 나타나지 않고 빈 데이터가 나타납니다. 실제 동작이 되고 있다는 것입니다.
일단 데이터베이스에 데이터가 없기 때문에 데이터를 먼저 넣고 다시해보겠습니다.
"테스트 1: POST /detection (Create)"
•
"Postman에서 아까와 '똑같이' POST http://localhost:3000/detection 요청을 보냅니다."
•
"(JSON Body: { "filename": "my_test_image.jpg", "filetype": "image" })"
•
"Send!"
•
"결과(Response)를 보세요! 'This action...' 문자열 대신, id가 1로, createdAt이 현재 시간으로 찍힌 JSON 객체가 반환됩니다. DB에 '진짜' 데이터가 저장된 것입니다."
•
"DBeaver/DataGrip에서 detection 테이블을 새로고침해서 데이터가 들어갔는지 직접 확인해 보세요."
"테스트 2: GET /detection (Read)"
•
"이제 Postman에서 GET http://localhost:3000/detection 요청을 보냅니다."
•
"Send!"
•
"결과: This action... 문자열 대신, 방금 우리가 POST로 넣었던 id: 1 객체가 포함된 JSON 배열이 반환됩니다."
•
그리고 이런 요청을 보내는 동시에 사실 백엔드 서버에서도 계속해서 로그가 찍히고 있었습니다.
"축하합니다! 이것으로 4일차의 가장 어려운 실습을 마쳤습니다.
우리는 '가짜' API 서버를, '진짜' MySQL DB와 TypeORM으로 연동하여 '진짜' CRUD가 동작하는 API 서버로 업그레이드했습니다." 수정과 삭제도 마찬가지로 서비스 코드를 수정하면서 동작하도록 할 수 있게 됩니다.
detection.service.ts 최종
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateDetectionDto } from './dto/create-detection.dto';
import { UpdateDetectionDto } from './dto/update-detection.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Detection } from './entities/detection.entity';
@Injectable()
export class DetectionService {
// DI
constructor(
@InjectRepository(Detection)
private detectionRepository: Repository<Detection>,
) {}
// Create
async create(createDetectionDto: CreateDetectionDto): Promise<Detection> {
// 1. DTO로부터 Entity 인스턴스 생성
const newDetection = this.detectionRepository.create(createDetectionDto);
// 2. DB에 저장 (INSERT 쿼리가 실행됨)
return this.detectionRepository.save(newDetection);
}
// Find All
async findAll(): Promise<Detection[]> {
// 1. DB에서 'detection' 테이블의 모든 데이터를 조회 (SELECT * 쿼리)
return this.detectionRepository.find();
}
// Find One
async findOne(id: number): Promise<Detection> {
// 1. DB에서 'id' 컬럼을 기준으로 1개 조회 (SELECT * ... WHERE id = ...)
const detection = await this.detectionRepository.findOneBy({ id });
// 2. (에러 핸들링) 만약 해당 ID의 데이터가 없으면 404 에러 반환
if (!detection) {
throw new NotFoundException(`ID #${id}에 해당하는 탐지 내역을 찾을 수 없습니다.`);
}
// 3. 찾은 데이터 반환
return detection;
}
// Update
async update(id: number, updateDetectionDto: UpdateDetectionDto) {
// 1. 'id'로 찾아서, 'updateDetectionDto'의 내용으로 DB를 업데이트 (UPDATE 쿼리)
// (참고: 'update'는 결과로 업데이트된 Entity를 반환하지 않고, 영향 받은 행 수(Affected Rows) 등을 반환합니다.)
const updateResult = await this.detectionRepository.update(id, updateDetectionDto);
// 2. (에러 핸들링) 만약 업데이트된 행(affected)이 0개라면, 해당 ID가 없다는 뜻.
if (updateResult.affected === 0) {
throw new NotFoundException(`ID #${id}에 해당하는 탐지 내역을 찾을 수 없습니다.`);
}
// 3. 업데이트된 내용을 다시 조회하여 반환
return this.findOne(id);
}
// Delete
async remove(id: number) {
// 1. 'id'로 찾아서 DB에서 삭제 (DELETE 쿼리)
const deleteResult = await this.detectionRepository.delete(id);
// 2. (에러 핸들링) 만약 삭제된 행(affected)이 0개라면, 해당 ID가 없다는 뜻.
if (deleteResult.affected === 0) {
throw new NotFoundException(`ID #${id}에 해당하는 탐지 내역을 찾을 수 없습니다.`);
}
// 3. 성공 메시지 반환 (또는 삭제된 객체를 반환해도 됨)
return {
deleted: true,
message: `ID #${id} 탐지 내역이 성공적으로 삭제되었습니다.`,
};
}
}
SQL
복사
모듈 6: REST API 리팩토링 (이론/실습)
실습을 통해 우리는 detection.service.ts의 create, findAll (그리고 findOne, update, remove까지) 모든 CRUD 로직을 '진짜' MySQL DB와 TypeORM으로 연동하는 데 성공했습니다.
Postman으로 POST 요청을 보내면 DB에 데이터가 쌓이고, GET 요청을 하면 그 데이터가 JSON으로 반환되는, 완벽하게 '동작하는' API 서버를 만들었습니다."
"하지만, '동작하는' 서버가 '좋은' 서버, 혹은 '안전한' 서버를 의미할까요?"
"여기에는 두 가지 심각한 **'Problem'**이 숨어있습니다.
모듈 6에서는 3일차 React와 안전하게 통신하기 위해, 이 두 가지 문제를 해결하는 '리팩토링' 작업을 진행하겠습니다."
1. DTO (Data Transfer Object)
•
Problem: create(detection) 메서드가 클라이언트의 body를 그대로 받으면, 'body'에 뭐가 들었는지 알 수 없음 (타입 불안정).
"첫 번째 문제입니다. detection.controller.ts 파일을 열어보시죠.
create() 메서드를 보겠습니다."
"Nest CLI가 생성해 준 코드를 보면, @Body()의 타입으로 CreateDetectionDto가 이미 적용되어 있습니다.
사실 만약 CLI의 도움 없이 Express처럼 개발했다면, 이 코드는 아마 이렇게 생겼을 겁니다."
TypeScript
@Post()
create(@Body() body: any) { // <-- 'any' 타입!
return this.detectionService.create(body);
}
SQL
복사
위처럼 바꿔도 실제로 동작합니다. 기본 자바스크립트 개발자였으면 이렇게 코드가 작성될 가능성이 높습니다.
"body: any... any는 '무엇이든 될 수 있다'는 뜻이죠. 그럼 어떤것들이 들어와도 자동으로 인식하기 때문에 좋은것이 아닌가? 아닙니다. TypeScript가 타입 체크를 '포기'한 것입니다.
React가 filename을 보냈는지, username을 보냈는지, Service로 넘기기 전까지는 **'타입 안정성'**을 전혀 보장할 수 없습니다.
이것이 첫 번째 **Problem: '타입 불안정'**입니다."
"**'Solution'**은 CLI가 이미 적용해 준 **'DTO (Data Transfer Object)'**입니다.
DTO는 '데이터 전송 객체'의 약자로, 이름 그대로 클라이언트(React)와 서버(NestJS)가 '데이터를 주고받을 때 사용할 객체의 모양(Type/Class)'을 **'명시적으로 정의'**하는 것입니다." 규칙을 만들겠다라는 것이죠.
"Nest CLI가 src/detection/dto/create-detection.dto.ts 파일을 이미 생성해 줬습니다.
이 DTO 클래스 덕분에, 우리 Controller는 @Body() createDetectionDto: CreateDetectionDto처럼 '나는 CreateDetectionDto 모양이 아니면 받지 않겠다'라고 **'타입 계약'**을 할 수 있게 된 것입니다."
하지만 DTO는 있지만, 동작하지 않습니다. 실제로 숫자를 넣어보면 그냥 숫자로 변합니다.
DTO안에 타입이 정의되어 있지 않기 때문입니다. CLI는 추천 해줄뿐 개발자를 강요할 수 없기 때문에 껍데기정도를 작성해주는 것을 권장한 것입니다.
파일 이름은 문자열, 파일 타입도 문자열만 받도록 제한해보겠습니다.
export class CreateDetectionDto {
filename: string;
filetype: string;
TypeScript
복사
타입의 안정성을 확보한것 같지만, 숫자로 그냥 보내도 아직도 그대로 동작합니다.
•
Solution (DTO): '데이터 전송 객체'
◦
create-detection.dto.ts (CLI가 이미 생성함)
◦
클라이언트가 보내야 할 '데이터의 모양(타입/클래스)'을 명시.
•
detection.controller.ts 수정:
◦
(Before) @Body() body: any
◦
(After) @Body() createDetectionDto: CreateDetectionDto
2. Pipe (ValidationPipe)
•
Problem: DTO를 썼지만, 클라이언트가 filename을 빼먹거나, filetype을 숫자로 보내도 서버가 받음.
2. Pipe (ValidationPipe)
"좋습니다. DTO를 써서 '타입 안정성'은 확보했습니다.
그런데, 더 큰 문제가 남아있습니다.
캔버스의 두 번째 **'Problem'**을 보시죠.
'DTO를 썼지만, 클라이언트가 filename을 빼먹거나, filetype을 숫자로 보내도 서버가 받음.'"
"create-detection.dto.ts 파일은 현재 텅 빈 클래스(export class CreateDetectionDto {})입니다.
그래서 React가 POST 요청을 보낼 때, {} (빈 객체)를 보내도, CreateDetectionDto 타입이 맞긴 맞으니까 Controller를 통과해 버립니다!
결국 Service는 filename이 undefined인 상태로 this.detectionRepository.create(...)를 호출하고, DB는 NOT NULL 제약 조건 위배로 **'런타임 에러'**를 뿜게 됩니다."
"우리는 '교통 경찰'인 Controller 단계에서, 즉 'Service'에 도달하기 전에 이 잘못된 데이터를 '검증(Validate)'하고 차단해야 합니다."
"이 **'Solution'**이 바로 **'Pipe (ValidationPipe)'**입니다.
NestJS의 'Pipe'는 Controller 핸들러가 실행되기 직전에 데이터를 '변환'하거나 '검증'하는 미들웨어입니다."
•
npm install class-validator class-transformer
•
Solution (Pipe): 컨트롤러에 도달하기 전 '데이터 변환/검증'
•
npm install class-validator class-transformer
더 강력하게 유효성 검사를 하기위한 라이브러리를 설치하고, 조건을 세밀하게 만들어 보겠습니다. 우선 "1단계: 패키지 설치"
"Pipe는 class-validator와 class-transformer라는 두 라이브러리가 필요합니다.
•
class-validator: @IsString() 같은 '데코레이터'를 제공합니다.
•
class-transformer: Controller로 들어온 순수 JSON(Object)을, 우리가 정의한 CreateDetectionDto **'클래스 인스턴스'**로 변환해 줍니다.
전역으로 유효성 검사를 적용하기 위해 설정을 추가합니다.
•
main.ts에 app.useGlobalPipes(new ValidationPipe()) 추가.
•
create-detection.dto.ts 수정:
◦
@IsString()
◦
@IsNotEmpty() 같은 '데코레이터' 추가.
우선 글로벌 설정을 진행합니다.
main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
// 1. ValidationPipe 임포트
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 2. Global Pipe로 ValidationPipe 추가
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // DTO에 정의되지 않은 속성은 자동으로 제거
transform: true, // 요청 데이터를 DTO 클래스 인스턴스로 변환
}));
await app.listen(3000);
}
bootstrap();
TypeScript
복사
DTO에 세부적인 데코레이터로 유효성검사 조건을 추가해보겠습니다.
•
결과: 클라이언트가 filename을 빼먹고 요청하거나, 문자가 아니거나 등 제한조건과 다르면, NestJS가 400 Bad Request 에러를 '자동으로' 반환함.
실행 테스트
요약 및 5일차 예고
•
4일차 성과:
◦
NestJS로 'RESTful API 서버' 구축 완료.
◦
2일차 ERD(simple_erd.html) [cite: 1-61]를 TypeORM Entity로 구현.
◦
Postman으로 '실제 DB'에 CRUD가 되는 것을 확인.
◦
DTO/Pipe로 '안정적인' API 엔드포인트 완성.
•
5일차 예고 (AI 모델 분석):
◦
4일차에 만든 NestJS는 '메인 서버'임.
◦
5일차에는 'AI 모델'을 서빙할 별도의 **'Python (FastAPI) 서버'**를 구축할 것임.
◦
3일차(React) -> 4일차(NestJS) -> 5일차(FastAPI)로 이어지는 풀스택 아키텍처의 마지막 조각을 맞출 준비.
Related Posts
Search






















