시리즈 첫 번째 게시글인 "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: Inversion of Control)이란 90년 중반에 GoF의 디자인패턴에서도 언급됐던 용어로, 객체 내부에서 직접 필요한 클래스의 객체를 생성하지 않고 다른 객체에 위임함으로써 객체에 대한 제어권, 즉 프로그램의 흐름을 제3자에게 넘기는 것을 IoC 모델이라고 한다. Spring 뿐만 아니라 모든 객체지향 프로그램에서는 IoC 모델을 적용해 객체를 올바르게 캡슐화하고 이를 통해 높은 응집도와 낮은 결합도를 갖춰 변경에 유연한 프로그래밍을 지향한다.
그렇다면 의존성 주입(DI: Dependency Injection)이란 무엇일까? IoC는 Spring이 등장하기 이전부터 쓰인 용어이고, 범용적으로 사용되어 스프링을 IoC 컨테이너라고 하기엔 스프링이 제공하는 기능의 특징을 명확하게 설명할 수 없다. 그래서 좀 더 스프링이 제공하는 IoC 방식을 명확하게 설명하기 위해 만든 새로운 용어가 의존성 주입(DI)이다.
스프링을 IoC 컨테이너라고만 해서는 스프링이 제공하는 기능의 특징을 명확하게 설명하지 못한다. 스프링이 서블릿 컨테이너 처럼 서버에서 동작하는 서비스 컨테이너라는 뜻인지, 아니면 단순히 IoC 개념이 적용된 템플릿 메소드 패턴을 이용해 만들어진 프레임워크인지, 아니면 또 다른 IoC 특징을 지닌 기술이라는 것인지 파악하기 힘들다. 그래서 새로운 용어를 만드는 데 탁월한 재능이 있는 몇몇 사람의 제안으로 스프링이 제공하는 IoC 방식을 핵심을 짚어주는 의존관계 주입이라는, 좀 더 의도가 명확히 드러나는 이름을 사용하기 시작했다.
- 토비의 스프링 3.1
좀 더 명확하게 정리하자면 의존성 주입(DI)이란 아래와 같은 세 가지 조건을 충족하는 작업이라고 할 수 있다.
앞서 예로 들었던 Car
객체의 경우, KoreaTire
객체와 강한 결합도를 갖도록 코딩되었다. 해당 코드에 대해 IoC와 DI를 적용시켜보며 코드를 통해 IoC/DI에 대해 이해해보자.
먼저, Car
객체에서 사용할 Tire
에 대해 인터페이스로 선언한다. 그리고 Tire
인터페이스를 구현한 KoreaTire
와 AmericaTire
를 작성한다. 이와 같이 객체를 인터페이스로 선언함으로써 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)를 활용하여 의존성 주입을 하도록 구현하여야 한다. 스프링에서 @
어노테이션을 사용하는 경우 주로 해당 방식을 사용한다.
## 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 정보를 활용해 객체를 생성할 때는 ApplicationContext
의 getBean()
메소드에 등록한 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
객체의 브랜드를 수정할 수 있다는 점이다. 만약 KoreaTire
를 AmericaTire
로 수정하고 싶다면, repairShop.xml
파일의 tire
와 americaTire
bean을 다음과 같이 수정하기만 하면된다.
## repairShop.xml
```중략```
<bean id="koreaTire" class="repairShop.KoreaTire"></bean>
<bean id="tire" class="repairShop.AmericaTire"></bean>
즉, 프로그램 코드의 수정/재컴파일/재배포를 거치지 않아도 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