-
Spring) DI(의존성 주입) 그리고 IoC(제어 역전)Spring 2024. 8. 10. 18:27
DI 가 나오기 전 코드
DI 를 배우기 전에 DI 가 나오기 전에 여러가지 방법으로 객체를 생성했는지 Java 코드를 통해 알아봅시다.
1. 직접 인스턴스화
DI가 등장하기 전에 가장 일반적인 방법은 클래스 내부에서 필요한 객체를 직접 생성하는 것이었습니다. 즉, 클래스가 자신의 의존성을 직접 관리하는 방식입니다. 보통 학교나 학원에서 Java 를 배울때 객체를 배우고 그 객체를 생성하려면 new 를 통해 생성하는것으로 배워왔습니다.
public class PetController { private PetService petService = new PetService(); //직접 객체를 생성 public void handleRequest() { petService.doSomething(); //객체를 활용 } }
여기서 PetController는 PetService 객체를 직접 생성하고 사용합니다. 이러한 방식은 다음과 같은 문제점이 있습니다:
- 강한 결합: PetController와 PetService가 강하게 결합되어 있어, PetService의 구현을 변경하거나 다른 서비스로 대체하려면 PetController의 코드를 수정해야 합니다.
- 테스트 어려움: 단위 테스트 시 PetService를 목(Mocking) 객체로 대체하기 어렵습니다.
- 유연성 부족: 새로운 기능을 추가하거나 확장하기 어렵습니다.
2. 팩토리 패턴
팩토리 패턴도 DI 패턴이 등장하기 전에 자주 사용된 방법 중 하나입니다. 팩토리 패턴에서는 객체 생성을 전담하는 팩토리 클래스를 사용합니다.
public class PetServiceFactory { public static PetService createPetService() { return new PetService(); } } public class PetController { private PetService petService; public PetController() { this.petService = PetServiceFactory.createPetService(); } public void handleRequest() { petService.doSomething(); } }
문제점
1. 객체를 여러 곳에서 생성해야 함
public class PetController { private PetService petService = new PetService(); public void handleRequest() { petService.performService(); } } public class AnotherController { private PetService petService = new PetService(); // 중복된 객체 생성 }
2. 같은 객체를 공유할 수 없음
PetService service1 = new PetService(); PetService service2 = new PetService(); // service1과 service2는 서로 다른 인스턴스
3. 객체 생명 주기 관리의 어려움
public class MainApplication { public static void main(String[] args) { // 객체를 여러 번 생성 PetService service1 = new PetService(); PetController controller1 = new PetController(service1); PetService service2 = new PetService(); AnotherController controller2 = new AnotherController(service2); // 서로 다른 PetService 인스턴스를 사용 controller1.handleRequest(); controller2.handleRequest(); } }
DI 패턴
DI 패턴이 등장한 이유
DI 패턴은 위와같은 문제를 해결하기 위해 등장했습니다. DI를 사용하면 객체 간의 결합도를 낮추고, 의존성 관리를 외부 컨테이너(예: Spring)에게 맡겨 코드의 재사용성과 테스트 용이성을 크게 개선할 수 있습니다.
이러한 이유로 DI는 현대적인 객체지향 프로그래밍에서 중요한 설계 패턴으로 자리 잡게 되었습니다.
DI 패턴을 적용한 개선된 코드
// PetService 클래스: 비즈니스 로직을 처리하는 서비스 클래스 public class PetService { public void performService() { System.out.println("Performing service for pet"); } } // PetController 클래스: PetService에 의존하는 컨트롤러 클래스 public class PetController { private final PetService petService; // PetService를 외부에서 주입받는 생성자 public PetController(PetService petService) { this.petService = petService; } public void handleRequest() { petService.performService(); } } // AnotherController 클래스: 또 다른 컨트롤러 클래스 public class AnotherController { private final PetService petService; // PetService를 외부에서 주입받는 생성자 public AnotherController(PetService petService) { this.petService = petService; } public void handleRequest() { petService.performService(); } } // MainApplication 클래스: 의존성을 주입하고 애플리케이션을 실행하는 클래스 public class MainApplication { public static void main(String[] args) { // 의존성을 외부에서 한 번만 생성 PetService petService = new PetService(); // 생성된 PetService를 주입하여 컨트롤러 생성 PetController controller1 = new PetController(petService); AnotherController controller2 = new AnotherController(petService); // 각 컨트롤러에서 동일한 PetService 인스턴스를 사용 controller1.handleRequest(); controller2.handleRequest(); } }
개선된 코드의 장점
- 객체 재사용: PetService 객체를 한 번만 생성하고, PetController와 AnotherController에 주입하여 사용합니다. 이를 통해 객체를 재사용하고, 코드 중복을 제거했습니다.
- 상태 일관성 유지: 동일한 PetService 인스턴스를 사용하므로, 상태를 일관되게 유지할 수 있습니다. 모든 컨트롤러가 동일한 객체를 참조하므로, 상태 관리가 훨씬 쉬워집니다.
- 유지보수성 향상: PetService의 구현이 변경되더라도, 주입하는 코드만 수정하면 됩니다. PetService를 사용하는 모든 클래스에서 코드를 수정할 필요가 없습니다.
- 유연성 증가: PetService의 다른 구현체를 사용할 필요가 있을 때, 이를 생성하는 부분만 수정하면 됩니다. 예를 들어, AdvancedPetService라는 새로운 서비스 클래스를 도입할 때, 기존 코드를 거의 수정하지 않고도 DI를 통해 쉽게 적용할 수 있습니다.
DI 와 IoC(Inversion Of Control)
그래서 이 DI 를 어디서 사용하느냐? 바로 Spring boot 에서 많이 사용하고 있습니다.
Spring boot 코드를 보면 해당 코드와 같은 상황이 많이 벌어집니다.
@Service public class PetService { // 서비스 로직 } //1.@Autowired 방식 @Controller public class PetController { @Autowired private PetService petService; public void handleRequest() { petService.doSomething(); } } //2. 생성자 주입 방식 @Controller @RequiredArgsConstructor public class PetController { private final PetService petService; }
이 코드를 보시면 DI 를 "등록" 하는것에 대해서는 이해하셨겠지만, 결국 위에서 설명드린 자바코드 처럼 DI 를 등록한 이 "PetController" 를 사용할때 "PetService" 를 주입해줘야 하는데, 이것은 어디서 주입할까요?
IoC(Inversion Of Control) 란?
Spring 에서 의존성을 대신 주입해준다. 이때 나온것이 Ioc 즉 제어역전이라는것 입니다.
사실 위에 코드만으로 Spring boot 의 API 하나가 완성되었는데요, 이때 DI 를 등록해주는것을 Spring boot 에서 제공하고있습니다. 사실 의존성을 주입하는것은 기존에 프로그래머가 구현했어야 했는데,
이것을 프레임 워크에서 제공하고 제어하는것 = 제어 역전
즉, 객체의 생성과 관리에 대한 책임을 애플리케이션 코드에서 프레임워크(예: Spring)로 넘김으로써, 객체 간의 결합도를 낮추고 코드의 유연성과 확장성을 높이는 설계 원칙입니다.
예시
1. Bean 등록 ( MyService):
- MyService 클래스에서 @Service 으로 MyService 객체를 Spring 컨테이너에 등록했습니다. 이 등록된 Bean은 Spring 컨테이너에 의해 관리됩니다.
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Service public class MyService { // MyService 를 @Service 를 통해 Bean으로 등록 }
2. DI 적용 (MyController):
- MyController 클래스는 MyService 객체를 필요로 합니다. @Autowired를 사용하여 생성자 주입 방식으로 MyService Bean을 주입받고 있습니다.
- Spring이 MyController 객체를 생성할 때, MyService 에서 등록한 Bean을 자동으로 주입합니다.
@Controller public class MyController { private final MyService myService; @Autowired // 생성자 주입을 통해 MyService Bean을 주입받음 public MyController(MyService myService) { this.myService = myService; } public void doSomething() { myService.performAction(); // 주입받은 MyService Bean을 사용 } }
IoC 의 DI 주입 과정
1. Spring Boot 애플리케이션 시작
- Spring Boot 애플리케이션이 시작되면 SpringApplication.run() 메서드가 호출됩니다.
- 이 메서드는 Spring 애플리케이션 컨텍스트(ApplicationContext)를 초기화하고 부트스트랩합니다.
2. Spring ApplicationContext 초기화
- SpringApplication.run() 메서드는 Spring ApplicationContext를 생성합니다. ApplicationContext는 Spring의 핵심 컨테이너로서 모든 Spring 빈(bean)들을 관리합니다.
- ApplicationContext는 다양한 종류가 있지만, Spring Boot에서는 주로 AnnotationConfigApplicationContext 또는 AnnotationConfigServletWebServerApplicationContext가 사용됩니다.
3. Component Scan 및 빈 등록
- Spring Boot 애플리케이션이 시작되면, @SpringBootApplication 애너테이션을 통해 자동으로 @ComponentScan이 수행됩니다. 이 애너테이션은 특정 패키지 및 그 하위 패키지를 스캔하여 @Component, @Service, @Repository, @Controller 등과 같은 애너테이션이 붙어 있는 클래스를 찾아 빈으로 등록합니다.
- 또한, @Configuration 클래스에 정의된 @Bean 메서드들도 스캔되어 빈으로 등록됩니다.
4. 의존성 주입 준비
- 빈이 등록된 후, Spring은 이들 빈 간의 의존성을 분석합니다. 이 과정에서 생성자 주입, 필드 주입, 또는 세터 주입과 같은 방식으로 의존성을 주입할 준비를 합니다.
- Spring은 각 빈의 생성자 또는 필드에 주입해야 할 다른 빈을 찾습니다. 이 과정은 주로 @Autowired, @Inject, @Resource 등의 애너테이션을 통해 수행됩니다.
5. 의존성 주입 (Dependency Injection)
- Spring은 의존성 주입이 필요한 빈의 생성자나 필드에 해당 빈을 주입합니다.
- 생성자 주입: 클래스의 생성자에 의존성이 주입됩니다.
- 필드 주입: 클래스의 멤버 필드에 직접 주입됩니다.
- 세터 주입: 세터 메서드를 통해 주입됩니다.
- 이 과정에서 빈이 서로의 의존성을 충족하도록 연결됩니다.
Spring에서의 빈 초기화 순서 관리
Spring은 의존성 주입(Dependency Injection)과 관련된 빈의 초기화 순서를 자동으로 관리합니다. 이는 다음과 같은 방식으로 이루어집니다:
- 의존성 분석 및 빈 등록:
- Spring은 애플리케이션 컨텍스트를 초기화할 때, 먼저 모든 빈을 스캔하여 등록합니다. 이 과정에서 각 빈의 의존성을 분석합니다.
- B 클래스가 A 클래스에 의존성을 제공해야 한다면, B 클래스는 A 클래스보다 먼저 초기화되어야 합니다.
- 의존성 주입 시점:
- A 클래스가 초기화될 때, Spring은 A 클래스에 필요한 모든 의존성을 주입해야 합니다. 이 시점에서 B 클래스가 이미 초기화되어 있지 않다면, A 클래스의 초기화도 진행되지 않습니다.
- Spring은 이를 위해 의존성 그래프를 분석하여 필요한 빈을 미리 초기화하고, 그 후에 A 클래스를 초기화합니다.
- 순환 의존성 문제 해결:
- 만약 A 클래스와 B 클래스 간에 순환 의존성이 있는 경우, Spring은 이 문제를 해결하기 위해 @Lazy 주입 또는 Setter 주입을 사용하는 방법을 제공합니다.
- @Lazy 애너테이션을 사용하면, 해당 빈은 실제로 사용될 때 초기화되므로 순환 의존성 문제를 회피할 수 있습니다.
- 생성자 주입과 순서:
- 생성자 주입(Constructor Injection)을 사용하는 경우, Spring은 생성자에서 필요한 의존성을 미리 주입하므로, 생성자에 지정된 순서대로 초기화를 보장합니다. 만약 A 클래스의 생성자에서 B 클래스를 필요로 한다면, B 클래스가 먼저 초기화된 후 A 클래스의 생성자가 호출됩니다.
예시
@Component public class B { public B() { System.out.println("B initialized"); } } @Component public class A { private final B b; @Autowired public A(B b) { this.b = b; System.out.println("A initialized"); } } //B 클래스는 A 클래스의 생성자에 의해 주입되므로, //Spring은 먼저 B 클래스를 초기화한 후, A 클래스를 초기화합니다. //B initialized //A initialized
결론
DI(Dependency Injection)와 IoC(Inversion of Control)는 현대 소프트웨어 개발에서 핵심적인 설계 원칙으로, 두 개념은 서로 밀접하게 연결되어 있습니다.
IoC는 객체의 제어 흐름을 애플리케이션 코드에서 프레임워크나 외부 시스템으로 넘기는 것을 의미합니다. 이는 객체가 스스로 의존성을 관리하지 않고, 필요한 의존성을 외부에서 제공받도록 함으로써, 객체 간의 결합도를 낮추고 코드의 유연성과 확장성을 높입니다.
DI는 IoC를 실현하는 방법 중 하나로, 객체가 필요한 의존성을 외부에서 주입받는 방식입니다. DI를 통해 객체는 자신의 의존성을 직접 생성하지 않고, 외부에서 주입받아 사용하게 됩니다. 이로 인해 코드의 재사용성과 유지보수성이 크게 향상되며, 테스트 환경에서도 객체를 쉽게 대체하거나 Mocking할 수 있어 테스트 용이성도 증가합니다.
결론적으로, IoC는 객체의 제어 권한을 외부로 넘기는 원칙이며, DI는 이를 구현하는 구체적인 방법입니다. DI를 통해 IoC가 실현되며, 이 두 개념은 함께 사용될 때 객체지향 설계의 유연성을 극대화할 수 있습니다. 이를 통해 복잡한 애플리케이션에서도 일관성 있고 유지보수 가능한 구조를 설계할 수 있게 됩니다.
'Spring' 카테고리의 다른 글
Spring) Bean 의 생명주기(Bean Lifecycle) (0) 2024.08.20 Spring) AOP 와 프록시 패턴(Proxy Pattern) (0) 2024.08.16 Spring Boot: Java에서 Kotlin으로 (2) - TDD,BDD,DDD (0) 2024.07.28 Spring Boot: Java에서 Kotlin으로 (1) - Java vs Kotlin (0) 2024.07.26 [spring boot] JAVA 의 생성자와 Spring 의 @Bean (2) (0) 2023.04.22