낙서장이자 오답 노트이자 컨닝 페이퍼

패스트캠퍼스 환급챌린지 21일차 : 코드팩토리의 백엔드 아카데미 : 한 번에 끝내는 NestJS 패키지 - 기초부터 MSA까지 강의 후기 본문

NestJS

패스트캠퍼스 환급챌린지 21일차 : 코드팩토리의 백엔드 아카데미 : 한 번에 끝내는 NestJS 패키지 - 기초부터 MSA까지 강의 후기

NangIn 2025. 4. 21. 09:18

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.

 

강의 내용 정리   

 

Transaction 이론

 

트랜잭션이란?

  • 여러 개의 데이터 작업을 하나의 논리적 작업 단위로 묶어 실행하는 기능
  • All or Nothing: 전부 성공하거나, 전부 실패해야 일관성이 유지됨

 

사용 예: 계좌 이체

  1. A 계좌에서 1,000,000원 출금
  2. B 계좌로 1,000,000원 입금
    • → 중간에 실패 시, A만 출금된 상태가 될 수 있음
    • 트랜잭션이 없으면 데이터 무결성 깨짐

 

트랜잭션 필수 구성 요소

구성 요소  설명
BEGIN 트랜잭션 시작
COMMIT 트랜잭션 정상 완료 및 반영
ROLLBACK 오류 발생 시 전체 작업 취소

 

트랜잭션 문제 유형 (4가지)

  1. Lost Update
    • 두 트랜잭션이 동시에 같은 데이터를 갱신
    • 나중에 실행된 트랜잭션이 앞선 결과를 덮어씀
    • 예: 트랜잭션 1에서 1000 → 900, 트랜잭션 2에서 다시 800 덮어쓰기
    • 해결법: 낙관적 락(Optimistic Lock) → version 조건으로 동시성 제어
  2. Dirty Read
    • 다른 트랜잭션에서 아직 커밋되지 않은 데이터를 읽음
    • 예: 트랜잭션 1이 수정 후 커밋 전, 트랜잭션 2가 읽음 → 트랜잭션 1이 롤백되면 데이터 불일치
    • 해결법: READ COMMITTED 이상 트랜잭션 레벨 사용
  3. Non-Repeatable Read
    • 같은 트랜잭션 안에서 두 번 읽었는데 결과가 다름
    • 예: 트랜잭션 1이 읽고, 트랜잭션 2가 수정 후 커밋 → 트랜잭션 1이 다시 읽으면 값 변경됨
    • 해결법: REPEATABLE READ 이상 사용
  4. Phantom Read
    • 조건 쿼리로 조회한 후, 중간에 새로운 레코드가 추가되어 결과가 달라짐
    • 예: balance > 1000으로 조회 후, 중간에 해당 조건의 데이터가 추가됨
    • 해결법: SERIALIZABLE 트랜잭션 레벨 사용

 

트랜잭션 격리 수준 (Isolation Levels)   

격리 수준 Dirty Read Non-Repeatable Read  Phantom Read
READ UNCOMMITTED 발생 가능 발생 가능 발생 가능
READ COMMITTED ❌ 방지됨 발생 가능 발생 가능
REPEATABLE READ ❌ 방지됨 ❌ 방지됨 발생 가능
SERIALIZABLE ❌ 방지됨 ❌ 방지됨 ❌ 방지됨
  • 격리 수준이 높아질수록 정합성 보장은 강화되지만, 성능 저하가 발생함

 

트랜잭션 실행 방법(SQL)

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;

-- 여러 쿼리 실행

COMMIT;
-- 또는 ROLLBACK;

 

정리

  • 트랜잭션은 데이터 정합성을 지키기 위한 핵심 기능
  • 격리 수준은 성능과 안정성의 트레이드오프
  • 실무에서는 격리 수준을 상황에 맞게 선택하는 것이 중요
  • DB 초보자라도 트랜잭션 개념 + 대표적인 문제점 4가지는 반드시 이해하고 있어야 함

 

Create 함수에서 Transaction 적용하기

 

목적: Create 로직 전체를 트랜잭션으로 묶기

  • 여러 insert 작업을 하나의 트랜잭션 단위로 실행하여
    • 한 작업이라도 실패 시 전체 롤백
    • 모든 작업 성공 시 한 번에 커밋

 

트랜잭션 준비

  • DataSource 주입
