Berk Akademi
Birebir ders başvurusu Ücretsiz ön görüşme Ana Sayfa

Katmanlı Mimari Nedir? Java Projesinde Katmanlar Nasıl Ayrılır?

katmanli-mimari-nedir-java-projesinde-katmanlar-nasil-ayrilir
Bu yazıda neler var?
  1. Katmanlı Mimari Nedir? Java Projesinde Katmanlar Nasıl Ayrılır?
  2. Katmanlı mimari neden var?
  3. Önce tek sınıfla yazalım
  4. Sorumlulukları ayırmak: controller, service, repository
  5. Aynı örneği katmanlara ayıralım
  6. DTO kavramına kısa giriş
  7. Her proje üç katman gerektirmez
  8. Test edilebilirlik açısından fark
  9. Sık Sorulan Sorular

Katmanlı Mimari Nedir? Java Projesinde Katmanlar Nasıl Ayrılır?

Katmanlı mimari, bir uygulamadaki sorumlulukları HTTP isteğini karşılama, iş kuralını çalıştırma ve veriyi saklama gibi farklı görevlere göre ayrı sınıflarda toplama yaklaşımıdır. Java projelerinde bu genellikle controller, service ve repository olmak üzere üç katmana bölünür. Amaç, kodun her parçasının tek bir işten sorumlu olmasını sağlayıp değişiklik yapmayı ve test yazmayı kolaylaştırmaktır.

Katmanlı mimari neden var?

Küçük bir proje büyürken en çok karşılaşılan sorun, tek bir sınıfın gitgide her şeyi yapmaya başlamasıdır. HTTP isteğini okuyan kod, iş kuralını uygulayan kod ve veritabanına SQL yazan kod aynı metodun içinde yan yana durur. Başlangıçta bu hızlı görünür ama proje büyüdükçe iki somut sorun ortaya çıkar.

Birincisi bakım zorluğu. Bir iş kuralını değiştirmek istediğinizde, o kuralın HTTP detaylarıyla ve SQL sorgularıyla iç içe geçmiş olması yüzünden değişikliğin etkisini kestirmek zorlaşır. İkincisi test edilebilirlik sorunu. İş kuralını test etmek istediğinizde gerçek bir veritabanı bağlantısına ve sahte bir HTTP isteğine ihtiyaç duyarsınız, çünkü bu üçü birbirinden ayrılamaz haldedir. Katmanlı mimari, bu üç sorumluluğu ayrı sınıflara bölerek her birinin kendi başına değişebilmesini ve test edilebilmesini sağlar.

Önce tek sınıfla yazalım

Sorunu somutlaştırmak için, sipariş oluşturan bir endpoint'i tek bir sınıfta yazalım:

@RestController
@RequestMapping("/orders")
public class OrderController {

    private final JdbcTemplate jdbcTemplate;

    public OrderController(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @PostMapping
    public ResponseEntity<String> createOrder(@RequestBody Map<String, Object> body) {
        String customerId = (String) body.get("customerId");
        BigDecimal amount = new BigDecimal(body.get("amount").toString());

        // İş kuralı: minimum tutar kontrolü
        if (amount.compareTo(BigDecimal.TEN) < 0) {
            return ResponseEntity.badRequest().body("Minimum tutar 10 TL");
        }

        // İş kuralı: müşteri limiti kontrolü
        BigDecimal totalSpent = jdbcTemplate.queryForObject(
            "SELECT COALESCE(SUM(amount), 0) FROM orders WHERE customer_id = ?",
            BigDecimal.class, customerId);

        if (totalSpent.add(amount).compareTo(new BigDecimal("10000")) > 0) {
            return ResponseEntity.badRequest().body("Müşteri limiti aşıldı");
        }

        // Veri erişimi: kayıt oluşturma
        jdbcTemplate.update(
            "INSERT INTO orders (customer_id, amount, created_at) VALUES (?, ?, ?)",
            customerId, amount, Instant.now());

        return ResponseEntity.ok("Sipariş oluşturuldu");
    }
}

Bu kod çalışır ama üç ayrı sorun aynı metotta üst üste biner:

  • HTTP isteğini ayrıştırma (@RequestBody Map) ile iş kuralı aynı yerde
  • İş kuralları (minimum tutar, müşteri limiti) SQL sorgularıyla iç içe
  • JdbcTemplate doğrudan controller'a enjekte edilmiş, veri erişimi sızıntısı var
  • Bu metodu test etmek için gerçek bir veritabanı gerekiyor, iş kuralını tek başına test edemezsiniz
  • Aynı müşteri limiti kontrolü başka bir endpoint'te gerekirse kod kopyalanır

Sorumlulukları ayırmak: controller, service, repository

Bu sorunları çözmek için sorumluluklar üç katmana dağıtılır ve veri tek yönlü akar: controller service'i çağırır, service repository'yi çağırır. Hiçbir katman kendisinden sonraki katmanı atlamaz veya geriye doğru çağrı yapmaz.

