쇼핑카트에 제품들을 넣는 것으로 Bean에 대한 인스턴스의 범위도 알아보려고 한다.
일단 이 게시글에서 공통적으로 사용할 클래스들은 Product , Beverage , Bread , ShoppingCart 4개 클래스이다.
Product
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.ToString;
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Product {
private String name;
private int price;
}
Beverage
import lombok.ToString;
@ToString
public class Beverage extends Product{
private int capacity;
public Beverage() {
}
public Beverage(String name, int price, int capacity) {
super(name, price);
this.capacity = capacity;
}
}
Bread
import lombok.ToString;
import java.util.Date;
@ToString
public class Bread extends Product{
private java.util.Date bakedDate;
public Bread() {
}
public Bread(String name, int price, Date bakedDate) {
super(name, price);
this.bakedDate = bakedDate;
}
}
ShoppingCart
import java.util.ArrayList;
import java.util.List;
public class ShoppingCart {
private final List<Product> items;
public ShoppingCart() {
items = new ArrayList<>();
}
public void addItem(Product item) {
items.add(item);
}
public List<Product> getItems() {
return items;
}
}
롬복을 사용했기때문에 build.gradle에 대한 것도 남겨두겠다.
dependencies {
testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
// https://mvnrepository.com/artifact/org.springframework/spring-context
implementation 'org.springframework:spring-context:6.1.11'
// https://mvnrepository.com/artifact/org.projectlombok/lombok
implementation 'org.projectlombok:lombok:1.18.24'
annotationProcessor 'org.projectlombok:lombok:1.18.24'
}
bean scope란 스프링 빈이 생성될 때 생성되는 인스턴스의 범위를 의미한다.
| Bean Scope | Description |
|---|---|
| Singleton | 하나의 인스턴스만을 생성하고, 모든 빈이 해당 인스턴스를 공유하여 사용한다. |
| Prototype | 매번 새로운 인스턴스를 생성한다. |
| Request | HTTP 요청을 처리할 때마다 새로운 인스턴스를 생성하고, 요청 처리가 끝나면 인스턴스를 폐기한다. 웹 애플리케이션 컨텍스트에만 해당된다. |
| Session | HTTP 세션 당 하나의 인스턴스를 생성하고, 세션이 종료되면 인스턴스를 폐기한다. 웹 애플리케이션 컨텍스트에만 해당된다. |
Singleton은 애플리케이션 내에서 하나의 인스턴스만 사용, 모든 빈이 해당 인스턴스를 공유하며 사용한다.
- 장점 : 메모리 사용량 감소, 성능 향상
ContextConfiguration
import com.jehun.common.Beverage;
import com.jehun.common.Bread;
import com.jehun.common.Product;
import com.jehun.common.ShoppingCart;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
@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") // 기본값
public ShoppingCart cart() {
return new ShoppingCart();
}
}
Application
package com.ohgiraffers.section01.scope.singleton;
import com.ohgiraffers.common.Beverage;
import com.ohgiraffers.common.Bread;
import com.ohgiraffers.common.Product;
import com.ohgiraffers.common.ShoppingCart;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Application {
public static void main(String[] args) {
/* 설명. 빈 설정 파일을 기반으로 IoC 컨테이너 생성 */
ApplicationContext context = new AnnotationConfigApplicationContext(ContextConfiguration.class);
/* 설명. 붕어빵, 딸기우유, 쌈디수 등의 빈 객체를 반환 받는다. */
Product carpBread = context.getBean("carpBread", Bread.class);
Product milk = context.getBean("milk", Beverage.class);
Product water = context.getBean("water", Beverage.class);
/* 설명. 첫 번째 손님이 쇼핑 카트를 꺼내 물건을 담는다. */
ShoppingCart cart1 = context.getBean("cart", ShoppingCart.class);
cart1.addItem(carpBread);
cart1.addItem(milk);
/* 설명. 붕어빵과 쌈디수가 담겨있다. */
System.out.println("cart1에 담긴 내용 : " + cart1.getItems());
/* 설명. 두 번째 손님이 쇼핑 카트를 꺼내 물건을 담는다. */
ShoppingCart cart2 = context.getBean("cart", ShoppingCart.class);
cart2.addItem(water);
/* 설명. 붕어빵과 딸기우유와 쌈디수가 담겨있다. */
System.out.println("cart2에 담긴 내용 : " + cart2.getItems());
/* 설명. 두 카드의 hashcode를 출력해보면 동일한 것을 볼 수 있다. */
System.out.println("cart1의 hashcode : " + cart1.hashCode());
System.out.println("cart2의 hashcode : " + cart2.hashCode());
}
}
Application 에서 손님 2명이 각각 쇼핑 카트를 활용해 상품을 담는다고 했지만, Singleton으로 관리되는 cart는 원래 하나의 객체이므로 동일한 카트에 물건을 담는 상황이 발생.
-> 기본값이 singleton 스코프가 아닌 prototype 스코프가 필요할 수도 있다.
실행결과

prototype스코프를 갖는 Bean은 매번 새로운 인스턴스를 생성한다
- 장점 : 객체 생성에 대한 부담을 줄일 수 있다.
ContextConfiguration 에서 Scope 어노테이션만 prototype으로 바꿔주자.
...
@Bean
@Scope("prototype")
public ShoppingCart cart() {
return new ShoppingCart();
}
...
그대로 실행하면 다른 getBean()으로 인스턴스를 꺼내올 때마다 새로운 인스턴스를 생성하게 돼서 다르게 나온다.
실행결과

