최근 작성한 게시물

#Prisma#NestJS#Swagger

스웨거 반복 작업 탈출 + 모델 속성의 중앙화: Prisma Class Generator

NestJS 프로젝트를 진행하다 보면, 데이터베이스 스키마와 API 명세서 사이의 간극을 메우기 위해 끊임없는 단순 반복 작업에 직면하게 됩니다. 새로운 테이블이나 컬럼이 하나 추가될 때마다 Prisma 스키마를 수정하고, 요청/응답 DTO 파일을 새로 열어 @ApiProperty를 달아주고, class-validator와 class-transformer 데코레이터를 일일이 붙여야 하죠. 하지만 단순히 귀찮은 반복 작업(보일러플레이트)만을 피하고자 만든 것은 아닙니다. 가장 근본적이고 중요한 목적은 바로 '특정 모델에 대한 속성들의 완벽한 중앙화'였습니다. 이 두 가지 치명적인 문제(스웨거 작성 피로도 + 속성의 파편화)를 한 번에 해결하기 위해 AI에게 지시하여 AI에게 업무를 맡기기에는 일관된 모델 관리가 어려우며, 이 문제점을 해결하기위해 높은 비용의 토큰을 소비하기에는 비효율적이라 판단하였습니다. 직접 prisma-class-generator 라이브러리를 개발하게 되었습니다. 개발사유 NestJS 생태계에서는 일반적으로 다음과 같은 흐름으로 엔티티와 DTO를 분리해서 관리합니다: DB 스키마 정의: schema.prisma 비즈니스 로직: 데이터 계층 응답을 위한 순수 데이터 모델 User) 요청 검증: API 요청/응답 파라미터 검증을 위한 Request/Response DTO class-validator 사용) 문서화: 클라이언트를 위한 Swagger 문서용 DTO 속성 정의 @ApiProperty 사용) 개발 관점에서는 역할의 분리가 잘 되어있는 것 같지만, 유지보수 측면에서 보면 "가장 핵심이 되는 하나의 데이터 속성이 여러 파일에 흩어지게 된다"는 이슈가 있습니다. 예를 들어, User 테이블에 회원의 상태를 나타내는 isActive (Boolean) 컬럼을 하나 추가하거나, 타입을 변경했다고 가정해 보겠습니다 개발자는 Prisma 스키마를 수정하고, Model 인터페이스를 수정하고, Request DTO 파일을 열어 @IsBoolean()을 붙이고, Response DTO를 수정하고, 추가로 모든 곳에 @ApiProperty() 명세를 또 적어줘야 합니다. 단 한 곳이라도 빼먹으면 런타임 검증 에러가 나거나 스웨거 문서가 실제 스펙과 달라지는 이슈가 발생할 수 있습니다. 저는 이 파편화의 늪에서 벗어나, 프리즈마 모델을 모델의 기준으로 모델 속성들을 중앙에서 관리할 수 있는 도구가 있으면 좋겠다고 판단했습니다. prisma-class-generator 란? prisma-class-generator는 Prisma Schema(schema.prisma)라는 중앙화된 단일 문서 하나만 정의하면, 파싱을 통해 NestJS 환경에 최적화된 TypeScript 클래스(Model 및 DTO)를 자동으로 뽑아주는 Prisma Generator 라이브러리입니다. @nestjs/swagger의 @ApiProperty는 물론, class-validator 및 class-transformer의 검증 데코레이터까지 완벽하게 결합되어 생성됩니다. 따라서 개발자는 오직 schema.prisma 하나만 관리하면 모든 문서화와 검증 규칙이 알아서 동기화됩니다. 핵심 특징 완벽한 모델 속성의 중앙화: 데이터베이스 스키마와 DTO 속성, Swagger 문서화 내용이 오직 schema.prisma 한 곳에서 모두 통제됩니다. 완벽한 NestJS 결합: 추가 작업 없이 바로 Swagger 명세에 포함시킬 수 있고, NestJS의 파이프(validation pipe) 구조를 통한 검증이 즉시 가능합니다. Model과 DTO의 분리 제어: 순수 타입과 Swagger 속성만 포함된 Model 클래스와, 검증 데코레이터까지 전부 포함된 DTO 클래스를 독립적으로 생성하고 관리할 수 있습니다. 강력한 커스텀 타입 대응: 실무에서 빈번히 사용되는 Boolean, BigInt, Decimal 등에 대해, 어떤 커스텀 데코레이터나 파이프 경로를 쓸 것인지 schema.prisma에서 직접 설정할 수 있게 설계했습니다. 프로젝트별 내부 컨벤션에 완전히 융화됩니다. 유연한 네이밍 컨벤션: 조직마다 다른 구조에 맞출 수 있도록 클래스의 접두사/접미사(Prefix/Suffix) 설정이 가능합니다. 사용 방법 1. 패키지 설치 개발 의존성으로 패키지를 설치해 줍니다. npm install -D @inhanbyeol/prisma-class-generator (참고: 생성된 코드는 @nestjs/swagger, class-validator, class-transformer를 임포트하므로 해당 패키지들이 프로젝트에 설치되어 있어야 합니다.) 2. schema.prisma에 속성 중앙화 설정하기 이곳이 이제 모든 DTO 모델의 관제센터(Single Source of Truth)가 됩니다. 목적에 따라 제너레이터를 등록해 줍니다. // 1. 순수 모델(Model) 생성기 generator model { provider = "@inhanbyeol/prisma-class-generator" type = "model" output = "./generated-model" } // 2. 검증이 포함된 DTO 생성기 generator dto { provider = "@inhanbyeol/prisma-class-generator" type = "dto" output = "./generated-dto" // 회사/프로젝트 네이밍 컨벤션에 맞게 Prefix, Suffix 셋팅 가능! filePrefix = "base-" classPrefix = "Req" classSuffix = "Dto" // (필수) 커스텀 데코레이터가 필요한 특수 타입 처리 지정 (유연성의 핵심) isBooleanName = "MyBool" isBooleanPath = "@/custom/bool" // ... (BigInt, Decimal 타입에 대해서도 동일한 방식으로 지정 가능) } // 모델 작성 시 /// 주석을 달면 Swagger Description과 DTO 주석으로 한 번에 들어갑니다! model User { id Int @id @default(autoincrement()) /// 유저 고유 ID email String @unique /// 이메일 주소 name String? /// 이름 isActive Boolean @default(true) /// 활성 상태 여부 } 3. Generate 실행 npx prisma generate 명령어 한 줄이면 지정된 output 디렉토리에 스웨거 스펙과 검증 로직이 동기화된 모든 클래스가 즉각 쏟아져 나옵니다. 결과물 미리보기 속성을 한 곳에서 관리(schema.prisma) 했을 때 어떤 코드가 자동으로 떨어지는지 확인해 보세요. // generated-dto/base-user.dto.ts import { ApiProperty } from '@nestjs/swagger'; import { Prisma } from '@prisma/client'; import { IsInt, IsNotEmpty, IsString } from 'class-validator'; import { MyBool } from '@/custom/bool'; import { Type } from 'class-transformer'; export class ReqUserDto { /** 유저 고유 ID */ @ApiProperty({ type: Number }) @Type(() => Number) @IsInt() id!: number; /** 이메일 주소 */ @ApiProperty({ type: String }) @Type(() => String) @IsNotEmpty() @IsString() email!: string; /** 이름 */ @ApiProperty({ type: String, nullable: true }) @Type(() => String) @IsString() name!: string; /** 활성 상태 여부 */ @ApiProperty({ type: Boolean }) @MyBool() // <-- 스키마 설정해둔 커스텀 데코레이터 로직이 완벽히 매핑됩니다! isActive!: boolean; } 이제 더 이상 컬럼 하나를 수정하기 위해 3~4개의 파일을 찾아다닐 필요가 없습니다. 모든 것은 Prisma Schema 단 한 곳에서 이루어집니다. 마치며 prisma-class-generator의 핵심은 단순한 타이핑을 줄여주는 것이 아니라, 강력한 속성 중앙화 관리를 통해 데이터 스펙 불일치(Out-of-sync)라는 시스템적 버그 요소를 원천 차단하는 데 있습니다. 매번 반복되는 Swagger 명세 관리와 보일러플레이트 작성의 무거운 짐을 이 라이브러리에게 넘겨주세요. 남는 시간과 에너지는 백엔드 애플리케이션의 핵심 비즈니스 로직과 아키텍처를 설계하는 데 투자하시길 바랍니다! 궁금한 점이나 버그 제보, 발전 방향에 대한 기여(PR)는 언제든지 환영합니다.

