QueryDSL 구현체를 Infrastructure 레이어로 이동하면서 발생한 오류
목차
정상 상태
레이어드 아키텍처에서 인터페이스는 Application 레이어에, 그 구현체는 Infrastructure 레이어에 위치해야 해요.
이렇게 해야 의존성 역전 원칙(DIP)이 지켜지고, Application 레이어가 특정 기술(QueryDSL, JPA 등)에 직접 의존하지 않는 구조가 돼요.
문제 상황

개발 중에 QueryDSL 기반 검색 기능을 구현하면서, BoardSearch(인터페이스)와 BoardSearchImpl(구현체)을 모두 Application 레이어에 두고 있었어요.
이 구조의 문제를 인식하고, 두 가지 작업을 진행했어요.
1단계: 이름 정리
BoardSearch와 BoardSearchImpl이라는 이름은 역할이 불명확했어요.
QueryDSL을 사용하는 Repository 구현체인 만큼, 이름을 아래처럼 변경했어요.
BoardSearch→BoardSearchRepositoryBoardSearchImpl→BoardSearchRepositoryImpl
2단계: 구현체를 Infrastructure 레이어로 이동
인터페이스는 Application 레이어에 유지하고, 구현체인 BoardSearchRepositoryImpl을 Infrastructure 레이어로 옮겼어요.





그런데 이동 직후, 애플리케이션이 실행되지 않았어요.

원인 분석
오류 메시지를 확인해보니, Spring Data JPA가 BoardJpaRepository에서 searchAll(...) 메서드를 자동 구현하려다 실패한 거였어요.

Spring Data JPA의 쿼리 메서드 자동 생성 규칙을 확인해봤어요.
Spring Data JPA 공식 문서에 따르면, JPA는 findBy, findAllBy, countBy, deleteBy 등의 규약된 접두사와 엔티티 프로퍼티명의 조합으로 쿼리를 자동 생성해요.
findByTitleContaining(String keyword) → 자동 생성 가능findAllByTagIn(List<String> tags) → 자동 생성 가능searchAll(...) → 규약에 없음 → 자동 생성 불가
문제의 근본 원인은 BoardJpaRepository가 BoardSearchRepository 인터페이스를 extends로 확장하고 있었기 때문이에요.
Spring Data JPA는 BoardJpaRepository에 선언된 모든 메서드(상속받은 것 포함)를 쿼리 메서드로 해석하려 하거든요.
searchAll은 JPA 쿼리 메서드 규약에 맞지 않으므로 파싱 실패가 발생한 거예요.
정리하면:
BoardJpaRepository가BoardSearchRepository를 extends → JPA가searchAll을 자동 구현하려 시도searchAll은 JPA 쿼리 메서드 명명 규칙에 없는 이름 → 파싱 실패- 기존에는
BoardSearchImpl이 같은 패키지에 있어서 Spring Data JPA가 Custom Repository Implementation으로 인식했지만, Infrastructure 레이어로 이동하면서 이 연결이 끊어진 것
해결
BoardSearchRepositoryImpl을 Infrastructure로 이동했으므로, BoardJpaRepository가 BoardSearchRepository를 extends할 이유가 없어요.
각각 독립된 빈으로 관리하는 것이 더 적절하죠.

1단계: BoardJpaRepository에서 BoardSearchRepository extends 제거

2단계: BoardSearchRepositoryImpl에 @Repository 어노테이션 추가
@Component로 선언해도 빈 등록은 되지만, @Repository를 선택한 이유는 두 가지예요.
- 의미적 명확성: 데이터 접근 계층임을 명시
- 예외 변환: Spring이 데이터 접근 예외를
DataAccessException으로 자동 변환

3단계: Service에서 BoardSearchRepository를 직접 주입받아 사용
해결 후 구조
| 컴포넌트 | 변경 내용 |
|---|---|
BoardJpaRepository | BoardSearchRepository extends 제거. 순수 JPA 엔티티 관리만 담당 |
BoardSearchRepositoryImpl | @Repository로 독립 빈 등록. QueryDSL 기반 복잡 쿼리 담당 |
BoardServiceImpl | BoardSearchRepository를 private final로 직접 주입 |
결과적으로 BoardJpaRepository는 JPA 엔티티 관리만, BoardSearchRepositoryImpl은 QueryDSL 기반 복잡 쿼리만 담당하게 되어 관심사 분리(SRP)가 달성됐어요.
인터페이스 기반의 의존성 역전도 유지되고요.
번외: AI가 제안한 방법과의 차이

