BackEnd/Spring

Spring 학습_Day3_IoC_DI

Leo.K 2022. 6. 20. 17:05

기존 자바 코드에서 사용하던 객체 생성방법(main2)과 스프링에서 사용하는 객체 생성 방법(main)을 비교해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package t_tok01;
 
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
public class Main {
    public static void main2(String[] args) {
        
        //기존 자바 코드
        HelloWorld hello = new HelloWorld();
        hello.setMsg("Hello World");
        System.out.println(hello.getMsg());
    }
    
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        HelloWorld hello = (HelloWorld) context.getBean("helloWorld");
        System.out.println(hello.getMsg());
    }
}
 
cs

위의 코드를 보면 main2메서드는 우리가 평상시 자바를 사용할 때 많이 사용하는 객체를 생성하는 방법으로 익숙한 광경이다. 그 아래 main메서드는 beans.xml파일을 따로 만들어서 스프링이 객체를 생성해서 출력하도록 만드는 것이다. 자세한 태그의 설명은 코드에 첨부하도록 하겠다.

위의 beans.xml에서 <beans> 태그의 속성값은 바꿀일이 그닥 없기 때문에 사용할 때마다 복사해서 사용하면 된다. 그 아래 <bean>태그와 <property>속성만 내가 생성하고자 하는 객체에 따라서 유동적으로 수정한다. 

  • IoC(Inversion of Control)
    • 제어의 역전: 프로그램의 제어 흐름 구조가 개발자에서 스프링으로 역전되었다.
    • 이전에는 개발자가 프로그램의 흐름을 제어했지만 스프링은 xml파일에 환경설정만 해두면 프로그램의 흐름을 프레임워크가 주도하게 된다.
      • 객체의 생성, 생명주기 관리를 컨테이너가 담당
    •  Spring IoC container는 설정 정보 (configuration metadata)를 필요로 한다.
    • 설정 정보는 container가 객체를 생성하고, 객체간의 종속성을 이어줄 수 있도록 필요한 정보를 제공한다.

기존 자바 코드는 결과를 출력하기 위해서는 코드를 직접적으로 수정하고, 저장을 하면 자동으로 컴파일이 되어 클래스 파일이 생성이 되는데, 이 클래스 파일을 수정해야 다른 결과가 나오는 구조이다. [ 자바 파일 내부에서 수정 ]

하지만 스프링은 beans.xml파일의 내용만 수정하면 해당 내용을 소스코드 외부에서 주입하는 구조이다. [ 자바 파일 외부에서 수정 ]

 

스프링 컨테이너에 의해서 객체가 생성되고 객체에 대한 라이프 사이클을 관리하는 객체를 통틀어서 bean이라고 부른다.

Configureation Metadata는 xml파일 외에 Annotaion, 자바 config방식으로 진행할 수 있다.

  • DI(Dependency Injection)
    • 의존성 주입: IoC를 구현하는 디자인 패턴
    • 구성 요소간의 종속성을 소스코드에서 설정하지 않고, 외부의 설정 파일 등을 통해 주입하도록 하는 디자인 패턴
    • 장점
      • 종속성의 설정을 컴파일시에서 런타임시로 조정, 모듈간의 결합도 낮춤
      • 코드 재사용 촉진, 작성된 모듈을 여러 곳에서 소스코드 수정 없이 사용 가능
      • 단위 테스트 편의성 증대
    • DI를 써야 하는 이유
      • 과거 분리형 배터리를 사용하던 스마트폰과 일체형 배터리 스마트폰을 생각해보자. 
      • 일체형은 배터리를 교체할 수 없다고 가정
      • 이때, 만약 배터리에 누수 및 방전의 문제가 생겼다고 해보자. 
      • 일체형 배터리를 사용하는 핸드폰은 스마트폰 전체를 수리해야 한다. 
      • 분리형 배터리는 배터리만 교체해주면 된다. 
      • 이 예시를 프로그램적으로 설명하면, 분리형 배터리 스마트폰은 DI를 사용하여 스마트폰에 배터리를 주입해준 것이고 이로 인해 모듈 간의 결합도를 낮출 수 있다.(하나의 고장이 다른 부품에 영향을 주는 정도가 낮음), 일체형 배터리를 사용하는 스마트폰은 내부에 배터리가 일체형이기 때문에 결합도가 강하고 종속성 또한 강력하여 이후 수정이 매우 어렵다. 

 