  • Controller: HTTP isteğini karşılar, gövdeyi okur, uygun service metodunu çağırır ve sonucu HTTP yanıtına çevirir. İş kuralı veya SQL barındırmaz.
  • Service: İş kurallarını uygular (minimum tutar, müşteri limiti gibi). Veriye ihtiyaç duyduğunda repository'yi çağırır, ham SQL yazmaz.
  • Repository: Veritabanıyla konuşan tek katmandır. Sorgu ve kayıt işlemleri burada toplanır, iş kuralı bilmez.

Bu ayrım sayesinde her katman kendi işiyle ilgilenir ve diğerlerinin detayını bilmek zorunda kalmaz. Sonraki bölümde bu örneği gerçekten üç katmana bölüp kodun nasıl göründüğüne bakacağız.

Aynı örneği katmanlara ayıralım

Önceki bölümde tek sınıfta yazdığımız görev ekleme mantığını şimdi üç katmana bölelim. Aynı iş, sadece sorumluluklar ayrı sınıflarda.

// Controller - HTTP isteğini karşılar
@RestController
@RequestMapping("/tasks")
public class TaskController {

    private final TaskService taskService;

    public TaskController(TaskService taskService) {
        this.taskService = taskService;
    }

    @PostMapping
    public ResponseEntity<Task> createTask(@RequestBody TaskRequest request) {
        Task task = taskService.createTask(request);
        return ResponseEntity.ok(task);
    }
}
// Service - iş kuralları burada
@Service
public class TaskService {

    private final TaskRepository taskRepository;

    public TaskService(TaskRepository taskRepository) {
        this.taskRepository = taskRepository;
    }

