Resource 추상화

스프링의 Resource 객체는 java.net.URL을 추상화한 인터페이스입니다. Resource 객체는 스프링 내부에서 가장 많이 사용이 되는 인터페이스이며 스프링 IoC 컨테이너가 생성 될때, 컨테이너 설정 정보를 담는 파일들을 가져올때도 사용합니다.

Resource 인터페이스를 통해 추상화한 이유 java.net.URL의 한계로 클래스 패스를 기준으로 리소스를 읽어오는 기능이 존재하지 않기 때문입니다.


주요 메소드
  1. exists
  2. isOpen
  3. isFile
  4. isDirectory
  5. getFile(항상 파일로 가져올수 있는 것은 아님)

구현체
  1. UrlResource : URL을 기준으로 리소스를 읽어들이며 기본으로 제공하는 프로토콜에는 http, https, ftp, file, jar
  2. ClassPathResource : 클래스패스를 기준으로 리소스를 읽어들이며 접두어로 classapth:를 사용
  3. FileSystemResource : 파일 시스템을 기준으로 읽어들임
  4. ServletContextResource : 웹 어플리케이션 루트에서 상대경로로 리소스를 읽어들임

ResourceLoader

ResourceLoader는 리소스를 읽어오는 기능을 제공하는 인터페이스 입니다. ApplicationContext도 ResourceLoader를 상속하고 있습니다. 기능은 말그대로 리소스를 읽어오는 기능만 제공하고 있습니다.

구현체
  1. DefaultResourceLoader : UrlResource(경로가 http, https 등 프로토콜로 시작)와 ClassPathResource(경로가 classapth:로 시작)를 가져올때 사용
  2. FileSystemResourceLoader : DefaultResourceLoader를 상속하고 있으며 경로가 /로 시작하는 경우 FileSystemResource를 반환.
  3. GenericWebApplicationContext : DefaultResourceLoader를 상속하고 있으며 경로가 /로 시작하는 경우 ServletContextResource를 반환
    • DefaultResourceLoader를 직접 상속 하고 있지는 않음

리소스를 가져오는 코드는 아래와 같습니다.

1
resourceLoader.getResource("location 문자열");

Resource의 타입과 ApplicationContext 타입의 관계

Resource의 타입은 location 문자열과 ApplicationContext의 타입에 따라 결정됩니다. 위의 ResourceLoader 구현체를 통해서 Resource를 얻어오는게 아니라 bean으로 등록 된 ApplicationContext를 통해서 Resource를 가져올때는 아래와 같이 적용됩니다.

  1. ClassPathXmlApplicationContext => ClassPathResource
  2. FileSystemXmlApplicationContext => FileSystemResource
  3. WebApplicationContext => ServletContextResource

만약 ApplicationContext의 타입과 상관없이 리소스 타입을 강제하고 싶다면 접두어를 사용하면 됩니다.

  1. classpath
  2. file
  3. http

그래서 좀더 명확한 코드를 작성하기 위해 접두어를 사용해서 Resource를 가져오는 것이 좋습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = BeanConfig.class)
public class TestControllerTest {
@Autowired
ResourceLoader resourceLoader;

@Test
public void test() {
System.out.println(resourceLoader.getClass());

Resource resource = resourceLoader.getResource("test.properties");
System.out.println(resource.getClass());

ResourceLoader defulatResourceLoader = new DefaultResourceLoader();
Resource defaultResource = defulatResourceLoader.getResource("classpath:test.properties");
System.out.println(defaultResource.getClass());
}
}
1
2
3
class org.springframework.context.support.GenericApplicationContext
class org.springframework.core.io.DefaultResourceLoader$ClassPathContextResource
class org.springframework.core.io.ClassPathResource

Comment and share

MessageSource

스프링 메시지소는 국제화(i18n)을 제공하는 인터페이스입니다. 메시지 설정 파일을 통해서 각 국가에 해당하는 언어로 메세지를 제공할수 있습니다. ApplicationContext는 MessageSource를 구현하고 있습니다.

메시지 설정 파일

메시지 설정 파일은 프로퍼티파일을 사용하며 파일 이름에 [파일이름][언어][국가].properties 형식으로 파일을 추가해주면 됩니다. 아래와 같이 2개의 파일을 생성하게 되면 인텔리제이에서는 Bundle로 묶이는 것을 확인 할수 있습니다.

1
2
messages.properties : 기본 메시지       
messages_ko_KR.properties: 한국 메시지

파일이름이 messages로 시작하지 않아도 된다. 위의 형식만 맞춰주면 된다. 스프링 부트를 쓸 경우에는 messages로 시작하면 자동으로 등록 해준다.

메시지 가져오기

위와 같은 형식으로 파일을 생성한 후에 프로퍼티 작성 방식인 key=value 형식으로 값을 입력합니다.

