문제 발생 전
학교 선배 분께 좋은 기회로 스프링에 대해 멘토링을 받을 수 있게 되어, 멘토링 과제로 간단한 게시판부터 만들어보고 있었습니다.
문제 발생
게시글 (Board)에 대한 리포지토리를 작성하던 도중, 순환 참조 이슈가 발생했습니다.
저는 게시글에 관련된 리포지토리의 구현체들을 여러 개로 두고 싶어, 완전히 추상화된 인터페이스인 BoardRepository를 만들고, 스프링 데이터 JPA 인터페이스로 BoardJpaRepository를 만들었었습니다. 그리고 스프링 데이터 환경에서 BoardRepository를 구현하는 BoardJpaRepositoryImpl을 만들었습니다.
즉, 코드로 나타내면 다음과 같습니다.
public interface BoardRepository {
Long save(final Board board);
...
}
@Repository
@RequiredArgsConstructor
public class BoardJpaRepositoryImpl implements BoardRepository {
private final BoardJpaRepository boardJpaRepository;
@Override
public Long save(final Board board) {
boardJpaRepository.save(board);
...
}
}
public interface BoardJpaRepository extends JpaRepository<Board, Long> {
}
아래는 발생한 에러 로그입니다.
The dependencies of some of the beans in the application context form a cycle:
boardController defined in file [/Users/hyunjoonchoi/Desktop/2024/2024-mju-mentoring/build/classes/java/main/com/mju/mentoring/board/controller/BoardController.class]
↓
boardService defined in file [/Users/hyunjoonchoi/Desktop/2024/2024-mju-mentoring/build/classes/java/main/com/mju/mentoring/board/service/BoardService.class]
┌─────┐
| boardJpaRepositoryImpl defined in file [/Users/hyunjoonchoi/Desktop/2024/2024-mju-mentoring/build/classes/java/main/com/mju/mentoring/board/infrastructure/BoardJpaRepositoryImpl.class]
└─────┘
처음 가진 의문점, 해결법
처음에는 김영한 님의 스프링 데이터 JPA 강의와, 스프링 공식 문서를 봤을 때 제가 잘못하거나 놓친 것이 없는 줄 알았습니다.
김영한 님의 경우에서는 사용자 정의 인터페이스로 MemberRepositoryCustom을 만들고, 그것의 구현체로 MemberRepositoryImpl, 그리고 스프링 데이터 JPA 인터페이스로 MemberRepository (JpaRepository와 MemberRepositoryCustom을 다중 상속받음)로 설계해서 이름만 봤을 때는 문제가 없는 것으로 생각했습니다. 공식 문서에서도 그렇고요.
그래서 여전히 의문이 남아 직접 스프링 데이터 JPA 리포지토리에 이슈를 작성하였습니다.
![](https://blog.kakaocdn.net/dn/TuXt4/btsDGwGos5E/DfGFFqr3k5VCNTJL1Kvot1/img.png)
메인테이너 분의 리뷰
얼마 지나지 않아 메인테이너 분께서 리뷰해 주셨습니다.
You cannot inject a repository facade into its own repository fragment as this creates a circular dependency. You could make the repository @Lazy or inject an ObjectProvider<BoardJpaRepository> to defer object resolution to a time after the objects are constructed.
자신의 저장소 조각에 저장소 파사드를 삽입하면 원형 종속성이 생성되므로 저장소 파사드를 삽입할 수 없습니다. 저장소 @Lazy를 만들거나 ObjectProvider<BoardJpaRepository>를 삽입하여 개체를 구성한 후 시간으로 개체 해결을 연기할 수 있습니다.
@Lazy, ObjectProvider 등 아직은 저에게 생소한 개념이기도 하고, 공식 문서에서도 그와 관련된 내용을 더 찾을 수 없어 추가적인 질문을 하였습니다. (그런데 바로 closed 된 이슈..)
2차 (최종) 해결법
EnableJpaRepositories에 대해 살펴보다
그러던 도중, 이와 관련이 있는 EnableJpaRepositories 어노테이션에 대해 살펴보기로 했습니다. 김영한 님의 스프링 데이터 JPA에서는 이 어노테이션에서의 설정을 바꿈으로써 구현체를 자동으로 인식하는 것을 바꿀 수 있다고 하였습니다.
![](https://blog.kakaocdn.net/dn/cMTPvk/btsDGMhRLEi/mgYf4AYBOtZSJwKcdoO600/img.png)
여기에서 repositoryImplementationPostfix에 대해 확인해 보시면, 스프링 데이터 JPA는 커스텀 구현체가 작성될 시 기본적으로 스프링 데이터 JPA 인터페이스의 이름 뒤에 접미사로 Impl이 붙은 것을 구현체로 인식한다는 것을 알 수 있습니다.
이는 공식 문서에도 나와있는 내용입니다.
![](https://blog.kakaocdn.net/dn/2RTTr/btsDDc2UEqX/UiqYSoBse3XToMOD4lH7q0/img.png)
코드 재확인, 깨달음 및 해결법
그럼 다시 한번 문제 코드를 보겠습니다.
@Repository
@RequiredArgsConstructor
public class BoardJpaRepositoryImpl implements BoardRepository {
private final BoardJpaRepository boardJpaRepository;
@Override
public Long save(final Board board) {
boardJpaRepository.save(board);
...
}
}
문제점을 아시겠나요?
BoardJpaRepository는 스프링 데이터 JPA 인터페이스입니다. 그리고, 추가적으로 건들지 않을 경우 구현체가 있다면 자신의 이름 뒤에 Impl이 붙은 것을 구현체로 삼습니다.
그런데 그렇게 될 구현체의 이름이 BoardJpaRepositoryImpl인데, 이 클래스는 BoardJpaRepository를 참조하고 있습니다. 따라서, 구현체가 자신의 인터페이스를 참조하게 된 꼴입니다. 셀프 참조가 발생되게 된 거죠.
그래서 로그 또한 이렇게 BoardJpaRepositoryImpl 안에서만 계속 순환 참조가 발생된다고 했던 것으로 추측됩니다.
┌─────┐
| boardJpaRepositoryImpl defined in file [/Users/hyunjoonchoi/Desktop/2024/2024-mju-mentoring/build/classes/java/main/com/mju/mentoring/board/infrastructure/BoardJpaRepositoryImpl.class]
└─────┘
따라서 이 문제를 해결하고 싶으시다면 (저처럼 스프링 데이터 JPA 인터페이스를 주입받도록 하실 분들) 이름을 다르게 하셔야 합니다!
정리
결국에는 제가 잘못된 네이밍 전략을 사용했기 때문에 발생한 일이었음을 알게 되어, 메인테이너 분께 죄송한 마음이 들었습니다.
하지만 한편으로는 스프링 데이터 JPA에 대해 더 알게 된 귀중한 계기가 되었다고 생각합니다.
커스텀 JPA 구현체를 만들다가 충분히 발생할 수 있는 이슈라고 생각되기도 하고, "공식 문서에서는 이를 어떻게 알려주고 있을까?", "다른 사람들은 어떻게 해결했을까?"를 생각해 보며 문제를 해결해 보려고 다양한 방법을 활용하기도 했습니다.
그리고 뜻하지 않게 스프링 데이터 JPA 레포지토리에 직접 이슈를 남기게 되었기도 하였죠.
![](https://blog.kakaocdn.net/dn/dOaH3N/btsDBx7CQk3/GfEl8tsjvhBkbTnMd28FcK/img.png)
문제를 해결하거나 공부를 하다가 궁금한 게 있다면 이렇게 해결하기 전까지 자신이 할 수 있는 모든 방법을 동원해서 시도해 보는 게 빠르게 해결할 수 있는 방법이라는 것을 다시금 깨달을 수 있었습니다.
(마지막으로 영어에 대해 진짜 너무 약한 상태라는 것을 알게 되었습니다. 공부를 덜 하긴 했지만 영어권 분들과 의사소통할 때 이 정도는 아니었던 것 같은데, 이 점에 대해서도 반성하게 됐네요.)