부스트코스 강의를 듣고 정리한 내용.
Spring Core
Spring이란
Spring Framework란?
- 엔터프라이즈급 어플리케이션을 구축할 수 있는 가벼운 솔루션이자 One-Stop-Shop(모든 과정을 한 번에 해결)
- 원하는 부분만 가져다 사용할 수 있도록 모듈화가 잘 돼 있음
- IoC 컨테이너
- 선언적으로 트랜잭션 관리 가능
- 완전한 기능을 갖춘 MVC Framework 제공
- AOP 지원
- 도메인 논리 코드와 쉽게 분리될 수 있는 구조로 되어 있음
Framework 모듈
스프링 프레임워크는 약 20개의 모듈로 구성돼있고, 그 중 필요한 모듈만 가져다 사용할 수 있다.
AOP와 Instrumentation
- spring-AOP: AOP alliance와 호환되는 방법으로 AOP를 지원함
- spring-aspects: AspectJ와의 integration제공
- spring-instrument: instrumentation을 지원하는 클래스와 특정 WAS에서 사용하는 클래스로 더 구현체를 제공. 참고로 BCI(Byte Code Instrumentations)은 런타임이나 로드(Load) 때 클래스의 바이트 코드에 변경을 가하는 방법을 말한다.
Messaging
- spring-messaging: 스프링 프레임워크 4는 메시지 기반 어플리케이션을 작성할 수 있는 Message, MessageChannel, MessageHandler 등을 제공한다. 또한, 해당 모듈에는 메소드에 메시지를 메핑하기 위한 어노테이션도 포함되어 있으며 Spring MVC 어노테이션과 유사하다.
Data Access / Integration
- 데이터 엑세스/통합 계층은 JDBC, ORM, OXM, JMS 및 트랜잭션 모듈로 구성되어 있다.
- spring-jdbc: 자바 JDBC프로그래밍을 쉽게 할 수 있도록 기능 제공
- spring-tx: 선언적 트랜잭션 관리를 할 수 있는 기능 제공
- spring-orm: JPA, JDO및 Hibernate를 포함한 ORM API를 위한 통합 레이어 제공
- spring-oxm: JAXB, Castor, XMLBeans, JiBX 및 XStream과 같은 Object/XML 맵핑 지원
- spring-jms: 메시지 생성(producing) 및 사용(consuming)을 위한 기능을 제공, Spring Framework 4.1부터 spring-messaging모듈과의 integration 제공
Web
- 웹 계층은 spring-web, spring-webmvc, spring-websocket, spring-webmvc-portlet 모듈로 구성된다.
- spring-web: 멀티 파트 파일 업로드, 서블릿 리스너 등 웹 지향 통합 기능 제공. HTTP클라이언트와 Spring의 원격 지원을 위한 웹 관련 부분을 제공한다.
- spring-webmvc: Web-Servlet 모듈이라고도 불리며, Spring MVC 및 REST 웹 서비스 구현을 포함한다.
- spring-websocket: 웹 소켓 지원
- spring-webmvc-portlet : 포틀릿 환경에서 사용할 MVC 구현 제공
Spring IoC/DI 컨테이너
Container
컨테이너는 인스턴스의 life cycle을 관리하고, 생성된 인스턴스에게 추가적인 기능을 제공한다.
예를 들어 서블릿을 실행해주는 WAS는 Servlet 컨테이너를 가지고 있다고 말한다. WAS는 웹 브라우저로부터 서블릿 URL에 해당하는 요청을 받으면, 서블릿을 메모리에 올린 후 실행한다. 개발자가 서블릿 클래스를 작성했지만, 실제로 메모리에 올리고 실행하는 것은 WAS가 가지고 있는 Servlet 컨테이너인 것이다.
서블릿 컨테이너는 동일한 서블릿에 해당하는 요청을 받으면, 또 메모리에 올리지 않고 기존에 메모리에 올라간 서블릿을 실행하여 그 결과를 웹 브라우저에게 전달한다.
IoC (Inversion of Control)
컨테이너가 코드 대신 오브젝트의 제어권을 갖고 있어 IoC이라 부른다.
우리가 서블릿을 사용할 때, 서블릿 클래스는 개발자가 만들지만 그 서블릿의 메소드를 알맞게 호출하는 것은 WAS이다. 이렇게 개발자가 만든 어떤 클래스나 메소드를 다른 프로그램이 대신 실행해주는 것을 IoC라 한다.
DI (Dependency Integration)
DI란 클래스 사이의 의존 관계를 빈(Bean) 설정 정보를 바탕으로 컨테이너가 자동으로 연결해주는 것을 말한다.
DI가 적용되지 않은 예
class 엔진 {
}
class 자동차 {
엔진 v5 = new 엔진();
}
Spring에서 DI가 적용된 예
엔진 type의 v5변수에 아직 인스턴스가 할당되지 않았고, 컨테이너가 v5변수에 인스턴스를 할당해주게 된다.
@Component
class 엔진 {
}
@Component
class 자동차 {
@Autowired
엔진 v5;
}
Spring에서 제공하는 IoC/DI 컨테이너
- BeanFactory: IoC/DI에 대한 기본 기능을 가지고 있다.
- ApplicationContext: BeanFactory의 모든 기능을 포함하며, 일반적으로 BeanFactory보다 추천된다. 트랜잭션처리, AOP등에 대한 처리를 할 수 있다. BeanPostProcessor, BeanFactoryPostProcessor등을 자동으로 등록하고, 국제화 처리, 어플리케이션 이벤트 등을 처리할 수 있다.
- BeanPostProcessor: 컨테이너의 기본로직을 오버라이딩하여 인스턴스화와 의존성 처리 로직 등을 개발자가 원하는 대로 구현할 수 있도록 해준다.
- BeanFactoryPostProcessor: 설정된 메타데이터를 커스터마이징 할 수 있다.
xml 파일을 이용한 설정
Maven으로 프로젝트를 생성하고 XML 형식의 설정 파일을 만들어 IoC/DI 컨테이너에 대한 동작을 확인해보자.
이클립스에서 Artifact Id가 diexam01인 메이븐 프로젝트를 하나 생성한다.
pom.xml 파일에 JDK를 사용하기 위한 플러그인 설정을 추가한다. <build>
부분을 아래 코드처럼 수정하면 된다.
<build>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
설정이 변경되었으니 메이븐 프로젝트를 업데이트 해주고, 자바 버전이 제대로 변경되었는지 Properties > Java Compiler에서 JDK 버전을 확인한다. 1.8이면 제대로 된 거임
이제 DI를 테스트해볼 건데, DI는 쉽게 말하자면 내가 원하는 객체를 내가 생성하는 것이 아니라 Spring에서 제공하는 공장이 만들어서 나한테 주입시켜 주는 것이다. 그래서 공장이 자동으로 만들어줄 객체가 필요한데, 그런 객체를 bean이라 부른다. 예전에는 visual한 컴포넌트를 bean이라 불렀으나 요즘에는 일반적인 자바 클래스를 의미한다.
그럼 src > main> java > kr.or.connect.diexam01 안에 UserBean이라는 클래스를 생성해보자.
bean은 1)기본 생성자를 가지고, 2)field는 private하게 선언하고 3)getter와 setter를 가져야 한다.
그럼 이 클래스를 Spring이 가진 공장이 만들어내게 하기 위해 Spring 프레임워크를 사용할 것이므로, pom.xml에 dependency 정보를 추가한다.
그 전에 <properties>
에 아래의 설정을 넣어준다.
<spring.version> 4.3.14.RELEASE</spring.version>
<properties>
안의 값들은 상수처럼 사용할 수 있는 것들이다. <dependency>
나 pom.xml 안에서 해당 값들이 필요할 때 상수처럼 사용할 수 있도록 해주는 것이다.
그리고 스프링 라이브러리와 관련된 디펜던시를 추가한다.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
그리고 설정 파일들을 담을 resources 디렉토리를 src > main 안에 만들어주자.
resources 안에 application.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="userBean" class="kr.or.connect.diexam01.UserBean"></bean>
</beans>
xml 파일로 Spring 설정 파일을 만들 때 가장 바깥쪽 태그를 root element라 부르는데, root element는 반드시 beans
여야 한다.
그리고 이 설정파일을 읽어들일 스프링 컨테이너에게 정보를 줘야 하는데 이때 사용되는 element가 <bean>
element이다.
bean에는 id와 클래스 등의 속성을 넣어준다. 이렇게 하면 new()
로 객체를 생성하는 거랑 같은 의미임..!
스프링 컨테이너는 이런 객체를 하나만 생성해 갖고있다(싱글톤).
이제 스프링이 가진 공장을 만드는 코드인 ApplicationContextExam01.java를 작성한다.
package kr.or.connect.diexam01;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class ApplicationContextExam01 {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
System.out.println("초기화 완료");
UserBean userBean = (UserBean)ac.getBean("userBean");
userBean.setName("kim");
System.out.println(userBean.getName());
UserBean userBean2 = (UserBean)ac.getBean("userBean");
if(userBean == userBean2) System.out.println("같은 인스턴스입니다."); // singleton
}
}
ApplicationContext는 객체를 생성할 때 싱글톤 패턴을 이용한다. 그래서 사용자가 계속 getBean()으로 요청을 하더라도 객체들이 계속 생성되는 것이 아니라 만들어진 하나의 bean을 계속 이용한다.
이번에는 DI를 확인해보자. 일단 Car라는 클래스와 Engine 클래스를 작성한다.
Engine.java
package kr.or.connect.diexam01;
public class Engine {
public Engine() {
System.out.println("Engine 생성자");
}
public void exec() {
System.out.println("엔진이 동작합니다.");
}
}
Car.java
package kr.or.connect.diexam01;
public class Car {
private Engine v8;
public Car() {
System.out.println("Car 생성자");
}
public void setEngine(Engine e) {
this.v8 = e;
}
public void run() {
System.out.println("엔진을 이용하여 달립니다.");
this.v8.exec();
}
}
c.run()
을 호출하기 위해서는 메인함수에서 Engine과 Car 객체를 각각 new로 생성한다. 지금까지는 이렇게 해왔으나 이제부터는 객체를 생성하는 과정을 Spring IoC 컨테이너가 대신 해 줄 것이다.
이 과정을 Spring 컨테이너가 하게 하려면 설정 파일에 해당 Bean들을 등록해야 한다. 따라서 resource 폴더 내의 applicationConfig.xml 파일 안에 bean을 하나 등록한다.
<bean id="e" class="kr.or.connect.diexam01.Engine" />
<bean id="c" class="kr.or.connect.diexam01.Car">
</bean>
Engine과 Car에 대한 bean을 등록함으로서 Engine과 Car의 인스턴스가 싱글톤으로 생성되었다. 하지만 이 코드에서 Car 인스턴스의 엔진을 set하려면 코드의 수정이 필요하다.
<bean id="e" class="kr.or.connect.diexam01.Engine" />
<bean id="c" class="kr.or.connect.diexam01.Car">
<property name="engine" ref="e"></property>
</bean>
bean 안에 property
라는 element를 사용한다. name
은 property의 이름, ref
는 참고할 bean의 id를 의미한다. property는 getter나 setter를 의미하는데, bean 태그 안에서는 모두 값을 설정하는 것이므로 setEngine()을 의미하는 것이고, setEngine()을 파라미터로 Engine 타입을 받으므로 ref에 e를 넣어주었다. 그래서
Engine e = new Engine();
Car c = new Car();
c.setEngine(e);
까지의 코드를 실행한 것이라 보면 된다.
위 예제를 실행하는 ApplicationContextExam02.java는 아래와 같다.
package kr.or.connect.diexam01;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class ApplicationContext02 {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
Car car = (Car)ac.getBean("c");
car.run();
}
}
DI를 사용했을 때의 장점은 무엇일까? 바로 위 코드에서 Engine이란 클래스가 사용되지 않는다. 즉, 사용자는 자신이 직접 사용할 Car 클래스만 알고 있으면 된다.
Spring이 버전업되면서 xml보다는 어노테이션과 Java Config를 함께 사용해 설정하는 방법이 더 많이 이용되고 있다.
Java Config를 이용한 설정
이번에는 Java Config로 의존성 주입을 설정해보자. 먼저 ApplicationConfig.java라는 클래스를 생성한다.
가장 먼저 할 일은 @Configuration
어노테이션을 사용해 config 파일임을 알려주는 것이다.
@Configuration
public class ApplicationConfig {}
다음은 bean을 등록할 건데, bean은 @Bean
이라는 어노테이션을 사용한다.
@Bean
public Car car(Engine e) {
Car c = new Car();
c.setEngine(e);
return c;
}
@Bean
public Engine engine() {
return new Engine();
}
Spring은 설정을 위해 다양한 어노테이션을 제공한다.
@Configuration
: 스프링 설정 클래스를 선언하는 어노테이션@Bean
: bean을 정의하는 어노테이션@ComponentScan
:@Controller
,@Service
,@Repository
,@Component
어노테이션이 붙은 클래스를 찾아 컨테이너에 등록@Component
: 컴포넌트 스캔의 대상이 되는 어노테이션 중 하나로써 주로 util이나 기타 지원 클래스에 붙이는 어노테이션@Autowired
: 주입 대상이 되는 bean을 컨테이너에서 찾아 주입하는 어노테이션
ApplicationContext중에서 AnnotationConfigApplicationContext
는 JavaConfig클래스를 읽어들여 IoC와 DI를 적용한다. 이때 AnnotationConfigApplicationContext는 설정파일 중에 @Bean
이 붙어 있는 메소드들을 자동으로 실행하여 그 결과로 리턴하는 객체들을 기본적으로 싱글톤으로 관리한다.
그럼 이제 설정 파일을 읽어 실행시켜주는 자바 파일을 만들어보자.
public class ApplicationContextExam03 {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(ApplicationConfig.class);
Car car = (Car)ac.getBean(Car.class);
car.run();
}
}
파라미터로 요청하는 class 타입으로 지정할 수도 있다.
다시 돌아와 ApplicationConfig.java를 보면, car 메소드에서는 파라미터를 받아들이지 않는 Car 객체를 먼저 생성한다. ApplicationContext는 파라미터를 받아들이지 않는 bean 생성 메소드를 먼저 다 실행해 반환받은 객체를 관리한다. 그리고 나서 파라미터에 생성된 객체들과 같은 타입이 있는 객체가 있을 경우 파라미터로 전달해 객체를 생성한다. 이렇게 전달받은 파라미터를 Car의 setEngine()에 파라미터로 설정해 리턴한다.
어노테이션을 좀 더 활용하면 이런 설정을 보다 간단하게 만들 수 있다. 새로운 ApplicationConfig를 작성해보자.
@Configuration
@ComponentScan("kr.or.connect.diexam01")
public class ApplicationConfig2 {}
이번에는 @Configuration
어노테이션과 함께 @ComponentScan
어노테이션도 작성한다. @Controller
, @Service
, @Repository
, @Component
어노테이션이 붙은 클래스를 찾아 컨테이너에 알아서 등록하라는 어노테이션이다. @ComponentScan
을 수행할 때는 component scan을 할 패키지의 이름을 알려줘야 한다. 그리고 Car 클래스와 Engine 클래스 위에 @Component
를 붙이면 된다.
@Component
public class Car { ... }
그리고 Car의 setEngine() 메소드를 삭제하고, Engine v8에 @Autowired
라는 어노테이션을 붙인다. @Autowired
는 주입 대상이 되는 bean을 컨테이너에서 찾아 주입하는 어노테이션이다. @Autowired
가 알아서 해주므로 setter 메소드는 더이상 필요가 없다.