IoC & Bean Model
Bu modülde Spring’in çalışma kalbine iniyoruz: nesneleri kim oluşturur, kim birbirine bağlar, hangi obje kaç kere yaratılır, hangi obje ne kadar yaşar ve Spring bu kararları uygulama çalışırken nasıl yönetir? Amacımız kavram ezberlemek değil; MiniBank uygulamasını büyütürken Spring container’ın ne yaptığını okuyabilir hale gelmek.
Bu modül sonunda neyi netleştirmiş olacağız?
İlk modülde Spring Boot’un uygulamayı ayağa kaldırırken bizim yerimize bazı default bean’leri oluşturduğunu, component scanning ile class’larımızı bulduğunu ve ApplicationContext içinde yönettiğini gördük. Bu modülde artık şu soruya odaklanıyoruz: Spring bulduğu class’ları gerçek çalışan objelere nasıl dönüştürüyor?
Bu konu çok temel gibi görünür; ama production’da karşılaşılan birçok Spring problemi aslında buraya dayanır. Bir bean neden bulunmadı? Neden aynı instance kullanılıyor? Prototype tanımladığım halde neden her çağrıda yeni obje gelmedi? Lazy açınca hata neden startup’ta değil de request sırasında çıktı? Bu soruların cevabı IoC ve Bean Model’i doğru okumaktan geçer.
IoC mental model
Objeleri doğrudan new ile yönetmek yerine Spring container’a emanet etmenin ne kazandırdığını anlayacağız.
Bean creation süreci
Spring’in önce bean tariflerini topladığını, sonra dependency graph’a göre gerçek instance’ları oluşturduğunu göreceğiz.
Scope ve lifecycle davranışı
Singleton, prototype, request, session ve lazy davranışlarını sadece tanım olarak değil, runtime etkisiyle öğreneceğiz.
Başlamadan önce kavramları aynı hizaya getirelim
Bu modülde sık geçecek kavramların hepsini en basit cümleyle sabitleyelim. Bazıları daha önce biliniyor olabilir; yine de aynı dili kullanmak eğitim boyunca çok işimize yarar.
Bean
En basit hali: Spring tarafından oluşturulan ve yönetilen objedir.
Neden önemli? Bir class’ın Spring özelliklerinden yararlanması için genellikle bean olarak container içinde bulunması gerekir.
Dependency
En basit hali: Bir class’ın çalışmak için ihtiyaç duyduğu başka obje ya da servistir.
Neden önemli? Dependency açık yazılmazsa class’ın gerçek ihtiyacı gizlenir; test etmek ve bakım yapmak zorlaşır.
Container
En basit hali: Bean’leri tutan, oluşturan ve birbirine bağlayan çalışma ortamıdır.
Neden önemli? Spring’de objelerin yaşam döngüsünü tek tek biz değil, container yönetir.
ApplicationContext
En basit hali: Spring’in en yaygın kullanılan container tipidir.
Neden önemli? Bean tanımları, gerçek bean instance’ları, environment, event ve resource yönetimi burada yaşar.
BeanDefinition
En basit hali: Bean’in nasıl oluşturulacağını anlatan tariftir.
Neden önemli? Spring önce tarifi çıkarır, sonra gerçek objeyi oluşturur; bu iki aşamayı ayırmak birçok davranışı anlamayı kolaylaştırır.
Scope
En basit hali: Bir bean’den kaç instance oluşacağını ve ne kadar yaşayacağını belirleyen kuraldır.
Neden önemli? Singleton ile prototype aynı davranmaz; web uygulamalarında request/session scope da farklı yaşam süresine sahiptir.
Lazy Initialization
En basit hali: Bean’in uygulama başlarken değil, ilk ihtiyaç duyulduğunda oluşturulmasıdır.
Neden önemli? Startup süresini azaltabilir ama hatayı startup’tan runtime’a taşıyabilir.
ObjectProvider
En basit hali: Bir bean’i ihtiyaç anında container’dan kontrollü şekilde almamızı sağlar.
Neden önemli? Singleton içinden her kullanımda yeni prototype instance almak gibi durumlarda temiz bir çözümdür.
Önce basit düşünelim, sonra Spring’in iç modeline yaklaşalım
IoC ve Bean Model’i tek cümleye indirgersek: “Objeleri Spring oluştursun, bağımlılıkları Spring bağlasın, yaşam döngüsünü Spring yönetsin.” Bu cümle basit görünür; ama arkasında uygulamanın mimarisini, test edilebilirliğini, startup davranışını ve runtime hatalarını etkileyen ciddi bir model vardır.
Bu yüzden bu bölümde kavramları ansiklopedi gibi listelemeyeceğiz. Her başlıkta önce “bu ne işe yarar?” diyeceğiz, sonra basit bir Java örneğiyle göreceğiz, ardından Spring’in içeride ne yaptığına bir tık daha yaklaşacağız. Sonunda da bunu MiniBank üzerinde kodlayacağız.
IoC nedir? Framework kontrolü ne demek?
IoC, yani Inversion of Control, nesneleri doğrudan bizim yaratmamız yerine bu kontrolü Spring’e vermemizdir. Daha sade söylersek: “Ben class’ımı yazarım, ihtiyacım olan dependency’leri constructor’da belirtirim; Spring uygun objeleri oluşturur ve bana verir.”
Önce Spring yokmuş gibi düşünelim
Spring kullanmadığımız bir Java uygulamasında bir class başka bir class’a ihtiyaç duyduğunda genellikle new kullanır. Küçük bir örnekte bu doğal görünür. Mesela AccountService kendi içinde AccountRepository yaratabilir. Ama uygulama büyüdükçe bu yaklaşım zincirleme bir probleme dönüşür: service repository’yi yaratır, repository connection objesine ihtiyaç duyar, başka bir service başka bir helper yaratır, testte bunları değiştirmek zorlaşır.
public class AccountService {
private final AccountRepository repository =
new AccountRepository();
public Account findByIban(String iban) {
return repository.findByIban(iban);
}
}
@Service
@RequiredArgsConstructor
public class AccountService {
private final AccountRepository repository;
public Account findByIban(String iban) {
return repository.findByIban(iban);
}
}
İkinci örnekte AccountService, repository’yi kendi oluşturmaz. Sadece constructor üzerinden ihtiyacını açıkça söyler. Spring uygulama başlarken AccountRepository bean’ini bulur, AccountService bean’ini oluşturur ve repository’yi constructor’a verir. Böylece AccountService’in işi sadece account iş mantığı olur; obje yaratma işi class’ın içinden çıkar.
IoC şu anlama gelir: “Uygulamanın nesne grafiğini ben elle kurmayayım; Spring kursun.” Nesne grafiği dediğimiz şey, hangi objenin hangi objeye bağlı olduğu ve bu objelerin hangi sırayla oluşturulacağıdır.
IoC sadece “dependency injection” mı?
Dependency Injection, IoC’nin en görünür sonucudur; ama IoC ondan daha geniştir. Spring sadece dependency vermez. Bean’leri bulur, bean tariflerini çıkarır, yaratım sırasını belirler, lifecycle hook’larını çalıştırır, scope kurallarını uygular, gerekirse proxy oluşturur ve uygulama kapanırken destroy aşamasını yönetir.
@Service, @Component gibi annotation’lar kullanılır.Senior seviyede neden önemli?
IoC modelini bilmeyen geliştirici Spring’i “annotation koyunca çalışan şey” gibi görür. Bu kısa vadede yeterli olabilir; ama production’da bean bulunmadığında, circular dependency çıktığında, test çok yavaşladığında veya singleton bean içinde state tutulduğu için request’ler birbirini etkilediğinde sorun yaşanır. IoC modelini bilen geliştirici ise hatayı “Spring bozuldu” diye değil, container’ın hangi aşamada ne yapamadığı şeklinde okur.
Bean creation süreci: Spring objeyi nasıl oluşturur?
Bir class’ın üzerinde @Service olması, o class’ın anında obje olduğu anlamına gelmez. Spring önce bu class’ın bir bean adayı olduğunu keşfeder, sonra onun için bir BeanDefinition oluşturur, daha sonra ihtiyaç duyulan gerçek instance’ı yaratır.
BeanDefinition ile gerçek bean arasındaki fark
Bunu bir yemek tarifi gibi düşünebiliriz. BeanDefinition tariftir: “Bu bean hangi class’tan yaratılacak, scope’u ne olacak, hangi dependency’leri var, lazy mi, init method’u var mı?” Gerçek bean ise bu tariften üretilmiş yemektir. Spring önce bütün tarifleri toplar; sonra bu tariflerden gerçek objeleri oluşturur.
BeanDefinition: Bean’in nasıl oluşturulacağını anlatır.
Bean instance: Runtime’da kullanılan gerçek objedir.
Spring startup sırasında kabaca ne olur?
@PostConstruct ve post processor adımları çalışır.Constructor injection bu süreçte neden doğal durur?
Constructor injection, bean’in çalışmak için ihtiyaç duyduğu dependency’leri daha yaratılırken ister. Bu Spring’e açık bir sinyal verir: “Bu dependency olmadan bu bean oluşturulamaz.” Böylece eksik bean varsa uygulama başlangıçta hata verir. Bu davranış production açısından değerlidir; çünkü hata request sırasında değil, uygulama ayağa kalkarken görülür.
@Service
@RequiredArgsConstructor
public class TransferService {
private final AccountService accountService;
private final TransferRepository transferRepository;
public void transfer(TransferCommand command) {
// dependency'ler burada null olamaz; bean oluşurken zorunlu gelmiştir.
}
}
Bean creation hatalarını nasıl düşünmeliyiz?
Spring startup’ta “bean yaratamadım” diyorsa genelde şu kategorilerden biri vardır: dependency eksiktir, aynı tipten birden fazla bean vardır ve Spring hangisini seçeceğini bilemez, circular dependency vardır, constructor içinde hata fırlatılmıştır, ya da profile/conditional nedeniyle beklenen bean hiç oluşmamıştır. Bu modülde özellikle dependency graph ve scope tarafını netleştiriyoruz.
Bean scope’ları: Singleton, Prototype, Request, Session
Scope, bir bean’den kaç tane instance oluşturulacağını ve bu instance’ın ne kadar süre yaşayacağını belirler. Bu sadece teorik bir ayar değildir; memory kullanımı, thread-safety, state yönetimi ve web request davranışı üzerinde doğrudan etkisi vardır.
Singleton — default davranış
Spring’de default scope singleton’dır. Yani ApplicationContext içinde ilgili bean’den tek instance oluşturulur ve uygulama boyunca aynı instance tekrar tekrar kullanılır. Service, repository, controller gibi çoğu bean için bu doğrudur; çünkü bu class’lar genelde state tutmamalı, gelen request’e göre sadece işlem yapmalıdır.
@Service
public class AccountService {
// @Scope yazmasak da default olarak singleton'dır.
}
Singleton bean aynı instance olarak tüm request’lerde kullanılır. Bu yüzden request’e özel veriyi service field’ında tutmak tehlikelidir. Örneğin private String currentUser; gibi bir alan singleton service içinde tutulursa, farklı request’ler birbirini etkileyebilir.
Prototype — her talepte yeni instance
Prototype scope, container’dan her istendiğinde yeni instance oluşturulması anlamına gelir. Kısa ömürlü, state taşıyan, her kullanımda taze olması gereken objelerde düşünülebilir. Ancak önemli bir detay var: prototype bean’in lifecycle’ı singleton kadar uzun süre Spring tarafından takip edilmez. Spring prototype’ı oluşturur ve verir; sonrasındaki yaşam yönetimi çoğunlukla kullanan tarafa kalır.
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ReferenceNumberGenerator {
private final String instanceId = UUID.randomUUID().toString();
public String nextReference() {
return "REF-" + instanceId.substring(0, 8);
}
}
Request ve Session scope
Web uygulamalarında request scope, her HTTP request için yeni bean instance anlamına gelir. session scope ise her HTTP session için ayrı instance demektir. Bunlar özellikle web context gerektirir ve service katmanında rastgele kullanılmamalıdır. Çünkü bean’in yaşam süresi artık uygulama değil, HTTP request veya session ile ilişkilidir.
Singleton
ApplicationContext içinde tek instance. Service/repository için en yaygın ve default davranış.
Prototype
Container’dan her talepte yeni instance. Kısa ömürlü stateful işler için düşünülebilir.
Request
Her HTTP request için yeni instance. Web context’e bağlıdır.
Session
Her HTTP session için yeni instance. Kullanıcı oturumu boyunca yaşar.
MiniBank’ta scope kararını nasıl vereceğiz?
MiniBank’ta AccountService, TransferService, CustomerService gibi servisler singleton olacak. Çünkü bunlar request’e özel state tutmayacak. Ancak referans numarası veya müşteri numarası üretmek için her çağrıda taze state isteyen küçük generator objelerini prototype olarak modelleyeceğiz. Bu sayede scope davranışını gerçek bir ihtiyaç üzerinden göreceğiz.
Lazy beans ve initialization mantığı
Normalde singleton bean’ler uygulama başlarken oluşturulur. Lazy initialization ise bean’i startup sırasında değil, ilk ihtiyaç duyulduğu anda oluşturur. Bu bazen startup süresini azaltır; ama hataları da ilk kullanım anına taşıyabilir.
Neden lazy kullanılır?
Büyük uygulamalarda yüzlerce bean olabilir. Bazı bean’ler nadiren kullanılır; örneğin sadece admin operasyonlarında çalışan bir export service, sadece özel bir batch akışında kullanılan bir parser veya sadece belirli profile’da devreye giren bir integration client. Bu bean’leri startup’ta oluşturmak yerine ilk kullanımda oluşturmak mantıklı olabilir.
@Service
@Lazy
public class HeavyReportService {
public HeavyReportService() {
// Ağır initialization örneği
}
}
spring:
main:
lazy-initialization: true
Risk nerede?
Lazy bean, startup’ı daha hızlı gösterebilir ama gerçek hatayı gizleyebilir. Constructor’da eksik config okuyan, dependency’si yanlış olan veya initialization sırasında hata fırlatan bir bean düşünelim. Eager yaratımda bu hata startup sırasında çıkar. Lazy yaratımda ise uygulama başarılı başlamış gibi görünür; ama ilgili endpoint’e ilk request geldiğinde patlar. Production’da bu davranış istenmeyebilir.
Lazy initialization performans ayarı gibi görünür ama hata zamanlamasını değiştirir. Kritik servislerde fail-fast davranışı genellikle daha değerlidir: uygulama yanlış config ile ayağa kalkmasın, başlangıçta hata versin.
Lazy dependency injection ile karıştırmayalım
@Lazy bazen circular dependency’yi ertelemek için de kullanılır. Bu teknik olarak çalışabilir; ama çoğu zaman tasarım problemini gizler. İlk modüldeki yaklaşımımızı hatırlayalım: döngüsel bağımlılık varsa bunu mümkünse yeni bir sorumluluk sınırıyla çözmek daha temizdir.
Prototype bean’in singleton içine inject edilme problemi
Bu bölüm modülün en kritik noktasıdır. Çünkü “prototype her seferinde yeni instance demek” cümlesi doğrudur; ama eksiktir. Prototype bean’in ne zaman istendiği çok önemlidir. Eğer prototype bean singleton bean oluşturulurken constructor’a verilirse, o singleton içinde aynı prototype instance kullanılmaya devam eder.
Yanlış beklenti
Diyelim ki ReferenceNumberGenerator prototype, TransferReferenceService singleton. Eğer prototype generator’ı doğrudan constructor injection ile singleton service içine verirsek, Spring singleton service’i oluştururken bir tane generator yaratır ve verir. Sonraki method çağrılarında aynı service instance kullanılacağı için aynı generator referansı kullanılmaya devam eder.
@Service
@RequiredArgsConstructor
public class TransferReferenceService {
private final ReferenceNumberGenerator generator;
public String createReference() {
// generator prototype olsa bile bu field aynı referansı tutar.
return generator.nextTransferReference();
}
}
Doğru mental model
ObjectProvider ile ihtiyaç anında almak
Eğer singleton service içinde her method çağrısında yeni prototype instance istiyorsak, dependency’yi doğrudan instance olarak değil, provider olarak alabiliriz. ObjectProvider<ReferenceNumberGenerator> bize her ihtiyaç anında container’dan yeni generator isteme imkanı verir.
@Service
@RequiredArgsConstructor
public class TransferReferenceService {
private final ObjectProvider<ReferenceNumberGenerator> generatorProvider;
public String createReference() {
ReferenceNumberGenerator generator = generatorProvider.getObject();
return generator.nextTransferReference();
}
}
Scope sadece annotation değildir; injection zamanı ve kullanım şekliyle birlikte düşünülür. Prototype bean’i singleton içine direkt vermek ile ihtiyaç anında provider üzerinden almak aynı davranış değildir.
MiniBank Case 02 — Bean Scope & Reference Generation
Bu case’de MiniBank’a transfer referansı ve müşteri numarası üretimi ekliyoruz. Eğitmen transfer referansı tarafını birlikte kodlayacak; katılımcılar aynı pattern’i customer tarafına uygulayacak. Hedefimiz hata avlamak değil, scope davranışını gerçek bir geliştirme adımıyla öğrenmek. Görev listesi, doğrulama komutları, kod adımları ve kabul kriterleri Lab Dashboard'da.
Lab sırasında geride kalırsan lab-02-complete branch'ine geçip herkesle aynı checkpoint'ten devam edebilirsin.
Akılda kalacak noktalar
Class’lar kendi dependency’lerini yaratmaz; ihtiyaçlarını açıkça bildirir.
Tarif ile gerçek obje ayrımını bilmek startup davranışını anlamayı kolaylaştırır.
Service içinde request’e özel mutable state tutmak ciddi concurrency problemleri yaratabilir.
Prototype bean singleton içine direkt inject edilirse singleton oluşturulurken alınan referans saklanır.
Singleton içinden her kullanımda yeni prototype instance almak için temiz bir yöntemdir.
5 soru ile hızlı kontrol
1. IoC en basit haliyle ne demektir?
Objeleri ve bağımlılıklarını doğrudan bizim yönetmemiz yerine, bu kontrolü Spring container’a vermemizdir.
2. BeanDefinition ile bean instance arasındaki fark nedir?
BeanDefinition bean’in nasıl oluşturulacağını anlatan tariftir. Bean instance ise runtime’da kullanılan gerçek objedir.
3. Singleton scope ne anlama gelir?
ApplicationContext içinde ilgili bean’den tek instance oluşturulması ve aynı instance’ın uygulama boyunca tekrar kullanılmasıdır.
4. Prototype bean singleton içine direkt inject edilirse ne olur?
Prototype bean, singleton bean oluşturulurken bir kez inject edilir. Her method çağrısında otomatik olarak yenilenmez.
5. ObjectProvider neden kullanılır?
Bir bean’e ihtiyaç anında erişmek, özellikle singleton içinden her kullanımda yeni prototype instance almak için kullanılır.