[Spring] 4. Bean
예제 디렉토리 구조 및 공통 클래스
package org.example.common;
public abstract class Product {
private String name; //상품명
private int price; //상품가격
public Product() {}
public Product(String name, int price) {
super();
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
@Override
public String toString() {
return name + " " + price;
}
}
package org.example.common;
public class Beverage extends Product {
private int capacity; //용량
public Beverage() {
super();
}
public Beverage(String name, int price, int capacity) {
super(name, price);
this.capacity = capacity;
}
public int getCapacity() {
return this.capacity;
}
public void setCapacity(int capacity) {
this.capacity = capacity;
}
@Override
public String toString() {
return super.toString() + " " + this.capacity;
}
}
package org.example.common;
public class Bread extends Product {
private java.util.Date bakedDate; //생산시간
public Bread() {
super();
}
public Bread(String name, int price, java.util.Date bakedDate) {
super(name, price);
this.bakedDate = bakedDate;
}
public java.util.Date getBakedDate() {
return this.bakedDate;
}
public void setBakedDate(java.util.Date bakedDate) {
this.bakedDate = bakedDate;
}
@Override
public String toString() {
return super.toString() + " " + this.bakedDate;
}
}
package org.example.common;
public class ShoppingCart {
private final List<Product> items; //쇼핑카트에 담긴 상품들
public ShoppingCart() {
items = new ArrayList<>();
}
public void addItem(Product item) {
items.add(item);
}
public List<Product> getItem() {
return items;
}
}
Bean Scope
: 스프링 빈이 생성될 때 생성되는 인스턴스의 범위
스프링에서는 다양한 bean scope를 제공한다.
Bean Scope | Description |
Singleton | 하나의 인스턴스만을 생성하고, 모든 빈이 해당 인스턴스를 공유하여 사용한다. |
Prototype | 매번 새로운 인스턴스를 생성한다. |
Request | HTTP 요청을 처리할 때마다 새로운 인스턴스를 생성하고, 요청 처리가 끝나면 인스턴스를 폐기한다. 웹 애플리케이션 컨텍스트에만 해당된다. |
Session | HTTP 세션 당 하나의 인스턴스를 생성하고, 세션이 종료되면 인스턴스를 폐기한다. 웹 애플리케이션 컨텍스트에만 해당된다. |
1. Singleton
Spring Framework에서 Bean의 기본 스코프는 singleton이다.
Singleton은 애플리케이션 내에서 하나의 인스턴스만 생성하고, 모든 빈이 해당 인스턴스를 공유하여 사용한다.
이를 통해 메모리 사용량을 줄이고 성능 향상을 기대할 수 있다.
package org.example.section01.scope.subsection01.singleton;
@Configuration
public class ContextConfiguration {
@Bean
public Product carpBread() {
return new Bread("붕어빵", 1000, new java.util.Date());
}
@Bean
public Product milk() {
return new Beverage("딸기우유", 1500, 500);
}
@Bean
public Product water() {
return new Beverage("지리산암반수", 3000, 1000);
}
@Bean
// @Scope("singleton")
// Bean의 기본 스코프는 "singleton" 이다.
public ShoppingCart cart() {
return new ShoppingCart();
}
}
package org.example.section01.scope.subsection01.singleton;
public class Application {
public static void main(String[] args) {
/* 빈 설정 파일 기반으로 IoC 컨테이너 생성*/
ApplicationContext applicationContext
= new AnnotationConfigApplicationContext(ContextConfiguration.class);
/* 슈퍼에 상품이 진열 되어 있다. (빈 객체를 반환받는다) */
Product carpBread = applicationContext.getBean("carpBread", Product.class);
Product milk = applicationContext.getBean("milk", Product.class);
Product water = applicationContext.getBean("water", Product.class);
/* 손님1이 쇼핑 카트를 꺼내 상품을 카트에 담는다. */
ShoppingCart cart1 = applicationContext.getBean("cart", ShoppingCart.class);
cart1.addItem(carpBread);
cart1.addItem(milk);
System.out.println("cart1에 담긴 상품 : " + cart1.getItem());
/* 손님2가 쇼핑 카트를 꺼내 상품을 카트에 담는다. */
ShoppingCart cart2 = applicationContext.getBean("cart", ShoppingCart.class);
cart2.addItem(water);
System.out.println("cart2에 담긴 상품 : " + cart2.getItem());
/*두 카트의 hashcode를 출력해보면 동일하다*/
System.out.println("cart1의 hashcode : " + cart1.hashCode());
System.out.println("cart2의 hashcode : " + cart2.hashCode());
}
}
위의 예제에서는 손님 두 명이 각각 쇼핑 카트를 이용해 상품을 담는 것인데,
singleton으로 관리가 되다보니 의도하지 않게 하나의 카트만 이용된다. (두 cart의 hashCode가 같다.)
따라서 이런 경우에는 singleton scope가 아닌 prototype scope가 필요하다.
2. Prototype
prototype 스코프를 갖는 Bean은 매번 새로운 인스턴스를 생성한다.
이를 통해 의존성 주입 등의 작업에서 객체 생성에 대한 부담을 줄일 수 있다.
package org.example.section01.scope.subsection02.prototype;
@Configuration
public class ContextConfiguration {
@Bean
public Product carpBread() {
return new Bread("붕어빵", 1000, new java.util.Date());
}
@Bean
public Product milk() {
return new Beverage("딸기우유", 1500, 500);
}
@Bean
public Product water() {
return new Beverage("지리산암반수", 3000, 1000);
}
@Bean
@Scope("prototype") // default 값인 singleton에서 prototype으로 변경
public ShoppingCart cart() {
return new ShoppingCart();
}
}
ShoppingCart의 bean scope을 prototype으로 설정하니 두 shoppingCart가 각각 만들어지는 것을 확인할 수 있다.
(두 cart의 hashCode가 다르다.)\
3. xml 설정
위의 예제에서는 모두 Java 빈 객체 설정을 이용했는데, XML 파일에서도 설정할 수 있다.
XML 파일에 <bean> 태그를 이용하면 아래와 같이 속성을 기재할 수 있다.
<!-- singleton 설정 -->
<bean id="cart" class="패키지명.ShoppingCart" scope="singleton"/>
<!-- prototype 설정 -->
<bean id="cart" class="패키지명.ShoppingCart" scope="prototype"/>
init, destroy method
스프링 빈은 초기화(init)와 소멸화(destroy)의 라이프 사이클을 가지고 있다.
* init-method 속성
init-method 속성을 사용하면 스프링이 빈 객체를 생성한 다음 초기화 작업을 수행할 메소드를 지정할 수 있다.
이 메소드는 빈 객체 생성자가 완료된 이후에 호출된다.
init-method 속성으로 지정된 메소드는 일반적으로 빈의 초기화를 위해 사용된다.
* destroy-method 속성
destroy-method 속성을 사용하면 빈 객체가 소멸될 때 호출할 메소드를 지정할 수 있다.
이 메소드는 ApplicationContext의 close() 메소드가 호출되기 전에 빈 객체가 소멸될 때 호출된다.
destroy-method 속성으로 지정된 메소드는 일반적으로 사용하던 리소스를 반환하기 위해 사용된다.
1. Java 설정 방식
예제를 위해 Owner 클래스를 추가해주었다.
package org.example.section02.initdestroy.subsection01.java;
public class Owner {
public void openShop() {
System.out.println("사장님이 가게 문을 열었습니다. 이제 쇼핑을 하실 수 있습니다.");
}
public void closeShop() {
System.out.println("사장님이 가게 문을 닫았습니다. 이제 쇼핑을 하실 수 없습니다.");
}
}
package org.example.section02.initdestroy.subsection01.java;
@Configuration
public class ContextConfiguration {
@Bean
public Product carpBread() {
return new Bread("붕어빵", 1000, new java.util.Date());
}
@Bean
public Product milk() {
return new Beverage("딸기우유", 1500, 500);
}
@Bean
public Product water() {
return new Beverage("지리산암반수", 3000, 1000);
}
@Bean
@Scope("prototype") // default 값인 singleton에서 prototype으로 변경
public ShoppingCart cart() {
return new ShoppingCart();
}
/* init-method로 openShop 메소드, destroy-method로 closeShop 메소드를 설정한다.*/
@Bean(initMethod = "openShop", destroyMethod = "closeShop")
public Owner owner() {
return new Owner();
}
}
package org.example.section02.initdestroy.subsection01.java;
public class Application {
public static void main(String[] args) {
ApplicationContext applicationContext
= new AnnotationConfigApplicationContext(ContextConfiguration.class);
/* 슈퍼에 상품이 진열 되어 있다. */
Product carpBread = applicationContext.getBean("carpBread", Product.class);
Product milk = applicationContext.getBean("milk", Product.class);
Product water = applicationContext.getBean("water", Product.class);
/* 한 손님이 쇼핑 카트를 꺼내 상품을 카트에 담는다. */
ShoppingCart cart = applicationContext.getBean("cart", ShoppingCart.class);
cart.addItem(carpBread);
cart.addItem(milk);
System.out.println("cart에 담긴 상품 : " + cart.getItem());
/* 다른 손님이 쇼핑 카트를 꺼내 상품을 카트에 담는다. */
ShoppingCart cart2 = applicationContext.getBean("cart", ShoppingCart.class);
cart2.addItem(water);
System.out.println("cart2에 담긴 상품 : " + cart2.getItem());
/* destroy 메소드는 빈 객체 소멸 시점에 동작하므로
컨테이너가 종료되지 않을 경우 확인할 수 없다.
GC가 해당 빈을 메모리에서 지울 때 destroy 메소드가 동작하게 되는데
메모리에서 지우기 전에 프로세스는 종료된다.
따라서 아래와 같이 강제로 컨테이너를 종료시키면 destroy 메소드가 동작한다.*/
((AnnotationConfigApplicationContext) applicationContext).close();
}
}
2. Annotation 설정 방식
Application은 위의 Java 설정 방식에서 사용한 것과 같다.
package org.example.section02.initdestroy.subsection02.annotation;
@Component
public class Owner {
@PostConstruct // initMethod와 같은 설정 어노테이션
public void openShop() {
System.out.println("사장님이 가게 문을 열었습니다. 이제 쇼핑을 하실 수 있습니다.");
}
@PreDestroy // destroyMethod와 같은 설정 어노테이션
public void closeShop() {
System.out.println("사장님이 가게 문을 닫았습니다. 이제 쇼핑을 하실 수 없습니다.");
}
}
package org.example.section02.initdestroy.subsection02.java;
@Configuration
@ComponentScan("com.ohgiraffers.section02.initdestroy.subsection02.annotation")
public class ContextConfiguration {
@Bean
public Product carpBread() {
return new Bread("붕어빵", 1000, new java.util.Date());
}
@Bean
public Product milk() {
return new Beverage("딸기우유", 1500, 500);
}
@Bean
public Product water() {
return new Beverage("지리산암반수", 3000, 1000);
}
@Bean
@Scope("prototype") // default 값인 singleton에서 prototype으로 변경
public ShoppingCart cart() {
return new ShoppingCart();
}
}
3. XML 설정 방식
Owner 클래스는 위의 Java 설정 방식에서 사용한 것과 같다.
<!--src\main\resources\section02\initdestroy\subsection03\xml\spring-context.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 태그를 통해 bean를 등록한다.-->
<bean id="carpBread" class="org.example.common.Bread">
<constructor-arg name="name" value="붕어빵"/>
<constructor-arg name="price" value="1000"/>
<constructor-arg name="bakedDate" ref="today"/>
</bean>
<bean id="today" class="java.util.Date" scope="prototype"/>
<bean id="milk" class="org.example.common.Beverage">
<constructor-arg name="name" value="딸기우유"/>
<constructor-arg name="price" value="1500"/>
<constructor-arg name="capacity" value="500"/>
</bean>
<bean id="water" class="org.example.common.Beverage">
<constructor-arg name="name" value="지리산암반수"/>
<constructor-arg name="price" value="3000"/>
<constructor-arg name="capacity" value="500"/>
</bean>
<bean id="cart" class="org.example.common.ShoppingCart" scope="prototype"/>
<bean id="owner" class="org.example.section02.initdestroy.subsection03.xml.Owner"
init-method="openShop" destroy-method="closeShop"/>
</beans>
package org.example.section02.initdestroy.subsection03.java;
public class Application {
public static void main(String[] args) {
ApplicationContext applicationContext
= new GenericXmlApplicationContext("section02/initdestroy/subsection03/xml/spring-context.xml");
/* 슈퍼에 상품이 진열 되어 있다. */
Product carpBread = applicationContext.getBean("carpBread", Product.class);
Product milk = applicationContext.getBean("milk", Product.class);
Product water = applicationContext.getBean("water", Product.class);
/* 한 손님이 쇼핑 카트를 꺼내 상품을 카트에 담는다. */
ShoppingCart cart = applicationContext.getBean("cart", ShoppingCart.class);
cart.addItem(carpBread);
cart.addItem(milk);
System.out.println("cart에 담긴 상품 : " + cart.getItem());
/* 다른 손님이 쇼핑 카트를 꺼내 상품을 카트에 담는다. */
ShoppingCart cart2 = applicationContext.getBean("cart", ShoppingCart.class);
cart2.addItem(water);
System.out.println("cart2에 담긴 상품 : " + cart2.getItem());
/* Spring 컨테이너 종료 전에 프로세스가 종료 되어 destroyMethod 확인이 되지 않으므로
* 컨테이너 종료 코드를 작성하여 확인한다. */
((GenericXmlApplicationContext) applicationContext).close();
}
}
Properties
1. Properties
key와 value가 쌍으로 이루어진 간단한 파일으로, 보통 소프트웨어 설정 정보를 저장할 때 사용된다.
Spring에서는 Properties를 이용하여 빈의 속성 값을 저장하고 읽어올 수 있다.
Properties 파일의 각 줄은 아래와 같은 형식으로 구성된다. 주석은 #으로 시작하며, 빈 줄은 무시된다.
# 주석
key=value
#src\main\resources\section03\properties\subsection01\properties\product-info.properties
bread.carpbread.name=\uBD95\uC5B4\uBE75
bread.carpbread.price=1000
beverage.milk.name=\uB538\uAE30\uC6B0\uC720
beverage.milk.price=1500
beverage.milk.capacity=500
beverage.water.name=\uC9C0\uB9AC\uC0B0\uC554\uBC18\uC218
beverage.water.price=3000
beverage.water.capacity=500
// src\main\java\org\example\section03\properties\subsection01\properties\ContextConfiguration
@Configuration
// resource 폴더 하위 경로를 기술하여 읽어올 properties 파일을 설정한다. (폴더의 구분은 / 혹은 \\으로 한다.)
@PropertySource("section03/properties/subsection01/properties/product-info.properties")
public class ContextConfiguration {
/* 필드에 읽어온 값 주입 */
/* @Value 어노테이션을 사용하여 properties의 값을 읽어온다.
@Value 어노테이션은 빈의 속성 값을 자동으로 주입받을 수 있는 어노테이션이다.
치환자 문법(placeholder) 을 이용하여 properties에 저장 된 키 값을 입력하면
value에 해당하는 값이 가져와서 주입된다.
양 옆에 공백이 있을 경우 값을 읽어오지 못하므로 주의한다.
뒤에 : 을 사용하여 해당 key 값이 없을 경우의 대체 값을 작성할 수 있다.*/
@Value("${bread.carpbread.name:붕어빵}") // 해당 key 값이 없을 경우 "붕어빵" 이 주입된다.
private String name;
@Value("${bread.carpbread.price:0}")
private int price;
@Bean
public Product carpBread() {
return new Bread(name, price, new java.util.Date());
}
@Bean /* 매개변수에 읽어온 값 주입 */
public Product milk(@Value("${beverage.milk.name}") String name,
@Value("${beverage.milk.price:0}") int price,
@Value("${beverage.milk.capacity:0}") int capacity) {
return new Beverage(name, price, capacity);
}
@Bean
public Product water() {
return new Beverage("지리산암반수", 3000, 1000);
}
@Bean
@Scope("prototype") // default 값인 singleton에서 prototype으로 변경
public ShoppingCart cart() {
return new ShoppingCart();
}
}
Application은 Bean Scope의 Singleton에서 이용한 코드와 같다.
2. 국제화(internationalization, i18n)
: 소프트웨어를 다양한 언어와 문화권에 맞게 번역할 수 있도록 디자인하는 과정
Spring Framework에서 i18n은 MessageSource 인터페이스와 property 파일을 이용해 구현된다.각 언어별로 property 파일을 정의하면 Spring은 사용자의 Locale에 맞게 적절한 파일을 선택하여 애플리케이션 텍스트를 올바르게 번역할 수 있다.
먼저 resources 폴더에 한국어와 영어 버전의 에러 메세지를 properties 파일로 정의한다.
properties 파일에 {인덱스} 형식으로 문자를 입력하면 값을 전달하면서 value를 불러올 수 있다.
각 파일의 끝에는 Locale이 적절하게 입력돼야 한다.
# src\main\resources\section03\properties\subsection02\i18n\message_en_US.properties
# \uC601\uC5B4 \uBC84\uC804 \uC5D0\uB7EC \uBA54\uC138\uC9C0
error.404=Page Not Found.
error.500=Something Wrong! The developer''s fault. Who is developer? It''s {0} at {1}
# src\main\resources\section03\properties\subsection02\i18n\message_ko_KR.properties
# \uD55C\uAD6D\uC5B4 \uBC84\uC804 \uC5D0\uB7EC \uBA54\uC138\uC9C0
error.404=\uD398\uC774\uC9C0\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.
error.500=\uC11C\uBC84 \uB0B4\uBD80 \uC624\uB958\uC785\uB2C8\uB2E4. \uAC1C\uBC1C\uC790\uB294 \uB204\uAD6C? {0} \uD604\uC7AC \uC2DC\uAC04 {1}
// src\main\java\org\example\section03\properties\subsection02\i18n\ContextConfiguration
@Configuration
public class ContextConfiguration {
@Bean
/*MessageSource을 이용해 i18n을 구현할 수 있다.*/
public MessageSource messageSource() {
/* 접속하는 세션의 locale에 따라 자동 재로딩 하는 용도의 MessageSource 구현체 */
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
/* 다국어 메세지를 읽어올 properties 파일의 이름을 설정 */
messageSource.setBasename("section03/properties/subsection02/i18n/message");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
}
package com.ohgiraffers.section03.properties.subsection02.i18n;
public class Application {
public static void main(String[] args) {
ApplicationContext applicationContext
= new AnnotationConfigApplicationContext(ContextConfiguration.class);
String error404KR = applicationContext.getMessage("error.404", null, Locale.KOREA);
String error500KR = applicationContext.getMessage(
"error.500",
new Object[] {"여러분", new java.util.Date()},
Locale.KOREA);
System.out.println("error404KR: " + error404KR);
System.out.println("error500KR: " + error500KR);
String error404US = applicationContext.getMessage("error.404", null, Locale.US);
String error500US = applicationContext.getMessage(
"error.500",
new Object[] {"you", new java.util.Date()},
Locale.US);
System.out.println("error404US: " + error404US);
System.out.println("error500US: " + error500US);
}
}