-
[springboot] 의존성주입(DI)와 순환참조 문제 해결스프링&스프링부트 2024. 8. 14. 15:36
자바 - 생성자 인강을 보고 정리한 후 스프링 프레임워크 기반 실무의 코드를 보니, 더 많은 것들이 보였다.
각 서비스마다 생성자 주입이 되어 있었는데 인강에서 보는 자바의 기본적인 생성자 주입 방법과는 차이가 있었다.
의존성 주입 (Dependency Injection, DI)
DI란 외부에서 두 객체 간의 관계를 결정해 주는 디자인 패턴으로, 인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않도록 하고 런타임 시에 관계를 동적으로 주입하여 유연성을 확보하고 결합도를 낮출 수 있게 해 준다.
- 의존성이란 한 객체가 다른 객체를 사용할 때 의존성이 있다고 한다.
- 의존성 주입이란 두 객체 간의 관계를 맺어주는 것이다.
의존성 주입 방법
1) 생성자 주입
2) 필드 주입
3) 수정자 주입
( Spring 4부터는 생성자 주입 권장 )
1) 생성자 주입 EX
@Component public class ExService { private final ExRepository exRepository; @Autowired public ExService(ExRepository exRepository) { this.exRepository = exRepository; } }
-생성자를 호출할 때 1번만 호출되기 때문에 final 사용 가능 (불변성 보장)
-생성자가 1개인 경우 @Autowired 생략 가능2) 필드 주입 EX
@Component public class ExService { @Autowired private final ExRepository exRepository; }
-코드의 간결성
- 반드시 DI 프레임워크가 존재해야 하므로 사용 지양
-클래스 외부에서 접근이 불가능해 테스트하기 어려움3) 수정자 주입 EX
@Component public class ExService { private final ExRepository exRepository; @Autowired public void setExService(ExRepository exRepository) { this.exRepository = exRepository; } }
-변경 가능성이 있는 의존 관계에서 사용
-생성자 호출 이후 필드 변수에서 변경이 일어나야 하기 때문에 final 사용 불가능생성자 주입 방식 권장
- 대부분의 의존 관계는 애플리케이션이 종료될 때까지 변경될 경우가 거의 없다
- DI 프레임워크에 의존하지 않고 순수 자바 언어로도 작동하여 자바 언어의 객체 지향 특징이 잘 살아있다
- @RequiredArgsConstructor 어노테이션 사용하면 해당 클래스의 의존성 관계가 변경될 때마다 생성자 코드 수정의 번거로움 해결실무 서비스 코드를 보면 하나의 서비스 안에 무수히 많은 서비스들이 주입되어 있었다
여기서 특이한 점은 다른 서비스들은 생성자 주입 방식으로 되어 있었지만 자기 자신은 수정자 주입 방식으로 되어있었다는 점이었다.
@Service public class MainService extends BaseService { /** * OtherService1 */ private final OtherService1 otherService1; /** * OtherService2 */ private final OtherService2 otherService2; ... /** * OtherService10 */ private final OtherService10 otherService10; /** * MainService */ private MainService selfService; /** * 생성자 * @param otherService1 * @param otherService2 ... * @param otherService10 */ public MainService( OtherService1 otherService1, OtherService2 otherService2, ... OtherService10 otherService10, ) { this.otherService1 = otherService1; this.otherService2 = otherService2; ... } @Lazy @Autowired private void setMainService(MainService mainService) { this.selfService = mainService; }
이런 구조로 되어 있었다.
왜 생성자 주입과 수정자 주입이 함께 사용되었을지 여쭤보니 원래는 이렇게 구성하면 안 되지만 순환참조 때문에 어쩔 수 없다고 하셨다. 순환참조 문제를 해결하기 위해 생성자 주입이 아닌 setter 주입으로 변경하고 @lazy 어노테이션을 사용했다.
순환 참조
A 클래스가 B 클래스의 Bean을 주입받고 B 클래스가 A 클래스의 Bean을 주입받는 상황처럼 서로 순환되어 참조할 경우 발생하는 문제 (메모리에 함수의 CallStack이 계속 쌓여 스프링 애플리케이션 로딩시점에서 StackOverflow 에러가 발생)
- A클래스가 B클래스에 의존, B클래스가 C클래스에 의존하는 경우, C > B > A 순서로 Bean을 생성한다.
- A클래스가 B클래스에 의존, B클래스가 A클래스에 의존하는 경우, A클래스의 Bean을 생성하는데 B의 Bean이 필요한데 없으니 B클래스의 Bean 먼저 생성한다. 하지만 B클래스가 다시 A클래스를 의존하고 있기 때문에 A클래스의 Bean을 먼저 만들게 된다. 이 상황 때문에 무한 반복에 빠지게 되며 결과적으로 Bean을 생성하지 못하게 된다.
cf. autowired를 이용한 필드 주입에서 어플리케이션 구동 시점에 에러가 발생하지 않는 이유는 빈의 생성과 조립 시점이 분리되어 있기 때문이다. 생성자 주입은 객체의 생성과 조립이 동시에 실행되기 때문에 에러를 사전에 발견할 수 있지만, autowired는 모든 객체의 생성이 완료된 후에 조립(의존관계주입)이 처리된다.
cf. 주입 방식에 따라 빈을 주입하는 순서
필드주입/수정자주입 : 먼저 빈을 생성한 후 주입하려는 빈을 찾아 주입한다.
생성자주입 : 빈을 생성하기 전에 주입하려는 빈을 찾는다.순환참조 문제 해결
순환참조 문제가 발생할 수 있는 구조자체를 만들지 않는 것이 좋다.
하지만 어쩔 수 없는 지금과 같은 경우에는
1) @Lazy 어노테이션을 통해 임의로 해결
2) 필드 주입방식 혹은 setter 주입방식 이용
cf. @lazy
spring은 기본적으로 즉시 초기화하는데, @lazy는 초기화를 지연시킨다.
@lazy를 적용하게 되면 해당 클래스 객체를 사용하려고 할 때 초기화되어 사용하게 된다.
(참고)
https://engineerinsight.tistory.com/46
https://velog.io/@sujin1018/%EC%8A%A4%ED%94%84%EB%A7%81-Spring-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85-%EB%B0%A9%EB%B2%95-%EB%B0%8F-%EC%83%9D%EC%84%B1%EC%9E%90-%EC%A3%BC%EC%9E%85-%EB%B0%A9%EB%B2%95%EC%9D%98-%EC%9E%A5%EC%A0%90
https://ch4njun.tistory.com/269
https://mangkyu.tistory.com/125728x90'스프링&스프링부트' 카테고리의 다른 글
[스프링MVC - 1편] 웹서버 & 웹 애플리케이션 서버 (0) 2024.12.22 [spring] DTO 와 VO 무슨 차이가 있을까 (0) 2024.08.14 [스프링부트/웹 애플리케이션 개발]스프링 데이터 JPA, QueryDSL (0) 2023.01.17 [스프링부트/웹 애플리케이션 개발]API 개발 고급 - 실무 필수 최적화 (0) 2023.01.17 [스프링부트/웹 애플리케이션 개발]API 개발 고급 정리 (0) 2023.01.16