참조

토비의 스프링 vol1. 


DI

DI의 등장

IoC 컨테이너라던 스프링.

DI 라는 용어는 어쩌다가 나왔을까??

IoC 라는 용어는 “매우 폭넓게 적용되고 있는 용어” 이다. 스프링 고유의 것이 아니다.

 

객체 지향프로그래밍에서 IoC를 구현하는 기술에는
- service locator pattern을 사용
- DI를 사용
- contextualized lookup을 사용
- template method 디자인 패턴 사용
- 전략 디자인 패턴 사용
등등이 있다. 

따라서 스프링이 제공하는 IoC 방식을 좀 더 명시적으로, 확실하게 짚어주기 위해 “ DI(의존관계주입)” 이라는 용어가 등장한다. (이게 위에서 말한 DI 다 ) 

참고로, 주입이라는 용어를 사용했지만 “참조값”이 외부로부터 “전달” 되어오는 것을 말한다.

의존관계

의존관계에는 “방향성” 이 있다.

  • 의존 : B가 변하면, A에 영향을 미치는 경우
    • A는 B에 의존하고 있다 ( 방향성 : A —> B )
    • A 가 B를 사용하고 있는 경우.
    • 이 경우, B는 반대로 A에 의존하지 ❌ ( A 의 변화가 아무런 영향을 주지 않는다)

인터페이스를 통한 느슨한 의존관계

인터페이스에 대해서만 의존관계를 만든다면, “구현체와의 관계는 느슨해” 진다. ( 둘 사이에 “인터페이스 라는 추상화” 가 더해졌으니 )

결합도가 낮아진

변화에 영향을 덜 받는다.

  • 클래스 다이어그램에서, A가 B라는 인터페이스에 의존관계를 갖는다면, B의 구현체들(B1,B2..)과의 관계는 느슨해진다.

런타임 의존관계

인터페이스를 통한 의존관계를 갖는 경우, 런타임 시 어떤 구현체가 올 지 알 수 없다.

A는 어떤 구현체를 사용할지 미리 정해둘 수 없다 → A의 코드에 드러나지 않는다.

  • A와 B1(구현체)와의 관계는 “런타임 시에 맺게”된다.
    • B1 : 실제 사용대상인 객체 → 의존 객체.

 

🚀 의존관계 주입

구체적인 객체” - “사용하는 객체” 를 런타임시에 연결해 주는 작업

다음 조건들을 만족해야

  • 클래스 모델, 코드에는 “런타임시 의존관계” 가 드러나지 않는다.
    • 이를 위해서는, Interface에만 의존해야한다.
  • 런타임 시 의존관계를 결정하는 것 : 컨테이너, 팩토리 ( 제 3의 존재)
  • 사용할 객체에 대한 참조를, 외부에서 주입해줌으로서 → 의존관계 생성.

DI 컨테이너는 “자신이 결정한 의존관계”를 맺어줄 클래스의 “객체를 생성”하고 , 이 객체와 관계를 맺는( 이 객체를 사용하는) 측(A)의 메소드 파라미터로, 생성된 객체의 참조값을 전달한다.

이 모습이 마치, 컨테이너가, A에게 주입해주는 것과 같다고 하여, “의존관계 주입” 이라고 부른다.

DI와 IoC

DI는, 자신이 사용할 객체에 대한 선택과 제어권을 “외부로” 넘기고 , 자신은 수동적으로 주입받은 객체를 사용한다는 점에서 IoC 개념에 들어맞는다.

DI의 장점

  • 코드에는 런타임 클래스에 대한 의존관계가 나타나지 않는다.
  • 인터페이스를 통해 결합도가 낮은 코드를 만들어 → 다른 책임의 구현체로 바뀌어도 자신은 영향받지 않는다.
    • 변경을 통한 다양한 확장 가능. .
  • 자신의 책임 자체가 변경되는 경우 외에는, 불필요한 변화가 발생하지 않는다.

의존관계 검색

스프링이 제공하는 IoC 방법

  • 의존관계 주입
  • 의존관계 검색

스스로 검색하는 것 → 자신이 필요로 하는 의존객체를 능동적으로 찾는다.

  • 물론, “구현체”를 사용자 측에서 “결정한다는 말은 아님” ( 여전히 인터페이스를 사용함)
    • 런타임시 관계를 맺는 의존객체를 결정 및 생성하는 것은 → 외부컨테이너에게 IoC로 맡기고
    • 이를 “가져올 때”는 스스로 컨테이너에게 요청하는 것.

