Архитектура приложения: слои и поток запроса

Приложение на Spring Boot построено слоями: каждый слой отвечает за свою задачу и общается только с соседом. Это и есть основа поддерживаемого кода.
Суть: запрос проходит через DispatcherServlet к контроллеру, контроллер зовёт сервис (бизнес-логика), сервис — репозиторий (доступ к данным), репозиторий — базу. Ответ возвращается тем же путём наружу.

Почему вообще нужны слои? Представьте, что вся логика свалена в один класс: и разбор HTTP, и расчёты, и SQL-запросы. Такой код невозможно тестировать и страшно менять — правка в одном месте ломает три других. Слоистая архитектура разделяет ответственность: каждый слой знает только то, что ему нужно, и заменяем независимо.

Четыре слоя

Контроллер (Controller) — «дверь» приложения. Он принимает HTTP-запрос, достаёт параметры и тело, вызывает сервис и формирует HTTP-ответ. Он не считает и не ходит в базу — только переводит между миром HTTP и миром Java.

Сервис (Service) — мозг приложения. Здесь живёт бизнес-логика: проверки правил, расчёты, координация нескольких репозиториев, транзакции. Сервис работает с сущностями и ничего не знает про HTTP.

Репозиторий (Repository) — слой доступа к данным. Он умеет сохранять, искать и удалять записи. В Spring Data JPA вы даже не пишете SQL — достаточно объявить интерфейс.

База данных — постоянное хранилище. Обычно PostgreSQL или MySQL.

Как работает под капотом: поток запроса

Когда приходит HTTP-запрос, его первым встречает DispatcherServlet — центральный диспетчер Spring MVC. Он смотрит на URL и метод (GET/POST), находит нужный метод контроллера через HandlerMapping и передаёт ему управление. Дальше запрос идёт вглубь по слоям, а ответ — наружу.

  Клиент (браузер / фронтенд)
        |  HTTP-запрос  GET /api/users/7
        v
  +-------------------+
  | DispatcherServlet |  выбирает обработчик по URL
  +-------------------+
        |
        v
  +-------------------+
  |    Controller     |  разбирает запрос, зовёт сервис
  +-------------------+
        |
        v
  +-------------------+
  |     Service       |  бизнес-логика, транзакции
  +-------------------+
        |
        v
  +-------------------+
  |   Repository      |  доступ к данным (JPA)
  +-------------------+
        |
        v
  +-------------------+
  |  База данных      |  SELECT ... WHERE id = 7
  +-------------------+
        |
        v  ответ поднимается обратно тем же путём
   JSON {"id":7,"name":"Анна"}  -->  Клиент

Обратите внимание: данные не «перепрыгивают» через слой. Контроллер не лезет в базу напрямую, а сервис не формирует JSON. Каждый слой говорит только с соседом — это правило держит код в порядке.

Смоделируем поток запроса на простом «языко-независимом» примере, который показывает идею делегирования по слоям:

# Аналог потока запроса: каждый слой делегирует соседу
db = {7: {"id": 7, "name": "Анна"}}

def repository(user_id):
    return db.get(user_id)          # доступ к данным

def service(user_id):
    user = repository(user_id)      # бизнес-логика
    if user is None:
        raise ValueError("Пользователь не найден")
    return user

def controller(request_id):
    try:
        user = service(request_id)  # разбор запроса
        return {"status": 200, "body": user}
    except ValueError as e:
        return {"status": 404, "body": {"error": str(e)}}

print(controller(7))    # 200, найден
print(controller(99))   # 404, не найден

Запустите этот код кнопкой «Попробуй сам ▶». Это та же логика, что во взрослом приложении: контроллер ловит ошибку из сервиса и превращает её в HTTP-статус.

Частые ошибки

  • SQL в контроллере. Когда контроллер сам ходит в базу, слои смешиваются и тестировать становится невозможно.
  • Бизнес-логика в контроллере. Контроллер должен быть тонким: разобрал запрос — делегировал сервису.
  • Возврат сущностей наружу. Сущность БД не должна напрямую улетать в JSON-ответ — для этого есть DTO (отдельный раздел).

Best practices

  • Держите контроллеры тонкими, сервисы — с логикой, репозитории — только с доступом к данным.
  • Раскладывайте код по пакетам controller, service, repository, dto, entity.
  • Никогда не позволяйте слою перепрыгивать через соседа.

Итог: слоистая архитектура — фундамент Spring Boot. Запрос проходит DispatcherServlet → Controller → Service → Repository → БД и возвращается обратно. Запомните этот путь — мы будем возвращаться к нему весь курс.

Проверьте себя
1. В каком порядке HTTP-запрос проходит слои Spring Boot приложения?
AController → Repository → Service → БД
BDispatcherServlet → Controller → Service → Repository → БД
CService → Controller → БД → Repository
DБД → Repository → Controller → Service
2. За что отвечает слой Service?
AЗа разбор HTTP-запроса и формирование ответа
BЗа прямой доступ к таблицам базы данных
CЗа бизнес-логику, проверки правил и транзакции
DЗа отрисовку HTML-страниц