TypeORM은 TypeScript 친화적인 ORM(Object Relational Mapping)으로, 엔티티를 중심으로 데이터베이스 모델과 애플리케이션 코드를 자연스럽게 연결해 줍니다. 특히 NestJS와 함께 사용되는 경우가 많아, TypeScript 타입 설계 → DB 스키마 → 비즈니스 로직이 하나의 흐름으로 이어지는 것이 큰 장점입니다.
TypeORM이 TypeScript와 잘 맞는 이유
TypeORM은 다음 요소들을 모두 TypeScript 기반으로 제공합니다.
- 엔티티(Entity)를 class로 정의
- 데코레이터 기반 매핑
- Repository / QueryBuilder 타입 지원
- 엔티티 속성에 직접 타입 적용
즉, 엔티티 클래스 자체가 타입 + 스키마 + 도메인 모델의 역할을 동시에 수행합니다.
기본 Entity 정의하기
TypeORM의 핵심은 Entity입니다. 엔티티는 DB 테이블과 1:1로 매핑됩니다.
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from "typeorm";
@Entity("users")
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 100 })
email: string;
@Column({ length: 50 })
name: string;
@Column({ default: true })
isActive: boolean;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
이 클래스 하나로 다음이 모두 결정됩니다.
- 테이블 이름 (
users) - 컬럼 타입과 제약조건
- TypeScript 타입
Entity 필드 타입 설계 시 주의점
엔티티 필드는 단순히 DB 타입만 고려하면 안 됩니다.
- TypeScript 타입과 DB 타입의 일치 여부
- null 가능성
- 도메인 규칙 반영 여부
@Column({ nullable: true })
nickname?: string;
nullable 컬럼은 TypeScript에서도 반드시 optional(?) 또는 null을 고려해야 합니다.
Repository 패턴과 타입 안정성
TypeORM은 기본적으로 Repository 패턴을 제공합니다.
import { Repository } from "typeorm";
import { User } from "./user.entity";
export class UserService {
constructor(
private readonly userRepository: Repository<User>
) {}
async findByEmail(email: string): Promise<User | null> {
return this.userRepository.findOne({
where: { email },
});
}
}
Repository에 제네릭으로 Entity를 지정하면:
- 조회 결과 타입이 자동 추론
- 잘못된 필드 접근을 컴파일 단계에서 차단
find / findOne 사용 시 null 처리
TypeORM의 조회 메서드는 결과가 없을 수 있습니다.
const user = await repo.findOne({ where: { id } });
이때 반환 타입은:
User | null
따라서 실무에서는 다음과 같은 패턴이 일반적입니다.
if (!user) {
throw new Error("User not found");
}
null 가능성을 무시하면, 런타임 오류로 이어집니다.
관계(Relation) 매핑과 타입
TypeORM은 관계 매핑도 TypeScript 타입으로 표현합니다.
@Entity("posts")
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@ManyToOne(() => User, (user) => user.posts)
user: User;
}
@Entity("users")
export class User {
// ...
@OneToMany(() => Post, (post) => post.user)
posts: Post[];
}
관계 필드는 다음을 반드시 고려해야 합니다.
- 지연 로딩 여부
- optional 관계 가능성
- 조회 시 join 여부
Entity ≠ API 응답 (중요)
실무에서 가장 흔한 실수 중 하나는 Entity를 그대로 API 응답으로 사용하는 것입니다.
- 민감 정보 노출 위험
- 관계 필드 무한 참조
- API 계약 불안정
따라서 반드시 별도의 View/DTO 타입으로 변환합니다.
type UserView = {
id: number;
email: string;
name: string;
};
function toUserView(user: User): UserView {
return {
id: user.id,
email: user.email,
name: user.name,
};
}
QueryBuilder와 타입 활용
복잡한 쿼리가 필요할 경우 QueryBuilder를 사용합니다.
const users = await repo
.createQueryBuilder("user")
.where("user.isActive = :active", { active: true })
.getMany();
QueryBuilder 역시 결과 타입은 Entity 기준으로 추론됩니다.
단, raw query(getRawMany)를 사용할 경우 반환 타입이 느슨해지므로 별도 타입 정의가 필요합니다.
실무에서 자주 겪는 문제와 대응
- 엔티티에 비즈니스 로직 과도하게 포함
- nullable 컬럼과 TypeScript 타입 불일치
- Entity를 DTO처럼 사용
- Relation 로딩 전략 미설계
대부분 “엔티티의 책임 범위”를 명확히 하지 않아서 발생합니다.
실무 권장 설계 요약
- Entity는 DB 매핑 + 최소한의 도메인 상태만 표현
- API 응답은 반드시 별도 타입(View/DTO) 사용
- Repository는 Entity 단위로만 접근
- null 가능성은 타입으로 명확히 표현
- 관계 매핑은 필요할 때만 로딩
TypeORM + TypeScript 조합의 핵심은 “엔티티를 타입과 설계의 중심에 두되, 경계를 명확히 나누는 것”입니다.
- Entity는 DB 관점
- Service는 도메인/유스케이스 관점
- API 응답은 계약(View/DTO) 관점
'개발 > Typescript' 카테고리의 다른 글
| [TYPESCRIPT] React Props와 State 타입 정의 — 컴포넌트 안정성을 결정하는 핵심 (0) | 2025.12.31 |
|---|---|
| [TYPESCRIPT] React + TypeScript 프로젝트 시작하기 — 실무에서 바로 쓰는 기본 세팅 (0) | 2025.12.30 |
| [TYPESCRIPT] NestJS에서 TypeScript 활용하기 — “타입을 설계의 중심”으로 가져오는 실무 패턴 (0) | 2025.12.28 |
| [TYPESCRIPT] TypeScript + Express 프로젝트 설정하기 (0) | 2025.12.27 |
| [TYPESCRIPT] Axios와 함께 쓰는 TypeScript — API 호출을 타입으로 통제하는 실무 패턴 (0) | 2025.12.26 |
