[Spring] 스프링 컨테이너(Spring Container) feat. 제어의 역행
안녕하세요 jju_developer입니다.
지난 시간에 이어 Spring 컨테이너에 대해서 좀 더 자세히 알아보겠습니다.
▼▼▼ 스프링 기초 ▼▼▼
[Spring] 스프링 시작하기- 기초 설명 및 스프링 프로젝트 파일 만들기
안녕하세요 jju_developer입니다. 오늘부터 2주간 스프링에 대해서 배운 부분을 공유드리려고 합니다. 첫 번째 주는 Spring Legacy 두 번째 주는 Spring 부트에 대해서 배울 예정입니다. 1. 스프링(Spring)이
jju240.tistory.com
지난 시간에 스프링 프레임 워크의 특징에 대해서 알아봤는데요!
스프링 프레임워크는 줄여서 4가지 특징이 있습니다.
1. 경량
2, 제어의 역행
3. 관점지향
4. 컨테이너
이 특징 중, 컨테이너에 대해 더욱 자세히 살펴보겠습니다.
스프링은 스프링 컨테이너를 통해 객체를 관리하는데, 스프링 컨테이너에서 관리되는 객체를 빈(Bean)이라고 합니다.
이번에는 스프링 컨테이너(Spring Container)에 대해 알아보도록 하겠습니다.
JSP 배울 때 아파치 Tomcat을 이용하여 서블릿 컨테이너를 사용한 것처럼,
스프링도 마찬가지로 스프링 컨테이너에 특정 객체를 미리 만들어 놓고 이 컨테이너에서 각각의 맞는
객체를 찾아서 변수에 주입시켜 주는 것입니다.
스프링 컨테이너란?
스프링 컨테이너는 자바 객체의 생명 주기를 관리하며, 생성된 자바 객체들에게 추가적인 기능을 제공하는 역할을 합니다. 여기서 말하는 자바 객체를 스프링에서는 빈(Bean)이라고 부릅니다.
그리고 저번 시간에 배웠던 IoC와 DI의 원리가 이 스프링 컨테이너에 적용됩니다.
소프트웨어 개발에서 유지보수 측에서 비용을 줄이는 방법이 중요합니다.
개발자는 new 연산자, 인터페이스 호출, 팩토리 호출 방식으로 객체를 생성하고 소멸시킬 수 있는데,
스프링 컨테이너가 이 역할을 대신해 줍니다.
즉, 제어 흐름을 외부에서 관리하는 것이죠.(=제어의 역행)
또한, 객체들 간의 의존 관계를 스프링 컨테이너가 런타임 과정에서 알아서 만들어 줍니다.
[정리]
- 자바 객체를 담고 있다.
- 언제든지 스프링 컨테이너로 부터 필요한 객체를 가져와 사용할 수 있다.
- Bean들의 생명주기를 관리
- Spring Container는 애플리케이션을 구성하는 Bean들을 관리하기 위해 IoC를 사용
이름 | 설명 |
IoC(Inversion of Control) | 개발자는 New 연산자, 인터페이스 호출, 팩토리 호출방식으로 객체를 생성하고 소멸시킨다. IoC란 인스턴스의 생성부터 소멸까지의 객체 생명주기 관리를 개발자가하는 대신 스프링(컨테이너)가 관리 |
DI(Dependency Injection) | IoC를 실제로 구현하는 방법으로서 의존성있는 컴포넌트들 간의 관계를 개발자가 직접 코드로 명시하지 않고 컨테이너인 Spring이 런타임에 찾아서 연결해주게 하는 것 |
스프링에서 IOC 컨테이너 구현방법
🌷스프링 컨테이너의 동작 순서🌷
1. TVUser 클라이언트가 스프링 설정 파일을 로딩하여 컨테이너 구동
2. 스프링 설정 파일에 <bean> 등록된 SamsungTV 객체 생성
3. getBean() 메서드로 이름이 'tv'인 객체를 요청(Lookup)
4. SamsungTV 객체 반환
스프링 컨테이너 두 종류
1. 빈팩토리 BeanFactory
- 빈을 등록하고 생성하고 조회하고 돌려주고, 그 외에 부가적인 빈을 관리하는 기능을 담당합니다.
- 빈 팩토리가 빈의 정의는 즉시 로딩하는 반면, 빈 자체가 필요하게 되기 전까지는 인스턴스화를 하지 않습니다.
- getBean()이 호출되면, 팩토리는 의존성 주입을 이용해 빈을 인스턴스화하고 빈의 특성을 설정하기 시작합니다.
여기서 빈의 일생이 시작됩니다. (DI : Dependency Injection 의존성 주입)
getBean()은 Object 타입을 return 합니다. 그래서 객체에 넣을 때 원하는 객체로 형변환을 해줘야 합니다.
예시: TV tv = (TV)factory.getBean("tv");
컨테이너에 tv라는 객체가 저장되어 있고, 가져올 때 TV로 형변환을 해준 것입니다!
2. 애플리케이션 컨텍스트 ApplicationContext
- 빈 팩토리를 상속한, 빈 팩토리를 확장한 향상된 컨테이너입니다.
- 기본적인 기능은 빈 팩토리와 동일하고 스프링이 제공하는 각종 부가 서비스를 추가로 제공합니다.
- 국제화가 지원되는 텍스트 메시지를 관리해 줍니다.
- 이미지 같은 파일 자원을 로드할 수 있는 포괄적인 방법을 제공해 줍니다.
- 리너스로 등록된 빈에게 이벤트 발생을 알려줍니다.
Spring에서는 실제로는 빈의 생성과 관계설정 외에 추가적인 기능이 필요한데, 이러한 이유로 Spring에서는 빈 팩토리를 상속받아 확장한 애플리케이션 컨텍스트 (Application Context)를 주로 사용합니다.
애플리케이션 컨텍스트에는 직접 오브젝트를 생성하고 관계를 맺어주는 코드가 없고, 그런 생성 정보와 연관관계 정보에 대한 설정을 읽어 처리합니다.
예를 들어 @Configuration과 같은 어노테이션이 대표적인 IoC의 설정정보입니다.
[빈 (Bean) 요청 시 처리 과정]
클라이언트에서 해당 빈을 요청하면 애플리케이션 컨텍스트는 다음과 같은 과정을 거쳐 빈을 반환합니다.
1. ApplicationContext는 @Configuration 이 붙은 클래스들을 설정 정보로 등록해 두고,
@Bean 이 붙은 메서드의 이름으로 빈 목록을 생성합니다.
2. 클라이언트가 해당 빈을 요청합니다.
3. ApplicationContext는 자신의 빈 목록에서 요청한 이름이 있는지 찾습니다!
4. ApplicationContext 는 설정 클래스로부터 빈 생성을 요청하고, 생성된 빈을 돌려줍니다.
애플리케이션 컨텍스트는 @Configuration 이 붙은 클래스들을 설정 정보로 등록해 두고,
@Bean 이 붙은 메서드의 이름으로 빈 목록을 생성합니다.
그리고 클라이언트가 해당 빈을 요청한다면 애플리케이션 컨텍스트는 자신의 빈 목록에서 요청한 이름이 있는지 찾고,
있다면 해당 빈 생성 메서드(@Bean)를 호출하여 객체를 생성하고 돌려줍니다.
(구체적으로는 Spring 내부에서 Reflection API를 이용해 빈 정의에 나오는 클래스 이름을 이용하거나 또는 빈 팩토리를 통해 빈을 생성합니다.)
스프링 XML 설정
(1) <beans> 루트 엘리먼트 (2) <import> 엘리먼트 (3) <bean> 엘리먼트 (4) <bean> 엘리먼트 속성 |
(1) <beans> 루트 엘리먼트
스프링 컨테이너는 <bean> 저장소에 해당하는 XML 설정 파일을 참조하여
<bean>의 생명주기를 관리하고 여러 가지 서비스를 제공합니다.
(2) <import> 엘리먼트
스프링 기반의 애플리케이션은 단순한 <bean> 등록 외에도 트랜잭션 관리, 예외 처리, 다국어 처리 등 복잡하고 다양한 설정이 필요합니다.
기능별 여러 XML 파일로 나누어 설정하고 하나로 통합할 때 <import> 엘리먼트를 사용합니다.
(3) <bean> 엘리먼트
스프링 설정 파일에 클래스를 등록하려면 <bean> 엘리먼트를 사용합니다.
(4) <bean> 엘리먼트 속성 (4가지)
- 스프링 설정 파일에 클래스를 등록할 때 사용합니다.
- class 속성은 필수입니다. 패키지 경로가 포함된 전체 클래스 경로를 정확하게 지정해야 합니다.
- id 속성은 객체를 위한 이름을 지정할 때 사용합니다. 속성값에는 CamelCase를 사용합니다.
작성 규칙에 따라 에러가 발생한다!
id와 같은 기능을 하는 name 속성도 있습니다. id와 다르게 다양한 문자열을 허용합니다.
name과 id 속성값은 모두 전체 스프링 파일 내에게 유일해야 합니다.
4-1 init-method 속성
스프링 컨테이너는 설정 파일에 등록된 클래스를 객체 생성할 때 디폴트 생성자를 호출합니다.
객체 생성 후, 멤버변수 초기화 작업이 필요할 때 init-method를 사용합니다.
빈으로 등록된 클래스 객체를 생성한 후 init-method로 지정된 initMethod()를 호출합니다.
• Servlet 컨테이너는 web.xml 파일에 등록된 Servlet 클래스의 객체를 생성할 때 디폴트 생성자만 인식합니다.
따라서 생성자로 Servlet 객체의 멤버변수를 초기화할 수 없습니다.
그래서 서블릿은 init() 메서드를 재정의(Overriding)하여 멤버변수를 초기화합니다.
• 스프링 컨테이너 역시 스프링 설정 파일에 등록된 클래스를 객체 생성할 때 디폴트 생성자를 호출합니다.
따라서 멤버변수 초기화 작업이 필요하다면, <bean> 엘리먼트에 init-method 속성을 사용합니다.
4-2 destoy-method 속성
init-method와 마찬가지로 스프링 컨테이너가 객체를 제거하기 직전에 호출되는 메서드이다.
• <bean> 엘리먼트에서 destroy-method 속성을 이용하여 스프링 컨테이너가 객체를 삭제하기 직전에 호출될 임의의 메서드를 지정할 수 있습니다.
4-3 lazy-init 속성
컨테이너가 구동될 때 설정 파일에 등록된 <bean>들은 생성하는 즉시 로딩(pre-loading) 방식으로 동작합니다.
그러므로 자주 사용 되지 않는 <bean>은 메모리 낭비가 될 수 있습니다.
컨테이너 구동 시점이 아닌 <bean>이 사용되는 시점에 객체를 생성하도록 할 때 사용됩니다.
• 스프링에서는 컨테이너가 구동되는 시점이 아닌 해당 <bean>이 사용되는 시점에 객체를 생성하도록 lazy-init 속성을 제공합니다.
4-4 scope 속성
스프링 컨테이너가 관리하는 빈들은 대부분 단 하나만 생성되어 운용됩니다.
이 개념을 singleton이라고 합니다.
scope의 기본 값은 singleton으로, scope 속성을 생략하는 경우가 일반적입니다.
반대로 스프링 컨테이너로부터 필요한 객체를 요청할 때마다
새로운 객체를 생성할 때는 prototype을 사용할 수 있습니다.
위처럼 자바에서 싱글톤 패턴을 사용하려면, 프라이빗으로 접근을 제한시키고, 프라이빗이기 때문에 getter를 만들어야 하고 자신 타입의 필드에 객체를 생성해 대입하는 등 많은 작업을 해야 합니다.
하나의 객체만 생성하도록 제어하기 위해 싱글톤 패턴(singleton pattern) 사용하나, 싱글톤 패턴을 구현하려면 위처럼
일일이 클래스에 패턴 관련 코드를 작성해야 하므로 매우 귀찮은 일입니다.
• 스프링 컨테이너는 컨테이너가 생성한 bean을 어느 범위에서 사용할 수 있는지 지정할 수 있는데,
이때 scope 속성을 사용합니다.
scope 속성값은 기본이 싱글톤입니다.
이는 해당 bean이 스프링 컨테이너에 의해 단 하나만 생성되어 운용되도록 합니다.
일반적으로 scope은 생략하므로 스프링 컨테이너가 관리하는 <bean>들은 대부분 싱글톤으로 운영됩니다.
• scope 속성을 "prototype"으로 지정하면 해당 <bean>이 요청될 때마다 매번 새로운 객체를 생성하여 반환한다.
여기서 scope를 프로토타입으로 지정하면
이렇게 지정을 하면 tv를 호출할 때마다 매번 새로운 객체가 생성이 되게 됩니다.
이렇게 하지 않고 객체를 싱글톤으로 만들려면,
// applicationContext.xml
<bean id="tv" class="polymorphism.SamsungTV" scope="singleton" />
<bean id="tv" class="polymorphism.SamsungTV" scope="prototype" />
이렇게 스코프에 scope="singleton"으로 지정하면 됩니다.
의존성 주입
(1) 스프링의 의존성 관리 방법
스프링은 IoC를 다음 두 가지 형태로 지원합니다.
• Dependency Lookup: 컨테이너가 애플리케이션 운용에 필요한 객체를 생성하고 클라이언트는 컨테이너가 생성한 객체를 검색하여 사용하는 방식
• Dependency Injection: 객체 사이의 의존관계를 스프링 설정 파일에 등록된 정보를 바탕으로 컨테이너가 자동으로 처리해 주는 방식,
대부분 이 방식을 사용하여 개발합니다.
(2) 의존성 관계
의존성(Dependency) 관계란 객체와 객체의 결합 관계이다. 즉, 하나의 객체에서 다른 객체의 변수나 메서드를 이용해야 한다면 이용하려는 객체에 대한 객체 생성과 객체의 레퍼런스 정보가 필요하다.
[context.xml]
[삼성티비] ->티비의 장착되는 스피커는 뭘 장착할지 안정했으나,
위의 빈 설정파일에 의해 소니 스피커로 장착하였습니다.
[결론]
만약 생성자에 아규먼트가 두 개인 경우에는
// context.xml의 일부
<bean id="tv" class="polymorphism.SamsungTV">
<constructor-arg index="0" ref="sony"></constructor-arg>
<constructor-arg index="1" value="27000000"></constructor-arg>
</bean>
<bean id="sony" class="polymorphism.SonySpeaker"></bean>
<bean id="apple" class="polymorphism.AppleSpeaker"></bean>
이렇게 두 개를 넣어줘야 합니다.
TV를 실행할 때 삼성티비의 가격이 출력이 됩니다.
만약 소니 스피커가 아닌 애플스피커를 넣고 싶으면
context.xml에 해당 부분을 소니스피커가 아닌 애플 스피커로 넣어주시면 됩니다.
이처럼 코드에 직접적으로 무슨 스피커를 장착하겠다고 지정하지 않고
외부에서 유연하게 변경될 수 있도록 설정한 것입니다!!
지금까지는 SamsungTV 객체가 SonySpeaker를 이용하여 동작했지만 유지보수 과정에서 다른 스피커로 교체하는 상황도 발행할 것입니다.
의존성 주입은 이런 상황을 매우 효과적으로 처리해 줍니다.
이러한 과정이 DI과정입니다!
어노테이션 기반 설정 - 최종
(1) Context 네임스페이스 추가
Context 관련 네임스페이스와 스키마 문서의 위치를 등록해야 합니다.
(2) 컴포넌트 스캔(component-scan) 설정
스프링 설정 파일에 애플리케이션에서 사용할 객체들을 <bean> 등록하지 않고 자동으로 생성하려면 <context:component-scan/>이라는 엘리먼트를 정의해야 합니다.
(3) @Component
<context:component-scan>를 설정했으면 이제 스프링 설정 파일에 클래스들을 일일이
<bean> 엘리멘트로 등록할 필요가 없습니다.
@Component만 클래스 선언부 위에 설정하면 됩니다.
// src/main/java/polymorphism/LgTV.java의 일부
package polymorphism;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component("tv")
public class LgTV implements TV {
@Autowired
private Speaker speaker;
public LgTV() {
System.out.println("===> LgTV 객체 생성됨");
}
}
어노테이션 (표기; @) |
설명 |
@Autowired | 변수 위에 설정하여 해당 타입의 객체를 찾아서 자동으로 할당. org.springframework.beans.factory.annotation.Autowired |
@Qualifier | 특정 객체의 이름을 이용하여 의존성을 주입할 때 사용. (DI) org.springframework.beans.factory.annotation.Qualifier |
@Inject | @Autowired 똑같습니다. (Java 제공 어노테이션) - 타입 javax.annotation.Inject |
@Resource | 해당 타입의 객체를 찾아서 자동으로 할당하고, 객체를 이용해 의존성을 주입할 떄 사용. (@Autowired와 @Qualifier의 기능을 결합한것) // 타입 또는 이름으로 주입 DI javax.annotation.Resource |
스피커를 구현한 구현 클래스에 있는 객체를 찾아서 알아서 넣어주는 Autowired 예시입니다.
context.xml에 있는 context 스캔이 폴리모피즘 패키지 중에서 에노테이션 component가 있는 부분을 찾아서
컨테이너에서 자기 타입과 같은 것을 자동으로 주입시키는 것입니다.
이처럼 코드에서 넣는 것이 아니라 에노테이션 @ 을 활용하여 의존성 주입을 할 수 있습니다!!!
결론적으로 대부분 실무나 프로젝트를 진행할 때 필드에 @Autowired를 붙여 쓴 코드가 많습니다.
특히 생성자에 주입하는 경우가 많은데 이는 Spring에서는 수시로 객체 생성이나 의존성 높은 설계를 피하기 위함도 있고, 의존성 주입 관계에서 대부분 변경이 일어나지 않기에 불변성을 보장하며,
순환 참조 문제의 원인을 파악하기도 쉽기 때문입니다.
@Resource를 사용하는 이유?
위 설명들을 보면 @Resource는 빈의 이름을 우선순위로 하여 DI를 하기 때문에
같은 타입의 빈이 두 개 이상일 경우 충돌을 방지하기 위해 사용하는 것입니다.
하지만 @Autowired 만으로도 @Quilfier와 @Primary를 사용한다면 위에서 설명한 @Resource의 이점을 똑같이 사용할 수 있는 게 아닌가 하는 생각이 들 수 있습니다.
추가적으로 @Resource는 스프링에 종속적이지 않기 때문에 스프링 프레임워크를 사용하다가 다른 프레임워크로 변경할 경우에 유리한 점이 있기는 합니다.
우리가 같은 타입의 빈이 수십, 수백 개 이상이 있는 대규모 프로젝트를 진행한다고 가정할 때에
이렇게 같은 타입의 Bean들이 많은 프로젝트를 다루다 보면@Primary 어노테이션으로 우선순위를 정하게 될 경우
개발자 자신도 모르는 사이(단순한 개발자의 실수)에 빈들이 꼬일 수 있습니다.
또한 @Qulifier를 사용할 경우에도 개발자가 직접 Bean의 이름을 지정해줘야 하기 때문에 실수가 있을 수 있습니다.
유지 보수성을 높이기 위해서는 명확성이 중요하기 때문에 @Resource를 사용하는 것이다.
항상 자동으로 객체를 넣는것이 좋은것만은 아니고 그렇기 때문에 섞어서 사용을 하는 것 입니다.
스프링으로 의존성 주입을 처리할 때, XML 설정과 어노테이션 설정은 장단점이 서로 상충합니다.
XML 방식은 자바 소스를 수정하지 않고 XML 파일의 설정만 변경하면 실행되는 Speaker를 교체할 수 있어서 유지보수가 편하지만 XML 설정에 대한 부담 역시 존재합니다.
반대로
어노테이션 기반 설정은 XML 설정에 대한 부담도 없고, 의존관계에 대한 정보가 자바 소스에 들어있어서 사용하기는 편하지만 자바소스를 수정하지 않고 Speaker를 교체할 수 없다는 문제가 생깁니다.
이런 문제를 서로의 장점을 조합하는 것으로 해결할 수 있습니다.
기본적으로 @Autowire를 쓰고 나서 스캔을 하게 된다면
@component에 ("tv") 하나만 있으면 Autowire를 쓰고 티비 객체가 만들어 집니다.
context.xml에 이렇게 지정을 하면 소니 객체는 xml 로 만들고 tv 객체는 에노테이션인 오토로 만든것 입니다.
추가 어노테이션
@Component를 이용하여 스프링 컨테이너가 해당 클래스 객체를 생성하도록 설정할 수 있습니다.
그런데 시스템을 구성하는 모든 클래스에 @Component를 할당하면 어떤 클래스가 어떤 클래스가 어떤 역할을 수행하는지 파악하기 어렵습니다.
개발자가 직접 작성한 Class를 Bean으로 등록하기 위한 어노테이션입니다.
Component를 구체화하여 상속받고 있는 어노테이션이 @Controller, @Service, @Repository 이 있습니다.
모두 동일하게 IoC 컨테이너에 의해 자동으로 생성되어 Bean을 생성하지만,
@Component를 사용하는 것보다 MVC 구성으로 각 역할에 맞는 어노테이션을 사용하는 것을 권장합니다.
그럼 지금까지 스프링 컨테이너에 대한 전체적인 설명이었습니다.
오늘도 수고하셨습니다.