인한별
인한별
#NestJS

NestJS에서 Joi 검증과 Config 객체를 하나로 합치기

일반적으로 NestJS에서 ConfigModule과 Joi를 함께 사용하면 다음과 같은 중복 작업이 발생합니다. app.config.ts: 클래스 내부에 타입과 필드 정의 validation-schema.ts: Joi를 이용해 동일한 필드들에 대한 검증 로직 작성 이 방식은 환경 변수가 추가될 때마다 두 파일을 모두 수정해야 하며, 실수로 한 곳을 누락할 위험이 있습니다. 이를 해결하기 위해 데코레이터 하나로 필드 정의와 검증을 동시에 처리하는 구조를 설계했습니다. 핵심 아키텍처 Metadata Reflection: 클래스 필드에 @JoiValidate 데코레이터를 달아 Joi 스키마 정보를 메타데이터로 저장합니다. Dynamic Schema Building: 모듈 초기화 단계에서 클래스에 저장된 메타데이터를 읽어와 하나의 Joi.object() 스키마를 동적으로 생성합니다. Auto Mapping: 클래스 생성자에서 ConfigService를 통해 검증된 값을 자신의 필드에 자동으로 할당합니다. 커스텀 데코레이터 및 상수 (joi-validate.decorator.ts) Reflect-metadata를 활용해 각 프로퍼티가 가져야 할 검증 규칙을 클래스 레벨의 맵에 저장합니다. // joi-validate.decorator.ts export function JoiValidate(schema: Joi.Schema) { return (target: object, propertyKey: string) => { // 기존에 저장된 스키마 맵을 가져오거나 새로 생성 const schemas = (Reflect.getMetadata(JOI_SCHEMAS_KEY, target) as object) || {}; schemas[propertyKey] = schema; // 메타데이터에 다시 저장 Reflect.defineMetadata(JOI_SCHEMAS_KEY, schemas, target); }; } 공급원 (app-config.ts) 이제 이 파일 하나만 관리하면 됩니다. 타입 정의와 검증 규칙이 한곳에 모여 있습니다. @Injectable() export class AppConfig { // 서버 포트 @JoiValidate(Joi.number().required()) readonly PORT: number; constructor(private configService: ConfigService) { // 생성자에서 ConfigService의 값을 클래스 필드에 자동 바인딩 const keys = Object.keys(this); keys.forEach(key => { (this as Record<string, unknown>)[key] = this.configService.get<unknown>(key); }); } } 생성자 로직을 통해 config.PORT처럼 직관적으로 환경 변수에 접근할 수 있으며, 타입 안전성도 보장됩니다. 동적 스키마 주입 (app-config.module.ts) ConfigModule.forRoot 설정 시, 클래스 메타데이터로부터 스키마를 추출해 주입합니다. ConfigModule.forRoot({ validationSchema: (() => { // AppConfig 클래스 프로토타입에 저장된 모든 Joi 스키마를 객체로 변환 const schemas = (Reflect.getMetadata(JOI_SCHEMAS_KEY, AppConfig.prototype) as object) || {}; return Joi.object(schemas).options({ convert: true }); })(), }), 이 구조의 장점 유지보수성 향상: 새로운 환경 변수를 추가할 때 AppConfig 클래스에 필드와 데코레이터만 추가하면 끝납니다. 가독성: 각 변수가 어떤 제약 조건(필수 여부, 타입 등)을 가지는지 코드 상에서 직관적으로 확인할 수 있습니다. 타입 안정성: 클래스 필드에 정의된 타입을 그대로 사용하므로 IDE의 자동 완성 기능을 100% 활용할 수 있습니다.

