Guider/Backend/BackendDevGuide0011
Backend#11

BackendDevGuide0011

GraphQL과 REST API 고급 설계 완전 정복

 
BackendDevGuide · Episode 11
🔗

GraphQL과 REST API
고급 설계 완전 정복

A부터 Z까지 — 현업에서 바로 쓰는 API 설계 완전 가이드

📡 REST API 🔗 GraphQL ⚡ gRPC 🔒 보안 🚀 성능
📚
12
핵심 챕터
💻
50+
실전 코드
🎯
100%
현업 적용
A-Z
완전 가이드

📋 학습 목차 — 이 글을 다 읽으면 이런 것들을 할 수 있습니다

📡 Chapter 01 · REST API란? 기초부터 설계 원칙
🏗️ Chapter 02 · REST API 고급 설계 패턴
🔗 Chapter 03 · GraphQL 기초 완전 이해
⚙️ Chapter 04 · GraphQL 심화 — Resolver, DataLoader
🔄 Chapter 05 · GraphQL Subscription 실시간 통신
⚖️ Chapter 06 · REST vs GraphQL vs gRPC 완전 비교
🔒 Chapter 07 · API 보안 — 인증/인가 완전 정복
🚀 Chapter 08 · API 성능 최적화 전략
📝 Chapter 09 · API 버전 관리와 문서화
🏭 Chapter 10 · API Gateway와 MSA 패턴
🧪 Chapter 11 · API 테스팅 전략
🎯 Chapter 12 · 면접 Q&A + 실무 체크리스트

📡 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가지 제약 조건 (아키텍처 원칙)

🖥️
1. Client-Server
UI(클라이언트)와 데이터(서버)를 분리. 각자 독립적으로 발전 가능
📦
2. Stateless
서버는 클라이언트 상태를 저장하지 않음. 각 요청은 독립적으로 처리
💾
3. Cacheable
응답은 캐시 가능/불가능 명시. 캐시로 성능 향상
🌐
4. Uniform Interface
일관된 인터페이스. 리소스 기반 URL, HTTP 메서드 표준 사용
🏗️
5. Layered System
계층화 구조. 클라이언트는 중간 서버(게이트웨이, 로드밸런서) 존재를 몰라도 됨
📜
6. Code on Demand
(선택) 서버가 실행 코드를 클라이언트에 전송 가능 (JavaScript 등)

🔢 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총사

📖
Query
데이터 읽기 (GET과 동일). 여러 리소스를 한 번에 조회
✏️
Mutation
데이터 변경 (POST/PUT/DELETE). 생성, 수정, 삭제
🔔
Subscription
실시간 업데이트 (WebSocket). 채팅, 알림

📜 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번)
}
DataLoader 없이
11번
1(게시글) + 10(각 작성자)
DataLoader 적용 후
2번
1(게시글) + 1(작성자 일괄)

🔄 Chapter 05

GraphQL Subscription — 실시간 통신 구현

"채팅, 알림, 실시간 대시보드 — Subscription으로 만들어봅니다"

📡 Subscription 작동 원리

Subscription은 WebSocket을 통해 서버에서 클라이언트로 실시간 이벤트를 전달합니다. 마치 유튜브 알림 구독처럼, "이 이벤트 발생하면 알려줘"를 서버에 등록하는 개념입니다.

🔄 Subscription 흐름
1. 클라이언트 구독 시작
2. WebSocket 연결
3. 이벤트 발생
4. 서버→클라이언트 Push
// 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의 구현체입니다.

🔄 OAuth 2.0 Authorization Code Flow
1 사용자가 "구글로 로그인" 버튼 클릭
2 서버 → 구글 인증 서버로 리다이렉트 (client_id, scope, redirect_uri 포함)
3 사용자가 구글에서 로그인 + 권한 승인
4 구글 → redirect_uri로 Authorization Code 전달
5 서버가 Code → Access Token 교환 (서버-서버, 안전)
6 Access Token으로 구글 API에서 사용자 정보 조회 → 자체 JWT 발급
// 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 복잡, 잘 안 씀
💡 현업 추천: 신규 프로젝트는 URI 버전(/v1/)을 가장 많이 사용합니다. GitHub, Twitter, Stripe 등 대부분의 API가 URI 버전 관리를 사용합니다.

📖 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 Gateway 주요 기능
라우팅
/api/users → User Service
/api/orders → Order Service
인증
JWT 검증 일원화
각 서비스 부담 감소
Rate Limiting
서비스별 제한
DDoS 방어
로드 밸런싱
여러 인스턴스에
트래픽 분산
Circuit Breaker
장애 서비스 격리
연쇄 장애 방지
캐싱
응답 캐시로
서비스 부하 감소
# 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 만들기

