본문 바로가기
Web/Spring

Spring(3)

by 미티치 2016. 5. 12.

『 목차 』


- 컨테이너란? IoC/DI란? (퍼옴)

- 스프링 컨테이너 생명주기

- 빈 범위(Scope)





스프링 컨테이너 생명주기를 공부하기 앞서서 컨테이너가 뭔지에 대해서 공부해야한다.


토비의 스프링은 다음과 같이 말한다. 스프링은 거대한 컨테이너임과 동시에 Ioc/DI를 기반으로 하고 있는 거룩한 존재이며 서비스 추상화를 통해 삼위일체로 분리되는 3단 변신로봇이라고 한다. 이럴수가! 뭔말하는지는 하나도 모르겠지만 일단 말만 들어도 엄청난데다 가격까지 공짜다. 게다가 이걸 쓰는 사람들마다 칭찬 또 칭찬 일색이니 궁금해서 참을 수가 없다.


근데 말이다…. 필자는 스프링의 지독한 뉴비이므로 여기서 뉴비답게 눈치없게 한번 굴어보려 한다. 일단 스프링이 대단하고 무지 엄청나다는 건 알겠는데…. 컨테이너는 뭐고 IoC/DI는 뭐란 말인가? 평생 살면서 컨테이너란 위의 사진같은 것 밖에 모르는데… 그리고 서비스 추상화? 난 아직 서비스가 뭔지도 모르겠다.


컨테이너란?

컨테이너란 당신이 작성한 코드의 처리과정을 위임받은 독립적인 존재라고 생각하면 된다. 컨테이너는 적절한 설정만 되있다면 누구의 도움없이도 프로그래머가 작성한 코드를 스스로 참조한 뒤 알아서 객체의 생성과 소멸을 컨트롤해준다. 거기다 컨테이너를 이용함으로써 얻게되는 이득은 어마어마 하며, 이런 강력한 기능을 무료로 이용할 수 있다는 점 또한 엄청나다.

허나 소수의 독립심 강하고 줏대있는 사람들은 내가 잘 알지도 못하는 프로그램을 무작정 써야 한다는 것에 반감을 살 수도 있다. 게다가 프로그래머가 다른 라이브러리에 의존하지 않고 직접 코드를 작성해나간다면 완성된 프로그램에 대한 이해도 또한 상상을 초월할 것이다. 혹은 인간이 그렇게 줏대없이 프로그램만 의존하다가 터미네이터나 매트릭스 같은 세상이 되버리면 어떡하냐고 필자에게 따질 수도 있는 것이다.

만약 이러저러한 이유로 컨테이너를 거부하겠다면 필자는 굳이 사용하지 않아도 상관은 없다고 말하겠다. 진심으로 실력만 된다면사용자가 직접 컨테이너를 만들어써도 상관은 없다.

그러나 직접 컨테이너를 구현해보고자 키보드에 손을 올렸다면 아마 배보다 배꼽이 더큰 컨테이너의 어마어마한 기능에 입이 쩍벌어질 것이다. 빈정상하라고 하는 말은 아니지만 사실 당신이 구현하고자 하는 코드와 컨테이너의 구현 코드는 비교도 안될 정도로 수준차가 어마어마하다. 그러므로 코드의 이해가 당장 힘들어지고 뭐가뭔지 모르겠더라도 일단 막강한 기능을 제공하는 컨테이너를 이용하기로 하자.

다시 말하지만 프로그래머가 작성한 코드는 컨테이너를 사용하게 됨으로서 프로그래머의 손을 떠나 컨테이너의 영역으로 떠나버리게 된다. (정확히 말하자면 컨테이너가 맘대로 객체를 생성하는 게 아니라 프로그램을 이용하는 이용자의 호출에 의해 컨테이너가 동작하게 되는 구조이다.) 여하튼 컨테이너를 이용한다면 우리의 골치아픈 문제는 다 컨테이너 알아서 해주므로 쓸데없는 버그나 난해한 과제들에 골치 아파질 이유가 없어진다. 당신은 컨테이너의 기능을 이용하고 사용 중 오류가 있으면 일단 무조건 개발사 탓을 하면 되므로 회사에서 짤릴 가능성까지 어느 정도 낮아진 셈이다!

