JavaRush /Курси /Spring REST & MVC /Каркас пакетів і залежності шарів

Каркас пакетів і залежності шарів

Spring REST & MVC
Рівень 9 , Лекція 0
Відкрита

1. Структура пакетів як архітектура

Коли в проєкті всього три класи, можна дозволити собі легкий хаос: покласти все поруч, назвати пакет «як вийде» і не думати про наслідки. Але саме в цей момент ви закладаєте звичку, яка згодом раптово обертається на запитання: «А чому в нас сервіс із HTTP-анотаціями, а репозиторій — усередині контролера?». Структура пакетів — це не про красу в IDE. Це про те, як мозок читає код, як Spring знаходить ваші біни і як ви утримуєте межі між шарами, поки проєкт дорослішає.

У навчальних проєктах особливо підступна ілюзія: «поки маленьке — неважливо». Насправді саме в маленькому проєкті найлегше домовитися про правила і дотримуватися їх. Якщо ви зараз зафіксуєте зрозумілі зони відповідальності, то далі кожна нова тема курсу, навіть без «магії», ляже в уже готовий каркас. І навпаки: якщо каркас не зафіксувати, то будь-який наступний клас намагатиметься оселитися «кудись» — зазвичай поруч із тим, хто його написав. Це природна урбаністика: якщо не планувати місто, воно виросте само, але паркування у вас будуть на деревах.

Є ще один практичний момент: Spring Boot сканує компоненти за пакетами. Тобто те, куди ви поклали клас, впливає не лише на естетику, а й на те, чи взагалі працюватиме застосунок без ручного налаштування. Тому пакетна структура — це одночасно і архітектура, і частина налаштування застосунку.

2. Базовий пакет com.example.tasktracker

У Java пакет — це не просто «папка з файлами». Це адреса у вашому проєкті, частина імені класу і водночас спосіб домовитися про межі. У Spring Boot є додатковий нюанс: від пакета, де лежить ваш клас застосунку з @SpringBootApplication, зазвичай починається сканування компонентів. Тобто Spring «дивиться» вниз по дереву пакетів і шукає @RestController, @Service, @Repository та інші компоненти. Тому базовий пакет потрібно вибирати свідомо й зафіксувати один раз — щоб потім не ловити загадкові ситуації, коли контролер є, а ендпоїнта немає.

У нашому курсі базовий пакет зафіксовано: com.example.tasktracker. Це просте, але дуже корисне рішення. Воно робить проєкт передбачуваним: усі ваші класи лежать усередині одного кореня, Spring бачить їх без додаткових танців, а ви завжди розумієте, де центр міста, а де вже райони. І так, example — тому що це навчальний проєкт. У реальному продукті там буде домен вашої компанії, але принцип той самий.

Мінімальний орієнтир — головний клас застосунку:

package com.example.tasktracker;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

// Точка входу застосунку: від пакета цього класу зазвичай починається сканування компонентів у Spring Boot.
@SpringBootApplication
public class TaskTrackerApplication {
    public static void main(String[] args) {
        // Запускаємо Spring-контекст і сам застосунок.
        SpringApplication.run(TaskTrackerApplication.class, args);
    }
}

Коли цей клас живе в com.example.tasktracker, то контролер у com.example.tasktracker.api.controller буде знайдений автоматично. А якщо ви раптом покладете TaskController у пакет com.example.controllers, Spring його, найімовірніше, не побачить, якщо ви спеціально не налаштували scanning. Тому правило просте: усе, що належить застосунку, має жити під com.example.tasktracker.

Щоб було простіше сприймати структуру, корисно тримати в голові ось таку «карту» пакетів — як дерево каталогів:

# Базова структура пакетів (каркас проєкту)
com.example.tasktracker
├── api
│   └── controller
├── domain
│   ├── model
│   └── service
├── infrastructure
│   ├── repository
│   │   └── inmemory
│   └── storage
└── config

Це ще не всі майбутні пакети проєкту, але для сьогоднішньої лекції — саме той каркас, який нам потрібен, щоб код не розповзався.

3. Зони проєкту: api, domain, infrastructure, config

Коли ви чуєте слово «архітектура», є ризик уявити величезну діаграму на пʼять екранів і людей, які сперечаються про неї до втрати свідомості. Іноді це одна й та сама людина. Нам так не треба. Нам потрібна проста й практична модель, яка допомагає відповісти на базове запитання: куди покласти новий клас і від кого він може залежати? Для цього ми ділимо проєкт на чотири зони відповідальності: api, domain, infrastructure і config. Це не релігія і не «єдино правильний шлях», а нормальний навчальний каркас.

