[Spring] IoC와 DI에 대한 이해

DYKO·2022년 9월 7일
0

Spring Framework

목록 보기
3/7
post-thumbnail

Spring의 기반, 스프링 삼각형

시리즈 첫 번째 게시글인 "Spring의 등장"편에서 Spring의 핵심은 POJO 프로그래밍이라고 설명하며 이를 위해 Spring에서 제공하고 있는 주요기술 3가지를 스프링 삼각형을 통해 가볍게 소개했다. 이번 포스팅부터 스프링 프레임워크를 이해하기 위해 3대 프로그래밍 모델, IoC/DI, AOP, PSA에 대해 차례로 다룰 예정이다.


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

의존성(Dependency)란 두 모듈간의 연결, 객체지향언어에서는 두 클래스 간의 관계라고도 한다. 의존관계는 항상 방향성이 있어 두 객체간 의존관계가 있을 때, A ---> B 와 같이 점선 화살표로 표현하며 'A가 B에 의존한다.' 라고 말한다. 더 깊이 나누면 의존하는 객체(전체)와 의존되는 객체(부분) 사이의 집합 관계(Aggregation)와 구성 관계(Composition)로 구분할 수도 있다.

package repairShop

public class Car {
	private KoreaTire _tire;
	public Car () { _tire = new Tire(); }
}

public class KoreaTire {
	public String getBrand(){
    	return "Korea Tire";
    }
}

위 코드로 예를 들면 Car 라는 객체는 생성자에서 KoreaTire 객체를 직접 생성하게 된다. 즉, Car 객체를 사용하기 위해서는 KoreaTire 객체가 반드시 정의되어 있어야 한다. 이 때, Car 객체는 KoreaTire 객체에 의존한다고 표현할 수 있으며 두 객체간 직접적인 연관 관계가 발생하며 강한 결합(Tight Coupling)이라고 한다. 그리고 객체간의 결합도가 높을수록 추후 유지보수가 힘들기 때문에 객체에 대한 제어권을 이전함으로써 결합도를 낮추는 것이 바로 제어의 역전(IoC: Inversion of Control) 이다.

💡 IoC와 DI의 정의

제어의 역전(IoC: Inversion of Control)이란 90년 중반에 GoF의 디자인패턴에서도 언급됐던 용어로, 객체 내부에서 직접 필요한 클래스의 객체를 생성하지 않고 다른 객체에 위임함으로써 객체에 대한 제어권, 즉 프로그램의 흐름을 제3자에게 넘기는 것을 IoC 모델이라고 한다. Spring 뿐만 아니라 모든 객체지향 프로그램에서는 IoC 모델을 적용해 객체를 올바르게 캡슐화하고 이를 통해 높은 응집도와 낮은 결합도를 갖춰 변경에 유연한 프로그래밍을 지향한다.

그렇다면 의존성 주입(DI: Dependency Injection)이란 무엇일까? IoC는 Spring이 등장하기 이전부터 쓰인 용어이고, 범용적으로 사용되어 스프링을 IoC 컨테이너라고 하기엔 스프링이 제공하는 기능의 특징을 명확하게 설명할 수 없다. 그래서 좀 더 스프링이 제공하는 IoC 방식을 명확하게 설명하기 위해 만든 새로운 용어가 의존성 주입(DI)이다.

스프링을 IoC 컨테이너라고만 해서는 스프링이 제공하는 기능의 특징을 명확하게 설명하지 못한다. 스프링이 서블릿 컨테이너 처럼 서버에서 동작하는 서비스 컨테이너라는 뜻인지, 아니면 단순히 IoC 개념이 적용된 템플릿 메소드 패턴을 이용해 만들어진 프레임워크인지, 아니면 또 다른 IoC 특징을 지닌 기술이라는 것인지 파악하기 힘들다. 그래서 새로운 용어를 만드는 데 탁월한 재능이 있는 몇몇 사람의 제안으로 스프링이 제공하는 IoC 방식을 핵심을 짚어주는 의존관계 주입이라는, 좀 더 의도가 명확히 드러나는 이름을 사용하기 시작했다.

