출시·고도화 중
Spring Boot 안내서 · 4/6
Spring Boot 서버는 보통 요청 사이에 상태를 메모리에 두지 않고 데이터베이스에 저장합니다. 이 장에서는 Spring Data JPA로 데이터를 다루고, 트랜잭션으로 일관성을 지키고, 입력값을 검증하고, Flyway로 스키마를 관리하는 방법을 다룹니다.
PostgreSQL을 쓴다면 다음 의존성을 추가합니다. Initializr에서는 data-jpa, postgresql, flyway, validation을 고르면 됩니다.
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-flyway")
implementation("org.flywaydb:flyway-database-postgresql")
runtimeOnly("org.postgresql:postgresql")
}연결 정보는 spring.datasource.*로 적습니다. 스키마는 Flyway에 맡기고, Hibernate는 엔티티와 테이블이 맞는지 검사만 하게 합니다.
spring:
datasource:
url: jdbc:postgresql://localhost:5432/demo
username: demo
password: ${DB_PASSWORD}
jpa:
hibernate:
ddl-auto: validate
open-in-view: false엔티티는 테이블 한 행에 대응하는 클래스입니다. JPA는 인자 없는 생성자가 필요하므로 protected로 하나 둡니다.
@Entity
public class Todo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 200)
private String title;
private boolean done;
protected Todo() {}
public Todo(String title) { this.title = title; }
public void complete() { this.done = true; }
public Long getId() { return id; }
public String getTitle() { return title; }
public boolean isDone() { return done; }
}저장소는 인터페이스만 선언하면 Spring Data가 구현을 만들어 줍니다. JpaRepository가 save, findById, findAll, deleteById 같은 기본 메서드를 제공하고, 메서드 이름 규칙으로 쿼리를 만들 수도 있습니다.
public interface TodoRepository extends JpaRepository<Todo, Long> {
List<Todo> findByDoneFalseOrderByIdAsc();
@Query("select t from Todo t where lower(t.title) like lower(concat('%', :keyword, '%'))")
List<Todo> search(@Param("keyword") String keyword);
}여러 작업을 하나로 묶어야 하는 곳은 서비스 계층입니다. @Transactional을 붙인 메서드는 시작할 때 트랜잭션을 열고, 정상 종료하면 커밋, 런타임 예외가 나면 롤백합니다. 트랜잭션 안에서 읽어 온 엔티티의 값을 바꾸면 save를 부르지 않아도 커밋할 때 반영됩니다(변경 감지).
@Service
@Transactional(readOnly = true)
public class TodoService {
private final TodoRepository repository;
public TodoService(TodoRepository repository) {
this.repository = repository;
}
public List<Todo> open() {
return repository.findByDoneFalseOrderByIdAsc();
}
@Transactional
public void complete(Long id) {
Todo todo = repository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
todo.complete();
}
}주의할 점이 두 가지 있습니다. 기본적으로 체크 예외는 롤백하지 않으므로 필요하면 rollbackFor를 지정합니다. 그리고 @Transactional은 프록시로 동작하므로, 같은 클래스 안에서 다른 메서드를 직접 호출하면 적용되지 않습니다.
요청 객체에 Bean Validation 제약을 붙이고 컨트롤러 매개변수에 @Valid를 붙이면, 검증에 실패한 요청은 컨트롤러에 들어오기 전에 400 응답으로 끝납니다.
public record CreateTodo(@NotBlank @Size(max = 200) String title) {}
@PostMapping
public TodoResponse create(@Valid @RequestBody CreateTodo request) {
return service.create(request.title());
}오류 응답 형식을 정하고 싶다면 @RestControllerAdvice에서 MethodArgumentNotValidException을 받아 ProblemDetail(RFC 9457 형식)로 돌려줄 수 있습니다.
Flyway는 src/main/resources/db/migration의 SQL 파일을 버전 순서대로 실행하고, 어디까지 적용했는지 flyway_schema_history 테이블에 기록합니다. 애플리케이션이 시작될 때 아직 적용하지 않은 파일만 실행됩니다.
-- src/main/resources/db/migration/V1__create_todo.sql
create table todo (
id bigint generated by default as identity primary key,
title varchar(200) not null,
done boolean not null default false
);한 번 적용된 마이그레이션 파일은 고치지 않습니다. 변경이 필요하면 V2__add_due_date.sql처럼 새 파일을 추가합니다.
JpaRepository를 상속한 인터페이스만으로 기본 CRUD와 쿼리 메서드를 얻습니다.@Transactional로 겁니다.@Valid로 검증합니다.validate로 둡니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.