728x90
실무에서 인터페이스 기반 UserRepository 구조를 선택한 이유
최근 Node.js + TypeORM 프로젝트에서 사용자 정보를 가져오는 UserRepository 구조를 리팩토링하면서, 이전까지 사용하던 단순한 방식 대신 인터페이스 기반 추상화 구조를 도입했습니다.
// Before - 서비스에서 직접 TypeORM 사용
const userRepo = AppDataSource.getRepository(User);
const user = await userRepo.findOneBy({ id: 1 });
처음에는 이렇게 사용하는 게 더 간단하고 빨라 보였습니다. 하지만 기능이 점점 복잡해지면서 다음과 같은 문제점들이 누적되기 시작했습니다.
기존 방식의 문제점
- 비즈니스 로직과 DB 접근 로직이 섞임
- 중복된 쿼리 로직이 여러 곳에 퍼짐
- Mock Repository를 주입하기 어려워 테스트가 어려움
- 향후 DB 교체나 구조 변경 시 리팩토링 비용이 큼
- OOP 5대 원칙(SOLID) 중 SRP, DIP를 지키기 어려움
그래서 이렇게 바꿨습니다
구조 개요
// IUserRepository.ts
export interface IUserRepository {
findById(id: number): Promise<User | null>;
}
// UserRepository.ts
export class UserRepository implements IUserRepository {
private repo = AppDataSource.getRepository(User);
async findById(id: number): Promise<User | null> {
return await this.repo.findOneBy({ id });
}
}
왜 이 구조가 더 좋은가?
1. 역할 분리 (SRP 원칙 실현)
서비스는 무엇을 할지에 집중하고, 레포지토리는 어떻게 조회할지만 책임집니다.
// 서비스에서는 이렇게만 사용
const user = await this.userRepository.findById(userId);
→ SRP(Single Responsibility Principle, 단일 책임 원칙) 지킴
2. 인터페이스 기반 추상화 (OCP / DIP 실현)
const repo: IUserRepository = new UserRepository();
- 테스트에서는 Mock을 쉽게 주입 가능
- 향후 MongoDB, InMemory 구조로 교체해도 서비스 변경 없음
→ OCP(개방-폐쇄 원칙), DIP(의존 역전 원칙) 실현
3. 테스트가 쉬워짐
const mockUserRepository: IUserRepository = {
findById: jest.fn().mockResolvedValue(mockUser),
};
→ DB 없이 유닛 테스트 가능
→ 테스트 속도 및 안정성 확보
4. DB 접근 캡슐화
await this.repo.findOneBy({ id });
- 쿼리빌더, soft delete, relations 등 복잡한 쿼리들도
UserRepository에만 집중되므로 DB 접근 로직의 일관성 유지
5. 확장성
필요에 따라 다음과 같은 메서드를 추가해도,
서비스 코드에는 변경이 없습니다.
async findWithProfile(id: number): Promise<User | null> {
return await this.repo.findOne({
where: { id },
relations: ['profile'],
});
}
✋ Repository를 직접 사용하는 방식과의 비교
항목 | 인터페이스 기반 구조 | Repository 직접 사용 |
SRP 준수 | O | X |
테스트 용이성 | O (Mock 주입 가능) | X (DB 의존적) |
OCP / DIP | O (유연한 대체 가능) | X (강결합) |
DB 접근 일관성 | O | X |
쿼리 확장성 | O | X |
마치며
처음에는 AppDataSource.getRepository(User)를 직접 사용하는 게 간편해 보였습니다.
하지만 유지보수성과 테스트, 확장성을 고려하면, 인터페이스 기반의 Repository 추상화 구조는 반드시 필요한 설계 패턴입니다.
서비스와 DB 접근을 분리하고, 테스트 가능성과 유연성을 확보하고 싶다면,
지금이라도 IUserRepository + UserRepository 구조를 도입해보세요.
반응형
'개발 기록 > 회고' 카테고리의 다른 글
Spring Service에서 객체 생성 책임을 분리해야 하는 이유 – 실무에서 Factory 패턴을 도입한 회고 (1) | 2025.05.29 |
---|---|
Spring MVC Controller에서 new를 없애야 하는 진짜 이유 (0) | 2025.05.28 |
Spring Boot 입문기 – @Getter, @Setter, 그리고 Controller 어노테이션을 처음 마주했을 때 (2) | 2025.05.26 |
TokenService는 왜 static이어야 했을까? 객체지향을 넘나드는 JS 설계 회고 (0) | 2025.05.12 |
Node.js 인증 서비스의 구조적 문제와 개선을 위한 리팩토링 여정 (0) | 2025.05.08 |