F-LAB JAVA · 5주차 · Phase 8 · Spring 컨테이너
★ 깊이 파기 — 4주차 동시성과 만나는 지점
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
ApplicationContext 는 싱글톤 레지스트리로 모든 빈을 기본 싱글톤 (1개 인스턴스) 으로 관리하므로 getBean 을 100번 호출해도 같은 객체가 반환되며, 따라서 빈은 여러 스레드가 공유하므로 상태 (state) 를 갖지 않는 stateless 로 설계해야 한다.
Spring 빈은 기본적으로 싱글톤 이라, 컨테이너에 1개 인스턴스만 존재하고 getBean 을 몇 번 호출하든 같은 객체를 반환한다 (ApplicationContext = 싱글톤 레지스트리).
왜 싱글톤인가 — 서버 환경에서는 초당 수많은 요청이 같은 서비스·DAO 를 사용하는데, 매 요청마다 객체를 생성하면 메모리·GC 부담이 크므로, 1개를 공유하는 것이 효율적이다.
그런데 1개 빈을 여러 스레드가 동시에 사용하므로, 빈이 변경 가능한 인스턴스 상태 (필드) 를 가지면 동시성 문제 (4주차의 가시성·원자성) 가 발생한다 — 따라서 빈은 stateless 로 설계해야 한다.
전통 싱글톤 (getInstance()+ private 생성자) 은 구현이 번거롭고 테스트·상속이 어렵지만, Spring 싱글톤은 컨테이너가 관리하여 평범한 객체를 싱글톤으로 쓰며, 빈 필드로 ConcurrentHashMap 같은 스레드 안전한 객체나 final 불변 객체를 두는 것은 안전하다.
싱글톤 레지스트리 = 공용 복합기:
빈 = 공용 복합기 (1대):
- 회사에 1대 (싱글톤)
- 모든 직원(스레드) 공유
- getBean 100번 = 같은 복합기
왜 1대 (싱글톤):
- 직원마다 복합기 X (낭비)
- 1대 공유 (효율, 메모리/GC)
stateless 필요:
- 복합기에 "내 문서" 저장하면 (상태)
- 다른 직원이 보거나 섞임 (동시성 문제)
- → 출력하고 바로 가져감 (stateless)
상태 두면 (4주차 동시성):
- 가시성: 내 설정 다른 사람 안 보임
- 원자성: 동시 출력 섞임
안전한 필드:
- 복합기 자체 기능 (불변)
- 스레드 안전 부품 (ConcurrentHashMap)
→ OK
→ ApplicationContext = 싱글톤 레지스트리 (빈 1개 공유), stateless 필수 (동시성).
1. getBean 100번 = 1객체
2. ApplicationContext = 싱글톤 레지스트리
3. 모든 빈 기본 싱글톤
4. 왜 싱글톤 (메모리/GC)
5. stateless 필요 (멀티스레드)
6. 4주차 동시성 연결
7. 전통 vs Spring 싱글톤
8. ConcurrentHashMap 필드는 OK
9. 면접 + 자기 점검
// getBean 100번 = 1객체
for (int i = 0; i < 100; i++) {
ShipmentDao dao = ctx.getBean(ShipmentDao.class);
// 매번 같은 인스턴스
}
// 100번 호출, 1개 객체
ShipmentDao dao1 = ctx.getBean(ShipmentDao.class);
ShipmentDao dao2 = ctx.getBean(ShipmentDao.class);
System.out.println(dao1 == dao2); // true
System.out.println(dao1.equals(dao2)); // true
// 같은 객체 (싱글톤)
1개만 존재:
컨테이너 안:
- ShipmentDao 빈 1개
- 모든 요청 공유
→ getBean = 그 1개 반환
public class SingletonProof {
public void prove(ApplicationContext ctx) {
// 여러 번 조회
ShipmentDao dao1 = ctx.getBean(ShipmentDao.class);
ShipmentDao dao2 = ctx.getBean(ShipmentDao.class);
ShipmentDao dao3 = ctx.getBean(ShipmentDao.class);
// 모두 같은 인스턴스
assert dao1 == dao2; // true
assert dao2 == dao3; // true
// 100번, 1000번 호출해도 같은 빈
// → ILIC 의 431 API 가 같은 ShipmentDao 공유
}
}
class ShipmentDao { }
getBean 100번 = 1객체인 이유는?
답:
1. 같은 객체:
동일성:
1개 존재:
공유:
싱글톤 레지스트리:
ApplicationContext:
- 싱글톤 객체 등록·관리
- 빈 = 싱글톤
- 1개씩 보관
→ 싱글톤 레지스트리
레지스트리 (Registry):
등록소:
- 빈 이름 → 인스턴스
- 1개씩
- 조회 제공
Map<String, Object> (싱글톤)
싱글톤 관리:
컨테이너가:
- 빈 1개 생성
- 보관
- 재사용
→ 싱글톤 패턴 대신
→ 컨테이너가 보장
// ApplicationContext = 싱글톤 레지스트리
public class SingletonRegistry {
public void explain(ApplicationContext ctx) {
// 컨테이너 내부 (싱글톤 레지스트리):
// "shipmentDao" → ShipmentDao 인스턴스 (1개)
// "connectionMaker" → ConnectionMaker 인스턴스 (1개)
// "shipmentService" → ShipmentService 인스턴스 (1개)
// 각 빈 1개씩, 모든 요청 공유
// → 싱글톤 레지스트리
ShipmentDao dao = ctx.getBean(ShipmentDao.class);
// 등록된 1개 반환
}
}
class ShipmentDao { }
ApplicationContext = 싱글톤 레지스트리인가?
답:
1. 싱글톤 레지스트리:
레지스트리:
관리:
보장:
기본 싱글톤:
Spring 빈:
- 기본 스코프 = 싱글톤
- 명시 안 해도 싱글톤
@Scope("singleton") 기본
빈 스코프:
singleton (기본):
- 1개 인스턴스
prototype:
- 매번 새 객체
request/session:
- 웹 요청/세션별
// 기본 싱글톤 (명시 불필요)
@Service
public class ShipmentService {
// @Scope("singleton") 생략 (기본)
}
// 명시
@Service
@Scope("singleton")
public class ExplicitSingleton { }
// 프로토타입 (매번 새 객체)
@Component
@Scope("prototype")
public class StatefulBean {
// getBean 마다 새 인스턴스
}
// 상태 있는 빈 (드묾)
// 대부분 싱글톤 (기본)
@Repository
public class ShipmentDao { // 싱글톤
// 모든 요청 공유
}
@Service
public class ShipmentService { // 싱글톤
private final ShipmentDao shipmentDao;
public ShipmentService(ShipmentDao dao) { this.shipmentDao = dao; }
}
@RestController
public class ShipmentController { // 싱글톤
private final ShipmentService service;
public ShipmentController(ShipmentService s) { this.service = s; }
}
// 모두 싱글톤 → stateless 설계 필요 (다음)
모든 빈이 기본 싱글톤인가?
답:
1. 기본 싱글톤:
스코프:
프로토타입:
대부분:
서버 환경:
초당 수많은 요청:
- 같은 서비스/DAO 사용
- 매번 생성하면?
- 객체 폭증
매번 생성 문제:
요청마다 객체:
- 메모리 ↑
- GC 부담
- 생성 비용
초당 1000 요청 × 객체 N개
= 폭증
싱글톤 효율:
1개 공유:
- 메모리 절약
- GC 부담 ↓
- 생성 비용 1회
→ 서버 환경 적합
stateless 전제:
싱글톤 가능 이유:
- 빈이 stateless
- 상태 없음
- 공유 안전
→ 1개로 충분
// 왜 싱글톤 (ILIC 서버)
@Service
public class ShipmentService {
private final ShipmentDao shipmentDao; // 불변 (안전)
public ShipmentService(ShipmentDao dao) {
this.shipmentDao = dao;
}
// 메서드 — 상태 없음 (stateless)
public void processShipment(Shipment shipment) {
// 파라미터/지역 변수만 (스레드별)
validate(shipment);
shipmentDao.add(shipment);
}
private void validate(Shipment s) { }
}
// ILIC: 초당 수많은 요청
// - 매번 ShipmentService 생성하면 메모리/GC 폭증
// - 1개 공유 (싱글톤) → 효율
// - stateless 이므로 공유 안전
class ShipmentDao { void add(Shipment s) {} }
왜 싱글톤인가 (메모리/GC) ?
답:
1. 서버 환경:
매번 생성:
싱글톤 효율:
전제:
stateless (무상태):
빈이 인스턴스 상태(필드)를
갖지 않음.
- 변경 가능 필드 X
- 요청별 데이터는 파라미터/지역
왜 필요:
싱글톤 빈:
- 여러 스레드 공유
- 인스턴스 필드 = 공유 상태
상태 있으면:
- 동시성 문제
- 데이터 섞임
// ❌ 위험 (인스턴스 상태)
@Service
public class ShipmentServiceBad {
private Shipment currentShipment; // 인스턴스 상태 (위험!)
public void process(Shipment s) {
this.currentShipment = s; // 스레드 A 설정
// 스레드 B 가 동시에 다른 s 설정 → 섞임
save(this.currentShipment); // 어느 s?
}
private void save(Shipment s) { }
}
// ✓ 안전 (stateless)
@Service
public class ShipmentServiceGood {
private final ShipmentDao shipmentDao; // 불변 (안전)
public ShipmentServiceGood(ShipmentDao dao) {
this.shipmentDao = dao;
}
public void process(Shipment s) {
// 파라미터/지역 변수 (스레드별, 안전)
validate(s);
shipmentDao.add(s);
// 인스턴스 상태 변경 X
}
private void validate(Shipment s) { }
}
class ShipmentDao { void add(Shipment s) {} }
// stateless 설계 (ILIC)
@Service
public class FreightService {
private final FreightRateRepository rateRepo; // 불변 의존성
public FreightService(FreightRateRepository repo) {
this.rateRepo = repo;
}
// stateless 메서드
public BigDecimal calculate(Shipment shipment) {
// 모든 데이터를 파라미터/지역 변수로 (스레드별)
BigDecimal rate = rateRepo.findRate(shipment.getRoute());
BigDecimal weight = shipment.getWeight();
return weight.multiply(rate);
// 인스턴스 필드에 저장 X (stateless)
}
}
// ❌ 절대 하면 안 됨
// private BigDecimal lastCalculatedFreight; // 동시성 문제!
class FreightRateRepository {
BigDecimal findRate(String route) { return BigDecimal.ONE; }
}
stateless가 필요한 이유는? (멀티스레드)
답:
1. stateless:
왜:
위험:
안전:
4주차 동시성 연결:
싱글톤 빈 + 멀티스레드:
- 인스턴스 상태 = 공유 변수
- 가시성/원자성 문제
→ 4주차 동시성이 여기서
// 가시성 문제 (싱글톤 빈)
@Service
public class CounterService {
private boolean active = true; // 가시성 위험
// 스레드 A: active 변경
// 스레드 B: 변경 안 보일 수 있음 (캐시)
// → 4주차 가시성 문제
}
// 원자성 문제 (싱글톤 빈)
@Service
public class CounterService {
private int count = 0; // 원자성 위험
public void increment() {
count++; // 여러 스레드 동시 → 손실
// → 4주차 원자성 문제
}
}
// 정말 상태 필요하면 — 4주차 도구
@Service
public class SafeCounterService {
// Atomic (원자성 + 가시성)
private final AtomicInteger count = new AtomicInteger();
public void increment() {
count.incrementAndGet(); // 안전 (4주차 CAS)
}
// 또는 volatile (가시성)
private volatile boolean active = true;
}
// 4주차 동시성 도구로 안전하게
// 싱글톤 빈 + 4주차 동시성 (ILIC)
@Service
public class ShipmentMetricsService {
// 통계는 상태 필요 → 4주차 동시성 도구
private final AtomicLong totalProcessed = new AtomicLong(); // Atomic
private final LongAdder requestCount = new LongAdder(); // 고경쟁
private volatile boolean monitoringEnabled = true; // volatile
// 싱글톤 빈이지만 동시성 안전
public void recordProcessing() {
totalProcessed.incrementAndGet(); // 원자성 (4주차 CAS)
requestCount.increment(); // 고경쟁 (4주차 LongAdder)
}
public long getTotal() {
return totalProcessed.get(); // 가시성 (Atomic 내부 volatile)
}
}
// 싱글톤 빈에 상태 필요 시 → 4주차 동시성 도구 필수
// 5주차(싱글톤) + 4주차(동시성) 만나는 지점
4주차 동시성과의 연결은?
답:
1. 연결:
가시성:
원자성:
도구:
// 전통 싱글톤 (getInstance)
public class TraditionalSingleton {
private static final TraditionalSingleton INSTANCE
= new TraditionalSingleton();
private TraditionalSingleton() { } // private 생성자
public static TraditionalSingleton getInstance() {
return INSTANCE;
}
}
전통 싱글톤 단점:
- private 생성자 (상속 어려움)
- static (테스트 어려움)
- 인터페이스 구현 제약
- DI 어려움
- 전역 상태 (안티패턴 논란)
Spring 싱글톤:
평범한 객체:
- 일반 클래스
- 컨테이너가 1개 관리
- 인터페이스 OK
- 테스트 쉬움 (Mock)
- DI 자연스러움
| 항목 | 전통 | Spring |
|---|---|---|
| 구현 | getInstance | 평범한 객체 |
| 생성자 | private | public |
| 범위 | JVM 전역 | 컨테이너 |
| 테스트 | 어려움 | 쉬움 |
| DI | 어려움 | 자연 |
// 전통 싱글톤 (번거로움)
public class ConnectionPoolOld {
private static final ConnectionPoolOld INSTANCE = new ConnectionPoolOld();
private ConnectionPoolOld() { }
public static ConnectionPoolOld getInstance() { return INSTANCE; }
// 테스트 어려움, DI X, 상속 X
}
// Spring 싱글톤 (평범)
@Component
public class ConnectionPool {
// 평범한 클래스
// 컨테이너가 싱글톤 관리
// 인터페이스 구현 OK, 테스트 쉬움, DI 자연
}
// 사용 (DI)
@Service
public class ShipmentService {
private final ConnectionPool pool; // 주입 (전통은 getInstance)
public ShipmentService(ConnectionPool pool) {
this.pool = pool;
}
}
// Spring 싱글톤: 평범한 객체 + 컨테이너 관리
전통 싱글톤 (getInstance) vs Spring 싱글톤 차이는?
답:
1. 전통:
단점:
Spring:
차이:
안전한 필드:
싱글톤 빈에 가능:
- final 불변 객체
- 스레드 안전 객체 (ConcurrentHashMap)
- 읽기 전용 데이터
왜 OK:
ConcurrentHashMap:
- 스레드 안전 (내부 동기화)
- 동시 접근 안전
→ 싱글톤 빈 필드 OK
// 안전한 필드 (싱글톤 빈)
@Component
public class CacheService {
// 스레드 안전 (OK)
private final ConcurrentHashMap<Long, Shipment> cache
= new ConcurrentHashMap<>();
public void put(Shipment s) {
cache.put(s.getId(), s); // 스레드 안전
}
public Shipment get(Long id) {
return cache.get(id); // 스레드 안전
}
}
// ConcurrentHashMap = 스레드 안전 → 싱글톤 빈 OK
// 불변 객체 필드 (OK)
@Component
public class ConfigHolder {
private final FreightConfig config; // 불변 (OK)
public ConfigHolder() {
this.config = loadConfig();
}
// 불변이므로 공유 안전
private FreightConfig loadConfig() { return new FreightConfig(); }
record FreightConfig() {}
}
// 안전한 싱글톤 빈 필드 (ILIC)
@Component
public class ShipmentCache {
// 1. 스레드 안전 컬렉션 (OK)
private final ConcurrentHashMap<Long, Shipment> cache
= new ConcurrentHashMap<>();
// 2. 불변 의존성 (OK)
private final ShipmentDao shipmentDao;
// 3. 동시성 도구 (OK, 4주차)
private final AtomicLong hitCount = new AtomicLong();
public ShipmentCache(ShipmentDao dao) {
this.shipmentDao = dao;
}
public Shipment get(Long id) {
Shipment cached = cache.get(id); // 스레드 안전
if (cached != null) {
hitCount.incrementAndGet(); // 원자성 (4주차)
return cached;
}
Shipment loaded = shipmentDao.get(id);
cache.put(id, loaded); // 스레드 안전
return loaded;
}
// ❌ 절대 안 됨:
// private Shipment lastAccessed; // 변경 가능 상태 (위험)
}
class ShipmentDao { Shipment get(Long id) { return null; } }
// 싱글톤 빈 필드:
// - 스레드 안전 객체 OK
// - 불변 OK
// - 변경 가능 상태 X
빈에 ConcurrentHashMap 필드는 왜 OK인가?
답:
1. 안전한 필드:
왜 OK:
불변도 OK:
위험:
| Q | 핵심 답변 |
|---|---|
| getBean 100번? | 1객체 (싱글톤) |
| 싱글톤 레지스트리? | ApplicationContext |
| 기본 싱글톤? | 명시 안 해도 |
| 왜 싱글톤? | 메모리/GC 효율 |
| stateless? | 멀티스레드 공유 |
| 4주차 연결? | 인스턴스 상태 = 동시성 |
| 전통 vs Spring? | getInstance vs 평범한 객체 |
| ConcurrentHashMap? | 스레드 안전 (OK) |
| 위험한 필드? | 변경 가능 상태 |
| 안전한 필드? | 불변, 스레드 안전 |
답:
답:
답:
답:
답:
1. 싱글톤 레지스트리
2. stateless 필수 (4주차 연결)
3. 전통 vs Spring + 안전한 필드
이번 Unit에서 싱글톤 레지스트리를 봤다면, 다음은 DI (★ 깊이 + 5주차 완주 + 종합 졸업 시험).
🌱 Phase 8 — Spring 컨테이너
✅ Unit 8.1 빈 팩토리와 ApplicationContext
✅ Unit 8.2 getBean()의 동작
✅ Unit 8.3 싱글톤 레지스트리 ★깊이 ← 여기
⏭ Unit 8.4 의존관계 주입 DI ★깊이 (5주차 완주!)
✅ Part A — 동시성 마무리 (7 Unit)
🌱 Part B — 토비의 스프링
✅ Phase 3~7 (15 Unit)
🌱 Phase 8 — Spring 컨테이너 (3/4 진행)
총: 25/26 Unit (마지막 1개!)
★ 깊이 파기 — 싱글톤 레지스트리 (4주차 동시성 연결)