"외부에는 암호화된 ID를, 내부에는 숫자 ID를."
NestJS의 AOP(Pipe, Interceptor, Decorator)를 활용하여 비즈니스 로직 침범 없이 Auto Increment PK를 숨기는 예제 프로젝트입니다.
REST API를 개발할 때 GET /posts/1, GET /users/100 처럼 Auto Increment PK를 그대로 노출하면 다음과 같은 보안 위협과 문제가 발생할 수 있습니다.
- Insecure Direct Object References (IDOR): 예측 가능한 패턴으로 인해 권한 없는 리소스에 접근 시도가 쉬워집니다.
- Business Intelligence Leak:
id: 100다음에id: 101이 생성되는 것을 보고 유저가 서비스의 성장 추이나 데이터 개수를 유추할 수 있습니다. - Crawling: 연속된 숫자를 대입하여 손쉽게 데이터를 긁어갈 수 있습니다.
UUID를 사용하는 방법도 있지만, 이는 저장 공간 낭비와 RDBMS의 Clustered Index 성능 저하를 유발합니다.
따라서 "DB 성능을 위해 내부적으로는 Auto Increment(Number)를 유지하되, 외부 통신 시에만 암호화된 문자열(String)을 사용하는" 방법을 고안했습니다.
이 프로젝트는 관심사의 분리(Separation of Concerns) 원칙을 지키기 위해 NestJS의 기능을 적극 활용했습니다.
- Hybrid CipherService:
- Node.js
crypto모듈(AES-256-CTR) 기반 암호화. - **DI(Dependency Injection)**와 Decorator 양쪽에서 사용하기 위해
Instance와Static변수를 모두 활용한 하이브리드 구조 설계.
- Node.js
- Transparent Decryption (Request):
DecryptIdPipe: 요청이 컨트롤러에 도달하기 전, 암호화된 ID를 자동으로 복호화하여 Controller는 순수한 숫자(Number)로 로직을 수행합니다.
- Transparent Encryption (Response):
@SecretPkDecorator: 응답이 나갈 때class-transformer를 통해 ID를 자동으로 암호화합니다.
- Deterministic Encryption:
- URL의 영속성을 위해 고정된 IV(Initialization Vector)를 사용하여, 동일한 ID는 언제나 동일한 암호문을 가집니다.
- Framework: NestJS
- Language: TypeScript
- Encryption: Node.js
crypto(AES-256-CTR),scryptSync - Serialization:
class-transformer - Validation:
class-validator
# 1. 패키지 설치
$ pnpm install
# 2. 환경 변수 설정 (.env 파일 생성 필요)
# 아래 'Environment Variables' 섹션 참고
# 3. 서버 실행
$ pnpm run start:dev프로젝트 루트에 .env 파일을 생성하고 다음 값을 설정해 주세요.
(보안을 위해 강력한 비밀번호를 사용하세요.)
# 암호화에 사용될 비밀키 (문자열)
SECRET_PK_KEY=your-super-secret-password-should-be-long
# 키 생성을 위한 솔트 (Salt)
SECRET_PK_SALT=random-salt-value
# 고정된 초기화 벡터 (정확히 16글자여야 함)
SECRET_PK_IV=0123456789012345컨트롤러는 암호화/복호화 로직을 전혀 신경 쓰지 않습니다.
@Get(':id')
// Pipe가 암호문을 숫자로 변환해 줍니다.
findOne(@Param('id', DecryptIdPipe) id: number) {
// id는 number 타입입니다 (ex: 12345)
return this.service.findOne(id);
}응답 객체에 데코레이터만 붙이면 됩니다.
export class UserResponseDto {
@SecretPk() // 나갈 때 자동 암호화
id: number;
email: string;
}- Internal DB:
id: 1 - API Response:
{ "id": "32eaa1fe35f6...", "email": "user@example.com" }
핵심 로직은 src/common에 모여 있습니다.
src
├── common
│ ├── cipher.service.ts # 암호화 코어 (Static/Instance Hybrid)
│ ├── decrypt-id.pipe.ts # 요청 복호화 파이프
│ └── secret-pk.decorator.ts # 응답 암호화 데코레이터
├── dto
│ └── sample.dto.ts # @SecretPk 적용 예시
├── app.controller.ts # 적용된 컨트롤러 예시
└── main.ts # 전역 Interceptor/Pipe 설정
이 프로젝트의 상세한 구현 과정과 고민은 아래 블로그에서 확인할 수 있습니다.