JAVA-SOLID

[OOP] 스프링 삼각형(IoC/DI)

EJUN 2023. 6. 18. 19:16

스프링을 이해하기 위해서는 스프링 삼각형이 대단히 중요하다.

스프링 삼각형

프로그래밍에서의 의존성이 의미하는 것은 무엇일까?

대답은 전체가 부분에 의존한다는 것을 의미한다. 자바에서는 new키워드가 의존관계를 표현한다고도 한다.

의존성을 주입하는 방법에는 스프링을 이용한 방법과 이용하지 않은 방법 2가지가 존재한다.

우선 스프링을 사용하지 않고 의존성을 주입하는 방법에 대해 설명하겠다.

 

스프링을 사용하지 않고 의존성을 주입하는 방법(생성자 주입)
public class IocDi {
    public static void main(String[] args) {
        Case c=new ClearCase();
        Phone phone=new Phone(c);
        System.out.println(phone.getCaseType());
    }
}

interface Case{
    String getCase();
}

class Phone{
    Case c;
    public Phone(Case c) {
        this.c=c;
    }
    public String getCaseType(){
        return c.getCase();
    }
}

class ClearCase implements Case{
    @Override
    public String getCase() {
        return "투명 케이스";
    }
}

class JellyCase implements Case{
    @Override
    public String getCase() {
        return "젤리 케이스";
    }
}

위의 코드는 생성자를 이용한 의존성 주입을 한 코드이다.

스마트폰 사용자가 투명케이스와 젤리케이스 중 어떤 케이스를 쓸지 고민하기 때문에 단순히 new를 이용한 의존성 주입에

비해 유연성이 많이 향상되었다.

Phone phone=new Phone(c); 이 부분이 바로 생성자를 이용하여 의존성을 주입하는 핵심 부분이다.

이런식으로 구현을 한 경우의 장점은 Phone생성자는 Case인터페이스를 구현한 객체만 들어오기만 해도

상관이 없다는 장점이 있다. 

이 말은 즉 유연성도 좋아지면서 확장성 또한 기대할 수 있다.

 

 

스프링을 사용하지 않고 의존성을 주입하는 방법(속성 주입)
public class PropertyDi {
    public static void main(String[] args) {
        Case1 case1=new JellyCase1();
        Phone1 phone1=new Phone1();
        phone1.setCase(case1);
    }
}
interface Case1{
    String getCase();
}

class Phone1{
    Case1 c;
    public void setCase(Case1 c) {
        this.c = c;
    }
}

class ClearCase1 implements Case1{
    @Override
    public String getCase() {
        return "투명 케이스";
    }
}

class JellyCase1 implements Case1{
    @Override
    public String getCase() {
        return "젤리 케이스";
    }
}

위의 코드를 보면 이번에는 생성자를 통한 의존성 주입이 아니라 속성을 통해 의존성을 주입한 코드이다.

앞서 말한 코드와 위 코드의 차이점은 Phone1클래스에 인자를 가진 생성자가 없다는 것이다.

대신에 Setter메소드가 들어와있다는 차이점이 있다.

phone1.setCase(case1); 바로 이 부분이 속성을 이용하여 의존성 주입을 한 부분이다.

 

그럼 생성자를 통한 의존성 주입과 속성을 이용하여 의존성을 주입한 것 중에 더 많이 사용되는 것은 무엇일까?

요즘은 생성자를 이용한 의존성 주입 방법을 더 많이 사용하고 있다.

왜냐하면 생성자를 이용하여 의존성을 주입하면 한번 생성하였기 때문에 잘 바꾸지를 않지만

속성을 이용하여 의존성을 주입하게 되면 자주 바꿀수도 있는데 요즘은 한번 의존성을 주입하면 바꾸는 일이

거의 없기 때문에 생성자를 이용한 의존성 주입을 더 사용하는 편이다.

 

스프링을 이용한 의존성 주입

스프링을 이용하여 의존성을 주입하는 방법에는 xml파일을 이용하여 bean등록하여 의존성주입을 하는 방법과

어노테이션을 이용하여 의존성을 주입하는 방법이 있다.

 

우선 xml을 이용하여 의존성을 주입하는 방법은 생성자 주입 방법과 속성을 통한 의존성 주입 둘 다 가능하다.

public class PropertyDi {
    public static void main(String[] args) {
        ApplicationContext context=new ClassPathXmlApplicationContext("DI.xml",PropertyDi.class);
        Case1 case1=(Case1)context.getBean("case1",Case1.class);
        Phone1 phone1= (Phone1) context.getBean("phone1",Phone1.class);
    }
}

위처럼 xml파일에 bean을 등록해준 다음

<?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="case1" class="JAVA_OOP.DI.JellyCase1"></bean>
        <bean id="Phone1" class="JAVA_OOP.DI.Phone1"></bean>
</beans>

xml파일에서 id와 어떤 클래스를 통해 인스턴스화 할지 정해주면 된다.

 

이렇게하고 위 코드를 실행하면 "젤리케이스"가 출력이 될 것이다.

<bean id="case1" class="JAVA_OOP.DI.ClearCase1"></bean> // 이 부분만 수정
<bean id="Phone1" class="JAVA_OOP.DI.Phone1"></bean>

만약 투명케이스를 출력하고 싶다면 xml 파일을 위처럼 수정하고 main()메소드를 실행했을 때 투명케이스가 된다.

바로 이런 점이 의존성주입의 장점이라고 할 수 있다.

왜냐면 main()메소드는 수정할 필요 없이 xml파일만 수정하고 재컴파일도 필요없고 재실행만 하면 되기 때문이다.

 

