📋 학습 목차 — 이 글을 다 읽으면 이런 것들을 할 수 있습니다
📡 Chapter 01
REST API란? — 기초부터 완벽하게
"REST를 모르면 백엔드 개발자가 될 수 없습니다. REST는 인터넷의 언어입니다."
🍕 REST가 뭔지 피자 가게로 이해해봅시다
REST(Representational State Transfer)는 웹 서비스를 만들기 위한 설계 원칙(아키텍처 스타일)입니다. 어렵게 느껴지지만, 피자 가게를 예로 들면 완전히 이해할 수 있습니다.
🍕 피자 가게 REST API 예시
피자 가게(서버)에 손님(클라이언트)이 주문하는 상황을 API로 표현하면:GET /pizzas → 피자 메뉴 전체 보여줘GET /pizzas/1 → 1번 피자(페퍼로니) 정보 보여줘POST /orders → 피자 주문할게PUT /orders/5 → 5번 주문 내용 전체 변경할게PATCH /orders/5 → 5번 주문에서 음료만 바꿀게DELETE /orders/5 → 5번 주문 취소할게
📏 REST의 6가지 제약 조건 (아키텍처 원칙)
🔢 HTTP 상태 코드 완전 정복 — 현업에서 자주 쓰는 것들
| 코드 | 의미 | 사용 시나리오 |
|---|---|---|
| 200 | OK | GET, PUT, PATCH 성공 |
| 201 | Created | POST로 리소스 생성 성공. Location 헤더에 새 리소스 URL 포함 |
| 204 | No Content | DELETE 성공. 응답 본문 없음 |
| 400 | Bad Request | 요청 데이터 형식/값이 잘못됨 (유효성 검사 실패) |
| 401 | Unauthorized | 인증 실패 (로그인 필요, 토큰 없음/만료) |
| 403 | Forbidden | 인가 실패 (로그인은 됐지만 권한 없음) |
| 404 | Not Found | 리소스가 존재하지 않음 |
| 409 | Conflict | 충돌 (이미 존재하는 이메일로 가입 시도 등) |
| 422 | Unprocessable Entity | 형식은 맞지만 비즈니스 로직 검증 실패 |
| 500 | Internal Server Error | 서버 내부 오류 (예상치 못한 예외) |
| 503 | Service Unavailable | 서버 과부하 또는 점검 중 |
💻 REST API 실전 코드 — NestJS로 구현
// 1. 기본 CRUD REST API (NestJS)
// pizzas.controller.ts
@Controller('pizzas')
export class PizzasController {
constructor(private readonly pizzasService: PizzasService) {}
// GET /pizzas?page=1&limit=10&sort=name
@Get()
findAll(@Query() query: FindAllPizzasDto) {
return this.pizzasService.findAll(query);
}
// GET /pizzas/:id
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.pizzasService.findOne(id);
}
// POST /pizzas
@Post()
@HttpCode(HttpStatus.CREATED)
create(@Body() createPizzaDto: CreatePizzaDto) {
return this.pizzasService.create(createPizzaDto);
}
// PATCH /pizzas/:id (부분 수정)
@Patch(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body() updatePizzaDto: UpdatePizzaDto,
) {
return this.pizzasService.update(id, updatePizzaDto);
}
// DELETE /pizzas/:id
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id', ParseIntPipe) id: number) {
return this.pizzasService.remove(id);
}
}
🏗️ Chapter 02
REST API 고급 설계 패턴 — 현업 수준으로 올리기
"URL 설계 하나로 API의 품질이 결정됩니다"
✅ REST API URL 설계 황금 규칙
| 규칙 | ❌ 나쁜 예 | ✅ 좋은 예 |
|---|---|---|
| 소문자 사용 | /GetUsers | /users |
| 명사 사용 (동사 X) | /getUser/createOrder | /users /orders |
| 복수형 사용 | /user/order | /users /orders |
| 계층 관계 표현 | /getUserOrders?userId=1 | /users/1/orders |
| 하이픈 사용 (언더바 X) | /product_categories | /product-categories |
| 확장자 미포함 | /users.json | /users (Accept 헤더 사용) |
| 행위는 HTTP 메서드로 | /users/1/delete | DELETE /users/1 |
💻 페이지네이션, 필터링, 정렬 구현 패턴
// 커서 기반 페이지네이션 (대용량 데이터에 적합)
// 오프셋 방식: GET /users?page=2&limit=20 (1억 건 이상 시 느림)
// 커서 방식: GET /users?cursor=eyJpZCI6MTAwfQ&limit=20 (항상 빠름)
// Response 표준화 예시
{
"data": [...],
"meta": {
"total": 1000,
"page": 1,
"limit": 20,
"hasNext": true,
"nextCursor": "eyJpZCI6MjB9"
},
"links": {
"self": "/users?cursor=abc&limit=20",
"next": "/users?cursor=eyJpZCI6MjB9&limit=20"
}
}
// 필터링: GET /products?category=pizza&minPrice=10&maxPrice=30&sort=-price
// sort=-price 는 가격 내림차순 (마이너스 = 내림차순 컨벤션)
// 에러 응답 표준화 (RFC 7807 Problem Details)
{
"type": "https://api.example.com/errors/not-found",
"title": "Resource Not Found",
"status": 404,
"detail": "Pizza with id 999 does not exist",
"instance": "/pizzas/999"
}
🎯 HATEOAS — REST의 완성형
HATEOAS(Hypermedia As The Engine Of Application State)는 REST의 가장 성숙한 단계입니다. 응답에 다음에 할 수 있는 행동의 링크를 포함시켜, 클라이언트가 API 문서 없이도 탐색할 수 있게 합니다.
// 일반 REST 응답
{ "id": 1, "status": "PENDING", "amount": 25000 }
// HATEOAS 응답 — 다음 행동까지 알려줌
{
"id": 1,
"status": "PENDING",
"amount": 25000,
"_links": {
"self": { "href": "/orders/1" },
"cancel": { "href": "/orders/1/cancel", "method": "POST" },
"pay": { "href": "/orders/1/payment", "method": "POST" },
"customer": { "href": "/customers/42" }
}
}
🔗 Chapter 03
GraphQL 기초 완전 이해 — 왜 Facebook이 만들었을까?
"REST의 Over-fetching, Under-fetching 문제를 해결하기 위해 태어났습니다"
😫 REST의 문제점 — 왜 GraphQL이 필요한가?
❌ Over-fetching 문제
사용자 이름만 필요한데 전체 정보를 다 받아오는 문제GET /users/1
필요: name
받는 것: id, name, email, phone, address, birthdate, avatar, createdAt... 다 받음!
→ 불필요한 데이터 전송 → 느린 응답
❌ Under-fetching 문제
한 화면을 위해 여러 API를 여러 번 호출해야 하는 문제
유저 프로필 페이지 구성 시:
1) GET /users/1 (유저 정보)
2) GET /users/1/posts (게시글)
3) GET /users/1/followers (팔로워)
→ 3번의 API 호출 필요 → N+1 문제
✅ GraphQL의 해결책
# 단 1번의 요청으로 필요한 것만 정확히!
query GetUserProfile {
user(id: 1) {
name # 이것만 필요!
posts {
title
createdAt
}
followers {
name
avatar
}
}
}
📐 GraphQL 핵심 개념 3총사
📜 GraphQL Schema 설계 완전 가이드
Schema는 GraphQL의 핵심입니다. 마치 계약서처럼, 클라이언트와 서버가 주고받을 데이터의 형태와 규칙을 정의합니다.
# Schema Definition Language (SDL)
type User {
id: ID! # ! = Non-null (필수)
name: String!
email: String!
age: Int # ! 없으면 null 가능
role: UserRole! # Enum 타입
posts: [Post!!]! # Post 배열, 배열도 Non-null
createdAt: String!
}
enum UserRole {
ADMIN
USER
MODERATOR
}
type Post {
id: ID!
title: String!
content: String!
author: User! # 중첩 타입
tags: [String!!]!
publishedAt: String
}
# Query 타입 — 읽기 작업 정의
type Query {
user(id: ID!): User
users(
page: Int = 1
limit: Int = 20
role: UserRole
): [User!!]!
post(id: ID!): Post
searchPosts(keyword: String!): [Post!!]!
}
# Mutation 타입 — 쓰기 작업 정의
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
createPost(input: CreatePostInput!): Post!
}
# Input 타입 — Mutation 입력 정의
input CreateUserInput {
name: String!
email: String!
password: String!
}
input UpdateUserInput {
name: String # 모두 선택적 (일부만 수정 가능)
email: String
}
⚙️ Chapter 04
GraphQL 심화 — Resolver, DataLoader, 성능
"Resolver는 GraphQL의 엔진입니다. 잘 만들면 날고 못 만들면 기어갑니다"
⚙️ Resolver 완전 이해 — 각 필드의 요리사
Resolver는 GraphQL 쿼리의 각 필드 값을 어떻게 가져올지 정의하는 함수입니다. 마치 레스토랑의 각 요리사처럼, 각자 맡은 요리(필드)를 만드는 역할입니다.
// NestJS + GraphQL Resolver 구현
@Resolver(() => User)
export class UsersResolver {
constructor(
private usersService: UsersService,
private postsService: PostsService,
) {}
// Query Resolver: user(id: "1") 처리
@Query(() => User, { nullable: true })
async user(@Args('id') id: string): Promise<User | null> {
return this.usersService.findOne(id);
}
// Mutation Resolver: createUser(...) 처리
@Mutation(() => User)
async createUser(
@Args('input') input: CreateUserInput,
@Context() ctx: GqlContext, // 인증 정보 등 컨텍스트
): Promise<User> {
return this.usersService.create(input);
}
// Field Resolver: user.posts 필드 처리
@ResolveField(() => [Post])
async posts(@Parent() user: User): Promise<Post[]> {
// ⚠️ N+1 문제 발생 지점!
// 10명의 유저를 조회하면 posts도 10번 쿼리됨
return this.postsService.findByUserId(user.id);
}
}
🔥 DataLoader — N+1 문제 완전 해결
문제 상황: 게시글 10개 목록 조회 시, 각 게시글의 작성자 정보를 가져오기 위해 10번의 쿼리가 발생 (N+1 = 1+10 = 11번)
// DataLoader 구현 (NestJS)
@Injectable()
export class UserDataLoader {
private loader: DataLoader<string, User>;
constructor(private usersService: UsersService) {
this.loader = new DataLoader<string, User>(
async (userIds: string[]) => {
// 10번 개별 조회 대신 한 번에 IN 쿼리!
// SELECT * FROM users WHERE id IN (1,2,3,...,10)
const users = await this.usersService.findByIds(userIds);
// ID 순서대로 반환 (DataLoader 요구사항)
return userIds.map(id => users.find(u => u.id === id));
},
{
batch: true, // 배치 처리
cache: true, // 같은 요청 내 캐싱
maxBatchSize: 100,
}
);
}
load(id: string) { return this.loader.load(id); }
loadMany(ids: string[]) { return this.loader.loadMany(ids); }
}
// Resolver에서 DataLoader 사용
@ResolveField(() => User)
async author(@Parent() post: Post): Promise<User> {
// 이제 자동으로 배치 처리됨!
return this.userDataLoader.load(post.authorId);
// 11번 → 2번으로 줄어듦!
// 1) SELECT posts (1번)
// 2) SELECT users WHERE id IN (...) (1번)
}
🔄 Chapter 05
GraphQL Subscription — 실시간 통신 구현
"채팅, 알림, 실시간 대시보드 — Subscription으로 만들어봅니다"
📡 Subscription 작동 원리
Subscription은 WebSocket을 통해 서버에서 클라이언트로 실시간 이벤트를 전달합니다. 마치 유튜브 알림 구독처럼, "이 이벤트 발생하면 알려줘"를 서버에 등록하는 개념입니다.
// NestJS Subscription 구현
@Resolver(() => Message)
export class MessagesResolver {
constructor(
private messagesService: MessagesService,
@Inject('PUB_SUB') private pubSub: PubSub,
) {}
// 메시지 전송 Mutation
@Mutation(() => Message)
async sendMessage(
@Args('input') input: SendMessageInput,
): Promise<Message> {
const message = await this.messagesService.create(input);
// 구독자들에게 이벤트 발행
await this.pubSub.publish(`MESSAGE_SENT_${input.roomId}`, {
messageSent: message,
});
return message;
}
// Subscription 정의
@Subscription(() => Message, {
filter: (payload, variables) => {
// 같은 채팅방의 메시지만 받기
return payload.messageSent.roomId === variables.roomId;
},
})
messageSent(@Args('roomId') roomId: string) {
return this.pubSub.asyncIterator(`MESSAGE_SENT_${roomId}`);
}
}
// 클라이언트 쿼리
subscription OnMessageSent($roomId: String!) {
messageSent(roomId: $roomId) {
id
content
sender { name avatar }
createdAt
}
}
⚖️ Chapter 06
REST vs GraphQL vs gRPC — 완전 비교
"언제 뭘 써야 할지 알면 진짜 백엔드 개발자입니다"
🔥 3대 API 기술 완전 비교표
| 구분 | 📡 REST | 🔗 GraphQL | ⚡ gRPC |
|---|---|---|---|
| 데이터 형식 | JSON | JSON | Protocol Buffers (바이너리) |
| 엔드포인트 | 여러 개 (/users, /posts...) | 단일 (/graphql) | 서비스 메서드 |
| Over/Under Fetching | ⚠️ 발생 가능 | ✅ 없음 | ⚠️ 발생 가능 |
| 성능 | 보통 | 보통~좋음 | 🚀 매우 빠름 (2~10배) |
| 실시간 | SSE 또는 별도 WS | ✅ Subscription | ✅ Streaming |
| 학습 난이도 | ⭐ 쉬움 | ⭐⭐⭐ 보통 | ⭐⭐⭐⭐ 어려움 |
| 캐싱 | ✅ HTTP 캐시 기본 | ⚠️ 별도 구현 필요 | ⚠️ 별도 구현 필요 |
| 주요 사용처 | 범용 웹 API | 복잡한 클라이언트 | 서비스 간 통신 |
| 대표 사용 기업 | 대부분의 서비스 | GitHub, Shopify, Twitter | Google, Netflix, Uber |
📡 REST를 선택하세요
- 단순한 CRUD API
- 브라우저 캐싱 활용 필요
- 팀이 REST에 익숙
- 공개 API 제공
- 파일 업로드/다운로드
🔗 GraphQL을 선택하세요
- 모바일 앱 (데이터 절약)
- 복잡한 관계형 데이터
- 다양한 클라이언트 지원
- 빠른 프로토타이핑
- 실시간 + REST 동시 필요
⚡ gRPC를 선택하세요
- 마이크로서비스 간 통신
- 낮은 레이턴시 필수
- 대용량 데이터 스트리밍
- 다언어 지원 필요
- 엄격한 스키마 계약
⚡ gRPC 기초 — Protocol Buffers 이해
// user.proto (스키마 정의)
syntax = "proto3";
package user;
service UserService {
rpc GetUser (GetUserRequest) returns (UserResponse) {}
rpc CreateUser (CreateUserRequest) returns (UserResponse) {}
// 서버 스트리밍 RPC
rpc ListUsers (ListUsersRequest) returns (stream UserResponse) {}
}
message GetUserRequest {
string id = 1;
}
message UserResponse {
string id = 1;
string name = 2;
string email = 3;
int64 createdAt = 4;
}
// JSON vs Protobuf 크기 비교
// JSON: { "id": "1", "name": "Kim" } → 28 bytes
// Protobuf: 바이너리 인코딩 → ~10 bytes (3배 이상 작음!)
🔒 Chapter 07
API 보안 완전 정복 — 인증과 인가
"보안이 빠진 API는 열쇠 없이 집 문만 달아놓은 것과 같습니다"
🔐 인증(Authentication) vs 인가(Authorization)
🪪 인증 (Authentication)
"너가 누구냐?"
예: 로그인 → 신분 확인
회사 건물 들어갈 때 직원증 보여주는 것
방법: JWT, Session, OAuth2
🛡️ 인가 (Authorization)
"너 여기 들어올 권한 있냐?"
예: 관리자만 접근 가능한 API
직원증 있어도 사장실엔 못 들어가는 것
방법: RBAC, ABAC, ACL
🔑 JWT (JSON Web Token) 완전 마스터
// JWT 구조: Header.Payload.Signature
// eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiIxMjMifQ.SIGNATURE
// NestJS JWT 전체 구현
// 1. auth.service.ts
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
) {}
async login(email: string, password: string) {
const user = await this.usersService.findByEmail(email);
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
throw new UnauthorizedException('Invalid credentials');
}
const payload = { sub: user.id, email: user.email, role: user.role };
return {
accessToken: this.jwtService.sign(payload, { expiresIn: '15m' }),
refreshToken: this.jwtService.sign(payload, { expiresIn: '7d' }),
};
}
async refreshToken(refreshToken: string) {
try {
const payload = this.jwtService.verify(refreshToken);
const newAccessToken = this.jwtService.sign(
{ sub: payload.sub, email: payload.email, role: payload.role },
{ expiresIn: '15m' }
);
return { accessToken: newAccessToken };
} catch (e) {
throw new UnauthorizedException('Invalid refresh token');
}
}
}
// 2. jwt.guard.ts - API 보호
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
return super.canActivate(context);
}
}
// 3. roles.guard.ts - RBAC 권한 체크
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) return true;
const { user } = context.switchToHttp().getRequest();
return roles.includes(user.role);
}
}
// 4. 사용 예시
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('ADMIN')
@Delete(':id')
deleteUser(@Param('id') id: string) {
return this.usersService.remove(id);
// ADMIN만 호출 가능!
}
🔐 OAuth 2.0 + Social Login 구현
OAuth 2.0은 "다른 서비스의 내 권한을 빌려주는" 프로토콜입니다. "구글로 로그인" 버튼이 OAuth 2.0의 구현체입니다.
// NestJS Google OAuth 구현 (Passport.js)
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor(private authService: AuthService) {
super({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/auth/google/callback',
scope: ['email', 'profile'],
});
}
async validate(accessToken, refreshToken, profile) {
const { id, emails, name, photos } = profile;
return this.authService.findOrCreateGoogleUser({
googleId: id,
email: emails[0].value,
name: `${name.givenName} ${name.familyName}`,
avatar: photos[0].value,
});
}
}
🚀 Chapter 08
API 성능 최적화 전략 — 빠른 API 만들기
"100ms 응답 vs 1000ms 응답 — 사용자 이탈률이 7배 차이납니다"
⚡ Rate Limiting — API 남용 방지
Rate Limiting은 특정 시간 내 API 호출 횟수를 제한합니다. DDoS 공격 방지, 서버 보호에 필수입니다.
// NestJS Rate Limiting 설정
// npm install @nestjs/throttler
// app.module.ts
@Module({
imports: [
ThrottlerModule.forRoot([
{
name: 'short',
ttl: 1000, // 1초
limit: 3, // 1초에 3회
},
{
name: 'medium',
ttl: 60000, // 1분
limit: 20, // 1분에 20회
},
{
name: 'long',
ttl: 60 * 60 * 1000, // 1시간
limit: 100, // 1시간에 100회
},
]),
],
providers: [
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
})
// 특정 엔드포인트에만 적용
@Throttle({ default: { limit: 5, ttl: 60000 } })
@Post('login')
login() { ... }
// Rate Limit 초과 시 응답
// HTTP 429 Too Many Requests
// Retry-After: 60 (헤더로 대기 시간 전달)
💾 Redis 캐싱으로 API 10배 빠르게
// Cache-Aside 패턴 구현
@Injectable()
export class ProductsService {
constructor(
@InjectRepository(Product) private repo: Repository<Product>,
@Inject(CACHE_MANAGER) private cacheManager: Cache,
) {}
async findOne(id: string) {
const cacheKey = `product:${id}`;
// 1. 캐시 확인
const cached = await this.cacheManager.get(cacheKey);
if (cached) {
return cached; // 캐시 히트! DB 조회 없음
}
// 2. DB 조회
const product = await this.repo.findOne({ where: { id } });
if (!product) {
throw new NotFoundException();
}
// 3. 캐시 저장 (1시간)
await this.cacheManager.set(cacheKey, product, 3600);
return product;
}
async update(id: string, dto: UpdateProductDto) {
await this.repo.update(id, dto);
// 데이터 변경 시 캐시 무효화
await this.cacheManager.del(`product:${id}`);
return this.findOne(id);
}
}
// 성능 비교
// DB 직접 조회: ~50ms (네트워크 + 쿼리)
// Redis 캐시: ~1ms (50배 빠름!)
📝 Chapter 09
API 버전 관리 & 문서화 — Swagger 완전 정복
"좋은 문서 없는 좋은 API는 없습니다"
📌 API 버전 관리 3가지 방법
| 방법 | 예시 | 장점 | 단점 |
|---|---|---|---|
| URI 버전 | /api/v1/users /api/v2/users |
직관적, 구현 쉬움, 캐싱 용이 | URL이 지저분해짐 |
| 헤더 버전 | API-Version: 2 | URL 깔끔, RESTful | 브라우저 테스트 어려움 |
| 미디어 타입 버전 | Accept: application/vnd.api+json;version=2 | 가장 RESTful | 복잡, 잘 안 씀 |
📖 Swagger/OpenAPI 문서 자동화 (NestJS)
// main.ts — Swagger 설정
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('Pizza API')
.setDescription('The Pizza API documentation')
.setVersion('1.0')
.addBearerAuth() // JWT 인증 UI 추가
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/docs', app, document); // /api/docs로 접근
await app.listen(3000);
}
// Controller — 상세 문서화
@ApiTags('pizzas') // 태그 그룹화
@ApiBearerAuth() // JWT 필요 표시
@Controller('pizzas')
export class PizzasController {
@ApiOperation({ summary: '피자 목록 조회', description: '전체 피자 메뉴를 페이지네이션으로 조회합니다' })
@ApiQuery({ name: 'page', required: false, type: Number, description: '페이지 번호' })
@ApiResponse({ status: 200, description: '성공', type: [Pizza] })
@ApiResponse({ status: 401, description: '인증 필요' })
@Get()
findAll(@Query() query: FindAllDto) {
return this.pizzasService.findAll(query);
}
}
// DTO — 모델 문서화
export class CreatePizzaDto {
@ApiProperty({ example: '페퍼로니 피자', description: '피자 이름' })
@IsString()
@MinLength(2)
name: string;
@ApiProperty({ example: 18000, description: '가격 (원)', minimum: 1000 })
@IsNumber()
@Min(1000)
price: number;
}
🏭 Chapter 10
API Gateway & MSA 패턴
"마이크로서비스 시대의 API 관리는 Gateway가 담당합니다"
🏗️ API Gateway 역할
API Gateway는 모든 클라이언트 요청을 받아 적절한 마이크로서비스로 라우팅하는 단일 진입점입니다. 공항의 게이트처럼, 모든 탑승객(요청)이 각 게이트(서비스)로 이동하기 전에 통과하는 곳입니다.
/api/users → User Service
/api/orders → Order Service
JWT 검증 일원화
각 서비스 부담 감소
서비스별 제한
DDoS 방어
여러 인스턴스에
트래픽 분산
장애 서비스 격리
연쇄 장애 방지
응답 캐시로
서비스 부하 감소
# AWS API Gateway + Lambda 구성
# serverless.yml
service: pizza-api
provider:
name: aws
runtime: nodejs18.x
functions:
getUsers:
handler: src/users/get.handler
events:
- httpApi:
path: /v1/users
method: GET
authorizer:
name: jwtAuthorizer # JWT 검증 Lambda
createOrder:
handler: src/orders/create.handler
events:
- httpApi:
path: /v1/orders
method: POST
authorizer:
name: jwtAuthorizer
🧪 Chapter 11
API 테스팅 전략 — 견고한 API 만들기
"테스트 없는 코드는 시한폭탄입니다"
🔺 테스트 피라미드 전략
// NestJS API 통합 테스트 (Supertest)
describe('PizzasController (e2e)', () => {
let app: INestApplication;
let accessToken: string;
beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
// 로그인해서 토큰 획득
const loginRes = await request(app.getHttpServer())
.post('/auth/login')
.send({ email: 'test@test.com', password: 'password' });
accessToken = loginRes.body.accessToken;
});
it('GET /pizzas — 인증 없이 401 반환', () => {
return request(app.getHttpServer())
.get('/pizzas')
.expect(401);
});
it('GET /pizzas — 인증 후 200 반환 + 목록 확인', () => {
return request(app.getHttpServer())
.get('/pizzas')
.set('Authorization', `Bearer ${accessToken}`)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(Array.isArray(res.body.data)).toBeTruthy();
});
});
it('POST /pizzas — 피자 생성 성공 + 201 반환', async () => {
const createDto = { name: '테스트 피자', price: 18000 };
const res = await request(app.getHttpServer())
.post('/pizzas')
.set('Authorization', `Bearer ${accessToken}`)
.send(createDto)
.expect(201);
expect(res.body.name).toBe(createDto.name);
expect(res.body.id).toBeDefined();
});
afterAll(async () => await app.close());
});
🎯 Chapter 12
면접 Q&A + 실무 체크리스트
"이 글을 다 읽었다면 면접에서 당당히 답할 수 있습니다"
💬 면접 단골 질문 30선 — 모범 답변 포함
PATCH: 리소스 부분 수정. 전송한 필드만 변경됩니다.
예시: 사용자 전화번호만 바꾸고 싶을 때
PUT - 이름, 이메일, 전화번호 모두 보내야 함
PATCH - 전화번호만 보내면 됨 (더 효율적)
예: 10개 게시글 조회 후 각 작성자 조회 = 1+10 = 11번 쿼리
해결책: DataLoader 패턴 사용
- 같은 요청 내 동일 타입 ID들을 모아(배치) 한 번에 조회
- 중복 조회 방지 (캐싱)
- 결과: 11번 → 2번으로 감소
JWT: 클라이언트에 상태 저장 (Stateless). 서버 부담 없음. 토큰 탈취 시 만료 전까지 취소 불가
현업 선택 기준:
- 단일 서버, 보안 중요: Session
- 분산 서버, MSA, 모바일: JWT + Refresh Token Rotation
멱등 메서드: GET, PUT, DELETE, HEAD
비멱등 메서드: POST
예: DELETE /users/1을 10번 호출해도 결과는 동일 (첫 번째는 삭제, 나머지는 404)
반면 POST /orders를 10번 호출하면 10개의 주문이 생성됨
실무 적용: 결제 API에서 Idempotency-Key 헤더로 중복 결제 방지
GraphQL 선택: 다양한 클라이언트(모바일/웹), 복잡한 관계 데이터, 실시간 + 쿼리 동시 필요
현업에서는 두 가지를 혼용하기도 합니다:
- 공개 API: REST (캐싱, 단순성)
- 내부 대시보드: GraphQL (복잡한 데이터 요구사항)
예: frontend.com에서 api.example.com 호출 시 브라우저가 차단 → CORS 헤더로 허용 설정
NestJS 설정:
app.enableCors({ origin: ['https://frontend.com'], credentials: true })✅ 현업 API 개발 체크리스트
🎨 설계 단계
☐ 에러 응답 표준화 (RFC 7807)
☐ 페이지네이션 전략 결정
☐ API 버전 관리 계획
☐ 인증/인가 방식 설계
🔒 보안 단계
☐ Refresh Token 안전하게 저장
☐ Rate Limiting 설정
☐ CORS 화이트리스트 설정
☐ Input 유효성 검사 (class-validator)
⚡ 성능 단계
☐ Redis 캐싱 전략 적용
☐ N+1 문제 해결 (DataLoader/Join)
☐ 응답 압축 (gzip)
☐ Connection Pooling 설정
📝 문서화 단계
☐ 에러 코드 목록 문서화
☐ API 변경 이력 관리
☐ 예제 요청/응답 포함
☐ E2E 테스트 작성
BackendDevGuide0011 완성!
GraphQL과 REST API 고급 설계 완전 정복 A-Z
REST API부터 GraphQL, gRPC, 보안, 성능, 테스팅까지
현업 백엔드 개발자가 알아야 할 모든 것을 담았습니다. 💪