icon안동민 개발노트

API 서버 구현


 TypeScript를 사용한 강력하고 타입 안전한 API 서버 구현은 현대적인 백엔드 개발의 핵심입니다.

 이 절에서는 Node.js 환경에서 Express 또는 NestJS를 사용하여 RESTful API 서버를 구현하는 과정을 살펴봅니다.

RESTful API 서버 구현

 NestJS를 사용한 API 서버 구현 예시

  1. 프로젝트 설정
npm i -g @nestjs/cli
nest new api-server
cd api-server
  1. 컨트롤러 구현
// src/users/users.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
 
@Controller('users')
export class UsersController {
  @Get()
  findAll() {
    return 'This action returns all users';
  }
 
  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return 'This action adds a new user';
  }
}
  1. DTO 정의
// src/users/dto/create-user.dto.ts
export class CreateUserDto {
  readonly name: string;
  readonly email: string;
}

데이터베이스 연동

 TypeORM을 사용한 데이터베이스 연동

  1. 설치
npm install @nestjs/typeorm typeorm pg
  1. 엔티티 정의
// src/users/user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
 
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;
 
  @Column()
  name: string;
 
  @Column()
  email: string;
}
  1. 서비스 구현
// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
 
@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}
 
  findAll(): Promise<User[]> {
    return this.usersRepository.find();
  }
 
  create(user: User): Promise<User> {
    return this.usersRepository.save(user);
  }
}

미들웨어 및 인터셉터 구현

 로깅 인터셉터 예시

// src/logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
 
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Before...');
    const now = Date.now();
    return next
      .handle()
      .pipe(
        tap(() => console.log(`After... ${Date.now() - now}ms`)),
      );
  }
}

인증 및 권한 부여

 JWT를 사용한 인증 구현

  1. 설치
npm install @nestjs/jwt passport-jwt
  1. JWT 전략 구현
// src/auth/jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
 
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: 'secretKey',
    });
  }
 
  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}
  1. 가드 사용
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Request() req) {
  return req.user;
}

API 문서화

 Swagger를 사용한 API 문서화

  1. 설치
npm install @nestjs/swagger swagger-ui-express
  1. 설정
// main.ts
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
 
const config = new DocumentBuilder()
  .setTitle('API 문서')
  .setDescription('API 설명')
  .setVersion('1.0')
  .build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
  1. DTO 데코레이터 추가
import { ApiProperty } from '@nestjs/swagger';
 
export class CreateUserDto {
  @ApiProperty()
  readonly name: string;
 
  @ApiProperty()
  readonly email: string;
}

에러 처리 및 로깅

 전역 예외 필터 구현

// src/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
 
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();
 
    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
      });
  }
}

캐싱 전략

 Redis를 사용한 캐싱 구현

  1. 설치
npm install @nestjs/cache-manager cache-manager redis
  1. 캐시 모듈 설정
import { CacheModule } from '@nestjs/cache-manager';
import * as redisStore from 'cache-manager-redis-store';
 
@Module({
  imports: [
    CacheModule.register({
      store: redisStore,
      host: 'localhost',
      port: 6379,
    }),
  ],
})
export class AppModule {}
  1. 캐시 사용
@Injectable()
export class UsersService {
  constructor(
    @Inject(CACHE_MANAGER) private cacheManager: Cache,
    private usersRepository: Repository<User>,
  ) {}
 
  async findAll(): Promise<User[]> {
    const cachedUsers = await this.cacheManager.get<User[]>('all_users');
    if (cachedUsers) {
      return cachedUsers;
    }
    const users = await this.usersRepository.find();
    await this.cacheManager.set('all_users', users, { ttl: 3600 });
    return users;
  }
}

테스트 작성

 단위 테스트 예시

// src/users/users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
 
describe('UsersService', () => {
  let service: UsersService;
 
  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [UsersService],
    }).compile();
 
    service = module.get<UsersService>(UsersService);
  });
 
  it('should be defined', () => {
    expect(service).toBeDefined();
  });
 
  // 추가 테스트 케이스...
});

Best Practices

 1. 관심사 분리

  • 컨트롤러, 서비스, 리포지토리 레이어 분리
  • 각 모듈의 책임 명확히 정의

 2. 의존성 주입 활용

  • 느슨한 결합과 테스트 용이성 확보

 3. 환경 설정 관리

  • ConfigService를 사용한 환경별 설정 관리

 4. 비즈니스 로직 캡슐화

  • 서비스 레이어에 비즈니스 로직 집중

 5. 데이터 유효성 검사

  • DTO를 사용한 데이터 유효성 검사
  • class-validator 라이브러리 활용

 6. 보안 강화

  • CORS 설정
  • Helmet 미들웨어 사용
  • 입력 데이터 산이화

 7. 성능 최적화

  • 데이터베이스 인덱싱
  • N+1 문제 해결을 위한 적절한 쿼리 작성

 8. 로깅 전략

  • 구조화된 로깅 구현
  • 로그 레벨 관리

 9. 에러 처리

  • 일관된 에러 응답 포맷 정의
  • 예외 필터 활용

 10. 확장성 고려

  • 마이크로서비스 아키텍처 고려
  • 모듈화를 통한 기능 확장 용이성 확보

 TypeScript를 사용한 API 서버 구현은 타입 안정성과 개발 생산성을 크게 향상시킵니다.

 NestJS와 같은 프레임워크를 사용하면 구조화된 아키텍처와 풍부한 기능을 쉽게 구현할 수 있습니다.

 미들웨어, 인터셉터, 파이프 등을 활용하면 요청 처리 흐름을 세밀하게 제어할 수 있습니다.

 이는 로깅, 데이터 변환, 유효성 검사 등 다양한 크로스커팅 관심사를 효과적으로 처리하는 데 도움이 됩니다.

 인증과 권한 부여는 API 서버의 핵심 기능입니다. JWT나 OAuth를 사용한 인증 메커니즘을 구현하고, 적절한 가드를 사용하여 각 엔드포인트의 접근을 제어할 수 있습니다.

 API 문서화는 개발자 경험과 협업에 중요한 요소입니다. Swagger를 통한 자동 문서 생성은 API의 일관성을 유지하고 클라이언트 개발자와의 커뮤니케이션을 원활하게 합니다.

 에러 처리와 로깅은 운영 환경에서 중요한 역할을 합니다. TypeScript의 타입 시스템을 활용하여 구조화된 에러 객체를 정의하고, 전역 예외 필터를 통해 일관된 에러 응답을 제공할 수 있습니다.

 캐싱은 API 성능 최적화의 핵심 전략입니다. Redis와 같은 인메모리 데이터 스토어를 사용하여 타입 안전한 캐싱 레이어를 구현할 수 있습니다.