인한별
인한별
#Infra#Ubuntu#Docker

서버 초기 설정: Docker 계정 분리 및 Nginx 환경 구성 완벽 가이드

새로 생성한 EC2 인스턴스에 안전하고 체계적인 배포 환경을 구축하는 것은 안정적인 인프라 운영의 첫걸음입니다. 최고 관리자(root) 계정으로 모든 것을 처리하기보다는 전용 관리 계정을 생성하고, 프로젝트 구조에 맞는 디렉토리를 미리 세팅해 두면 보안과 유지보수 측면에서 훨씬 유리합니다. 이번 글에서는 docker-manager라는 전용 계정을 만들고, Docker 설치와 Nginx 설정을 위한 기본 환경을 구축하는 방법을 단계별로 정리해 보겠습니다. 1. 도커 관리용 전용 계정 생성 보안과 명확한 권한 관리를 위해 도커 컨테이너와 설정 파일들을 전담하여 관리할 전용 유저를 생성합니다. adduser docker-manager 2. 필수 디렉토리 생성 및 권한 부여 Nginx 설정, Docker Compose 파일, 그리고 환경 변수(.env)를 분리하여 관리할 디렉토리를 만듭니다. 생성한 디렉토리의 소유권은 방금 만든 docker-manager 계정으로 변경해 줍니다. # Nginx 설정 파일 디렉토리\ mkdir /home/docker-manager/nginx-configs\ sudo chown docker-manager /home/docker-manager/nginx-configs/ # Traefik 설정 파일 디렉토리\ mkdir /home/docker-manager/traefik sudo chown docker-manager /home/docker-manager/traefik/ mkdir /home/docker-manager/traefik/dynamic sudo chown docker-manager /home/docker-manager/traefik/dynamic # Docker Compose 파일 디렉토리 mkdir /home/docker-manager/compose sudo chown docker-manager /home/docker-manager/compose/ # 환경 변수(env) 파일 디렉토리 mkdir /home/docker-manager/env sudo chown docker-manager /home/docker-manager/env/ 3. Docker 엔진 설치 및 전용 네트워크 구성 Ubuntu 환경에 Docker를 설치하고, 앞으로 띄울 컨테이너들 간의 통신을 제어할 전용 서브넷 네트워크를 구축합니다. # 1. 패키지 업데이트 및 HTTPS 관련 필수 패키지 설치 apt-get update apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common -y # 2. Docker 공식 GPG 키 추가 및 저장소(Repository) 설정 curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - add-apt-repository -y "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" # 3. Docker 엔진 설치 apt-get update -y apt-get install docker-ce docker-ce-cli containerd.io -y # 4. 도커 전용 네트워크(router) 생성 docker network create --subnet 172.30.0.0/16 router 최근 최신 버전의 Ubuntu에서는 apt-key add 명령어가 deprecated(권장되지 않음) 경고를 띄울 수 있습니다. 하지만 일반적인 환경에서는 여전히 정상적으로 작동하며 패키지를 설치하는 데 무리가 없습니다. 4. Nginx 설정(conf) 구성 준비 기본적인 뼈대가 모두 완성되었습니다. 이제 앞서 생성한 /home/docker-manager/nginx-configs 디렉토리 내부에 리버스 프록시나 로드밸런싱을 수행할 conf 파일을 작성해주면 됩니다. 이 디렉토리를 추후 Nginx 도커 컨테이너의 볼륨(Volume)으로 마운트하면, 호스트 서버에서 손쉽게 Nginx 설정을 변경하고 컨테이너에 즉각 반영할 수 있습니다. # /home/docker-manager/nginx.yml services: nginx: image: nginx container_name: nginx volumes: - /home/docker-manager/nginx-configs:/etc/nginx/conf.d/ - /etc/letsencrypt:/etc/letsencrypt - /var/www/certbot:/var/www/certbot ports: - "80:80" - "443:443" restart: unless-stopped logging: driver: "json-file" options: max-size: "30m" # 로그 파일 하나당 최대 100MB max-file: "3" # 로그 파일 최대 3개 유지 (오래된 순 삭제) environment: - TZ=Asia/Seoul networks: - router networks: router: external: true