Сенс поділу дуже прикладний. api — це все, що стосується вхідних і вихідних HTTP-запитів. domain — внутрішня модель і прикладні операції, які не повинні знати про транспорт (HTTP) і деталі зберігання (у якій Map лежать задачі). infrastructure — деталі зберігання та технічні реалізації: in-memory-репозиторії, storage-абстракції. config — технічна «проводка»: біни, конфігураційні класи, налаштування, які не є предметною логікою.

Зручно зафіксувати це в таблиці — вона замінює сотню правил на словах:

Зона Типові пакети Що там живе Чого там бути не повинно
api api.controller контролери, HTTP-вхід, анотації Spring MVC зберігання даних, бізнес-правила «як треба», робота з колекціями як із БД
domain domain.model,
domain.service
внутрішня модель, прикладні операції, контракти (інтерфейси) @RestController, @RequestParam, HTTP-статуси, знання про Map у репозиторії
infrastructure infrastructure.repository.inmemory,
infrastructure.storage
реалізація зберігання (in-memory), технічні деталі правила рівня «що можна користувачеві», HTTP-відповіді, бізнес-рішення
config config @Configuration, створення бінів, технічні налаштування логіка «створити задачу», «змінити статус», «валідувати заголовок»

Якщо хочеться візуалізації, то наш каркас можна намалювати так:

flowchart TD
    API[api: контролери] --> DOMAIN[domain: модель + сервіси]
    DOMAIN -->|через інтерфейси| INFRA[infrastructure: репозиторії/реалізації сховища]
    CONFIG[config] --> API
    CONFIG --> DOMAIN
    CONFIG --> INFRA

Тут важливо вловити думку: контролери ведуть у домен, домен використовує інфраструктуру, найчастіше через інтерфейси, а config не «командує» бізнесом, а просто допомагає зібрати застосунок.

4. Пакет api.controller: що всередині

Контролери — це як ресепшен в офісі. На ресепшені можна запитати, куди пройти, оформити перепустку і зрозуміти, хто ви та навіщо прийшли. Але якщо на ресепшені починають вести бухгалтерію, ремонтувати кавомашину і писати квартальний звіт, офіс перетворюється на реаліті-шоу. Так само і api.controller: це місце, де ми приймаємо HTTP-запит, витягуємо з нього дані з path, query або body, а потім делегуємо роботу в сервісний шар. Контролер відповідає за веб-частину, а не за те, щоб «зберігати задачі в ArrayList».

У нашому каркасі контролери живуть у пакеті com.example.tasktracker.api.controller. У цьому пакеті нормально бачити @RestController, @RequestMapping, @GetMapping та інших друзів. Тут же нормально бачити веб-типи й механізми Spring MVC. Але тут не повинно бути колекцій, які «тимчасово побудуть базою даних», бо тимчасове в IT — це зазвичай назавжди, лише з гіркою усмішкою.

Мінімальний приклад тонкого контролера, який залежить від сервісу:

package com.example.tasktracker.api.controller;

import com.example.tasktracker.domain.service.TaskService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

// Контролер відповідає лише за HTTP-вхід і делегування в доменний шар.
@RestController
@RequestMapping("/api/v1/tasks") // Базовий шлях для ендпоїнтів задач.
public class TaskController {
    private final TaskService taskService; // Залежність на доменний сервіс, а не на репозиторій.

    // Передавання залежності через конструктор: простіше тестувати і простіше читати.
    public TaskController(TaskService taskService) {
        this.taskService = taskService;
    }
}

Зверніть увагу: контролер нічого не знає про те, як саме зберігаються задачі. Він знає лише, що є TaskService, який уміє виконувати прикладні операції. Це ключовий принцип архітектури: контролер не має бути «центром всесвіту», інакше він швидко стане найбільшим класом проєкту і, мабуть, найстрашнішим.

5. Пакет domain.model: домен без Spring MVC

Доменна модель — це внутрішній світ вашого застосунку. Це те, як ви уявляєте сутності та їхній стан усередині сервісу, а не те, як вони виглядають зовні в HTTP. Навіть якщо зараз здається, що «ну ми ж просто віддаємо JSON, навіщо ці складнощі», корисно заздалегідь відокремлювати внутрішню модель від веб-шару. Це робить проєкт стійким: ви зможете змінювати контролери й формат відповіді, не переписуючи ядро, і навпаки — зможете поліпшити доменну модель, не ламаючи публічний шар.

Тому Task та інші доменні класи живуть у com.example.tasktracker.domain.model. Усередині domain.model не повинно бути анотацій Spring MVC, не повинно бути знання про ResponseEntity, і загалом домен має бути максимально «лише Java». Це не означає «без Spring взагалі», але означає «без веб-специфіки».

Мінімальний приклад доменної моделі задачі:

package com.example.tasktracker.domain.model;

