[Spring Data] Spring Data JPA 소개, 프로젝트 환경 설정
Topic
- 스프링 데이터 JPA 소개
- 프로젝트 생성
- 라이브러리 살펴보기
- H2 데이터베이스 설치
- 스프링 데이터 JPA와 DB 설정, 동작 확인
01. 스프링 데이터 JPA 소개
스프링 데이터 JPA란 스프링 프레임워크와 JPA 기반 위에서 JPA를 더욱 편리하게 사용하기 위한 기술.
01-1. 스프링 데이터 JPA의 장점
- Repository에 구현 클래스 없이 Interface만으로 개발이 가능
- 기본적인 CRUD 기능을 JPA가 모두 제공
- 반복적인 작업을 줄여 핵심 비즈니스 로직에 집중 가능
02. 프로젝트 생성
02-1. 사용 기능
- version: spring boot 2.7.0
- groupId: study
- artifactid: data-jpa
- lib
- web
- jpa
- h2
- lombok
02-1. 프로젝트 생성 후 간단 테스트
@RestController
public class TestController {
@GetMapping("/test")
public String test() {
return "test";
}
}
- localhost:8080/test로 컨트롤러 요청 확인
- Build Tool 변경
- Lombok 셋팅
03. 라이브러리 살펴보기
03-1. gradle 의존관계 확인
/gradlew dependencies --configuration compileClasspath
Welcome to Gradle 7.4.1!
Here are the highlights of this release:
- Aggregated test and JaCoCo reports
- Marking additional test source directories as tests in IntelliJ
- Support for Adoptium JDKs in Java toolchains
For more details see https://docs.gradle.org/7.4.1/release-notes.html
> Task :dependencies
------------------------------------------------------------
Root project 'data-jpa'
------------------------------------------------------------
compileClasspath - Compile classpath for source set 'main'.
+--- org.projectlombok:lombok -> 1.18.24
+--- org.springframework.boot:spring-boot-starter-data-jpa -> 2.7.0
| +--- org.springframework.boot:spring-boot-starter-aop:2.7.0
| | +--- org.springframework.boot:spring-boot-starter:2.7.0
| | | +--- org.springframework.boot:spring-boot:2.7.0
| | | | +--- org.springframework:spring-core:5.3.20
| | | | | \--- org.springframework:spring-jcl:5.3.20
| | | | \--- org.springframework:spring-context:5.3.20
| | | | +--- org.springframework:spring-aop:5.3.20
| | | | | +--- org.springframework:spring-beans:5.3.20
| | | | | | \--- org.springframework:spring-core:5.3.20 (*)
| | | | | \--- org.springframework:spring-core:5.3.20 (*)
| | | | +--- org.springframework:spring-beans:5.3.20 (*)
| | | | +--- org.springframework:spring-core:5.3.20 (*)
| | | | \--- org.springframework:spring-expression:5.3.20
| | | | \--- org.springframework:spring-core:5.3.20 (*)
| | | +--- org.springframework.boot:spring-boot-autoconfigure:2.7.0
| | | | \--- org.springframework.boot:spring-boot:2.7.0 (*)
| | | +--- org.springframework.boot:spring-boot-starter-logging:2.7.0
| | | | +--- ch.qos.logback:logback-classic:1.2.11
| | | | | +--- ch.qos.logback:logback-core:1.2.11
| | | | | \--- org.slf4j:slf4j-api:1.7.32 -> 1.7.36
| | | | +--- org.apache.logging.log4j:log4j-to-slf4j:2.17.2
| | | | | +--- org.slf4j:slf4j-api:1.7.35 -> 1.7.36
| | | | | \--- org.apache.logging.log4j:log4j-api:2.17.2
| | | | \--- org.slf4j:jul-to-slf4j:1.7.36
| | | | \--- org.slf4j:slf4j-api:1.7.36
| | | +--- jakarta.annotation:jakarta.annotation-api:1.3.5
| | | +--- org.springframework:spring-core:5.3.20 (*)
| | | \--- org.yaml:snakeyaml:1.30
| | +--- org.springframework:spring-aop:5.3.20 (*)
| | \--- org.aspectj:aspectjweaver:1.9.7
| +--- org.springframework.boot:spring-boot-starter-jdbc:2.7.0
| | +--- org.springframework.boot:spring-boot-starter:2.7.0 (*)
| | +--- com.zaxxer:HikariCP:4.0.3
| | | \--- org.slf4j:slf4j-api:1.7.30 -> 1.7.36
| | \--- org.springframework:spring-jdbc:5.3.20
| | +--- org.springframework:spring-beans:5.3.20 (*)
| | +--- org.springframework:spring-core:5.3.20 (*)
| | \--- org.springframework:spring-tx:5.3.20
| | +--- org.springframework:spring-beans:5.3.20 (*)
| | \--- org.springframework:spring-core:5.3.20 (*)
| +--- jakarta.transaction:jakarta.transaction-api:1.3.3
| +--- jakarta.persistence:jakarta.persistence-api:2.2.3
| +--- org.hibernate:hibernate-core:5.6.9.Final
| | +--- org.jboss.logging:jboss-logging:3.4.3.Final
| | +--- net.bytebuddy:byte-buddy:1.12.9 -> 1.12.10
| | +--- antlr:antlr:2.7.7
| | +--- org.jboss:jandex:2.4.2.Final
| | +--- com.fasterxml:classmate:1.5.1
| | +--- org.hibernate.common:hibernate-commons-annotations:5.1.2.Final
| | | \--- org.jboss.logging:jboss-logging:3.3.2.Final -> 3.4.3.Final
| | \--- org.glassfish.jaxb:jaxb-runtime:2.3.1 -> 2.3.6
| | +--- jakarta.xml.bind:jakarta.xml.bind-api:2.3.3
| | +--- org.glassfish.jaxb:txw2:2.3.6
| | \--- com.sun.istack:istack-commons-runtime:3.0.12
| +--- org.springframework.data:spring-data-jpa:2.7.0
| | +--- org.springframework.data:spring-data-commons:2.7.0
| | | +--- org.springframework:spring-core:5.3.20 (*)
| | | +--- org.springframework:spring-beans:5.3.20 (*)
| | | \--- org.slf4j:slf4j-api:1.7.32 -> 1.7.36
| | +--- org.springframework:spring-orm:5.3.20
| | | +--- org.springframework:spring-beans:5.3.20 (*)
| | | +--- org.springframework:spring-core:5.3.20 (*)
| | | +--- org.springframework:spring-jdbc:5.3.20 (*)
| | | \--- org.springframework:spring-tx:5.3.20 (*)
| | +--- org.springframework:spring-context:5.3.20 (*)
| | +--- org.springframework:spring-aop:5.3.20 (*)
| | +--- org.springframework:spring-tx:5.3.20 (*)
| | +--- org.springframework:spring-beans:5.3.20 (*)
| | +--- org.springframework:spring-core:5.3.20 (*)
| | \--- org.slf4j:slf4j-api:1.7.32 -> 1.7.36
| \--- org.springframework:spring-aspects:5.3.20
| \--- org.aspectj:aspectjweaver:1.9.7
\--- org.springframework.boot:spring-boot-starter-web -> 2.7.0
+--- org.springframework.boot:spring-boot-starter:2.7.0 (*)
+--- org.springframework.boot:spring-boot-starter-json:2.7.0
| +--- org.springframework.boot:spring-boot-starter:2.7.0 (*)
| +--- org.springframework:spring-web:5.3.20
| | +--- org.springframework:spring-beans:5.3.20 (*)
| | \--- org.springframework:spring-core:5.3.20 (*)
| +--- com.fasterxml.jackson.core:jackson-databind:2.13.3
| | +--- com.fasterxml.jackson.core:jackson-annotations:2.13.3
| | | \--- com.fasterxml.jackson:jackson-bom:2.13.3
| | | +--- com.fasterxml.jackson.core:jackson-annotations:2.13.3 (c)
| | | +--- com.fasterxml.jackson.core:jackson-core:2.13.3 (c)
| | | +--- com.fasterxml.jackson.core:jackson-databind:2.13.3 (c)
| | | +--- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.3 (c)
| | | +--- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.3 (c)
| | | \--- com.fasterxml.jackson.module:jackson-module-parameter-names:2.13.3 (c)
| | +--- com.fasterxml.jackson.core:jackson-core:2.13.3
| | | \--- com.fasterxml.jackson:jackson-bom:2.13.3 (*)
| | \--- com.fasterxml.jackson:jackson-bom:2.13.3 (*)
| +--- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.3
| | +--- com.fasterxml.jackson.core:jackson-core:2.13.3 (*)
| | +--- com.fasterxml.jackson.core:jackson-databind:2.13.3 (*)
| | \--- com.fasterxml.jackson:jackson-bom:2.13.3 (*)
| +--- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.3
| | +--- com.fasterxml.jackson.core:jackson-annotations:2.13.3 (*)
| | +--- com.fasterxml.jackson.core:jackson-core:2.13.3 (*)
| | +--- com.fasterxml.jackson.core:jackson-databind:2.13.3 (*)
| | \--- com.fasterxml.jackson:jackson-bom:2.13.3 (*)
| \--- com.fasterxml.jackson.module:jackson-module-parameter-names:2.13.3
| +--- com.fasterxml.jackson.core:jackson-core:2.13.3 (*)
| +--- com.fasterxml.jackson.core:jackson-databind:2.13.3 (*)
| \--- com.fasterxml.jackson:jackson-bom:2.13.3 (*)
+--- org.springframework.boot:spring-boot-starter-tomcat:2.7.0
| +--- jakarta.annotation:jakarta.annotation-api:1.3.5
| +--- org.apache.tomcat.embed:tomcat-embed-core:9.0.63
| +--- org.apache.tomcat.embed:tomcat-embed-el:9.0.63
| \--- org.apache.tomcat.embed:tomcat-embed-websocket:9.0.63
| \--- org.apache.tomcat.embed:tomcat-embed-core:9.0.63
+--- org.springframework:spring-web:5.3.20 (*)
\--- org.springframework:spring-webmvc:5.3.20
+--- org.springframework:spring-aop:5.3.20 (*)
+--- org.springframework:spring-beans:5.3.20 (*)
+--- org.springframework:spring-context:5.3.20 (*)
+--- org.springframework:spring-core:5.3.20 (*)
+--- org.springframework:spring-expression:5.3.20 (*)
\--- org.springframework:spring-web:5.3.20 (*)
(c) - dependency constraint
(*) - dependencies omitted (listed previously)
A web-based, searchable dependency report is available by adding the --scan option.
BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed
- 위 명령어 입력 시 현재 gradle 의존관계를 확인할 수 있다
- spring-boot-stater 하나를 받음으로써, 다른 의존 관계를 전부 받아올 수 있다
- AssertJ Document
- 핵심 라이브러리
- Spring MVC
- Spring ORM
- JPA, Hibernate
- Spring Data JPA
- 기타 라이브러리
- H2 DB Client
- 커넥션 풀 : 부트 기본은 HikariCP
- 로깅 SLF4J & LogBack
- 테스트(Test)
04. 스프링 데이터 JPA와 DB 설정, 동작 확인
spring:
datasource:
url: jdbc:h2:tcp://localhost/~/datajpa
username: sa
password:
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
# show_sql: true
format_sql: true
logging.level:
org.hibernate.SQL: debug
# org.hibernate.type: trace
- 기본적으로 DB와의 Connection을 위해 application.yml 파일을 작성
- 상세한 내용은 생략
04-1. 회원 엔티티 생성
package study.datajpa.entity;
import lombok.*;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
//@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
//@Setter
public class Member {
@Id
@GeneratedValue
private Long id;
private String userName;
protected Member() {
}
public Member(String userName) {
this.userName = userName;
}
}
- 위와 같이 Member 엔티티를 생성
- 맴버 변수로는 시퀸스(id), 회원명(userName)을 갖는다
- 웬만하면 Entity에 Setter 사용은 지양 해야 한다
- JPA 스펙에 맞춰 protected 생성자 생성
- 회원명을 초기화 할 수 있는 생성자 생성
- 참조변수.setUserName(‘username’)과 같은 데이터 변경을 막기 위함
04-2. 회원 레포지토리 생성
다음은 간단한 레포지토리를 생성 해보자
레포지토리 생성 방식은 클래스, 인터페이스 방식으로 진행이 될 예정이다
package study.datajpa.repository;
import org.springframework.stereotype.Repository;
import study.datajpa.entity.Member;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
@Repository
public class MemberJpaRepository {
@PersistenceContext
private EntityManager em; // Injection EntityManager from spring IOC container
public Member save(Member member) {
em.persist(member);
return member;
}
public Member find(Long id) {
return em.find(Member.class, id);
}
}
- @PersistenceContext 어노테이션을 통해 EntityManager 객체를 주입 받는다(from IOC Container)
- 가장 기본이 회원 저장, 아이디 기반 조회 메서드 생성
04-3. 회원 테스트 코드 작성(MemberJpaRepository)
package study.datajpa.repository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
import study.datajpa.entity.Member;
import static org.assertj.core.api.Assertions.*;
@SpringBootTest
@Transactional
class MemberJpaRepositoryTest {
@Autowired MemberJpaRepository memberJpaRepository;
@Test
// @Rollback(false)
public void testMember() throws Exception {
//given
Member member = new Member("memberA");
//when
Member savedMember = memberJpaRepository.save(member);
Member findMember = memberJpaRepository.find(savedMember.getId());
//then
assertThat(findMember.getId()).isEqualTo(member.getId());
assertThat(findMember.getUserName()).isEqualTo(member.getUserName());
assertThat(findMember).isEqualTo(member); // 동일한 하나의 Transaction 단위에서 JPA는 동일한 객체 반환을 보장 한다, 1차 캐시
}
}
- Test의 경우 Junit5를 기반으로 진행 한다
- MemberJpaRepository를 테스트 하기 위한 간단한 테스트 코드 작성
- CASE 01: 저장된 회원의 id가 기존 회원의 id와 같은가? (True)
- CASE 02: 저장된 회원의 이름이 기존 회원의 이름과 같은가? (True)
- CASE 03: 저장된 회원 객체가 기존 회원 객체와 같은가? (True)
- JPA는 기본적으로 동일한 하나의 트랜잭션 단위 내에서 동일한 객체 반환을 보장한다
- 1차캐시, 영속성 컨텍스트
- @Transactional 어노테이션이 없는 경우 예외가 발생 한다
- InvalidDataAccessApiUsageException
- 이러한 이유로 인해, @Transactional 어노테이션을 추가 해주어야 한다
04-4. 회원 레포지토리 생성
이번에는 Spring Data JPA를 활용하기 위한 인터페이스(Interface) 레포지토리를 생성 해보자.
package study.datajpa.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import study.datajpa.entity.Member;
public interface MemberRepository extends JpaRepository<Member, Long> {
}
위에서 만든 MemberJpaRepository 클래스와는 다르게 MemberRepository는 JpaRepository<T, E> 라는 인터페이스 를 상속 받은 상태다. MemberRepository에 save, find와 같은 메서드를 생성 해야 할 것 같지만, 이미 JpaRepository 를 상속 받으면서 모든 메서드를 제공 받을 수 있는 상태다.
JpaRepository 인터페이스
/*
* Copyright 2008-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository;
import java.util.List;
import javax.persistence.EntityManager;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.QueryByExampleExecutor;
/**
* JPA specific extension of {@link org.springframework.data.repository.Repository}.
*
* @author Oliver Gierke
* @author Christoph Strobl
* @author Mark Paluch
* @author Sander Krabbenborg
* @author Jesse Wouters
* @author Greg Turnquist
*/
@NoRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
/*
* (non-Javadoc)
* @see org.springframework.data.repository.CrudRepository#findAll()
*/
@Override
List<T> findAll();
/*
* (non-Javadoc)
* @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Sort)
*/
@Override
List<T> findAll(Sort sort);
/*
* (non-Javadoc)
* @see org.springframework.data.repository.CrudRepository#findAll(java.lang.Iterable)
*/
@Override
List<T> findAllById(Iterable<ID> ids);
/*
* (non-Javadoc)
* @see org.springframework.data.repository.CrudRepository#save(java.lang.Iterable)
*/
@Override
<S extends T> List<S> saveAll(Iterable<S> entities);
/**
* Flushes all pending changes to the database.
*/
void flush();
/**
* Saves an entity and flushes changes instantly.
*
* @param entity entity to be saved. Must not be {@literal null}.
* @return the saved entity
*/
<S extends T> S saveAndFlush(S entity);
/**
* Saves all entities and flushes changes instantly.
*
* @param entities entities to be saved. Must not be {@literal null}.
* @return the saved entities
* @since 2.5
*/
<S extends T> List<S> saveAllAndFlush(Iterable<S> entities);
/**
* Deletes the given entities in a batch which means it will create a single query. This kind of operation leaves JPAs
* first level cache and the database out of sync. Consider flushing the {@link EntityManager} before calling this
* method.
*
* @param entities entities to be deleted. Must not be {@literal null}.
* @deprecated Use {@link #deleteAllInBatch(Iterable)} instead.
*/
@Deprecated
default void deleteInBatch(Iterable<T> entities) {
deleteAllInBatch(entities);
}
/**
* Deletes the given entities in a batch which means it will create a single query. This kind of operation leaves JPAs
* first level cache and the database out of sync. Consider flushing the {@link EntityManager} before calling this
* method.
*
* @param entities entities to be deleted. Must not be {@literal null}.
* @since 2.5
*/
void deleteAllInBatch(Iterable<T> entities);
/**
* Deletes the entities identified by the given ids using a single query. This kind of operation leaves JPAs first
* level cache and the database out of sync. Consider flushing the {@link EntityManager} before calling this method.
*
* @param ids the ids of the entities to be deleted. Must not be {@literal null}.
* @since 2.5
*/
void deleteAllByIdInBatch(Iterable<ID> ids);
/**
* Deletes all entities in a batch call.
*/
void deleteAllInBatch();
/**
* Returns a reference to the entity with the given identifier. Depending on how the JPA persistence provider is
* implemented this is very likely to always return an instance and throw an
* {@link javax.persistence.EntityNotFoundException} on first access. Some of them will reject invalid identifiers
* immediately.
*
* @param id must not be {@literal null}.
* @return a reference to the entity with the given identifier.
* @see EntityManager#getReference(Class, Object) for details on when an exception is thrown.
* @deprecated use {@link JpaRepository#getReferenceById(ID)} instead.
*/
@Deprecated
T getOne(ID id);
/**
* Returns a reference to the entity with the given identifier. Depending on how the JPA persistence provider is
* implemented this is very likely to always return an instance and throw an
* {@link javax.persistence.EntityNotFoundException} on first access. Some of them will reject invalid identifiers
* immediately.
*
* @param id must not be {@literal null}.
* @return a reference to the entity with the given identifier.
* @see EntityManager#getReference(Class, Object) for details on when an exception is thrown.
* @deprecated use {@link JpaRepository#getReferenceById(ID)} instead.
* @since 2.5
*/
@Deprecated
T getById(ID id);
/**
* Returns a reference to the entity with the given identifier. Depending on how the JPA persistence provider is
* implemented this is very likely to always return an instance and throw an
* {@link javax.persistence.EntityNotFoundException} on first access. Some of them will reject invalid identifiers
* immediately.
*
* @param id must not be {@literal null}.
* @return a reference to the entity with the given identifier.
* @see EntityManager#getReference(Class, Object) for details on when an exception is thrown.
* @since 2.7
*/
T getReferenceById(ID id);
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example)
*/
@Override
<S extends T> List<S> findAll(Example<S> example);
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example, org.springframework.data.domain.Sort)
*/
@Override
<S extends T> List<S> findAll(Example<S> example, Sort sort);
}
- 위와 같이 인터페이스 간의 상속을 통해 많은 메서드들이 제공이 되는 상태
- 대표적으로 findAll, saveAll, save, findBy 등의 메서드가 존재 한다
04-5. 회원 테스트코드 작성
위에서 작성한 테스트코드와 동일한 내용이지만, Spring Data JPA의 동작을 확인하기 위해 생성 하였다
package study.datajpa.repository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
import study.datajpa.entity.Member;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@Transactional
class MemberRepositoryTest {
@Autowired MemberRepository memberRepository; // AS-IS memberJpaRepository
@Test
@Rollback(false)
public void test() throws Exception {
//given
Member member = new Member("memberA");
//when
Member savedMember = memberRepository.save(member);
Member findMember = memberRepository.findById(savedMember.getId()).get();
//then
assertThat(findMember.getId()).isEqualTo(member.getId());
assertThat(findMember.getUserName()).isEqualTo(member.getUserName());
assertThat(findMember).isEqualTo(member);
}
}
여기서 짚고 넘어가야 하는 부분은 MemberRepository 인터페이스 안에 개발자가 어떠한 메서드도 생성하지 않았는데, 테스트 코드는 잘 동작 한다는 점이다. 즉, JpaRepository<T, E>를 상속 받음으로써 기존에 개발자가 작성해야 하는 많은 로직을 자동화 시킬 수 있다는 장점이 존재 한다.
댓글남기기