스프링 순환 참조(Circular Reference)

Dope
Webdev TechBlog
Published in
8 min readMay 25, 2020

서론

순환 참조에 대한 설명은 책과 인터넷에 널린 전반적인 내용(지식)을 기반으로한 내용이고, 에러를 해결하면서 얻은 개인적인 생각이 담겨있습니다.

다르게 생각하시는 분이나, 혹은 정확한 정보를 기반으로 더 좋은 내용을 공유해주실 분들은 댓글 남겨주시면 감사하겠습니다!!

순환 참조(Circular Reference)

웹 개발을 하다보면 비지니스 로직을 서비스에서 작성하고(정확히 말하자면 서비스 구현체, serviceImpl) 다른 서비스에서 작성된 로직을 이용하기 위해서 생성자를 통한 의존성 주입을 통해 해당 로직을 재사용하는 경우가 많습니다.

보통 테이블을 설계하고나서 repository, service, serviceImpl, controller, vo 등 자바 클래스들을 만들어가면서 개발을 시작하는데 일반적으로 순환 참조에 대해서 깊게 생각안하고 코딩을 하더라도 순환 참조라는 오류를 쉽게 접하기는 힘든 것 같습니다. 왜냐하면 저는 만 1년이 다되가는 개발자인데 이 글을 작성하는 오늘.. 처음 순환 참조라는 오류에 직면했습니다.

순환 참조 오류를 접하기 힘든 이유 중 하나가, 테이블을 설계하고 테이블 간의 PK, FK 관계처럼 클래스간의 의존관계가 코딩을 하면서 자연스럽게 형성되는 경우가 많기 때문입니다.

우리는 비지니스 로직을 서비스 단에 구현해야 한다는 것을 알고 있습니다.

물론 JPA 를 사용하게 되면 도메인단에 구현해야 하는 것으로 알고있는데 JPA 를 사용하지 않는다는 가정(공공기관 사업을 주력으로하는 중소기업 혹은 SI 회사들은 대부분 JPA 를 사용 안함)하에 설명하겠습니다.

왜 비지니스 로직을 서비스 단에 구현해야 하는지 모르시는 분들은 아래 링크를 통해 배우실 수 있습니다.

비지니스 로직을 서비스 단에 구현해야 하는 이유는 재사용성확장성 때문인데 보통 웹 개발을 하다보면 단순히 DB 조회만 하면 되는데, 굳이 습관처럼 Service 에 메서드를 만들고 ServiceImpl 에 구현하는 자신의 모습을 볼 수 있습니다.

아래 예제를 통해서 어떤 경우에 순환참조가 되는지 자세하게 보겠습니다.

  • ABCServiceImpl
@RequriedArgsConstructor
@Serivce
public class ABCServiceImpl implements ABCService {
private final ABCRepository abcRepository;
private final XYZService xyzService;
@Override
public void createABC(){
xyzService.createABC();
abcRepository.createABC();
// 나머지 생략
}
@Override
public List<ABCVo> findAllABC(){
// 단순히 DB 조회만 해서 목록 리턴
return abcRepository.findAllABC();
}
// 다른 메서드 생략}
  • XYZServiceImpl
@RequriedArgsConstructor
@Serivce
public class XYZService implements XYZService {
private final XYZRepository xyzRepository;
private final ABCService abcService;
@Override
public void createXYZ() {
List<ABCVo> abcList = abcService.findAllABC();
// 등록 로직 생략
}

// 다른 메서드 생략
}

위 처럼 ABCServiceImpl 과 XYZServiceImpl 이 있을 때 각 ServiceImpl 에서 서로의 Service 의존성 주입을 받고 있는 경우(서로 참조하고 있는경우) 이러한 상황에서 ABCService 빈이 메모리에 올라가기 전에 XYZService 빈이 ABCService 빈을 의존주입하는 상황이나 혹은 그 반대의 경우 문제가 발생합니다. 이러한 상황을 순환 참조라고 합니다.

