Eğitim Portalı/Spring Boot/Servis Bileşenleri ve Katmanlı Mimari
Spring Boot03-spring-boot/10-service-bilesenleri

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'in DataAccessException'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:

AnotasyonKatmanÖzel davranış
@ComponentGenelTemel bean
@Controller/@RestControllerSunumWeb istek eşleme (MVC)
@Serviceİş(Anlamsal; özel davranış yok ama niyeti belgeler)
@RepositoryVeriPersistence 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 gerekir
Ornek1.java
1// 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}
canlı çalıştırma için ortam gerekir

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.