내친 김에 당신이 필자와 같은 初뉴비라면 임기응변을 위해서라도 반드시 스프링의 개발자와 자바의 개발자 정도가 누군지는 알아두어야 한다. 이런걸 미리미리 알아둬야 나중에 직장 상사가 괜한 버그로 트집을 잡을 때 무조건 이 사람들 탓으로 돌릴 수 있게 된다.

자바의 아버지, 제임스 고슬링 - 선 마이크로시스템즈라는 회사와 함께 자바라는 언어를 탄생시키는데 혁혁한 공을 세운 인물이며 SUN이 오라클로 인수된 뒤 얼마 지나지 않아 회사를 퇴임하고 현재 구글에서 근무 중이다. 개인적으로 C와 UNIX를 만든 데니스 리치 다음으로 훌륭한 프로그래머라고 생각한다.
스프링 혁명가, 로드 존슨 - 프로그래밍 계의 락스타같은 존재라 할 수 있다. 그가 한 유명한 말로 "항상 프레임워크 기반으로 접근하라"라는 말이 있는데 이 말은 곧 당신이 한 클래스에서 DB에 넣고 빼는 등 온갖 짓거리로 코드를 짜고 있다면 당신이 어디가서 프로그래밍의 프자도 꺼내서는 안된다는 말이다.

당신은 이 서블릿 클래스가 몇시몇분몇초에 호출될지 정확히 알고 있는가?

다시 본론으로 돌아와 스프링과 더불어 대표적인 컨테이너를 예로 들자면 톰캣과 같은 WAS(Web Application Service)를 들 수 있다. 우리가 열심히 작성한 서블릿을 써줄지 말지는 톰캣이 결정하므로 당연히 컨테이너 아닌가. 혹여나 그래도 이해가 가지 않는다면… 그냥 당신이 쓰고 있는 코드가 도통 언제 호출될지도 감이 안오는 막막한 상태라면 올바르게 컨테이너를 이용하고 있다고 하자. 그런 의미에서 컨테이너는 당신의 사수나 수석 프로그래머라고 할 수 있다. 내가 쓴 코드를 그 사람들이 써줄지 말지는 모르는 일이니 말이다.


IoC/DI란?

IoC/DI란 Inversion of Control과 Dependency Injection의 줄임말이다. 한글로 번역하면 제어의 역전, 의존적 주입이라고 하는데 목에 힘을 좀 빼고 솔직히 말하자면 살다살다 한글로 번역까지 했는데 이해 못할 말이 튀어나올 줄은 몰랐다. 그래서 나름 좀… 쪽팔려도 뉴비의 표현으로 바꾸자면 "대신 해줌(IoC)"과 "미리 찜해 놓음(DI)"라고 잠시 명칭을 정정하도록 하겠다.

일단 IoC란 무엇인가? 당신이 위의 컨테이너의 설명글을 꼼꼼히 읽어왔다면 IoC는 지금 당신이 생각하고 있는게 맞다. 바로 컨테이너다. 컨테이너는 당신이 작성한 코드 컨트롤(객체의 생성과 소멸)을 대신 해주는데 IoC덕분에 우리는 미친 사람마냥 언제 호출될지도 모르는 코드를 마음껏! 칠 수 있게 되었다.

사실 IoC는 스프링만의 코딩방식은 아니고 다른 프레임워크에서도 사용되는 방식인데 굳이 스프링만 우리는 IoC 방식이라고 목이 힘주어 강조하는 이유는 스프링은 정말 철저하게 IoC 방식을 써먹었다는 것이다. 그러므로 우리가 여기서 얻을 수 있는 결론은 어디가서 스프링 좀 안다고 IoC가 스프링만의 고유한 방식이라고 떠벌리면 안된다는 것일테다.

그렇다면 DI란 또 무엇인가? 이것은 마치 당신이 받아먹기도 전에 미리 받아먹겠노라고 선언하는…, 주는 놈은 생각도 않았는데 먼저 말부터 꺼내놓는 파렴치한 코딩방식이라고 할 수 있다. 예를 들어 당신이 조촐한 회식으로 사장님과 중국집을 갔는데 당당히 "저는 짜장면 안먹고 양장피 먹겠습니다."라고 선언한다면 당신은 DI개념이 아주 확실한 프로그래머인 셈이다.

게다가 사실 DI 마저도 스프링만의 고유한 방식은 아니라는 거다. DI란 전설과도 같은 프로그래밍 예언서 GoF 중에서 가장 유명한 전략패턴을 일종의 프레임워크 처럼 편리하게 사용할 수 있도록 만든 것이라 할 수 있는데 스프링은 이런 전설로만 알아오던 전략패턴을 뉴비마저도 산소 호흡하듯 능수능란하게 사용하게끔 편리화하였다.

