Spring Core/02. Spring Container와 DI

02. Dependency와 Dependency Injection

  • -
반응형

이번 포스트에서는 스프링의 가장 큰 특징 중 하나인 DI(Dependency Injection)의 개념과 필요성에 대해 알아보자.

참고로 pom.xml에 설정했던 <dependency>와는 무관하다.

 생활에서의 의존성과 의존성 주입 그리고 Ioc Container

 

의존성

우리는 멀리 떨어져있는 사람들과 통화(business logic)를 위해 전화기를 사용한다. 통화를 위해서는 당연히 전화기를 가져야(has a) 한다. 이처럼 통화를 위해 반드시 전화기를 사용하는 것을 '전화기에 의존하고 있다'라고 하고 이때 '전화기는 우리의 의존성'이 된다.

의존성과 has a

의존성인 전화기를 갖기 위해서 전통적으로는 직접 구매를 했지만 요즘은 렌탈이나 구독 서비스가 등장하고 있다.

 

"아이폰13, 매달 4만원에 쓴다"…윤곽 드러난 애플의 구독서비스

"아이폰13, 매달 4만원에 쓴다"…윤곽 드러난 애플의 구독서비스, 올해 말에서 내년 중 출시 소비자가 싸게 임차해 사용 애플도 더 많은 수익 창출

www.hankyung.com

생각해보면 정수기, 비데 등 실 생활에서 사용하는 제품들을 구매하지 않고 렌탈해서 사용하는 경우가 많은데 왜 이런 렌탈 서비스가 유행하게 되었을까?

 

직접 구매하기와 렌탈 하기의 차이점

  구매하는 경우 렌탈하는 경우
 보유 과정
(has a)
핵심 사용 통화 한다.
상대적 장/단점 내꺼니까 내맘대로 사용 가능(ex: 당근하기) 내 맘대로 팔면 계약 위반, 총 비용은 좀 더 비쌀수도..
제품 관리를 직접 해야 함(가격도 비싸고, 신경쓸게 많음) 제품 관리를 일단 렌탈 업체에서 처리(유지보수 용이)
제품 교체시 보유 과정에서 겪었던 일들은 다시 반복
결론 통화(business logic)를 위해 보유한 전화기! 당근할 일은 거의 없지만 관리는 언제나 필요

그럼 제품에 대한 관리의 부담은 언제 발생 했을까?

바로 제품을 구매 했을 때 발생한다. 반대로 렌탈 업체가 공급해준 제품을 사용하면 그만큼 유지보수에 대한 부담이 줄어들고 렌탈업체에서 제공하는 서비스를 받을 수 있게 된다.

 

 

 

의존관계와 DI(의존 관계 주입)

 

의존 관계

빈의 컨테이너로써의 스프링은 빈의 생성, 관계 설정, 객체 관리 즉 빈의 라이프사이클을 담당한다. 이중 관계설정을 이해하기 위해 먼저 의존관계(dependency)라는 용어에 대한 이해가 필요하다. 

의존관계란 어떤 객체가 비지니스로직 처리를 위해 다른 객체에 의존하는 관계로 쉽게 말하면  객체 간의 has-a 관계를 말한다.

예를 들어 전화기(SPhone와 LPhone)와 이를 사용하는 사용자(PhoneUser) 그리고 테스트하는 클래스(PhoneTest)를 생각해 보자. PhoneUser는 SPhone를 사용하려면 아래와 같이 SPhone를 멤버 변수로 선언하고 사용할 것이다. 즉 has a 관계를 맺는다.  이때의 SPhone을 PhoneUser의 의존성이라고 하고 'PhoneUser는 SPhone에 의존하고 있다'고 한다.

PhoneUser는 SPhone을 멤버로 갖는다(has a).

public class PhoneUser {
    SPhone phone;               // Has a 관계 발생    
    public PhoneUser(){
        phone = new SPhone();   // 직접 객체 생성
    }    
    // phone 사용 코드
}

 

SPhone을 잘 사용하다가 실증이 나서 LPhone으로 변경되어야 한다면 PhoneUser에는 어떤 변화가 발생할까?

public class PhoneUser {
    LPhone phone;                // 의존 관계인 SPhone을 LPhone으로 변경   
    public PhoneUser(){
        phone = new LPhone();    // 생성되는 객체 변경 필요
    }
    // phone 사용 코드
}

멤버인 phone이 변경되면서 원래 SPhone을 사용하던 코드는 모두 LPhone을 사용하는 코드로 변경되야 한다. 프로젝트를 구성하고 있는 클래스들을 살펴보면 쉽사리 이런 의존성들을 발견할 수 있는데 의존성이 변경될 때마다 의존하는 객체가 변경되야 한다면 프로젝트 유지 보수에 심각한 문제를 야기할 수 있다. 결국 우리는 퇴근을 늦게 할 수밖에 없다. 


그럼 어떻게 해야 의존성 객체가 변경될 때 의존하는 객체의 변경을 없애서 프로젝트의 유지보수성을 향상하고 퇴근을 빨리 해볼까?

 

interface 사용하기

가장 쉽게 접근해 볼 수 있는 방식은 interface를 사용하는 것이다. 의존성인 LPhone과 SPhone에 인터페이스를 적용해서 PhoneUser와 loose coupling하도록 만들어주면 조금 숨통이 트일 것이다.

interface를 적용하면 PhoneUser는 아래와 같이 변경될 것이다. 

PhoneUser는 SPhone이 아니라 SmartPhone에 의존한다.

 

