본문 바로가기

개발/Spring Boot

[Spring Boot] Service + DI + JPA + MySQL

이번 실습의 목표는 Spring Boot의 핵심 구조를 한 번에 경험해보는 것이다.

POST /todos
       ↓
Controller
       ↓
Service
       ↓
Repository
       ↓
JPA / Hibernate
       ↓
MySQL

 

Step 1. 먼저 실습 패키지 구조를 만들자

src/main/java/com/example/todolist 아래에 다음 구조를 만든다.

com.example.todolist
│
├── TodolistApplication.java
│
└── todo
    ├── Todo.java
    ├── TodoRepository.java
    ├── TodoService.java
    └── TodoController.java

 

앞으로 대부분의 Spring Boot 프로젝트에서 이 네 가지 개념을 계속 만나게 될 것이다.

Entity
Repository
Service
Controller

지금부터 하나씩 만들어보자.

 

Step 2. Entity 만들기

Entity는 DB의 테이블과 연결되는 Java 객체를 의미한다.

 

todo/Todo.java

package com.example.todolist.todo;

import jakarta.persistence.*;

@Entity
@Table(name = "todos")
public class Todo {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private boolean completed = false;

    protected Todo() {
    }

    public Todo(String title) {
        this.title = title;
    }

    public Long getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }

    public boolean isCompleted() {
        return completed;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public void setCompleted(boolean completed) {
        this.completed = completed;
    }
}

여기서 가장 중요한 것은

@Entity

으로 이 어노테이션은 Spring/JPA에게 다음과 같은 의미이다.

"이 Java 객체를 DB 테이블과 연결해서 사용할 것이다."

 

즉,

Java

Todo
 ├─ id
 ├─ title
 └─ completed

가

MySQL

todos
+----+----------------------+-----------+
| id | title                | completed |
+----+----------------------+-----------+

와 연결된다.

 

즉, Todo 클래스는 DB의 todos 테이블과 연결되고, 각 필드는 테이블의 컬럼과 대응된다.

 

Entity 클래스 자체는 DB의 테이블 구조를 표현하고, Entity 객체 하나는 그 테이블의 한 행(row) 데이터를 표현한다.

 

 

Spring Boot에서는 기본적으로 @Entity가 붙은 클래스를 검색하여 JPA Entity로 처리한다.

 

이제 코드를 살펴보자.

 

@Id

@Id
private Long id;

DB의 Primary Key이다. 

MySQL에서

id BIGINT PRIMARY KEY


와 비슷한 역할을 한다.

 

@GeneratedValue

@GeneratedValue(strategy = GenerationType.IDENTITY)

 

ID를 직접 지정하지 않고 MySQL이 자동으로 증가시키도록 한다.


예를 들어 데이터를 저장하면 ID가 

1 → 2 → 3 → 4 → ...


형태로 만들어진다.

 

따라서 요청을 할 때 이렇게 하지 않고

{
    "id": 1,
    "title": "Spring 공부"
}

 

다음과 같이 한다.

{
    "title": "Spring 공부"
}

 

Step 3. Repository 만들기

Repository는 Entity 객체를 기준으로 DB에 접근해서 CRUD를 수행하는 계층을 의미한다.

 


이번에는 DB와 실제로 통신할 객체를 만들어 보자.

 

todo/TodoRepository.java

package com.example.todolist.todo;

import org.springframework.data.jpa.repository.JpaRepository;

public interface TodoRepository extends JpaRepository<Todo, Long> {
}

이게 끝이다.

 

굉장히 이상하게 느껴질 수 있다.

 

DB에 저장하려는데

INSERT ...
SELECT ...
UPDATE ...
DELETE ...

같은 SQL이 하나도 없다.

그런데 이것만으로 다음 기능을 사용할 수 있다.

todoRepository.save(todo);

todoRepository.findAll();

todoRepository.findById(id);

todoRepository.delete(todo);

todoRepository.deleteById(id);

 

Spring Data Repository가 기본 CRUD 구현을 제공하기 때문이다.

 

즉, 개발자가 기본 CRUD 기능을 구현하기 위해 직접 코드를 작성할 필요없이 Spring Data JPA를 사용하면 Spring이 실행될 때 구현체를 만들어 준다.

 

Step 4. Service 만들기

todo/TodoService.java

package com.example.todolist.todo;

import java.util.List;

import org.springframework.stereotype.Service;

@Service
public class TodoService {

    private final TodoRepository todoRepository;

    public TodoService(TodoRepository todoRepository) {
        this.todoRepository = todoRepository;
    }

