스프링 삼각형과 설정 정보(1) - IoC/DI(3)

de_sj_awa·2021년 6월 6일
0
post-custom-banner

1. IoC/DI - 제어의 역전/의존성 주입

6. 스프링을 통한 의존성 주입 - @Autowired를 통한 속성 주입

의사 코드
운전자가 종합 쇼핑몰에서 자동차를 구매 요청한다.
종합 쇼핑몰은 자동차를 생산한다.
종합 쇼핑몰은 타이어를 생성한다.
종합 쇼핑몰은 자동차에 타이어를 장착한다.
종합 쇼핑몰은 운전자에게 자동차를 전달한다.

의사 코드는 이전과 동일하다.

Car라고 하는 클래스에 tire라고 하는 속성을 만들고 설정자 메서드를 만들다고 해보자. 그러면 대부분 다음과 같은 코드를 작성한다.

Tire tire;

public void setTire(Tire tire) {
    this.tire = tire;
}

반드시 설정자 메서드를 통해 tire 값을 주입해야 하는 것일까? 스프링의 속성 주입 방법 가운데 @Autowired를 이용하는 방법을 알아보자.

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

@Autowired
Tire tire;

import문 하나와 @Autowired 애노테이션을 이용하면 설정자 메서드를 이용하지 않고도 종합쇼핑몰인 스프링 프레임워크가 설정 파일을 통해 설정자 메서드 대신 속성을 주입해 준다.

변경된 스프링 설정 파일(여기서는 expert004.xml)을 보자.

expert004.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       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 
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
  
    <context:annotation-config/>
  
    <bean id="tire" class="expert004.KoreaTire"></bean>
    <bean id="americatire" class="expert004.AmericaTire"></bean>
    <bean id="car" class="expert004.Car"></bean>
  
</beans>

@Autowired의 의미를 이해해보자. 이것은 스프링 설정 파일을 보고 자동으로 속성의 설정자 메서드에 해당하는 역할을 해주겠다는 의미다. 그리고 XML 파일에서 무넉가가 사라졌다. 기존의 XML 설정 파일과 비교해보자.

기존의 XML 설정 파일
<bean id="car" class="expert003.Car">
  <property name="tire" ref="KoreaTire"></property>
</bean>

새로운 XML 설정 파일
<bean id="car" class="expert004.Car"></bean>

property 태그가 왜 사라졌을까? @Autowired를 통해 car의 property를 자동으로 엮어줄 수 있으므로(자동 의존성 주입) 생략 가능해진 것이다.

그럼 이번에는 자바 코드에서 달라진 부분을 살펴보자.

  • Tire.java - 전혀 변한게 없다.
  • KoreaTire.java - 역시 변한게 없다.
  • AmericaTire.java - 역시 변한게 없다.
  • Car.java - @Autowired를 사용하도록 바뀌었다.
  • Driver.java - 역시 변경된 부분이 없다.

Car.java

package expert004;

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

public class Car {
    @Autowired
    Tire tire;
    
    public String getTireBrand() {
        return "장착된 타이어 : " + tire.getBrand();
    }
}

번외 1. AmericaTire로 변경된 Driver.java를 실행하려면 어디를 고쳐야 할까?

재컴파일할 필요 없이 expert004.xml에서 bean의 id 속성만 변경하면 된다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       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 
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
  
    <context:annotation-config/>
  
    <bean id="tire02" class="expert004.KoreaTire"></bean>
    <bean id="tire" class="expert004.AmericaTire"></bean>
    <bean id="car" class="expert004.Car"></bean>
  
</beans>

번외 2. 위 번외 1에서 KoreaTire 부분을 완전히 삭제하고, AmericaTire의 id 속성을 삭제해보자.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       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 
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
  
    <context:annotation-config/>
  
    <bean class="expert004.AmericaTire"></bean>
    <bean id="car" class="expert004.Car"></bean>
  
</beans>

그런 다음 Driver.java를 다시 실행해보자. 정상적으로 구동되는 것을 확인할 수 있을 것이다. 왜 정상적으로 구동됄까>

기존에는 Car.java에서 @Autowired가 지정된 tire 속성과 expert004.xml 파일에서 bean의 id 속성이 일치하는 것을 찾아 매칭시킨 것 같았다. 그런데 여기서는 bean의 id 속성이 없는데 어떻게 매칭된 것일까?

바로 인터페이스의 구현 여부가 답이다. 스프링의 @Autowired는 바로 type 기준 매칭에 있다. 만약 같은 타입을 구현한 클래스가 여러 개 있다면 그때 bean 태그의 id로 구분해서 매칭하게 되는 것이다.

스프링의 @Autowired는 id와 type 중 type 구현에 우선순위가 있다.

7. 스프링을 통한 의존성 주입 - @Resource를 통한 속성 주입

의사 코드
운전자가 종합 쇼핑몰에서 자동차를 구매 요청한다.
종합 쇼핑몰은 자동차를 생산한다.
종합 쇼핑몰은 타이어를 생성한다.
종합 쇼핑몰은 자동차에 타이어를 장착한다.
종합 쇼핑몰은 운전자에게 자동차를 전달한다.