- 토비의 스프링 3.1

좀 더 명확하게 정리하자면 의존성 주입(DI)이란 아래와 같은 세 가지 조건을 충족하는 작업이라고 할 수 있다.

  • 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않음. 그러기 위해서는 인터페이스에만 의존해야 함
  • 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정
  • 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입)해줌으로써 생성됨

💡 스프링을 활용하지 않은 의존성 주입 방법

앞서 예로 들었던 Car 객체의 경우, KoreaTire 객체와 강한 결합도를 갖도록 코딩되었다. 해당 코드에 대해 IoC와 DI를 적용시켜보며 코드를 통해 IoC/DI에 대해 이해해보자.

먼저, Car 객체에서 사용할 Tire 에 대해 인터페이스로 선언한다. 그리고 Tire 인터페이스를 구현한 KoreaTireAmericaTire를 작성한다. 이와 같이 객체를 인터페이스로 선언함으로써 Car 객체가 사용하는 Tire 객체의 브랜드가 변경되어도 Car 클래스 내부의 코드를 수정할 필요 없이 _tire 변수에 선언하는 클래스만 갈아끼워주면 된다. 이후, 새로운 브랜드가 생겼을 때도 Tire 인터페이스를 구현한다면 Car 클래스를 수정할 필요가 없어 확장성도 보장된다.

package repairShop

public interface Tire {
	String getBrand();
}
package repairShop

public class KoreaTire implements Tire {
	public String getBrand() { 
		return "Korea Tire";	
    }
}
package repairShop

public class AmericaTire implements Tire {
	public String getBrand() { 
		return "America Tire";	
    }
}
package repairShop

public class Car {
	private Tire _tire;
    
    ``` 의존성 주입 코드 작성 ```
}

위와 같은 객체가 작성되었을 때, 이제 Car 객체를 사용하기 위해 해당 객체를 사용하는 클래스에서 Tire에 대한 의존성을 주입해야 하는데, 스프링 프레임워크를 활용하지 않고 하는 방법은 생성자속성(setter)를 이용하는 방법이 있다.

생성자를 통한 의존성 주입

Car 객체에서 Tire 객체의 의존성을 주입을 위해 생성자의 인자로 Tire 객체를 넘겨 받아 _tire에 주입하도록 구현한다. 그리고 Car 객체와 Tire 객체를 제어하는 Engineer 클래스를 아래와 같이 구현하여 Car 객체에서 어떤 브랜드의 Tire를 사용할지 결정하도록 한다.

package repairShop

public class Car {
	private Tire _tire;
    
    public Car (Tire tire) {
    	this._tire = tire;
    }
    
    public String getBrand() {
    	return _tire.getBrand();
    }
}
package repairShop

public class Engineer{
	public static void main(String[] args){
    	Tire tire = new KoreaTire();
        //Tire tire = new AmericaTire();
        
        Car car = new Car(tire);
        System.out.println(car.getBrand());
    }
}
결과: Korea Tire

속성을 통한 의존성 주입

속성을 통한 의존성 주입은 Engineer에서 생성된 Tire 객체를 생성자의 인자가 아니라 속성 접근자 메서드(setter)의 인자로 받아 _tire 변수에 넣어줌으로써 의존관계를 생성하는 방식이다. Car 객체에서 setter를 통해 의존성을 주입받도록 하기 위해 Car 클래스와 Engineer 클래스를 아래와 같이 수정하였다.

package repairShop

public class Car {
	private Tire _tire;
    
    public Tire getTire () {
    	return _tire; 
    }
    public void setTire (Tire tire) {
    	this._tire = tire;
    }
    public String getBrand() {
    	return _tire.getBrand();
    }
}
package repairShop

public class Engineer{
	public static void main(String[] args){
    	Tire tire = new KoreaTire();
        Car car = new Car();
        car.setTire(tire);
        
        System.out.println(car.getBrand());
        
        tire = new AmericaTire();
       	car.setTire(tire);
        
        System.out.println(car.getBrand());
    }
}
결과: Korea Tire
	 AmericaTire