// Доменна сутність: жодних веб-анотацій і жодного знання про HTTP.
public class Task {
    private final String id; // Ідентифікатор зазвичай незмінний.
    private String title;    // Назву можна змінювати (наприклад, під час операції "перейменування").

    public Task(String id, String title) {
        this.id = id;
        this.title = title;
    }
}

Так, це поки що дуже простий клас. І це нормально: сьогодні нам важлива не багата модель, а правильне місце, де вона живе. Зʼявиться більше полів — ви додасте їх сюди. Але ключовий принцип уже має бути зафіксований: доменна модель не лежить поруч із контролером «бо так швидше», інакше завтра ви почнете тягнути в неї веб-деталі, і вона перестане бути доменною.

Ще одна корисна думка: домен — це місце, де зручно тримати контракти прикладних операцій. У нашому курсі це буде пакет domain.service. Але деталі сервісних інтерфейсів та їхньої ролі ми розберемо в наступній лекції; зараз достатньо зрозуміти межу: domain — це «серце» застосунку, і воно не повинно знати, через які двері (HTTP) до нього прийшли.

6. infrastructure: деталі зберігання

Інфраструктурний шар часто сприймають як щось «другосортне»: мовляв, справжня логіка в домені, а інфраструктура — це нудні деталі. Але насправді інфраструктура — це місце, де ви чесно визнаєте: так, дані десь зберігаються; так, є файли; так, є колекції; так, є технічні обмеження. У навчальному проєкті ці деталі будуть простішими, ніж у продакшні, але архітектурно їх усе одно потрібно винести в окрему зону, щоб не заражати домен і контролери.

У нашому проєкті in-memory-репозиторії живуть у пакеті com.example.tasktracker.infrastructure.repository.inmemory. Тут доречні Java-колекції, Map, ініціалізація структури зберігання, а також @Repository, щоб Spring бачив цей клас як компонент, придатний для впровадження залежності.

Скелет in-memory-репозиторію може виглядати так:

package com.example.tasktracker.infrastructure.repository.inmemory;

import org.springframework.stereotype.Repository;

import java.util.LinkedHashMap;
import java.util.Map;

@Repository // Робимо клас Spring-компонентом рівня "зберігання/доступу до даних".
public class InMemoryTaskRepository {
    // Просте сховище в памʼяті. LinkedHashMap корисний тим, що зберігає порядок додавання.
    private final Map<String, Object> tasks = new LinkedHashMap<>();
}

Цей приклад навмисно спрощений: ми поки не обговорюємо інтерфейси репозиторіїв і конкретні методи. Це тема наступної лекції. Але тут уже видно головне: колекція знаходиться в репозиторії, а не в контролері. Контролер не має бути «мінісховищем», інакше ви змішаєте веб-вхід і зберігання, і проєкт перестане бути зрозумілим.

Ще один важливий принцип: інфраструктура часто залежить від домену, тому що реалізує контракти, які домен визначив. Це нормально. Ненормально, коли домен залежить від конкретної in-memory-реалізації, тому що тоді ви більше ніколи не зможете замінити «колекції» на щось інше без переписування ядра.

7. config: технічні біни

Конфігурація — це те місце, де живуть технічні рішення: які біни створити, яку реалізацію обрати, які залежності склеїти. У новачків часто виникає спокуса зробити config «пакетом для всього незрозумілого» або, навпаки, розпихати конфігурацію по домену, щоб було ближче до логіки. Обидва підходи зазвичай закінчуються тим, що в бізнес-коді зʼявляються технічні деталі, а технічний код починає вирішувати бізнес-питання. Це якби електрик у квартирі вирішував, де у вас буде кухня, тому що «проводам так зручніше».

У нашому каркасі конфігураційні класи живуть у com.example.tasktracker.config. Тут доречні @Configuration і @Bean. І навіть якщо зараз здається, що «нам не потрібно жодного біна вручну», корисно заздалегідь зафіксувати місце, куди такі речі складатимуться, щоб потім не розмазувати їх по проєкту.

Приклад простого технічного біна — системний годинник. Це ніби дрібниця, але дуже швидко стає корисним, коли ви хочете, щоб час брали не «з повітря», а з одного місця:

package com.example.tasktracker.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Clock;

@Configuration // Технічна конфігурація: тут "проводка", а не предметна логіка.
public class TimeConfig {
    @Bean
    public Clock clock() {
        // Єдине джерело часу, яке зручно підміняти в тестах.
        return Clock.systemUTC();
    }
}

Зверніть увагу, чого тут немає: тут немає методів «створити задачу» або «отримати список задач». Конфігурація не повинна розвʼязувати прикладні задачі. Її роль — допомогти застосунку зібратися, як конструктор, а не стати окремим сервісним шаром «тому що зручно».