위의 휴대폰 예시를 직접 코드를 구현해보면서 그 차이점을 확인해보자. 

먼저 핸드폰이라는 공통적인 속성을 구현 상속할 WhitePhone과 BlackPhone이 필요한데 이를 위해 Phone이라는 인터페이스를 만들어준다.  편의상 하나의 코드에 합쳐서 보여주도록 하겠다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
//------------------------------------------Phone Interface----------------------------------------------
package t_tok02;
 
public interface Phone {
    void show();
}
 
 
 
//------------------------------------------WhitePhone Class----------------------------------------------
package t_tok02;
 
//일체형 배터리 스마트폰. 배터리가 내장되어 있음
public class WhitePhone implements Phone{
    private Battery battery;
    
    
    
    public WhitePhone() {
        //자바 소스코드안에서 객체를 개발자가 만들어준다.
        //배터리를 교체할 수 있는 방법이 없다.
        this.battery = new Battery();
    }
 
 
 
    @Override
    public void show() {
        // TODO Auto-generated method stub
        System.out.println(this.getClass().getSimpleName() + " : " + battery.getName());
    }
    
}
 
 
//------------------------------------------BlackPhone Class----------------------------------------------
package t_tok02;
 
//분리형 배터리 스마트폰. 배터리를 교체할 수 있음
public class BlackPhone implements Phone{
    private Battery battery;
 
    public BlackPhone() {
        battery = new Battery();
    }
 
    public BlackPhone(Battery battery) {
        super();
        this.battery = battery;
    }
 
    public void setBattery(Battery battery) {
        this.battery = battery;
    }
 
    @Override
    public void show() {
        // TODO Auto-generated method stub
        System.out.println(this.getClass().getSimpleName() + " : " + battery.getName());
    }
    
    
}
 
cs

 

공통적으로 사용하는 Battery클래스 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package t_tok02;
 
public class Battery {
    private String name;
    
    
    
    public Battery() {
        this.setName("배터리");
    }
 
    public Battery(String name) {
        this.name = name;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
    
}
 
cs

 

다음으로 위의 클래스들을 출력해 줄 main class를 작성해준다. 여기까지는 아직 spring을 구현하지 않고 우리가 평상시에 자바에서 사용하는 방법으로 DI를 연습해보는 예제이다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package t_tok02;
 
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
public class Main {
    public static void main(String[] args) {
        //배터리 일체형
        WhitePhone wp = new WhitePhone();
        wp.show();
        
        //배터리 분리형 - DI 디자인 패턴
        BlackPhone bp = new BlackPhone();
        bp.show();
        
        //새로 산 배터리 객체 
        // -> 생성자 주입 방법
        Battery newBattery = new Battery("새로 산 배터리 -> 생성자");
        bp.setBattery(newBattery);
        bp.show();
        
        // -> 세터 주입법
        newBattery.setName("새로 산 배터리 -> 세터");
        bp.setBattery(newBattery);
        bp.show();
    }
}
 
cs

차이점이라면 블랙폰과 화이트폰 모두 스마트폰 내부에 배터리가 있다.(객체가 생성자를 통해 만들어 짐.) 하지만 블랙폰은 외부에서 값을 주입할 수 있도록 생성자와 세터메서드가 있지만, 화이트폰은 분리해서 교체가 불가능하기 때문에, 없다. 위의 배터리 클래스를 보면 알겠지만, 배터리 객체를 생성하면 기본적으로 "배터리"라는 값이 셋팅 된 상태로 객체가 생성되기 때문에 화이트폰이나 블랙폰이나 재정의한 show메서드를 호출하면 "배터리"가 출력이된다. 

하지만 우리가 지금 눈여겨 봐야 할 점은 초기값이 아니라 변경 방법이다. 반복적으로 말하지만 블랙폰만 배터리를 교체할 수 있는데, 이를 생성자와 세터로 구현을 하면 위의 보이는 소스코드처럼 출력 값을 변경할 수 있다.(=배터리 교체). 파라미터가 있는 생성자와 세터로 값을 셋팅하는 것은 코드만 보고 이해할 수 있을거라 생각하고 부가적인 설명은 하지 않겠다. 

현재 블랙폰은 객체에 내장된 배터리를 BlackPhone Class 외부에서 값을 변경한 것이다. 그렇다면 이를 Spring에서는 어떻게 구현하는 것일까? 위의 코드에서 Spring으로 구현하는 코드와 beans.xml파일만 추가로 첨부하도록 하겠다.   

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package t_tok02;
 
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
public class Main {
    public static void main(String[] args) {
        
        //....
 
        //스프링을 이용한 방법
        //main코드 수정x. 외부에서 bean객체를 수정해서 주입을 바꿈. -> 수정됨
        //주입을 사용해서 객체를 생성하면 원래 소스코드를 수정할 필요가 없기 때문에, 의존성, 결합성을 낮춰서 재사용성을 늘릴 수 있음.
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        BlackPhone newBp = (BlackPhone) context.getBean("blackPhone");
        newBp.show();
    }
}
 
cs

 

[ beans.xml ]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="UTF-8"?>
<!--xmlns : xmlnamespace-->
<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-3.0.xsd">
    
    
    <bean id="helloWorld" class="t_tok01.HelloWorld">
        <property name="msg" value="Korea Hello!"/>
    </bean>
    
