본문 바로가기
Spring

[Spring] IoC/DI 제어의 역전/의존성 주입 (1)

by jaee_ 2021. 10. 2.
본 글은 📚 스프링 입문을 위한 자바 객체지향의 원리와 이해 를 읽고 정리한 내용입니다.

스프링 삼각형과 설계 정보

스프링을 이해하는 데에 POJO(Plain Old Java Object)를 기반으로 스프링 삼각형이라는 애칭을 가진 IoC/DI , AOP, PSA라고 하는 스프링의 3대 프로그래밍 모델에 대한 이해가 필수다. 오늘은  IoC/DI에 대해서 알아보자. 

 

프로그래밍에서 의존성이란?

IoC(Inversion of Control / 제어의 역전) 라고도 하는 DI(Dependency Injection / 의존성 주입)를 알아보기 전에 프로그래밍에서 의존성이란 무엇인지 알아보자.

 

의사코드 자바로 표현
운전자가 자동차를 생산한다. new Car();
자동차는 내부적으로 타이어를 생산한다. Car 객체 생성자에서 new Tire();

그리고 의존성을 단순하게 정의하면 다음과 같다.

의존성은 new다.
new를 실행하는 Car와 Tire 사이에서 Car가 Tire에 의존한다.

 

결론적으로 전체가 부분에 의존한다고 표현할 수 있다. 더 깊이 들어가면 의존하는 객체(전체)와 의존되는 객체(부분) 사이에 집합 관계 (Aggregation)와 구성 관계 (Composition)로 구분할 수도 있지만 지금은 그저 전체와 부분이라고 받아들이면 된다. 전체가 부분에 의존한다는 것과 "프로그래밍에서 의존 관계는 new로 표현된다!"를 기억하자. 

 

우선 스프링을 적용하지 않은 기존 방식으로 자바 코드를 작성한 후 점진적으로 스프링 Annotation 방식으로 변경해보자. 

 

클래스 다이어그램 : 의존 관계를 직접 해결
시퀀스 다이어그램 : 의존 관계를 직접 해결

// Tire 인터페이스
public interface Tire {
	String getBrand();
}
// Tire 인터페이스를 구현한 KoreaTire 클래스
public class KoreaTire implements Tire{

	@Override
	public String getBrand() {
		return "코리아 타이어";
	}

}
// Tire 인터페이스를 구현한 AmericaTire 클래스
public class AmericaTire implements Tire{

	@Override
	public String getBrand() {
		return "미국 타이어";
	}

}
// Tire를 생산할 Car 클래스
public class Car {
	Tire tire;
	
    // 객체를 생성하면 tire를 생산한다.
	public Car() {
		tire = new KoreaTire();
//		tire = new AmericaTire(); -- AmericaTire는 주석처리
	}
	
	public String getTireBrand() {
		return "장착된 타이어 : "+tire.getBrand();
	}
}
// 실행 클래스
public class Driver {
	public static void main(String[] args) {
		Car car = new Car();
		System.out.println(car.getTireBrand());
	}
}

// 결과
장착된 타이어 : 코리아 타이어

여기서 주의 깊게 볼 부분은 Car 클래스의 new KoreaTire() 부분이다. 바로 자동차가 타이어를 생산 (new)하는 부분, 즉 의존관계가 일어나고 있는 부분이다. 

new KoreaTire() - 타이어 생산

위의 코드를 정리하면 아래와 같다. 

  • 자동차는 타이어에 의존한다. ( Car 클래스의 new KoreaTire() 부분 )
  • 운전자는 자동차를 사용한다. ( Driver 클래스의 main 메서드의 new Car() 부분 )
  • 운전자가 자동차에 의존한다고 봐도 된다. ( Driver 클래스의 new Car() 부분 )
  • 자동차의 생성자 코드에서 tire 속성에 새로운 타이어를 생성해서 참조할 수 있게 해주었다. (Car 클래스의 Car생성자 메서드)

