Spring IoC/DI

심야·2022년 11월 18일
0

웹 개발

목록 보기
26/46

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

의존성

의사코드

운전자가 자동차를 생산한다.
자동차는 내부적으로 타이어를 생산한다.

자바로 표현

new Car();
Car 객체 생성자에서 new Tire();

의존성 정의

new를 실행하는 CarTire 사이에서 CarTire에 의존한다.

변수에 값을 할당하는 모든 곳에 의존 관계가 생긴다. 즉, 대입 연산자(=)에 의해 변수에 값이 할당되는 순간에 의존이 생긴다. 변수가 지역 변수이건 속성이건, 할당되는 값이 리터럴이건 객체이건 의존은 발생한다.

그래서 의존 대상은 내부에 있을수도 있고 외부에 있을수도 있다. 그리고 DI는 외부에 있는 의존 대상을 주입하는 것을 말한다. 의존 대상을 구현하고 배치할 때 SOLID와 응집도는 높이고 결합도는 낮추라는 기본 원칙에 충실해야 한다. 그래야 프로젝트 구현과 유지보수가 수월해진다.

Tire.java

public interface Tire {
    String getBrand();
}

KoreaTire.java

public class KoreaTire implements Tire{
    public String getBrand(){
        return "코리아 타이어";
    }
}

AmericaTire.java

public class AmericaTire implements Tire{
    public String getBrand(){
        return "미국 타이어";
    }
}

Car.java

public class Car {
    Tire tire;
    public Car(){
        tire = new KoreaTire(); // 의존 간계가 일어난다.
        // tire = new AmericaTire();
    }

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

new KoreaTire(); 부분이 바로 자동차가 타이어를 생산(new)하는 부분, 의존 관계가 일어나고 있는 부분이다.

Driver.java

public class Driver {
    public static void main(String[] args) {
        Car car = new Car();
        System.out.println(car.getTireBrand());
    }
}

정리

  1. 자동차는 타이어에 의존한다.
  2. 운전자는 자동차를 사용한다.
  3. 운전자가 자동차에 의존한다고 봐도 된다.
  4. 자동차 생성자 코드에서 tire 속성에 새로운 타이어를 생성해서 참조할 수 있게 해주었다.

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

의사코드

운전자가 타이어를 생산한다.
운전자가 자동차를 생산하면서 타이어를 장착한다.

자바로 표현 - 생성자 인자 이용

Tire tire = new KoreaTire();
Car car = new Car(tire);

주입

외부에서라는 뜻을 내포한 단어
위 코드에선 자동차 내부에서 타이어를 생산하는 것이 아니라 외부에서 생산된 타이어를 자동차에 장착하는 작업이 주입이다.

생성자의 인자로 주입하기

public class Car {
    Tire tire;
    public Car(){
        tire = new KoreaTire(); // 의존 간계가 일어난다.
        // tire = new AmericaTire();
}

기존 코드는 Car 객체가 Tire를 직접 생산하는 방식, 즉 Tire에 대한 의존성을 자체적으로 해결하는 방식이다.

이번에는 외부에서 생산된 tire 객체를 Car 생성자 인자로 주입(장착)하는 형태로 구현하겠다.

Car.java - 생성자 인자로 주입하기

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

기존 코드와 비교했을 때 생성자 부분이 달라졌다. new가 사라지고 생성자에 인자가 추가되었다.

Driver.java

public class Driver {
    public static void main(String[] args) {
        Tire tire = new KoreaTire();
        // Tire tire = new America.tire();
        Car car = new Car(tire);

        System.out.println(car.getTireBrand());
    }
}

new를 사용해 타이어를 생산하는 코드가 Driver.java로 이동했다.
생산된 tire 객체 참조 변수를 Car생성자 인자로 전달한다.

기존 코드에서는 Car가 구체적으로 KoreaTire를 생산할지 AmericaTire를 생산할지 결정했다. 그러나 이러한 코드는 유연성이 떨어진다. 기존 코드를 현실세계에 비유하면 "자동차가 생산될 때 어떤 타이어를 생산해서 장착할까"라는 고민을 자동차가 스스로 하는 것이다. 그러나 변경된 Driver.java 코드는 "운전자가 차량을 생산할 때 운전자가 '직접' 어떤 타이어를 장착할까" 고민하는 것이다.

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

의사코드

운전자가 타이어를 생산한다.
운전자가 자동차를 생산한다.
운전자가 자동차에 타이어를 장착한다.

자바로 표현 - 속성 접근자 메서드 사용

Tire tire = new KoreaTire();
Car car = new Car();
car.setTire(tire);

생성자를 통해 의존성을 주입하는 것을 현실 세계에서 보면 자동차를 생산(구입)할 때 한번 타이어를 장착하면 더 이상 타이어를 교체 장착할 방법이 없다. 그래서 더 현실적인 방법인 운전자가 원할 때 마다 CarTire를 교체하는 것이다. 이를 구현하려면 속성을 통한 의존성 주입이 필요하다.

스프링을 통한 의존성 주입 - XML 파일 사용

의사코드

운전자가 종합 쇼핑몰에서 타이어를 구매한다.
운전자가 종합 쇼핑몰에서 자동차를 구매한다.
운전자가 자동차에 타이어를 장착한다.

자바로 표현 - 속성 메서드 사용

ApplicationContext context = new ClassPathXmlApplicationContext("expert002.xml", Driver.class);

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

Driver.java


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

public class Driver {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("expert002/expert002.xml");
        