인터페이스를 사용한다고 했는데, 그럼 어떻게 의존객체를 가져올까??

  • getBean()메소드
    • Bean 이름을 통하거나
    • 타입(인터페이스) 으로 검색 할 수 있음.
      • 타입 검색도, 결국은 여기에서 인터페이스 타입으로 사용하는 객체를 검색하는 것이다보니, 여기(사용자)에서 구현체를 결정하는 것은 아니게 되는 거임. 
applicationContext.getBean("memberServiceImpl",MemberService.class);
  • 단점
    • 객체 팩토리 클래스(여기서는 applicationContext , 스프링 API ) 가 코드에 나타나버림.
  • 언제쓰나?
    • 의존관계 주입을 받을 수 없지만, 스프링 컨테이너에 담긴 객체를 사용하려는 경우.
  • 사용하는 객체는, 스프링 빈이어야하는가?
    • 의존관계 “검색” 의 경우, 검색하는 객체 자신은ㅇ, 스프링 빈일 필요가 없다. ( 스프링 빈을 검색해서 사용하려는 것 뿐 )
    • 의존관계 “주입”(DI)의 경우에는, A 와 B 사이에 DI가 적용되려면, A(사용자)도 반드시 스프링 컨테이너에서 생성 관리하는 “빈 객체” 여야 했다.

오해 말자

💥외부에서 , 파라미터로 객체를 넘겨줬다고(단순히 객체를 주입한다고), 다 DI가 아니다.

  • ❌ 파라미터가 이미 “특정 클래스 타입(Concrete class ) 으로 고정” 되어있다면, DI가 일어날 수 없다
public void takeMe(MyConcreteClass myClass){..}

public void takeMe(MyInterface myClass){...}
  • ⭕ DI에서 말하는 주입 : " 동적으로 구현클래스를 결정 해 제공 " 받을 수 있도록, " 인터페이스 타입 "의 파라미터를 통해 이뤄져야 한다.

의존관계 주입 응용

구현체를 바꿔서, 기능 구현체를 교환해가며 쓸 수 있다는 것은 알 테고..

부가기능추가

예를들어 , DAO가 얼마나 많은 DB를 연결해 사용하는지 파악하고 싶다면?

실제 Connection 을 얻어올 때 마다 Count를 증가할 수 있으면 좋겠다.

  • 모든 Dao에서 makeConnection 호출 할 때 마다 카운트 증가? → 그럼 분석 작업 끝나면 또 제거를 해야함.. 그리고 이렇게 되면, Dao 측에서, DB 연결횟수를 세는 책임까지 지게된다.

기존 모습

@Configuration
pubilc class DaoFactory{
	@Bean
	public UserDao userDao(){
			return new UserDao(connectionMaker());
	}
	@Bean
	public ProductDao prodeuctDao(){return new ProductDao(connectionMkaer());}

	@Bean
	public ConnectionMaker connectionMaker(){return new DConnectionMkaker();}
}
  • 별개의 ConnectionMaker를 만들자
    • 커넥션 개수를 카운팅 하는 기능이 추가된 ConnectionMaker라면 좋겠다 ( Dao 측의 코드에 변화가 없게 되기 때문)
    • ConnectionMaker에서는 “커넥션 개수를 카운팅” 하도록 하고
      • 실제 커넥션 연결을 생성해주는 ConnectionMaker를 주입받아오면(기존의 것을 사용가능 ) 좋겠다.
public class CountingConnectionMaker implements ConnectionMaker{int counter = 0;
	private ConnectionMaker realConnectionMkaer;
  public CountinConnectionMaker(ConnectionMaker realConnectionMaker){
     this.realConnectionMkaker = realConnectionMaker;
	}
	public Connection makeConnection() throws ..{
		this.count++;return realConnectionMaker.makeConnection();
	}
	public int getCounter(){ return this.counter;}

그러면 단지 설정정보 코드만 수정해주면, UserDao에서는 CountingConnectionMaker를 사용가능해짐

@Configuration
pubilc class DaoFactory{
	@Bean
	public UserDao userDao(){
			return new UserDao(connectionMaker());
	}
	@Bean
	public ProductDao prodeuctDao(){return new ProductDao(connectionMkaer());}

	@Bean
	public ConnectionMaker connectionMaker(){return new CountingConnectionMaker(realConnectionMaker());}
	@Bean 
	public ConnectionMaker realConnectionMaker(){return new DConnectionMkaker();}

}

의존관계 주입방법

기존 : 생성자 주입 방법

수정자메소드 (setter) 주입 방법