인한별
인한별
#APIArchitecture

화면에 종속되지 않는 API 설계법

웹 서비스가 복잡해지면서 백엔드 개발자들이 흔히 마주하는 요구사항이 있습니다. "새로운 프로필 페이지가 생겼는데, 여기에 딱 맞는 데이터만 모아서 내려주는 API 하나 만들어주세요." 빠른 피드백과 당장의 프론트엔드 개발 편의성을 위해 이런 요청을 수락하고 특정 페이지나 액션을 위한 API를 만들기 쉽습니다. 하지만 시간이 지날수록 이는 백엔드 시스템에 큰 부메랑으로 돌아옵니다. 오늘은 왜 특정 페이지를 위한 API 구성을 피해야 하는지, 그리고 백엔드의 책임을 줄이는 올바른 API 설계 방향에 대해 이야기해보려 합니다. 특정 페이지에 국한된 API의 함정 화면(UI) 요구사항에 맞춰 데이터를 반환하거나 특정 액션만을 위해 구성된 API(이하 UI 종속적 API)는 다음과 같은 치명적인 단점을 가집니다. 재사용성의 실종과 API의 파편화 A 페이지를 위해 만든 GET /api/page-a-data API가 있다고 가정해 보겠습니다. 얼마 뒤 비슷한 데이터를 보여주는 B 페이지가 생겼을 때, 이 API를 재사용할 수 있을까요? 대부분의 경우 불가능합니다. B 페이지는 A 페이지와 UI 구성이 조금 다르거나, 필요한 데이터의 깊이가 다르기 때문입니다. 결국 백엔드는 GET /api/page-b-data라는 또 다른 불필요한 API를 추가로 구현해야 합니다. UI 변경이 백엔드 배포로 이어지는 강한 결합도 프론트엔드에서 화면의 레이아웃을 바꾸거나 버튼 하나를 추가할 때마다 백엔드 API도 수정되어야 합니다. 이는 프론트엔드와 백엔드의 강한 결합을 의미하며, 독립적인 배포와 유지보수를 어렵게 만듭니다. 해결책 이러한 악순환을 끊어내기 위해서는 API 설계의 패러다임을 바꿔야 합니다. API는 프론트엔드의 '화면'을 그리기 위해 존재하는 것이 아니라, 백엔드가 관리하는 '도메인 모델(리소스)' 그 자체를 반환해야 합니다. UI 구성의 책임은 클라이언트에게 백엔드는 데이터의 무결성을 검증하고, 비즈니스 로직을 처리하며, 시스템이 관리하는 리소스의 상태를 제공하는 데 집중해야 합니다. 회원(User), 주문(Order), 상품(Product) 등 도메인 관점에서의 RESTful한 API를 제공하고, 이 데이터들을 조합하고 가공하여 화면에 그리는 책임은 프론트엔드(클라이언트)로 위임해야 합니다. 유연성과 재사용성의 확보 도메인 중심의 범용적인 API를 설계하면, 새로운 페이지나 기능이 추가되더라도 기존 API를 조합하여 충분히 대응할 수 있습니다. 백엔드는 불필요한 API 엔드포인트를 늘릴 필요가 없고, 프론트엔드는 백엔드의 배포를 기다리지 않고도 유연하게 화면을 구성할 수 있게 됩니다.

인한별
인한별