자바 백엔드 입문 20편 — Bean Scope Singleton과 Prototype

2026-05-16자바 백엔드 입문

자바 백엔드 입문 20편. Spring Bean의 6가지 Scope 중 핵심인 Singleton과 Prototype의 차이를 호텔 룸 비유로 풀고, 멀티스레드 환경에서 Singleton 함정과 해결법까지 풀어쓴 학습 노트.

📚 자바 백엔드 입문 · 20편 — Bean Scope Singleton과 Prototype

이 글은 자바 백엔드 입문 시리즈 59편 중 20편이에요. 16편에서 "Bean은 기본적으로 한 번만 만들어 재사용" 이라고 짧게 다뤘는데, 이번 20편이 그 "몇 번 만들 것인가" 의 답을 정리하는 글이에요. Bean Scope 라고 부르고, 6가지가 있는데 입문자는 두 개(Singleton·Prototype)만 잡으면 90% 끝납니다.

Bean Scope가 헷갈리는 이유

처음 "Scope" 라는 단어를 들으면 "범위?" 가 떠오르는데, 정확히 어떤 범위인지가 처음엔 잡히지 않아요.

이 글에서는 호텔 룸 비유로 풀어요. Singleton = "한 호텔에 단 한 개뿐인 프레지덴셜 스위트", Prototype = "체크인할 때마다 새 객실 할당". 끝까지 따라오시면 6가지 Scope가 한 그림에 들어와요.

Bean Scope = 같은 클래스로 객체를 몇 개 만드는가

Bean Scope"Spring 컨테이너가 이 클래스의 Bean을 몇 개 만들고, 언제까지 보관할지" 의 정책이에요. 같은 @Component 가 박혀 있어도 Scope에 따라 동작이 완전히 달라져요.

@Scope 어노테이션으로 지정해요. 안 박으면 기본값 = Singleton.

@Component
@Scope("singleton")   // 기본값 — 생략 가능
public class OrderService { ... }

@Component
@Scope("prototype")   // 매번 새 객체
public class OrderRequest { ... }

6가지 Scope 한눈에

Spring 공식 문서에 따르면 6개 Scope를 지원해요.

Scope 의미 사용 빈도
singleton 컨테이너에 1개. 모든 요청에서 같은 객체 재사용 99% (기본값)
prototype 호출할 때마다 새 객체 1% (가끔)
request HTTP 요청 1개당 1개. 요청 끝나면 폐기 가끔 (웹 전용)
session HTTP 세션 1개당 1개. 사용자 1명당 1개 드물게 (웹 전용)
application ServletContext 1개당 1개 매우 드묾 (웹 전용)
websocket WebSocket 세션 1개당 1개 매우 드묾 (웹 전용)

뒤 4개(request·session·application·websocket)는 웹 환경 전용. 일반 백엔드 입문에서는 거의 안 봐요. 입문자가 잡아야 할 건 singleton과 prototype 두 개.

Singleton — 호텔의 단 하나뿐인 스위트

Singleton Scope = "이 클래스의 Bean은 컨테이너 안에 단 한 개". 누가 몇 번을 요청해도 같은 객체가 반환돼요.

@Component
public class OrderService { ... }   // @Scope 안 박으면 자동 Singleton

ApplicationContext ctx = ...;
OrderService a = ctx.getBean(OrderService.class);
OrderService b = ctx.getBean(OrderService.class);
// a == b  →  true (완전히 같은 객체)

비유로 풀어볼게요. 호텔에 프레지덴셜 스위트가 단 한 개 있어요. 누가 예약해도 같은 방. 다음 손님도 같은 방. 다 만 한 방이라 "호텔 어디서 어떻게 요청해도 그 방 한 개" 가 돌아옵니다.

왜 Singleton이 기본일까? 성능 때문이에요. 한 컨트롤러나 서비스 객체를 매번 새로 만들면 GC 부담 폭증. 99% 케이스에서 "객체 안에 상태(state)가 없다면" 같은 객체를 재사용해도 문제없어요. 비즈니스 로직 클래스는 보통 stateless라 Singleton이 최적이에요.

Singleton의 함정 — 멀티스레드와 인스턴스 변수

Singleton이 성능엔 좋은데, 인스턴스 변수가 있으면 위험해져요.

@Component
public class OrderCounter {
    private int count = 0;   // ⚠ 인스턴스 변수 — Singleton에선 공유됨

    public int next() {
        count++;
        return count;
    }
}

OrderCounter 가 Singleton이라 모든 사용자 요청에서 같은 객체. 한 사용자가 next() 호출하면 count = 1, 동시에 다른 사용자가 호출하면 count = 2. 멀티스레드 환경에서 race condition 이 발생해요. 두 스레드가 동시에 count++ 하면 한 증가가 사라질 수 있어요.

해결법 3가지: 1. 인스턴스 변수 안 쓰기 — Spring 컨트롤러·서비스는 stateless로 짜는 게 표준 2. synchronized 또는 AtomicInteger 같은 thread-safe 도구 사용 3. 꼭 상태가 필요하면 Prototype Scope 로 바꾸기

99% 케이스에서 답은 1번. "왜 Spring 컨트롤러 안의 인스턴스 변수가 위험한가" 가 면접 단골 — Singleton이라 모든 스레드에서 공유되어 race condition 발생, 라는 답이 정답.

Prototype — 체크인할 때마다 새 방

