회사에서 데이터베이스 커넥션을 받고 쿼리를 작성해서 CRUD를 진행했습니다. TypeORM을 한 번도 사용해보지 않은 저는 어떤 방식으로 데이터를 건드릴 수 있을지 찾아보았습니다.
물론 이전 글에서 어떻게 접근하는지 약간씩 등장하긴 합니다.
자 이제부터, 데이터에 접근할 수 있는 방법 4가지에 대해서 글을 작성하도록 해보겠습니다.
4가지가 무엇인지 먼저 말씀드리고 글을 작성하겠습니다.
첫번째, QueryRunner
두 번째, Entity Manager
세 번째, Repository
네 번째, Query Builder
순서대로 설명드리겠습니다.
약간의 배경지식
코드에서 맨 위에 나오고 객체 불러올때 쓰이는 DataSource란 무엇인가?
TypeORM에서 데이터베이스 연결 설정을 관리하는 객체입니다. 연결한 데이터베이스의 유형, 호스트, 포트, 사용자명, 비밀번호, 데이터베이스 이름등의 설정을 포함합니다. DataSource를 통해 TypeORM은 데이터베이스에 연결하고, 연결 풀을 관리하며, 엔티티와 매핑을 수행합니다.
Query Runner
QueryRunner란 무엇인가?
데이터베이스에서 직접 SQL 쿼리를 실행할 수 있는 객체입니다. 트랜잭션을 관리하고 데이터베이스와의 상호작용을 더 세밀하게 제어할 수 있도록 합니다.
일반적으로 QueryRunner는 DataSource이 제공하는 커넥션 풀(Connection Pool)에서 하나의 커넥션을 가져와 사용합니다. 또한, 데이터베이스 연결을 직접 제어하고 트랜잭션을 수동으로 관리할 수 있게 해 줍니다.
커넥션 풀(Connection Pool)이란 무엇인가? 왜 사용하는가?
커넥션을 한번의 실행마다 한 번씩 맺는 것은 상당한 리소스를 부담하게 됩니다. 특히나 TCP를 사용하기 때문에 3 way handshake와 4 way handshake로 속도 또한 느리고 부담이 가는 방식이기 때문에 커넥션을 한 번에 많이 만들어놓고 커넥션을 한 번씩 빌려서 사용하는 것입니다.
트랜잭션 관리가 수동이지만, 명시적으로 트랜잭션을 관리하기 때문에 시작 커밋과 롤백을 지정할 수 있어 예상할 수 있는 흐름을 작성할 수 있습니다. 또한, 직접 SQL 쿼리를 작성할 수 있기 때문에 저수준의 데이터베이스 조작이 필요할때 유용할 것입니다. 이러한 이유로 복잡한 트랜잭션이나 고성능 쿼리 작업을 수행할 때 사용됩니다.
항상 저수준이라고 한다면? 성능의 최적화와 연관이 많습니다.
예를 들어서, 엄청 들어 맞는다 안 맞는 예시라고 말할 순 없지만 이건 유통과 비슷합니다. 우리나라의 문제는 유통과정이 아주 아주 아주 복잡하게 연관이 되어있다는 것이죠? 그래서 실제로 소비자가 물건을 받았을 때 가격이 상당히 높게 책정되어 있는 것입니다.
여기서 최적화라고 하는 것을 "싼 가격에 좋은 품질"이라고 보았을때 유통업체를 거치는 것보다 현지에서 직접 찾아가 좋은 제품을 고를 수 있다면? "가격을 최적화해서 사는 것"이 되겠죠?
이런것과 유사하다고 생각합니다. 저수준의 퍼포먼스는 직접 지역에 찾아간다는 것이고 이러한 퍼포먼스를 보여준다면? 우리는 최적화된 즉, 싼 가격에 제품을 받을 수 있다는 것을 생각할 수 있습니다.
QueryRunner는 제가 회사에서 직접 커넥션을 받아오고, 쿼리를 작성하는 것과 유사했습니다. 그래서 조금은 더 이해가 잘 갔던 것 같습니다. 아래의 코드 예시를 보면서 QueryRunner의 작성법을 보겠습니다.
QueryRunner를 사용한 CRUD입니다.
수동으로 Transaction을 섞은 내용까지 있습니다. 그리고 findOneBy, save, remove와 같은 함수들은 자주 사용하다보면 익숙해지거나 익숙해지기 전에 마스터를 하고 싶으시다면? 공식 문서를 보시면 자세히 잘 나와있습니다.
const dataSource = new DataSource({
...
});
async function runQueryRunnerExample() {
const queryRunner: QueryRunner = dataSource.createQueryRunner();
// 트랜잭션 시작
await queryRunner.startTransaction();
try {
// CREATE - 새로운 유저 삽입
await queryRunner.manager.save(User, {
name: "John Doe"
});
// READ - 유저 조회
const user = await queryRunner.manager.findOneBy(User, { name: "John Doe" });
console.log("Inserted User:", user);
// UPDATE - 유저 이름 변경
if (user) {
user.name = "Jane Doe";
await queryRunner.manager.save(User, user);
console.log("Updated User:", await queryRunner.manager.findOneBy(User, { id: user.id }));
}
// DELETE - 유저 삭제
if (user) {
await queryRunner.manager.remove(User, user);
console.log("Deleted User:", await queryRunner.manager.findOneBy(User, { id: user.id }));
}
// 트랜잭션 커밋
await queryRunner.commitTransaction();
} catch (error) {
console.error("Error occurred:", error);
// 오류 발생 시 롤백
await queryRunner.rollbackTransaction();
} finally {
// QueryRunner 릴리즈 (연결 해제)
await queryRunner.release();
}
}
QueryRunner를 이용했을 때 수동으로 SQL 쿼리를 작성할 수 있습니다.
async function updateUserStatus(userId: number) {
const queryRunner: QueryRunner = dataSource.createQueryRunner();
// 1. 트랜잭션 시작
await queryRunner.startTransaction();
try {
// 2. 직접 SQL 쿼리 실행
await queryRunner.query('UPDATE users SET isActive = 1 WHERE id = ?', [userId]);
// 3. 트랜잭션 커밋
await queryRunner.commitTransaction();
} catch (error) {
// 4. 트랜잭션 롤백
await queryRunner.rollbackTransaction();
console.error('Transaction failed:', error);
} finally {
// 5. QueryRunner 해제 (커넥션 반환)
await queryRunner.release();
}
}
Entity Manager
Entity Manager란 무엇인가?
데이터베이스 작업을 수행하는 데 사용되는 TypeORM의 기본 클래스입니다. Entity Manager를 사용하면 엔티티에 대해 CRUD 작업을 수행할 수 있으며, 이를 통해 데이터베이스와 상호작용 합니다.
Entity Manager는 사용이 간편하며 직관적이고 CRUD 작업에 최적화 되어있습니다. 위에서 작성했던 함수인 save, find, delete 등의 메서드를 통해 직관적으로 수행할 수 있게 됩니다. 이는 코드의 가독성을 높여줄 수 있습니다.
하지만, 트랜잭션 관리를 포함한 세밀한 제어는 자동으로 역시나 처리되지 않습니다. 트랜잭션은 직접 해줘야 합니다.
아래 EntityManager를 사용한 코드를 살펴보겠습니다.
여기에선 간단한 save, find, delete에 대한 내용은 작성하지 않았습니다. QueryRunner와 유사하기 때문입니다.
아래 코드 내용을 보면서 느낀점은 회사 프로젝트를 진행하며 트랜잭션을 작성할 때 긴 쿼리문을 전부 작성하면서 계속해서 코드가 늘어나고 점점 복잡해지는 현상을 많이 겪었습니다. 현재도 겪고 있고요. 이 내용을 보면서 typeORM을 사용한다면 이러한 부분을 조금이라도 줄여서 피로도를 낮출 수 있지 않을까?라는 생각하게 되었습니다.
async function createUserWithProduct() {
const manager = dataSource.manager; // EntityManager 인스턴스
// 트랜잭션을 수동으로 관리
await manager.transaction(async (transactionalEntityManager) => {
const user = new User();
user.name = "John Doe";
const savedUser = await transactionalEntityManager.save(user);
const product = new Product();
product.name = "Gadget";
product.user = savedUser;
await transactionalEntityManager.save(product);
});
}
Repository
Repository란 무엇인가?
특정 엔티티 타입에 대해 데이터베이스 작업을 수행할 수 있는 클래스입니다. Repository는 EntityManager의 기능을 상속받습니다. 또한, 특정 엔티티에 특화된 메서드를 제공합니다. 이는 엔티티 단위로 작업을 수행하기에 적합합니다. 예를 들어 findOneBy와 같은 메서드는 특정 조건에 맞는 엔티티를 쉽게 찾을 수 있게 해 줍니다.
역시나, 트랜잭션은 수동으로 관리해야하며 세밀한 제어가 어려운 점이 있습니다. 엔티티 단위로 작업을 수행하는 데 중점을 두고 있기 때문입니다. 복잡한 트랜잭션이나 엔티티가 아닌 작업과 세밀한 제어가 필요할 때 제약이 있을 수 있습니다.
Repository는 기본적인 것 같습니다. 기본으로 사용하는 느낌?
async function createUser() {
// User 엔티티에 대한 Repository 생성
const userRepository: Repository<User> = dataSource.getRepository(User);
const user = new User();
user.name = "John Doe";
// User 엔티티 저장
await userRepository.save(user);
console.log('User saved:', user);
}
async function findUserByName(userName: string) {
// User 엔티티에 대한 특정 메서드 사용
const userRepository: Repository<User> = dataSource.getRepository(User);
const user = await userRepository.findOneBy({ name: userName });
console.log('User found:', user);
}
QueryBuilder
QueryBuilder란 무엇인가?
이전 글에서도 보셨을지 모르겠지만? 강력한 QueryBuilder로 쿼리를 생성해 문제를 해결한다고 작성되어 있습니다. 아마도요. 그리고 공식문서에는 QueryBuilder만 상세하게 작성되어 있네요? 확실히 밀어주는 느낌입니다.
아래부터 설명을 시작해보겠습니다.
QueryBuilder는 동적쿼리를 생성할 수 있습니다. 조건에 따라 쿼리를 동적으로 생성해야 할 때 매우 유용합니다. 복잡한 조건문이나 조인을 포함한 쿼리를 작성할 때, 코드의 가독성과 유지보수성을 높일 수 있습니다. 이 말뜻은 직관적이고 명확하게 코드를 작성할 수 있다는 것을 의미합니다.또한, 조건문(where), 정렬(order by), 그룹핑(group by) 등의 고급 쿼리 기능을 제공해 복잡한 쿼리를 쉽게 작성할 수 있습니다.
QueryBuilder는 복잡한 동적 쿼리를 생성할 수 있으며, 코드의 가독성과 유지보수성을 높여주기까지 합니다. 여러 테이블 간의 조인이나 관계가 있는 경우에 매우 유용할 것으로 보입니다.
아래는 QueryBuilder 코드 예시입니다.
현업에서 조인, 조건, 그룹 같은 것은 자주 사용됩니다. 그럴때마다 쿼리를 작성하는 것은 문제가 되지 않습니다. 하지만 이렇게 작성되어서 최적화된 쿼리를 만들어줄 수 있다면? 당연히 사용할 수 있겠습니다.
하지만, ORM의 최적화된 쿼리가 항상 최적화를 이끌어 내는 것은 아니기 때문에 항상 실행계획(explain)을 통해 최적화도 생각해 보아야 합니다.
const dataSource = new DataSource({
...
});
async function getUsersWithOrders(minTotal: number) {
// QueryBuilder를 사용하여 동적 쿼리 생성
const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.leftJoinAndSelect("user.orders", "order") // User와 Order 조인
.where("order.total > :minTotal", { minTotal }) // 조건 추가
.orderBy("user.name", "ASC") // 정렬
.getMany();
return users;
}
위에 작성한 네가지를 정리해 작성해 보겠습니다.
QueryRunner, Repository, Entity Manager 중 QueryRunner는 트랜잭션 관리나 복잡한 쿼리 실행이 필요한 경우 유용하지만, 일반적인 CRUD 작업에는 Entity Manager나 Repository를 사용하는 것이 더 간편하고 효율적입니다. 그리고 QueryBuilder는 동적 쿼리 생성, 조인, 조건문, 정렬 및 그룹핑 등 복잡한 쿼리 작업이 필요한 경우에 유용합니다.
각 방법은 특정 용도에 맞게 설계되었으므로, 사용 사례에 따라 적절한 방법을 선택하는 것이 중요합니다. 한 가지만 사용하지 않고 다양하게 사용하여 주어진 문제를 알맞게 해결할 수 있을 것 같습니다. 필요에 따라서 적용해 문제를 해결해 볼 수 있겠습니다.
'Project > TypeORM' 카테고리의 다른 글
[번역] OneToMany / ManyToOne Relations (0) | 2024.08.21 |
---|---|
[번역] @OneToOne Relations (0) | 2024.08.19 |
왜 ORM을 사용하려고 하는데요? (0) | 2024.08.14 |
너랑 나랑 무슨 관계(relation)야? (0) | 2024.08.13 |
왜냐하면, ORM을 사용하기로 했다. (0) | 2024.08.12 |