TypeORM을 사용한 이유는 개발의 편의성을 높이기 위한 이유가 컸다.
TypeORM의 @Entity
, @PrimaryGeneratedColumn
, @Column
등의 데코레이터를 통해 데이터베이스 테이블을 TypeScript 클래스에 매핑 가능하다.
TypeORM을 통해 객체 간의 연관 관계를 @OneToMany
, @ManyToOne
, @ManyToMany
, @OneToOne
와 같은 데코레이터를 통해 표현 가능하다. 이를 통해 연관된 객체를 자동으로 불러올 수 있다.
SQL 쿼리문을 직접 작성하지 않고, typeorm에서 제공하는 메소드(find
, store
, delete
)를 통해서 데이터베이스에 접근할 수 있다. 이러한 기능은 추후 DB가 변경되었을 때, SQL문을 수정할 필요가 없어서 DB에 의존하지 않는 코드를 작성할 수 있다.
다음과 같은 엔티티 클래스를 통해 DB 상에 매핑할 수 있다.
typeorm 모듈 옵션에 logging 옵션을 true로 전달한다.
//back/src/config/typeOrmConfig.ts
const ormConfig: TypeOrmModuleOptions = {
...
logging: true,
};
다음과 같이 백엔드 서버에 로깅이 남는다.
2024-11-22 02:10:02 nest_container | query:
SELECT `Program`.`id` AS `Program_id`, `Program`.`name` AS `Program_name`, `Program`.`profile_url` AS `Program_profile_url`, `Program`.`running_time` AS `Program_running_time`, `Program`.`genre` AS `Program_genre`, `Program`.`actors` AS `Program_actors`, `Program`.`price` AS `Program_price`, `Program`.`place_id` AS `Program_place_id`
FROM `Program` `Program`
2024-11-22 02:10:02 nest_container | query:
SELECT `place`.`id` AS `place_id`, `place`.`name` AS `place_name`, `place`.`address` AS `place_address`, `place`.`overview_svg` AS `place_overview_svg`, `place`.`overview_height` AS `place_overview_height`, `place`.`overview_width` AS `place_overview_width`, `place`.`sections` AS `place_sections`, `place`.`overview_points` AS `place_overview_points`
FROM `Place` `place` INNER JOIN `Program` `Program`
ON `Program`.`place_id` = `place`.`id`
WHERE `Program`.`id` IN (?) -- PARAMETERS: [1]
2024-11-22 02:10:02 nest_container | [RealTicket] 31 11/21/2024, 5:10:02 PM LOG [HttpRequest] GET /program 200 - 35ms +29s
현재 TypeORM의 로딩 정책은 지연 로딩(Lazy Loading)이다.
지연 로딩을 선택한 이유는 다음과 같다.
지연 로딩이 아니라면 로딩 정책이 EAGER로 설정되는데, 이는 하나의 엔티티를 조회하게 되면 해당 엔티티와 연관된 모든 엔티티를 끌어오게 된다.
만약 모든 엔티티를 끌어오지만 해당 엔티티를 사용하지 않는다면 불필요한 조인이 발생하게 된다.
조인의 비용은 상당하기 때문에 불필요한 조인을 최소화하는 것이 중요하다.
또한 만약에 양쪽 엔티티에서 서로를 eager 정책으로 로딩하게 되면 순환 참조가 발생해 에러가 난다.
반면 지연 로딩은 불필요한 조인, 순환 참조 문제를 해결하며 효율적으로 엔티티를 끌어올 수 있다.
@Entity({ name: 'Program' })
export class Program {
...
@ManyToOne(() => Place, (place) => place.programs, { lazy: true })
@JoinColumn({ name: 'place_id', referencedColumnName: 'id' })
place: Promise<Place>;
...
}
@Entity({ name: 'Place' })
export class Place {
...
@OneToMany(() => Program, (program) => program.place, { lazy: true })
programs: Promise<Program[]>;
...
}
실제로 쿼리를 살펴보면, 엔티티를 조인해서 끌어오기 보다 아이디를 그대로 저장하고 있다.
SELECT `Program`.`id` AS `Program_id`,
`Program`.`name` AS `Program_name`,
`Program`.`profile_url` AS `Program_profile_url`,
`Program`.`running_time` AS `Program_running_time`,
`Program`.`genre` AS `Program_genre`,
`Program`.`actors` AS `Program_actors`,
`Program`.`price` AS `Program_price`,
`Program`.`place_id` AS `Program_place_id` // 아이디를 저장한다.
FROM `Program` `Program`
그렇다면 지연 로딩에서 필요한 연관된 엔티티를 어떻게 끌어올 수 있을까?
단순 await를 통한 로딩
다음과 같이 await를 통해 연관된 엔티티를 가지고 올 수 있다.
async findMainPageProgramData() {
const programs: Program[] = await this.programRepository.selectAllProgram();//첫번째 쿼리
return programMainPageDtos: ProgramMainPageDto[] = await Promise.all(
programs.map(async (program: Program) => {
const place = await program.place; //2번째 쿼리
return new ProgramMainPageDto({
...program,
place: new PlaceMainPageDto(place),
});
}),
);
}
연관된 엔티티를 끌어오기 위해 쿼리를 다시 날리기 때문에 비효율적이다. → 쿼리 통신량 증가, 다시 쿼리를 만들어서 보내는데까지 시간 낭비
2024-11-22 02:16:54 nest_container | query: SELECT `Program`.`id` AS `Program_id`, `Program`.`name` AS `Program_name`, `Program`.`profile_url` AS `Program_profile_url`, `Program`.`running_time` AS `Program_running_time`, `Program`.`genre` AS `Program_genre`, `Program`.`actors` AS `Program_actors`, `Program`.`price` AS `Program_price`, `Program`.`place_id` AS `Program_place_id` FROM `Program` `Program`
2024-11-22 02:16:54 nest_container | query: SELECT `place`.`id` AS `place_id`, `place`.`name` AS `place_name`, `place`.`address` AS `place_address`, `place`.`overview_svg` AS `place_overview_svg`, `place`.`overview_height` AS `place_overview_height`, `place`.`overview_width` AS `place_overview_width`, `place`.`sections` AS `place_sections`, `place`.`overview_points` AS `place_overview_points` FROM `Place` `place` INNER JOIN `Program` `Program` ON `Program`.`place_id` = `place`.`id` WHERE `Program`.`id` IN (?) -- PARAMETERS: [1]
join의 기능을 전혀 사용하지 못하게 된다.
typeorm의 find메서드에 relations 옵션 추가
async selectAllProgram(): Promise<Program[]> {
return await this.ProgramRepository.find({
relations: ['place']
});
}
엔티티 호출은 그대로 await를 통해 연관된 엔티티에 접근하지만 추가 쿼리만 만들어지지 않는다.
쿼리를 확인해보면, join을 통해 하나의 쿼리로 끌어오는 것을 알 수 있다. 즉, 선택적으로 필요한 부분만 EAGER 로딩 정책을 적용하고 있다.
2024-11-22 02:42:59 nest_container | query:
SELECT `Program`.`id` AS `Program_id`,
`Program`.`name` AS `Program_name`,
`Program`.`profile_url` AS `Program_profile_url`,
`Program`.`running_time` AS `Program_running_time`,
`Program`.`genre` AS `Program_genre`,
`Program`.`actors` AS `Program_actors`,
`Program`.`price` AS `Program_price`,
`Program`.`place_id` AS `Program_place_id`,
`Program__Program_place`.`id` AS `Program__Program_place_id`,
`Program__Program_place`.`name` AS `Program__Program_place_name`,
`Program__Program_place`.`address` AS `Program__Program_place_address`,
`Program__Program_place`.`overview_svg` AS `Program__Program_place_overview_svg`,
`Program__Program_place`.`overview_height` AS `Program__Program_place_overview_height`,
`Program__Program_place`.`overview_width` AS `Program__Program_place_overview_width`,
`Program__Program_place`.`sections` AS `Program__Program_place_sections`,
`Program__Program_place`.`overview_points` AS `Program__Program_place_overview_points`
FROM `Program` `Program` LEFT JOIN `Place` `Program__Program_place`
ON `Program__Program_place`.`id`=`Program`.`place_id`
QueryBuilder를 통해 직접 커스텀할 수도 있다.