Spring Context: Bean Scope ve Yaşam Döngüleri
23 min read

Spring Context: Bean Scope ve Yaşam Döngüleri

Spring Context tarafından yönetilen bean'lerin sahip olabileceği scope'lara ve yaşam döngülerine detaylıca bakıyoruz.
Spring Context: Bean Scope ve Yaşam Döngüleri
Photo by imso gabriel / Unsplash

Şimdiye kadar Spring Context'i ve Spring tarafından yönetilen instance yani bean'leri oluşturma ve yönetme konusunda güzel bir yol aldık. Lakin Spring'in bean'leri nasıl ve ne zaman yarattığına hiç odaklanmadık. Bu açıdan bakıldığında, yalnızca framework'ün varsayılan yaklaşımlarına güvenmiş olduk. Fakat uygulamalarımız büyüyüp karmaşıklaşmaya başladığında  sadece framework'e güvenmek yeterli olmayabilir. Bu nedenle bu yazımızda Spring'in context'inde yer alan bean'leri nasıl yönettiğini daha detaylı inceleyeceğiz.

Spring, bean oluşturmak ve yaşam döngülerini yönetmek için birden fazla farklı yaklaşıma sahiptir. Spring dünyasında bu yaklaşımlara scope adı veriyoruz. Bu yazıda, Spring uygulamalarında sık sık bulabileceğiniz iki scope'u ele alıyoruz: singleton ve prototype.

NOT: İlerleyen yazılarda web uygulamaları için geçerli olan üç bean scope'unu daha tartışıyor olacağız.: request, session ve application.

Singleton, Spring'te bir bean'in varsayılan (default) scope'udur ve şimdiye kadarki yazılarımızda hep bu scope'u kullandık. Bu nedenle şimdi singleton bean scope'unu tartışarak başlayacağız.

1. Singleton Bean Scope

Singleton bean scope, Spring'in context'indeki bean'leri yönetmek için varsayılan yaklaşımını tanımlar. Aynı zamanda production uygulamalarında en çok karşılaşacağınız bean scope'udur.

Devam etmeden önce Singleton Pattern yazımızı okuyarak daha iyi bir temel atabilirsiniz:

Singleton Pattern - Tek Nesne Deseni
Singleton, bir sınıfın yalnızca tek bir instance’a sahip olmasını ve bu instance’a genel bir erişim noktası sağlamanıza olanak tanır.

Bir sonraki başlıkta, Spring'in singleton bean'leri nasıl oluşturduğunu ve yönettiğini öğrenerek tartışmamıza başlıyoruz ki bu da onları nerede kullanmanız gerektiğini anlamak için oldukça önemlidir. Bu amaçla, bean'leri tanımlamak için kullanabileceğiniz farklı yaklaşımları kullanan iki örnek alacağız ve Spring'in bu bean'ler için davranışını analiz edeceğiz. Daha sonra gerçek dünyadaki senaryolarda singleton bean kullanmanın kritik yönlerini tartışacağız. Bu bölümü, iki singleton bean instantiation (ilklendirme) yaklaşımını (lazy ve eager) tanımlayarak ve bunları uygulamalarda nerede kullanmanız gerektiğini tartışarak sonlandırıcağız.

1.1 Singleton Bean Nasıl Çalışır?

Spring'in Singleton scope'lu bean'leri nasıl yönettiğiyle bölümümüze başlayalım. Özellikle Singleton scope, Spring'te varsayılan (ve en çok kullanılan) bean scope'u olduğundan bu scope'u kullanırken ne bekleyeceğinizi bilmeniz gerekir. Bu bölümde, Spring'in davranışının anlaşılmasını kolaylaştırmak için yazdığınız kod ile Spring Context'i arasındaki bağlantıyı açıklayacağım. Daha sonra davranışı birkaç örnekle test edeceğiz.

Spring, context'ini yüklediğinde ve bean'e bir ad (bazen bean ID olarak da adlandırılır) atadığında singleton bir bean oluşturur. Belirli bir bean'e başvurduğunuzda her zaman aynı instance'ı elde ettiğiniz için de bu scope'u singleton olarak adlandırıyoruz. Ama dikkatli olun! Eğer bean'ler farklı adlara sahiplerse, Spring Context'inde aynı tipte daha fazla instance'a sahip olabilirsiniz. Bu yönü vurguluyorum çünkü "Singleton" tasarım desenini zaten biliyor ve muhtemelen geçmişte kullanmış olabilirsiniz. Singleton tasarım desenini bilmiyorsanız aşağıdaki paragrafı atlayabilirsiniz.

Ancak Singleton deseninin ne olduğunu biliyorsanız, Spring'te çalışma şekli size garip görünebilir, çünkü normal uygulamalar da bir tipin yalnızca bir instance'ı vardır. Spring'te ise, singleton kavramı aynı tipte birden fazla instance'a izin verir ve singleton ad başına benzersizdir, ancak uygulama başına benzersiz değildir (Şekil 1).

Şekil 1: Singleton pattern Spring'te bean adları bazında unique instance sağlar.

Bir uygulamadaki singleton bir sınıf, uygulamaya yalnızca bir instance sunan ve bu instance'ın oluşturulmasını yöneten bir sınıf anlamına gelir. Ancak Spring singleton, context'in bu tipin yalnızca bir instance'ına sahip olduğu anlamına gelmez. Bu, bir adın instance'a atandığı ve aynı instance'a her zaman bu adla başvurulacağı anlamına gelir.

1.1.1 @Bean Anotasyonuyla Singleton Scope Bean Tanımlama