@Injectable()
export class MovieService {
  constructor(
    private readonly dataSource: DataSource, // TypeORM Core
  ) {}
}

 

QueryRunner 생성 및 트랜잭션 시작

  • createQueryRunner() → DB와 직접 통신하는 트랜잭션 단위 쿼리러너 생성
const qr = this.dataSource.createQueryRunner();
await qr.connect(); // DB 연결
await qr.startTransaction(); // 트랜잭션 시작

 

try-catch-finally 블록 구성

try {
  // ...실행할 모든 DB 쿼리
  await qr.commitTransaction(); // 모든 쿼리 성공 시 커밋
} catch (err) {
  await qr.rollbackTransaction(); // 에러 발생 시 롤백
  throw err;
} finally {
  await qr.release(); // 연결 해제
}

 

기존 로직 트랜잭션으로 옮기기

  • 아래 로직들을 모두 try 블록 안으로 이동
    • MovieDetail 생성
    • Movie 생성
    • Movie와 Genre 관계 연결
    • (기타 필요한 작업)
  • 주의
    • 기존 repository.save(), repository.find() 등을 qr.manager로 변경해야 함:
    const director = await qr.manager.findOne(Director, {
      where: {
        id: createMovieDto.directorId,
      },
    });
    await qr.manager.createQueryBuilder().insert()... // 등등
    

 

커밋 후 최종 데이터 조회

  • 트랜잭션 커밋 이후, 별도로 조회하여 클라이언트에 반환
const result = await this.movieRepository.findOne({
  where: { id: movieId },
  relations: ['detail', 'director', 'genres'],
});
return result;

 

트랜잭션 적용 확인 방법

  • 정상 케이스
    • Postman으로 요청 → 모든 엔티티가 정상 생성됨
  • 강제 에러 삽입
    • throw new NotFoundException('일부러 에러 던짐');
      
    • 예상: 아무 데이터도 생성되지 않음 (롤백 확인용)
  • 트랜잭션 미적용 시
    • 강제 에러 발생해도 앞단에 실행된 일부 쿼리는 DB에 반영됨 (일관성 깨짐)

 

실무에서 트랜잭션이 필요한 이유

상황  트랜잭션 필요성
연속된 insert/update 작업 중간 실패 시 전체 롤백 보장
관계 데이터 다수 생성 참조 무결성 보장
트랜잭션 누락 시 데이터 유실, 중복 생성, 고아 레코드 발생 가능

 

요약

  • createQueryRunner() + startTransaction() 으로 트랜잭션 시작
  • qr.manager 로 모든 DB 작업 통합
  • try-catch-finally 구조로 커밋/롤백/해제 처리
  • 에러 발생 시 전체 작업 원복 (롤백)

 

Update 함수 Transaction 적용하기

 

목표

  • 기존의 Movie update() 로직을 트랜잭션 기반으로 전환
  • 다단계 쿼리 중 하나라도 실패하면 전체 롤백
  • 성공 시에만 모든 변경사항을 커밋

 

QueryRunner 생성 및 트랜잭션 시작

const qr = this.dataSource.createQueryRunner();
await qr.connect();              // DB 연결
await qr.startTransaction();     // 트랜잭션 시작
  • this.dataSource는 @Injectable() 서비스에서 주입받은 TypeORM의 DataSource 인스턴스

 

try-catch-finally 패턴 구성

try {
  // 트랜잭션 내 작업
  await qr.commitTransaction(); // 전체 커밋
} catch (err) {
  await qr.rollbackTransaction(); // 실패 시 롤백
  throw err;
} finally {
  await qr.release(); // DB 커넥션 반환
}
  • catch에서 반드시 에러를 throw 해야 프론트에도 에러가 전파됨

 

트랜잭션 내부에서의 쿼리 실행

await qr.manager.findOne(Movie, { where: { id } }); // 영화 조회
await qr.manager.findOne(Director, { where: { id: dto.directorId } }); // 감독 조회
await qr.manager.find(Genre, { where: { id: In(dto.genreIds) } }); // 장르들 조회
  • 기존의 repository.find() → qr.manager.find()로 변경

 

최종 응답은 커밋 이후

await qr.commitTransaction(); // 전체 커밋

return this.movieRepository.findOne({
  where: { id },
  relations: ['detail', 'director', 'genres'],
});

 

