의존성 주입은 스프링 프레임워크의 핵심 기능입니다.
우리는 주로 필드에 @Autowired 어노테이션을 사용해서 의존성을 주입 하곤 합니다.
그런데 IntelliJ에서 경고 표시가 뜹니다.
Field injection 방식이 아닌, Constructor injection을 추천한다는 내용입니다.
Field injection is not recommended Inspection info: Reports injected or autowired fields in Spring components. The quick-fix suggests the recommended constructor-based dependency injection in beans and assertions for mandatory fields.
Field injection (필드 주입)
@RestController public class BookController { @Autowired private BookService bookService; @GetMapping("/book") public String book() { return bookService.callBook(); } }
Setter injection (수정자 주입)
@RestController public class BookController { private BookService bookService; @Autowired public void setBookService(BookService bookService) { this.bookService = bookService; } @GetMapping("/book") public String book() { return bookService.callBook(); } }
Constructor injection (생성자 주입)
@RestController public class BookController { private final BookService bookService; @Autowired public BookController(BookService bookService) { this.bookService = bookService; } @GetMapping("/book") public String book() { return bookService.callBook(); } }
생성자 주입은 클래스 객체가 생성될 때 모든 의존성이 완전한 상태로 주입됩니다.
1. 생성자 인수에 사용하는 빈의 인스턴스를 호출 2. 생성자 인수가 주입되면, 클래스의 빈 인스턴스를 생성하고 스프링 컨테이너에 등록
필드와 수정자 주입은 필드나 메서드를 통해 의존성을 주입하며, 객체가 생성된 후에도 의존성 변경이 가능합니다.
1. 클래스의 빈 인스턴스를 생성해서 스프링 컨테이너에 등록 2. 필드나 메서드를 통해 빈 의존성 주입
생성자 주입이 다른 주입 방식과 다른 점은 클래스에 필요한 객체가 완전한 상태일 때만, 클래스의 인스턴스가 생성된다는 점입니다.
그래서 생성자 주입은 안전하고 추천하는 방식입니다.
DI(Depedency Injection)는 외부에서 관리되는 의존 객체를 주입하는 방식입니다.
객체 사이의 느슨한 결합을 가능하게 하는 객체 지향 프로그래밍의 디자인 패턴입니다.
스프링 컨테이너가 DI를 관리하는 DI 컨테이너 역할을 합니다.
여기에 프레임워크에서 객체를 제어하는 IoC(Inversion of Control, 제어의 역전) 개념도 포함됩니다.
의존성 주입의 장점은 확장성에 있습니다.
코드의 재사용성과 유연성이 높아 지며, 객체의 느슨한 결합으로 구조화되고 테스트가 용이한 프로젝트를 구성할 수 있습니다.
둘 이상의 객체가 서로를 참조 하면서 객체 생성에 실패하는 문제입니다.
나는 절대 그런 바보 같은 코드를 짜지 않는다!
물론 그렇게 생각할 수 있지만, 프로젝트 코드가 많아지고 협업을 하면서 충분히 발생할 수 있는 오류입니다.
BookService에서 AuthService 객체 의존성을 주입 받습니다.
@Service public class BookService { private final AuthorService authorService; @Autowired public BookService(AuthorService authorService) { this.authorService = authorService; } public String callBook() { return "Book called"; } }
AuthService도 BookService 객체 의존성을 주입 받습니다.
@Service public class AuthorService { private final BookService bookService; @Autowired public AuthorService(BookService bookService) { this.bookService = bookService; } public String callAuthor() { return "Author called"; } }
BookService ←→ AuthorService 서로 참조하는 구조입니다.
그러면 스프링 빈의 초기화 과정에서 순환 참조가 발생하고 오류가 납니다.
이쯤 되면 스프링 컨테이너는 어떤 객체를 먼저 생성해야 하는지 알 수가 없습니다.
바로 멍청이 상태가 됩니다.
위에 적절한 샘플 코드를 작성했습니다.
바로 생성자 주입 방식을 사용하면 됩니다.
물론 생성자 주입 방식을 사용해도 순환 참조는 발생합니다.
하지만 애플리케이션 구동 시점에 오류를 알수 있어서, 바로 조치가 가능합니다.
*************************** APPLICATION FAILED TO START *************************** Description: The dependencies of some of the beans in the application context form a cycle: bookController ┌─────┐ | bookService defined in file [/Users/cjgojh/workspace/test/test-code-spring/spring/target/classes/com/devfoxstar/spring/Service/BookService.class] ↑ ↓ | authorService defined in file [/Users/cjgojh/workspace/test/test-code-spring/spring/target/classes/com/devfoxstar/spring/Service/AuthorService.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.
반면 필드와 수정자 주입 방식은 애플리케이션이 정상 구동되고, 호출하는 시점에 오류를 알수 있습니다.
이미 코드가 Release 됐으면 끔찍한 상황이 발생할 수도 있습니다.
참고로 스프링 2.6부터는 순환 참조가 기본적으로 금지됐습니다.
그래서 어떤 주입 방식을 사용해도 애플리케이션 구동 시점에 오류가 발생합니다.
그래도 순환 참조를 쓰고 싶다면 아래 스프링 옵션으로 설정할 수 있습니다.
그런데 굳이?
spring.main.allow-circular-references=true
추가로 @Lazy 어노테이션을 활용해서 지연 처리도 가능합니다.
하지만 지연 처리에 따른 성능 이슈 발생 가능성이 있고, 순환 참조는 좋은 구조가 아니기에 지양하는 게 좋습니다.