스프링프레임워크 :: 5.스프링부트 JPA 프로그래밍
JPA 개요
JPA(Java Persistence API)
는 자바 영속성 API 즉 영속성 프로그래밍을 위한 라이브러리로 이해할 수 있습니다. 영속성이라는 용어 자체는 프로그램이 종료되어도 사라지지 않는 데이터의 특성을 의미 합니다. 일반적으로 데이터베이스를 통해 영속성 구현이 가능합니다.
자바 데이터베이스 프로그래밍의 문제점
자바에서는 처음에 JDBC(Java Database Connectivity)
를 만들어 표준 규격을 정하고 구현은 DB벤더에 맡기는 방식으로 영속성 관리를 지원 했습니다. 이는 SQL 기반의 개발이었으며 지금도 가장 기본적인 데이테이베이스 연동 프로그램을 구현하는 방법입니다.
그러나 JDBC 방식은 관계형 데이터베이스와의 연동을 위해 자바 객체를 SQL 형태로 풀어서 서술해야 하며 DB로 부터 가지고온 데이터의 경우 다시 자바 객체로 변환하는 등의 작업을 거쳐야 하는 문제가 있습니다. 특히 데이터베이스 연동 프로그램의 규모가 커지고 복잡해질수록 관련된 자바객체와 쿼리등을 관리하는 것 역시 점점 어려워집니다.
웹 프로그램이라고 가정하고 사용자 입력을 저장하는 경우 다음과 같은 과정을 거치게 됩니다.
- 입력한 값들을 자바 객체(DO: Data Object 혹은 Entity)로 매핑
- DAO(Data Access Object) 클래스에서 해당 객체를 인자로 하는 저장 메서드 호출
- 메서드 구현부에서는 쿼리를 생성하기 위해 필드값들을 가져와 SQL을 만들고 실행
- 실행 결과를 다시 DO 혹은 DTO(Data Transfer Object) 형태로 만들어 View 에 전달
ORM과 JPA
이러한 문제 해결을 위해 등장한 것이 ORM(Object Relational Mapping)
기술 입니다. 프로그램의 복잡도를 줄이고 자바 객체와 쿼리를 분리할 수 있으며 트랜잭션 처리나 기타 데이터베이스 관련 작업들을 좀 더 편리하게 처리할 수 있는 방법으로 Hibernate
, Mybatis
등이 있습니다.
Java EE에 포함된 EJB
의 Entity Bean 역시 같은 목적에서 나온것으로 여러 과정을 거쳐 지금은 자바의 모든 환경에서 사용할 수 있는 독립적인 API 규격인 JPA가 되었습니다.
Mybatis
는 SQL 을 프로그램에서 분리해서 처리할 수 있는 방법으로 SQL Mapper
라고도 합니다. 데이터베이스의 구조상 복잡한 쿼리가 많은 경우 선호하는 방법이며 지금도 많은 곳에서 사용하고 있습니다.
JPA
는 ORM
에 대한 자바 API 규격이며 Hibernate 는 JPA를 구현한 구현체 입니다. Hibernate 이외에도 EcipseLink, DataNucleus, OpenJPA, TopLink 등이 있습니다. JPA는 Hibernate 로 부터 많은 부분을 수용하고 있습니다.
JPA 장점
- 객체지향적으로 데이터를 관리할 수 있기 때문에 전체 프로그램 구조를 일관되게 유지할 수 있다.
- 테이블 생성, 변경, 관리가 쉽다. (JPA를 잘 이해하고 있는 경우)
- 로직을 쿼리에 집중하기 보다는 객체 자체에 집중 할 수 있다.
- 빠른 개발이 가능하다.
JPA 단점
- 어렵다. 장점을 살리기 위해서는 알아야 할게 많다.
- 잘 이해하고 사용하지 않으면 데이터 손실이 있을 수 있다.
- 충분한 경험없이 실제 프로젝트에 적용할 경우 성능상 문제가 있을 수 있다.
- JPA로 모든 쿼리를 처리할수 없으므로 JPQL, QueryDSL 등도 사용할 수 있어야 한다.
JPA 쿼리 구현 방법
JPA에서 데이터베이스 쿼리를 구현하는 방법은 다음의 4가지가 있습니다.
- Named Query Method
- JPQL
- Criteria
- QueryDSL
Named Query Method
가장 기본적인 방법으로 개발자가 별도의 쿼리를 작성하지 않고 메서드 이름을 조합해 원하는 쿼리를 실행하는 방식 입니다.
스프링에서는 리파지토리 인터페이스에 다음과 같이 메서드 선언부만 추가하고 사용하면 됩니다.
Named Query Method example
List<City> findByPopulationGreaterThan(int p);
JPQL
JPA 에서는 개발자가 직접 쿼리를 작성하지 않고 제공되는 이름조합 기반의 메서드를 통해 쿼리구현이 가능하지만 복잡한 쿼리는 처리하기 어렵습니다. 따라서 예외적인 쿼리들을 처리할 수 있는 방법이 나오게 되었는데 그것이 JPQL 입니다.
기본적으로 SQL과 거의 유사하며 JPA 안에서 사용할 수 있는 형태로 확장된 구조 입니다. 스프링에서는 @Query 애너테이션을 리파지토리 인터페이스의 메서드 선언 부분에 작성하면 됩니다.
JPQL example
@Query("SELECT c FROM City c WHERE c.population > :p")
public List<City> findCity(@Param("p") int p);
Criteria
그러나 JPQL 은 문자열로 작성되기 때문에 SQL과 마찬가지의 문제가 있고 구조적으로도 좋지 못하기 때문에 메서드 호출로 쿼리를 생성하는 Criteria 쿼리가 나오게 되었습니다.
타입 지원을 통해 개발도구의 문법체크와 코드 지원을 받을 수 있어 매우 편리합니다.
Criteria example
List<City> find City(int p) {
CriteriaQuery<City> q = cb.createQuery(City.class);
Root<City> c = q.from(City.class);
q.select(c);
q.where(cb.greaternThan(q.get("population"), p));
List<City> cityList = q.getResultList();
return cityList;
}
QueryDSL
Criteria 쿼리는 JPA에 포함된 표준 규격이지만 코드가 다소 복잡한 문제가 있어 이를 해결하기 위해 개발된 오픈소스 라이브러리 입니다.
Criteria 와 유사한 방식의 코드를 지원하지만 코드가 단순하고 가독성이 높다는 장점이 있습니다. 다만 일부 설정이 추가 되어야 하고 자동생성되는 코드들도 있어 관리가 필요 합니다.
QueryDSL example
List<City> find City(int p) {
JPAQuery query = new JPAQuery(em);
QCity qcity = new QCity("city");
List<City> cityList = query.from(qcity)
.where(qcity.population.goe(p))
.list(qcity);
return cityList;
}
Spring Data JPA에서는 이들 방법을 모두 지원하고 있으며 기본적인 JPA 구현이외에 필요한 복잡한 쿼리들을 원하는 방식으로 처리할 수 있습니다. 물론 가장 원초적이라고 할 수 있는 JDBC 혹은 스프링 JDBC Template 등을 사용할수도 있습니다.
Spring Boot JPA 예제 셋업
Spring Data JPA
스프링프레임워크의 서브 프로젝트중 하나로 JPA를 스프링프레임워크에서 손쉽게 사용할 수 있도록 도와줍니다.
반복되는 코드 구조를 단순화 하거나 자동화 할 수 있으며 서비스나 핵심 기능에 집중할 수 있습니다. 내부적으로 기본 JPA 구현체는 Hibernate 를 사용하고 있습니다.
프로젝트 설정
기존 예제의 프로젝트를 계속 사용하기 때문에 별도의 프로젝트 생성은 필요 없습니다. 다만 pom.xml 에 다음과 같이 필요한 라이브러리들을 추가합니다.
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
spring-boot-starter-data-jpa
: 스프링부트 JPA 스타터로 JAP 사용을 위한 관련 라이브러리 설치와 설정을 자동으로 진행h2
: h2 데이터베이스 및 JDBC 드라이버spring-boot-devtools
: 개발중 소스 수정시 자동 서버 리로드 기능 사용
패키지 생성 및 코드 복사
com.example.jpa 패키지를 생성하고 ProductManager.java 를 제외한 나머지 소스를 복사해 옵니다.
- 기존 Demo로 시작하는 클래스들은 Jpa 로 시작하도록 이름을 변경 합니다.
- Application 은 SpringStudyJpaApplication.java 로 변경 합니다.
JpaRestController.java
JpaWebController.java
Product.java
SpringStudyJpaApplication.java
데이터베이스
JPA 학습을 위해서는 당연히 데이터베이스가 필요합니다. MySQL, MariaDB, Oracle 등 모든 관계형 데이터베이스를 사용할 수 있습니다. 여기서는 별도의 설치가 필요없고 바로 사용이 가능한 내장형(Embedded) 데이터베이스인 H2 를 사용합니다.
H2 데이터베이스는 단독 실행을 통해 서버 모드로 실행할 수도 있고 메모리 DB나 파일로 부터 실행하는 방법등 다양한 운영이 가능합니다. 여기서는 임베디드 모드를 사용할 것이기 때문에 pom.xml 설정만으로 추가적으로 DB설치는 필요 없습니다.
만일 H2 데이터베이스를 서버 모드로 실행하려면 공통기초->개발환경구축하기 를 참고하기 바랍니다.
JPA 관련 환경 설정
application.propertis 에 데이터베이스접속과 JPA 관련 설정들을 추가 합니다.
# H2 DB console
웹 기반의 H2 데이터베이스 관리 도구 실행을 지정하는 부분 입니다. localhost:8080/h2-console 로 접속할 수 있도록 설정 합니다.
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# H2 DB Connection
H2 데이터베이스를 사용하기 위한 JDBC 설정 입니다.
spring.datasource.url=jdbc:h2:file:./data/demodb
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driver-class-name=org.h2.Driver
- 일반적인 JDBC url, driver class 이름등을 설정.
jdbc:h2:file:./data/demodb
는 프로젝트 폴더 바로 아래에 data 폴더를 생성하고 DB파일을 생성.
# JPA 설정
은 JPA와 관련한 테이블 자동생성 및 데이터 스크립트 실행 설정 입니다. JPA와 관련해서는 많은 설정 항목이 있으며 추후 진행하면서 다루게 됩니다.
spring.datasource.initialization-mode=embedded
spring.jpa.hibernate.ddl-auto=update
spring.datasource.initialization-mode
는 테이블 및 데이터 자동 초기화 설정 입니다. schema.sql, data.sql(data-h2.sql)등의 파일이 있으면 자동으로 해당 파일의 내용으로 데이터베이스 초기화 작업을 수행 합니다.
- always: 항상 엔티티 객체에 대한 테이블 구조를 새롭게 생성.
- embedded: 내장형 데이터베이스(H2와 같은)에 대해서만 생성.
- never: 스키마를 자동생성하지 않음.
spring.jpa.hibernate.ddl-auto
은 테이블 자동 생성 설정 입니다. 옵션에 따라 다음과 같이 동작 합니다.
- create: 기존에 존해하는 테이블을 삭제하고 새로운 테이블을 생성.
- create-drop: 테이블을 먼저 생성한다음 모든 작업이 끝나면 테이블을 삭제 -> 보통 테스트용.
- none: DDL 생성을 끔.
- update: 기존 스키마와 엔티티를 비교해 변경된 사항이 있으면 갱신함.
- validate: 필요한 테이블과 컬럼등이 존재하는지 검증후 문제가 있으면 예외발생.
Spring Boot JPA 예제 구현
도메인 클래스
Entity 클래스 라고도 합니다. JPA에서 테이블 구조를 대신하는 객체로 스프링에서는 @Entity 애너테이션 설정으로 생성할 수 있습니다. 일반적인 POJO 구조이고 이전 예제와 달리 초기 데이터 생성이 필요 없으므로 생성자는 없어도 됩니다.
Product.java
package com.example.jpa;
@Entity
public class Product {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private int id;
private String name;
private int price;
...getter/setter 생략
}
@Entity
- 테이블과 링크될 클래스임을 나타냄.
- 언더스코어 네이밍(_)으로 테이블 이름을 매칭합니다.
- ex) ShopManager.java -> shop_manager 테이블 생성됨.
@Id
- 해당 테이블의 PK 필드를 나타냅니다.
- @GeneratedValue 옵션을 지정해야 자동증가 컬럼을 사용할 수 있음.
@GeneratedValue
- PK의 생성 규칙을 지정함.
- 기본값은 AUTO 로, MySQL의 auto_increment와 같음.
@Column
- 테이블의 컬럼을 나타내며 굳이 선언하지 않더라도 해당 클래스의 필드는 모두 컬럼이 됨.
- 기본값 외에 추가로 변경이 필요한 경우 사용.
- ex) 문자열의 경우 VARCHAR(255)가 기본 -> 사이즈를 조정하거나 타입을 변경할대 사용.
JPA 리파지토리
DAO 클래스에 해당하는것으로 직접 클래스를 구현하는 것이 아니라 인터페이스를 생성하면 자동으로 구현된 객체가 참조 되는 형식 입니다. 만일 QueryDSL 등을 사용하거나 별도 쿼리 구현이 필요한 경우 해당 인터페이스를 직접 구현하는 클래스를 만들어 사용할수도 있습니다.
클래스와 애너테이션의 패키지는 org.springframework.data
에 있는 것으로 import 하기 바랍니다.
놀랍게도 데이터베이스 처리와 관련된 코드는 다음의 코드가 전부 입니다. 부분 코드가 아닙니다.
package com.example.jpa;
public interface ProductRepository extends JpaRepository<Product, Integer> {
}
JPA 리파지토리 인터페이스를 만들때 상속할 수 있는 인터페이스들이 있습니다. 대표적으로 다음의 세가지 인터페이스들을 주로 사용하게 됩니다.
만일 이들 인터페이스를 상속받지 않고 모든 인터페이스 메서드를 직접 처리하려면 @Repository 애너테이션을 추가한 다음 필요한 메서드를 선언해 사용해야 합니다.
CrudRepository
가장 기본이 되는 리파지토리로 이름과 같이 기본적인 CRUD 기능을 제공 합니다.
PagingAndSortingRepository
페이지 기능과 정렬 기능이 추가된 리파지토리 입니다. CrudRepository 를 상속받고 있습니다.
JpaRepository
PagingAndSortingRepository 를 상속받고 있습니다. JPQL 등을 사용할 수 있으며 가장 많이 사용되는 인터페이스 입니다.
컨트롤러 설정
이전 강좌에서 만든 상품정보 Rest 및 MVC 컨트롤러를 코드를 복사해서 JPA리파지토리와 연동되도록 수정 합니다. 여기서는 JpaRestController
클래스만 살펴봅니다. MVC 컨트롤러도 동일한 관점에서 수정하면 됩니다.
먼저 리파지토리를 참조하기 위해 오토와이어링을 사용합니다. productRepo 는 스프링에 의해 자동 생성된 ProductRepository 인터페이스 구현 클래스에 대한 참조를 가지게 됩니다.
@Autowired
ProductRepository productRepo;
다음은 상품 목록을 전달하는 url 매핑 처리 입니다. productRepo 의 findAll() 메서드를 호출하고 있습니다.
@GetMapping("/product")
public Iterable<Product> getAll() {
return productRepo.findAll();
}
아이디로 특정 상품을 검색하기 위해 findById() 를 사용할 수 있습니다. 이때 Optional 을 리턴하기 때문에 다음과 같이 예외처리를 해주어야 합니다.
@GetMapping("/product/{id}")
public Product getProduct(@PathVariable int id) {
return productRepo.findById(id).orElseThrow(() -> new RuntimeException("product not found!!"));
}
상품 등록의 경우 save() 메서드를 사용하면 되고 삭제의 경우 deleteById()를 사용하면 됩니다.
@PostMapping("/product")
public String addProduct(@RequestBody Product p) {
productRepo.save(p);
return "product added!!";
}
@GetMapping("/product/delete/{id}")
public String delProduct(@PathVariable int id) {
productRepo.deleteById(id);
return "product deleted!!";
}
실행 및 테스트
SpringStudyJpaApplication 클래스를 실행하고 web 과 api 에 대해 각각 동작을 테스트 해보도록 합니다.
- web : http://localhost:8080/web
- api : http://localhost:8080/api/product
별도의 테이블 생성과 데이터베이스 코드 구현 없이 모든 기능이 동작하는 것을 확인할 수 있습니다.
데이터베이스에 저장된 값의 확인을 위해서는 H2 console 을 이용하도록 합니다. http://localhost:8080/h2-console 로 접속한 다음 설정 사항을 확인한다음 접속해서 원하는 쿼리를 작성해 조회해 봅니다.