의사코드를 보면 이전과 동일하다. 바로 이전 절에서는 @Autowired를 통해 속성을 주입했었는데, 자동으로 묶어준다는 의미보다 더 직관적인 말은 없을까? 자동차에게 타이어는 재료 또는 자원, 부품이라고 할 수 있다. 그리하여 이제부터는 Autowired라는 표현 대신 Resource라는 표현을 쓰겠다. 따라서 변경되는 부분은 @Autowired 부분뿐이다.

Car.java

package expert005;

import javax.annotation.Resource;

public class Car {
    @Resource
    Tire tire;
    
    public String getTireBrand() {
        return "장착된 타이어 : " + tire.getBrand();
    }
}

@Autowired는 스프링의 어노테이션이다. @Resource는 자바 표준 어노테이션이다. 스프링 프레임워크를 사용하지 않는다면 @Autowired는 사용할 수 없고 오직 @Resource만을 사용해야 한다. 그리고 이전 절에서 살펴봤듯이 @Autowired의 경우 type과 id 가운데 매칭 우선순위는 type이 높다. @Resource의 경우 type과 id 가운데 매칭 우선순위는 id가 높다. @Resource의 경우 id로 매칭할 빈을 찾기 못한 경우 type으로 매칭할 빈을 찾게 된다.

8. 스프링을 통한 의존성 주입 - @Autowired vs @Resource vs <property> 태그

@Autowired와 @Resource 어노테이션은 이전 절에서 살펴본 대로 두 객체 사이에 의존성을 해결해준다. 둘을 비교해보자.

@Autowired @Resource
출처 스프링 프레임워크 표준 자바
소속 패키지 org.springframework.beans.factory.annotation.Autowired javax.annotation.Resource
빈 검색 방식 byType 먼저, 못 찾으면 byName byName 먼저, 못 찾으면 byType
특이사항 @Qualifier("") 협업 name 어트리뷰트
byName 강제하기 @Autowired
@Qualifier("tire1")
@Resource(name="tire1")

사례 연구 1. XML 설정 - 한 개의 빈이 id 없이 tire 인터페이스를 구현한 경우

expert006.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       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 
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
  
    <context:annotation-config/>
  
    <bean class="expert006.KoreaTire"></bean>
    <bean id="car" class="expert006.Car"></bean>
  
</beans>

Car.java - @Resource를 이용한 tire 속성 주입

package expert006;

import javax.annotation.Resource;

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

public class Car {
    @Resource
    Tire tire;
    
    public String getTireBrand() {
        return "장착된 타이어 : " + tire.getBrand();
    }
}

Car.java - @Autowired를 이용한 tire 속성 주입

package expert006;

import javax.annotation.Resource;

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

public class Car {
    @Autowired
    Tire tire;
    
    public String getTireBrand() {
        return "장착된 타이어 : " + tire.getBrand();
    }
}

각각의 경우 Driver.java를 실행해 보면 두 경우 모두 아무 문제 없이 실행되는 것을 확인할 수 있다.

사례 연구 2. 두 개의 빈이 id 없이 tire 인터페이스를 구현한 경우

expert006.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       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 
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
  
    <context:annotation-config/>
  
    <bean class="expert006.KoreaTire"></bean>
    <bean class="expert006.AmericaTire"></bean>
    <bean id="car" class="expert006.Car"></bean>
  
</beans>

각각의 경우 Driver.java를 실행해 보면 두 경우 동일한 오류 메시지를 출력한다.