또한 xml을 이용하여 속성주입을 하려면 xml파일만 수정하면 된다.

 

 <bean id="case1" class="JAVA_OOP.DI.JellyCase1"></bean>
        <bean id="phone1" class="JAVA_OOP.DI.Phone1">
                <property name="case1" ref="case1"></property>
        </bean>

이런식으로 <property>를 이용하여 하면 되는데 name은 클래스에서 사용하는 setter이름을 의미하고,

ref는 setter에 주입할 객체의 이름 을 의미한다.

즉 ref는 항상 다른 bean id의 이름이 와야한다.

 

@Autowired를 이용한 의존성 주입

스프링에는 많은 어노테이션이 있는데 의존성 주입을 도와주는 어노테이션 중 하나가 바로 @Autowired어노테이션이다.

public class SpringDi {
    public static void main(String[] args) {

    }
}

interface Case{
    String getCase();
}

class Phone{
    private final Case c;
    @Autowired
    Phone(Case c) {
        this.c = c;
    }
}

이런식으로 Phone클래스에 @Autowired를 사용하면 자동으로 의존성 주입을 해준다.

@Autowired는 스프링 설정파일이나 스프링 컨테이너를 참고하여 자동으로 속성의 설정자 메소드(Getter/Setter)에

해당하는 역할을 해준다는 의미이다.

 

그렇다면 만약 xml파일에 빈으로 등록하는 상황이 있다고 하자.

 //Phone클래스
 @Autowired
    Case case;
 
 //xml파일
 <bean id="case" class="JellyCase"></bean>

위 코드에서는 JellyCase클래스를 매칭 시킬 것이다.

 

 //Phone클래스
 @Autowired
    Case case;
 
 //xml파일
 <bean class="JellyCase"></bean>

그럼 위 코드는 id속성이 없으니까 매칭이 안될까?

대답은 매칭이 성공적으로 된다.

 

그 이유는 바로 @Autowired는 id속성보다는 Type을 더 높은 우선순위로 매칭시키기 때문에 매칭이 가능하다.

JellyCase는 Case인터페이스를 상속받은 클래스이다.

즉, 동일한 Type이기 때문에 의존관계가 성립이 된다.

 //Phone클래스
 @Autowired
    Case case;
 
 //xml파일
 <bean id="case" class="JellyCase"></bean>
 <bean id="case" class="ClearCase"></bean>

그럼 위 코드를 실행한 경우 정상적으로 실행이 될까?

정답은 컴파일이 성공적으로 이루어지지 않는다.

 

왜냐하면 둘 다 id속성이 존재하지 않고, 동일한 Type이기 때문에 스프링이 사용자가 무엇을 원하는지 알 수 없어

오류를 발생시킨다.

 

즉, @Autowired는 id속성보다는 Type속성의 우선순위가 더 높다.

 

또는 스프링을 이용하여 @Autowired를 하면 xml파일을 사용하지 않고도 @Component 어노테이션을 이용하여 

해당 객체를 스프링 컨테이너에 등록하고 자동으로 의존성을 주입받을 수 있다.

 

근데 만약 동일한 타입의 빈이 여러개이면 @Autowired는 누구에게 의존성을 주입해줄까?

class Phone{
  @Autowired
    Case case;
}

@Component
class ClearCase implements Case{
    @Override
    public String getCase() {
        return "투명 케이스";
    }
}

@Component
class JellyCase implements Case{
    @Override
    public String getCase() {
        return "젤리 케이스";
    }
}

위의 코드가 있다고 가정하자.

JellyCase와 ClearCase가 동일한 Case타입을 참조하고 Bean으로 등록이 되어있다.

아까 위에서 Bean으로 동일한 Type을 등록한 경우랑 똑같은 상황이다.

 

이런 경우에는 @Primary나 @Qualifier어노테이션을 이용하여 의존성 주입을 할 수 있다.

class Phone{
  @Autowired 
    Case case;
}

@Component @Primary
class ClearCase implements Case{
    @Override
    public String getCase() {
        return "투명 케이스";
    }
}

@Component
class JellyCase implements Case{
    @Override
    public String getCase() {
        return "젤리 케이스";
    }
}

위처럼 @Primary어노테이션을 붙이면 동일한 타입이 있어도 우선적으로 의존성 주입을 해준다는 의미이다.

class Phone{
  @Autowired @Qualifier("ClearCase")
    Case case;
}

@Component 
class ClearCase implements Case{
    @Override
    public String getCase() {
        return "투명 케이스";
    }
}

@Component
class JellyCase implements Case{
    @Override
    public String getCase() {
        return "젤리 케이스";
    }
}

위처럼 @Qualifier(객체 이름) 어노테이션을 사용하게 되면 사용자가 해당 객체에게 의존성을 주입한다고 

선언하는 의미이다.

 

위처럼 동일한 타입의 bean이 여러개 있어도 두 개의 어노테이션을 골라서 사용하면 의존성 주입 문제를 해결할 수 있다.

 

@Resource를 이용한 의존성 주입

@Resource 어노테이션의 기능은 @Autowired 어노테이션의 기능과 동일하다.

다만 차이점이 있다면 @Resource는 bean의 id속성을 먼저 확인한다는 차이가 있다.

 

정리하면

  @Autowired @Resource
출처 Spirng Framework Standard JAVA
빈 검색 방식 byType우선, 못 찾을 시 byName  byName우선, 못 찾을 시 byType
특이사항 @Qualifier("")협업 name어트리뷰트
byName 강제하기 @Autowired
@Qualifier("name")
@Resource(name="name")