1
2
// messages.properties
greeting=Hello, so good {0}
1
2
// messages_ko_KR.properties
greeting=안녕하세요 {0}

ReloadableResourceBundleMessageSource를 Bean으로 등록 해 줍니다. 여기에서 basename은 경로를 포함한 파일이름까지 적어주면 됩니다. 예를 들어 클래스패스에서 common/message-common_ko_KR.properties로 구성할 예정이라면 messageSource.setBasename("classpath:common/message-common") 이렇게 입력해주면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@ComponentScan
public class BeanConfig {

@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
}

위처럼 설정이 완료가 되면 메시지를 가져올 준비가 되었습니다. messageSource.getMessage(“이름”, new String[]{“파라미터1”, “파라미터2..”}, Locale) 순으로 작성해주면 됩니다. 두번째 파라미터인 배열을 넘기면 프로퍼티에서 작성했었던 {0}에 값이 설정이 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = BeanConfig.class)
public class TestControllerTest {

@Autowired
MessageSource messageSource;

@Test
public void test() {
System.out.println(messageSource.getMessage("greeting", new String[]{"1"}, Locale.KOREA));
}
}

메시지소스 리로딩

ReloadableResourceBundleMessageSource는 리로딩 기능을 가지고 있습니다. 프로퍼티의 변경을 감지해서 적용 해주는 기능을 가지고 있습니다. 설정은 bean 생성시에 아래 한줄을 추가 해주면 됩니다.

1
messageSource.setCacheSeconds(60);
xml에서 MessageSource bean 등록 하기
1
2
3
4
5
6
7
8
9
10
11
12
13
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basenames">
<list>
<value>classpath:/messages.properties/message-common</value>
</list>
</property>
<property name="defaultEncoding">
<value>UTF-8</value>
</property>
<property name="cacheSeconds">
<value>60</value>
</property>
</bean>

Comment and share

Environment

ApplicationContext는 EnvironmentCapable 인터페이스를 구현하고 있습니다. 이 인터페이스는 getEnvironment 메소드를 제공하며 호출시 Environment를 반환해 줍니다. Environment 클래스는 프로파일 및 프로퍼티 값과 관련이 있습니다.

프로파일

개발을 하다보면 로컬, 개발, 운영등 각 환경마다 설정을 달리 해주어야 하는 경우가 발생합니다. 이때 각 환경마다 활성화할 Bean을 관리해주는 역할을 하는게 프로파일입니다.

프로파일을 설정하면 앱 구동시에 설정된 프로파일 active 값에 따라서 해당 bean을 등록 할지 여부를 결정하게 됩니다. 예를 들어 A라는 Bean은 테스트시에만 쓰고 싶다면, 해당 Bean을 테스트 프로파일로 구성하면 테스트시에만 활성화 되게 됩니다.

설정 방법

설정 파일에 클래스에 정의를 하면 해당 설정 파일에서 정의한 모든 Bean을 한번에 정의할수 있습니다.

1
2
3
4
5
6
@Configuration
@ComponentScan
@Profile("test")
public class BeanConfig {

}

메소드로 정의하여 개별적으로 정의 할수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

<!-- DevBean.class -->
@Component
public class DevBean {
}

<!-- LocalBean.class -->
@Component
public class LocalBean {
}

<!-- AllBean.class -->
@Component
public class AllBean {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class BeanConfig {

@Bean
@Profile("local")
public LocalBean localBean() {
return new LocalBean();
}

@Bean
@Profile("dev")
public DevBean devBean() {
return new DevBean();
}

@Bean
public AllBean allBean() {
return new AllBean();
}
}

Profile을 설정하지 않은 allBean와 활성화 프로파일로 설정한 dev만 bean이 등록된것을 확인할수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = BeanConfig.class)
@ActiveProfiles("dev")
public class TestControllerTest {

@Autowired
ApplicationContext ctx;

@Test
public void test() {
System.out.println(Arrays.toString(ctx.getBeanDefinitionNames()));
}
}
1
[...,allBean, devBean]]

위의 방법 이외에도 Bean 등록할 클래스에 직접 사용할수도 있습니다.

1
2
3
4
@Component
@Profile("local")
public class LocalBean {
}
프로파일 문자열

지금까지 사용했었던 프로파일 문자열은 사용자가 임의로 만들수 있습니다. 그리고 지금까지는 활성화할 프로파일을 하나만 설정하였지만 연산자를 통해서 한번에 여러개의 프로파일을 활성화할수 있습니다.

!를 문자열 앞에 붙이면 반대의 의미가 됩니다.

1
!dev 는 dev가 아닌 것만 활성화

&는 and의 의미를 가지고 있습니다.

1
abc & def는 abc 이면서 def인것만 활성화

|는 or의 의미를 가지고 있습니다.

