[Let's Git It] Repository DIP 패턴 — 기술 독립적인 Repository 설계

dobby·2026년 5월 3일

Let's git it BE

목록 보기
12/20

Let's Git It 프로젝트에서 팀이 채택한 Repository 구조 설계 방식을 정리합니다.
JPA/QueryDSL에 종속되지 않는 Service를 만들기 위한 4-file DIP 패턴을 다룹니다.


문제 인식

처음 MemberRepository를 이렇게 작성했다.

public interface MemberRepository extends JpaRepository<Member, UUID> {
    Optional<Member> findByEmail(String email);
    boolean existsByEmail(String email);
}

AuthServiceImpl에서 이렇게 주입받아 사용했다.

@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {
    private final MemberRepository memberRepository; // JPA 직접 의존
}

언뜻 보면 문제없어 보이지만, 여기에는 기술 종속성 문제가 있다.


무엇이 문제인가

MemberRepositoryJpaRepository를 상속하는 순간, Service가 JPA라는 기술을 직접 알게 된다.

Service → MemberRepository (extends JpaRepository) → JPA 기술

나중에 JPA를 QueryDSL로 바꾸거나, 특정 조회를 MongoDB로 처리하고 싶다면?
MemberRepository 인터페이스 자체를 수정해야 하고, 이에 의존하는 Service도 영향을 받는다.


해결 방법 — 4-file DIP 패턴

팀에서 채택한 구조는 Repository를 4개 파일로 분리한다.

MemberRepository.java       ← Service가 의존하는 순수 인터페이스
MemberRepositoryImpl.java   ← 구현체 (JPA + QueryDSL 조합)
MemberJpaRepository.java    ← Spring Data JPA 인터페이스
MemberDslRepository.java    ← QueryDSL 복잡한 조회 (필요 시)

핵심은 Service가 순수 인터페이스에만 의존한다는 것이다.

Service → MemberRepository (순수 인터페이스)
                ↑
        MemberRepositoryImpl (JPA + QueryDSL 실제 구현)

실제 코드

MemberRepository.java — Service가 의존하는 순수 인터페이스

package com.gitcat.letsgitit.domain.member.repository;

import java.util.Optional;
import java.util.UUID;

import com.gitcat.letsgitit.domain.member.entity.Member;

public interface MemberRepository {

    Optional<Member> findByEmail(String email);
    boolean existsByEmail(String email);
    Member save(Member member);
    Optional<Member> findById(UUID id);
}

MemberJpaRepository.java — Spring Data JPA

package com.gitcat.letsgitit.domain.member.repository;

import java.util.UUID;

import org.springframework.data.jpa.repository.JpaRepository;

import com.gitcat.letsgitit.domain.member.entity.Member;

public interface MemberJpaRepository extends JpaRepository<Member, UUID> {

    Optional<Member> findByEmail(String email);
    boolean existsByEmail(String email);
}

MemberRepositoryImpl.java — 구현체 (JPA + QueryDSL 조합)

package com.gitcat.letsgitit.domain.member.repository;

import java.util.Optional;
import java.util.UUID;

import org.springframework.stereotype.Repository;

import com.gitcat.letsgitit.domain.member.entity.Member;

import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepository {

    private final MemberJpaRepository memberJpaRepository;
    // private final MemberDslRepository memberDslRepository; // QueryDSL 필요 시 추가

    // 단순 조회 → JPA 위임
    @Override
    public Optional<Member> findByEmail(String email) {
        return memberJpaRepository.findByEmail(email);
    }

    @Override
    public boolean existsByEmail(String email) {
        return memberJpaRepository.existsByEmail(email);
    }

    @Override
    public Member save(Member member) {
        return memberJpaRepository.save(member);
    }

    @Override
    public Optional<Member> findById(UUID id) {
        return memberJpaRepository.findById(id);
    }
}

비유로 이해하기

Service → "이메일로 멤버 찾아줘" (인터페이스에 요청)
              ↓
Impl → "JPA로 찾아올게" or "QueryDSL로 찾아올게"

Service는 "뭘 해줘" 만 알고, "어떻게 해줄게" 는 Impl이 담당한다.

나중에 JPA를 QueryDSL로 바꾸더라도 Impl만 수정하면 되고 Service는 한 줄도 바뀌지 않는다.


변경 전 vs 변경 후

변경 전변경 후
Service 의존 대상JpaRepository 상속 인터페이스순수 Java 인터페이스
JPA → QueryDSL 변경 시Service도 수정 필요Impl만 수정
기술 종속성있음없음
파일 수1개3~4개

도메인 간 의존성과의 차이

이 패턴과 혼동하기 쉬운 개념이 있다.

Repository DIP 패턴 — 기술 독립성

Service → 순수 인터페이스 (JPA/QueryDSL 몰라도 됨)

도메인 간 의존성 규칙 — 도메인 경계 유지

AuthService → MemberRepository (X) : 다른 도메인 Repository 직접 접근
AuthService → MemberService (O)    : 서비스를 통해 접근

둘 다 별개의 규칙이다. Repository DIP는 기술 교체에 대한 유연성이고, 도메인 간 의존성은 도메인 경계를 지키는 것이다.


정리

Before: Service → MemberRepository (extends JpaRepository) → JPA 종속
After:  Service → MemberRepository (순수 인터페이스)
                        ↑
                MemberRepositoryImpl → MemberJpaRepository (JPA)
                                     → MemberDslRepository (QueryDSL, 필요 시)

처음에는 파일이 늘어나서 번거롭게 느껴질 수 있다. 하지만 프로젝트 규모가 커질수록 기술 교체나 쿼리 최적화가 필요한 순간이 생긴다. 그때 Service를 건드리지 않아도 된다는 게 이 구조의 진짜 장점이다.

profile
느리게 한걸음

0개의 댓글