위에서는 의존이 일어나고 있는 두 객체 사이에 직접 의존성을 해결하는 코드를 작성해 봤다. 이제 의존성을 주입하는 코드를 작성해보자. 


스프링 없이 의존성 주입하기 1 - 생성자를 통한 의존성 주입

의사코드 자바로 표현 - 생성자 인자 이용
운전자가 타이어를 생산한다. Tire tire = new KoreaTire()
운전자가 자동차를 생산하면서 타이어를 장착한다 Car car = new Car(tire)
🔍 주입이란?
주입이란 말은 외부에서라는 뜻을 내포하고 있는 단어다.
결국 자동차 내부에서 타이어를 생산하는 것이 아니라 외부에서 생산된 타이어를 자동차에 장착하는 작업이 주입이다.

앞 에서 직접 의존성을 해결하는 경우의 시퀀스 다이어그램은 다음과 같았다. Car 객체가 Tire를 직접 생산하는, 즉 Tire에 대한 의존성을 자체적으로 해결하는 방식이었다. 

시퀀스 다이어그램 : 의존 관계를 직접 해결

이번에는 외부에서 생산된 tire객체를 Car 생성자의 인자로 주입(장착)하는 형태로 구현해보자. 시퀀스 다이어그램은 다음과 같다.

Tire 에 대한 의존성을 Car 생성자의 인자 주입으로 해결

 

Tire 에 대한 의존성을 Car 생성자의 인자 주입으로 해결

이제 Tire 에 대한 의존성을 Car 생성자의 인자 주입으로 해결하는 코드를 보자. 위의 코드와 동일한 Tire, KoreaTire, AmericaTire 클래스는 작성하지 않고 변경되는 Car, Driver 클래스만 새로 작성하겠다. 

// Car 클래스
public class Car {
	Tire tire;
	
	public Car(Tire tire) {	// 변경된 부분
		this.tire = tire;
	}
	
	public String getTireBrand() {
		return "장착된 타이어 : "+tire.getBrand();
	}
}
// Driver 클래스
public class Driver {
	public static void main(String[] args) {
		Tire tire = new KoreaTire();
		Car car = new Car(tire);
		
		System.out.println(car.getTireBrand());
	}
}

new 를 통해 타이어를 생산하는 부분이 Car클래스가 아닌 Driver로 변경했다. 그리고 생산된 tire 객체 참조 변수를 Car 생성자의 인자로 전달한다. 

 

이런 구현 방식에는 어떤 장점이 있을까? 기존 코드에선 Car가 구체적으로 KoreaTire를 생산할지 AmericaTire를 생산할지를 결정했었다. 그러한 코드는 유연성이 떨어진다고 한다. 

 

변경된 코드는 운전자가 차량을 생산할 때 어떤 타이어를 장착할지 고민하고 장착하며 자동차는 타이어를 장착할까 고민하지 않아도 된다.

 

그렇다면 어떤 이점이 있는 것일까?

 

기존 방식에서라면 Car는 KoreaTire, AmericaTire에 대해 정확히 알고 있어야만 그에 해당하는 객체를 생성할 수 있었다. 의존성 주입을 적용할 경우 Car는 그저 Tire 인터페이스를 구현한 어떤 객체가 들어오기만 하면 정상적으로 작동하게 된다. 의존성 주입을 하면 확장성도 좋아지는데, 나중에 ChinaTire, JapanTire 등등 어떤 새로운 타이어 브랜드가 생겨도 각 타이어 브랜드들이 Tire 인터페이스를 구현한다면 Car.java 커드를 변경할 필요 없이 사용할 수 있기 때문이다. (또한 다시 컴파일할 필요도 없다.)

 