Prototype Scope = "이 클래스를 요청할 때마다 새 객체". getBean() 또는 @Autowired 시점에 매번 new 가 일어나요.

@Component
@Scope("prototype")
public class OrderRequest {
    private final LocalDateTime createdAt = LocalDateTime.now();
}

OrderRequest a = ctx.getBean(OrderRequest.class);
OrderRequest b = ctx.getBean(OrderRequest.class);
// a != b  →  true (서로 다른 객체)
// a.createdAt != b.createdAt (생성 시각도 다름)

비유 — 호텔이 매번 새 방을 할당. 손님이 체크인할 때마다 다른 객실 번호를 받는 그림. "같은 카드(Bean 이름)로 요청해도 매번 다른 방".

언제 Prototype을 쓰나? 객체가 상태를 가져야 하고 그 상태가 호출별로 달라야 할 때. 예: 일회용 작업 컨텍스트, 사용자별 임시 데이터 holder. 실무에서는 정말 드물게 만나요.

⚠️ Prototype 함정 — 소멸 콜백 안 불림

Singleton Bean은 컨테이너 종료 시 @PreDestroy 자동 호출. Prototype Bean은 컨테이너가 더 이상 관리 안 해서 — 소멸 콜백이 자동으로 안 불려요. 자원 정리가 필요하면 사용자가 직접 처리해야 합니다.

Singleton이 Prototype을 주입받을 때 — 함정 + 해결

여기서 자주 막히는 함정이에요. Singleton Bean이 Prototype Bean을 주입받으면 — Singleton이 처음 만들어질 때 Prototype도 한 번만 주입돼서 "매번 새 객체" 가 안 일어나요.

@Component
public class OrderService {
    @Autowired
    private OrderRequest request;   // Prototype이지만 한 번만 주입됨
}

OrderService 가 Singleton이라 처음 만들어질 때 OrderRequest 한 개를 받아 보관. 그 후 모든 호출에서 같은 OrderRequest 가 쓰여요. Prototype 의미가 사라지는 거예요.

해결법 3가지: 1. ObjectProvider<T>request.getObject() 호출 시마다 새 객체 2. @Lookup 어노테이션 — 메서드 호출 시마다 새 객체 반환 3. Provider<T> (JSR-330 표준)

@Component
public class OrderService {
    @Autowired
    private ObjectProvider<OrderRequest> requestProvider;

    public void process() {
        OrderRequest req = requestProvider.getObject();   // 매번 새 객체
    }
}

실무에서는 ObjectProvider 패턴이 가장 깔끔.

Request·Session — 웹 환경 전용 Scope

웹 백엔드 짤 때 가끔 만나는 두 Scope.

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext { ... }   // HTTP 요청 1개당 1개

@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserSession { ... }      // 사용자 세션 1개당 1개
  • Request Scope — HTTP 요청 처리 동안만 유효한 객체. 요청 끝나면 폐기
  • Session Scope — 한 사용자가 로그인부터 로그아웃까지 사용하는 객체

proxyMode = ScopedProxyMode.TARGET_CLASS 가 필수. Singleton 컨트롤러가 Request Bean을 주입받을 때 "매 요청마다 새로운 RequestContext" 가 되려면 프록시가 필요하거든요.

실무는 보통 Request·Session 데이터를 "HttpServletRequest·HttpSession 매개변수" 로 처리해요. Scope를 직접 박는 일은 드물어요.

한 줄 정리 — Bean Scope = Bean을 몇 개 만들고 언제까지 보관하나의 정책. Singleton(기본) 이 99%. Prototype은 매번 새 객체. Request·Session은 웹 전용.

시험 직전 한 번 더 — Bean Scope 입문자가 매번 헷갈리는 것

  • Bean Scope = Bean의 인스턴스 수와 생명 주기 정책
  • 6가지 = singleton / prototype / request / session / application / websocket
  • 기본값 = singleton@Scope 안 박으면 자동
  • Singleton = 컨테이너에 1개. 모든 요청에서 같은 객체
  • Prototype = 요청할 때마다 새 객체
  • Request·Session·Application·WebSocket = 웹 환경 전용 (Spring Web MVC 필요)
  • Singleton 기본 = 성능 + 메모리 효율 (객체 새로 만들 비용 절감)
  • Spring 컨트롤러·서비스 = stateless로 짜는 게 표준
  • Singleton + 인스턴스 변수 = race condition 위험 (면접 단골)
  • 해결법 3가지 = ① 인스턴스 변수 X (1순위) ② thread-safe 도구 ③ Prototype
  • Prototype 함정 = @PreDestroy 자동 호출 X — 자원 정리 직접
  • Singleton이 Prototype 주입 받을 때 = 한 번만 주입돼서 Prototype 의미 X
  • 해결법 = ObjectProvider<T> / @Lookup / Provider<T> (JSR-330)
  • ObjectProvider.getObject() 호출 시마다 새 객체
  • Request Scope = HTTP 요청 1개당 1개, 끝나면 폐기
  • Session Scope = 사용자 세션 1개당 1개 (로그인~로그아웃)
  • Application Scope = ServletContext 1개당 1개 (앱 전체 생명주기)
  • WebSocket Scope = WebSocket 세션 1개당 1개
  • Request·Session Bean 주입 시 proxyMode = ScopedProxyMode.TARGET_CLASS 필수
  • 실무 = Singleton 99%, Prototype 1%, 나머지 4개 매우 드묾

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!