← Kursa Dön
📄 Text · 15 min

Bean Scopes (Kapsamlar)

Giriş

Spring'de bir bean oluşturulduğunda, o bean'in ne kadar yaşayacağı ve kimlerle paylaşılacağı scope (kapsam) ile belirlenir. Varsayılan olarak tüm bean'ler singleton'dır — yani container'da tek bir instance vardır ve herkes aynı nesneyi kullanır. Ama her senaryo singleton'a uygun değildir.

Gerçek Dünya Analojisi

Bir ofisi düşünün:

  • Singleton = Ofisteki yazıcı. Herkes aynı yazıcıyı kullanır. Bir tane yeterli, herkes sırayla.

  • Prototype = Post-it kağıt bloğu. Herkes kendi bloğunu alır, birbirinin notlarını görmez.

  • Request = Toplantı odası. Her toplantı (HTTP request) için ayrı oda tahsis edilir, toplantı bitince oda boşalır.

  • Session = Kişisel çekmece. Her çalışanın (kullanıcı oturumu) kendi çekmecesi var, şirket terk edilene kadar kullanılır.

Neden Scope Önemli?

Yanlış scope seçimi ciddi hatalara yol açar:

  • Singleton bean'de mutable state → race condition (veri yarışı)

  • Prototype olması gereken bean singleton yapılırsa → veri kirlenmesi

  • Her request'te yeni instance gereken bean singleton yapılırsa → kullanıcılar arası veri sızıntısı


Singleton Scope — Varsayılan ve En Yaygın

Container'da bean'in tek bir instance'ı vardır. Tüm injection noktalarına aynı nesne verilir:

@Service // Varsayılan scope: singleton
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User findById(Long id) {
        return userRepository.findById(id).orElseThrow();
    }
}

Bu UserService tüm uygulama ömrü boyunca tek instance olarak yaşar. 100 controller, 50 diğer service — hepsi aynı UserService nesnesini paylaşır.

Singleton ve Thread Safety

Singleton bean'ler birden fazla thread tarafından eşzamanlı kullanılır. Bu yüzden mutable state tutmak tehlikelidir:

// ❌ TEHLİKELİ — Singleton'da mutable state
@Service
public class CounterService {
    private int count = 0; // Tüm thread'ler aynı değişkeni paylaşır

    public int increment() {
        return ++count; // Race condition! 2 thread aynı anda artırabilir
    }
}

// ❌ TEHLİKELİ — Kullanıcıya özel veri singleton'da
@Service
public class CartService {
    private List<Item> items = new ArrayList<>(); // Herkesin sepeti aynı!

    public void addItem(Item item) {
        items.add(item); // Ali'nin eklediğini Ayşe de görür!
    }
}

// ✅ GÜVENLİ — Stateless (durumsuz) singleton
@Service
public class PricingService {
    private final BigDecimal taxRate = new BigDecimal("0.20"); // immutable, final

    public BigDecimal calculatePrice(BigDecimal base) {
        return base.add(base.multiply(taxRate)); // Her çağrı bağımsız
    }
}

💡 İpucu: Singleton bean'lerde asla mutable instance variable tutmayın. Tüm veriler metot parametreleri veya local variable'lar üzerinden akmalıdır. Singleton = stateless.


Prototype Scope — Her Seferinde Yeni Instance

Her injection noktasında veya her getBean() çağrısında yeni bir instance oluşturulur:

@Component
@Scope("prototype") // veya @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ShoppingCart {
    private final List<CartItem> items = new ArrayList<>();
    private final String cartId = UUID.randomUUID().toString();

    public void addItem(CartItem item) {
        items.add(item);
    }

    public List<CartItem> getItems() {
        return Collections.unmodifiableList(items);
    }

    public BigDecimal getTotal() {
        return items.stream()
                .map(CartItem::getSubtotal)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    public String getCartId() {
        return cartId;
    }
}

Prototype Dikkat Noktaları

⚠️ Dikkat 1: Spring, prototype bean'lerin @PreDestroy metodunu çağırmaz! Bean oluşturulduktan sonra Spring onu "unutur". Yaşam döngüsü yönetimi sizin sorumluluğunuzdadır.

@Component
@Scope("prototype")
public class TempFileHandler {
    private File tempFile;

    @PostConstruct
    public void init() {
        tempFile = File.createTempFile("upload_", ".tmp");
    }