    public Todo create(String title) {

        Todo todo = new Todo(title);

        return todoRepository.save(todo);
    }

    public List<Todo> findAll() {
        return todoRepository.findAll();
    }
}

여기에서 이번 실습에서 가장 중요한 Spring 개념이 등장한다.

 

@Service

이다.

 

Service 계층은 애플리케이션의 실제 핵심 규칙과 업무 로직과 같은 비즈니스 로직을 처리하는 영역으로 

@Service 어노테이션을 사용하는 것은 

 

Spring에게

"이 비즈니스 로직을 처리하는 서비스 클래스에 대한 객체를 네가 생성하고 관리해줘"

 

라고 알려주는 것이다.

이제 DI(Dependency Injection, 의존성 주입)를 이해해보자.

다음 코드를 살펴보면

private final TodoRepository todoRepository;

public TodoService(TodoRepository todoRepository) {
    this.todoRepository = todoRepository;
}

 

다음과 같이 new 키워드를 사용하여 직접 객체를 생성하지 않았다.

TodoRepository repository = new TodoRepository();

 

대신에

 

생성자로 받았다.

public TodoService(TodoRepository todoRepository)

즉 생성자 방식을 사용함으로써


TodoService 객체가 동작함에 있어ㅈ TodoRepository 객체가 필요한 경우, 


Spring이 알아서 TodoRepository 객체를 찾아 생성자 메서드에 넣어주고 TodoService 객체와 TodoRepository 객체를 연결한다.

 

이것이 바로

Dependency Injection, DI, 의존성 주입

 

이다.

Spring

TodoRepository 객체 생성
        │
        ↓ 주입
TodoService 객체 생성

즉,

TodoService
    │
    │ 연결(TodoService가 동작함에 있어 TodoRepository가 필요함)
    ↓
TodoRepository

 

TodoService의 생성자에 TodoRepository가 필요하다고 명시해 두었기 때문에, Spring이 서비스 객체를 생성하는 시점에 준비된 레포지토리 객체를 생성자에 전달하여 두 객체를 연결해준다.

 

Step 5. Controller 만들기

HTTP 요청을 받는 Controller를 만들어보자.

 

todo/TodoController.java

package com.example.todolist.todo;

import java.util.List;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/todos")
public class TodoController {

    private final TodoService todoService;

    public TodoController(TodoService todoService) {
        this.todoService = todoService;
    }

    @PostMapping
    public Todo create(@RequestBody Todo todo) {
        return todoService.create(todo.getTitle());
    }

    @GetMapping
    public List<Todo> findAll() {
        return todoService.findAll();
    }
}

여기에서도 방금 전과 같은 방식의 DI가 사용되었다.

private final TodoService todoService;

public TodoController(TodoService todoService) {
    this.todoService = todoService;
}

Spring이 TodoService 객체를 Controller 객체에 주입한다.

 

결국 구조는

TodoService에 TodoRepository 주입
       ↓
TodoController에 TodoService 주입

가 된다.

 

Step 6. 지금까지의 구조

현재 코드 관계를 도식화해보자면

             HTTP
              ↓
      TodoController
              │
              ↓
        TodoService
              │
              ↓
      TodoRepository
              │
              ↓
       Spring Data JPA
              │
              ↓
          Hibernate
              │
              ↓
            MySQL

Spring Boot에서 정말 많이 보게 될 구조로

Controller → Service → Repository

이 세 단어와 관계는 반드시 기억하자.

 

Step 7. MySQL 연결 설정

src/main/resources/application.properties

spring.application.name=todolist

spring.datasource.url=jdbc:mysql://localhost:3306/spring_study
spring.datasource.username=spring
spring.datasource.password=spring1234

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

여기서 학습 단계에서 중요한 설정이 있다.

spring.jpa.hibernate.ddl-auto=update

이 설정 때문에 Hibernate가 Entity를 보고 테이블을 생성하거나 필요한 스키마 변경을 반영한다.

 

즉, 개발자가 직접

CREATE TABLE todos ...

를 작성하지 않아도 된다.

 

학습 단계에서 편리하기에 사용하는 것이고 실무에서는 DB 스키마 변경을 ddl-auto=update에 맡기는 방식은 일반적으로 피한다.

이후에는 Flyway/Liquibase 같은 migration 도구를 배우는 것이 좋다.

 

Step 8. 서버 실행

이제 터미널에서

./gradlew BootRun

을 실행한다.

 

정상적으로 실행되면 로그에서 SQL이 나타날 것이다.

 

Step 9. MySQL에서 실제 테이블 확인

새 터미널을 하나 열고