    <bean id="battery" class="t_tok02.Battery">
        <!--세터를 이용한 방법-->
        <property name="name" value="스프링으로 산 배터리 - 세터"></property>
    </bean>
    
    <bean id="blackPhone" class="t_tok02.BlackPhone">
        <!--인자값이 있는 생성자를 사용하는 방법-->
        <constructor-arg ref="battery2" />
    </bean>
    
    <bean id="battery2" class="t_tok02.Battery">
        <constructor-arg name="name" value="스프링으로 산 배터리 - 생성자"></constructor-arg>
    </bean>
 
</beans>
cs

 

위의 소스 코드는 Spring을 사용해 의존성 주입 즉, 객체의 값을 바꾸어 준것이고, Main소스코드의 코드는 하나도 변하지 않았다. 

먼저 Main.java에서 14~16행만 보자. beans.xml 파일을 읽어올 context라는 객체를 하나 만들고, 이 객체가 읽어온 파일에서 id가 blackPhone인 bean객체를 읽어온 내용을 기반으로 해서 블랙폰 객체를 새로 만든다. 

다음으로, beans.xml파일에서 13~25행만 눈여겨 보자.

1. 18~21행.

빈즈 객체를 생성해줄때는 태그를 만들고 그 안에 property태그를 만들어서 세터로 주입하는 방법과, constructor태그를 만들어서 오버로드된 생성자를 통해 주입하는 방법이 있다. 이때 ref=""부분에 내가 사용할 방법이 정의된, id값을 넣으면 된다.

위 그림 상에서는 ref="battery"를 하면 세터 주입 방법으로 인해 아래와 같은 의존성 주입이 실행된다. 

반대로 ref="battery2"를 하면 생성자를 통한 주입 방법으로 인해 아래와 같은 의존성 주입이 실행된다.

이처럼 스프링으로 구현한 DI를 보면 메인 소스 코드인 Main.java에서는 단 한글자의 수정이 일어나지 않고 외부 설정파일인 beans.xml만 수정이 되었다. 이를 통해 클래스간의 의존성을 낮추어 결합도를 낮출 수 있고, 코드의 재사용성이 늘어난다. 

처음 해본 내용인데, 생각보다 많이 어렵다. 의미도 추상적이고. 연습 코드를 많이 실행시켜보면서 많은 연습을 하길 바란다.

'BackEnd > Spring' 카테고리의 다른 글

SpringCollection_국비Day82  (0) 2022.06.28
Spring학습_Day4_AOP  (0) 2022.06.28
Spring_국비Day81  (0) 2022.06.27
Spring 학습 Day2_개발환경 구축  (0) 2022.06.18
Spring 학습 Day1_개요 및 특징  (0) 2022.06.18