    @PreDestroy
    public void cleanup() {
        tempFile.delete(); // ⚠️ BU ASLA ÇAĞRILMAZ!
    }
}

⚠️ Dikkat 2: Prototype bean'i singleton'a inject ettiğinizde, prototype bir kez oluşturulur ve singleton onu sonsuza dek kullanır — yani prototype de fiilen singleton olur:

@Service // Singleton
public class OrderService {
    private final ShoppingCart cart; // Prototype

    public OrderService(ShoppingCart cart) {
        // ⚠️ Bu cart bir kez oluşturulup sonsuza dek kullanılır!
        // Her request'te yeni cart OLUŞMAZ!
        this.cart = cart;
    }
}

Çözüm: Scope Proxy veya ObjectFactory/Provider kullanın.


Scope Proxy — Farklı Scope'ları Birlikte Çalıştırma

Singleton bean'e kısa ömürlü (prototype, request, session) bir bean enjekte etmek istediğinizde proxy kullanmanız gerekir:

// Proxy ile — her erişimde yeni prototype instance
@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ShoppingCart {
    private final List<CartItem> items = new ArrayList<>();
    // ...
}

@Service // Singleton
public class OrderService {
    private final ShoppingCart cart; // Proxy nesnesi enjekte edilir

    public OrderService(ShoppingCart cart) {
        this.cart = cart;
        // Bu cart aslında bir PROXY
        // Her cart.addItem() çağrısında proxy, yeni prototype instance alır
    }

    public void addToCart(CartItem item) {
        cart.addItem(item); // Her çağrıda gerçek prototype'a yönlendirilir
    }
}

ObjectFactory / ObjectProvider Alternatifi

@Service
public class OrderService {
    private final ObjectFactory<ShoppingCart> cartFactory;

    public OrderService(ObjectFactory<ShoppingCart> cartFactory) {
        this.cartFactory = cartFactory;
    }

    public void processOrder() {
        ShoppingCart cart = cartFactory.getObject(); // Her çağrıda yeni instance
        cart.addItem(new CartItem("Laptop", 1, new BigDecimal("15000")));
        // İşlem bittikten sonra cart garbage collect olur
    }
}

// veya Provider kullanarak
@Service
public class OrderService {
    private final Provider<ShoppingCart> cartProvider; // Jakarta inject Provider

    public OrderService(Provider<ShoppingCart> cartProvider) {
        this.cartProvider = cartProvider;
    }

    public void processOrder() {
        ShoppingCart cart = cartProvider.get(); // Her çağrıda yeni instance
    }
}

Web Scopes — HTTP'ye Özel Kapsamlar

Web uygulamalarında kullanılan özel scope'lar. spring-boot-starter-web dependency'si gerektirir.

Request Scope — Her HTTP İsteğinde Yeni

@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
       proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
    private String correlationId = UUID.randomUUID().toString();
    private Instant startTime = Instant.now();
    private String clientIp;
    private String userId;

    public String getCorrelationId() { return correlationId; }
    public Instant getStartTime() { return startTime; }

    public void setClientIp(String ip) { this.clientIp = ip; }
    public String getClientIp() { return clientIp; }

    public void setUserId(String userId) { this.userId = userId; }
    public String getUserId() { return userId; }

    public long getElapsedMs() {
        return Duration.between(startTime, Instant.now()).toMillis();
    }
}
// Kullanım — her request kendi RequestContext'ini alır
@RestController
@RequiredArgsConstructor
public class ApiController {
    private final RequestContext requestContext; // Proxy enjekte edilir
    private final UserService userService;

    @GetMapping("/api/data")
    public ResponseEntity<DataDto> getData() {
        System.out.println("Request ID: " + requestContext.getCorrelationId());
        System.out.println("Elapsed: " + requestContext.getElapsedMs() + "ms");
        // Her HTTP request için farklı correlationId
        return ResponseEntity.ok(userService.getData());
    }
}

Session Scope — Kullanıcı Oturumu Boyunca

@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserPreferences {
    private String language = "tr";
    private String theme = "light";
    private String currency = "TRY";
    private int pageSize = 20;

    // Getter/Setter
    public String getLanguage() { return language; }
    public void setLanguage(String language) { this.language = language; }
    public String getTheme() { return theme; }
    public void setTheme(String theme) { this.theme = theme; }
}

Application Scope

