One-to-One (일대일)하나의 User가 하나의 Profile을 가지는 경우.
@OneToOne 데코레이터와, 외래 키(Foreign Key)가 생성될 위치에 @JoinColumn 데코레이터를 함께 사용합니다.
// user.entity.ts
@OneToOne(() => Profile)
@JoinColumn()
profile: Profile;
Many-to-One / One-to-Many (다대일 / 일대다)가장 흔하게 사용되는 관계로, 하나의 User가 여러 개의 Post를 작성하는 경우.
@ManyToOne (N쪽, e.g., Post): 관계의 주인(Owner)이 되는 쪽. 이 엔티티의 테이블에 외래 키 컬럼이 생성됩니다.
@OneToMany (1쪽, e.g., User): 관계의 주인이 아닌 쪽. mappedBy 옵션을 사용하여, 상대편 엔티티의 어떤 속성에 의해 이 관계가 매핑되는지를 명시합니다.
// post.entity.ts (N쪽, 주인)
@ManyToOne(() => User, user => user.posts)
user: User;
// user.entity.ts (1쪽, 주인이 아님)
@OneToMany(() => Post, post => post.user)
posts: Post[];
Many-to-Many (다대다)하나의 Post가 여러 개의 Tag를 가질 수 있고, 하나의 Tag도 여러 Post에 속할 수 있는 경우.
@ManyToMany와 @JoinTable 데코레이터를 함께 사용하면, TypeORM이 두 엔티티를 연결하는 중간 테이블(Junction Table)을 자동으로 생성하고 관리해줍니다.
// post.entity.ts
@ManyToMany(() => Tag)
@JoinTable()
tags: Tag[];
심화: 만약 중간 테이블에 추가적인 컬럼(e.g., createdAt)이 필요하다면, 중간 테이블에 해당하는 엔티티를 직접 만들고 Many-to-One 관계 두 개로 풀어내는 것이 더 유연한 설계입니다.
연관된 엔티티를 조회할 때, 잘못된 방식으로 접근하면 성능에 심각한 영향을 미치는 N+1 문제가 발생할 수 있습니다.
N+1 문제: 게시글 N개를 조회하기 위해 쿼리 1번을 실행한 후, 각 게시글의 작성자 정보를 얻기 위해 N번의 추가 쿼리가 발생하는 현상.
| 로딩 방식 | 설명 | 문제점 |
|---|---|---|
| Eager Loading | • 엔티티 옵션에 eager: true 설정.• 주 엔티티 조회 시 항상 연관 엔티티를 JOIN하여 함께 불러옴. | • 항상 필요하지 않은 데이터까지 불러와 비효율적일 수 있음. • 의도치 않은 N+1 문제 유발 가능성이 높음. (사용 지양) |
| Lazy Loading | • 관계 속성에 접근하는 시점에 별도의 쿼리를 실행하여 데이터를 불러옴. | • 편리하지만, 반복문 안에서 사용될 경우 N+1 문제를 직접적으로 유발함. |
| QueryBuilder (권장) | • leftJoinAndSelect 메서드를 사용하여, 필요한 관계 데이터를 단 한 번의 쿼리로 명시적으로 함께 불러옴. | • 가장 효율적이고 예측 가능한 방식. |
// QueryBuilder를 이용한 N+1 문제 해결
async findPostsWithAuthors(): Promise<Post[]> {
return this.postsRepository
.createQueryBuilder('post') // 'post'는 별칭
.leftJoinAndSelect('post.user', 'user') // 'post'의 'user' 속성을 'user'라는 별칭으로 JOIN
.getMany(); // 여러 개의 결과를 가져옴
}
트랜잭션이란 여러 개의 데이터베이스 작업을 하나의 논리적인 작업 단위로 묶는 것입니다. 이 작업 단위 내의 모든 연산은 모두 성공(Commit)하거나, 하나라도 실패하면 모두 실패(Rollback)해야 합니다. (All or Nothing)
왜 필요한가?: 여러 테이블에 걸쳐 데이터를 수정하는 작업(e.g., 주문 생성 시 상품 재고 차감) 도중 오류가 발생했을 때, 데이터의 일관성과 무결성을 보장하기 위해 필수적입니다.
DataSource (또는 EntityManager)의 transaction 메서드를 사용하는 것이 가장 안전하고 간결합니다.
동작 방식:
DataSource를 주입받습니다.this.dataSource.transaction(async manager => { ... })를 호출합니다.transaction 메서드에 전달된 콜백 함수 내부의 모든 데이터베이스 작업은 하나의 트랜잭션으로 묶입니다.manager는 트랜잭션 전용 EntityManager입니다.COMMIT되고, 중간에 에러가 발생하면 자동으로 ROLLBACK 됩니다.// order.service.ts
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { Order } from './entities/order.entity';
import { Product } from '../products/entities/product.entity';
@Injectable()
export class OrderService {
constructor(private readonly dataSource: DataSource) {}
async createOrder(orderData): Promise<void> {
await this.dataSource.transaction(async (transactionalEntityManager) => {
// 1. 주문 생성
const order = transactionalEntityManager.create(Order, orderData);
await transactionalEntityManager.save(order);
// 2. 상품 재고 차감
const product = await transactionalEntityManager.findOneBy(Product, { id: orderData.productId });
if (product.stock < orderData.quantity) {
throw new Error('재고가 부족합니다.'); // 에러 발생 시 모든 작업이 자동으로 롤백됨
}
product.stock -= orderData.quantity;
await transactionalEntityManager.save(product);
});
}
}
@OneToOne, @ManyToOne, @OneToMany, @ManyToMany 등의 데코레이터를 사용하여 엔티티 간의 관계를 정의합니다.leftJoinAndSelect를 사용하여 명시적으로 데이터를 함께 불러오는 것이 가장 좋습니다.DataSource.transaction() 메서드를 사용하는 것이 가장 안전하고 효율적인 방법입니다.