첫 번째 Car 클래스처럼 생성자를 통해 의존성을 주입하게 되면, 객체를 초기화할 때 외에는 더 이상 Tire 객체를 교체할 수 없다는 문제가 생긴다. 즉, Car 객체를 생성한 이후에도 Tire 객체를 수정하기 위해서는 위와 같이 속성 접근자 메서드(setter)를 활용하여 의존성 주입을 하도록 구현하여야 한다. 스프링에서 @어노테이션을 사용하는 경우 주로 해당 방식을 사용한다.


💡 스프링 프레임워크를 활용한 의존성 주입

XML 파일 사용

## repairShop.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
   <bean id="tire" class="repairShop.KoreaTire"></bean>
   <bean id="americaTire" class="repairShop.AmericaTire"></bean>
   <bean id="car" class="repairShop.Car"></bean>
<beans>

스프링에서는 ApplicationContext 를 통해 코드에서 XML 파일에 저장한 Bean에 대한 정보를 불러옴으로써 객체를 지정할 수 있다. 해당 XML파일을 스프링 빈 설정 파일이라고 하며, 위의 repairShop.xml과 같이 bean 태그를 이용해 id 속성과 인스턴스화 할 클래스를 지정하는 class 속성을 등록할 수 있다.

repairShop.xml 에서 등록한 Bean 정보를 활용해 객체를 생성할 때는 ApplicationContextgetBean() 메소드에 등록한 id를 인자로 넘겨주면, 해당 id로 등록된 bean의 class 속성에 지정된 클래스의 인스턴스를 생성하여 넘겨준다. 앞서 작성했던 Engineer 클래스에서 XML 파일을 이용하여 의존성 주입을 하려면 아래와 같이 코드를 수정하면 된다.

// Engineer.java
package repairShop;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Engineer{
   public static void main(String[] args){
      AppicationContext context = new ClassPathXmlApplicationContext("repairShop/repairShop.xml");

      Tire tire = (Tire)context.getBean("tire");
      Car car = (Car)context.getBean("car");
      car.setTire(tire);

      System.out.println(car.getTireBrand());
   }
}
결과: Korea Tire

스프링을 통해 위와 같이 XML 파일로 의존성 주입을 하게 됐을 때 얻을 수 있는 이점은 코드를 수정하지 않고 Tire 객체의 브랜드를 수정할 수 있다는 점이다. 만약 KoreaTireAmericaTire로 수정하고 싶다면, repairShop.xml 파일의 tireamericaTire bean을 다음과 같이 수정하기만 하면된다.

## repairShop.xml
   ```중략```
   <bean id="koreaTire" class="repairShop.KoreaTire"></bean>
   <bean id="tire" class="repairShop.AmericaTire"></bean>

즉, 프로그램 코드의 수정/재컴파일/재배포를 거치지 않아도 XML 파일을 수정함으로써 참조하는 객체를 변경할 수 있다. 이는 이미 배포된 환경에서 변경 사항이 생겼을 때, 그 무엇보다도 큰 이점이라고 할 수 있다. (운영자 입장에서 코드가 수정되면, 검증 -> 빌드 -> 배포 -> 검증 단계를 거쳐야 하기 때문에 무척이나 귀찮다...)


스프링 설정 파일(XML)에서 속성 주입

스프링에서는 스프링 빈 설정 파일을 통해 인스턴스화할 객체의 클래스를 지정하는 방법을 위에서 설명했는데, 더 나아가 XML 파일에서 직접 Car 클래스의 속성을 아래와 같이 property태그를 통해 지정할 수 있다. id="tire"인 bean에 직접 KoreaTire나 AmericaTire를 지정해준 방식과 달리 car bean의 tire 속성은 koreaTire bean을 참조한다고 지정해주는 것이다. 이런 방식은 직접 bean의 id를 계속 바꿔주는 것보다 클래스에 대해 각각의 bean으로 등록하고 특정 bean의 ref 속성만 수정해주면 되니 좀 더 유지보수하기 쉽다.