만약 이를 제품화 한다면 Car.java, Tire.java를 하나의 모듈로, Driver.java와 KoreaTire.java, AmericaTire.java를 각각 하나의 모듈로 만들면 나중에 새로은 ChainaTire.java가 생겨도 Driver.java, ChinaTire.java만 컴파일해서 배포하면 된다. 다른 코드는 재컴파일 및 재배포할 필요가 없다. 즉, 코드 재컴파일과 재배포에 대한 부담을 덜 수 있다는 것이다. 이것은 인터페이스를 구현했기에 얻는 이점이라고 볼 수 있다. 

현실 세계의 표준 규격 준수 = 프로그래밍 세계의 인터페이스 구현
위의 글에서는 디자인 패턴의 전략 패턴을 응용하고 있다. 전략 패턴의 3요소인 클라이언트, 전략, 컨텍스트에 해당하는 요소를 찾아보자.

전략패턴의 3요소
 - 전략 메서드를 가진 전략 객체
 - 전략 객체를 사용하는 컨텍스트
 - 전략 객체를 생성해 컨텍스트에 주입하는 클라이언트 (제3자)
----------------------------------------------------------------------------  
전략 : Tire를 구현한 인터페이스
컨텍스트 : Car의 getTireBrand() 메서드
클라이언트 : Driver의 main() 메서드

앞에서 생성자를 통해 의존성을 주입해봤다. 이어서 속성을 통해 의존성을 주입해보자.

스프링 없이 의존성 주입하기 2 - 속성을 통한 의존성 주입

의사 코드 자바로 표현 - 속성 접근자 메서드 사용
운전자가 타이어를 생산한다. Tire tire = new KoreaTire();
운전자가 자동차를 생산한다. Car car = new Car();
운전자가 자동차에 타이어를 장착한다. car.setTire(tire);

이번에는 속성을 통해 의존성을 주입해보자. 생성자를 통해 의존성을 주입하는 것을 현실세계의 예로 들어 생각해보면 자동차를 생산(구입)할 때 한 번 타이어를 장착하면 더 이상 타이어를 교체 장착할 방법이 없다는 문제가 생긴다. 더 현실적인 방법은 운전자가 원할 때 Car의 Tire를 교체하는 것이다. 자바에서 이를 구현하려면 생성자가 아닌 속성을 통한 의존성 주입이 필요하다. 

생성자를 통해 의존성을 주입하면 객체를 생성할 때 생성자의 인자로 들어간 의존성은 더이상 변경이 안된다. 
즉, 생성자를 통해 객체가 이미 생성되어서 의존성을 변경하고 싶어도 변경할 수 없다는 이야기이다. 

속성 접근자를 이용해 의존 객체를 주입하는 경우의 시퀀스 다이어그램은 다음과 같다. 

Tire에 대한 의존성을 Car의 속성 주입으로 해결

자 이제 코드로 살펴보자. 이전에 사용했던 Tire, KoreaTire, AmericaTire는 코드가 동일하니 따로 작성하지 않겠다. Car, Driver만 작성해서 보도록 하자. 

// Car 클래스
public class Car {
	Tire tire;

	public Tire getTire() {
		return tire;
	}
	
	public void setTire(Tire tire) {
		this.tire = tire;
	}
	
	public String getTireBrand() {
		return "장착된 타이어 : "+tire.getBrand();
	}
}

Car클래스에 생성자는 없어졌고, tire 속성에 대한 접근자 및 설정자 메서드가 생겼다.

// Driver 클래스
public class Driver {
	public static void main(String[] args) {
		
		Tire tire = new KoreaTire();
		Car car = new Car();
		car.setTire(tire);
		
		System.out.println(car.getTireBrand());
	}
	
}

//결과
장착된 타이어 : 코리아 타이어

생성자를 통해 의존성을 주입하지 않고 속성에 대한 접근자 메서드를 만들어 접근제어자 메서드를 통해 의존성을 주입하였다.  

 

지금까진 스프링을 사용하지 않고 의존성을 주입하였다. 스프링을 이용해 의존성을 주입하는 방법은 다음 글에서 살펴보자. 

댓글