Tüm ServletContext boyunca tek instance (singleton'a benzer ama servlet context başına):

@Component
@Scope("application")
public class AppMetrics {
    private final AtomicLong totalRequests = new AtomicLong(0);
    private final AtomicLong totalErrors = new AtomicLong(0);

    public void incrementRequests() { totalRequests.incrementAndGet(); }
    public void incrementErrors() { totalErrors.incrementAndGet(); }
    public long getTotalRequests() { return totalRequests.get(); }
    public long getTotalErrors() { return totalErrors.get(); }
}

Scope Karşılaştırma Tablosu

ScopeInstance SayısıYaşam SüresiKullanım Alanı@PreDestroy
singleton1Container ömrüStateless servisler✅ Çalışır
prototypeSınırsızKullanıcıya bağlıStateful nesneler❌ Çalışmaz
requestİstek başına 1HTTP isteğiİstek bazlı context✅ Çalışır
sessionSession başına 1Kullanıcı oturumuKullanıcı tercihleri✅ Çalışır
application1ServletContextUygulama metrikleri✅ Çalışır
websocketWS session başına 1WebSocket bağlantısıWS oturum verisi✅ Çalışır

Yaygın Hatalar ve Çözümleri

Hata 1: Singleton'da Mutable State

// ❌ Thread-unsafe singleton
@Service
public class ReportService {
    private List<String> currentReport = new ArrayList<>(); // Paylaşılıyor!

    public void addLine(String line) {
        currentReport.add(line); // 2 kullanıcı aynı anda eklerse → veri karışır
    }
}

// ✅ Stateless singleton
@Service
public class ReportService {
    public Report generate(ReportRequest request) {
        List<String> lines = new ArrayList<>(); // Local variable — thread-safe
        lines.add("Header: " + request.getTitle());
        // ...
        return new Report(lines);
    }
}

Hata 2: Prototype Bean'i Singleton'a Proxy'siz Inject Etmek

// ❌ Prototype fiilen singleton gibi davranır
@Component
@Scope("prototype")
public class TempProcessor { }

@Service
public class ProcessService {
    private final TempProcessor processor; // Bir kez oluşturulur, hep aynı!
    public ProcessService(TempProcessor processor) {
        this.processor = processor;
    }
}

// ✅ Scope proxy veya ObjectFactory kullanın
@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class TempProcessor { }

Hata 3: Web Scope Bean'i Test'te Kullanmak

// ❌ Web scope bean'i unit test'te çalışmaz
// (active HTTP request yok)
@Autowired
private RequestContext requestContext; // Hata!

// ✅ Test'te mock scope kullanın
@TestConfiguration
public class TestScopeConfig {
    @Bean
    @Primary
    public RequestContext testRequestContext() {
        return new RequestContext(); // Normal nesne
    }
}

Scope Seçim Rehberi — Karar Tablosu

Hangi scope'u ne zaman kullanmalısınız? Aşağıdaki tablo pratik kılavuzdur:

SenaryoScopeNeden
Service katmanı (UserService, OrderService)SingletonStateless — state tutmaz, thread-safe
Repository katmanıSingletonJPA EntityManager zaten thread-safe proxy
ControllerSingletonRequest bilgisi method parametrelerinde gelir
Geçici hesaplama nesnesi (ReportBuilder)PrototypeHer kullanımda sıfır state ile başlamalı
Request bazlı audit contextRequestHangi kullanıcı, hangi IP, hangi endpoint
Kullanıcı tercihleri (tema, dil, sepet)SessionOturum boyunca korunmalı
WebSocket bağlantı state'iWebSocketHer WebSocket session için ayrı

@Scope ile @Bean Kullanımı

@Component tabanlı tarama yerine @Configuration sınıfında @Bean ile de scope belirlenebilir:

@Configuration
public class ProcessorConfig {

    // Her injection'da yeni instance
    @Bean
    @Scope("prototype")
    public ReportBuilder reportBuilder() {
        return new ReportBuilder();
    }

    // Her HTTP request'te yeni instance
    @Bean
    @Scope(value = WebApplicationContext.SCOPE_REQUEST,
           proxyMode = ScopedProxyMode.TARGET_CLASS)
    public AuditContext auditContext() {
        return new AuditContext();
    }
}

Bu yaklaşım özellikle 3rd party kütüphane sınıflarına scope vermek istediğinizde kullanışlıdır — onlara @Component ekleyemezsiniz çünkü kaynak kodları sizde değildir.

Thread Safety ve Scope İlişkisi

// ❌ Singleton bean'de mutable field — RACE CONDITION
@Service
public class CounterService {
    private int count = 0; // Tüm thread'ler aynı değişkeni paylaşır!

    public int increment() {
        return ++count; // Thread-safe DEĞİL
    }
}

// ✅ Seçenek 1: AtomicInteger ile thread-safe
@Service
public class CounterService {
    private final AtomicInteger count = new AtomicInteger(0);

    public int increment() {
        return count.incrementAndGet(); // Thread-safe
    }
}

// ✅ Seçenek 2: Prototype scope — her çağıran kendi instance'ını alır
@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class CounterService {
    private int count = 0;

    public int increment() {
        return ++count; // Her instance bağımsız, thread-safe
    }
}

Kural: Singleton scope'ta mutable instance variable tutmaktan kaçının. Ya AtomicInteger/ConcurrentHashMap gibi thread-safe yapılar kullanın, ya da scope'u değiştirin. Ancak %95+ durumda stateless singleton en iyi seçenektir.

Application Scope (Servlet Context)

Nadir kullanılan bir scope daha vardır: application. Bu scope, ServletContext ömrü boyunca tek instance tutar — singleton'a benzer ama farkı, Spring container yerine Servlet container yaşam döngüsüne bağlı olmasıdır:

@Component
@Scope(value = WebApplicationContext.SCOPE_APPLICATION,
       proxyMode = ScopedProxyMode.TARGET_CLASS)
public class GlobalRateLimiter {
    private final Map<String, AtomicInteger> counters = new ConcurrentHashMap<>();

    public boolean isAllowed(String clientId, int maxPerMinute) {
        AtomicInteger counter = counters.computeIfAbsent(
            clientId, k -> new AtomicInteger(0)
        );
        return counter.incrementAndGet() <= maxPerMinute;
    }
}

Custom Scope Oluşturma

Spring'in yerleşik scope'ları yetmezse kendi scope'unuzu tanımlayabilirsiniz:

public class TenantScope implements Scope {

    private final ThreadLocal<Map<String, Object>> scopedObjects =
        ThreadLocal.withInitial(HashMap::new);

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Map<String, Object> scope = scopedObjects.get();
        return scope.computeIfAbsent(name, k -> objectFactory.getObject());
    }

    @Override
    public Object remove(String name) {
        return scopedObjects.get().remove(name);
    }

    @Override
    public String getConversationId() {
        return TenantContext.getCurrentTenant();
    }

    // registerDestructionCallback, resolveContextualObject — genelde no-op
}

