@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
AppConfig에서 new를 중복 호출하는 것 같은데 정말 객체가 여러개 생성되는거 아닐까?
그러면 메모리가 낭비되지 않을까?
순수한 자바 DI 컨테이너는 싱글톤이 아니다
public class SingletonTest {
@Test
@DisplayName("스프링 없는 순수한 DI 컨테이너")
void pureContainer() {
AppConfig appConfig = new AppConfig();
// 호출할 때마다 객체 새로 생성
MemberService memberService1 = appConfig.memberService();
MemberService memberService2 = appConfig.memberService();
// 서로 다른 인스턴스
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
assertThat(memberService1).isNotSameAs(memberService2);
}
}
memberservice 매번 new로 객체가 생성이 된다.
만약 요청이 많이 들어온다면? 메모리 낭비가 심각해짐!
문제점: 여러 명이 동시에 요청 -> 여러개의 객체를 생성
싱글톤이 해결책!
public class SingletonService {
private static final SingletonService instance = new SingletonService();
public static SingletonService getInstance() {
return instance;
}
private SingletonService() { }
public void logic() {
System.out.println("싱글톤 객체 로직 호출");
}
}
@Test
void singletonServiceTest() {
SingletonService s1 = SingletonService.getInstance();
SingletonService s2 = SingletonService.getInstance();
assertThat(s1).isSameAs(s2);
}
-private static final SingletonService instance = new SingletonService();
이 코드를 통해 객체를 딱 한번만 호출하고 이는 변경되지 않도록 설정
-private SingletonService()
private 생성자로 자동 생성자를 추가하지 않았다
외부에서 new로 생성하는 것을 막는다
문제점
1. 코드 복잡
2.DIP, OCP 위반 가능성
3. 테스트 어려움
4. private 생성자로 확장성 떨어짐
5. 유연성 부족
이 문제점을 해결하는 것이 싱글톤 컨테이너
@Test
@DisplayName("스프링 컨테이너는 싱글톤을 보장한다")
void springContainer() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService m1 = ac.getBean("memberService", MemberService.class);
MemberService m2 = ac.getBean("memberService", MemberService.class);
assertThat(m1).isSameAs(m2); // ✅ 성공!
}
두 번 호출해도 같은 객체를 사용한다 --> 싱글톤 적용
스프링 컨테이너에 등록된 빈들을 자동으로 싱글톤 형식으로 관리해준다.
즉 스프링은 객체를 하나만 생성해서 공유해준다!
싱글톤 컨테이너
- 스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.
- 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스
트리라 한다.
- 스프링 컨테이너의 이런 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.
- 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
- DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있다.
싱글톤 방식은 STATELESS(무상태)여야 한다!
여러 클라이언트가 하나의 인스턴스를 사용하기 때문에 문제점이 발생한다.
만약 사용자 A가 K라는 인스턴스의 값을 4로 지정하고 사용자 B는 10으로 지정한다면
마지막으로 지정한 10이라는 값이 모두에게 통일될 것이다.
그렇게 되면 사용자 A는 4라는 값이 나와야 하는 K가 10이 나오게 되는 것입니다!
package hello.core.singleton;
public class StatefulService {
private int price; //상태를 유지하는 필드
public void order(String name, int price) {
System.out.println("name = " + name + " price = " + price);
this.price = price; //여기가 문제!
}
public int getPrice() {
return price;
}
}
문제점 예시
package hello.core.singleton;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
public class StatefulServiceTest {
@Test
void statefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
// ThreadA: A사용자 10000원 주문
statefulService1.order("userA", 10000);
// ThreadB: B사용자 20000원 주문
statefulService2.order("userB", 20000);
// ThreadA: 사용자A 주문 금액 조회
int price = statefulService1.getPrice();
// ThreadA: 사용자A는 10000원을 기대했지만, 20000원 출력
System.out.println("price = " + price);
Assertions.assertThat(price).isEqualTo(20000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
-StatefulService 의 price 필드는 공유되는 필드인데, 특정 클라이언트가 값을 변경한다.
-사용자A의 주문금액은 10000원이 되어야 하는데, 20000원이라는 결과가 나왔다.
-실무에서 이런 경우를 종종 보는데, 이로인해 정말 해결하기 어려운 큰 문제들이 터진다.(몇년에 한번씩 꼭 만난
다.)
진짜 공유필드는 조심해야 한다! 스프링 빈은 항상 무상태(stateless)로 설계하자!!
Spring에서 @Bean 메서드를 여러 번 호출해도 객체가 하나만 생성되는 이유
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
만약 memberService와 orderService가 각각 호출되면...
memberRepository는 각각 주소값이 다른 두 객체가 생성되지 않을까?
테스트: 진짜 같은 인스턴스일까
@Test
void configurationTest() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);
System.out.println("memberService → memberRepository = " + memberService.getMemberRepository());
System.out.println("orderService → memberRepository = " + orderService.getMemberRepository());
System.out.println("AppConfig → memberRepository = " + memberRepository);
assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
}
결과
call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService
-memberRepository()는 딱 한 번만 호출됨!
-memberService, orderService, AppConfig 모두 같은 객체를 공유함!
스프링은 어떻게 이걸 가능하게 했을까?
By @Configuration + CGLIB 프록시 기술
Spring은 @Configuration이 붙은 클래스(AppConfig)를 내부적으로 감싸서
CGLIB라는 바이트코드 조작 라이브러리로 프록시(비서) 객체를 생성합니다.
bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$123abc
이는 다음과 같은 역할을 한다
// 실제 예상 동작
@Bean
public MemberRepository memberRepository() {
if (이미 컨테이너에 등록되어 있다면) {
return 기존 객체;
} else {
MemoryMemberRepository repo = new MemoryMemberRepository();
등록;
return repo;
}
}
만약 configuration을 뺀다면?
// @Configuration 삭제
public class AppConfig { ... }
출력 결과
call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService
call AppConfig.memberRepository
call AppConfig.memberRepository
직접 new를 두 번 했는데 왜 객체는 하나일까? 원리가 무엇일까?
먼저 다음 테스트 코드를 실행
@Test
void configurationDeep() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
}
출력 결과
bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70
CGLIB 프록시가 생성됨!
왜 이런 프록시 객체를 만들까?
이 프록시 객체 안에 싱글톤 보장 로직을 넣기 때문이다
아래 코드 로직
@Bean
public MemberRepository memberRepository() {
if (이미 컨테이너에 등록된 인스턴스가 있다면) {
return 기존 인스턴스;
} else {
MemoryMemberRepository repo = new MemoryMemberRepository();
등록;
return repo;
}
}
즉 프록시로 감싸서 중복 객체가 생성되지 않도록 관리
만약 @Configuration을 뺐다면?
//@Configuration 제거
public class AppConfig {
// 그대로 유지
}
출력 결과
call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService
call AppConfig.memberRepository
call AppConfig.memberRepository
-memberRepository()가 세 번이나 호출됨
-서로 다른 인스턴스가 생성되어 싱글톤이 깨져버림
'WINK-(Web & App) > Spring Boot 스터디' 카테고리의 다른 글
[2025 1학기 스프링 부트 스터디] 이상래 #6주차 (0) | 2025.05.18 |
---|---|
[2025 1학기 스프링 부트 스터디] 정다은 #6주 (1) | 2025.05.18 |
[2025 1학기 스프링부트 스터디] 장민주 #5주차 (0) | 2025.05.13 |
[2025 1학기 스프링부트 스터디] 여민호 #5주차 (0) | 2025.05.13 |
[2025 1학기 스프링 부트 스터디] 남윤찬 #5주차 (0) | 2025.05.13 |