좀 더 구체적으로 DI에 대해 말하자면, DI방식으로 코드를 작성한다면 현재 사용하고 있는 객체의 메서드가 어떻게 작성되었는지 알 필요가 없으며 솔직히 그 클래스가 기능을 잘 구현했는지 조차도 알 필요가 없다. 그저 당신이 확인할 사항은 클래스의 기능을 추상적으로 묶어둔 인터페이스만 갖다 쓰면 그만이다. 나머지는 스프링에서 찜한 걸 연결시켜줄테니 말이다.

아래 그림은 위의 글로만 써놓은 전략패턴과 DI에 대해 정말 간단히 예제로 만든 코드이다.

 

먼저 말했듯 DI는 스프링만 가지고 있는 고유의 기능이 아니므로 자바의 오브젝트 단위에서도 DI의 구현이 가능하다. 먼저 StrategyPatternService 클래스를 보면 sp라는 객체는 인터페이스 객체인 StrategyPattern로 선언되었으며 StrategyPattern의 메서드인 dependency_injection()를 호출하고 있긴 하다. 그러나 객체 생성 과정에서 객체 sp에 StrategyPatternImpl 클래스를 주입시켜버림으로써 결과적으로 객체 sp는 StrategyPatternImpl의 dependency_injection()이란 메서드를 호출해버리고 말았다는 것이다.

그러므로 위에서 말했듯이 당신은 인터페이스의 메서드만 이용하더라도 위와같이 구현부는 나중에 주입을 통해 해결하므로 획기적인 분업과 동시에 구현 클래스를 쉽게 교체할 수 있다는 장점을 얻게 된다. 물론 스프링은 위의 이미지와 같이 단순한 방식으로 주입하진 않는다. 주입 방식에만 클래스 형태와 XML형태 2가지가 있으며 그 세부적으로도 어마어마하게 많은 방식으로 DI를 할 수 있다.

가끔 스프링을 쓰다보면 이런 다양한 방식이 진짜 편리와 우수한 기능을 위해서인지… 아니면 일부러 사용자를 헷갈리게 하려는 건지 분간이 안갈 때가 종종 있다. (개인적으로 로드 존슨이 프로그래머들을 편집증으로 몰아가기 위해 꾸민 음모가 아닌가 싶다…)

여하튼 IoC/DI의 개념을 요약하자면 정신나간듯 언제 사용될 지도 모르는 코드를 쳐대면서(IoC) 동시에 사용하고 있는 코드가 뭔지도 모르면서 일단 갖다 쓰는(DI) 획기적이고 진보적인 프로그래밍 작성방식이라 이해하면 되겠다.

출처 http://egloos.zum.com/springmvc/v/487497






■ 스프링 컨테이너 생명주기


스프링 컨테이너 생명 주기는 다음과 같다.



스프링 컨테이너 생성

AbstractApplicationContext ctx = new  GenericXmlApplicationContext(); 


스프링 컨테이너 설정 ( xml파일은 수정할 때마다 톰캣이 새로 파일을 읽어오기 때문에 refresh 필수! )

ctx.load("classpath:applicationCTX.xml");

ctx.refresh();


스프링 컨테이너 사용

Pencil pencil = ctx.getBean("pencil", Pencil.class);


스프링 컨테이너 종료

ctx.close();



여태까지 우리는 AbstractApplicationContext ctx = new  GenericXmlApplicationContext("classpath:applicationCTX.xml"); 

이렇게 선언했다. 이 코드는 스프링 컨테이너를 생성과 동시에 설정하는 방식이다.



그럼 이제 스프링 빈 생명주기에 대해 알아보자.



빈의 생명주기를 제어하는 방식은 3가지가 있다.


1) InitializingBeanDisposableBean 생명주기 인터페이스를 구현하고 초기화와 소멸작업을 위한 afterPropertiesSet() destroy() 메소드를 구현 (콜백함수)

  -> 이러한 스프링에 종속된 인터페이스를 구현하면 빈이 스프링에 종속되므로 스프링 IoC 컨테이너 외부에서 재사용하지 못한다.


2) @PostConstruct@PreDestroy 생명주기 어노테이션을 사용하여 초기화 및 소멸 콜백 메소드를 적용할 수 있다. 