## repairShop.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
   
   <bean id="koreaTire" class="repairShop.KoreaTire"></bean>
   <bean id="americaTire" class="repairShop.AmericaTire"></bean>
   
   <bean id="car" class="repairShop.Car">
      <property name="tire" ref="koreaTire"></property>
   </bean>
<beans>

그리고 위와 같이 XML 파일에 직접 속성을 지정해주면, car bean을 객체를 생성하며 tire속성에 koreaTire bean을 자동적으로 결합시켜주기 때문에, 아래와 같이 코드 상에서 Car 객체의 속성 접근자(setter)를 호출할 필요가 없다.

// Engineer.java
package repairShop;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Engineer{
   public static void main(String[] args){
      AppicationContext context = new ClassPathXmlApplicationContext("repairShop/repairShop.xml");

      Car car = (Car)context.getBean("car");
      // Tire tire = (Tire)context.getBean("tire");
      // car.setTire(tire);
      System.out.println(car.getTireBrand());
   }
}
결과: Korea Tire

@Autowired를 통한 속성 주입

스프링에서 제공하고 있는 @Autowired 어노테이션은 설정자 메소드를 이용하지 않고도 클래스의 속성을 주입할 수 있다. 클래스를 구현할 때, 속성에 @Autowired 어노테이션을 붙여주면 스프링 프레임워크가 XML 설정 파일을 통해 설정자 메소드 대신 속성을 주입해 주는데, 이를 사용하기 위해서는 스프링 설정 파일을 다음과 같이 수정해야 한다.

## repairShop.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:context="http://www.springframework.org/schema/context"
   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">

   <context:annotation-config/>
   
   <bean id="tire" class="repairShop.KoreaTire"></bean>
   <bean id="americaTire" class="repairShop.AmericaTire"></bean>   
   <bean id="car" class="repairShop.Car"></bean>
<beans>

설정 파일에 context관련 spring config와 context:annotation-config 태그를 추가하여 @Autowired 어노테이션을 사용할 수 있도록 설정하고, 기존의 Car 클래스의 tire 속성에 다음과 같이 어노테이션을 붙여 의존관계를 주입하게 된다. 이 때, @Autowired 를 통해 의존성을 주입하기 때문에 XML 설정파일에서 car bean에 속성을 지정해주던 property 태그는 생략이 가능해진다.

// Car.java
package repairShop

import org.springframework.beans.factory.annotation.Autowired;

public class Car {
   @Autowired
   private Tire _tire;
   
   public String getTireBrand(){
      return _tire.getBrand();
   }
}

(2023.01.24 추가) 실제 업무 시 @Autowired 어노테이션 사용률이 떨어져 알아보니 순환참조 문제와 같은 이유로 생성자를 사용하여 객체 주입방식이 권장되고 있다고 한다. Spring Boot를 사용하는 우리 회사에서는 @RequiredArgsConstructor 어노테이션을 이용하여 주입을 해주고 있다. 다만, 테스트 코드를 작성 시에는 생성자를 이용한 객체 주입이 어려워 @Autowired을 사용한다.

추가로 스프링 프레임워크 위에서만 사용 가능한 @Autowired 어노테이션 대신 자바 표준 어노테이션인 @Resource로 대체해서 의존성 주입도 가능하다. 두 개의 어노테이션은 작성 방법은 동일하지만, @Autowired 어노테이션이 bean을 매칭 시 type을 우선순위에 두는 것과 달리 @Resource 어노테이션은 id를 우선순위에 둔다는 차이점이 있다. 두 어노테이션의 상세한 내용과 차이점에 대해서는 해당 글에서 설명하기에는 내용이 많아 추후 기회가 되면 다룰 예정이다.



참고 문헌 및 사이트

토비의 스프링 3.1 Vol. 1 스프링의 이해와 원리, 이일민, 에이콘출판
스프링 입문을 위한 자바 객체지향의 원리와 이해, 김종민, 위키북스
DEPENDENCY(의존성) 이란?? - Tony Programming
IoC, DI란 무엇일까 - Yungwang Ryu

profile
엔지니어가 되는 그 날 까지!

0개의 댓글