왜 Repository를 직접 쓰지 않고 인터페이스를 쓰는가
2025. 8. 2. 18:42
728x90

실무에서 인터페이스 기반 UserRepository 구조를 선택한 이유

최근 Node.js + TypeORM 프로젝트에서 사용자 정보를 가져오는 UserRepository 구조를 리팩토링하면서, 이전까지 사용하던 단순한 방식 대신 인터페이스 기반 추상화 구조를 도입했습니다.

// Before - 서비스에서 직접 TypeORM 사용
const userRepo = AppDataSource.getRepository(User);
const user = await userRepo.findOneBy({ id: 1 });

처음에는 이렇게 사용하는 게 더 간단하고 빨라 보였습니다. 하지만 기능이 점점 복잡해지면서 다음과 같은 문제점들이 누적되기 시작했습니다.


기존 방식의 문제점

  1. 비즈니스 로직과 DB 접근 로직이 섞임
  2. 중복된 쿼리 로직이 여러 곳에 퍼짐
  3. Mock Repository를 주입하기 어려워 테스트가 어려움
  4. 향후 DB 교체나 구조 변경 시 리팩토링 비용이 큼
  5. 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 구조를 도입해보세요.

반응형