1
abc | def는 abc이거나 def인것만 활성화

프로퍼티

프로퍼티는 다양한 방법으로 정의할수 있는 설정값입니다. 프로퍼티는 key=value로 구성이 됩니다.

추가 방법

@PropertySource("classpath:파일위치")로 추가가 가능합니다. XML에서도 프로퍼티 등록이 가능합니다. 되도록 @Configuration이 선언된 클래스에 함께 사용하도록 합시다.

프로퍼티 값 가져오기

등록 된 프로퍼티는 Environment에서 가져올수 있습니다. Environment도 Bean으로 등록 되어 있어 의존 주입 받아 쓰거나, ApplicationContext에서 getEnvironment 메소드로 가져올수 있습니다.

1
2
Environment en = application.getEnvironment();
en.getProperty("key");

Comment and share

싱글톤

스프링에서 Scope에 대한 설정을 하지 않고 Bean 등록을 하게 되면 싱글톤으로 등록이 됩니다. 이 경우에는 하나의 bean을 사용하게 됩니다.

테스트용으로만 사용 할 Library 클래스입니다.

1
2
3
4
@Component
public class Library {

}

동일한지 테스트 코드를 실행해 봅니다. 정상 실행이 되는 것을 확인할수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestControllerTest {

@Test
public void test() {
ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanConfig.class);
Library library1 = (Library) ctx.getBean("library");
Library library2 = (Library) ctx.getBean("library");

System.out.println(library1);
System.out.println(library2);
assertEquals(library1, library2);
}
}
1
2
kr.co.spring.Library@6293abcc
kr.co.spring.Library@6293abcc
프로토타입

프로토타입으로 설정을 하는 경우 사용할때 마다 새로운 객체를 받습니다. 사용하는 방법은 프로토타입으로 사용할 객체 위에 @Scope("prototype")를 추가해주면 됩니다.

1
2
3
4
5
@Component
@Scope("prototype")
public class Library {

}

동일한 테스트 코드를 실행했을때 이번에는 두개의 객체가 서로 다르다를 에러가 발생하엿습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestControllerTest {

@Test
public void test() {
ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanConfig.class);
Library library1 = (Library) ctx.getBean("library");
Library library2 = (Library) ctx.getBean("library");

System.out.println(library1);
System.out.println(library2);
assertEquals(library1, library2);
}
}
1
2
3
4
5
6
kr.co.spring.Library@2133814f
kr.co.spring.Library@4c15e7fd

java.lang.AssertionError:
Expected :kr.co.spring.Library@2133814f
Actual :kr.co.spring.Library@4c15e7fd
싱글톤 타입에서 프로토 타입 의존주입

프로토타입 bean에서 싱글톤타입의 bean을 의존주입 받아서 사용할때는 아무런 문제가 발생하지 않습니다. 하지만 반대의 경우에는 개발자의 의도와는 다른 결과를 발생시킬수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- SingletonBean -->
@Component
public class SingletonBean {
@Autowired
PrototypeBean prototypeBean;

public PrototypeBean getPrototypeBean() {
return prototypeBean;
}
}

<!-- PrototypeBean -->
@Component
@Scope("prototype")
public class PrototypeBean {

}
1
2
3
4
5
6
7
8
9
10
11
public class TestControllerTest {

@Test
public void test() {
ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanConfig.class);
SingletonBean singletonBean = (SingletonBean) ctx.getBean("singletonBean");

System.out.println(singletonBean.getPrototypeBean());
System.out.println(singletonBean.getPrototypeBean());
}
}
1
2
kr.co.spring.PrototypeBean@37918c79
kr.co.spring.PrototypeBean@37918c79

위의 결과를 보았을때 프로토타입으로 Scope를 설정했지만 동일한 객체인것을 확인 할수 있습니다.

해결방법1. 프록시객체를 의존주입하기

@Scope 어노테이션에 프록시 설정을 하여 해당 프로토타입 빈을 감싸는 프록시 객체를 반환하는 방식을 사용하여 해결할수 있습니다.

1
2
3
4
5
@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class PrototypeBean {

}
1
2
kr.co.spring.PrototypeBean@28eaa59a
kr.co.spring.PrototypeBean@3427b02d
해결방법2. ObjectProvider 사용

프로토타입 bean을 의존주입 받을 때 ObjectProvider<타입>을 사용 할수 있습니다. 실제 bean을 사용할 때는 getIfAvailable메소드를 이용합니다.

1
2
3
4
5
6
7
8
9
@Component
public class SingletonBean {
@Autowired
ObjectProvider<PrototypeBean> prototypeBean;

public PrototypeBean getPrototypeBean() {
return prototypeBean.getIfAvailable();
}
}
1
2
kr.co.spring.PrototypeBean@222114ba
kr.co.spring.PrototypeBean@3d121db3

