2026. 2. 10. 21:55ㆍSpring Boot/Spring Data JPA
Spring Data JPA 커스텀 리포지토리 🧩
“Repository는 왜 Fragment(조각)로 조립되는가?”
Spring Data Repository는 겉으로는 UserRepository extends JpaRepository<...> 같은 “인터페이스” 하나로 끝나는 것처럼 보이지만, 내부적으로는 여러 조각(Fragment) 을 모아 만든 합성(Composition) 프록시입니다.
여기서 말하는 Fragment는 크게 세 종류입니다:
- ✅ Base Repository (예:
SimpleJpaRepository기반 CRUD 구현) - ✅ Repository Aspects (예: Querydsl, QBE 같은 기능 조각)
- ✅ Custom Fragment Interfaces + Implementations (우리가 직접 만드는 조각)
즉, Repository는 “상속”이 아니라 조립(Composition) 으로 확장됩니다. 🔧
1) Customizing Individual Repositories: 가장 기본 패턴 🧱
1-1. Fragment 인터페이스 + Impl 구현체 만들기
Spring Data에서 개별 리포지토리에 커스텀 기능을 추가하려면, 먼저 Fragment 인터페이스를 만들고, 그에 대응하는 구현체(Impl postfix) 를 만듭니다.
interface CustomizedUserRepository {
void someCustomMethod(User user);
}
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
@Override
public void someCustomMethod(User user) {
// Your custom implementation
}
}
여기서 핵심 규칙 🔥
- 인터페이스 이름 + Impl 이 기본 탐색 규칙입니다.
CustomizedUserRepository→CustomizedUserRepositoryImpl
그리고 postfix(기본값 Impl)는 설정으로 바꿀 수 있습니다:
@Enable<StoreModule>Repositories(repositoryImplementationPostfix = …)
(JPA라면 @EnableJpaRepositories(...))
2) “예전 방식(Deprecated)” — Repository 이름 기반 단일 커스텀 구현 😵💫
문서가 말하는 “Historically…”는 과거 Spring Data가 커스텀 구현을 찾던 방식입니다.
- 과거에는 “리포지토리 인터페이스 이름”에서 커스텀 구현체 이름을 유도해서
사실상 1개의 커스텀 구현만 붙이기 쉬운 패턴이 있었습니다.
하지만 문서는 분명히 말합니다:
- ❌ 이 “single-custom implementation naming”은 deprecated
- ✅ 이제는 fragment-based programming model 로 옮기라고 권장
왜냐하면 단일 구현 naming 패턴은 원치 않는 동작(undesired behavior) 을 유발할 수 있기 때문입니다.
특히 아래 규칙이 중요합니다 👇
3) “같은 패키지 + 리포지토리이름+postfix” 규칙의 위험 ⚠️
Spring Data는 다음 규칙을 갖고 있습니다:
“리포지토리 인터페이스가 있는 같은 패키지에,
(리포지토리 인터페이스 이름 + postfix) 클래스가 있으면
그것을 커스텀 구현으로 간주한다.”
즉, UserRepository가 있는 패키지에 UserRepositoryImpl 같은 클래스가 있으면
원치 않게 커스텀 구현으로 인식될 수 있습니다. 😱
그래서 문서가 말하는 것:
- “A class following that name can lead to undesired behavior.”
- 그래서 deprecated이고 fragment 방식으로 가라.
4) 구현체는 Spring Data에 의존하지 않는다 ✅ (그냥 Spring Bean)
문서의 중요한 포인트 중 하나:
“The implementation itself does not depend on Spring Data and can be a regular Spring bean.”
즉, CustomizedUserRepositoryImpl은:
- 그냥 일반 클래스/스프링 빈이 될 수 있고
- DI로
JdbcTemplate같은 것도 주입 가능하고 - AOP(트랜잭션, 로깅 등)도 적용 가능
👉 “리포지토리 커스텀 구현은 Spring Data 전용 특수 객체가 아니라 평범한 빈이다” 라는 뜻입니다. 🌱
5) 리포지토리 인터페이스에 Fragment 인터페이스를 “추가”해서 조립하기 🧩
이제 실제로 조립합니다:
interface UserRepository
extends CrudRepository<User, Long>, CustomizedUserRepository {
// Declare query methods here
}
이렇게 하면 UserRepository 프록시가 내부적으로:
- base CRUD 기능 + CustomizedUserRepository 구현체 기능
을 합쳐서 클라이언트에 제공합니다. 🎁
6) Repository Composition: “조각의 우선순위와 충돌 해결” 🥊
Spring Data는 “Fragments are imported in the order of their declaration.” 라고 합니다.
즉, 아래처럼 여러 fragment를 붙이면:
interface UserRepository
extends CrudRepository<User, Long>, HumanRepository, ContactRepository { }
Spring Data는 선언된 순서대로 fragment를 가져오고(import),
그리고 이 규칙이 매우 중요합니다:
- ✅ Custom implementations have a higher priority than
- base implementation
- repository aspects
즉, 커스텀 구현이 base 메서드를 “가로채서” 바꿀 수 있습니다. 😈 (정상 기능입니다)
6-1. 메서드 시그니처 충돌(ambiguity) 해결
두 fragment가 동일한 메서드 시그니처를 제공하면?
- “This ordering … resolves ambiguity if two fragments contribute the same method signature.”
👉 선언 순서 + 우선순위 규칙으로 결정됩니다.
7) Fragment 재사용 가능 ♻️ (여러 리포지토리에서 공유)
Fragment는 특정 repository 전용이 아닙니다.
interface CustomizedSave<T> {
<S extends T> S save(S entity);
}
class CustomizedSaveImpl<T> implements CustomizedSave<T> {
@Override
public <S extends T> S save(S entity) {
// custom save
}
}
interface UserRepository
extends CrudRepository<User, Long>, CustomizedSave<User> { }
interface PersonRepository
extends CrudRepository<Person, Long>, CustomizedSave<Person> { }
여기서 핵심은:
CustomizedSave<T>라는 fragment를 만들어두면- 여러 리포지토리에 “플러그인처럼” 붙여서
- 공통 save 정책(감사로그, 검증, soft delete 등)을 재사용 가능 🎯
8) Configuration: Impl 탐색 범위와 postfix 규칙 🔍
문서가 말하는 자동 탐색 규칙:
- “repository가 발견된 base-package 아래를 스캔”
- “구현체는 postfix(기본 Impl)로 끝나야 함”
그리고 postfix는 바꿀 수 있습니다:
@EnableJpaRepositories(repositoryImplementationPostfix = "MyPostfix")
class Configuration { … }
이때 탐색 대상 클래스명은:
- 기본:
com.acme.repository.CustomizedUserRepositoryImpl - 변경:
com.acme.repository.CustomizedUserRepositoryMyPostfix
즉 postfix를 바꾸면 클래스명 규칙도 함께 바뀝니다. 🧷
9) Resolution of Ambiguity: 같은 이름 구현체가 여러 개면? 🤔
만약 같은 클래스명 패턴을 가진 구현체가 “서로 다른 패키지”에서 발견되면,
Spring Data는 bean name 으로 선택합니다.
예:
class CustomizedUserRepositoryImpl implements CustomizedUserRepository { }
@Component("specialCustomImpl")
class CustomizedUserRepositoryImpl implements CustomizedUserRepository { }
여기서 기본 선택은 첫 번째 구현체입니다. 이유는:
- 첫 번째 구현체의 bean name이 기본 규칙인
customizedUserRepositoryImpl - 이것이 fragment interface 이름 + postfix(Impl)과 매칭되기 때문
그런데 UserRepository 인터페이스에:
@Component("specialCustom")
를 붙이면, Spring Data는:
- repository bean name(
specialCustom) + Impl - →
specialCustomImpl
과 매칭되는 구현체를 선택합니다. 🎯
즉, “리포지토리의 빈 이름”이 구현체 선택에도 영향을 줍니다.
10) Manual Wiring: 구현체를 “내가 직접” 빈으로 등록하고 싶다 🧵
보통은 자동 스캔 + 자동 와이어링으로 충분하지만,
구현체 fragment에 특별한 설정이 필요하면 수동 등록을 할 수 있습니다.
문서 요지:
- bean을 직접 선언하되
- 이름을 규칙에 맞춰서 등록하면
- 인프라가 그 빈을 “자동 생성 대신” 참조한다.
예시 코드(문서의 Example 3):
class MyClass {
MyClass(@Qualifier("userRepositoryImpl") UserRepository userRepository) {
…
}
}
여기서 포인트는:
userRepositoryImpl같은 이름이 “규칙”과 연결되어 있다는 것- 수동 빈 등록 시에도 명명 규칙이 중요하다는 것 🏷️
11) Registering Fragments with spring.factories: 외부 라이브러리처럼 배포하기 📦
문서의 큰 제약:
“infrastructure only auto-detects fragments within repository base-package”
즉, fragment 구현이 다른 namespace(패키지)에 있거나
외부 jar로 제공되면 자동 탐색이 안 될 수 있습니다.
이를 우회하는 방식이:
META-INF/spring.factories에 fragment 등록 ✅
11-1. 예: 조직 공통 SearchExtension 만들기 🔎
Fragment 인터페이스
public interface SearchExtension<T> {
List<T> search(String text, Limit limit);
}
- 제네릭
<T>로 repository domain type에 맞추는 설계가 핵심입니다.
Fragment 구현체 + SearchService DI
class DefaultSearchExtension<T> implements SearchExtension<T> {
private final SearchService service;
DefaultSearchExtension(SearchService service) {
this.service = service;
}
@Override
public List<T> search(String text, Limit limit) {
return search(RepositoryMethodContext.getContext(), text, limit);
}
List<T> search(RepositoryMethodContext metadata, String text, Limit limit) {
Class<T> domainType = metadata.getRepository().getDomainType();
String indexName = domainType.getSimpleName().toLowerCase();
List<String> jsonResult = service.search(indexName, text, 0, limit.max());
return jsonResult.stream().map(…).collect(toList());
}
}
여기서 아주 중요한 문장 🧨
RepositoryMethodContext.getContext()로 invocation metadata를 얻는다.- metadata에서 domain type을 얻어 index 이름을 만든다.
- 즉 “어떤 리포지토리에서 호출되었는지”를 런타임에 알아내어 동작을 일반화한다.
12) RepositoryMethodContext 메타데이터 노출은 “비싸다” 💸 (기본 OFF)
문서가 직접 말합니다:
“Exposing invocation metadata is costly, hence it is disabled by default.”
그래서 RepositoryMethodContext.getContext() 를 쓰려면
repository factory에 metadata 노출을 켜야 합니다.
12-1. 해결책: RepositoryMetadataAccess 마커 인터페이스 ✅
구현체에 RepositoryMetadataAccess 를 추가하면 인프라가 감지하고 켭니다.
class DefaultSearchExtension<T>
implements SearchExtension<T>, RepositoryMetadataAccess {
// ...
}
13) spring.factories 등록 🧾
이제 외부 제공을 위해:
META-INF/spring.factories 에 등록합니다.
com.acme.search.SearchExtension=com.acme.search.DefaultSearchExtension
이렇게 패키징하면, repository base-package 스캔에 안 걸려도
해당 fragment를 “등록 기반”으로 주입할 수 있게 됩니다. 🚚
13-1. 사용하기: 리포지토리에 인터페이스만 추가하면 끝 ✅
interface MovieRepository
extends CrudRepository<Movie, String>, SearchExtension<Movie> { }
즉, 사용자는 구현체를 몰라도 되고
interface만 추가하면 repository가 확장됩니다. 👌
14) Customize the Base Repository: 모든 리포지토리에 공통 적용 🌍
앞의 fragment 방식은 “각 repository interface에 붙여야” 합니다.
하지만 전체 리포지토리에 공통 정책을 강제하고 싶다면?
✅ 커스텀 base repository class 를 만들고,repositoryBaseClass 로 갈아끼웁니다.
14-1. SimpleJpaRepository 상속
class MyRepositoryImpl<T, ID> extends SimpleJpaRepository<T, ID> {
private final EntityManager entityManager;
MyRepositoryImpl(JpaEntityInformation entityInformation,
EntityManager entityManager) {
super(entityInformation, entityManager);
this.entityManager = entityManager;
}
@Override
@Transactional
public <S extends T> S save(S entity) {
// implementation goes here
}
}
핵심 규칙 ⚙️
- store-specific repository factory가 사용할 수 있도록
슈퍼 클래스가 요구하는 생성자 형태를 반드시 제공해야 합니다. - 생성자가 여러 개면, 보통:
EntityInformation + EntityManager(또는 template)받는 생성자를 오버라이드하라.
14-2. 설정으로 적용
@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }
이제 모든 repository의 base가 MyRepositoryImpl이 됩니다. 🌐
15) Customizing the Repository Factory: “프록시 생성 과정”에 개입 🏭
fragment/base class보다 더 깊게 들어가면
repository instance 생성 파이프라인 자체를 만질 수 있습니다.
15-1. RepositoryFactoryCustomizer
문서 예시:
factoryBean.addRepositoryFactoryCustomizer(repositoryFactory -> {
repositoryFactory.addInvocationListener(…);
repositoryFactory.addQueryCreationListener(…);
repositoryFactory.addRepositoryProxyPostProcessor((factory, repositoryInformation) ->
factory.addAdvice(…)
);
});
이 방식의 성격 🎛️
- “완전한 커스텀 팩토리”까지는 아니고
- “선택적 조정(리스너/어드바이스 추가)”에 좋음
- 특정 repository만 영향 주도록 BeanPostProcessor로 연결하는 게 이상적
- 그리고 아주 중요한 주의:
customizer beans are not applied automatically
(특히 multi-repository / multi-module에서 원치 않는 결합 방지)
16) Customize the Repository Factory Bean: 가장 강력하지만 가장 무겁다 🧨
문서가 말하는 최강 옵션:
- 커스텀 repository factory bean 제공
(RepositoryFactorySupport,TransactionalRepositoryFactoryBeanSupport, store-specific factory bean 상속)
이 방식은:
- repository 생성 전체를 바꿀 수 있지만
- 노력도 가장 크고
- 보통 “코어 생성 메커니즘을 바꿔야 할 때만” 필요합니다.
문서가 요약한 핵심 고려사항 3개 🎯
- repositoryBaseClass
- base class가 어떤 메서드를 구현하고
- 어떤 메서드는 aspect/custom impl이 처리해야 하는지 결정
- repositoryFragmentsContributor
- 표준 fragment 수집 이후 조립에 추가 기여
- store module이 Querydsl/QBE 같은 기능을 붙일 때도 사용
- third-party extension SPI 역할도 함
- exposeMetadata
RepositoryMethodContext.getContext()메타데이터 노출 여부
17) Using JpaContext in Custom Implementations: 다중 EntityManager 환경 🧠
마지막 파트는 실전에서 매우 자주 터지는 문제입니다.
여러 EntityManager가 있을 때
커스텀 구현체에 “올바른” EntityManager를 어떻게 주입할 것인가?
일반적 방법:
@PersistenceContext(name=...)로 명시@Autowired+@Qualifier로 특정 EM 주입
그런데 Spring Data JPA 1.9부터는 더 편한 도구가 있습니다:
✅ JpaContext
17-1. 도메인 타입으로 EntityManager 얻기
class UserRepositoryImpl implements UserRepositoryCustom {
private final EntityManager em;
@Autowired
public UserRepositoryImpl(JpaContext context) {
this.em = context.getEntityManagerByManagedType(User.class);
}
…
}
장점 ✨
User가 다른 persistence unit으로 옮겨가도,- repository 구현 코드를 바꾸지 않고도
- context가 적절한 EntityManager를 찾아줍니다.
단, 전제 조건이 있습니다:
- 해당 도메인 타입이 하나의 EntityManager에 의해 관리된다고 “가정”할 수 있어야 합니다.
마무리: 언제 무엇을 써야 하나요? 🧭 (정리)
- ✅ 특정 리포지토리만 기능 추가: Fragment 인터페이스 +
Impl(권장 패턴) - ✅ 여러 리포지토리에 공통 기능 재사용: Generic Fragment (
SearchExtension<T>,CustomizedSave<T>같은) - ✅ 외부 라이브러리/다른 패키지에서 fragment 제공:
spring.factories등록 - ✅ 모든 리포지토리 save/find 등 동작을 일괄 변경:
repositoryBaseClass(커스텀SimpleJpaRepository) - ✅ 프록시 생성/리스너/어드바이스 수준에서 개입:
RepositoryFactoryCustomizer - ✅ 생성 메커니즘 자체를 바꿔야 함: Custom Repository Factory Bean (최후의 수단)
- ✅ 다중 EntityManager에서 도메인별 EM 자동 선택:
JpaContext
'Spring Boot > Spring Data JPA' 카테고리의 다른 글
| Spring Data (0) | 2025.03.07 |
|---|---|
| Spring Data JPA (0) | 2024.10.24 |
| JPA Query Methods (0) | 2024.10.23 |
| Auditing (0) | 2024.10.22 |
| OPTIMISTIC, OPTIMISTIC_FORCE_INCREMENT (0) | 2024.10.22 |