[Spring]@RequiredArgsConstructor와 순환 참조

봄도둑·2022년 7월 19일
1

Spring 개인 노트

목록 보기
5/17

취업하기 전 학원에서 공부한 코드와 혼자 작성해둔 코드를 가볍게 훑어보던 중 제 눈에 확 띄게 들어온 녀석이 있었습니다.

@RestController
@AllArgsConstructor
@RequestMapping("/project/member")
public class MemberController {

    @Autowired
    MemberService memberService;
		
		//...
}

저의 시선을 한 번에 강탈해버린 매혹적인 녀석 @Autowired , 이 녀석을 처음 배웠을 때는 세상 모든 spring container의 문제를 해결해줄 것만 같았던 녀석이었지만 현업에서 첫 Autowired를 썼던 순간 제가 가지고 있던 환상은 무참히 깨지고 말았습니다.

@Autowired 는 무슨 녀석이길래 저의 환상을 한번에 날려버렸을까요? 먼저 spring의 의존성 주입을 임의의 코드 없이 생성하고 설정해주는 친구들을 만나봅시다.

1. 의존성을 주입하는 다양한 방법

스프링 프레임 워크를 구성하는 핵심 특징 중 하나인 의존성 주입(Dependency Injection, DI)가 있습니다. 의존성 주입이란 외부에서 두 객체 간의 관계를 결정해주는 디자인 패턴으로 두 객체 사이에 인터페이스를 사이에 둬서 클래스 레벨에서는 의존 관계가 고정되지 않도록 하고 런타임 시에 관계를 동적으로 주입해 유연성을 확보하고 결합도를 낮출 수 있게 해줍니다. 조금 더 쉽게 풀어 쓰자면 어떤 객체가 사용하는 의존 객체를 직접 만들어 사용하는 게 아니라, 외부로부터 주입 받아 사용하는 방법입니다.

의존성 주입을 받지 않게 되면 결국 객체 내부에서 다른 객체를 생성하게 되는데 이는 강한 결합 관계를 가지게 됩니다. (여기서 다룰 내용은 의존성을 주입하는 방법이기 때문에 의존성에 대해서는 간단히 설명하는 정도로만 다루겠습니다.)

그렇다면, 이러한 의존성 주입을 하는 방법들은 무엇이 있을까요? 크게는 2가지 방법이 있습니다. 생성자를 통해 주입하는 경우와 필드를 통해 주입하는 경우, 2가지가 있습니다. 이 때 생성자를 통해서 주입하는 방법의 경우 @RequiredArgsConstructor 을 통해 간편하게 주입해줄 수 있고, @Autowired 를 통해 필드 주입을 간편하게 진행할 수 있습니다. 이러한 어노테이션을 사용하면 의존성 주입을 임의의 코드 없이 편리하게 진행할 수 있습니다.

@RequiredArgsConstructor 는 final 필드, 혹은 @NonNull 어노테이션이 붙은 필드에 대해 초기화 하고 생성자를 만들어주는 lombok의 annotation 중 하나로 생성자 주입에서 사용되는 @AllArgsConstructor (모든 필드에 대해 생성자 생성), @NoArgsConstructor (파라미터가 없는 생성자 생성)과 같은 기능을 수행합니다.

  1. @RequiredArgsConstructor 를 사용하지 않고 생성자를 주입할 경우
@Service
public class BannerServiceImpl implements BannerService {

    private BannerRepository bannerRepository;

    private CommonFileUtils commonFileUtils;

    @Autowired
    public BannerServiceImpl(BannerRepository bannerRepository, CommonFileUtils commonFileUtils) {
        this.bannerRepository = bannerRepository;
        this.commonFileUtils = commonFileUtils;
    }
}
  1. @RequiredArgsConstructor 를 사용하지 않고 @Autowired 를 사용해 필드 주입할 경우
@Service
public class BannerServiceImpl implements BannerService {

    @Autowired
    private BannerRepository bannerRepository;

    @Autowired
    private CommonFileUtils commonFileUtils;
}
  1. @RequiredArgsConstructor 를 사용해 주입할 경우
@Service
@RequiredArgsConstructor
public class BannerServiceImpl implements BannerService {

    private final BannerRepository bannerRepository;

    private final CommonFileUtils commonFileUtils;
}

2. 생성자 주입을 사용해야 하는 이유