위의 방법의 경우에는 스프링 클래스를 사용하기 때문에 스프링에 의존이 되어 첫번째 방법을 추천.

Comment and share

IoC 컨테이너는 Bean 라이프 사이클을 관리하여 주고 특정한 시점에 Bean에게 이를 알려줄수 있는 메커니즘을 제공합니다. 스프링에서는 주로 init와 destroy 이벤트를 제공합니다.

InitializingBean와 DisposableBean 인터페이스

InitializingBean 인터페이스를 활용해서 Bean의 초기화 작업을 진행할수 있습니다. 해당 인터페이스에서 제공하는 afterPropertiesSet메소드를 구현하면 Spring이 Bean의 생성 후에 해당 메소드를 호출해 줍니다.

1
2
3
4
5
6
7
8
9
10
@Component
public class Book implements InitializingBean {
@Autowired
BeanFactory beanFactory;

public void afterPropertiesSet() throws Exception {
System.out.println("Bean이 등록되었습니다.");
System.out.println(beanFactory.getBean("book"));
}
}
1
2
Bean이 등록되었습니다.
kr.co.spring.Book@3745e5c6

DisposableBean 인터페이스에서 제공하는 destroy메소드를 구현하면 Bean의 소멸 직전에 해당 메소드를 호출해 줍니다.

1
2
3
4
5
6
7
8
9
10
11
@Component
public class Book implements DisposableBean {

@Autowired
BeanFactory beanFactory;

public void destroy() throws Exception {
System.out.println("Bean이 삭제됩니다..");
System.out.println(beanFactory.getBean("book"));
}
}

xml 빈 등록시 사용자 라이프사이클 메소드 정의

xml에서 빈을 수동으로 등록시에 위의 InitializingBean, DisposableBean을 사용하지 않고 사용자 메소드를 정의 가능합니다.

1
<bean id="book" class="kr.co.spring.Book" init-method="initBook" destroy-method="destroyBook"/>
1
2
3
4
5
6
7
8
9
public class Book {
public void initBook() {
System.out.println("빈이 등록되었습니다.");
}

public void destoryBook() {
System.out.println("빈이 삭제되었습니다.");
}
}

@PostConstruct와 @PreDestroy

좀 더 간편하게 어노테이션을 이용해서 위와 동일하게 사용할수 있습니다.

@PostConstruct는 Bean이 생성 된 후에 호출 될 메소드 위에 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
@Component
public class Book {
@Autowired
BeanFactory beanFactory;

@PostConstruct
public void init() throws Exception {
System.out.println("Bean이 등록되었습니다.");
System.out.println(beanFactory.getBean("book"));
}
}
1
2
Bean이 등록되었습니다.
kr.co.spring.Book@644baf4a

@PreDestroy는 Bean이 소멸 직전에 호출 될 메소드 위에 사용합니다.

1
2
3
4
5
6
7
8
@Component
public class Book {

@PreDestroy
public void destroy() throws Exception {
System.out.println("빈이 삭제되었습니다.");
}
}

기타

ApplicationContext 클래스도 Bean으로 등록되어 있기 때문에 의존주입 받을수 있습니다. 이 뿐 아니라 ApplicationContext가 상속하고 있는 BeanFactory, ResourceLoader 등도 Bean으로 등록되어 있기 때문에 ApplicationContext를 의존주입 받지 않고 필요한 기능을 가진 Bean을 의존주입해서 사용하는 것이 가시성에 좋습니다(ex : Bean과 관련된 처리를 할때는 BeanFactory사용).

Comment and share

예제 소스 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

<!-- BookStore.class -->

public interface BookStore {

}

<!-- NobleBookStore.class -->

@Service
public class NobleBookStore implements BookStore {

}

<!-- EssayBookStore.class -->

@Service
public class EssayBookStore implements BookStore {

}

중복된 의존주입

동일한 타입의 Bean을 의존 주입 받으려고 하면 문제가 발생합니다(동일한 타입의 Bean 등록에는 문제가 없다).

1
2
3
4
5
6
7
8
9
@Component
public class City {
@Autowired
BookStore bookStore;

public BookStore getBookStore() {
return bookStore;
}
}
1
Error creating bean with name 'city': Unsatisfied dependency expressed through field 'bookStore'

동일한 타입의 Bean이 2개가 존재하기 때문에 위와 같은 에러가 발생합니다.


해결방법

@Primary 어노테이션 사용

우선적으로 의존 주입하고 싶은 class에 @Primary 어노테이션을 사용합니다.

1
2
3
4
5
@Service
@Primary
public class NobleBookStore implements BookStore {

}
1
2
3
4
5
6
7
8
9
public class TestControllerTest {

@Test
public void test() {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanConfig.class);
System.out.println(((City)applicationContext.getBean("city")).getBookStore());

}
}
1
kr.co.spring.NobleBookStore@16e7dcfd