mysql -u spring -p

로 접속한다.

 

DB 선택:

USE spring_study;

 

TABLE 확인:

SHOW TABLES;

 

다음처럼 보이면 성공이다.

 

구조를 확인해보자.

DESC todos;

 

SQL로 CREATE TABLE을 하지 않았음에도 불구하고 테이블이 만들어진 것을 볼 수 있다.

 

바로

Todo.java
        ↓
@Entity
        ↓
JPA / Hibernate
        ↓
todos 테이블

때문이다.

 

Step 10. 첫 번째 데이터 저장

이제 Spring Boot API를 호출해보자.

 

터미널에서 다음 명령어를 실행한다.

curl -X POST http://localhost:8080/todos \
  -H "Content-Type: application/json" \
  -d '{"title":"Spring Boot 공부하기"}'

 

정상이라면 응답이

{"title":"Spring Boot 공부하기","completed":false,"id":1}

가 나온다.

 

여기서 굉장히 중요한 일이 일어났다.

요청 하나가 다음 흐름을 거친 것이다.

POST /todos

        ↓

TodoController

@RequestBody
JSON → Todo 객체

        ↓

TodoService

new Todo(title)

        ↓

TodoRepository

save(todo)

        ↓

Hibernate

INSERT SQL 생성

        ↓

MySQL

todos 테이블에 저장

 

MySQL에서 직접 확인해보자.

SELECT * FROM todos;

 

여기까지 성공했다면 Spring Boot → JPA → MySQL 연결까지 실제로 완성한 것이다.

 

Todo도 몇 개 더 넣어보자.

터미널에서

curl -X POST http://localhost:8080/todos \
  -H "Content-Type: application/json" \
  -d '{"title":"JPA 공부하기"}'
curl -X POST http://localhost:8080/todos \
  -H "Content-Type: application/json" \
  -d '{"title":"MySQL 공부하기"}'

 

MySQL로 확인해보면

 

Step 11. 데이터 전체 조회

이번에는 GET 요청이다. 

 

터미널에서

curl http://localhost:8080/todos

을 입력하면

 

결과로

[
  { title: "Spring Boot 공부하기", completed: false, id: 1 },
  { title: "JPA 공부하기", completed: false, id: 2 },
  { title: "MySQL 공부하기", completed: false, id: 3 },
]

가 나타난다.

 

Step 12. @RequestBody는 무엇인가?

예를 들어, POST 요청으로

{ "title": "Spring Boot 공부하기" }

를 보냈다고 하자.

 

Controller에는

@PostMapping
public Todo create(@RequestBody Todo todo) {
    return todoService.create(todo.getTitle());
}

가 있다.

 

@RequestBody가 JSON을 Java 객체로 변환한다.

 

즉,

          HTTP JSON
{ "title": "Spring Boot 공부하기" }

              ↓ @RequestBody

           Java 객체
          Todo todo

이다.

 

그리고

todo.getTitle()

을 하면

 

Spring Boot 공부하기

을 얻을 수 있다.

 

Step 13. 오늘 가장 중요한 개념 5개

개념 역할
@RestController HTTP 요청 받음
@Service 비즈니스 로직 담당
JpaRepository DB 접근
@Entity Java 객체 ↔ DB 테이블
DI 필요한 객체를 Spring이 연결

 

그리고 다음 구조를 이해한다.

요청

POST /todos
      │
      ▼
┌──────────────┐
│  Controller  │  HTTP 담당
└──────┬───────┘
       │
       ▼
┌──────────────┐
│   Service    │  비즈니스 로직
└──────┬───────┘
       │
       ▼
┌──────────────┐
│  Repository  │  DB 접근
└──────┬───────┘
       │
       ▼
┌──────────────┐
│     JPA      │
│  Hibernate   │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│    MySQL     │
└──────────────┘

 

그리고

 

현재 Controller에서

@RequestBody Todo todo

처럼 Entity를 요청 데이터로 바로 사용하고 있는데 학습 단계이므로 흐름을 쉽게 보기 위해 일부러 이렇게 한 것이고 

 

실무에서는 보통

HTTP
 ↓
DTO
 ↓
Controller
 ↓
Service
 ↓
Entity

처럼 DTO와 Entity를 분리한다.

다음 실습에서 다룰 것이다.

 

지금 학습 단계에서는 다음에 대해 설명할 수 있으면 충분하다.

Controller → 요청을 받는다
Service    → 로직을 처리한다
Repository → DB에 접근한다
Entity     → DB 테이블과 연결된다
DI         → Spring이 객체 관계를 연결한다