8. Напрям залежностей між шарами

Коли ми говоримо «залежність» у Java, це не філософія. Це дуже конкретна річ: import, тип у параметрі методу, тип у полі класу, анотація з чужого шару. Тобто залежність — це коли один клас буквально не може скомпілюватися без іншого. Тому напрям залежностей — це головний спосіб утримати архітектуру в руках: ви просто не дозволяєте шарам «переплутатися місцями».

Базовий напрям викликів і залежностей у нашому проєкті має бути простим і читабельним: controller -> service -> repository/storage. Контролер знає сервіс. Сервіс знає репозиторій, зазвичай через інтерфейс. Репозиторій знає, як зберігати дані в памʼяті. І при цьому доменна модель та сервіси не повинні раптом «знати про HTTP», бо тоді домен стає частиною веб-шару.

Щоб це закріпити, корисно дивитися на проєкт як на систему обмежень. Наприклад, так:

flowchart LR
    C[api.controller] --> S[domain.service]
    S --> R[infrastructure.repository.inmemory]
    S --> ST[infrastructure.storage]

А тепер — що вважається «червоним прапорцем». Якщо ви бачите в domain.model import із Spring MVC, це майже завжди помилка архітектури:

package com.example.tasktracker.domain.model;

import org.springframework.web.bind.annotation.GetMapping; // Так не повинно бути: домен не має знати про веб-анотації.

public class Task {
    // Навіть "нешкідливий" import із Spring MVC тягне домен у бік веб-шару.
}

Чому це погано? Тому що домен перестає бути «ядром застосунку» і перетворюється на продовження веб-шару. У результаті будь-які зміни веб-частини будуть протягуватися в домен, а ви втратите сенс поділу на шари.

Ще один практичний спосіб швидко перевірити напрям залежностей — проста матриця «хто від кого може залежати»:

Хто залежить \ Від кого
api
domain
infrastructure
config
api (рідко) ✅ так ⚠️ краще не напряму ✅ іноді
domain ❌ ні ✅ так ❌ ні (від реалізацій) ⚠️ небажано
infrastructure ❌ ні ✅ так ✅ так ⚠️ іноді
config ✅ так ✅ так ✅ так ✅ так

Тут найважливіші дві стрічки: domain не залежить від api, і domain не залежить від конкретної інфраструктури. Це і є той самий каркас, який робить застосунок придатним до подальшого розвитку, навіть якщо зараз ви зберігаєте все в памʼяті.

9. Типові помилки під час роботи зі шарами

Помилка №1: «Складемо все в один пакет, а потім розберемося».
Майже ніколи не розбираються. Зазвичай виходить так: ви додаєте другий контролер, поруч зʼявляється сервіс, поруч репозиторій, потім ще один, а через тиждень проєкт перетворюється на лотерею «знайди потрібний клас за назвою». Набагато дешевше одразу домовитися про каркас: api / domain / infrastructure / config.

Помилка №2: пакет util як комірка «для всього, що не влізло».
util — це як домашня шухляда «різне»: спочатку зручно, потім там живе весь ваш побут, включно з пультом від телевізора 2012 року і інструкціями до мікрохвильовки. Якщо ви не можете назвати пакет точніше, ніж util, це зазвичай сигнал, що відповідальність класу не визначена. У навчальному проєкті краще витратити дві хвилини на точну назву і зберегти ясність.

Помилка №3: доменна модель починає залежати від веб-шару.
Щойно в domain.model зʼявляється @RequestParam, @JsonProperty або ResponseEntity, архітектура вже «поїхала». Домен має бути внутрішнім світом застосунку. Він може жити з простими Java-типами, але не повинен знати, як саме ви приймаєте запити.

Помилка №4: зберігання даних у контролері «тому що швидко».
Сьогодні «швидко», завтра «чому у нас список задач очищується під час кожного запиту», післязавтра «чому тести неможливо написати без запуску всього сервера». Контролер — це вхід, а зберігання — інфраструктура. Навіть якщо це зберігання на Map, воно має жити в репозиторії.

Помилка №5: конфігурація перетворюється на бізнес-логіку.
Іноді в config починають писати методи на кшталт createDefaultTasks() і навіть викликають їх із @Bean. Це виглядає як зручний хак, але дуже швидко змішує технічний шар зі змістом предметної області. config має «збирати застосунок», а не «робити роботу застосунку».

1
Задача
Spring REST & MVC, 9 рівень, 0 лекція
Недоступна
Каркас пакетів для Book API
Каркас пакетів для Book API
1
Задача
Spring REST & MVC, 9 рівень, 0 лекція
Недоступна
Інформація про застосунок через пакет config
Інформація про застосунок через пакет config
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