@Qulifier 어노테이션 사용

@Autowired와 함께 사용하며 사용하고자 하는 빈의 아이디를 적어주면 됩니다(빈의 아이디를 따로 설정하지 않으면 default로 class 명에 첫글자가 소문자인 아이디로 설정된다).

1
2
3
4
5
6
@Component
public class City {
@Autowired
@Qualifier("nobleBookStore")
BookStore bookStore;
}

@Qulifier 어노테이션 사용하지 않기

기본적으로 @Autowried는 타입으로 의존주입을 해주지만 타입이 존재하지 않으면 Bean Name으로도 의존주입이 가능합니다. 그래서 굳이 @Qulifier를 사용하지 않아도 사용이 가능합니다.

아래의 코드를 보면 이해가 가능합니다.

1
2
3
4
5
@Component
public class City {
@Autowired
BookStore nobleBookStore; // 기존에 bookStore 대신 nobleBookStore를 사용하과 있다.
}

동일한 타입의 Bean 모두 의존주입 받기

동일한 타입의 Bean들을 모두 의존주입을 받을수가 있습니다.

1
2
3
4
5
@Component
public class City {
@Autowired
List<BookStore> bookRepoitories;
}

Comment and share

@Autowried는 Bean으로 등록된 객체를 자동으로 삽입해주는 역할을 합니다. 생성자, 필드, set메소드를 활용 하여 적용이 가능합니다.

예제 Class 등록

해당 포스팅 시에 사용 할 Book 클래스입니다. 아래에서 사용 될 Book 클래스는 아래 소스 코드를 사용하게 됩니다.

1
2
3
4
5
6
7
8
package kr.co.spring;

import org.springframework.stereotype.Component;

@Component
public class Book {

}

생성자를 이용한 방법

생성자 메소드 위에 @Autowired만 등록 해서 사용 하면 됩니다.

1
2
3
4
5
6
7
8
9
@Component
class BookStore {
private Book book;

@Autowired
public BookStore(Book book) {
this.book = book;
}
}

생성자에서 @Autowired는 생략이 가능합니다.


setter를 이용한 방법

setter 메소드 위에 @Autowired만 등록 해서 사용 하면 됩니다. 생성자와는 달리 @Autowired를 붙여주지 않으면 의존주입이 되지 않습니다.

1
2
3
4
5
6
7
8
9
@Component
class BookStore {
private Book book;

@Autowired
public setBook(Book book) {
this.book = book;
}
}

setter를 활용하면 Bean을 필수적으로 의존주입 받을지 여부를 선택할수 있습니다. 옵션을 주지 않는 경우 의존주입 받을 Bean이 없는 경우 에러가 발생합니다. 이때 @Autowired(required = false)를 사용해서 만약 Bean이 존재하지 않는 경우 의존주입을 받지 않도록 설정이 가능합니다.

setter 메소드 이름은 상관이 없습니다.


필드에 직접 사용

필드 위에 @Autowired를 등록해서 사용이 가능합니다. 가장 심플하게 코드를 작성할수 있습니다. setter와 마찬가지로 required 옵션을 사용가능합니다.

1
2
3
4
5
@Component
class BookStore {
@Autowired
private Book book;
}
외부 라이브러리를 Bean 등록하고 의존주입

수정이 불가능한 외부라이브러리를 Bean으로 등록하고 해당 Bean에 의존주입까지 해주어야 하는 경우가 있습니다.

1
2
3
4
<bean id="library" class="kr.co.spring.Library">
<constructor-arg value="라이브러리"/>
<property name="location" value="서울"/>
</bean>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Library {
private String name;
private String location;

public Library(String name) {
this.name = name;
}

public void setLocation(String location) {
this.location = location;
}

@Override
public String toString() {
return "Library{" +
"name='" + name + '\'' +
", location='" + location + '\'' +
'}';
}
}
1
2
3
4
5
6
7
8
9
public class TestControllerTest {

@Test
public void test() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring/spring-bean.xml");
Library library = (Library) applicationContext.getBean("library");
System.out.println(library.toString());
}
}
1
Library{name='라이브러리', location='서울'}

만약 생성자에서 파라미터를 여러개 받는 경우에는 index를 이용하면 됩니다.

1
2
3
4
5
6
7

<bean id="library" class="kr.co.spring.Library">
<constructor-arg index="0" value="1"/>
<constructor-arg index="1" value="라이브러리"/>

<property name="location" value="서울"/>
</bean>

type 속성을 이용해서도 사용 가능합니다.

1
2
3
4
5
6
<bean id="library" class="kr.co.spring.Library">
<constructor-arg value="1" type="int"/>
<constructor-arg value="라이브러리" type="java.lang.String"/>

