Spring Boot JPA 알아보기
Spring Boot에서 JPA를 사용하는 방법에 대해 정리한 내용입니다.
1. Spring Boot JPA
1.1. JPA (Java Persistence API)
JPA는 EJB(Enterprise Java Beans) 3.0 스펙에서 Hibernate를 기반으로 JavaSE, JavaEE를 위한 영속성 관리와 ORM을 위한 기술 표준입니다. 즉, ORM을 사용하기 위한 표준 API 인터페이스를 정의한 것입니다. JPA를 사용하기 위해서는 Hibernate, OpenJPA, EclipseLink, DataNucleus 등과 같이 JPA를 구현한 ORM 프레임워크를 사용해야합니다.
Spring의 Spring Data JPA를 사용하면 JPA를 더 편리하게 사용할 수 있는데 Repository 인터페이스에 정해진 규칙에 따라 메서드를 작성하면 Spring이 구현체를 만들어서 Bean에 등록해줍니다.
JPA 사용에 따른 장단점은 다음과 같습니다.
- 장점
객체 지향적인 코드 사용으로 비즈니스 로직에 좀 더 집중 가능.
객체 지향적인 데이터 관리로 일관된 구조 유지 가능.
SQL 대신 객체를 사용하기 때문에 생산성과 재사용성이 높아져서 유지보수가 용이하고 빠른 개발이 가능.
DBMS에 대한 종속성이 줄어들어 객체 자체에만 집중 가능.
테이블에 대한 생성, 수정 과정이 줄어들어 관리가 쉬움. - 단점
잘 사용하기 위해선 높은 학습 비용을 필요로 함.
통계와 같은 복잡한 쿼리 처리에 불리한 점이 있음.
잘못 사용한 경우엔 성능 문제나 데이터 손실이 발생할 수 있음.
기존 환경이 데이터베이스 중심으로 설계되어 있는 경우엔 사용하기에 어려움이 있음.
1.2. ORM (Object-Relational Mapping)
ORM은 객체와 RDB(Relational Database) 테이블을 매핑하여 객체 지향적으로 다루는 기술입니다. ORM 프레임워크를 사용하여 객체와 테이블을 매핑하고 SQL 쿼리가 아닌 자바 메서드를 이용한 데이터 조작이 가능합니다. 객체와 테이블 또한 트랜잭션과 같은 데이터베이스 관련 작업들을 좀 더 편리하게 처리할 수 있습니다.
참고로 MyBatis, iBatis와 같은 SQL Mapper와 ORM의 차이는 다음과 같습니다.
- ORM
객체와 테이블을 매핑.
ORM 프레임워크가 SQL을 생성해서 처리.
ex) Hibernate - SQL Mapper
객체와 SQL을 매핑.
사용자의 SQL 생성이 필요.
JDBC API 사용에 따른 응답 결과를 객체로 매핑하는 과정을 처리.
ex) MyBatis, iBatis
1.3. Hibernate
JPA를 사용하기 위해서 이를 구현한 ORM 프레임워크입니다. javax.persistence.EntityManager와 같은 JPA의 인터페이스를 직접 구현한 라이브러리이며 내부적으로는 JDBC API를 사용합니다.
Hibernate의 특징은 다음과 같습니다.
- SQL을 사용하지 않고 메서드를 이용한 쿼리를 수행하며 객체 중심적 개발이 가능.
- 추상화된 데이터 접근 계층을 제공하여 데이터베이스에 종속적이지 않음.
- 메서드를 이용한 쿼리 수행의 한계를 보완하기 위해 JPQL, Native Query를 지원.
2. Spring Data JPA 사용하기
2.1. 디렉터리 구조 설정
예제 구성을 위해 아래와 같이 디렉터리 구조를 설정해줍니다.
src
└─ main
└─ java
└─ com
└─ freestrokes
├─ controller
│ └─ BoardController.java
├─ domain
│ └─ Board.java
├─ dto
│ └─ BoardDto.java
├─ repositroy
│ └─ BoardRepository.java
├─ service
│ └─ BoardService.java
└─ SpringBootJpaApplication.java
2.2. dependency 추가하기 (gradle)
build.gradle 파일에 아래와 같이 spring-boot-starter-data-jpa와 spring-boot-starter-jdbc 의존성을 추가해줍니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
implementation "org.springframework.boot:spring-boot-starter-jdbc"
implementation "junit:junit:4.13.2"
testImplementation 'org.springframework.boot:spring-boot-starter-test'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
}
2.3. application.yml 설정 (MySQL 연동)
MySQL 연동을 위해 application.yml 파일을 아래와 같이 작성해줍니다.
spring:
jpa:
open-in-view: false
database: mysql
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
properties:
hibernate:
show_sql: true
format_sql: true
use_sql_comments: true
hibernate:
ddl-auto: none
naming:
physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
datasource:
url: jdbc:mysql://{HOST}:{PORT}/testdb?useUnicode=true&characterEncoding=utf8&useSSL=false
username: {USERNAME}
password: {PASSWORD}
다음은 yml 설정에 사용한 속성값들에 대한 내용입니다.
- spring.jpa.open-in-view
OSIV(Open Session In View) 활성화 옵션.
기본값은 true이며 활성화 됐을 경우엔 응답이 완료되거나 view가 렌더링 될 때까지 영속성 컨텍스트를 유지.
성능과 확장성 면에서 좋지 않기 때문에 false로 설정. - spring.jpa.database
JPA 데이터베이스를 설정. - spring.jpa.database-platform
JPA 데이터베이스의 플랫폼을 설정. - spring.jpa.properties.hibernate
하이버네이트 관련 설정.
- show_sql
하이버네이트 SQL 로깅 설정. - format_sql
하이버네이트 SQL 포맷팅 설정. - use_sql_comments
하이버네이트 SQL 주석 설정.
- show_sql
- spring.jpa.hibernate.ddl-auto
데이터베이스 초기화 전략을 설정.
- none
아무 것도 실행하지 않음. - create
애플리케이션 실행시 기존 테이블을 모두 삭제하고 다시 생성. - create-drop
애플리케이션 실행시 기존 테이블을 모두 삭제하고 다시 생성.
애플리케이션 종료시 테이블을 모두 삭제. - update
애플리케이션 실행시 변경된 스키마만 반영. - validate
엔티티와 테이블 내용이 동일하게 매핑되었는지 확인.
일치하지 않을 경우 애플리케이션을 실행시키지 않음.
- none
- spring.jpa.hibernate.naming
엔티티와 테이블에 대한 네이밍 전략을 설정.
- physical-strategy
물리적 명칭 전략.
암시적(논리적) 명칭 전략이 적용된 이후에 적용 됨.
데이터베이스의 공통적인 명칭에 사용하는 것이 권장 됨. - implicit-strategy
암시적(논리적) 명칭 전략.
하이버네이트에서 기본적으로 논리적 명칭을 설정하기 위해 사용.
엔티티에서 @Table, @Column 어노테이션을 사용하여 이름을 설정하지 않은 경우 적용 됨.
- physical-strategy
- spring.datasource
데이터베이스 연결 관련 설정.
- url
데이터베이스 url. - username
데이터베이스 계정. - password
데이터베이스 비밀번호.
- url
2.4. Entity 생성
JPA를 사용해서 테이블과 매핑할 엔티티 클래스를 작성해줍니다.
package com.freestrokes.domain;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Getter
@NoArgsConstructor
@Entity(name = "board")
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", unique = true, nullable = false)
private Long id;
@Column(name = "title", length = 500)
private String title;
@Column(name = "content", columnDefinition = "TEXT")
private String content;
@Column(name = "author", length = 100)
private String author;
public void update(Board board) {
this.title = board.getTitle();
this.content = board.getContent();
this.author = board.getAuthor();
}
@Builder
public Board(
String title,
String content,
String author
) {
this.title = title;
this.content = content;
this.author = author;
}
}
다음은 엔티티 클래스에 사용한 JPA 어노테이션들에 대한 내용입니다.
- @Entity
객체와 테이블을 매핑하기 위해 사용.
name 속성을 사용하여 엔티티 이름 지정 가능.
엔티티 이름의 기본값은 camelCase로 작성된 클래스명을 under_score 네이밍에 매칭하여 작성.
파라미터가 없는 기본 생성자가 필요 (public 또는 protected)
final 클래스, inner 클래스, interface, enum에는 사용 불가.
저장할 필드에는 final 사용 불가. - @Id
매핑된 테이블에서 기본키(PK)로 사용할 필드를 지정하기 위해 사용. - @GeneratedValue
기본키(PK)로 지정된 필드에 대해 생성 규칙을 명명하기 위해 사용.
strategy 속성을 GenerationType.IDENTITY로 지정하면 AUTO_INCREMENT로 적용 됨. - @Column
객체 필드를 테이블 필드에 매핑하기 위해 사용.
name 속성을 사용하여 필드 이름 지정 가능.
함께 사용한 lombok 어노테이션에 대한 내용은 다음과 같습니다.
- @Getter
필드에 대한 접근자 메서드를 생성. - @NoArgsConstructor
파라미터가 없는 기본 생성자를 생성. - @Builder
엔티티 클래스에 빌더 패턴을 적용하기 위해 사용.
필드 변경에 따른 유연성과 가독성을 높이고 불변성을 유지하기 위해 사용.
클래스가 아닌 생성자에 사용시 생성자에 포함된 필드에만 적용 됨.
(update 메서드는 custom setter로 사용하기 위해 추가)
2.5. DTO 생성
계층간 데이터 교환을 위해 사용할 DTO 클래스를 작성해줍니다. 용도에 따라 RequestDto, ResponseDto를 나눠서 작성해줍니다.
package com.freestrokes.dto;
import com.freestrokes.domain.Board;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
public class BoardDto {
@Getter
public static class RequestDto {
private String title;
private String content;
private String author;
@Builder
public RequestDto(
String title,
String content,
String author
) {
this.title = title;
this.content = content;
this.author = author;
}
public Board toEntity() {
return Board.builder()
.title(title)
.content(content)
.author(author)
.build();
}
}
@Getter
public static class ResponseDto {
private Long id;
private String title;
private String content;
private String author;
@Builder
public ResponseDto(
Long id,
String title,
String content,
String author
) {
this.id = id;
this.title = title;
this.content = content;
this.author = author;
}
public Board toEntity() {
return Board.builder()
.title(title)
.content(content)
.author(author)
.build();
}
}
}
2.6. Repository 생성
엔티티에 대한 CRUD 메서드를 사용하기 위해 JpaRepository 인터페이스를 작성해줍니다. JpaRepository<Entity 클래스, PK 타입>를 상속 받도록 구현해주면 해당 엔티티의 기본적인 CRUD 메서드가 자동으로 생성됩니다.
package com.freestrokes.repository;
import com.freestrokes.domain.Board;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface BoardRepository extends JpaRepository<Board, Long> {
}
- @Repository
해당 클래스가 Repository 클래스인 것을 명시.
스프링의 component-scan에 의해 스프링 빈(bean)으로 자동 등록 됨.
JPA 예외 발생시 스프링이 추상화한 예외로 변환하여 서비스 계층에 반환.
2.7. Controller 생성
API 요청을 받을 Controller 클래스를 작성해줍니다.
package com.freestrokes.controller;
import com.freestrokes.dto.BoardDto;
import com.freestrokes.service.BoardService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/v1")
public class BoardController {
private final BoardService boardService;
@GetMapping(path = "/boards", produces = "application/json")
public ResponseEntity<List<BoardDto.ResponseDto>> getBoards() throws Exception {
List<BoardDto.ResponseDto> result = boardService.getBoards();
return new ResponseEntity<List<BoardDto.ResponseDto>>(result, HttpStatus.OK);
}
@PostMapping(path = "/boards", produces = "application/json")
public ResponseEntity<BoardDto.ResponseDto> postBoard(@RequestBody BoardDto.RequestDto boardRequestDto) throws Exception {
BoardDto.ResponseDto result = boardService.postBoard(boardRequestDto);
return new ResponseEntity<BoardDto.ResponseDto>(result, HttpStatus.OK);
}
@PutMapping(path = "/boards/{id}", produces = "application/json")
public ResponseEntity<BoardDto.ResponseDto> putBoard(@PathVariable Long id, @RequestBody BoardDto.RequestDto boardRequestDto) throws Exception {
BoardDto.ResponseDto result = boardService.putBoard(id, boardRequestDto);
return new ResponseEntity<BoardDto.ResponseDto>(result, HttpStatus.OK);
}
@DeleteMapping(path = "/boards/{id}", produces = "application/json")
public ResponseEntity<?> deleteBoard(@PathVariable Long id) throws Exception {
boardService.deleteBoard(id);
return new ResponseEntity<>("{}", HttpStatus.OK);
}
}
다음은 Controller 클래스에 사용한 어노테이션들에 대한 내용입니다.
- @RestController
@Controller 어노테이션에 @ResponseBody 어노테이션이 추가된 형태.
응답 객체를 ResponseEntity로 감싸서 반환. - @RequestMapping
요청 받을 url과 http 메서드를 설정. - @GetMapping
GET 요청을 매핑하기 위한 어노테이션. - @PostMapping
POST 요청을 매핑하기 위한 어노테이션. - @PutMapping
PUT 요청을 매핑하기 위한 어노테이션. - @DeleteMapping
DELETE 요청을 매핑하기 위한 어노테이션. - @RequestBody
요청 받은 데이터를 Java 객체로 변환해주는 어노테이션.
본문의 JSON, XML, Text 등의 데이터가 HttpMessageConverter를 통해 파싱되어 변환 됨. - @PathVariable
url path에서 동일한 이름의 변수에 대해 파라미터로 처리해주는 어노테이션.
함께 사용한 lombok 어노테이션에 대한 내용은 다음과 같습니다.
- @RequiredArgsConstructor
final 또는 @NonNull 필드만 파라미터로 받는 생성자를 생성.
@Autowired 어노테이션 없이 의존성을 주입하기 위해 사용.
2.8. Service 생성
비즈니스 로직을 수행하는 Service 클래스를 작성해줍니다.
package com.freestrokes.service;
import com.freestrokes.domain.Board;
import com.freestrokes.dto.BoardDto;
import com.freestrokes.repository.BoardRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@Service
public class BoardService {
private final BoardRepository boardRepository;
public List<BoardDto.ResponseDto> getBoards() throws Exception {
List<BoardDto.ResponseDto> boardsResponseDto = boardRepository.findAll()
.stream()
.map(item -> {
return BoardDto.ResponseDto.builder()
.id(item.getId())
.title(item.getTitle())
.content(item.getContent())
.author(item.getAuthor())
.build();
})
.collect(Collectors.toList());
return boardsResponseDto;
}
public BoardDto.ResponseDto postBoard(BoardDto.RequestDto boardRequestDto) throws Exception {
Board board = boardRepository.save(boardRequestDto.toEntity());
BoardDto.ResponseDto boardResponseDto = BoardDto.ResponseDto.builder()
.id(board.getId())
.title(board.getTitle())
.content(board.getContent())
.author(board.getAuthor())
.build();
return boardResponseDto;
}
public BoardDto.ResponseDto putBoard(Long id, BoardDto.RequestDto boardRequestDto) throws Exception {
Optional<Board> persistBoard = boardRepository.findById(id);
if (persistBoard.isPresent()) {
Board board = Board.builder()
.title(boardRequestDto.getTitle())
.content(boardRequestDto.getContent())
.author(boardRequestDto.getAuthor())
.build();
persistBoard.get().update(board);
boardRepository.save(persistBoard.get());
} else {
throw new NoSuchElementException();
}
BoardDto.ResponseDto boardResponseDto = BoardDto.ResponseDto.builder()
.id(persistBoard.get().getId())
.title(persistBoard.get().getTitle())
.content(persistBoard.get().getContent())
.author(persistBoard.get().getAuthor())
.build();
return boardResponseDto;
}
public void deleteBoard(Long id) throws Exception {
boardRepository.deleteById(id);
}
}
- @Service
해당 클래스가 비즈니스 로직을 포함한 Service 클래스인 것을 명시.
스프링의 component-scan에 의해 스프링 빈(bean)으로 자동 등록 됨.
3. CRUD API 테스트하기
Gradle 빌드 후에 Spring Boot 애플리케이션을 실행하고 Postman을 이용하여 다음과 같이 API 테스트를 해줍니다.
3.1. postBoard (POST)
3.2. getBoards (GET)
3.3. putBoard (PUT)
3.4. deleteBoard (DELETE)
이상으로 Spring Boot에서 JPA를 사용하는 방법에 대해 알아봤습니다.
※ References
- spring.io, Spring Data JPA, https://spring.io/projects/spring-data-jpa
- velog.io/@swchoi0329, Spring Boot에서 JPA 사용하기, https://velog.io/@swchoi0329/Spring-Boot%EC%97%90%EC%84%9C-JPA-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0
- memostack.tistory.com, Spring Boot 에서 JPA 사용하기 (MySQL 사용), https://memostack.tistory.com/155#1.1.%20MySQL%20%EC%84%A4%EC%B9%98
- goddaehee.tistory.com, [스프링부트 (7)] Spring Boot JPA(1) - 시작 및 기본 설정, https://goddaehee.tistory.com/209
- 김영한 지음, 『자바 ORM 표준 JPA 프로그래밍』, 에이콘 (2015), 1장 JPA 소개 (p29 ~ p62)
- 김영한 지음, 『자바 ORM 표준 JPA 프로그래밍』, 에이콘 (2015), 4장 엔티티 매핑 (p121 ~ p153)
- www.daleseo.com, [자바] 자주 사용되는 Lombok 어노테이션, https://www.daleseo.com/lombok-popular-annotations/
- mangkyu.tistory.com, [Java] 빌더 패턴(Builder Pattern)을 사용해야 하는 이유, https://mangkyu.tistory.com/163
- victorydntmd.tistory.com, [SpringBoot] JPA 설정하기 ( MySQL ), https://victorydntmd.tistory.com/323
- velog.io/@mumuni, Hibernate5 Naming Strategy 간단 정리, https://velog.io/@mumuni/Hibernate5-Naming-Strategy-%EA%B0%84%EB%8B%A8-%EC%A0%95%EB%A6%AC
- nefertirii.github.io, 하이버네이트 명명 전략(Hibernate Naming Strategies), https://nefertirii.github.io/jpa/hibernate-naming-strategies/
- jaehoney.tistory.com, DTO를 Inner static class로 간결하게 관리하기! (+ domain 분리), https://jaehoney.tistory.com/157
- velog.io/@hermaeus, 7.JpaRepository 인터페이스, https://velog.io/@hermaeus/7.JpaRepository-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4
- medium.com/webeveloper, @RequiredArgsConstructor 를 이용한 의존성 주입(Dependency Injection), https://medium.com/webeveloper/requiredargsconstructor-%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85-dependency-injection-4f1b0ac33561
- mangkyu.tistory.com, [Spring] @Controller와 @RestController 차이, https://mangkyu.tistory.com/49