  • Circular Reference Error Message
**************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
| aBCServiceImpl defined in file [/Users/baek/workspace/spring-boot/target/classes/com/baekjh/demo/spring/circular/ABCServiceImpl.class]
↑ ↓
| xYZServiceImpl defined in file [/Users/baek/workspace/spring-boot/target/classes/com/baekjh/demo/spring/circular/XYZServiceImpl.class]
└─────┘

즉, 순환 참조를 통해 알 수 있는것은 한 서비스에서 다른 서비스를 의존성 주입받아서 사용하는 경우 A 는 B 에 의존하고 있다 라고 표현할 수있습니다. 왜냐하면 의존성 주입을 받아서 사용하려는 서비스 구현체에서 구현된 비지니스 로직을 그대로 재사용 한다는 의미가 되기 때문입니다.

위 예제를 토대로 XYZServiceImpl 에서 ABCService 의존성 주입을 받고 있는데 그 이유가 ABCService 에 있는 findAllABC 메서드를 재활용 하기 위함이면, 잘못된 설계입니다. 왜냐하면 findAllABC 는 어떠한 로직도 없고 단순 DB 조회만 하는 역할을 하기 때문에 이런 경우에는 Repository Interafce 를 의존성주입받아 사용하면 순환참조 문제가 해결됩니다.

서비스 단에서의 순환 참조 문제는 대부분 단순 DB 조회를 하기 위해서 한 서비스에서 다른 서비스의 의존성을 참조하여 호출하는 경우에 발생할 가능성이 큽니다. 따라서 아래처럼 단순 DB 조회는 Repository 의존성 주입으로 변경하여 해결하는 편이 좋습니다.

  • 단순 DB 조회는 Repository 의존성 주입으로 해결
@RequriedArgsConstructor
@Serivce
public class XYZService implements XYZService {
private final XYZRepository xyzRepository;
private final ABCRepository abcRepository;
@Override
public void createXYZ() {
List<ABCVo> abcList = abcRepository.findAllABC();
// 등록 로직 생략
}

// 다른 메서드 생략
}

만약에 ABCServiceImpl 의 createABC 메서드에서 XYZServiceImpl 에 있는 createXYZ 로직을 그대로 가져다 사용해야 하는경우에는 위 처럼 XYZService 의존성 주입을 받아야 하는것이 맞습니다. 그런데 XYZServiceImpl 에서도 ABCServiceImpl 에 있는 어떠한 로직을 그대로 재활용 하기 위해서 ABCService 의존성을 주입받아야 하는 상황이면 순환 참조가 발생되기 때문에 잘못된 설계입니다.

순환 참조가 발생했을 때 가장 좋은 해결책은 다시 설계 하는 것입니다.

다른 방법으로는 위에서 설명드린 것처럼 Repository 와 Service 의 역할을 활용하여 가급적 확장성이 없을 것 같은 단순 DB 역할만 해야 하는 메서드들은 굳이 서비스에서 생성하지말고 리포지토리 에서만 생성해서 리포지토리를 의존성 주입받아 사용하면 해결됩니다.

생성자를 통한 의존성 주입의 장점

생성자를 통한 의존성 주입의 장점은 객체 생성 시점에서 순환 참조가 일어나기 때문에 스프링 애플리케이션이 실행되지 않습니다. 즉, 컨테이너가 빈을 생성하는 시점에서 객체생성에 사이클 관계가 생기기 때문입니다. 반면 필드 주입이나 수정자 주입은 객체 생성 시점에 순환 참조가 일어나는지 알 방법이없습니다. 수정자 주입은 스프링 애플리케이션이 구동되고 있는 과정에서 순환 참조를 하고 있는 부분에 대한 호출이 이루어질경우 StackOverflowError 가 발생합니다.

다른 장점은 의존성 주입이 필요한 필드를 final 로 선언하여 Immutable 하게 사용할 수 있습니다. 또한 의존관계가 설정되지 않으면 객체 생성이 불가능하기 때문에 NullPointError 를 방지할 수 있습니다.

위와 같은 장점들 때문에 스프링 진영에서는 생성자를 통한 의존성 주입 사용을 가장 권장하고 있는 것입니다.

결론

한 서비스에서 다른 서비스 의존성을 주입받는 경우 의존 관계에 있다 라고 표현할 수 있습니다. 즉, 상당히 밀접한 관계로 볼 수 있습니다. 만약 리포지토리 의존성 주입을 받는 경우에는 서로 관계가 있다 정도로 볼 수 있습니다. 자주 발생하는 에러는 아니지만 순환 참조에 대해서 잘 모르고 코딩을 하다보면 언젠가 한 번쯤 겪을 에러 입니다. 따라서 순환 참조에 대해서 정확하게 인지하고 테이블 설계 뿐만 아니라, 클래스간의 의존 관계 설계에 대한 부분도 항상 생각하면서 코딩하는게 좋다고 생각합니다.

--

--

Dope
Webdev TechBlog

Developer who is trying to become a clean coder.