  • setter 메소드 : 주로, 객체내부 속성값 변경 용도
    • 부가적으로, 입력값에 대한 검증 등의 작업을 하기도 한다.

수정 자 메소드를 통해, 외부에서 제공받은 객체 참조값을 저장해두었다가, 내부 메소드에서 사용하게 할 수 있다.

일반 메소드 주입

세터 메소드는 “ 하나의 파라미터” 만 가진다는 제약이 존재.

여러개의 파라미터를 갖는 일반 메소드를 DI로 사용할 수도 있긴 함..

내가 배우기로는 생성자 주입을 자주사용하라 했었는데.. 왜 그럴까? ( 일단 , 세터같은 것은 열어두면 좋지 않다는 것 .. 정도만알고있음) 뒤에서 찾아보자 

XML을 이용한 설정

앞선 DaoFactory의 단점 ..이라고도 볼 수 있는 것

  • JAVA코드기반 설정정보다 보니 =⇒ DI 구성이 바뀔 때 마다, 자바코드를 수정 → 다시 컴파일

XML : 컴파일과 같은 별도의 빌드작업이 없다.

<beans>
	<bean id = "myConnectionMaker" class ="springbook.user.dao.DconnectionMaker"/>
	<beean id ="userDao" class = "springbook.user.dao.UserDao">
		<property name = "connectionMaker" ref ="myConnectionMaker" />
	</bean>
</beans>
  • 빈 이름 : @Bean 메소드의 이름 (getBean에서 사용되는 이름)
  • 빈 클래스 : 빈 객체 생성시 사용 클래스
  • 빈 의존 객체 : 생성자 또는 수정자 통해 넣어주는 의존객체. ( 없으면 생략 가능 )
@Bean --> <bean
public ConnectionMaker 
connectionMaker(){   ---> id = "connectionMaker"    
	return new DconnectionMaker();  --> class ="springbook...DConnectionMaker"
}

Xml 이용 시에는, 수정자 메소드를 사용해 의존관계를 주입하는 것이 편리하다.

userDao.setConnectionMaker(connectionMaker());

<bean id = "userDao" class = "springbook.user.dao.UserDao">
	<property name = "connectionMaker" ref ="connectionMaker"/>
</bean>

궁금 : 요즘 XML을 이용한 설정은 잘 하지 않는 듯 한데, 왜 그럴까?? ❓❓저정리

설정정보에, 프로퍼티 값의 주입

설정정보에는 다른 빈이나 객체 뿐만 아니라, String같은 단순한 값을 넣어줄 수도 있다.

참조값이 아닌, 단순 정보도 객체 초기화 과정에서 넣어줄 수 있다는 것. 이 때는, DI처럼 객체의 구현클래스를 동적으로 바꾸기 위한 목적은 당연히 아니겠다.

@Bean 
public DataSource dataSource(){
	SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
	dataSource.setDriverClass(com.mysql.jdbc.Driver.class);
	///

xml에서는 Value값의 자동변환이 있음

<bean id ="dataSource"
	class ="org.springframework.jdbc.datasource.SimpleDriverDataSource"/>
	<property name="driverClass" value = "com.mysql.jdbc.Driver"/>
	<property name="url" value = "jdbc:mysql://localhost/springbook"/>
<property name="driverClass" value = "com.mysql.jdbc.Driver"/>

driverClass 는 문자열 타입이 아니라 java.lang.Class 타입이다. 그런데 XML에서는 텍스트 형태로 value에 들어가 있다.

분명

Class driverClass = "com.mysql.jdbc.Driver" ; // 컴파일 에러가 날텐데?

스프링은, 프로퍼티 값을, setter 메소드의 파라미터 타입을 참조하여, 적절한 형태로 변환해준다.

setDriverClass() 메소드의 파라미터 타입이 Class임을 확인하고 "com.mysql.jdbc.Driver" 를 com.mysql.jdbc.Driver.class 객체로 자동 변경해주는 것.

Class driverClass = Class.forName("com.mysql.jdbc.Drive");
dataSource.setDriverClass(driverClass);
  • 스프링은 value에 지정한 텍스트 값을 적절한 타입으로 변환해준다.

💥 하지만, Spring에서는 생성자 주입을 사용하라고들 한다. 

https://reflectoring.io/constructor-injection/

 

Why You Should Use Constructor Injection in Spring

Dependency injection is a common approach to implement loose coupling among the classes in an application. There are different ways of injecting dependencies and this article explains why constructor injection should be the preferred way.

reflectoring.io

책에서는 xml을 사용할 때라서 그런지는 모르겠지만, setter 주입에 대한 언급이 많았다. 

하지만 스프링에서는 생성자 주입을 사용하라고들 말한다. 

 

앞서 봤듯이  Dependency Injection은, 클래스들 사이의 느슨한 결합을 구현하기 위한 것이었다. ( 객체 사이에는 단방향 의 관계가 생기곤 한다. A -> B : A가 B를 사용하는 것. A는 B에 대한 의존성을 갖는다  )( 따라서, 주입 을 통해, 객체에게 필요한 의존성을 제공하는 것 이다 ) 

의존성을 주입하는 데에는 다양한 방법들이 있는데 그 중에서도 왜 생성자 주입을 하라고들 하는 걸까? 

 

- 앞에서 말했지만, DI는 IoC 를 구현하는 기술 중 하나다.

(

 외부에서 의존성 객체를 주입한다 - 외부에서 객체를 결정하고 생성하여, A 객체에게 주입해줌으로서 관계를 맺어준다.

 A에서는 이에 대해서 알지 못한다.

 즉, 객체를 생성하고 의존성을 주입하는 역할이 해당 객체를 사용하는 객체 자신이 아닌, 프레임워크에게 주어진 것이 다.

 따라서 Inversion of Control 이 일어난 것 이다

생성자 주입

참고로 Spring 4.3 이전에는, 생성자에 @Autowired 를 써줘야지만, 생성자 주입이 됐다. 

그 이후 버전에서는, 생성자가 하나만 존재하는 경우에는, @Autowired 를 쓰는 것은 선택이 되었음. 

세터 메소드 주입

이 세터 메소드에는 @Autowired 어노테이션을 꼭 붙여줘야 한다. 

그 프로퍼티에 대한 세터메소드의 argument로서 객체가 주입된다. 

 

세터 주입과 , 필드 주입 모두 사용한다면

 스프링에서는, 세터 주입 메소드를 사용하여 의존성 주입을 한다. 

- 고로, 하나의 클래스에 대해 두 가지 타입의 주입을 하는 것은 매우 매우 매우 안좋은 습관임. ( 가독성이 쭉쭉 떨어지기 때문이다. )

 

 

🤔아무튼.. 왜 생성자 주입을 하라는 걸까?

1. 필요한 모든 의존성들은 initialized time에 사용가능해야한다. 

우리는 생성자를 호출함으로서 객체를 생성한다. 

필요한 모든 디펜던시들을 생성자의 파라미터를 통해 받아오도록 하고 있다고 가정해보자.

  • 우리는  모든 디펜던시들이 주입되지 않는다면 이 객체는 인스턴스화 되지 않을 것임을 100프로 확신할 수 있게 된다. 

IoC 컨테이너는, 생성자에게 제공되어야 하는 모든 아규먼트들이 "생성자에 전달되기 전에 생성되어 사용가능" 해지게끔 해 준다(❓❓❓) . 덕분에 NullPointerException 을 예방할 수 있는 것이다. 

 

이로 인해서, 생성자 주입이 유용한 이유가 또 추가되는데

  • 어떤 비즈니스 로직을 작성할 때 마다 " 필요한 디펜던시가 로드 되었는지(null은 아닌지) 체크할 필요가 없어진다" 는 것 ( 생성자 호출 시에, 로드되지 않았다면, 이 객체 자체가 생성되지를 않을 테니까..) 

 

 

🤔2. 생성자 주입을 통해 냄새나는 코드를 알아챌 수가 있음

생성자 주입을 통해 이 bean이 너무 많은 다른 객체들에 의존하고 있지는 않은지 알아차릴 수가 있다. 

생성자가 매우 많은 arugment를 갖고 있다는 것 == 현재 클래스가 매우 많은 책임(역할)을 가지고 있을 수도 있다는 것을 의미한다. 

 

이 경우, 우리는 관심사의 분리를 통해 코드를 리팩토링할 수 있겠지. 

 

🤔3. 테스트에서 발생하는 에러를 방지해줌

생성자 주입은, 유닛 테스트 작성을 간소화(?) 시켜준다.

무슨 말이냐면,  A 객체가 디펜던시를 세터 주입을 받도록 작성되어있다고 가정해보자.

이 경우, 테스트 코드에서 세터 메소드를 통해 디펜던시를 주입하는 것을 까먹으면 NullPointerExcpetion이 일어날 것임. 

 

하지만 생성자 주입으로 모든 디펜던시들을 받도록 해 놓았다면, 생성자를 통해 모든 디펜던시들에 대한 유효한 객체들을 제공하게끔.. 정확히는 잊을 수가 없게끔 해 준다. 그러면 우리는 모킹 라이브러리 같은 것들을 사용해 목 객체를 생성하여 생성자에 전달해주면된다. 

즉, 생성자 주입은 모든 디펜던시들이 사용가능할 때에만 테스트 케이스가 실행될 것을 확실하게 해주는 것이다. 

 

🤔4. 불변성(Immutability)

생성자 주입은, 불변 객체를 생성하는데 도움이 된다. 왜냐하면, 객체를 생성하는 유일한 방법이 그 생성자 시그니쳐 뿐이기 때문이다. 

따라서 일단 빈이 생성되고 나면, 우리는 더이상 디펜던시를 변경할 수가 없다. 

 

반면, 세터 주입의 경우에는, 객체 생성 이후에도 디펜던시를 주입하는 것이 가능하다. 이는 이 클래스로 만들어지는 객체들은 가변객체임을 의미하고, 멀티 스레드 환경에서는 스레드 세이프 하지 않게 되고, 가변성으로 인해 디버그 하기가 어려워진다. 

 

 


정리

  • 책임이 다른 코드를 분리 → 별개의 클래스로
  • 전략에 따라 바뀔 수 있는 클래스 → 인터페이스화 && 다른클래스에서는, 인터페이스를 통해서만 접근하도록 함.
    • 전략이 바뀌어, 구현 클래스가 바뀌더라도, 사용 클래스의 코드는 수정할 필요없다( 전략 패턴)
  • 자신의 책임 자체가 변경되는 경우 외에는, 불필요한 변화가 발생 x
    • 자신이 사용하는 객체의 기능은 자유롭게 확장, 변경 가능
  • 한쪽의 기능 변화가 다른쪽 변화를 요구 x ( 낮은 결합도 )
  • 자신의 책임과 관심사에만 순수하게 집중( 높은 응집도)
  • 객체 생성, 다른 객체와 관계맺는 작업의 제어권은 별도의 객체 팩토리에게로 넘겼다. 객체 팩토리의 기능을 일반화한 IoC 컨테이너가 등장.
    • 객체가 자신이 사용할 대상의 생성, 선택에 관한 책임을 지지 않게 되었음. ( 제어의 역전)
  • 전통적인 싱글톤 패턴의 문제점이 있었음 → 스프링은 스프링만의 방법으로, 스프링 빈들을 싱글톤으로 관리하는 싱글톤 레지스트리 역할을 한다.
  • 설계시점에는, 클래스 - 인터페이스 사이의 느슨한 의존관계만 만들고,
    • 런타임시 실제 사용할 구체적 의존 객체는 제3자의 도움으로 주입받아, 동적 의존관계를 갖게 해 주는 “IoC의 특별한 케이스 “ ( 의존관계주입)
  • 의존관계주입 방법 (생성자주입,setter주입,일반메소드주입)
  • XML을 이용해 DI설정저옵 만들기.
    • 의존객체가 아닌 일반 값을 외부에서 설정해 런타임시 주입하기

스프링은 객체가 어떻게 설계되고, 생성되고, 관계를 맺어지는지에 관심을 가질 뿐

객체를 설계,분리,개선,어떤 의존관계를 갖게 할지 결정하는 것은 개발자의 책임이다.

  • 객체지향 설계는 개발자의 몫이고, 이런 좋은설계와 코드를 적용하는데에 스프링이 좋은 동반자가 되어주는 것.

' > 토비의스프링' 카테고리의 다른 글

ch02 : 테스트의 필요성  (0) 2022.02.20
02. 원칙과 패턴  (0) 2022.02.13
관심사의 분리 ( ~1.3.3)  (0) 2022.02.09
복사했습니다!