아래 bean의 scope를 예제들을 통해 설명할 것이다. 설명할 내용은 아래와 같다.
이걸 “빵, 음료, 장바구니”라고 생각하면 되고, 스프링 빈으로 등록해서
“장바구니를 singleton/prototype으로 만들면 뭐가 다른가”를 보여주는 용도라고 생각하면 된다.
“스코프 = 이 타입의 빈 객체를 컨테이너가 몇 개 관리할지에 대한 범위”
| Scope | 의미 |
|---|---|
| singleton | 애플리케이션 전체에서 이 타입의 빈은 딱 1개 |
| prototype | getBean() 할 때마다 새로 1개씩 생성 |
| request | HTTP 요청 하나당 1개 (웹 환경에서만) |
| session | 세션 하나당 1개 (웹 환경에서만) |
@Bean
@Scope("singleton") // 안 써도 기본값이 singleton
public ShoppingCart cart() {
return new ShoppingCart();
}
ShoppingCart cart1 = context.getBean("cart", ShoppingCart.class);
cart1.addItem(carpBread);
cart1.addItem(milk);
ShoppingCart cart2 = context.getBean("cart", ShoppingCart.class);
cart2.addItem(water);
결과:
Bean의 기본 SCOPE는 Singleton 이다.
Depends on 과 @lazy
@Bean
@DependsOn({"carpBread", "milk", "water"})
@Lazy
@Scope("singleton") // 안 써도 기본값이 singleton
public ShoppingCart cart() {
System.out.println("쇼핑 카트 생성 시점");
return new ShoppingCart();
}
붕어빵 생성 시점
딸기우유 생성 시점
물 생성 시점
쇼핑 카트 객체 꺼내기전=========
쇼핑 카트 생성 시점
쇼핑 카트 객체 꺼낸 다음========
cart에 담긴 물품 : [붕어빵 1000 Fri Nov 21 15:32:25 KST 2025, 딸기우유 1500 500]
cart2에 담긴 물품 : [붕어빵 1000 Fri Nov 21 15:32:25 KST 2025, 딸기우유 1500 500, 지리산 절벽 암반수 3000 400]
붕어빵 생성 시점
딸기우유 생성 시점
물 생성 시점
쇼핑 카트 생성 시점
쇼핑 카트 객체 꺼내기전=========
쇼핑 카트 객체 꺼낸 다음========
cart에 담긴 물품 : [붕어빵 1000 Fri Nov 21 15:33:48 KST 2025, 딸기우유 1500 500]
cart2에 담긴 물품 : [붕어빵 1000 Fri Nov 21 15:33:48 KST 2025, 딸기우유 1500 500, 지리산 절벽 암반수 3000 400]
Lazy : 컨테이너 동작시점이 아니라 해당 객체 필요 시점에 인스턴스를 생성한다.
Depends on : 나열한 빈 인스턴스가 모두 생성된 뒤 생성되도록 설정
@Bean
@Scope("prototype")
public ShoppingCart cart() {
return new ShoppingCart();
}
Prototype계속해서 새로운 인스턴스가 생성된다.
결과
cart에 담긴 물품 : [붕어빵 1000 Fri Nov 21 15:40:43 KST 2025, 딸기우유 1500 500]
쇼핑 카트 생성 시점
cart2에 담긴 물품 : [지리산 절벽 암반수 3000 400]
<!-- singleton -->
<bean id="cart" class="...ShoppingCart" scope="singleton"/>
<!-- prototype -->
<bean id="cart" class="...ShoppingCart" scope="prototype"/>
Bean의 “생명주기”는 다음과 같다.
생성자 호출
초기화 메서드(init)
사용
소멸 전 destroy 메서드
→ 초기화할 때 이 메서드를 한 번 부름.
→ 끝날 때 이 메서드 한 번 부르는 것
⇒ init/destroy.
Owner 예제:
public class Owner {
public void openShop() {
System.out.println("사장님: 가게 문 염");
}
public void closeShop() {
System.out.println("사장님: 가게 문 닫음");
}
}
설정:
@Bean(initMethod = "openShop", destroyMethod = "closeShop")
public Owner owner() {
return new Owner();
}
컨테이너 생성:
ApplicationContext ctx =
new AnnotationConfigApplicationContext(ContextConfiguration.class);
// ... 로직 수행 후
((AnnotationConfigApplicationContext) ctx).close();
실행 순서:
즉,
“빈이 올라올 때 할 일, 내려갈 때 할 일”을 지정하는 것.
@Component
public class Owner {
@PostConstruct
public void openShop() {
System.out.println("사장님: 가게 문 염");
}
@PreDestroy
public void closeShop() {
System.out.println("사장님: 가게 문 닫음");
}
}
조건:
<bean id="owner"
class="...Owner"
init-method="openShop"
destroy-method="closeShop"/>
컨테이너 close() 할 때 destroy-method 호출되는 구조는 동일.
“코드에 하드코딩하지 말고 설정 값을 파일에 분리하자”라는 내용.
product-info.properties
bread.carpbread.name=붕어빵
bread.carpbread.price=1000
beverage.milk.name=딸기우유
beverage.milk.price=1500
beverage.milk.capacity=500
beverage.water.name=지리산암반수
beverage.water.price=3000
beverage.water.capacity=500
설정 클래스:
@Configuration
@PropertySource("section03/properties/subsection01/properties/product-info.properties")
public class ContextConfiguration {
@Value("${bread.carpbread.name:팥붕어빵}")
private String carpBreadName;
@Value("${bread.carpbread.price:0}")
private int carpBreadPrice;
@Value("${beverage.milk.name:}")
private String milkName;
@Value("${beverage.milk.price:0}")
private int milkPrice;
@Value("${beverage.milk.capacity:0}")
private int milkCapacity;
@Bean
public Product carpBread() {
return new Bread(carpBreadName, carpBreadPrice, new Date());
}
@Bean
public Product milk() {
return new Beverage(milkName, milkPrice, milkCapacity);
}
}
포인트:
@PropertySource("경로")@Value("${키:기본값}")${} 안에 properties의 key 치환자문법(playceholder)를 이용하여 properties에 저장된 key 값을 입력하면 value에 해당하는 값이 가져와져서 주입된다. 양 옆에 공백이 있을 경우 값을 읽어오지 못하므로 주의한다.:기본값은 키 없을 때 사용할 값필드 말고 파라미터에도 가능:
@Bean
public Product water(
@Value("${beverage.water.name:}") String waterName,
@Value("${beverage.water.price:0}") int waterPrice,
@Value("${beverage.water.capacity:0}") int waterCapacity
) {
return new Beverage(waterName, waterPrice, waterCapacity);
}
즉,
“에러 메시지, 안내 문구 등을 한국어/영어로 나누어 관리”할 때 사용.
message_ko_KR.properties
error.404=페이지를 찾을 수 없습니다!
error.500=개발자의 잘못입니다. 개발자는 누구? {0} 입니다. 현재시간 {1}
message_en_US.properties
error.404=Page Not Found!!
error.500=something wrong! The developer''s fault. who is devloper? It''s {0} at {1}
_ko_KR, _en_US가 Locale{0}, {1}은 자리 표시자 (매개변수로 치환)@Configuration
public class ContextConfiguration {
@Bean
public ReloadableResourceBundleMessageSource messageSource() {
ReloadableResourceBundleMessageSource ms =
new ReloadableResourceBundleMessageSource();
ms.setBasename("section03/properties/subsection02/i18n/message");
ms.setCacheSeconds(10);
ms.setDefaultEncoding("UTF-8");
return ms;
}
}
여기서 basename은
message_ko_KR.properties, message_en_US.properties의 “공통 앞부분” 이름.
ApplicationContext ctx =
new AnnotationConfigApplicationContext(ContextConfiguration.class);
String msg404kr = ctx.getMessage("error.404", null, Locale.KOREA);
String msg500kr = ctx.getMessage("error.500",
new Object[]{"여러분", new Date()}, Locale.KOREA);
String msg404us = ctx.getMessage("error.404", null, Locale.US);
String msg500us = ctx.getMessage("error.500",
new Object[]{"you", new Date()}, Locale.US);
{0}, {1} 등에 들어갈 값 배열→ Locale에 따라 알아서 ko/US 파일에서 메시지를 가져옴.
@Bean(initMethod="...")@PostConstruct@Bean(destroyMethod="...")@PreDestroy<bean init-method="" destroy-method="">.properties)@PropertySource로 파일 등록@Value("${key:기본값}")으로 값 주입message_ko_KR.properties, message_en_US.propertiesReloadableResourceBundleMessageSource 빈 등록getMessage("key", args, Locale)로 언어별 메시지 조회