        Car car = context.getBean("car", Car.class);
        Tire tire = context.getBean("tire", Tire.class);
        
        car.setTire(tire);

        System.out.println(car.getTireBrand());
    }
}

Driver.java 파일의 코드가 변경되었다. 자세히 살펴보겠다.

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

스프링 프레임워크에 대한 정보를 가지고 있는 패키지 목록이다.

ClassPathXmlApplicationContext("expert002/expert002.xml");

스프링 프레임워크에 필요한 정보이다.

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

CarTire를 구매하는 코드이다.

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/scema/beans/spring-beans.xsd">

	<bean id="tire" class="expert002.KoreaTire"></bean>
	<bean id="americaTire" class="expert002.AmericaTire"></bean>
	<bean id="car" class="expert002.car"></bean>
</beans>

스프링 프레임워크에서 사용할 tire, americaTire, car 목록을 등록했다.

이 과정을 현실 세계로 풀어보면 다음과 같다.

**"KoreaTire 상품이 tire라는 이름으로 진열돼 있고, 구매(getBean)할 수 있다.

스프링 도입 효과

위 코드에서 스프링을 도입했을 때 일어나느 효과는 무엇일까?
자동차 타이어 브랜드를 변경할 때 그 무엇도 재컴파일/재배포하지 않아도 XML 파일만 수정하면 프로그램 실행 결과를 바꿀 수 있다.

스프링을 통한 의존성 주입 - 스프링 설정 파일(XML)에서 속성 주입

의사코드 - 점멈 더 현실 세계를 닮아간다.

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

Driver.java

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

public class Driver {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("expert003/expert003.xml");

        Car car = context.getBean("car", Car.class);

        System.out.println(car.getTireBrand());
    }
}

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="expert003.KoreaTire"></bean>

    <bean id="americaTire" class="expert003.AmericaTire"></bean>

    <bean id="car" class="expert003.Car">
        <property name="tire" ref="koreaTire"></property>
        <!--
        <property name="tire" ref="americaTire"></property>
        -->
    </bean>

</beans>

스프링 설정 파일(XML) 효과

기존 코드에서 아래 코드가 삭제되었다.

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

car.setTire(tire); 이 코드는 property name="tire" 부분으로 Tire tire = context.getBean("tire", Tire.class); 이 코드는 <bean id="koreaTire" class="expert003.KoreaTire"></bean> 그리고 ref="koreaTire" 부분으로 대체되었다.

<bean id="koreaTire" class="expert003.KoreaTire"></bean>
<property name="tire" ref="koreaTire"></property>

얼핏보면 조금 복잡해진 것 같지만 현실 세계에 더 가까워졌으며 유지보수하기 편한 방식으로 변하였다.

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