트랜잭션 테스트

  • 정상 케이스
    • PATCH 요청으로 title, directorId, genreIds를 변경 → 전체 정상 적용
  • 실패 케이스
    • 중간에 강제로 에러 발생:
    throw new NotFoundException('일부러 에러 던짐');
    
  • 결과:
    • Movie 테이블: 변경 없음
    • MovieDetail: 변경 없음
    • Genre 관계: 그대로 유지됨
    • 전체 롤백 성공!

 

트랜잭션의 실전 의의 

목적 설명
데이터 일관성 보장 다단계 update 중 일부만 반영되는 문제 방지
실무 신뢰성 확보 네트워크 장애, 논리 에러 등에서도 rollback 가능
트랜잭션 기반 로직 설계 연관된 DB 연산은 가능한 트랜잭션으로 묶기

 

학습 후기         

 

이번에 트랜잭션 개념과 적용 방법을 집중적으로 학습하면서, 데이터베이스 무결성을 유지하기 위한 가장 핵심적인 기법이 무엇인지 명확하게 이해할 수 있었습니다. 단순히 여러 쿼리를 순차적으로 실행하는 것과, 이를 하나의 트랜잭션으로 묶어서 처리하는 것 사이에는 굉장히 큰 차이가 있었습니다. 특히 실습을 통해 에러를 인위적으로 던져보면서 롤백이 정확히 작동하는지를 검증하는 과정에서, 트랜잭션이 없다면 발생할 수 있는 문제점들이 얼마나 쉽게 발생할 수 있는지를 체감했습니다.

 

트랜잭션을 구성하는 기본 요소인 BEGIN, COMMIT, ROLLBACK의 흐름을 TypeORM에서 QueryRunner를 활용하여 직접 구현해보니, 기존 repository.save()로 처리했던 방식에 비해 조금은 복잡하지만 훨씬 명확하고 신뢰할 수 있는 방식임을 알게 되었습니다. 또한 qr.manager를 통해 같은 트랜잭션 컨텍스트 안에서 모든 DB 작업을 일관되게 처리할 수 있다는 점도 실무적으로 매우 강력한 장점이라고 느꼈습니다.

 

특히 인상 깊었던 부분은 트랜잭션이 단순히 동시에 실행되는 여러 작업을 묶는 기능이 아니라, 장애가 발생해도 전체 시스템의 신뢰성과 일관성을 지키는 핵심 장치라는 점이었습니다. 예를 들어 영화 등록 시, MovieDetail → Movie → Genre 관계 연결까지 총 세 단계의 insert 작업이 각각 독립적으로 실행되면, 중간에 하나라도 실패했을 때 데이터베이스는 즉시 오염될 수 있습니다. 그러나 이를 트랜잭션으로 묶으면 "전부 성공" 또는 "전부 취소"의 원자성이 보장되어 훨씬 안전한 설계를 할 수 있습니다.

또한 트랜잭션에서 발생할 수 있는 네 가지 문제점(Lost Update, Dirty Read, Non-Repeatable Read, Phantom Read)를 격리 수준과 함께 학습하면서, 단순한 개념 설명을 넘어서 왜 격리 수준을 상황에 따라 적절히 선택해야 하는지도 함께 고민할 수 있었습니다. 높은 격리 수준을 설정하면 더 많은 정합성을 확보할 수 있는 대신 성능은 저하되므로, 단순히 "무조건 SERIALIZABLE!"이라는 접근보다는, 비즈니스 요구사항에 맞는 전략적 선택이 필요하다는 현실적인 인사이트도 얻을 수 있었습니다.

 

정리하자면, 이번 학습을 통해 단순한 ORM 사용에 그치지 않고, 실무에서의 안정성과 데이터 정합성을 고려한 구조를 어떻게 만들 것인가에 대해 깊은 고민을 할 수 있었습니다. 트랜잭션은 선택이 아니라 필수이며, 특히 관계형 데이터 조작이 빈번한 서비스에서는 이 기능을 제대로 이해하고 활용할 수 있어야 진짜 백엔드 개발자라고 느꼈습니다. 이 과정을 통해 한 단계 더 실무적인 사고방식에 가까워진 기분이 들었습니다.

 

학습 인증샷       

                       

수강 인증 사진
학습 인증샷
공부 시작 시간
공부 종료 시간

https://abit.ly/lisbva