<property name="location" value="서울"/>
</bean>

예제에서는 value를 사용하였지만 bean을 의존주입 받을 때는 ref를 사용하면 됩니다.

Comment and share

IoC란

Ioc란 Inversion of Control의 약자로써, 어떤 객체가 사용하는 의존 객체를 직접 만들어 사용하는게 아니라 주입받아서 사용하는 방법을 일컫는 말입니다.

스프링 IoC 컨테이너

컨테이너는 보통 인스턴스의 생명주기를 관리하며, 생성된 인스턴스에게 추가적인 기능을 제공하는 것입니다. 스프링 컨테이너는 어플리케이션 컴포넌트의 중앙 저장소와 같은 역할을 하며 빈 설정 소스로부터 빈 정의를 읽어 들여 빈을 구성 하고 제공하는 역할을 합니다.

IoC 컨테이너의 핵심적인 역할을 하는 인터페이스가 BeanFactory이다.

IoC 컨테이너의 장점

  1. 의존주입을 통한 빈 관리가 용이하다.
  2. 싱글톤 사용을 할수 있다.
  3. 라이프사이클 인터페이스를 제공해준다.
    • @PostConstruct : 메소드 위에 사용하면 빈이 생성될때 실행된다(ApplicationContext에서 제공).
  4. 의존주입 방법을 통해서 테스트 하기 쉬운 코드를 작성할수 있다.
    • Mock 객체를 생성해서 의존성 주입을 할수 있어 테스트 하기에 용의하다.

Bean

스프링 IoC 컨테이너가 관리하는 객치입니다.

Bean 등록방법

1. XML을 이용한 방법

bean이라는 태그를 이용해서 Bean 등록을 할수 있다.

1
<bean id="인스턴스명" class="클래스경로"/>

옵션
  1. id : 보통 카멜케이스로 작성을 하며 사용할 인스턴스명을 입력해주면 된다.
  2. class : Bean 등록 하려는 클래스의 경로를 입력해주면 된다.
  3. scope : 말그대로 스코프를 설정
    • 기본값은 싱글톤
    • 매번 다른 값을 사용하려면 prototype으로 설정
    • 이외에도 request, session 등이 존재
  4. autowire

의존주입

property 태그를 이용해서 의존주입 관계를 형성 할수 있습니다. name은 의존주입할 set메소드 이름이고, ref는 Bean의 id입니다. 인스턴스가 아닌 경우 ref 대신 value를 사용합니다.

1
2
3
4
<bean id="book" class="kr.co.Book"/>
<bean id="store" class="kr.co.Store">
<property name="set메소드이름" ref="의존주입할 Bean의 id"/>
</bean>
1
2
3
4
5
6
7
class Store {
private Book book;

public void setBookTest(Book book) { // property의 name으로 bookTest를 입력해주어야 함
this.book = book;
}
}

property의 name의 값으로 입력해야 하는 것은 set메소드의 이름이다. set메소드의 인자의 이름이 아니다. 위의 코드로 예를 들면 bookTest를 입력해주어야 한다.


예제 코드
  • java
  • xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- Test.class -->

public class Test {
public void test() {
System.out.println("너는 안녕?");
}
}

<!-- TestController.class -->

public class TestController {
private Test test;


public void test() {
System.out.println("안녕하세요");
test.test();
}

public void setTest(Test test) {
this.test = test;
}
}

테스트 코드를 통해서 XML 빈 등록이 잘 되고 있음을 확인할수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TestControllerTest {

@Test
public void test() {
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("spring/spring-bean.xml");

TestController testController =
(TestController) applicationContext.getBean("testController");

testController.test();
System.out.println("등록 된 Bean" + Arrays.toString(applicationContext.getBeanDefinitionNames()));
}
}
1
등록 된 Bean[test, testController]

// 클래스 패스로 ApplicationContext 생성 (다른 방법도 존재)
ApplicationContext context = new ClassPathXmlApplicationContext(“클래스패스경로”);
context.getBeanDefinitionNames(); // 등록된 모든 Bean Name확인
context.getBean(“빈Name”); // Bean 가져오기


단점

XML로 작성하는 경우에는 문자열로 작성을 하기 때문에 자동완성기능 등 IDE의 도움을 받는 것에 제한이 되기 때문에 작성하는데에 어려움이 존재 합니다(지원 되는 것도 존재하지만 그래도 자바소스 작성시보다는 불편하다).


component-scan

지금까지는 수동으로 Bean을 등록을 하였었는데 이 방법으로는 수많은 Bean들을 직접 등록하기가 힘듭니다. 그래서 어노테이션을 통해서 좀더 간편하게 Bean등록하는 방법인 component-scan이 등장하게 되었습니다. package를 설정하여 해당 package내에서 @Component 어느테이션이 등록 된 객체를 Bean에 등록해 줍니다.