( 이 콜백 메소드를 호출하기 위해서는 IoC컨테이너에 CommonAnnotationBeanPostProcessor 인스턴스가 등록되어 있어야 한다? )


3) xml에서 Bean선언시 init-method와 destroy-method 속성을 설정해 콜백 메소드의 이름을 지정한다.


 


다음 예제는 위의 1,2번을 이용하여 빈의 생명주기를 제어하는 방식을 보여주는 예제이다. Student.java는 InitializingBean, DisposableBean을 implements해서 구현한 방식이고 AnnotationStudent.java는 Annotation을 사용해서 구현한 방식이다. 


△ 예제 )

src/main/java

- com/student/ex

-MainClass.java

-Student.java

-AnnotationStudent.java

- com/main/resources

-applicationCTX.xml

 



[ Student ]

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
package com.student.ex;
 
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
 
public class Student implements InitializingBean, DisposableBean{
    private String name;
    private int age;
    
    public Student(String name, int age){
        this.name = name;
        this.age = age;
    }
 
    public String getName() {                    return name;                   }
    public void setName(String name) {        this.name = name;                 }
    public int getAge() {                            return age;                }
    public void setAge(int age) {                this.age = age;                }
 
    @Override
    public void destroy() throws Exception {
        // TODO Auto-generated method stub
        System.out.println("destroy()");
    }
    @Override
    public void afterPropertiesSet() throws Exception {
        // TODO Auto-generated method stub
        System.out.println("afterPropertiesSet()");
    }
}
cs


 

[ AnnotationStudent ]

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 com.student.ex;
 
import javax.annotation.*;
 
public class AnnotationStudent {
 
    private String name;
    private int age;
    
    public AnnotationStudent(String name, int age){
        this.name = name;
        this.age = age;
    }
 
    public String getName() {        return name;                              }
    public void setName(String name) {        this.name = name;                }
    public int getAge() {        return age;                                   }
    public void setAge(int age) {        this.age = age;                       }
    
    @PostConstruct
    public void initMethod(){
        System.out.println("initMethod()");
    }
    @PreDestroy
    public void destroyMethod(){
        System.out.println("destroyMethod()");
    }
}
cs

 

 

[ MainClass ]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.student.ex;
 
import org.springframework.context.support.GenericXmlApplicationContext;
 
public class MainClass {
 
    public static void main(String[] args) {
        GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
        
        ctx.load("classpath:applicationCTX.xml");
        ctx.refresh();   //xml파일을 수정하면 톰캣이 새로 파일을 읽어와야하기 때문에 refresh사용
        ctx.close();
    }
}
cs


[ applicationCTX ]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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.2.xsd">
 
    <context:annotation-config></context:annotation-config>
    
    <bean id="student" class="com.student.ex.Student">
        <constructor-arg value="홍길순"></constructor-arg>
        <constructor-arg value="30"></constructor-arg>
    </bean>
 
    <bean id="annotationstudent" class="com.student.ex.AnnotationStudent">
        <constructor-arg value="홍길자"></constructor-arg>
        <constructor-arg value="40"></constructor-arg>
    </bean>
</beans>
cs

 

applicationCTX에서 context:annotation-config를 추가한 부분은 AnnotationStudent를 연결해 주기 위해 있는 코드야!



출력 결과 및 분석 


afterPropertiesSet()

initMethod()


destroyMethod()

destroy()



콜백함수란? 시스템에서 내부적으로 어떤 이벤트가 발생할때 자동으로 호출해주는 함수

- Student 객체에서 InitializingBean, DisposableBean를 구현했기 때문에 콜백함수 afterPropertiesSet(), initMethod()가 자동으로 호출된것! 

- AnnotationStudent 객체에서 생명주기 어노테이션 @PostConstruct ,  @PreDestroy을 구현했기 때문에 initMethod()와 destroy()메소드를 호출 ( 어노테이션으로 찾아가기때문에 메소드 이름이 initMethod()가 아니더라도 호출됨 )









■ 스프링 빈 범위(Scope)

 


스프링에서 빈 범위는 총 6가지가 있다. 기본적으로 지정을 안해주면 singleton으로 지정이된다.

1) singleton

2) prototype

3) request

4) session

5) page

6) application


1,2번은 빈즈 기반, 3~6번은 웹 기반 scope이다.

