Active Record Pattern을 애플리케이션 레이어에서 캡슐화를 시켜야 할까
궁금한게, FindOrdersByUserIdUseCase
같이 구현이 한 줄로 끝날 것 같은 단순한 쿼리 스크립트도 꼭 굳이 캡슐화를 해야할까? 게다가 ORM을 쓰는 경우라면 더욱이 Repository 패턴을 자동으로 사용하게 될텐데?
TL;DR#
- 다음 조건에 해당하는 액티브 레코드 패턴들은 캡슐화를 하게 되면 우발적 복잡성이 늘어날 수 있다:
- 읽기만 수행
- 비즈니스 로직이 없음
- 너무 단순해서 한 줄에 끝남
- 단순한 액티브 레코드 패턴들을 다음과 같이 리팩터링하라:
- Application Service에 읽기전용 연산들을 묶어두라
- 순수 읽기만 수행하고 비즈니스 로직이 없는 경우, 특히나 ORM을 사용하는 경우, 애플리케이션 계층에서 직접 액티브 레코드 패턴 스크립트를 작성하는 것을 허용한다.
- 때로 읽기 작업이 데이터베이스에 부담이 될때 Query Object를 사용하는 것을 권장한다.
- TypeORM같이 레포지토리 패턴을 라이브러리 단에서 지원하는 경우, 레포지토리를 상속하여 커스텀 레포지토리를 사용하는 방법을 고안할 수 있다.
GPT 설전#
You're absolutely right—creating a dedicated UseCase
class for something as simple as FindOrderByUserIdUseCase
might feel like over-engineering and can lead to unnecessary complexity, often referred to as "accidental complexity." In such scenarios, you can adopt a more pragmatic approach while still respecting the principles of separation of concerns and maintainability.
Here’s how you can streamline your design:
1. Combine Simple Queries into Application Services#
Instead of creating a specific UseCase class for a single-line query, you can use an Application Service or a shared service class to house such simple, read-only operations.
Example:#
class OrderService {
constructor(private readonly orderRepository: OrderRepository) {}
findOrdersByUserId(userId: string): Promise<Order[]> {
return this.orderRepository.findByUserId(userId);
}
}
- Benefits:
- Reduces the need for multiple trivial UseCase classes.
- Keeps the focus on the domain when business logic is more significant.
- Simplifies the codebase without violating SRP (Single Responsibility Principle).
2. Direct Repository Access in Specific Layers#
In cases where the operation is purely a read query (and doesn't involve business logic), it's acceptable to directly access the repository in the Application Layer, especially if you're using TypeORM.
Example:#
class OrderController {
constructor(private readonly orderRepository: OrderRepository) {}
async getOrdersByUserId(req: Request, res: Response) {
const userId = req.params.userId;
const orders = await this.orderRepository.findByUserId(userId);
res.json(orders);
}
}
- Caveat: Ensure that this approach is used only for simple queries and does not introduce business logic into controllers or the repository.
3. Use Query Objects for Read-Only Queries#
For read-heavy operations with potential reuse, a Query Object Pattern can abstract the query logic while avoiding full-fledged UseCase classes.
Example:#
class FindOrdersByUserIdQuery {
constructor(private readonly orderRepository: OrderRepository) {}
execute(userId: string): Promise<Order[]> {
return this.orderRepository.findByUserId(userId);
}
}
- This provides a clean interface for queries without overloading the domain or application layers.
4. Avoid "Obtrusive UseCase Classes" with Generalization#
For trivial use cases, consider grouping similar operations under a more generic application service or use case.
Example:#
class OrderUseCase {
constructor(private readonly orderRepository: OrderRepository) {}
findOrdersByUserId(userId: string): Promise<Order[]> {
return this.orderRepository.findByUserId(userId);
}
createOrder(orderData: CreateOrderDto): Promise<Order> {
const order = this.orderRepository.create(orderData);
return this.orderRepository.save(order);
}
}
- When to Apply: Grouping related operations is a pragmatic choice when individual classes would add unnecessary complexity.
5. Leverage TypeORM Features#
TypeORM provides convenient ways to simplify your repository logic using decorators and query builders. Use them to streamline queries while keeping domain logic separate.
Example:#
@EntityRepository(Order)
export class OrderRepository extends Repository<Order> {
findByUserId(userId: string): Promise<Order[]> {
return this.createQueryBuilder("order")
.where("order.userId = :userId", { userId })
.getMany();
}
}
Key Takeaways#
- Avoid over-engineering trivial UseCases. Use Application Services or group similar functionality.
- For simple queries, direct repository access in the Application Layer is acceptable if kept clean and consistent.
- For read-heavy applications, consider the Query Object Pattern.
- Be pragmatic: Overhead can harm maintainability more than it helps with adherence to theory.
Choose the approach that balances simplicity, readability, and scalability.