TIL - 20260301

juni·2026년 3월 1일

TIL

목록 보기
281/316

0301 NestJS 심화 (1/N): TypeORM 관계 매핑과 트랜잭션


✅ 1. TypeORM 관계 매핑 (Relations)

  • 이전에는 단일 엔티티만 다루었지만, 실제 애플리케이션에서는 여러 엔티티가 서로 관계를 맺습니다. TypeORM은 데코레이터를 사용하여 이러한 관계를 객체 지향적으로 쉽게 정의할 수 있게 해줍니다.

➕ 1-1. One-to-One (일대일)

  • 하나의 User가 하나의 Profile을 가지는 경우.

  • @OneToOne 데코레이터와, 외래 키(Foreign Key)가 생성될 위치에 @JoinColumn 데코레이터를 함께 사용합니다.

    // user.entity.ts
    @OneToOne(() => Profile)
    @JoinColumn()
    profile: Profile;

➕ 1-2. 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[];

➕ 1-3. 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 관계 두 개로 풀어내는 것이 더 유연한 설계입니다.


✅ 2. 관계 데이터 로딩과 N+1 문제

  • 연관된 엔티티를 조회할 때, 잘못된 방식으로 접근하면 성능에 심각한 영향을 미치는 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(); // 여러 개의 결과를 가져옴
}

✅ 3. 트랜잭션 (Transaction) 관리

  • 트랜잭션이란 여러 개의 데이터베이스 작업을 하나의 논리적인 작업 단위로 묶는 것입니다. 이 작업 단위 내의 모든 연산은 모두 성공(Commit)하거나, 하나라도 실패하면 모두 실패(Rollback)해야 합니다. (All or Nothing)

  • 왜 필요한가?: 여러 테이블에 걸쳐 데이터를 수정하는 작업(e.g., 주문 생성 시 상품 재고 차감) 도중 오류가 발생했을 때, 데이터의 일관성과 무결성을 보장하기 위해 필수적입니다.

➕ NestJS와 TypeORM에서의 트랜잭션 처리 (권장 방식)

  • DataSource (또는 EntityManager)의 transaction 메서드를 사용하는 것이 가장 안전하고 간결합니다.

  • 동작 방식:

    1. 서비스의 생성자에서 DataSource를 주입받습니다.
    2. 트랜잭션이 필요한 메서드 내에서 this.dataSource.transaction(async manager => { ... })를 호출합니다.
    3. transaction 메서드에 전달된 콜백 함수 내부의 모든 데이터베이스 작업은 하나의 트랜잭션으로 묶입니다.
    4. 콜백 함수 내에서 사용하는 manager트랜잭션 전용 EntityManager입니다.
    5. 콜백 함수가 성공적으로 완료되면 자동으로 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);
    });
  }
}

📌 요약

  • TypeORM에서는 @OneToOne, @ManyToOne, @OneToMany, @ManyToMany 등의 데코레이터를 사용하여 엔티티 간의 관계를 정의합니다.
  • 연관된 데이터를 조회할 때 발생하는 N+1 문제를 피하기 위해, QueryBuilderleftJoinAndSelect를 사용하여 명시적으로 데이터를 함께 불러오는 것이 가장 좋습니다.
  • 여러 DB 작업을 원자적으로 처리해야 할 때는 트랜잭션을 사용해야 하며, NestJS에서는 DataSource.transaction() 메서드를 사용하는 것이 가장 안전하고 효율적인 방법입니다.

0개의 댓글