여기서 singleton과 prototype의 차이점!!



다음과 같은 예제에서 bean설정을 해주는 xml파일을 주목하자!


△ 예제 )

src/main/java

- com/scope/ex

-MainClass.java

-Student.java

- com/main/resources

-applicationCTX2.xml



이 예제에서 보면 MainClass에서 Student객체의 레퍼런스 student1과 student2는 applicationCTX2.xml에서 getBean으로 받아오고 있고,  applicationCTX2.xml를 보면 Bean을 한번만 설정하고 있어서 MainClass에서 받아오는 student1과 student2는 구조적으로 하나의 객체를 공유하고 있다.


[ Student ]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.scope.ex;
 
public class Student {
    private String name;
    private int age;
    
    public Student(String name, int age){
        this.name = name;
        this.age = age;
    }
    public String getName() {                    return name;                     }
    public void setName(String name) {        this.name = name;                     }
    public int getAge() {                            return age;                    }
    public void setAge(int age) {                this.age = age;                    }
}
cs


[ MainClass ]

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
package com.scope.ex;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
 
public class MainClass {
    public static void main(String[] args) {
 
        AbstractApplicationContext ctx = new  GenericXmlApplicationContext("classpath:applicationCTX2.xml");
        
        Student student1 = ctx.getBean("student", Student.class);
        System.out.println("이름 : "+student1.getName());
        System.out.println("나이 : "+student1.getAge());
        System.out.println("===============================");
        
        Student student2 = ctx.getBean("student", Student.class);
        student2.setName("홍길자");
        student2.setAge(100);
        
        System.out.println("이름 : "+student2.getName());
        System.out.println("나이 : "+student2.getAge());
        System.out.println("===============================");
        
        if(student1.equals(student2)){
            System.out.println("student1 == student2");
        }else{
            System.out.println("student1 != student2");
        }
        ctx.close();
    }
}
cs


[ applicationCTX2 ]

1
2
3
4
5
6
7
8
9
10
11
<?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="student" class="com.scope.ex.Student" scope="prototype">
        <constructor-arg value="홍길순"></constructor-arg>
        <constructor-arg value="30"></constructor-arg>
    </bean>
 
</beans>
cs




위의 코드의 결과는 아래와 같다.




왜 이런결과가 나올까?


xml에서 Bean은 객체를 한번만 가져왔고, MainClass에서 이 하나의 Bean을 student1, student2로 레퍼런스 두개를 생성해서 하나의 객체를 참조했는데 왜 student1.equals(student2)의 결과가 student != student2가 나올까? 하나의 객체인데?


정답은 applicationCTX2의 6번 줄에서 bean의 scope를 지정하는 부분!! 이부분이 핵심이다 !!!!


위에서 언급했듯이 scope를 지정하지 않으면 기본적으로 singleton으로 지정되지만, 위의 코드에서는 prototype으로 지정했다. 여기서 prototype과 singleton의 차이점을 알 수 있다.





자바에서 


Student stu1 = new Student();

Student stu2 = new Student();


if(stu1.equals(stu2)){

System.out.println(true);

}else{

System.out.println(false);

}



위의 코드는 false를 반환한다. 객체 student가 두개 생성되어 stu1, stu2는 각각의 객체를 참조한다.

이러한 방식은 prototype이다.


Student stu1 = new Student();

Student stu2 = stu1;


if(stu1.equals(stu2)){

System.out.println(true);

}else{

System.out.println(false);

}



하지만 위의 코드는 객체 student가 하나만 생성되어 stu1, stu2가 이 하나의 객체를 참조하기 때문에 true를 반환한다.

이러한 방식이 바로 singleton이다.





따라서 위의 예제에서 scope="singleton"으로 지정해주거나 또는 아예 scope를 지정해주지 않았다면 처음 우리가 예상했던 것처럼 하나의 객체를 공유하기 때문에 student1 == student2 결과가 출력되었을 것이다. 


하지만 scope을 prototype으로 지정해줌으로써 applicationCTX2.xml에서 bean을 하나만 지정해주었지만 객체는 두개가 생성되었기 때문에 

student1 != student2라는 결과가 나온 것이다.







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

Spring MVC(1)  (0) 2016.05.16
Spring 한글처리  (0) 2016.05.16
Spring(4)_Environment,Properties  (0) 2016.05.13
Spring(2)  (0) 2016.05.11
Spring(1)  (0) 2016.05.10