Singleton bean'in davranışını, Spring Context'e @Bean anotasyonuyla instance ekleyerek gösterelim ve sonra bir main sınıftan birden çok kez başvuralım. Bunu, bean'i her çağırdığımızda aynı instance'ı aldığımızı kanıtlamak için yapıyoruz.

Şekil 2, context'i yapılandıran kodla birlikte context'in bir gösterimidir. Görseldeki kahve çekirdeği, Spring Context'in içine eklediğimiz instance'ı temsil eder. Contaxt'in ilişkili bir ada sahip yalnızca bir instance (kahve çekirdeği) içerdiğini gözlemleyin. Daha önceki yazılarda tartıştığımız gibi, context'e bir bean eklemek için @Bean anotasyonunu kullanırken, @Bean anotasyonlu metodun adı bean'in adı olur.

Şekil 2: Singleton bean sunumu

Uygulama başlarken context'i başlatır ve bir bean ekler. Bu durumda, bean'i bildirmek için @Bean anotasyonunu kullanırız. Metodun adı bean'in tanımlayıcısı olur. Bu tanımlayıcıyı nerede kullanırsanız kullanın, aynı instance'a bir referans alırsınız.

Bu örnekte, bean'i Spring Context'e eklemek için @Bean ek açıklama yaklaşımını kullandım. Ama singleton bir bean'in sadece @Bean anotasyonu kullanılarak yaratılabileceğini düşünmenizi istemiyorum. Bean'i context' eklemek için stereotype anotasyonları (@Component gibi) kullansaydık da sonuç aynı olurdu. Bu gerçeği bir sonraki örnekle göstereceğiz.

Ayrıca, bu örnekte bean'i Spring Context'inden alırken bean adını açıkça kullandığımı unutmayın. Önceki yazılarımızda, Spring Context'inden aynı tipte tek bir bean olduğunda, adını kullanmadan sadece tipiyle alabildiğimizi öğrenmiştiniz. Bu örnekte, adı sadece aynı bean'e atıfta bulunmamızı zorlamak için kullandım.

Artık kdu yazalım ve bu örneği sonuçlandırmak için çalıştıralım. Bir sonraki kod snippet'inde sunulduğu gibi boş bir CommentService sınıfı tanımlamamız gerekiyor.

public class CommentService {
}

Sonraki kodda, Spring Context'e CommentService tipinde bir instance eklemek için @Bean anotasyonlu bir metod kullanan konfigürasyon sınıfı tanımını bulabilirsiniz.

@Configuration
public class ProjectConfig {
 
  @Bean                                       ❶
  public CommentService commentService() {
    return new CommentService();
  }
}

❶ CommentService bean'ini Spring Context'e ekler.

Bir sonraki kodda, Spring'in davranışını singleton bean'imizi test etmek için kullandığımız main class'ı bulacaksınız. CommentService bean'e iki kez başvuruyoruz ve her seferinde aynı referansı almayı bekliyoruz.

public class Main {
 
  public static void main(String[] args) {
    var c = new AnnotationConfigApplicationContext(ProjectConfig.class);
 
    var cs1 = c.getBean("commentService", CommentService.class);
    var cs2 = c.getBean("commentService", CommentService.class);
 
    boolean b1 = cs1 == cs2;        ❶
 
    System.out.println(b1);   
  }
}

❶ İki değişken aynı referansı tuttuğundan, bu işlemin sonucu "True" dur.

Uygulamayı çalıştırdığınızda konsolda "true" yazdırır, çünkü singleton bir bean kullandığımızdan, Spring her seferinde aynı referansı döndürür.

1.1.2 Steretype Anotasyonlarıyla Singleton Scope Bean Tanımlama

Daha önce de belirtildiği gibi, stereotype veya @Bean anotasyonlarını kullanırken Spring'in singleton bean davranışı hep aynıdır. Ancak bu bölümde, bu ifadeyi bir örnekle göstermek istiyorum.

İki service sınıfının bir repostiroy'e bağlı olduğu bir sınıf tasarım senaryosu düşünün. Şekil 3'te sunulduğu gibi CommentRepository adlı bir repository'e bağlı olarak hem CommentService hem de UserService'e sahip olduğumuzu varsayalım.

Şekil 3: Class tasarım örneği

İki service sınıfı, use case'lerini implemente etmek için bir repository'e bağımlıdır. Tüm bu sınıflar singleton bean olarak tasarlandığında, Spring Context'i bu sınıfların her birinin bir instance'ına sahip olacaktır.

Bu sınıfların birbirine bağımlı olmasının nedeni önemli değildir (bu sadece bir senaryodur). Bu sınıf tasarımının daha karmaşık bir uygulamanın parçası olduğunu varsayıyoruz ve bean'ler arasındaki ilişkiye ve Spring'in context'indeki bağlantıları nasıl kurduğuna odaklanıyoruz. Şekil 4, context'i yapılandıran konfigürasyon sınıfının görsel bir gösterimidir.

Şekil 4: Stereotype kullanarak oluşturduğumuz context'in örneği

Bean'ler, bunları oluşturmak için stereotype anotasyonlarını kullandığımızda da singleton scope'lu olur. Bir bean referansı inject etmek için @Autowired kullanırken, framework istenen tüm yerlerde singleton bean'inin referansını enjekte eder.

Bu davranışı üç sınıfı oluşturarak ve Spring'in service bean'lerini enjekte ettiği referansları karşılaştırarak kanıtlayalım. Spring her iki servise de aynı bean referansını enjekte eder. Aşağıdaki kod snippet'inde, CommentRepository sınıfının tanımını bulabilirsiniz:

@Repository
public class CommentRepository {
}

Sonraki kod, CommentService sınıfının tanımını sunar. Spring'e sınıfta bildirilen bir field'a CommentRepository tipinde bir instance eklemesini söylemek için @Autowired kullandığımı gözlemleyin. Ayrıca Spring'in her iki service bean'ine aynı nesne referansını enjekte ettiğini kanıtlamak için daha sonra kullanmayı düşündüğüm bir getter yöntemi tanımladım:

@Service
public class CommentService {
 
  @Autowired
  private CommentRepository commentRepository;
 
  public CommentRepository getCommentRepository() {
    return commentRepository;
  }
}

CommentService deki aynı mantığı izleyen, UserService sınıfını kodunu bir sonraki kod snippet'inde görebilirsiniz:

@Service
public class UserService {
 
  @Autowired
  private CommentRepository commentRepository;
 
  public CommentRepository getCommentRepository() {
    return commentRepository;
  }
}

Bu bölümdeki ilk örnektekinin aksine, konfigürasyon sınıfı boş kalır. Spring'e sadece stereotype anotasyonlu sınıfları nerede bulacağımızı söylememiz gerekiyor. Spring'e stereotype anotasyonlu sınıfları nerede bulacağımızı söylemek için @ComponentScan anotasyonunu kullanıyoruz. Yapılandırma sınıfının tanımı bir sonraki kod snippet'indedir:

@Configuration
@ComponentScan(basePackages = {"services", "repositories"})
public class ProjectConfig {
 
}

Main sınıfta, iki service için referanslar alıyoruz ve Spring'in her ikisinde de aynı instance'ı enjekte ettiğini kanıtlamak için bağımlılıklarını karşılaştırıyoruz. Aşağıdaki liste main sınıfı sunar.

public class Main {
 
  public static void main(String[] args) {
    var c = new AnnotationConfigApplicationContext(                 ❶
      ProjectConfig.class);
 
    var s1 = c.getBean(CommentService.class);                       ❷
    var s2 = c.getBean(UserService.class);                          ❷
 
    boolean b =                                                     ❸
      s1.getCommentRepository() == s2.getCommentRepository();
 
    System.out.println(b);                                          ❹
  }
}

❶ Konfigürasyon sınıfına bağlı Spring Context'i oluşturur

❷ Spring Context'inden service bean'lerinin referanslarını alır

❸ Spring tarafından eklenen repository bağımlılığının referanslarını karşılaştırır

❹ Bağımlılık (CommentRepository) singleton olduğundan, her iki service sınıfı da aynı referansı içerir ve bu nedenle bu satır her zaman "true" yazdırır.

1.2 Gerçek Dünyada Singleton Bean Senaryoları

Şimdiye kadar Spring'in singleton bean'leri nasıl yönettiğini tartıştık. Singleton bean ile çalışırken dikkat etmeniz gereken şeyleri de tartışmanın zamanı geldi. Singleton bean kullanmanız gereken veya kullanmamanız gereken bazı senaryoları göz önünde bulundurarak başlayalım.

Singleton bean scope, uygulamanın birden çok bileşeninin bir nesne instance'ını paylaşabileceğini varsaydığı için, dikkate alınması gereken en önemli şey bu bean'lerin değişmez (immutable) olması gerektiğidir. Çoğu zaman, gerçek bir uygulama birden fazla thread üzerinde eylemler yürütür (örneğin, herhangi bir web uygulaması). Böyle bir senaryoda, birden çok thread aynı nesne instance'ını paylaşır. Bu thread'ler instance'ı değiştirirse, bir race condition senaryosuyla karşılaşırsınız (Şekil 5).

Şekil 5: Race condition durumu

Birden çok thread tek bir bean'e erişirken, aynı instance'a erişiyorlar. Bu thread'ler instance'ı aynı anda değiştirmeye çalışırsa, bir race condition sorununa rastlarlar. Bean eşzamanlılık (concurrency) için tasarlanmamışsa, race condition beklenmeyen sonuçlara veya exception durumlarına neden olur.

Race condition, birden çok thread'in paylaşılan bir kaynağı değiştirmeye çalıştığı çok thread'li mimarilerde gerçekleşebilecek bir durumdur. Bir race condition durumunda, geliştiricinin beklenmeyen yürütme sonuçlarını veya hatalarını önlemek için thread'leri düzgün bir şekilde senkronize etmesi gerekir.

Değiştirilebilir (mutable) singleton bean istiyorsanız, bu bean'leri kendiniz concurrent yapmanız gerekir (özellikle thread senkronizasyonu kullanarak). Ama singleton bean'leri synchronized olmak için tasarlanmamıştır. Daha çok bir uygulamanın ana tasarımını tanımlamak ve sorumlulukları birbirine devretmek için kullanılırlar. Yine de teknik olarak senkronizasyon mümkündür, ancak iyi bir uygulama değildir. Çünkü concurrency uygulamanın performansını önemli ölçüde etkileyebilir. Çoğu durumda, aynı sorunu çözmek ve thread concurrency'den kaçınmak için başka araçlar bulacaksınız.

Daha önceki yazımızdan hatırlıyorsanız, size constructor injection'ın iyi bir uygulama olduğunu ve field injection yerine tercih edildiğini söylemiştim. Constructor injection'ın avantajlarından biri de, instance'ı immutable hale getirmenize izin vermesidir (bean'lerin field'larını final olarak tanımlayarak). Önceki örneğimizde, field injection'ı constructor injection ile değiştirerek CommentService sınıfının tanımını geliştirebiliriz. Sınıfın daha iyi bir tasarımı aşağıdaki kod snippet'indeki gibi görünür:

@Service
public class CommentService {
 
  private final CommentRepository commentRepository;      ❶
 
  public CommentService(CommentRepository commentRepository) {
    this.commentRepository = commentRepository;
  }
  
  public CommentRepository getCommentRepository() {
    return commentRepository;
  }
}

❶ Field'ı final yapmak, bu alanın değiştirilmemesinin amaçlandığını vurgular.

Bean'leri kullanırken şu üç konuyu göz önüne getirmelisiniz

  • Yalnızca Spring'in yönetmesi gerekiyorsa nesneleri bean yapın, böylece framework bu bean'i  belirli yeteneklerle güçlendirebilir. Nesnenin framework tarafından sunulan herhangi bir yeteneğe ihtiyacı yoksa, onu bir bean yapmanız gerekmez.
  • Bir nesneyi bean yapıp Spring Context'e eklemeniz gerekiyorsa, yalnızca değişmez (immutable) olduğunda singleton kullanmalısınız. Değişebilen (mutable) singleton bean tasarlamaktan kaçınmalısınız.
  • Bir bean'in değişebilir (mutable) olması gerekiyorsa, 2. nolu başlıkta anlatacağımız prototype scope'unu kullanmayı düşünmelisiniz.

1.3 Eager and Lazy Instantiation Kullanmak

Çoğu durumda, Spring context'ini başlattığında tüm singleton bean'leri oluşturur — bu Spring'in varsayılan davranışıdır. Şimdiye kadar bu varsayılan davranışı kullandık ve buna eager instantiation denir. Bu bölümde, framework'ün farklı bir yaklaşımını, lazy instantiation'u tartışacağız ve bu iki yaklaşımı karşılaştıracağız. Lazy instantiation ile Spring, context'i oluşturduğunda singleton instance'ları oluşturmaz. Bunun yerine, birisi bean'e ilk başvurduğunda o instance'ı oluşturur. Yaklaşımlar arasındaki farkı gözlemlemek için bir örnek verelim ve ardından bunları production uygulamalarında kullanmanın avantajlarını ve dezavantajlarını tartışalım.

İlk senaryomuzda, varsayılan (eager) initialization'ı test etmek için sadece bir bean'e ihtiyacımız olacak. Kullandığımız adlandırmaları aynı tutacağım ve bu sınıfa CommentService adını vereceğim. Bir sonraki kod snippet'inde olduğu gibi, @Bean veya stereotype anotasyonunu kullanarak bu sınıfı bean yapabiliriz. Ancak her iki durumda da, sınıfın constructor'ında konsola bir çıktı eklediğinizden emin olun. Bu şekilde, framework'ün bunu çağrıp çağırmadığını kolayca gözlemlemleyebiliriz:

@Service
public class CommentService {
 
  public CommentService() {
    System.out.println("CommentService instance created!");
  }
}

Stereotype anotasyonu kullanıyorsanız, yapılandırma sınıfına @ComponentScan anotasyonunu eklemeyi unutmayın. Bir sonraki kodda yapılandırma sınıfımız var:

@Configuration
@ComponentScan(basePackages = {"services"})
public class ProjectConfig {
 
}

Main sınıfta, yalnızca Spring Context intsance'ını ilklendireceğiz. Gözlemlenecek kritik husus, kimsenin CommentService bean'ini kullanmadığıdır. Ancak, Spring instance'ı oluşturur ve context'te depolar. Uygulamayı çalıştırırken CommentService bean sınıfının constructor'ında çıktıyı göreceğimiz için instance'ı Spring'in oluşturduğunu anlayabiliriz. Bir sonraki kod snippet'i Main sınıfını gösterir:

public class Main {
 
  public static void main(String[] args) {            ❶
    var c = new AnnotationConfigApplicationContext(ProjectConfig.class);
  }
}

❶ Bu uygulama Spring Context'i oluşturur, ancak CommentService bean'ini hiçbir yerde kullanmaz.

Uygulama bean'i hiçbir yerde kullanmasa bile, uygulamayı çalıştırırken konsolda aşağıdaki çıktıyı bulacaksınız:

CommentService instance created!

Şimdi @Lazy anotasyonunu sınıfın (stereotype anotasyonu yaklaşımı için) veya @Bean anotasyonulu metodun (@Bean anotasyonu yaklaşımı için) üzerine ekleyerek örneği değiştirin. Uygulamayı çalıştırırken çıktının artık konsolda görünmediğine dikkat edeceksiniz, çünkü Spring'e bean'i yalnızca biri kullandığında oluşturmasını emrettik. Ve örneğimizde, kimse CommentService bean'ini kullanmıyor.

@Service
@Lazy                          ❶
public class CommentService {
 
  public CommentService() {
    System.out.println("CommentService instance created!");
  }
}

❶ @Lazy anotasyonu Spring'e bean'i sadece biri o bean'i ilk kez istediğinde oluşturması gerektiğini söyler.

Şimdi Main sınıfını değiştirin ve bir sonraki kod snippet'inde olduğu gibi CommentService bean'ine bir çağrım ekleyin:

public class Main {
 
  public static void main(String[] args) {
    var c = new AnnotationConfigApplicationContext(ProjectConfig.class);
 
    System.out.println("Before retrieving the CommentService");
    var service = c.getBean(CommentService.class);                ❶
    System.out.println("After retrieving the CommentService");
  }
}

❶ Spring'in CommentService bean'ini çağırması gereken bu satırda, Spring de instance'ı oluşturur.