입사한지 얼마 지나지 않아 지금까지 작성한 프로젝트에 대해 간단한 코드 리뷰를 받았던 기억이 있습니다. 그리고 @Autowired 를 통해 굉장히 매콤하게 혼났던 기억도 같이 있습니다.

결론부터 빠르게 집고 가겠습니다. @Autowired 를 통한 필드 주입보다 우리는 @RequiredArgsConstructor 를 통한 생성자 주입을 사용해야 합니다. 이유는 아래와 같습니다.

  • NullPointerException 방지 가능
  • 주입 받을 필드를 final로 선언 가능 → controller 등 내부에서 주입된 필드가 다른 것으로 바꿔치기될 가능성이 낮음
  • 순환참조에 대한 방지 가능
    • 개발을 하다보면 여러 서비스들간의 의존 관계가 생기게 됨.

위의 이유들 중 무엇보다 중요한 것은 순환 참조에 대한 방지입니다. 그렇다면 간단한 예제를 통해 순환 참조에 대해 살펴보겠습니다. (순환 참조에 대한 예제는 walker님의 왜 생성자 주입이 권장되며 순환 참조란 뭘까? 글의 예제에서 가져왔습니다.)

//닭이 달걀을 낳음

@Component
public class Chicken {
    @Autowired
    Egg egg;

    public void layEgg(){
        egg.becomeChicken();
    }

}
//달걀이 닭이 됨

@Component
public class Egg {
    @Autowired
    Chicken chicken;

    public void becomeChicken() {
        chicken.layEgg();
    }

}
//닭이 달걀을 낳는다를 실행

@Bean
public CommandLineRunner run(Chicken chicken, Egg egg) throws Exception {
    return (String[] args) -> {
        chicken.layEgg(); // 실행
    };
}

달걀을 낳음 → 달걀이 닭이 됨 → 닭이 달걀을 낳음 → 달걀이 닭이 됨 → .... 이 무한 반복됩니다.

java.lang.StackOverflowError: null

이러한 구조에서 서로가 서로의 객체를 참조하면서 실행되는 메소드는 결국 무한루프를 일으키며 stack에 무한정 메소드를 쌓게 되고 결국 메모리는 폭발하게 됩니다. 필드 주입 혹은 수정자 주입에서 이러한 런타임 시, 즉 프로그램이 구동할 때 순환 참조의 문제가 발견됩니다.

필드 주입, 수정자 주입은 객체가 생성된 후 비즈니스 로직상에서 순환 참조가 일어나기 때문에 객체가 생성되는 시점에서 순환 참조 여부를 알 수 없다는 문제가 발생합니다. 즉, 해당 객체를 사용하게 되는 시점에서 순환 참조가 발견되기 때문에 이미 구동중인 프로그램의 메모리가 터지게 되는 문제가 발생합니다.

생성자 주입의 경우 객체가 생성되는 시점에 순환 참조 여부를 확인하기 때문에 최초 빈 생성 시 해당 순환 참조를 잡아내고 스프링 애플리케이션이 구동하지 않게 막습니다. 프로그램이 돌아가던 중 순환 참조를 만나 터지는 일이 없어지게 됩니다.

이러한 치명적인 오류를 일으키는 필드 주입의 경우, 혹시 모를 다른 장점이 있지 않을까요? 네, 편하다는 장점 외에는 아무것도 없습니다. 되도록 필드 주입보다는 생성자 주입을 사용하길 권장합니다.


*References

https://seon54.github.io/java/2020/10/04/spring-core-07/
https://velog.io/@gongmeda/스프링-핵심-원리-의존관계-자동-주입-2
https://velog.io/@developerjun0615/Spring-RequiredArgsConstructor-어노테이션을-사용한-생성자-주입
https://velog.io/@walker/Spring-왜-생성자-주입이-권장되며-순환참조란-뭘까
https://yaboong.github.io/spring/2019/08/29/why-field-injection-is-bad/
https://namocom.tistory.com/663
https://byul91oh.tistory.com/432
https://mangkyu.tistory.com/167
https://needjarvis.tistory.com/595
https://aeliketodo.tistory.com/23

profile
Java Spring 백엔드 개발자입니다. java 외에도 다양하고 흥미로운 언어와 프레임워크를 학습하는 것을 좋아합니다.

0개의 댓글