Series
1. IoC(제어의 역전)/ DI(의존관계 주입) 용어 정리
2. DI (Dependency Injection, 의존관계 주입) 이란?
제어의 역전/ 의존관계 주입을 간단히 정리한 거에 이어, DI (의존관계 주입)가 스프링에서 쓰이는 이유, 특징에 대해 정리하는 시간.
스프링은 자바 언어 기반 프레임워크로 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 도구라고 할 수 있다.
이것을 가능하게 해 주는 객체 지향 프로그래밍의 핵심은 '다형성'에 있다. 다형성이 무엇인지 간단하게 정리해 보자.
(객체 지향 프로그래밍과 다형성에 대한 자세한 내용 다른 포스팅에서 정리하도록 하겠다.)
🔍 다형성
다형성은 이름 그대로 객체의 속성이나 기능(메소드)이 여러 가지 다른 형태로 가질 수 있는 것을 말한다.
코드상으로는 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할 수 있는 것.
간단한 실생활에서의 예시를 들어보자.
- 영화를 만들기 위해서는 '배역'이 있고, 이를 연기하는 '배우'가 있다.
- 배역이 변하지 않는이상 이를 연기하는 배우가 어떤 사람이든 간에 배역에 대한 내용, 즉 영화의 스토리에는 영향을 끼치지 않는다.
여기서 배역을 '역할'로 보고 배우를 '구현' 으로 바라보고 객체 지향 구조로 가져오는 것이 '다형성'이다. '역할'과 '구현'이 어떤 것인지 생각해 보고 다른 예시를 살펴보자.
- 컵은 보통 사람이 음료를 담아 그것을 마시기 위해 사용된다.
- 컵이 손잡이가 있는 '머그컵' 이거나 손잡이가 없는 '텀블러' 이거나 모양이 달라도 컵의 기능은 그대로다.
여기서는 컵의 기능은 '역할'이고 컵의 형태는 '구현' 이다.
이것을 자바 프로그래밍에서는 아래와 같이 적용한다.
- 역할 = 인터페이스
- 구현 = 인터페이스를 구현한 클래스, 구현 객체
👀 다형성을 활용한 예시
이제 이것을 활용한 코드로 예시를 들어보자.
결제서비스를 개발하는데 고정금액이 할인되는 '정액할인'과 총금액의 퍼센트가 할인되는 '정률할인' 방법이 있고, 어떤 달에는 정액할인, 어떤 달에는 정률할인을 적용해야 한다고 치자.
좋은 객체 지향 애플리케이션은 변경에 대해 영향을 받지 않아야 하기 때문에 변경되는 부분을 추상화시키고 추상화에 의존하게 해야 한다. (객체 지향 설계의 5가지 원칙 참고) 이를 설계해 본다면 아래와 같이 할 수 있다.
변하는 부분인 '할인정책'을 Interface로 추상화시킨 것인데, 앞서 말한 '역할'이 이와 같다고 볼 수 있다. 따라서, '할인'이라는 역할을 먼저 부여하고, '정액' 혹은 '정률'이라는 구현을 분리해 객체를 설계한다.
설계를 했으니 코드로 살펴보자.
public interface DiscountPolicy {
public int discount(...) {
}
}
public class FixDiscountPolicy implements DiscountPolicy {
...
@Override
public int discount(...) {
}
}
public class Order {
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
...
}
❗ DI(의존관계 주입)의 필요성
다형성을 활용한 위 예시는 OCP(개방-폐쇄 원칙), DIP(의존관계 역전 원칙)를 완벽히 준수했다고 볼 수 있을까?
얼핏 보면 맞는 것 같지만 우선 주문 클라이언트(Order)는 추상화(인터페이스)에 의존하면서 동시에 구현(구체 클래스)에도 의존하고 있다. (Order클래스를 보면 DiscountPolicy와 FixDiscountPolicy 모두 참조하고 있다.)
- 추상화 의존 : DiscountPolicy
- 구현(구체 클래스) : FixDiscountPolicy
이를 보면 추상화에만 의존해야 한다는 DIP(의 원칙을 위반하는 것임을 알 수 있다.
그리고 다른 할인정책으로 변경한다면, 결국 클라이언트 코드에 영향을 주기 때문에 OCP를 위반하게 된다.
public class Order {
//할인정책 변경 전
//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
//할인정책 변경 후, 클라이언트 코드를 수정 후 다시 컴파일 해야 한다.
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
...
}
위 코드와 같이 구체 클래스와도 의존관계가 생기기 때문에, 이를 없애고자 RateDiscountPolicy의 인스턴스를 생성하는 코드를 지우면 어떻게 될까?
public class Order {
//인터페이스에만 의존하도록 코드 변경
//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private final DiscountPolicy discountPolicy;
...
}
당연하지만 구현체가 없기 때문에, NPE(NullPointerException) 에러가 발생할 것이다. 결론은 클라이언트는 클래스와 의존관계를 가지지 않으면서 동시에 구현객체를 참조할 수 있어야 하기 때문에 다음과 같이 외부에서 받아야 한다.
public class Order {
private final DiscountPolicy discountPolicy;
public Order(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
...
}
위 구조를 보면 Order는 DiscountPolicy 인터페이스에만 의존하기 때문에 실제로 어떤 객체가 주입되는지 알 수 없다. 이는 실행 시점에야 실제 생성된 객체 인스턴스의 참조가 연결되어 알 수 있는데, 이를 동적인 객체 의존관계 혹은 런타임 의존관계를 가진다고 한다.
🔍 결국, DI란?
정리하자면, 위와 같은 방식이 의존관계 주입(DI)이며, 풀어쓰자면
애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을 말하며 스프링은 이를 가능하게 해 주는 컨테이너를 제공해 준다. 추가로 의존관계 주입은 생성자 주입, 필드주입, 수정자 주입, 메서드 주입이 있는데 Spring4 이후로는 생성자 주입을 권고하고 있다. 이에 대한 내용도 다른 포스팅에서 정리하도록 하겠다.
코드로 예를 들면 다음과 같다.
public class BeanFactory {
public void order() {
//객체 생성
DiscountPolicy discountPolicy = new RateDiscountPolicy();
//의존성 주입
Order order = new Order(discountPolicy);
}
}
스프링은 클래스를 탐색하고, 애플리케이션 실행 시점에 필요한 객체를 생성해 의존관계 설정까지 해 준다. 스프링이 DI 컨테이너 라고도 불리는 이유가 여기에 있다. 결과적으로 DI 컨테이너로 인해 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할 수 있게 된다. 이는 다형성의 본질이며 객체지향적인 구조를 가지게 해준다.
스프링 컨테이너가 생성하고 관리하는 객체들을 빈(Bean)이라고 부르며, 스프링 빈을 관리하고 조회하는 최상위 인터페이스인 BeanFactory와 이를 상속받고 기능을 확장시킨 ApplicationContext가 있다. 애플리케이션 개발에는 여러 기능이 필요하기 때문에 ApplicationContext를 사용하고 이를 보통 스프링 컨테이너라고 지칭한다. 스프링 컨테이너에 빈의 정보를 등록하기 위한 여러 방법을 스프링에서 제공한다.
추가로 생각해 볼 수 있는 것은 스프링으로 만든 애플리케이션에 수많은 요청이 동시에 올 텐데 그때마다 스프링 컨테이너에서 객체를 새로 생성하게 된다면 메모리가 낭비될 것이라는 점이다. 이런 점을 해결하고자 스프링에서는 객체를 딱 1개만 생성해 관리하는데, 자세한 내용은 다음 포스팅에서 정리하겠다.
✅ 정리
- 객체 지향의 핵심은 다형성
- 다형성은 역할과 구현을 분리해 이해
- 다형성은 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경하는 것.
-> 클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있다. - 하지만 다형성 만으로는 프로그래밍에서 객체지향원칙인 OCP, DIP를 지킬 수 없다. ( 인터페이스에 의존하는 동시에 구현 클래스에 의존하게 된다.)
- DI로 해결(스프링에서 DI가 쓰이는 이유) 클라이언트 코드의 변경 없이 기능 확장이 가능 ( 변경에는 닫혀 있고, 확장에는 열려있는 유연한 구조 )
- 스프링에서는 DI컨테이너를 제공함.
'Backend > Spring' 카테고리의 다른 글
@Autowired 빈 설정 방식 (+ @Qualifier, @Primary) (0) | 2023.12.22 |
---|---|
다양한 의존관계 주입 방법 (생성자 주입을 선택해야 하는 이유) (0) | 2023.12.18 |
스프링 빈 설정하는 방법 (XML, @Configuration, @Component) (1) | 2023.12.17 |
스프링의 싱글톤 (+ 싱글톤 레지스트리란?) (0) | 2023.12.11 |
IoC(제어의 역전)/ DI(의존관계 주입) 용어 정리 (0) | 2023.11.29 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!