@Controller, @Service, @Repository 등 @Component 어노테이션을 상속한 어노테이션 또한 자동으로 등록 됩니다.


예제 코드
  • java
  • xml
1
2
3
4
5
6
7
8
package kr.co.spring;

import org.springframework.stereotype.Component;

@Component
public class ComponentScanTest {

}
1
2
3
4
5
6
7
8
9
10
public class TestControllerTest {

@Test
public void test() {
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("spring/spring-bean.xml");

System.out.println("등록 된 Bean" + Arrays.toString(applicationContext.getBeanDefinitionNames()));
}
}
1
등록 된 Bean[componentScanTest, ...]

클래스 설정파일로 Bean 등록

Spring 3.0 이후 보전부터 자바 소스 코드로도 Bean을 등록 할수 있는 방법이 추가 되었습니다. 자바 소스 코드로 작성하게 되면 IED의 기능을 활용할수 있기 때문에 XML보다 작성이 용이하다는 장점이 있습니다.

자바소스로 Bean 구성방법은 @Configuration과 @Bean으로 쉽게 구성이 가능합니다. @Configuration은 해당 클래스가 설정파일임을 명시해 줍니다. @Bean은 메소드 정의시에 사용하며 해당 메소드가 반환한 객체를 Bean으로 등록해줍니다. default로 메소드이름이 Bean Name이 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class BeanConfig {

@Bean
public Test test() {
return new Test();
}

@Bean
public TestController testController(Test test) {
TestController testController = new TestController();
testController.setTest(test);
return testController;
}
}
1
2
3
4
5
6
7
8
9
10
public class TestControllerTest {

@Test
public void test() {
ApplicationContext applicationContext =
new AnnotationConfigApplicationContext(BeanConfig.class); // 클래스설정파일로 Bean등록할때 사용하는 ApplicationContext
System.out.println("등록 된 Bean" + Arrays.toString(applicationContext.getBeanDefinitionNames()));

}
}
1
등록 된 Bean[..., test, testController]

@Bean을 활용한 DI에는 2가지 방법이 존재 합니다.

  1. 파라미터로 Bean 등록 된 객체를 작성하면 자동으로 주입해 줍니다(위의 방법).
  2. @Bean을 사용하는 메소드를 호출합니다(이 방법은 같은 클래스 내부에 존재할때만 가능).

클래스 설정파일로 component-scan

@Bean을 이용하는 방법 또한 직접 작성을 해야하기 때문에 등록하기가 힘듭니다. XML에서 compoennt-scan을 사용하였듯이 클래스 설정파일에서도 마찬가지로 component-scan을 사용할수 있습니다.

@ComponentScan(“base 패키지명”)으로 설정파일에 작성해주면 사용이 가능합니다.

1
2
3
4
5
@Configuration
@ComponentScan("kr.co.spring")
public class BeanConfig {

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
public class ComponentScanTest {

}


public class TestControllerTest {

@Test
public void test() {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanConfig.class);
System.out.println("등록 된 Bean" + Arrays.toString(applicationContext.getBeanDefinitionNames()));

}
}
1
등록 된 Bean[..., componentScanTest]

@ComponentScan 옵션
  1. @ComponentScan : 아무런 옵션도 주지 않는 경우로 해당 클래스가 위치한 패키지 내부를 basePackage로 설정합니다.
  2. @ComponentScan(basePackages = “패키지명”) : 위의 예제와 같은 경우로 직접 basePackage를 설정합니다.
  3. @ComponentScan(basePackageClasses = 클래스명.class) : 특정 클래스가 위치한 패키지를 basePackage로 설정할때 사용합니다.

스프링부트에서의 Bean 등록

스프링부트에서 제공하는 SpringBootApplication 어노테이션을 사용하면 위에서 작업했었던 component-scan, ApplicationContext 생성과 같은 소스코드를 작성할 필요가 없습니다. 해당 어노테이션이 자동으로 생성을 해 줍니다.


공부하면서 느낀점

XML로 Bean을 등록 하는 것보다는 IDE의 도움을 받을수 있는 자바소스로 설정하는게 좀더 편하다고 생각한다. 그리고 Bean을 직접 등록해서 사용하는 것보다는 component-scan을 이용하것이 좋을것 같다.

결론

직접 작성 한 클래스는 component-scan을 활용하고, 외부 라이브러리를 사용할시에는 @Bean을 직접 등록 해서 사용하자!

Comment and share

Spring-Test

스프링은 Junit을 이용하는 테스트 컨텍스트 프레임워크를 제공한다. 이를 이용하면 어노테이션을 이용하여 간편하게 컨텍스트를 사용할수 있다. 해당 방법이 없이 Bean을 사용하려면 직접 ApplicationContext를 생성하는 번거로운 작업을 진행하여야 하나 이로 인해 편하게 작업을 진행 할수 있다.