의사코드

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

Car.java

import org.springframework.beans.factory.annotation.Autowired;
public class Car {
    @Autowired
    Tire tire;
    public Tire getTire(){
        return tire;
    }

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

XML

기존의 XML

<bean id="car" class="expert003.Car">
        <property name="tire" ref="koreaTire"></property>
</bean>

변경된 XML

<bean id="car" class="expert004.car"></bean>

변경된 XML 코드를 보면 property태그가 사라졌다. @Autowired를 통해 carproperty를 자동으로 엮어줄 수 있어 (자동 의존성 주입) 생략이 가능해졌다.

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

의사코드

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

Car.java

import javax.annotation.Resource;
        
public class Car {
    @Resource;
    Tire tire;

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

@Autowired 어노테이션을 @Resource 로 변경했다. @Autowired는 스프링 어노테이션이고 @Resource는 자바 표준 어노테이션이다.

두 어노테이션은 어느 차이점이 있는지 비교해보겠다.

스프링을 통한 의존성 주입 - @Autowired🆚@Resource🆚property 태그

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

XML - 한 개의 빈이 id 없이 tire 인터페이스 구현한 경우

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.prg/schema/context/spring-context-3.1.xsd">

    <context:annotation-config></context:annotation-config>

  <bean class="expert006.KoreaTire"></bean>
    
    <bean id="car" class="expert006.car"></bean>
</beans>

Car.java

package expert006;

import javax.annotation.Resource;

public class Car {
    @Resource
    Tire tire;

    public String getTireBrand(){
        return "장착된 타이어: " + tire.getBrand();
    }
}
package expert006;

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

public class Car {
    @Autowired
    Tire tire;

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

정상적으로 작동한다.

XML - 두 개의 빈이 id 없이 tire 인터페이스를 구현한 경우

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.prg/schema/context/spring-context-3.1.xsd">

    <context:annotation-config></context:annotation-config>

    <bean class="expert006.KoreaTire"></bean>
    <bean class="expert006.AmericaTire"></bean>

    <bean id="car" class="expert006.car"></bean>
</beans>

에러 발생

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

XML - 두 개의 빈이 tire 인터페이스 구현하고 하나가 일치하는 id를 가진 경우

<?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.prg/schema/context/spring-context-3.1.xsd">

    <context:annotation-config></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>

실행 결과

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

XML - 두 개의 빈이 tire 인터페이스를 구현하고 일치하는 id가 없는 경우

<?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.prg/schema/context/spring-context-3.1.xsd">

    <context:annotation-config></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>

에러 발생

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

XML - 일치하는 id가 하나 있지만 인터페이스를 구현하지 않은 경우

<?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.prg/schema/context/spring-context-3.1.xsd">

    <context:annotation-config></context:annotation-config>

    <bean id="tire" class="expert006.Door"></bean>
    
    <bean id="car" class="expert006.car"></bean>
</beans>
Bean named 'tire' must be of type [expert006.Tire], but was actually of type [expert006.Door]

No matching bean of type [expert006.Tire] found for dependency: expected at least 1 bean ...

이 경우 byName 우선인 @Resource와 byType 우선인 @Autowired 에러 메시지가 서로 다르다.

큰 차이는 없는데 무엇을 써야할까?

@Autowired가 자동으로 주입되는 장점이 있지만 본 코드에서 사용한 Car 객체는 @Resource가 더 어울린다. 그런데 @Resource<property> 중에서는 <property>가 더 낫다. XML파일만 봐도 DI 관계를 더 쉽게 확인할 수 있고 유지보수성도 좋다. 그리고 프로젝트 규모가 커지면 XML 파일을 용도별로 분리할 수 있어 더욱 좋다.

그러나 개발에서 "무조건" 이라는 말은 없다. 따라서 프로젝트 규모와 팀 성향에 맞게 사용해야 한다.

출처

스프링 입문을 위한 객체 지향의 원리와 이해

profile
하루하루 성실하게, 인생 전체는 되는대로.

0개의 댓글