현재 회사에서 데이터베이스를 다룰 일이 있으면 TypeORM + MySQL으로 조합해서 사용하고 있다. ORM 특성상 객체지향적으로 데이터베이스를 다룰 수 있기 때문에 좀 더 직관적이고 쿼리 짜는 게 편리한 장점이 있다. 하지만 작성되어 있는 ORM 쿼리 실행문을 보다 보면 불필요한 SELECT 쿼리가 실행되는 문제가 있었다.
1. 불필요한 SELECT 문 실행 문제
아래는 불필요한 SELECT 문이 실행되는 부분의 코드이다.
const result = await this.hashTagsRepository.findOne({ name: 'test1' });
result.name = 'test2';
await this.hashTagsRepository.save(result);
ORM을 사용해서 특정 데이터를 찾아오고, 해당 데이터의 값을 변경 후 save() 하는 로직이다. ORM를 사용하게 되면 이런 직관적인 객체형태의 쿼리를 사용할 수 있게 되는데 내부에서 실행되는 로우쿼리를 보면 아래처럼 SELECT 문이 두 번 실행되는 문제가 있었다.
SELECT `HashTags`.`id` AS `HashTags_id`, `HashTags`.`name` AS `HashTags_name` FROM `HashTags` `HashTags` WHERE `HashTags`.`name` = ? LIMIT 1 -- PARAMETERS: ["test1"]
SELECT `HashTags`.`id` AS `HashTags_id`, `HashTags`.`name` AS `HashTags_name` FROM `HashTags` `HashTags` WHERE `HashTags`.`id` IN (?) -- PARAMETERS: [12]
START TRANSACTION
UPDATE `HashTags` SET `name` = ? WHERE `id` IN (?) -- PARAMETERS: ["test2",12]
COMMIT
해당 로우 쿼리를 보면 TypeORM의 save() 메서드는 특정 아이디 값으로 데이터를 찾아와 바뀐 객체를 저장하는 걸 알 수 있다. 찾아보니 PK가 있는 데이터를 저장할 때 id값으로 데이터를 찾은 후 업데이트 하는 현상이라고 하는데 사실 SELECT 문은 findOne() 부분에서 이미 한 번 실행됐기 때문에 save()에서 반복적으로 실행될 경우 리소스가 낭비되는 문제가 있었다.
2. findOne, save VS update
사실 지금까지는 쿼리로 찾아온 데이터를 객체로 관리하는 게 훨씬 편하다고 느껴서 findOne() -> save()의 형태를 사용했었다. 하지만 불필요한 SELECT 문의 실행이 있기 때문에 update() 메서드로 변경하고 두 조건의 속도를 비교해 봤다.
3. findOne, save 속도 측정
먼저 쿼리 작성 부분과 속도 측정 부분 코드이다. 속도 측정은 JS 내장 객체 performance 사용해서 측정했다.
// 데이터베이스 쿼리 부분
async test2() {
const result = await this.hashTagsRepository.findOne({ name: 'test1' });
result.name = 'test2';
await this.hashTagsRepository.save(result);
}
// 속도 측정 부분
async test2() {
const start = performance.now();
await this.tagsService.test2();
const end = performance.now();
const executionTime = end - start;
console.log(executionTime);
}
아래는 실행된 로우 쿼리와 속도 결과 값이다.
SELECT `HashTags`.`id` AS `HashTags_id`, `HashTags`.`name` AS `HashTags_name` FROM `HashTags` `HashTags` WHERE `HashTags`.`name` = ? LIMIT 1 -- PARAMETERS: ["test1"]
SELECT `HashTags`.`id` AS `HashTags_id`, `HashTags`.`name` AS `HashTags_name` FROM `HashTags` `HashTags` WHERE `HashTags`.`id` IN (?) -- PARAMETERS: [19]
START TRANSACTION
UPDATE `HashTags` SET `name` = ? WHERE `id` IN (?) -- PARAMETERS: ["test2",19]
COMMIT
// 속도 측정 결과 : 23.628125190734863
4. update 속도 측정
쿼리 작성은 createQueryBuilder() 사용해서 작성했다. where 조건으로 name이 test1 인걸 찾아서 test2로 변경하는 쿼리이다.
// 데이터베이스 쿼리 부분
async test3() {
await this.hashTagsRepository
.createQueryBuilder()
.update(HashTags)
.set({ name: 'test2' })
.where('name = :name', { name: 'test1' })
.execute();
}
// 속도 측정 부분
async test3() {
const start = performance.now();
await this.tagsService.test3();
const end = performance.now();
const executionTime = end - start;
console.log(executionTime);
}
아래는 로우 쿼리와 속도 결과 값이다. update 쿼리의 경우 SELECT 문의 실행 없이 바로 UPDATE 쿼리를 통해서 데이터를 업데이트하고 있어 속도 결과가 좀 더 빠르게 나왔다.
UPDATE `HashTags` SET `name` = ? WHERE `name` = ? -- PARAMETERS: ["test2","test1"]
// 속도 측정 결과 : 14.257957935333252
이처럼 update 쿼리로 변경해 쿼리 실행 속도가 빨라지는 것을 볼 수 있었다. 사실 결과만 놓고 보면 너무 당연한 거라고 생각할 수 있지만 별생각 없이 사용하던 TypeORM 쿼리 메서드들도 정말 단순한 방법으로 성능을 향상할 수 있다는 게 놀라웠다. 앞으로 단순히 기능 구현만 돌리지 않고 ORM 사용환경에서도 최대한의 최적화를 진행해 봐야겠다.
'DATABASE' 카테고리의 다른 글
Data Mapper 패턴, Active Record패턴 (0) | 2023.02.24 |
---|