Uygulamayı yeniden çalıştırın ve çıktıyı konsolda kontrol edin. Framework, bean'i yalnızca kullanılırsa oluşturuyor:

Before retrieving the CommentService
CommentService instance created!
After retrieving the CommentService

Peki ne zaman lazy veya eager ilklendirme kullanmalısınız? Çoğu durumda, context yaratıldığında framework'ün başlangıçta tüm instance'ları oluşturmasına izin vermek daha rahattır (eager); ve bu şekilde, bir instance diğerine lazım olduğunda, ilgili bean hazırda var olmuş olur.

Lazy ilklendirmede, framework'ün önce instance'ın var olup olmadığını denetlemesi ve yoksa sonunda oluşturması gerekir ve bu da performans ve zaman kaybı yaratır. Bu nedenle performans açısından, instance'ların zaten (eager) context'te olması daha iyidir, çünkü bir bean diğerine ihtiyaç duyduğunda framework onu tekrar ilklendirmeyle uğraşmaz. Eager ilklendirmenin bir başka avantajı, bir şeylerin yanlış olması durumunda framework'ün bir bean oluşturamamasıdır; böylece uygulamayı başlatırken bu sorunları gözlemleyebiliriz. Lazy ilklendirmede ise, birisi sorunu yalnızca uygulama zaten yürütülürken ve bean'in oluşturulması gereken noktaya ulaştığında gözlemler. Bu da production'da hiç istenmeyen sorunlara neden olabilir.

Yine de lazy instantiation tamamen kötü değildir. Eğer çeşitli müşterilerde farklı kullanım yeteneklerine sahip büyük bir monolitik uygulamaya sahipseniz, tüm bean'leri başlangıçta context'e doldurup memory ve zaman kaybı yaşamak yerine sadece müşterinizin kullanmadığı bileşenleri lazy ayarlayarak memory ve zamandan tasarruf edebilirsiniz.

Benim tavsiyem, eager ilklendirme olan varsayılan davranış ile devam etmektir. Bu yaklaşım genellikle daha fazla fayda sağlar. Kendinizi yekpare uygulamayla sunduğum gibi bir durumda bulursanız, önce uygulamanın tasarımı hakkında bir şey yapıp yapamayacağınızı görün. Genellikle, lazy ilklendirme kullanma ihtiyacı, uygulamanın tasarımında bir sorun olabileceğinin bir işaretidir. Örneğin, benim hikayemde, uygulama modüler bir şekilde veya mikro hizmetler olarak tasarlanmış olsaydı daha iyi olurdu. Böyle bir mimari, geliştiricilerin yalnızca belirli istemcilerin ihtiyaç duyduğu şeyi dağıtmasına yardımcı olurdu ve daha sonra bean'leri lazy hale getirmek gerekli olmazdı. Ancak gerçek dünyada, maliyet veya zaman gibi diğer faktörler nedeniyle her şey mümkün değildir. Sorunun gerçek nedenini tedavi edemiyorsanız, bazen semptomların en azından bazılarını tedavi edebilirsiniz.

2. Prototype Bean Scope

Prototype bean scope, Spring'in context'indeki bean'leri yönetmek için diğer bir yaklaşımını tanımlar. Bazı durumlarda, singleton yerine prototype scope'lu bean kullanırız. Öyleyse bu durumların neler olabileceğini ve nasıl prototype scope'lu bean'ler oluşturabileceğimizi öğrenelim.

Devam etmeden önce Prototype Pattern yazımızı okuyarak daha iyi bir temel atabilirsiniz:

Prototype Pattern - Prototip Deseni
Prototype pattern, kodunuzu sınıflarına bağımlı hale getirmeden mevcut nesneleri kopyalamanıza olanak tanıyan creational bir tasarım kalıbıdır.

2.1 Prototype Bean Nasıl Çalışır?

Uygulamalarda nerede kullanacağınızı tartışmadan önce prototype bean'leri yönetmek için Spring'in davranışını inceleyelim. Prototype yazımı okuduysanız mantığın ne kadar basit olduğunu görmüşsünüzdür. Spring direkt olarak context'teki beani yönetmez. Prototype scope'lu bir bean'e her başvurduğunuzda, Spring o bean'in tipinde yeni bir instance oluşturur ve referansını döner. Şekil 6'da, bean'i bir kahve bitkisi olarak temsil ettim (her bean talep ettiğimde, yeni bir instance alırsınız).

Şekil 6: Spring, context'teki bean'in tipini yönetir ve her seferinde o tipte instance oluşturup döner.

Bean scope'unu değiştirmek için @Scope anotasyonunu kullanıyoruz. Örneğimizde de bean'i bir kahve bitkisi olarak temsil edeceğiz, çünkü ona her başvurduğumuzda yeni bir instance alırız. Bu nedenle, cs1 ve cs2 değişkenleri her zaman farklı referanslar içerecektir ve kodun çıktısı her zaman "false" olacaktır.

Şekil 6'da gördüğünüz gibi, bean scope'unu değiştirmek için @Scope adlı yeni bir anotasyon kullanmamız gerekiyor. @Bean anotasyonu yaklaşımını kullanarak bean oluşturduğunuzda, @Scope bean'i bildiren metodun üzerinde @Bean anotasyonuyla birlikte kullanılır. Bean'i stereotype anotasyonuyla bildirirken de, @Scope anotasyonu ve bean'i bildiren sınıf üzerindeki stereotype anotasyonuyla birlikte kullanırsınız.

