
아, 내가 최대한 안그러려고 했는데 이게 하다보니...
보통 User 연관 로직에서 많이 발생되는 순환참조를 해결하는 방법을 알아보자.
- 순환참조란?
쉽게 말하면 두개 이상의 클래스가 서로가 서로를 Bean으로 DI하는 경우에 순환참조가 일어난다. 아래 대화를 통해 순환 참조를 쉽게 이해해보자.
SpringBoot : 철수야 놀러 갈래?
철수 : 영희가면 갈게.
SpringBoot : 영희야 놀러 갈래?
영희 : 민수가면 갈게.
SpringBoot : 민수야 놀러 갈래?
민수 : 철수가면 갈게.
SpringBoot : ?????????
다 같이 놀러가야 하는데 이러면 스프링부트도 놀러가기 싫어진다. 누구 하나라도 남에게 의존하지 않고 결정한다면 모두 놀러갈 수 있는데, 모두가 의존성을 요하는 부분이다. 이것이 순환참조 에러이다.
- 실제 순환 참조 에러
@RequiredArgsConstructor
@Service
public class ServiceA {
private final ServiceB serviceB;
public String hello() {
return "hello";
}
public String helloworld() {
return hello() + serviceB.world();
}
}
@RequiredArgsConstructor
@Service
public class ServiceB {
private final ServiceA serviceA;
public String world() {
return "world";
}
public String helloworld() {
return serviceA.hello() + world();
}
}
위와 같은 코드를 실행 후 에러 메시지를 보며 판단해보자.
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| serviceA defined in file [/java/main/com/example/circle/service/test/ServiceA.class]
↑ ↓
| serviceB defined in file [/java/main/com/example/circle/service/test/ServiceB.class]
└─────┘
Action:
Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
어디서 많이 본 메시지 아닌가. 스프링부트가 A에게 놀자 했는데 B놀면 논다하고... B한테가니까 A놀면 논단다... 당연히 위 설명과 같이 스프링부트는 팍 식어부러 DI 실패로 인한 에러가 난것이다.
- 해결 방법
해결 방법은 간단하다. 마지막에 물어본 친구가 맨 처음 물어본 친구에게 의존하지 않도록, 고리를 끊어버리는 방법이 가장 좋은 해결책이다. 물론 애초에 순환 참조가 되지않는 구조로 설계하는 것도 좋다. 하지만 이 글을 보고 있다면 의존하지 않고 해결할 수 없는 로직을 설계한 사람들일 것이다. (그건 나도 마찬가지~)
그런 사람들을 위해 준비했다.
1. @Lazy로 Bean 등록 지연
@RequiredArgsConstructor
@Service
public class ServiceA {
private final ServiceB serviceB;
public String hello() {
return "hello";
}
public String helloworld() {
return hello() + serviceB.world();
}
}
@Service
public class ServiceB {
private final ServiceA serviceA;
@Autowired
public ServiceB(@Lazy ServiceA serviceA) {
this.serviceA = serviceA;
}
public String world() {
return "world";
}
public String helloworld() {
return serviceA.hello() + world();
}
}
기존 코드 중 한쪽 서비스에 @Autoried를 통해 직접 생성자를 만들어 @Lazy 처리를 해준다. @Lazy 어노테이션을 이용하면 실제 해당 클래스 내부의 메서드가 실행될 때 DI 되기 때문에 순환참조 없이 진행할 수 있다.
하지만, @Lazy는 큰 단점이 있다. 로딩 시점이 아닌 Bean이 필요한 시점에 주입을 받기 때문에 해당 요청 시 Heap 메모리가 급작스럽게 증가할 수 있어, 메모리가 충분하지 않다면 장애가 발생할 수 있다고 한다. (엄청 큰 프로젝트 아니면 상관 없을듯..?)
때문에 스프링에서도 권장하지 않는 방식이다.
2. Manager Class 두기
@RequiredArgsConstructor
@Service
public class ServiceManager {
private final ServiceA serviceA;
private final ServiceB serviceB;
public String helloworld() {
return serviceA.hello() + serviceB.world();
}
}
해당 방식은 비즈니스 로직을 기준으로 서비스를 통합하는 경우 만들어 사용한다. 위에 사용한 예시는 극단적인 예시지만 실제 순환 참조가 일어났던 경우를 Manager 클래스로 해결한 과정을 간략하게 보면 아래와 같다.
@Service
public class UserService {
public User findUser(Integer userId) {
return userRepository.findById(userId).orElse(null);
}
}
@Service
public class BoardService {
public Board findBoard(Integer boardId) {
return boardRepository.findById(boardId).orElse(null);
}
public Integer boardCountByUserId(Interger userId) {
return boardRepository.countAllByUserId(userId);
}
}
@RequiredArgsConstructor
@Service
public class BoardServiceManager {
private final UserService userService;
private final BoardService boardService;
public BoardResponse getBoardResponse(Integer boardId) {
Board board = boardService.findBoard(boardId);
User user = userService.findUser(board.getUserId());
Integer userBoardCount = boardService.boardCountByUserId(board.getUserId());
user.setBoardCount(userBoardCount);
return new BoardResponse(board, user);
}
}
조금만 생각하면 해당 예시도 순환 참조, Manager 없이 해결할 수 있긴하다. 하지만 여러 로직이 결합되고 복잡해질수록 해당 부분을 찾아내기 어렵다. 그렇기에 임시방편으로 ServiceManager 클래스를 별도로 두기도 한다.
하지만 Manager 클래스도 단점이 존재한다. Manager에서 통합으로 서비스를 집중시켜버리면 단일 책임 원칙이 위반될 가능성이 매우 높다. 더하여 기존 Service 구축 중 Manager까지 생각하여 로직을 구성해야하기 때문에 한 단계 더 생각해야하고, 종국엔 Manager 클래스에 모든 메서드가 종합되는 기이한 현상으로 CI/CD에서 애를 먹을 수 있다.
이 외에도 EventListener를 이용한 방법도 있는 것 같지만 복잡성과 디버깅의 어려움이 있다기에 쳐다도 보지 않았다.
- 종합하면
순환 참조를 일어나지 않도록 로직을 구성하고 설계하는것이 가장 베스트!
피할 수 없다면 메모리 상황에 따른 간편한 @Lazy 설정이 좋아보이며, 메모리가 신경쓰인다면 Manager 클래스를 두는것이 좋을 것 같다.
'Back-End > Spring Boot' 카테고리의 다른 글
| [Spring Boot] HTTP 통신 방식과 Controller 연결 (0) | 2025.05.13 |
|---|---|
| [Spring Boot] Controller vs RestController (0) | 2025.05.13 |
| [Spring Boot] 의존성 주입, @Autowired vs @RequiredArgsConstructor (0) | 2025.05.13 |
| [Spring Boot] IntelliJ Spring Boot Project 생성 파일 분석 (0) | 2025.05.13 |
| [Spring Boot] FFmpeg + 스프링부트 연동 및 사용하기 (0) | 2025.05.12 |