[BroBean] BroBean 프로젝트 #14 순환 참조 오류를 ObjectProvider로 해결하기
📖 커플 기능을 구현하다 만난 순환 참조와 ObjectProvider
BroBean의 커플 기능을 구현하면서 기존에 만들어둔 인증 로직과 새롭게 추가한 커플 로직이 서로 연결되는 상황이 생겼다.
처음에는 자연스러운 구조라고 생각했다.
인증된 사용자의 정보를 확인하기 위해 AuthService에서 커플 정보를 확인해야 했고,
반대로 커플 관련 기능에서는 현재 로그인한 사용자의 정보를 확인하기 위해 AuthService를 다시 사용해야 했다.
문제는 서비스 간 의존성이 다음과 같은 구조가 되면서 발생했다.
AuthService
↓
CoupleService
↓
AuthService
Spring 입장에서는 두 Bean을 생성하기 위해 서로가 서로를 필요로 하는 상황이다.
결국 애플리케이션을 실행하면서 순환 참조(Circular Reference) 오류가 발생했다.
🔄 문제가 발생한 구조
당시 구조를 단순화하면 다음과 비슷했다.
@Service
@RequiredArgsConstructor
public class AuthService {
private final CoupleService coupleService;
}
그리고:
@Service
@RequiredArgsConstructor
public class CoupleService {
private final AuthService authService;
}
각각 서로를 생성자 주입받고 있다.
Spring이 AuthService를 생성하려고 하면:
AuthService 생성
↓
CoupleService 필요
↓
CoupleService 생성
↓
AuthService 필요
↓
AuthService 아직 생성되지 않음
이라는 구조가 만들어진다.
결국 어느 쪽도 먼저 완전히 생성될 수 없다.
AuthService → CoupleService → AuthService → ...
이렇게 의존성이 계속 이어지기 때문이다.
⚠️ 순환 참조는 왜 문제가 될까?
Spring의 생성자 주입은 Bean을 생성할 때 필요한 의존성을 생성자에서 전달받는다.
예를 들어:
public AuthService(CoupleService coupleService) {
this.coupleService = coupleService;
}
라면 AuthService를 만들기 전에 CoupleService가 준비되어 있어야 한다.
그런데 CoupleService 역시:
public CoupleService(AuthService authService) {
this.authService = authService;
}
를 필요로 한다면 문제가 발생한다.
AuthService를 만들려면
CoupleService가 필요하다.
CoupleService를 만들려면
AuthService가 필요하다.
서로 먼저 생성될 수 없는 구조다.
Spring에서는 이런 순환 참조를 발견하면 Bean 생성 과정에서 오류가 발생한다.
🛠️ ObjectProvider를 사용해보기
이 문제를 해결하면서 사용하게 된 것이 ObjectProvider다.
기존에는:
private final AuthService authService;
형태로 직접 주입받고 있었다면,
다음과 같이 변경할 수 있다.
private final ObjectProvider<AuthService> authServiceProvider;
그리고 실제로 AuthService가 필요한 시점에:
authServiceProvider
.getObject()
.getLoginUserInfo(user);
처럼 가져온다.
전체적으로 보면:
@Service
@RequiredArgsConstructor
public class CoupleService {
private final ObjectProvider<AuthService> authServiceProvider;
public void someMethod(User user) {
authServiceProvider
.getObject()
.getLoginUserInfo(user);
}
}
와 같은 형태다.
🧠 왜 ObjectProvider를 사용하면 동작할까?
핵심은 의존성을 가져오는 시점을 늦출 수 있다는 것이다.
기존 방식은:
private final AuthService authService;
Spring이 CoupleService를 생성하는 시점부터 AuthService를 필요로 한다.
반면:
private final ObjectProvider<AuthService> authServiceProvider;
에서는 AuthService 자체를 바로 주입받는 것이 아니다.
AuthService를 필요할 때 가져올 수 있는 Provider를 주입받는다.
즉:
기존
CoupleService 생성
↓
AuthService 필요
↓
AuthService 생성
이 아니라:
CoupleService 생성
↓
ObjectProvider 주입
↓
Bean 생성 완료
↓
실제 메서드 호출
↓
getObject()
↓
AuthService 획득
처럼 필요한 시점까지 실제 객체 획득을 지연시킬 수 있다.
이런 방식을 **지연 조회(Lazy Lookup)**라고 볼 수 있다.
🔎 ObjectProvider의 역할
ObjectProvider<T>는 Spring이 제공하는 객체 조회 방식 중 하나다.
ObjectProvider<AuthService>
라면:
AuthService를 필요할 때 조회할 수 있는 Provider
라고 이해하면 된다.
가장 단순하게 사용하면:
AuthService authService =
authServiceProvider.getObject();
이후:
authService.getLoginUserInfo(user);
처럼 사용할 수 있다.
또한 상황에 따라:
getIfAvailable()
같은 메서드를 활용해 Bean이 존재하지 않을 때의 처리도 할 수 있다.
🔄 이번 문제에서의 구조
ObjectProvider를 적용하고 나서는 의존성이 다음과 같이 바뀐다.
AuthService
↓
CoupleService
↓
ObjectProvider<AuthService>
중요한 차이는 CoupleService가 생성되는 시점에 실제 AuthService 객체를 직접 요구하지 않는다는 것이다.
실제 AuthService가 필요한 순간:
authServiceProvider.getObject()
를 호출해서 가져온다.
따라서 Bean 생성 시점의 순환 구조를 피할 수 있었다.
🤔 그렇다면 순환 참조가 생기면 무조건 ObjectProvider를 사용하면 될까?
개인적으로 이번 문제를 해결하면서 가장 생각해볼 부분이었다.
기술적으로는:
ObjectProvider<AuthService>
를 사용해서 순환 참조를 우회할 수 있다.
하지만 이것이 항상 좋은 설계라는 의미는 아니다.
오히려 서비스 간 의존성이:
AuthService
↕
CoupleService
처럼 서로 강하게 연결되어 있다는 것 자체가 설계상 신호일 수 있다.
특히 두 Service가 서로의 많은 기능을 사용하기 시작하면 시간이 지나면서:
AuthService
↓
CoupleService
↓
FarmService
↓
AuthService
처럼 의존성이 복잡해질 가능성이 있다.
따라서 단순히 ObjectProvider를 추가해서 오류를 없애는 것보다 왜 서로의 Service가 필요해졌는지 먼저 확인하는 것이 중요하다.
🧩 이번에는 왜 ObjectProvider를 선택했을까?
BroBean에서는 기존 인증 로직을 크게 변경하지 않고 커플 기능을 확장하는 과정에서 일시적으로 두 Service 사이의 의존 관계가 만들어졌다.
기존:
AuthService
가 담당하던 사용자 인증 정보 조회를 커플 기능에서도 사용해야 했고,
커플 연결 여부나 커플 관련 데이터를 처리하는 과정에서는 다시:
CoupleService
가 필요해졌다.
당장 전체 Service 구조를 다시 분리하기에는 변경 범위가 커질 수 있었기 때문에,
이번 상황에서는 ObjectProvider를 이용해 실제 객체 조회 시점을 늦추는 방식으로 문제를 해결했다.
private final ObjectProvider<AuthService> authServiceProvider;
그리고 필요한 곳에서:
authServiceProvider
.getObject()
.getLoginUserInfo(user);
형태로 사용했다.
⚠️ 그래도 기억해둘 것
ObjectProvider는 순환 참조 문제를 해결할 수 있는 도구 중 하나지만,
순환 참조 발생
↓
ObjectProvider 사용
↓
오류 해결
에서 끝내기보다는,
왜 Service가 서로를 필요로 하는가?
를 먼저 확인하는 것이 중요하다.
경우에 따라서는:
공통 로직 분리
↓
별도의 Service 생성
↓
Repository 직접 조회 구조 변경
↓
Facade 또는 별도 조회 객체 사용
등으로 의존성 자체를 제거하는 것이 더 좋은 해결책일 수 있다.
즉 ObjectProvider는 순환 참조를 없애는 설계 도구라기보다는 의존성의 실제 조회 시점을 지연시키는 Spring 기능에 가깝다.
🎯 마무리
이번 문제를 통해 ObjectProvider라는 기능을 알게 된 것도 있지만, 개인적으로는 Service 간 의존 관계를 다시 생각해보는 계기가 됐다.
처음에는:
private final AuthService authService;
처럼 필요한 Service를 바로 주입받는 것이 당연하다고 생각했다.
하지만 Service 간 책임이 확장되면서:
AuthService
↓
CoupleService
↓
AuthService
라는 순환 구조가 만들어질 수 있었다.
이때:
private final ObjectProvider<AuthService> authServiceProvider;
를 사용하고,
authServiceProvider
.getObject()
.getLoginUserInfo(user);
처럼 필요한 시점에 Bean을 조회하도록 변경하면서 문제를 해결할 수 있었다.
결국 이번에 기억해둘 핵심은 두 가지다.
ObjectProvider
→ 의존 객체의 조회 시점을 늦출 수 있다.
순환 참조
→ 단순히 오류만 해결하기보다
Service 간 책임과 의존 관계를 다시 확인해야 한다.
BroBean처럼 기능이 계속 확장되는 서비스에서는 처음에는 자연스럽게 보였던 Service 간 의존성이 기능 추가와 함께 복잡해질 수 있다.
이번 문제 역시 단순한 Spring 오류 하나를 해결한 것에서 끝내기보다는, 서비스가 서로의 역할을 어디까지 알아야 하는지 다시 생각해볼 수 있었던 경험으로 남겨두려고 한다.
[BroBean] BroBean 프로젝트 #13 개인·커플 모드와 가계부 API 구현 - 쿼리 성능을 다시 돌아보다
BroBean의 개인·커플 모드 전환 기능과 씨앗심기·성장일지 백엔드 및 API를 구현하고, 월별 가계부 조회 쿼리를 작성하는 과정에서 쿼리 성능을 다시 고민해본 경험을 정리합니다.
[BroBean] BroBean 프로젝트 #15 JPA 페이지네이션 적용, Page와 Slice의 차이
BroBean 프로젝트에서 페이지네이션을 적용하며 Spring Data JPA의 Page와 Slice 차이를 알아보고, 가계부와 알림 내역에 각각 다른 방식을 적용한 경험을 정리한다.