Prototype bean'lerde artık concurrency sorunu yaşamayız, çünkü bean'i isteyen her thread farklı bir instance alır. Bu nedenle mutable prototype bean'leri tanımlamak bir sorun olmaktan çıkar (Şekil 7).

Şekil 7: Prototype scope'lu bean'lerde concurreny sorunu olmaz

Birden çok thread belirli bir prototype bean istediğinde, her thread farklı bir instance alır. Bu şekilde, thread'ler arasında bir race condition problemi meydana gelmez.

2.1.1 @Bean Anotasyonuyla Prototype Scope Bean Tanımlama

Tartışmamızı zorlamak için bir proje yazalım ve Spring'in prototype bean'leri yönetme davranışını kanıtlayalım. Bunun için CommentService sınıfından bir bean oluşturuyoruz ve bu bean'den her istediğimizde yeni bir instance alabilmek için prototype olarak ilan ediyoruz. Sonraki kod snippet'i CommentService sınıfına ait:

public class CommentService {
}

Aşağıdaki kodda gösterildiği gibi, yapılandırma sınıfında CommentService sınıfına sahip bir bean tanımlıyoruz.

@Configuration
public class ProjectConfig {
 
  @Bean
  @Scope(BeanDefinition.SCOPE_PROTOTYPE)     ❶
  public CommentService commentService() {
    return new CommentService();
  }
}

❶ Bu bean'i prototype scope'lu yapar

Bean'den her istediğimizde yeni bir instance aldığımızı kanıtlamak için bir Main sınıf oluşturuyoruz ve bean'leri context'ten iki kez talep ediyoruz. Ardından aldığımız referansların farklı olduğunu gözlemlememiz lazım. Main sınıfının kodu şu şekildedir.

public class Main {
 
  public static void main(String[] args) {
    var c = new AnnotationConfigApplicationContext(ProjectConfig.class);
 
    var cs1 = c.getBean("commentService", CommentService.class);
    var cs2 = c.getBean("commentService", CommentService.class);
 
    boolean b1 = cs1 == cs2;    ❶
 
    System.out.println(b1);     ❷
  }
}

❶ cs1 ve cs2 iki değişkeni farklı instance'ların referanslarını içerir.

❷ Bu satır konsola her zaman "false" yazdırır.

Uygulamayı çalıştırdığınızda, konsolda her zaman "false" yazdığınıgörürsünüz. Bu çıktı, getBean() metodunu çağırırken alınan iki instance'ın farklı olduğunu kanıtlar.

2.1.2 Steretype Anotasyonlarıyla Prototype Scope Bean Tanımlama

Ayrıca, prototype anotasyonlu bean'lerin auto-wiring davranışını gözlemlemek için bir proje oluşturalım. CommentRepository prototype bean'ini tanımlayacağız ve bean'i diğer iki service bean'inde @Autowired kullanarak enjekte edeceğiz. Her service bean'inin farklı bir CommentRepository instance'ına referansı olduğunu gözlemleyeceğiz. Bu senaryo, singleton kapsamlı bean'ler için bölüm 1'de kullandığımız örneğe benziyor, ancak şimdi CommentRepository bean'i prototype'dır. Şekil 8 bean'ler arasındaki ilişkileri açıklar.

Şekil 8: Prototype scope'lu bean örneğimiz

Her service sınıfı CommentRepository'nin bir instance'ını istemektedir. CommentRepository bir prototype bean olduğundan, her service farklı bir CommentRepository instance'ı alır.

Sonraki kod snippeti CommentRepository sınıfının tanımını verir. Bean'in kapsamını prototype olarak değiştirmek için sınıf üzerinde kullanılan @Scope anotasyonunu gözlemleyebilirsiniz:

@Repository
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class CommentRepository {
}

İki service sınıfı, @Autowired anotasyonunu kullanarak CommentRepository tipinde bir instance ister. Sonraki kod snippet'i CommentService sınıfını sunar:

@Service
public class CommentService {
 
  @Autowired
  private CommentRepository commentRepository;
 
  public CommentRepository getCommentRepository() {
    return commentRepository;
  }
}

Önceki kod snippet'inde, UserService sınıfı da CommentRepository bean'inin bir instance'ını istemektedir. Konfigürasyon sınıfında, Spring'e stereotype anotasyonlarıyla açıklama eklenmiş sınıfları nerede bulacağını söylemek için @ComponentScan anotasyonunu kullanmamız gerekir:

@Configuration
@ComponentScan(basePackages = {"services", "repositories"})
public class ProjectConfig {
}

Projemize Main sınıfı ekliyoruz ve Spring'in CommentRepository bean'ini nasıl enjekte ettiğini test ediyoruz. Main sınıfı aşağıdaki kodda gösterilmiştir.

public class Main {
 
  public static void main(String[] args) {
    var c = new AnnotationConfigApplicationContext(ProjectConfig.class);
 
    var s1 = c.getBean(CommentService.class);   ❶
    var s2 = c.getBean(UserService.class);      ❶
 
    boolean b =                                 ❷
      s1.getCommentRepository() == s2.getCommentRepository();
 
    System.out.println(b);
  }
}

❶ Service bean'i context'ten referansları alır

❷ Eklenen CommentRepository instance'larının referanslarını karşılaştırır. CommentRepository bir prototype bean olduğundan, karşılaştırmanın sonucu her zaman "false" olacaktır.

2.2 Gerçek Dünyada Prototype Bean Senaryoları