"테스트 없는 코드는 시한폭탄입니다"

🔺 테스트 피라미드 전략

E2E 테스트 (10%) — 느림, 비쌈
통합 테스트 (30%) — API 레벨
단위 테스트 (60%) — 빠름, 저렴
// 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선 — 모범 답변 포함

Q. REST API에서 PUT과 PATCH의 차이점은?
PUT: 리소스 전체 교체. 전송하지 않은 필드는 null/기본값으로 됩니다.
PATCH: 리소스 부분 수정. 전송한 필드만 변경됩니다.

예시: 사용자 전화번호만 바꾸고 싶을 때
PUT - 이름, 이메일, 전화번호 모두 보내야 함
PATCH - 전화번호만 보내면 됨 (더 효율적)
Q. GraphQL의 N+1 문제란? 어떻게 해결하나요?
N+1 문제: 1번 쿼리 결과의 N개 항목 각각에 대해 추가 쿼리가 발생하는 현상

예: 10개 게시글 조회 후 각 작성자 조회 = 1+10 = 11번 쿼리

해결책: DataLoader 패턴 사용
- 같은 요청 내 동일 타입 ID들을 모아(배치) 한 번에 조회
- 중복 조회 방지 (캐싱)
- 결과: 11번 → 2번으로 감소
Q. JWT vs Session 인증 방식 비교
Session: 서버에 상태 저장. 서버 메모리 사용. 수평 확장 시 공유 필요 (Redis)
JWT: 클라이언트에 상태 저장 (Stateless). 서버 부담 없음. 토큰 탈취 시 만료 전까지 취소 불가

현업 선택 기준:
- 단일 서버, 보안 중요: Session
- 분산 서버, MSA, 모바일: JWT + Refresh Token Rotation
Q. API 설계 시 멱등성(Idempotency)이란?
멱등성: 같은 요청을 여러 번 실행해도 결과가 동일한 성질

멱등 메서드: GET, PUT, DELETE, HEAD
비멱등 메서드: POST

예: DELETE /users/1을 10번 호출해도 결과는 동일 (첫 번째는 삭제, 나머지는 404)
반면 POST /orders를 10번 호출하면 10개의 주문이 생성됨

실무 적용: 결제 API에서 Idempotency-Key 헤더로 중복 결제 방지
Q. REST API와 GraphQL 중 어떤 것을 선택하나요?
REST 선택: 단순 CRUD, 공개 API, HTTP 캐싱 필요, 팀이 REST에 익숙
GraphQL 선택: 다양한 클라이언트(모바일/웹), 복잡한 관계 데이터, 실시간 + 쿼리 동시 필요

현업에서는 두 가지를 혼용하기도 합니다:
- 공개 API: REST (캐싱, 단순성)
- 내부 대시보드: GraphQL (복잡한 데이터 요구사항)
Q. CORS란 무엇이고 어떻게 설정하나요?
CORS(Cross-Origin Resource Sharing): 다른 출처(도메인/포트)에서 API 호출을 허용하는 메커니즘

예: frontend.com에서 api.example.com 호출 시 브라우저가 차단 → CORS 헤더로 허용 설정

NestJS 설정: app.enableCors({ origin: ['https://frontend.com'], credentials: true })

✅ 현업 API 개발 체크리스트

🎨 설계 단계

☐ REST 원칙 준수 (명사 URL, HTTP 메서드)
☐ 에러 응답 표준화 (RFC 7807)
☐ 페이지네이션 전략 결정
☐ API 버전 관리 계획
☐ 인증/인가 방식 설계

🔒 보안 단계

☐ JWT 만료 시간 짧게 설정 (15분)
☐ Refresh Token 안전하게 저장
☐ Rate Limiting 설정
☐ CORS 화이트리스트 설정
☐ Input 유효성 검사 (class-validator)

⚡ 성능 단계

☐ DB 쿼리 최적화 (인덱스 확인)
☐ Redis 캐싱 전략 적용
☐ N+1 문제 해결 (DataLoader/Join)
☐ 응답 압축 (gzip)
☐ Connection Pooling 설정

📝 문서화 단계

☐ Swagger 문서 자동 생성
☐ 에러 코드 목록 문서화
☐ API 변경 이력 관리
☐ 예제 요청/응답 포함
☐ E2E 테스트 작성
🎉

BackendDevGuide0011 완성!

GraphQL과 REST API 고급 설계 완전 정복 A-Z

12
핵심 챕터
50+
실전 코드
A-Z
완전 가이드

REST API부터 GraphQL, gRPC, 보안, 성능, 테스팅까지
현업 백엔드 개발자가 알아야 할 모든 것을 담았습니다. 💪

반응형