웅글웅글: Nest 게시판 + 채팅 4. Entity: 희한한(극찬)TypeORM Lazy Loading과 Transaction 실험

메밀·2024년 2월 7일
0

웅글웅글: NestJS

목록 보기
4/9

1. 오랜만에 실험실 개장🔬

N+1이 싫으니까 relation 옵션이나 createQueryBuilder를 사용해 버릇해서
정작 웅글웅글 만들 땐 Lazy Loading에 대해 크게 생각 안 해봤던 것 같다.

이렇게 무작정 만들어버릇하면 안되는데 그래도 블로그 정리하면서 오랜만에 실험실도 만들고 뜻깊음~

결론은 ㅋㅋㅋ

JPA식 지독한 이분법의 fetchType 나라에서 왔기 때문에 정말 희한하고 재밌었음.

2. Lazy Loading 실험!

1) 의문의 시작: 디폴트 fetchType 없음😮

처음엔 TypeORM의 fetchType 디폴트가 lazy loading인 줄 알았는데, 그게 아니었음 ㅋㅋㅋㅋㅋ

예컨대, 이런 엔티티가 있다고 치자.

@Entity()
export class Comment {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  content: string;

  @ManyToOne(() => Board, board => board.comments)
  board: Board;
}

이때, 아래 코드를 실행하면

    const comment = await this.commentRepository.findOneBy({ id: 4 });
    console.log("comment", comment);
    console.log("comment.board", comment.board);

그냥 안들고옴ㅋㅋ
오류도 안 나고, SELECT문도 발생 안한다.

2) 코드를 까보자

@ManyToOne, @OneToMany 등 연관관계 설정용 데코레이터의 내부를 까보면

export interface RelationOptions {
        // ...생략
    lazy?: boolean;
    eager?: boolean;
        // ...생략
}

안 적어주면 eager도 아니고, lazy도 아니라는 걸 알 수 있다.

3) 그럼 lazy loading은 어떻게 하나!

n+1이고 뭐고 일단 셀렉트 두 번 날리게 해보자!

- 엔티티에 lazy 옵션만 먹였을 때

  @ManyToOne(() => Board, board => board.comments, { lazy: true })
  board: Board;
    const comment = await this.commentRepository.findOneBy({ id: 4 });
    console.log("comment", comment);
    console.log("comment.board", comment.board);

Promise<pending>이 뜬다.

- await으로 마무리

    const comment = await this.commentRepository.findOneBy({ id: 4 });
    console.log("comment", comment);
    const board = await comment.board;
    console.log("board", board);

결과적으로 TypeORM은 Promise로 Lazy Loading을 구현한다.
지연로딩된 객체를 사용하려면 await 해줘야한다.

그런데 Lazy Loading하면 딱 생각나는 게, N+1문제LazyInitializationException이잖아요??

2. NestJS의 Transaction❓

1) LazyInitializationException의 가능성은 없음?

그래서 레이어 분리해봤다.

///////////// entity-test.controller.ts
  @Get("lazy")
  async lazyLoading(): Promise<void> {
    const comment = await this.entityTestService.lazy();
    console.log("comment", comment);
    const board = await comment.board;
    console.log("board", board);
  }

///////////// entity-test.service.ts
  async lazy(): Promise<Comment> {
    return await this.commentRepository.findOneBy({ id: 4 });
  }

잘만된다(컬쳐쇼크)

이거 Spring Boot였으면 LazyInitializationException 각인데...❓
트랜잭션이 뭐 어떻게 돼있는 건지..? 심지어 난 아무것도 안했는데...!

2) NestJS와 Spring Boot의 트랜잭션 범위

NestJS, Spring Boot 서버가 모두 서비스 레이어 패턴이라고 해보자.
각각의 (기본적인) 트랜잭션 범위는 다음과 같다.

.Spring BootNestJS
ControllerXO
ServiceOO
RepositoryOO

즉, Spring Boot의 트랜잭션 범위는 서비스 레이어에서 관리한다.
NestJSHTTP 요청 단위로 트랜잭션이 생성되고 응답 종료 시 끝난다.
내가 뭘 할 필요는 없고 NestJS가 알아서 한다.

LazyInitializationException은 트랜잭션 종료 후 프록시 객체에 접근해서 발생하는 문제다.
NestJS는 트랜잭션 범위가 요청 전체라 LazyInitializationException에서 보다 자유롭다.

3. 각종 컬처쇼크 정리✏️

1) TypeORM은 default fetchType 없다. 이분법 타파하자 ㅋㅋㅋ
2) TypeORM의 lazy loading은 Promise로 구현되어 있다.
3) NestJS에서는 LazyInitializationException을 내는 게 안 내는 것보다 어렵다
4) 트랜잭션 범위가 엄청 크기 때문이다
5) NestJS는 컴포넌트 스캔은 나한테 시키면서 트랜잭션은 지가 알아서한다.

아 Nest 정말 넘 희한하고 재밌다 ㅋㅋㅋ

0개의 댓글