Şimdiye kadar Spring'in davranışa odaklanarak prototype bean'leri nasıl yönettiğini tartıştık. Bu bölümde, production uygulamalarda prototype scope'lu bean'leri kullanmanız gereken durumlara odaklanıyoruz. Bölüm 1.2'deki singleton uygulamalarında yaptığımız gibi, tartışılan özellikleri göz önünde bulunduracağız ve prototype bean'lerin hangi senaryolar için iyi olduğunu ve nelerden kaçınmanız gerektiğini analiz edeceğiz (singleton bean kullanarak).

Prototype bean'leri singleton bean'ler kadar sık göremezsiniz. Ancak, bir bean'in prototype olup olmayacağına karar vermek için kullanabileceğiniz iyi bir desen vardır. Singleton bean'lerin değişime (mutating) uğrayan nesnelerle pek iyi arkadaş olmadığını unutmayın. Örneğin yorumları işleyen ve doğrulayan CommentProcessor adlı bir class tasarladığımızı düşünün. Başka bir service ise CommentProcessor instance'ını kullanıyor olsun. Ayrıca CommentProcessor nesnesi, işlenecek veriyi attribute olarak depolar ve bu attribute değerini değiştirecek metoda sahiptir (Şekil 9).

Şekil 9: CommentService, CommetnProcessor nesnesini kullanıyor.

Aşağıdaki kod CommentProcessor sınıfını gösteriyor:

public class CommentProcessor {
  private Comment comment;
   
  public void setComment(Comment comment) {
    this.comment = comment;
  }
 
  public void getComment() {
    return this.comment;   
  }
  
  public void processComment() {      ❶
    // changing the comment attribute
  }
 
  public void validateComment() {     ❶
    // validating and changing the comment attribute
  }
}

❶ Bu iki metod Comment attribute'ünün değerini değiştirir.

Sonraki kod, CommentProcessor sınıfını kullanan CommentService sınıfını gösteriyor. Service sınıfının sendComment() metodu, diğer sınıfın constructor'ını kullanarak CommentProcessor'un bir instance'ını oluşturur ve sonra metodun logic'ini çalıştırır.

@Service
public class CommentService {
  
  public void sendComment(Comment c) {
    CommentProcessor p = new CommentProcessor();  ❶
 
    p.setComment(c);                              ❷
    p.processComment(c);                          ❷
    p.validateComment(c);                         ❷
 
    c = p.getComment();                           ❸
    // do something further
  }
}

❶ CommentProcessor instance'ını oluşturur

❷ Comment instance'ını değiştirmek için CommentProcessor instance'ını kullanır

❸ Değiştirilen Commetn instance'ını alır ve işlemlere devam eder

CommentProcessor nesnesi Spring context'inde bir bean bile değildir. Peki bean olması gerekiyor mu? Herhangi bir nesneyi bean yapmaya karar vermeden önce kendinize bu soruyu sormanız çok önemlidir. Bir nesnenin yalnızca Spring Framework'ünün sunduğu bazı yeteneklerle yönetilmesi gerekiyorsa context'te bir bean olması gerektiğini unutmayın. Senaryomuzu bu şekilde bırakırsak, CommentProcessor nesnesinin bir bean olması gerekmez.

Ancak, CommentProcessor bean'inin bazı verileri kalıcı hale getirmek için CommentRepository nesnesini kullanması gerektiğini ve CommentRepository'nin Spring Context içinde bir bean olduğunu varsayalım (Şekil 10).

Şekil 10: CommentProcessor sınıfı, CommentRepository sınıfını kullanır

CommentProcessor nesnesinin CommentRepository instance'ını kullanması gerekiyorsa, instance almanın en kolay yolu DI (Dependency Injection) kullanmaktır. Ancak bunu yapmak için Spring'in CommentProcessor'u bilmesi ve bunun için de CommentProcessor nesnesinin context'te bir bean olması gerekir.

Bu senaryoda, CommentProcessor bean'inin Spring'in sunduğu DI özelliğinden yararlanmak için bir bean olması gerekir. Genel olarak, Spring'in nesneyi belirli bir yetenekle büyütmesini istediğimiz her durumda, bir bean olması gerekir.