public class PhoneUser {
    SmartPhone phone;            // has a 대상을 interface로 변경   
    public PhoneUser(){
        phone = new SPhone();    // SPhone을 LPhone으로 바꾼다면?
    }
    // phone 사용 코드
}

실제 동작하는 객체는 SPhone이더라도 코드상에서 그것을 사용할 때는 상위 타입인 SmartPhone인 것이 중요하다.

하지만 다시 만약 구현체가 LPhone으로 변경된다면??

public class PhoneUser {
    SmartPhone phone;                
    public PhoneUser(){
        phone = new LPhone();     // 구현체가 변경되면 여전히 코드 수정 필요
    }
    // phone 사용 코드
}

PhoneUser는 여전히 SmartPhone타입으로 사용하기 때문에 수정해야 할 부분은 크게 줄어든다. 하지만 SPhone을 생성하는 코드를 LPhone 생성하는 코드로 변경해야 하는 일이 남는다. 

 

Factory Pattern의 적용

이럴 경우 Factory Pattern이라는 것을 적용할 수 있다. 말 그대로 객체를 공장에서 찍어내서 공급하는 형태이다.

다음은 Factory Pattern을 이용하는 예로 maker 정보에 따라 SPhone, LPhone이 반환된다. 이때 return type은 SmartPhone이다.

PhoneUser는 SmartPhone을 create 하지 않는다!

package com.doding.di.phone.config;

import com.doding.di.phone.LPhone;
import com.doding.di.phone.SPhone;
import com.doding.di.phone.SmartPhone;

public class PhoneFactory {
  public static SmartPhone getPhone(String maker) {
    if(maker.equalsIgnoreCase("s")){
      return new SPhone();
    }else{
      return new LPhone();
    }
  }
}

이를 사용하는 PhoneUser는 다음과 같다.

package com.doding.di.phone;

public class PhoneUser {
  private SmartPhone phone; // has a 관계

  public PhoneUser(SmartPhone phone) {    // 생성자를 통한 주입
    this.phone = phone;
  }

  public void setPhone(SmartPhone phone){ // settter를 통한 주입
    this.phone = phone;
  }
  public SmartPhone getPhone(){
    return this.phone;
  }

  public void usePhone() {
    System.out.printf("%s를 이용해 통화%n", phone);
  }
}

 

만약 lombok을 이용한다면 아래와 같이 작성할 수 있다.

package com.doding.di.phone;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class PhoneUser {
  private SmartPhone phone; // has a 관계

  public void usePhone() {
    System.out.printf("%s를 이용해 통화%n", phone);
  }
}

 

이제 PhoneUser는 의존성인 phone 타입의 객체를 생성자/setter를 통해서 주입 받고 있다.  결과적으로 PhoneUser는 SPhone 또는 LPhone의 구현체에서 완전히 독립함으로써 의존성 변경 시 코드 수정이 발생하지 않게 되었다. 만약 새로운 타입의 SmartPhone이 공급되더라도 수정되어야 할 것은 단지 설정을 담는 PhoneFactory이지 비지니스 로직을 가지는 PhoneUser는 영향을 받지 않는다.

하지만 독립을 쟁취하기 위해서는  시스템의 복잡도가 너무나 증가 했고 현재는 의존성 문제 해결 부분를 생각해서 Factory Pattern만 적용되었지만 추가로 빈 재사용성을 위해서 Singleton Pattern도 필요하고 빈 생성 시 선/후 관계 등 빈 라이프 사이클까지 생각하면 고민해야 할 내용이 상당히 많아진다. 비즈니스 로직을 구현에 집중해야할 개발자들가 이런 코드까지 작성하기는 부담스럽다.  

 

DI를 통한 의존성 주입

 

DI

드디어 궁극의 기술을 소개할 시점이 되었다. 스프링은 위와 같은 상황을 DI(Dependency Injection) 즉 의존성 주입이라는 것으로 처리한다.

그림을 보면 스프링은 SPhone이나 LPhone, PhoneUser 같은 객체를 빈이라는 개념으로 관리한다. 빈 객체의 생성은 스프링 컨테이너가 담당한다. 마치 제품을 미리 구매해 놓은 렌탈 회사와 같다. PhoneUser에서는 SmartPhone 타입의 멤버 변수 phone이 선언되어있다.  phone이 할당되는 시점은 스프링 컨테이너가 생성자 메서드를 호출하면서 SmartPhone 타입의 객체를 전달한 후이다. 객체관리는 프레임워크가 진행하기 때문이다.

이처럼 의존성인 SmartPhone을 PhoneUser에서 만들지 않고 외부에서 넣어주는 것을 의존성 주입 즉 DI라고 한다. 코드로는 별게 아니지만 이 일을 메타 설정에 의해 스프링 프레임워크가 해준다는 게 중요한 일이다.

결국 DI란 객체의 의존관계 즉 멤버 변수를 외부에서 설정해 주는 것으로 일반적으로 생성자 기반으로 처리하거나 setter 메서드를 이용해서 처리하게 된다.

반응형

'Spring Core > 02. Spring Container와 DI' 카테고리의 다른 글

06. 빈의 스코프  (0) 2024.02.22
05. Profile  (0) 2020.06.17
04. DI 처리 - 묵시적 DI  (0) 2020.06.17
03. DI 처리 - 명시적 DI  (0) 2020.06.16
01. Spring Application의 구성 요소  (0) 2020.06.15
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.