[JAVA Spring] 의존성 주입(Dependency Injection) & IoC(Inversion of Control)의 개념
[JAVA Spring] 의존성 주입(Dependency Injection) & IoC(Inversion of Control)의 개념 — #JAVASpring #자바스프링 #의존성주입 #DependencyInjection #스프링의존성 #IoC #IoC란 #inver...
#JAVASpring #자바스프링 #의존성주입 #DependencyInjection #스프링의존성 #IoC #IoC란 #inversionofcontrol #개발자의도구들
AI스쿨 msa기반 java 백엔드 코스 중에 공부한 내용을 작성하였습니다
\* 현재 Spring 프로젝트를 진행 중입니다. 전체적인 목차를 보시려면 여기를 눌러주세요.
서론
이전글을 통해 Spring java Bean을 알아보며 IoC container에 대해 언급한 적이 있었는데요. IoC 컨테이너가 자동으로 Bean 생성과 주기를 관리해주며, Dependency Injection역시 알아서 해준다고 하였습니다.
IoC와 Di에 대해서 잘 모르는 분들은 이해가 안가리라 생각되는데, 이번글을 통해서 핵심을 짚고 넘어갈 수 있었으면 좋겠습니다.
IoC(Inversion of Control)
우선 IoC에 대한 개념이해가 필요합니다. IoC는 Inversion of Control의 약자로, 제어권이 역전된 상태를 뜻합니다. 그럼 IoC에서 말하는 제어권이라는건 어떤 것을 의미할까요? 아니, 애초에 제어권이 무엇일까요?
제어권
제어권은 말그대로 객체를 생성하고, 제거하며 사용하는 등의 권리를 의미합니다. 예를들어 A라는 객체 내부에서 B라는 객체를 사용한다면, B의 제어권은 A에게 있다고 볼 수 있겠습니다.
| public class A { private B b; public MyClass(B b) { this.b = b; }} |
|---|
제어권의 역전(IoC)
한 프로그램에서의 제어권은 본래 프로그래머에게 있기 마련입니다. 사실상 모든 객체에 대한 제어권을 프로그래머가 가지고 있습니다. 하지만, Spring은 조금 다릅니다. Spring 프레임 워크는 특정 객체에 대한 제어권을 프로그래머가 아닌 프레임워크 자신이 직접 가지고 있기 때문입니다.
이를 IoC라고 부르며, 이는 전통적인 프로그래밍과 대조되는 Spring의 독특한 성질이 되겠습니다.
IoC의 이점
모든 객체를 프로그래머가 control한다고 가정하면, 일어날 수 있는 상황을 생각해봅시다. 아주 짧은 프로그래밍 코드면 상관이 없겠지만, 코드가 길어지고 필요한 객체가 많아질 수록, 프로그래머는 기능 구현이 아닌, 객체 관리에도 큰 신경을 써야합니다.
하지만 IoC 컨테이너를 사용하게 되면, 프레임 워크가 자동으로 객체를 관리해주기 때문에, 프로그래머는 기능적인 것에 더욱 집중할 수 있게 됩니다.
IoC는 또한 내부적으로 기능을 구현하여 객체에 덧붙이는 형태인데, 이는 복잡한 로직을 직접 구현하지 않아도 되는 장점이 있습니다.
| @RestController@AllArgsConstructorpublic class DataController { DataBaseModel dataBaseModel; @GetMapping("/time") public ResponseEntity<String> getTime() { LocalDateTime now = LocalDateTime.now(); String formattedNow = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); return ResponseEntity.ok(formattedNow); }} |
|---|
이전글에서 사용한 코드를 가져왔습니다. @RestController는 자동으로 IoC 컨테이너에 해당 객체를 등록해주는데, 이렇게 되면 DataController를 호출하지 않아도 DataController가 제기능을 하게 끔 할 수 있습니다.
| package com.example.webserver;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class WebserverApplication { public static void main(String\[\] args) { SpringApplication.run(WebserverApplication.class, args); }} |
|---|
실제 프로젝트의 main 코드인데, DataController를 제가 직접 선언하지 않아도 작동이 정상적으로 되는 것을 확인했습니다.
| @GetMapping("/time") |
|---|
또한 @RestController로 객체를 등록하면 여러 기능들도 제공하는데, 그중에서도 강력한 기능 중하나는 @GetMapping입니다. 이는 Http Request 요청을 자동으로 Get과 대응시켜 지정 경로가 맞으면 원하는 로직을 처리하도록 합니다. (여기서는 url이 /time인 경우)
DI (Dependency Injection)란?
IoC가 되었다면, 이제 Dependecy Injection을 할 수 있는 상태가 됩니다. 이 의존성 주입은 객체를 다른 객체에 주입하는 개념입니다. 이는 객체들이 하는 것이 아닌 어셈블러에서 수행된다고 합니다.
전통적 접근 방법
| public class Store { private Item item; public Store() { item = new ItemImpl1(); }} |
|---|
전통적으로 Java에서 사용하는 코드입니다. 저도 처음 배울때는 이렇게 코드를 작성했던 기억이 있는데요. 하지만, 여기에는 여러가지 문제들이 있습니다.
1.. 높은 결합도 (Tight Coupling)
현재 Store 객체는 item객체와 높은 결합도를 유지하고 있습니다. 만약 새로운 itemImp2 객체를 Store에서 사용하고자 한다면, Store객체 역시 수정이 필요합니다.
| class ItemImpl2 implements Item { public void use() { System.out.println("Using ItemImpl2"); }}// Modified Store class to use ItemImpl2public class Store { private Item item; public Store() { // *item = new ItemImpl1(); ** * item = new ItemImpl2(); // Direct instantiation of ItemImpl2 } public void performTask() { item.use(); } public static void main(String\[\] args) { Store store = new Store(); store.performTask(); // Output: Using ItemImpl2 }} |
|---|
새로운 ItemImp2객체를 Store에서 사용하고 싶다면, Store내부의 로직도 변경해야합니다.
Dependency Injection을 사용
| public class Store { private Item item; public Store(Item item) { this.item = item; }} |
|---|
생성자에 객체를 이렇게 Injection 받아서 사용하는 DI는 어떨까요?
| // Item interfaceinterface Item { void use();}// Implementation of Itemclass ItemImpl1 implements Item { public void use() { System.out.println("Using ItemImpl1"); }}// Another implementation of Itemclass ItemImpl2 implements Item { public void use() { System.out.println("Using ItemImpl2"); }}// Store class with dependency injectionpublic class Store { private Item item; public Store(Item item) { this.item = item; // Dependency is injected } public void performTask() { item.use(); } public static void main(String\[\] args) { // Using ItemImpl1 Item item1 = new ItemImpl1(); Store store1 = new Store(item1); store1.performTask(); // Output: Using ItemImpl1 // Using ItemImpl2 Item item2 = new ItemImpl2(); Store store2 = new Store(item2); store2.performTask(); // Output: Using ItemImpl2 }} |
|---|
Store내부에서는 직접적으로 Item을 인스턴스화 하는 코드가 없기 때문에, 다른 Item 인터페이스 객체를 사용해도 Store코드를 직접적으로 수정하지 않아도 됩니다.
이는 객체간의 결합도를 낮출 뿐 아니라, Store 객체 테스팅에도 유리합니다. 이유는 Mock객체로 Test객체를 만들어 Store에 주입하기만 하면 되기 때문이죠.
Type of DI
Constructor-Based Dependency
| @Componentpublic class Item { }@Componentpublic class Store { private final Item item; public Store(Item item) { this.item = item; }} |
|---|
@Component로 선언된 Item객체를 Store객체의 Constructor를 통해 Injection 할 수 있습니다.
Constructor-Based Dependency
| import lombok.Setter;@Componentpublic class Store { @Setter(onMethod\_ = @Autowired) // Lombok will generate the setter method with @Autowired annotation private Item item; public void performTask() { item.use(); }} |
|---|
Lombok을 사용해 Item을 자동으로 Setter로 걸어 사용할 수 있습니다. 이는 Spring이 지원하는 @Autowired 기능 덕분입니다. 이런식으로 Setter를 사용하여 의존성을 주입할 수 도 있습니다.