스프링 빈은 초기화(init)와 소멸화(destroy)의 라이프 사이클을 가지고 있다
객체가 생성되고 소멸될 때 추가적인 작업을 할 수 있다.
위의 클래스에서 시작할 때 Owner(사장)가 문을 열고, 끝날 때 닫는 것을 추가하려고 한다.
Owner
public class Owner {
public void openShop() {
System.out.println("사장님이 가게 문을 열었습니다. 이제 쇼핑을 하실 수 있습니다.");
}
public void closeShop() {
System.out.println(" 사장님이 가게 문을 닫았습니다. 이제 쇼핑을 하실 수 없습니다.");
}
}
이걸 이제 ContextConfiguration에서도 init, destory 메소드를 활용할 수 있다.
ContextConfiguration
public class ContextConfiguration {
// ...기존
/* 설명. Owner 빈 추가 */
@Bean(initMethod = "openShop", destroyMethod = "closeShop")
public Owner owner() {
return new Owner();
}
}
Application 에서도 종료하는 메소드를 추가해줘야 한다.
Application
public class Application {
public static void main(String[] args) {
// 기존의 코드
((AnnotationConfigApplicationContext)context).close();
}
}
destroy 메소드는 Bean 객체 소멸 시점에 동작하므로 컨테이너가 종료 되지 않을 경우 확인할 수 없다.
가비지 컬렉터가 해당 빈을 메모리에서 지울 때 destroy 메소드가 동작하게 되는데 메모리에서 지우기 전에 프로세스는 종료되기 때문이다.
따라서 아래와 같이 강제로 컨테이너를 종료시키면 destroy 메소드가 동작할 것이다.
실행 결과

Properties: 키/값 쌍으로 이루어진 간단한 파일이다. 보통 소프트웨어 설정 정보를 저장할 때 사용된다.
Properties 파일의 각 줄은 다음과 같은 형식으로 구성된다. 주석은 #으로 시작하며, 빈 줄은 무시된다.
# 주석
key=value
Properties 파일이 있고 yml파일이라는 것도 있으니 알아두면 좋다
지금은 properties를 만드려고 한다.
resources 폴더 하위 경로에 넣으면 된다.
section03/properties/subsection01/properties/product-info.properties
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
위처럼 파일을 만들어주는데 @PropertySource 어노테이션으로 경로를 적어서 사용하고 싶은 곳에서 읽어올 수도 있다.
그리고 @Value 어노테이션을 이용해서 properties의 값을 읽어올 수 있다.
ContextConfiguration
import com.jehun.common.Beverage;
import com.jehun.common.Bread;
import com.jehun.common.Product;
import com.jehun.common.ShoppingCart;
import com.jehun.section02.initdestory.Owner;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.annotation.Scope;
@Configuration
@PropertySource("section03/properties/subsection01/properties/product-info.properties")
public class ContextConfiguration {
/* 추가적인 팁 : 리터럴 변수
* #{}
*/
// 바인딩 변수
@Value("${bread.carpbread.name}")
private String carpBreadName;
@Value("${bread.carpbread.price}")
private int carpBreadPrice;
@Value("${beverage.milk.name}")
private String beverageMilkName;
@Value("${beverage.milk.price}")
private int beverageMilkPrice;
@Value("${beverage.milk.capacity}")
private int beverageMilkCapacity;
@Value("${beverage.water.name}")
private String beverageWaterName;
@Value("${beverage.water.price}")
private int beverageWaterPrice;
@Value("${beverage.water.capacity}")
private int beverageWaterCapacity;
// 기존 코드와 동일
}
@Value 어노테이션 안에는 key 값을 넣어주면 된다. (중괄호 주의..)
Application
import com.jehun.common.Beverage;
import com.jehun.common.Bread;
import com.jehun.common.Product;
import com.jehun.common.ShoppingCart;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Application {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(ContextConfiguration.class);
String[] beanNames = context.getBeanDefinitionNames();
for (String beanName : beanNames) {
System.out.println("beanName = " + beanName);
}
Product carpBread = context.getBean("carpBread", Bread.class);
Product milk = context.getBean("milk", Beverage.class);
Product water = context.getBean("water", Beverage.class);
ShoppingCart cart1 = context.getBean("cart", ShoppingCart.class);
cart1.addItem(carpBread);
cart1.addItem(milk);
System.out.println("cart1에 담긴 내용 : " + cart1.getItems());
ShoppingCart cart2 = context.getBean("cart", ShoppingCart.class);
cart2.addItem(water);
System.out.println("cart2에 담긴 내용 : " + cart2.getItems());
System.out.println("cart1의 hashcode : " + cart1.hashCode());
System.out.println("cart2의 hashcode : " + cart2.hashCode());
((AnnotationConfigApplicationContext)context).close();
}
}
실행결과

만약 한글이 깨진다면 ctrl + alt + s 로 setting에 들어가서

이렇게 UTF-8로 해준 뒤 properties에 ???로 된 부분을 다시 고친 뒤 실행하면 잘 될 것이다.