같은 문제를 AI에게 물어봤을 때, AI는 2가지 방법을 제안했어요:
BoardSearchRepositoryImpl클래스명을 관례에 맞게 변경@Repository스캔 범위를 확장
하지만 제가 선택한 방법은 달랐어요. extends에서 분리하고 private final BoardSearchRepository로 직접 주입하는 방식이에요.
왜 AI의 제안을 채택하지 않았는가:
- 방법 1(이름 변경)은 구현체를 다시 같은 패키지에 두는 것을 전제로 해요. 그러면 Infrastructure 레이어로 분리한 의미가 없어지죠.
- 방법 2(스캔 범위 확장)는
BoardJpaRepository가 여전히BoardSearchRepository를 extends하는 구조를 유지해요. JPA가searchAll을 쿼리 메서드로 해석하려는 근본 문제가 남아요. - 제 선택(extends 분리 + 독립 빈)은 각 Repository가 단일 책임을 갖고, 레이어 간 의존성 방향도 지켜지는 구조예요.
실제로 AI의 방법 1(이름 변경 + 같은 패키지)도 시도해봤어요. 동작은 했지만, 구현체가 Application 레이어에 다시 돌아가게 되어 레이어 분리를 한 의미가 사라졌어요. 방법 2(스캔 범위 확장)도 테스트했는데, 빈 등록은 되지만 BoardJpaRepository가 여전히 searchAll을 쿼리 메서드로 해석하려는 경고가 남았어요.
AI의 답변은 “Spring Data JPA의 Custom Repository 규칙 안에서 문제를 해결”하는 관점이었고, 제 선택은 “애초에 extends로 묶을 이유가 없다”는 아키텍처 관점이었어요. 세 가지 모두 동작하지만, 현재 프로젝트의 레이어 구조와 의존성 방향을 고려했을 때 분리가 가장 깔끔했어요.
Reference
Normal Behavior
In layered architecture, interfaces belong in the Application layer, and their implementations belong in the Infrastructure layer. This ensures the Dependency Inversion Principle (DIP) is upheld, preventing the Application layer from directly depending on specific technologies (QueryDSL, JPA, etc.).
The Problem

While implementing QueryDSL-based search functionality during development, both BoardSearch (interface) and BoardSearchImpl (implementation) were placed in the Application layer.
Recognizing the structural issue, two steps were taken.
Step 1: Name Cleanup
The names BoardSearch and BoardSearchImpl didn’t clearly convey their roles. Since they’re Repository implementations using QueryDSL, the names were changed:
BoardSearch→BoardSearchRepositoryBoardSearchImpl→BoardSearchRepositoryImpl
Step 2: Move Implementation to Infrastructure Layer
The interface stayed in the Application layer, while the implementation BoardSearchRepositoryImpl was moved to the Infrastructure layer.





However, immediately after the move, the application failed to start.

Root Cause Analysis
The error message revealed that Spring Data JPA failed while trying to auto-implement the searchAll(...) method in BoardJpaRepository.

According to the Spring Data JPA official documentation, JPA auto-generates queries using conventional prefixes like findBy, findAllBy, countBy, deleteBy combined with entity property names.
findByTitleContaining(String keyword) → Auto-generation possiblefindAllByTagIn(List<String> tags) → Auto-generation possiblesearchAll(...) → Not in convention → Cannot auto-generate
The root cause was that BoardJpaRepository was extending the BoardSearchRepository interface. Spring Data JPA tries to interpret all methods in BoardJpaRepository (including inherited ones) as query methods. Since searchAll doesn’t follow JPA query method naming conventions, parsing failed.
In summary:
BoardJpaRepositoryextendsBoardSearchRepository→ JPA tries to auto-implementsearchAllsearchAlldoesn’t match JPA query method naming conventions → Parsing failure- Previously,
BoardSearchImplbeing in the same package let Spring Data JPA recognize it as a Custom Repository Implementation, but moving to the Infrastructure layer broke this connection
Solution
Since BoardSearchRepositoryImpl was moved to Infrastructure, there’s no reason for BoardJpaRepository to extend BoardSearchRepository. Managing them as independent beans is more appropriate.

Step 1: Remove BoardSearchRepository extends from BoardJpaRepository

Step 2: Add @Repository annotation to BoardSearchRepositoryImpl
While @Component would also register the bean, @Repository was chosen for two reasons:
- Semantic clarity: Explicitly indicates a data access layer
- Exception translation: Spring automatically converts data access exceptions to
DataAccessException

Step 3: Inject BoardSearchRepository directly in the Service
Post-Fix Structure
| Component | Change |
|---|---|
BoardJpaRepository | Removed BoardSearchRepository extends. Handles only pure JPA entity management |
BoardSearchRepositoryImpl | Registered as independent bean with @Repository. Handles QueryDSL-based complex queries |
BoardServiceImpl | Directly injects BoardSearchRepository via private final |
As a result, BoardJpaRepository handles only JPA entity management, while BoardSearchRepositoryImpl handles only QueryDSL-based complex queries, achieving separation of concerns (SRP). Interface-based dependency inversion is also maintained.
Aside: Difference from AI-Suggested Approach

When asking AI about the same problem, it suggested only 2 approaches. But my chosen approach was different — separating the extends and directly injecting via private final BoardSearchRepository.
The AI’s suggestions may not be wrong, but considering the current project’s layer structure and dependency directions, separation was more appropriate in my judgment.
Rather than unconditionally accepting AI answers, it’s important to verify and judge them in the context of the current architecture. The premise should be that AI doesn’t present all possible solutions — making evidence-based choices yourself leads to better outcomes.
Reference
댓글
댓글 수정/삭제는 GitHub Discussions에서 가능합니다.