No unique bean of type [expert006.Tire] is defined:
expected single matching bean but found 2:
[expert006.KoreaTire#0, expert006.AmericaTire#0]

사례 연구 3. 두 개의 빈이 tire 인터페이스를 구현하고 하나가 일치하는 id를 가진 경우

expert006.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       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 
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
  
    <context:annotation-config/>
  
    <bean id="tire" class="expert006.KoreaTire"></bean>
    <bean id="tire2" class="expert006.AmericaTire"></bean>
    <bean id="car" class="expert006.Car"></bean>
  
</beans>

@Resource의 경우 Driver.java를 실행했을 때 정상적으로 작동한다.

장착된 타이어 : 코리아 타이어

@Autowired의 경우에도 Driver.java를 실행했을 때 정상적으로 작동한다.

장착된 타이어 : 코리아 타이어

사례 연구 4. 두 개의 빈이 tire 인터페이스를 구현하고 일치하는 id가 없는 경우

expert006.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       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 
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
  
    <context:annotation-config/>
  
    <bean id="tire1" class="expert006.KoreaTire"></bean>
    <bean id="tire2" class="expert006.AmericaTire"></bean>
    <bean id="car" class="expert006.Car"></bean>
  
</beans>

각각의 경우 Driver.java를 실행해 보면 두 경우 동일한 오류 메시지를 출력한다.

No unique bean of type [expert006.Tire] is defined:
expected single matching bean but found 2: [tire1, tire2]

사례 연구 5. 일치하는 id가 하나 있지만 인터페이스를 구현하지 않은 경우

expert006.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       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 
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
  
    <context:annotation-config/>
  
    <bean id="tire" class="expert006.Door"></bean>
    <bean id="car" class="expert006.Car"></bean>
  
</beans>

Door.java

package expert006;

// tire 인터페이스를 구현하지 않음
public class Door {

}

@Resource의 경우 Driver.java를 실행하면 다음과 같은 오류가 발생한다.

Bean name "tire" must be of type [expert006.Tire], but was actually of type [expert006.Door]

@Autowired의 경우 Driver.java를 실행하면 다음과 같은 오류가 발생한다.

No matching bean of type [expert006.Tire] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

여기서는 byName 우선인 @Resource와 byType 우선인 @Autowired의 에러 메시지가 서로 다르다는 것에 주목하자.

또한 @Autowired와 @Resource 중에 어떤 것을 사용해야 할까? 자동으로 주입된다는 의미에서는 @Autowired가 명확해 보이지만 실제 Car 입장에서 보면 @Resource라는 표현이 더 어울린다. 그리고 나중에 스프링이 아닌 다른 프레임워크로 교체되는 경우를 대비하면 자바 표준인 @Resource를 쓰는 것이 유리하다.

사실 @Resource는 <bean> 태그의 자식 태그인 <property>로 해결할 수 있다. 그럼 이 둘 중 어떤 것을 사용해야 할까? <property>를 사용하면 XML 파일만 봐도 DI 관계를 손쉽게 확인할 수 있다. 그렇지만 개발 생산성은 @Resource가 나을 것이다. 반면 <property>는 유지보수성이 좋다. 프로젝트의 규모가 커지면 XML 파일의 규모가 커지기 마련인데 XML 파일도 용도별로 분리할 수 있다. 그래서 더욱 <property>를 사용하는 것이 좋다.

사례 연구 6. XML 설정 - 두 개의 빈이 tire 인터페이스를 구현하고 속성과 일치하는 id가 없지만 @Resource 어노테이션의 name 속성이 id와 일치하는 경우

expert006.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       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 
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
  
    <context:annotation-config/>
  
    <bean id="tire1" class="expert006.KoreaTire"></bean>
    <bean id="tire2" class="expert006.AmericaTire"></bean>
    <bean id="car" class="expert006.Car"></bean>
  
</beans>

Car.java

package expert006;

import javax.annotation.Resource;

public class Car {
    @Resource(name="tire1")
    Tire tire;
    
    public String getTireBrand() {
        return "장착된 타이어 : " + tire.getBrand();
    }
}

Driver.java를 실행하면 정상적인 결과를 확인할 수 있다.

사례 연구 7. 사례 연구 6과 같도록 @Autowired를 지정하려면 다음과 같이 설정한다.

Car.java

package expert006;

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

public class Car {
    @Autowired
    @Qualifier("tire1")
    Tire tire;
    
    public String getTireBrand() {
        return "장착된 타이어 : " + tire.getBrand();
    }
}

Driver.java를 실행하면 정상적인 결과를 확인할 수 잇따.

사례 연구 8. 실무에서는 다음과 같이 설정한다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       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 
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
  
    <context:annotation-config/>
  
    <bean id="tire" class="expert006.KoreaTire"></bean>
    <bean id="tireOther" class="expert006.AmericaTire"></bean>
    <bean id="car" class="expert006.Car"></bean>
  
</beans>

Car.java

package expert006;

import javax.annotation.Resource;

public class Car {
    @Resource(name="tire")
    Tire tire;
    
    public String getTireBrand() {
        return "장착된 타이어 : " + tire.getBrand();
    }
}

만약 미국 타이어를 쓰고 싶다면 빈의 id만 바꾸면 된다.

<bean id="tireOther" class="expert006.KoreaTire"></bean>
<bean id="tire" class="expert006.AmericaTire"></bean>

프로그램을 재구동하면 미국 타이어가 장착된다. 재컴파일과 재배포가 필요없다. 설정 파일을 쓰는 가장 중요한 이유가 재컴파일/재배포 없이 프로그램의 실행 결과를 변경할 수 있기 때문이다.

마지막으로, 의존 관계는 new 뿐만이 아니라 사실 변수에 값을 할당하는 모든 곳에 의존 관계가 생긴다. 즉, 대입 연산자(=)에 의해 변수에 값이 할당되는 순간에 의존이 생긴다. 변수가 지역 변수이건 속성이건, 할당되는 값이 리터럴이던 객체이던 의존은 발생한다. 의존 대상이 내부에 있을 수도 있고, 외부에 있을 수도 있다. DI는 외부에 있는 의존 대상을 주입하는 것을 말한다. 의존 대상을 구현하고 배치할 때 SOLID와 응집도는 높이고 결합도는 낮추라는 기본 원칙에 충실해야 한다. 그래야 프로젝트의 구현과 유지보수가 수월해진다.

참고

profile
이것저것 관심많은 개발자.
post-custom-banner

0개의 댓글