@ContextConfiguration

Comment and share

DataSourceUtils

in java, api

DataSource

DataSource는 ConnectionPool을 관리하기 위한 목적으로 사용되는 객체로 DataSource를 통해서 Connection을 얻어오고 반납하는 등의 작업을 구현합니다. ConnectionPool에는 여러개의 Connection 객체가 생성되어 운용되는데, 이를 직접 웹 어플리케이션에서 다루기 힘들기 때문에 DataSource라는 개념이 도입되었습니다.

DataSourceUtils

DataSourceUtils 클래스는 JNDI에서 연결을 얻고 필요한 경우 연결을 닫는 메소드를 제공하는 기능을 제공 합니다. 그리고 DataSourceTransactionManager로 쓰레드에 기반한 연결을 지원합니다.

DataSourceUtils에서 제공하는 메소드

getConnection(DataSource dataSource)

getConnection메소드는 제공 된 DataSource를 기반으로 하여 Connection을 생성해줍니다. Connection 생성은 트랜잭션 동기화 여부에 따라 다르게 작동합니다. 만약 트랜잭션 동기화가 활성화 되어 있다면 Connection을 생성하고 트랜잭션 저장소에 바인딩 합니다. 이후 트랜잭션 동기화 작업을 종료 할때까지 getConnection메소드를 호출하면 동일한 Connection을 반환하는 것을 확인할수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class DataSourceUtilsTest {
@Autowired
DataSource dataSource;

public void testGetConnection() {
TransactionSynchronizationManager.initSynchronization();

Connection conn1 = DataSourceUtils.getConnection(dataSource);
Connection conn2 = DataSourceUtils.getConnection(dataSource);

System.out.println("동일한 인스턴스 인가요? " + conn1.equals(conn2));
}
}

트랜잭션이 동기화되어 있지 않을 경우에는 dataSource.getConnection()과 동일하게 작동합니다. 그래서 항상 새로운 Connection을 반환합니다.

1
2
3
4
5
6
7
8
9
10
11
public class DataSourceUtilsTest {
@Autowired
DataSource dataSource;

public void testGetConnection() {
Connection conn1 = DataSourceUtils.getConnection(dataSource);
Connection conn2 = DataSourceUtils.getConnection(dataSource);

System.out.println("동일한 인스턴스 인가요? " + conn1.equals(conn2));
}
}

releaseConnection(Connection conn, DataSource dataSource)

releaseConnection메소드 또한 getConnection와 비슷하게 동작 합니다. 만약 트랜잭션 동기화가 활성화 되어 있다면 트랜잭션 저장소에서 Connection을 초기화 합니다. 이때 Connection의 연결은 종료하지 않습니다. 그리고 release 후 다시 getConnection으로 Connection을 받아왔을때 동일한 값이라는 것을 확인 할수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DataSourceUtilsTest {
@Autowired
DataSource dataSource;

public void testGetConnection() {
TransactionSynchronizationManager.initSynchronization();

Connection conn = DataSourceUtils.getConnection(dataSource);

DataSourceUtils.releaseConnection(conn, dataSource);
System.out.println("Connection이 종료 되었니? " + conn.isClosed());

Connection conn2 = DataSourceUtils.getConnection(dataSource);

System.out.println("동일한 Connection 인가요? " + conn.equals(conn2));
}
}

위와 같이 releaseConnection 메소드를 호출 하더라도 Connection은 종료되지 않는다. 단지 트랜잭션에서만 제거되었을 뿐이다. 이 때문에 사용시 주의할 필요가 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class DataSourceUtilsTest {
@Autowired
DataSource dataSource;

public void testGetConnection() {
TransactionSynchronizationManager.initSynchronization();

Connection conn = DataSourceUtils.getConnection(dataSource);

System.out.println("트랜잭션 저장소에 저장 중인가? " + DataSourceUtils.isConnectionTransactional(conn, dataSource));

DataSourceUtils.releaseConnection(conn, dataSource);

TransactionSynchronizationManager.unbindResource(dataSource);
TransactionSynchronizationManager.clearSynchronization();

System.out.println("Connection이 종료 되었니? " + conn.isClosed());
System.out.println("트랜잭션 저장소에 저장 중인가? " + DataSourceUtils.isConnectionTransactional(conn, dataSource));
}
}

결론

DataSourceUtils를 사용하면 동기화된 트랜잭션의 Connection을 가져와서 사용하기에 좋다. 하지만 releaseConnection은 Connection을 종료해주는 기능이 아니므로 사용시에 주의할 필요가 있다.

트랜잭션 동기화 상태가 아닌 Connection이라면 releaseConnection메소드를 호출하면 close 해준다.

Comment and share

Moon Star

author.bio


author.job