🚀 들어가기에 앞서,
의존성 주입(DI)을 코드를 건드리지 않는 마법~🪄 이라고 생각하지 말자.
위에서 길게 말했지만 중요한 건, 의존성 분리의 핵심 장점이란 어디를 고쳐야 할지 명확해지고, 코드 수정이 최소화된다는 것이다.
개발에는 유지보수가 중요하고, 그 유지보수를 위해 마법을 부리는 게 아니라 보다 편하게 유지보수를 할 수 있게 해주는 것임을 명심하도록.
의존성 주입(Dependency Injection, DI) 이란?
: 어떤 객체가 사용하는 의존 객체를 직접 만들어 사용하는 게 아니라, 외부에서 주입 받아 사용하는 방법
(즉, Java를 배우면서 알던 new 연산자를 이용해 객체를 생성하는 것이 아니라고 보면 된다.)
강한 결합
객체 내부에서 다른 객체를 생성하는 것은 강한 결합도를 가지는 구조이다. A 클래스 내부에서 B 라는 객체를 직접 생성하고 있다면, B 객체를 C 객체로 바꾸고 싶은 경우에 A 클래스도 수정해야 하는 방식이기 때문에 강한 결합이다.
느슨한 결합
객체를 주입 받는다는 것은 외부에서 생성된 객체를 인터페이스를 통해서 넘겨받는 것이다. 이렇게 하면 결합도를 낮출 수 있고, 런타임시에 의존관계가 결정되기 때문에 유연한 구조를 가진다.
의존성 예시 코드
@Service
public class ExampleService {
private ExampleRepository exampleRepository;
public ExampleService(ExampleRepository exampleRepository) {
this.exampleRepository = exampleRepository;
}
}
public class ExampleRepository {
// DI Test
}
위 코드에서 ExampleService 클래스가 만들어지기 위해서는 ExampleRepository 클래스를 필요로 한다.
이를 ExampleService 클래스는 ExampleRepository 클래스의 의존성을 가진다라고 한다.
위 코드같이 설계하면 코드의 재활용성이 떨어지고, 위 예제에서 ExampleRepository가 수정될 경우, ExampleService도 함께 수정해야 하는 문제가 발생한다.
즉, 결합도가 높아지게 되는 것이다.
추가로 위의 코드에서, ExampleRepository 클래스를 직접 new로 객체 주입을 하는 게 아니라 생성자로 주입을 받고 있다.
이 역시 객체의 의존성을 주입하는 방법 중 하나로 제어의 역전(Inversion of Control)이라고도 하나, 여전히 의존성을 직접 주입하여 직접 관리해야 하므로, 결합도가 남아있다.
➕ 제어의 역전(Inversion of Control, IoC)이란?
: 프로그램의 제어 흐름을 직접 제어하지 않고 외부에서 제어하는 것.
객체의 생성과 관리, 그리고 객체 간의 의존성 주입을 프레임워크나 컨테이너가 담당하도록 하는 방식이다.
- IoC와 DI의 차이?
: IoC는 보다 광범위한 설계 원칙으로, 제어 흐름의 역전을 통해 소프트웨어의 유연성을 높이는 것을 목표로 한다. DI는 IoC를 구현하는 실제 기술로, 객체 간의 의존성 관리를 보다 쉽게 하기 위해 사용한다.
public class Computer {
MouseA mouseA = new MouseA();
public void click() {
mouseA.click();
}
}
위 코드는 컴퓨터가 특정 마우스에 직접 접근해 두 객체 사이의 결합도가 크다.
만약 마우스A에서 마우스B로 변경하는 경우에는, 컴퓨터 클래스 코드도 싹 다 수정해야 한다.
이는 객체지향의 원칙 중 하나인 DIP를 위배하는 것이다.
그러므로 결합도를 낮추기 위해 Computer 클래스는 추상화된 Mouse 인터페이스에 접근해야한다.
public class Computer {
Mouse mouse = new MouseA();
public void click() {
mouseA.click();
}
}
인터페이스를 추가해 설계를 다시하여 메서드는 특정 마우스에게 의존하지 않게 되었지만 Mouse 구현체를 생성하는 코드에서 여전히 결합이 발생한다.
즉, 우리가 결합도를 낮추기 위해 해야할 일은
이 두가지를 지키면 객체지향 원칙 중 하나인 OCP를 지키며 기능을 확장할 수 있다.
그럼 팩토리 패턴(Factory Pattern)을 사용해 객체를 주입하는 권한을 팩토리 객체에 넘겨보자.
팩토리 패턴(Factory Pattern) 이란?
객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를 만들지는 서브 클래스에서 결정하게 만드는 패턴이다.
public class Computer {
Mouse mouse;
public Computer(MouseFactory mouseFactory){
mouse = mouseFactory.getMouse("A");
}
public void click(){
mouse.click();
}
}
public class MouseFactory{
public Mouse getMouse(String mouse){
if(mouse.equals("A")) return new MouseA();
else if(mouse.equals("B")) return new MouseB();
else return new MouseC();
}
}
Computer가 MouseFactory에게 어떤 객체를 사용할 것인지 명시만 해주면 MouseFactory가 Mouse 구현체를 대신 생성(new)하여 주입(Injection)한다. 그리고 Computer는 특정한 Mouse 구현체와 직접 연관되는 것이 없어진다.
🤔 A를 B로 바꾸려면 내가 코드 변경해줘야 하는데, 뭐가 다른 건데??
여전히 A를 B로 직접 변경해야 한다는 점이 있지만, 실제로는 이런 식으로 하드 코딩 하지 않음
- 환경 변수나 설정 파일(config) 에서 값을 받아오도록 만들면 코드 수정 없이 변경할 수 있음
//application.properties 파일 mouse.type = A
Computer는 여전히 MouseFactory 를 직접 생성해야 한다.
완전히 의존성을 제거하기 위해, 생성자를 통한 주입(Dependency Injection, DI) 을 적용하자.
public class Computer {
private Mouse mouse;
public Computer(Mouse mouse) { // 외부에서 주입
this.mouse = mouse;
}
public void click() {
mouse.click();
}
}
이렇게 하면 Computer는 Mouse가 무엇인지 전혀 신경 쓰지 않아도 된다.
🤔 여기서 '외부'란 무엇인가?
Computer 객체를 생성하는 곳을 의미한다 (likeMain
클래스)
- 즉, Computer는 Mouse의 타입에 대해 신경 쓰지 않고 단지 Mouse 객체만 외부에서 받으면 된다.
- Main이나 다른 외부 클래스에서 Mouse 객체를 쉽게 바꿀 수 있다는 뜻!
자, 그럼 이제 Spring 프레임워크가 무엇을 하는 것인지 알아보자.
Spring 프레임워크는 스프링 컨테이너에 객체(Bean)를 미리 생성해놓는다. 이것을 Bean이라고 부른다. 생성된 Bean은 다른 객체에 주입할 수 있다.
이것이 의존관계 주입이다.
public class Computer {
@Autowired // 의존관계 주입
@Qualifier("mouseA") // 명시
Mouse mouse;
public void click(){
mouse.click();
}
}
@Configuration
public class SpringConfig{
@Bean
Mouse mouseA(){
return new MouseA(); // MouseA Bean 생성
}
@Bean
Mouse mouseB(){
return new MouseB(); // MouseB Bean 생성
}
@Bean
Mouse mouseC(){
return new MouseC(); // MouseC Bean 생성
}
}
@Configuration
으로 선언된 설정클래스에 명시된 Bean 정보는 BeanDefinition 객체 변환되고 Spring은 BeanDefinition을 참조하여 Bean을 생성하고 스프링컨테이너에 등록한다.
Bean 생성이 완료 후, Spring은 @Autowired
가 선언된 변수에 맞는 타입을 가진 Bean을 스프링 컨테이너에서 찾아 주입한다. @Qualifier
로 명시된 Bean 이름이 있으면 찾아서 주입한다.
@Configuration
과 @Bean
을 사용하면 된다.@Qualifier
를 사용한다.➕ DIP, OCP란?
DIP(Dependency Inversion Principle)
: 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 된다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.OCP(Open/Close Principle)
: 자신의 확장에는 열려있어야 하고, 주변의 변화에는 닫혀있어야 한다.
@Autowired
private ArticleRepository articleRepository;
위와 같은 코드가 있다고 해보자. @Autowired
를 통해 articleRepository를 주입해주는 것이다.
근데 spring이 어떻게 내가 원하는 articleRepository를 찾아서 주입한다는 것일까?🤔
👉
Spring Boot에서 의존성 주입은 IoC 컨테이너(Inversion of Control Container)가 주도하며, 이는 설정과 주석(annotation)을 기반으로 개발자가 원하는 알맞은 객체를 자동으로 찾아서 주입한다.
📖 작동 원리
1. IoC 컨테이너
: IoC 컨테이너는 애플리케이션의 컴포넌트 객체를 생성, 구성, 관리한다. IoC는 제어의 역전을 의미하며, 객체의 생성과 의존성 관리를 개발자가 아닌 컨테이너가 담당하는 것이다.
2. 컴포넌트 스캔과 빈 생성
컴포넌트 스캔
: Spring Boot는 애플리케이션이 시작될 때, 특정 패키지 및 하위 패키지에서 @Component, @Service, @Repository, @Controller와 같은 주석이 달린 클래스를 스캔하여 IoC 컨테이너에 빈(Bean)으로 등록한다.
빈 생성 : 스캔된 클래스는 자동으로 빈으로 등록되고, IoC 컨테이너가 관리하는 객체가 된다.
3. 빈 주입
: 빈 주입은 @Autowired, @Inject, @Resource 주석을 통해 이루어진다. 컨테이너는 의존하는 클래스에 필요한 빈을 자동으로 주입한다.
@Autowired
: 주로 필드나 생성자, 메서드에서 의존성을 자동으로 주입하는 데 사용@Inject
: 자바 표준 주입 주석으로, Spring의 @Autowired와 비슷하게 동작@Resource
: 자바의 JSR-250 표준 주석으로, 특정 이름의 빈을 주입할 때 사용+) Spring Framework에서 @Autowired
를 사용해 의존성을 주입할 때, 타입 기반으로 의존성을 검색한다.
@Autowired
는 클래스 필드나 생성자, 메서드의 타입(클래스 또는 인터페이스)을 기준으로 IoC 컨테이너에 등록된 빈을 찾아 주입한다.
4. 빈 검색과 주입 전략
@Autowired
가 많이 사용)@Qualifier
나 @Resource
를 사용)@Primary
주석을 사용하여 기본으로 주입할 빈을 지정할 수 있다.@Qualifier
를 사용하여 명시적으로 지정할 수 있다.@Configuration
과 @Bean
을 사용하면 된다.🧷 [Spring] 여러 빈이 동일한 타입을 가질 때, 의존성 주입을 하는 방법
위 내용을 요약하면 '의존성 주입(DI) = 객체 간의 결합도를 낮추는 것' 이라고 할 수 있겠다.
그럼 객체 간의 결합도를 낮추는 것은 왜 중요할까?
1. 유지보수성 향상
: 결합도가 낮으면 코드 변경이 더 쉽고, 한 객체의 수정이 다른 부분에 미치는 영향을 최소화할 수 있다.
위에서도 말했듯, 객체가 서로 강한 결합을 하고 있으면 하나의 객체의 수정 때문에 관련된 다른 객체도 함께 수정해야 하는 상황이 발생한다.
이는 유지보수를 어렵게 하는 일이다.2. 재사용성 증가
: 특정 객체가 다른 객체를 강하게 의존하지 않으면 다양한 상황에서 독립적으로 사용할 수가 있다.
재사용 가능한 코드를 작성하면 개발 속도를 높이고 중복을 줄일 수 있다.3. 시스템의 유연성과 확장성 향상
: 새로운 기능을 추가하거나 기존 기능을 확장할 때, 기존 코드를 최소한으로 수정하면서 필요한 변경을 적용할 수 있다.
이는 시스템을 더 쉽게 확장하고, 새로운 요구사항에 빠르게 대응할 수 있게 한다.4. 코드 가독성 향상
: 객체 간의 관계가 단순해 코드의 구조를 명확하게 쉽게 파악할 수 있다.5. 테스트 용이
: 내부에서 주입하지 않고 외부에서 주입을 하면 본인이 원하는 주입을 외부에서 만들어 넣은 후에 테스트를 할 수 있다.
🚀 글의 초입에도 언급했듯이,
의존성 주입(DI)을 코드를 건드리지 않는 마법~🪄 이라고 생각하지 말자.
위에서 길게 말했지만 중요한 건, 의존성 분리의 핵심 장점이란 어디를 고쳐야 할지 명확해지고, 코드 수정이 최소화된다는 것이다.
개발에는 유지보수가 중요하고, 그 유지보수를 위해 마법을 부리는 게 아니라 보다 편하게 유지보수를 할 수 있게 해주는 것임을 명심하도록.
참고 사이트
https://devlog-wjdrbs96.tistory.com/165 - 의존성 주입
https://lordofkangs.tistory.com/633 - 의존성 주입을 하는 이유