// Custom scope'u kaydedin
@Configuration
public class ScopeConfig {
    @Bean
    public static CustomScopeConfigurer customScopeConfigurer() {
        CustomScopeConfigurer configurer = new CustomScopeConfigurer();
        configurer.addScope("tenant", new TenantScope());
        return configurer;
    }
}

// Kullanım
@Component
@Scope("tenant")
public class TenantCacheManager {
    // Her tenant için ayrı cache instance
}

Multi-tenant uygulamalarda bu yaklaşım çok güçlüdür — her kiracı (tenant) kendi bean instance'larını alır.


Özet

  • Singleton (varsayılan) → %95 durumda yeterli, stateless servisler için

  • Prototype → her kullanımda yeni instance, stateful nesneler için — @PreDestroy çalışmaz!

  • Request → HTTP isteği başına bir instance — request bazlı context bilgisi

  • Session → kullanıcı oturumu boyunca — tercihler, sepet (geleneksel web app)

  • Application → ServletContext ömrü boyunca — global limiter gibi nadir senaryolar

  • Singleton'a kısa ömürlü bean inject ederken proxyMode veya ObjectFactory kullanın

  • Singleton bean'de mutable state tutmayın — race condition kaçınılmaz

  • Custom scope ile tenant, conversation veya workflow bazlı bean yönetimi mümkün

  • @Bean + @Scope ile 3rd party sınıflara da scope verilebilir

  • Thread safety singleton scope'un en kritik konusu — AtomicInteger, ConcurrentHashMap kullanın

  • Scope seçiminde şüpheye düştüğünüzde singleton ile başlayın; gerçekten state tutmanız gerektiğinde prototype veya request'e geçin