Öyleyse CommentProcessor'ı Spring Context'te bir bean yapıyoruz. Ama singleton scope'lu olabilir mi? Hayır. Bu bean'i singleton olarak tanımlarsak ve birden fazla thread aynı anda kullanırsa, bir race condition problemi yaşarız (bölüm 1.2'de tartışıldığı gibi). Böyle bir durumda hangi thread tarafından sağlanan comment'in doğru işlenip işlenmediğine emin olamayacağız. Bu senaryoda, her metod çağrısının CommentProcessor nesnesinin farklı bir instance'ını almasını istiyoruz. CommentProcessor sınıfını, bir sonraki kod snippet'inde sunulduğu gibi bir prototype bean olarak değiştirebiliriz:

@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class CommentProcessor {
 
  @Autowired
  private CommentRepository commentRepository;
}

Artık Spring Context'inden CommentProcessor'un her seferinde yeni bir instance'ını alabilirsiniz. Ama dikkatli olun! sendComment() metodunun her çağrısı için yeni bir instance'a ihtiyacınız olacak, bu nedenle bean'e yapılan istek metodun içinde olmalıdır. Böyle bir sonuç elde etmek için, @Autowired kullanarak doğrudan Spring Context'i (ApplicationContext) CommentService bean'ine ekleyebilirsiniz. sendComment() metodunda, bir sonraki kodda sunulduğu gibi, uygulama context'inden getBean() kullanarak CommentProcessor instance'ını alırsınız.

@Service
public class CommentService {
 
  @Autowired
  private ApplicationContext context;
  
  public void sendComment(Comment c) {
    CommentProcessor p = 
      context.getBean(CommentProcessor.class);     ❶
 
    p.setComment(c);   
    p.processComment(c);   
    p.validateComment(c);   
 
    c = p.getComment();   
    // do something further
  }
}

❶ Burada her zaman yeni bir CommentProcessor instance'ı sağlanır.

CommentProcessor'ı doğrudan CommentService bean içine ekleme hatasına düşmeyin. CommentService bean'i singleton' dır, bu da Spring'in bu sınıfın yalnızca bir instance'ını oluşturduğu anlamına gelir. Sonuç olarak, Spring, CommentService bean'inin kendisini oluşturduğunda bu sınıfın bağımlılıklarını yalnızca bir kez enjekte eder. Bu durumda, CommentProcessor'ın yalnızca bir instance'ıyla çalışırsınız. sendComment() metodunun her çağrısı da bu unique instance'ı kullanır ve bu nedenle bu singleton bean içinde birden çok thread ile race condition sorunlarıyla karşılaşırsınız. Bir sonraki kod bu yaklaşımı sunar. Bu davranışı denemek ve kanıtlamak için bunu bir egzersiz olarak kullanabilirsiniz.

@Service
public class CommentService {
 
  @Autowired
  private CommentProcessor p;       ❶
  
  public void sendComment(Comment c) {
  
    p.setComment(c);   
    p.processComment(c);   
    p.validateComment(c);   
 
    c = p.getComment();   
    // do something further
  }
}

❶ Spring, CommentService bean'ini  oluştururken bu bean'i de enjekte eder. Ancak CommentService singleton olduğundan, Spring ayrıca CommentProcessor'ı yalnızca bir kez oluşturur ve öyle enjekte eder.

Bu bölümü size prototype bean kullanma hakkındaki fikrimi vererek bitiriyorum. Genellikle geliştirdiğim uygulamalarda bunları ve genel olarak değiştirilebilir (mutable) instance'ları kullanmaktan kaçınmayı tercih ederim. Ancak bazen eski uygulamaları yeniden düzenlemeniz veya onlarla çalışmanız gerekir. Karşılaştığım bir uygulama birçok yerde değişime uğrayan (mutable) nesneler kullanıyordu ve tüm bu yerleri kısa sürede yeniden düzenlemek imkansızdı. Prototype bean kullanmamız gerekiyordu, bu da ekibin bu vakaların her birini aşamalı olarak yeniden düzenlemesini sağladı.

Özet olarak, singleton ve prototype scope'ları arasında hızlı bir karşılaştırma yapalım.

Singleton Prototype
Framework, bir adı gerçek nesne instance'ı ile ilişkilendirir. Framework, bir adı bir tiple ilişkilendirir.
Bir bean adına her başvurduğunuzda aynı nesne instance'ını alırsınız. Bean adına her başvurduğunuzda yeni bir instance elde edersiniz.
Spring'i, context yüklendiğinde veya ilk başvurulduğunda instance'ları oluşturacak şekilde yapılandırabilirsiniz. Framework, her zaman bean'e başvurduğunuzda instance'ları oluşturur.
Singleton, Spring'in varsayılan bean scope'udur. Bir bean'i açıkça prototype olarak işaretlemeniz gerekir.
Singleton bir bean'in değişebilir (mutable) attribute'lere sahip olması önerilmez. Prototype bean, değişebilir (mutable) attribute'lere sahip olabilir.

3. ÖZET

  • Spring'te bean scope'u (kapsamı), framework'ün nesne instance'larını nasıl yönettiğini tanımlar.
  • Spring iki bean scope'u sunar: singleton ve prototype. (Diğerlerini ileride göreceğiz)
  1. Singleton ile Spring, instance'ları doğrudan kendi context'inde yönetir. Her instance'ın unique bir adı vardır ve bu adı kullanarak her zaman belirli bir instance'a başvurursunuz. Singleton, Spring'in varsayılanıdır.
  2. Prototip ile Spring yalnızca nesne tipini dikkate alır. Her tipin onunla ilişkilendirilmiş unique bir adı vardır. Spring, bean adına her başvurulduğunda bu tipin yeni bir instance'ını oluşturur.
  • Spring'i, context başlatıldığında (eager) veya bean ilk kez istenildiğinde (lazy) singleton bir bean oluşturacak şekilde yapılandırabilirsiniz. Varsayılan olarak, bir bean eager olarak ilklendirilir.
  • Uygulamalarda, en sık singleton bean kullanırız. Aynı ada başvuran herkes aynı instance'ı aldığından, birden çok thread aynı insance'a erişebilir ve bu instance'ı kullanabilir. Bu nedenle, instance'ın değişmez (immutable) olması önerilir. Ancak, bean'in attribute'lerinde değişim (mutable) işlemleri yapmayı tercih ederseniz, thread senkronizasyonunu sağlamak sizin sorumluluğunuzdadır.
  • Eğer değişebilir (mutable) bir instance'a sahip olmanız gerekiyorsa, prototype scope'unu kullanmak iyi bir seçenek olabilir.
  • Singleton bir bean'e prototype scope'lu bir bean enjekte ederken dikkatli olun. Böyle bir şey yaptığınızda, singleton instance'ının her zaman Spring'in singleton instance'ını oluştururken enjekte ettiği aynı prototype instance'ını kullandığını bilmeniz gerekir. Bu genellikle kısır bir tasarımdır, çünkü bir bean'i prototype scope'lu yapmanın amacı her kullanım için farklı bir instance elde etmektir.