Servis Bileşenleri ve Katmanlı Mimari
Bir Spring Boot uygulaması büyüdükçe, tüm mantığı controller'lara doldurmak sürdürülemez hale gelir. Sağlam uygulamalar katmanlı mimari kullanır: her katmanın tek bir sorumluluğu vardır. Bu konu, Spring'in stereotip a…
Servis Bileşenleri ve Katmanlı Mimari
Bir Spring Boot uygulaması büyüdükçe, tüm mantığı controller'lara doldurmak sürdürülemez hale gelir.
Sağlam uygulamalar katmanlı mimari kullanır: her katmanın tek bir sorumluluğu vardır. Bu konu,
Spring'in stereotip anotasyonlarıyla (@Controller/@RestController, @Service, @Repository)
bu katmanları nasıl kurduğunu ve neden önemli olduğunu ele alır.
Üç klasik katman
İstemci → [Controller] → [Service] → [Repository] → Veritabanı (HTTP) (iş mantığı) (veri erişimi)
@RestController/@Controller(sunum katmanı): HTTP isteklerini karşılar, parametreleri alır, servise delege eder, yanıtı döndürür. İş mantığı içermez.@Service(iş katmanı): Uygulamanın iş kurallarını ve mantığını barındırır (doğrulama, hesaplama, orkestrasyon, transaction sınırları). Asıl "ne yapılacağı" buradadır.@Repository(veri katmanı): Veritabanı/depo erişimi. (Spring Data JPA ile çoğu zaman bir arayüz; ek olarak@Repository, JDBC istisnalarını Spring'inDataAccessException'larına çevirir.)
Örnek 1 (./Ornek1.java) bir ürün yönetimini üç katmanla kurar: Controller yalnızca HTTP'yi
servise bağlar, Service iş kuralını (negatif fiyat reddi) uygular, Repository kaydı tutar.
Stereotip anotasyonları
Hepsi aslında @Component'in özelleşmiş halleridir — Spring onları bileşen taramasında (component
scan) bulup bean olarak yönetir:
| Anotasyon | Katman | Özel davranış |
|---|---|---|
@Component | Genel | Temel bean |
@Controller/@RestController | Sunum | Web istek eşleme (MVC) |
@Service | İş | (Anlamsal; özel davranış yok ama niyeti belgeler) |
@Repository | Veri | Persistence istisnası çevirme |
Anlamsal olarak doğru anotasyonu seçmek, kodun niyetini belgeler ve araçların/AOP'nin katmana özel davranması (örn. yalnızca servislerde transaction) için zemin hazırlar.
Neden katmanlı mimari?
- Tek sorumluluk: Her katman bir işe odaklanır; değişiklik etkisi sınırlı kalır.
- Test edilebilirlik: Servisi, controller ve veritabanı olmadan birim test edebilirsin (repository'yi sahteyle/mock ile değiştirerek — topic 07).
- Yeniden kullanım: Aynı servis hem REST controller hem zamanlanmış görev hem mesaj dinleyicisi tarafından çağrılabilir.
- Bağımlılık enjeksiyonu: Katmanlar birbirine constructor injection ile bağlanır (Spring bağımlılıkları sağlar); somut sınıf yerine arayüze bağlanmak gevşek bağ sağlar.
İyi uygulamalar
- Controller "ince" olsun: yalnızca HTTP eşleme + servise delege. Mantık servise gitsin.
- İş kuralları ve transaction sınırları (
@Transactional) servis katmanında. - Katmanlar arası veri taşırken DTO'lar (record'lar) kullan; entity'leri doğrudan dışarı sızdırma.
- Bağımlılıkları constructor ile enjekte et (alan enjeksiyonu yerine — test ve değişmezlik için).
Özet
Katmanlı mimariyi ve Spring'in stereotip bileşenlerini öğrendik: HTTP'yi karşılayan
@RestController, iş mantığını barındıran @Service, veri erişimini yapan @Repository
(Örnek 1); bunların @Component türevleri olduğunu, neden katmanlı çalıştığımızı (test, yeniden
kullanım, tek sorumluluk) ve iyi uygulamaları gördük. Sırada, isteğin önüne girip ortak işleri yapan
yapı: interceptor.
▶ Kod Örnekleri(1)
Ornek1
ortam gerekir1// Ornek1: Servis bileşenleri ve katmanlı mimari — Controller -> Service -> Repository.
2// Çalıştırma: portal gömülü Tomcat ile başlatır, self-test çıktısını alır.
3package com.egitim.springboot.service;
4
5import org.springframework.boot.CommandLineRunner;
6import org.springframework.boot.SpringApplication;
7import org.springframework.boot.autoconfigure.SpringBootApplication;
8import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
9import org.springframework.context.annotation.Bean;
10import org.springframework.stereotype.Repository;
11import org.springframework.stereotype.Service;
12import org.springframework.web.bind.annotation.*;
13import org.springframework.web.client.RestClient;
14
15import java.util.*;
16import java.util.concurrent.atomic.AtomicLong;
17
18@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
19public class Ornek1 {
20
21 public static void main(String[] args) { SpringApplication.run(Ornek1.class, args); }
22
23 record Urun(Long id, String ad, double fiyat) {}
24
25 // 1) REPOSITORY katmanı: veri erişimi (burada bellek-içi; gerçekte JPA/JDBC).
26 @Repository
27 static class UrunRepository {
28 private final Map<Long, Urun> depo = new LinkedHashMap<>();
29 private final AtomicLong sayac = new AtomicLong();
30 Urun kaydet(String ad, double fiyat) {
31 long id = sayac.incrementAndGet();
32 Urun u = new Urun(id, ad, fiyat);
33 depo.put(id, u);
34 return u;
35 }
36 List<Urun> hepsi() { return new ArrayList<>(depo.values()); }
37 }
38
39 // 2) SERVICE katmanı: İŞ MANTIĞI burada (kurallar, hesaplama). Controller'dan ayrı.
40 @Service
41 static class UrunServisi {
42 private final UrunRepository repo; // bağımlılık enjekte edilir (constructor injection)
43 UrunServisi(UrunRepository repo) { this.repo = repo; }
44
45 Urun ekle(String ad, double fiyat) {
46 if (fiyat < 0) throw new IllegalArgumentException("fiyat negatif olamaz"); // iş kuralı
47 return repo.kaydet(ad, fiyat);
48 }
49 double toplamStokDegeri() { return repo.hepsi().stream().mapToDouble(Urun::fiyat).sum(); }
50 List<Urun> listele() { return repo.hepsi(); }
51 }
52
53 // 3) CONTROLLER katmanı: yalnızca HTTP'yi servise bağlar (iş mantığı YOK).
54 @RestController
55 @RequestMapping("/urunler")
56 static class UrunController {
57 private final UrunServisi servis;
58 UrunController(UrunServisi servis) { this.servis = servis; }
59
60 @PostMapping Urun ekle(@RequestParam String ad, @RequestParam double fiyat) { return servis.ekle(ad, fiyat); }
61 @GetMapping List<Urun> hepsi() { return servis.listele(); }
62 @GetMapping("/toplam") double toplam() { return servis.toplamStokDegeri(); }
63 }
64
65 @Bean
66 CommandLineRunner selfTest() {
67 return args -> {
68 RestClient c = RestClient.create("http://localhost:8080");
69 System.out.println("\n========= KATMANLI MİMARİ SELF-TEST =========");
70 c.post().uri("/urunler?ad=Klavye&fiyat=450").retrieve().toBodilessEntity();
71 c.post().uri("/urunler?ad=Mouse&fiyat=250").retrieve().toBodilessEntity();
72 System.out.println("POST x2 -> Controller -> Service (iş kuralı) -> Repository (kayıt)");
73 System.out.println("GET /urunler -> " + c.get().uri("/urunler").retrieve().body(String.class));
74 System.out.println("GET /urunler/toplam-> " + c.get().uri("/urunler/toplam").retrieve().body(String.class) + " TL");
75 System.out.println("=============================================");
76 };
77 }
78}Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek1.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.