    public Task createTask(TaskRequest request) {
        if (request.getTitle() == null || request.getTitle().isBlank()) {
            throw new IllegalArgumentException("Başlık boş olamaz");
        }
        Task task = new Task(request.getTitle(), false);
        return taskRepository.save(task);
    }
}
// Repository - veri erişimi
@Repository
public interface TaskRepository extends JpaRepository<Task, Long> {
}

Controller artık sadece HTTP ile ilgileniyor, service kuralları uyguluyor, repository veriyi saklıyor. Her sınıfın tek bir değişme sebebi var. Veritabanı sorgusu değişirse repository'ye dokunursun, iş kuralı değişirse service'e, response formatı değişirse controller'a. Değişikliklerin birbirine sızması engellenmiş oluyor.

DTO kavramına kısa giriş

Yukarıdaki örnekte iki farklı sınıf var: TaskRequest ve Task. Bunlar aynı şey değil, kasıtlı olarak ayrılmışlar.

TaskRequest, dışarıdan gelen veriyi temsil eder. İstemcinin gönderdiği JSON'un Java karşılığıdır, sadece title alanı taşır. Task ise uygulamanın iç modelidir, veritabanındaki tabloya karşılık gelir ve id, completed gibi dışarıdan gelmeyen alanları da içerir.

public class TaskRequest {
    private String title;
    // getter, setter
}

Bu tür sınıflara DTO (Data Transfer Object) denir. Amaç, dış dünyadan gelen veri ile uygulamanın iç modelini birbirinden ayırmak. Böylece veritabanı şeman değişse bile API sözleşmen bozulmaz, ya da API'ye yeni bir alan eklemek istediğinde entity sınıfını değiştirmek zorunda kalmazsın. İki tarafı birbirine bağımlı tutmamak, uzun vadede esneklik sağlar.

Her proje üç katman gerektirmez

Controller, service, repository ayrımı büyüyen projelerde işe yarıyor ama her uygulamada zorunlu değil. Basit bir CRUD işlemi yapan, iş kuralı içermeyen küçük bir servis için service katmanı çoğu zaman sadece repository'yi çağıran boş bir ara katman haline gelir. Bu durumda katman eklemek karmaşıklığı azaltmaz, tam tersine dosya sayısını artırıp okunabilirliği zorlaştırabilir.

Katmanlama ihtiyacını belirlerken bakılabilecek birkaç ölçüt var: iş kuralı var mı, birden fazla veri kaynağına erişim gerekiyor mu, aynı mantığı farklı yerlerden çağırma ihtiyacı olacak mı. Bu sorulardan hiçbirine evet demiyorsan, controller'ın repository'yi doğrudan çağırması yeterli olabilir. Katmanlı mimari tek doğru yapı değildir, projenin büyüklüğüne ve karmaşıklığına göre şekillendirilecek bir araçtır.

Test edilebilirlik açısından fark

Katmanlı mimarinin en somut faydası test yazarken ortaya çıkar. Tek sınıfta her şeyi yapan kodu test etmek istediğinizde, gerçek bir veritabanı bağlantısına ihtiyacınız olur. Bu da testleri yavaşlatır, kırılgan hale getirir ve CI ortamında ekstra kurulum gerektirir.

Service katmanını repository'den ayırdığınızda, repository'yi bir mock ile değiştirip service'i tek başına test edebilirsiniz:

@Test
void kullaniciBulunamazsaHataFirlatmali() {
    UserRepository mockRepo = mock(UserRepository.class);
    when(mockRepo.findById(1L)).thenReturn(Optional.empty());

    UserService service = new UserService(mockRepo);

    assertThrows(UserNotFoundException.class,
        () -> service.getUser(1L));
}

Bu test hiçbir veritabanına dokunmaz, milisaniyeler içinde çalışır ve service'in iş mantığını izole şekilde doğrular. Repository katmanı ayrı bir sınıf olduğu için, gerçek veritabanı erişimini gerektiren testleri de ayrı tutabilir, sadece kritik noktalarda entegrasyon testi yazabilirsiniz.

Tek sınıflı yaklaşımda bu ayrım mümkün değildir; her testte gerçek bağımlılıkları da beraberinde sürüklersiniz. Katmanlı mimari, bağımlılıkları enjekte edilebilir hale getirerek testleri hem hızlandırır hem de neyi test ettiğinizi netleştirir.

Sık Sorulan Sorular

Katmanlı mimari küçük projelerde gereksiz mi?

Çok küçük, tek seferlik scriptler için gereksiz olabilir. Ama büyüme potansiyeli olan projelerde erkenden katman ayırmak, sonradan yapılacak zorlu bir refactor'ü önler.

Service katmanı olmadan controller doğrudan repository çağırabilir mi?

Teknik olarak mümkündür ama iş mantığının nereye yazılacağı belirsizleşir. Basit CRUD işlemleri için kabul edilebilir, karmaşık mantık için önerilmez.

Mock kullanmak yerine gerçek veritabanıyla test etmek daha mı güvenilir?

Entegrasyon testleri değerlidir ama yavaştır. Birim testlerinde mock kullanmak, iş mantığını hızlıca ve izole doğrulamanızı sağlar; ikisi birbirini tamamlar.

DTO kullanmadan da katmanlı mimari kurulabilir mi?

Evet, DTO opsiyoneldir. Küçük projelerde entity'leri doğrudan taşımak yeterli olabilir, ama büyüyen projelerde katmanlar arası veri sızıntısını önlemek için DTO önerilir.

Katmanlı mimari, kodu büyük bir bulmacaya dönüştürmeden sorumlulukları düzenli tutmanın pratik bir yoludur. Projenizin büyüklüğüne göre katman sayısını ayarlayarak, hem bakımı kolay hem de test edilebilir bir yapı kurabilirsiniz.

Bu içerik aradığın cevabı verdi mi?
Yanıtın, hangi yazıları geliştirmemiz gerektiğini anlamamıza yardımcı olur.
Bu içeriğin üretilmesinde yapay zeka araçlarından destek alınmıştır.

Bu konudan sonra ne okuyabilirsin?

Tüm yazılar

İlgili Eğitimler

Berk Keskin, yazılım geliştirici ve eğitmen
Yazar

Berk Keskin Kimdir?

Yazılıma 12 yaşında başladı; İzmir Ekonomi Üniversitesi'ni bölüm birincisi ve yüksek şeref öğrencisi olarak tamamladı. Bugün yalnızca eğitim vermekle kalmıyor, sektörde aktif olarak yazılım projeleri geliştiriyor ve gerçek dünya deneyimini birebir derslerine taşıyor. Ezberden uzak, mühendislik zihniyetini merkeze alan sürdürülebilir öğrenme sistemleri tasarlayarak sorgulayan, üreten ve problem çözebilen yeni nesil yazılımcılar yetiştiriyor.

Sektörel Deneyim & Projeler

  • Ticarify Entegrasyon Yazılım logosu CEO Ticarify Entegrasyon YazılımPazaryerleri ve e-ticaret sitelerine otomatik e-fatura kesimi, sipariş ve kargo takibi hizmetleri sunan e-Dönüşüm platformunun API mimarisini ve yazılım ekibini yönetmektedir.
  • Benim Düğünüm logosu CEO Benim DüğünümDijital etkinlik ve anı paylaşım platformu.
  • Siberdizayn logosu Yazılım Ekibi Lideri SiberdizaynYüksek anlık oyuncu trafiğine sahip oyun kontrol panelleri ve sunucu altyapıları geliştiren yazılım ekibine liderlik etmektedir.
  • MEDYOGRAFYA 360° Dijital Çözümler logosu Dijital Strateji Lideri MEDYOGRAFYA 360° Dijital ÇözümlerŞirketlerin dijital çözümlerde uzun vadede nasıl ilerlemesi gerektiği ve dijital dönüşüm süreçlerinin yönetilmesine destek olmaktadır.
  • İzmir Ekonomi Üniversitesi logosu Danışma Kurulu Üyesi İzmir Ekonomi ÜniversitesiMezun olduğu üniversitesinde, Bilgisayar Programcılığı bölümünün akademik müfredatını güncel sektör ihtiyaçlarına göre şekillendirmek adına Danışma Kurulu'nda görev almaktadır.
WhatsApp Hemen Ara