728x90
1. JPA 설정
- spring-boot-starter-data-jpa
- 라이브러리
- JPA와 spring data JPA를 springboot와 통합하고, 설정도 간단히 할 수 있다.
- spring-boot-starter-jdbc를 포함(의존)한다. mybatis-spring-boot-starter도 spring-boot-starter-jdbc를 포함
- 추가되는 library
- hibernate-core : JPA 구현체인 하이버네이트 라이브러리
- jakarta.persistence-api : JPA 인터페이스
- spring-data-jpa : 스프링 데이터 JPA 라이브러리
- build.gradle
plugins {
id 'org.springframework.boot' version '2.6.5'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
// hibernate.version 버그로 인한 version downgrade 방법
ext["hibernate.version"] = "5.6.5.Final"
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
//테스트에서 lombok 사용
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
// JdbcTemplate 추가
// mybatis나 jpa 사용시, 해당 패키지 내부에 jdbcTemplate이 들어가있어서 주석처리
// implementation 'org.springframework.boot:spring-boot-starter-jdbc'
// MyBatis 추가 - 김영한 강사님 file을 그대로 사용한 것이여서 2.2.0으로 작성해야한다.
// boot 3.0 이상 사용시 version을 3.0.1로 변경 해야한다.
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
// JPA 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// H2 데이터베이스 추가
runtimeOnly 'com.h2database:h2'
//Querydsl 추가
implementation 'com.querydsl:querydsl-jpa'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
tasks.named('test') {
useJUnitPlatform()
}
// Querydsl 추가, 자동 생성된 Q클래스를 gradle clean으로 제거
clean {
delete file('src/main/generated')
}
- java/resources/application.properties
spring.profiles.active=local
#JPA log
logging.level.org.hibernate.SQL=DEBUG # 하이버네이트가 생성하고 실행하는 SQL 확인
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE # SQL에 바인딩 되는 params 확인
# springboot 3.0 이상 시, 변경된 로그
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.orm.jdbc.bind=TRACE
- test/resources/application.properties
spring.profiles.active=test
#JPA log
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
2. JPA 적용1 - 개발
- JPA 중요 2가지
- 객체와 테이블 매핑 (이번 기록 내용)
- 영속성 컨텍스트
2.1. Item - ORM 매핑
- @Entity : JPA가 사용하는 객체, 어노테이션 존재시, JPA가 인식, 해당 어노테이션인 붙은 객체를 엔티티라고 한다
- @Table : class명과 table명 동일할 경우 생략가능
- name은 대소문자 구분 안함
- @Id : 테이블의 PK와 해당 필드를 매핑
- @GeneratedValue(strategy = GenerationType.IDENTITY) : PK 생성 값을 데이터베이스에서 생성하는 IDENTITY 방식을 사용
ex) autoIncrement 같은 것 - @Column : 객체 필드를 테이블의 컬럼과 매핑
- name = "item_name" : 객체는 itemName이지만 테이블 컬럼은 item_name이므로 이렇게 매핑
- length = 10 : JPA 매핑 정보로 DDL(create table)도 생성할 수 있는데, 그때 컬럼의 길이값을 활용
- 생략시 필드의 이름을 테이블 컬럼 이름으로 사용 - springboot와 통합 사용시, 카멜 케이스와 언더스코어 자동 변환 해줌
- 기본 생성자가 필수 : public, protected 접근 제어자 사용, proxy 기술에 사용
package hello.itemservice.domain;
@Data
@Entity // 해당 어노테이션이 존재해야지 jpa 객체로 인정됨 == table이랑 맵핑되서 관리가 되는 객체
//@Table(name = "item") // 대소문자 구분없이 table명과 class 명이 동일하면 생략해도 된다.
public class Item {
@Id // PK를 지정
@GeneratedValue(strategy = GenerationType.IDENTITY) // autoIncrement
private Long id;
// table의 컬럼명과 객체 필드명이 동일할 시, @Column이 없어도 된다. 또한 카멜 케이스와 언더스코어도 자동으로 인식함
// 아래 컬럼들에 모두 @Column을 넣지 않아도 되지만 이해를 위해 item_name 필드에만 @Column 넣어둠
@Column(name = "item_name", length = 10) // length: jpa로 DDL(create table) 생성시, column 길이 값으로 사용 됨
private String itemName;
private Integer price;
private Integer quantity;
// JPA는 public || protected 기본 생성자가 필수. 스펙이다. 해당 생성자가 있어야 나중에 proxy 기술 사용 시 유용하게 쓰임.
public Item() {}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
2.2. JpaItemRepositoryV1
- private final EntityManger em
- 생성자를 보면 스프링을 통해 엔티티 매니저라는 것을 주입 받는다.
- JPA의 모든 동작은 엔티티 매니저를 통해서 이뤄진다.
- 엔티티 매니저는 내부에 데이터소스를 가지고 있고, 데이터베이스에 접근 가능하다.
- 참고로 jpa를 설정하려면 EntityManagerFactory, JPA 트랜잭션 매니저, 데이터소스 등등 다양한 설정을 해야하는데 이를 springboot가 자동화해준다.
- JpaBaseConfiguration를 참고하면 어떻게 설정하는 알 수 있다.
- @Transactional
- JPA의 모든 데이터 CRUD는 trransaction 내부에서 이뤄져야한다. 다만 select는 transaction이 없어도 된다.
- 나머지 crud도 어지간하면 service 계층에서 @Transactional을 걸기때문에 Repository 쪽에서 크게 신경 쓸 경우가 드물다.
- 해당 code에서 repository 계층에 @Transactional을 건 이유는 복잡한 비지니스 로직이 없어서 서비스 계층에서 트랜잭션을 걸지 않았기 때문에 @Repository에 transaction을 걸었다.
- 실제 실무에서도 필요한 경우 Repository 계층에 @Transactional을 거는 경우도 존재한다.
하지만 일반적으로 비지니스 로직을 사용하는 서비스 계층에 transaction을 걸어주는 것이 맞다.
- JPA의 모든 데이터 CRUD는 trransaction 내부에서 이뤄져야한다. 다만 select는 transaction이 없어도 된다.
package hello.itemservice.repository.jpa;
@Slf4j
@Repository // spring 예외 변환을 위한 AOP Proxy 적용대상
// jpa의 모든 데이터 변경은 transactional 안에서 이루어지기 때문에 @Transactional 적용 - 조회는 필요 없다.
// 현재 Service에 @Transactional이 없어서 repository에 넣음. 원래는 Service 계층에 @Transactional을 넣는게 맞다.
@Transactional
public class JpaItemRepository implements ItemRepository {
private final EntityManager em; // 내부에 dataSource, 등등 수행해야하는 과정을 boot가 다 수행해줘서 해당 class를 사용하면 된다.
// 생성자 주입
public JpaItemRepository(EntityManager em) {
this.em = em;
}
@Override
// @Transactional
public Item save(Item item) {
em.persist(item); // jpa entity인 Item의 정보를 이용해서 db에 저장 * persist: 영구 저장
return item;
}
@Override
// @Transactional
public void update(Long itemId, ItemUpdateDto updateParam) {
Item findItem = em.find(Item.class, itemId); // 1. 항상 select 수행
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
// update 저장을 알아서 해줌 - transaction이 commit 되는 시점에서 update query 만들어서 db에 날림
/* 추가 공부 내용
* select 후, setter 사용으로 값 update 시, 값이 변경되지 않은 부분은 dirty checking을 통해서 update query에서 제거
* null이 들어간 경우는 기존 값과 다른 것이므로 null로 update
* dirty checking : 변경된 엔티티의 상태를 감지하고 데이터베이스와의 동기화를 관리하는 방법 중 하나
* */
}
@Override
public Optional<Item> findById(Long id) {
Item item = em.find(Item.class, id); // find(entity.class, pk)
return Optional.ofNullable(item);
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
/* jpql
* sql과 95% 유사한, 객체 쿼리 언어 - 쿼리가 복잡할 경우 사용
* Item : 대소문자 주의, Item entity 객체 의미
* Item i : i는 alias(별칭)
* select i : i는 Item entity 자체를 의미 == class type 의미
* */
String jpql = "select i from Item i";
// 동적쿼리 없는 경우 사용 code
// List<Item> result = em.createQuery(jpql, Item.class).getResultList();
// 동적쿼리 넣은 경우 - 아주 불편한 단점 가짐
Integer maxPrice = cond.getMaxPrice();
String itemName = cond.getItemName();
if (StringUtils.hasText(itemName) || maxPrice != null) {
jpql += " where";
}
boolean andFlag = false;
if (StringUtils.hasText(itemName)) {
jpql += " i.itemName like concat('%',:itemName,'%')";
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) {
jpql += " and";
}
jpql += " i.price <= :maxPrice";
}
log.info("jpql={}", jpql);
TypedQuery<Item> query = em.createQuery(jpql, Item.class);
if (StringUtils.hasText(itemName)) {
query.setParameter("itemName", itemName);
}
if (maxPrice != null) {
query.setParameter("maxPrice", maxPrice);
}
return query.getResultList();
}
}
3. JPA 적용2 - 리포지토리 분석
- em.persist(item) : JPA에서 객체를 테이블에 저장할 때는 엔티티 매니저가 제공하는 persist() 메서드 사용
public Item save(Item item) {
em.persist(item); // jpa entity인 Item의 정보를 이용해서 db에 저장 * persist: 영구 저장
return item;
}
- em.update() 같은 메서드 전혀 호출하지 않는다.
- tx가 commit되는 시점에, 변경된 엔티티 객체가 있는지 확인 후, 특정 엔티티 객체가 변경된 경우에는 Update sql 실행
- test의 경우 마지막에 tx가 롤백 되기 때문에 update sql이 실행되지 않으므로 확인하고 싶으면 @Commit 을 붙여 확인
- 항상 em.find()로 찾은 후, update code 수행
public void update(Long itemId, ItemUpdateDto updateParam) {
Item findItem = em.find(Item.class, itemId); // 1. 항상 select 수행 - update 수행 여부의 비교의 대상이 된다
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
// update 저장을 알아서 해줌 - transaction이 commit 되는 시점에서 update query 만들어서 db에 날림
/* 추가 공부 내용
* select 후, setter 사용으로 값 update 시, 값이 변경되지 않은 부분은 dirty checking을 통해서 update query에서 제거
* null이 들어간 경우는 기존 값과 다른 것이므로 null로 update
* dirty checking : 변경된 엔티티의 상태를 감지하고 데이터베이스와의 동기화를 관리하는 방법 중 하나
* */
- em.find(Item.class, id) : 조회타입과 pk 값을 준다. 그러면 JPA가 조회 후, 결과를 객체로 변환 후 반환해준다.
public Optional<Item> findById(Long id) {
Item item = em.find(Item.class, id); // find(entity.class, pk)
return Optional.ofNullable(item);
}
- JPQL(java persistence query Language) : 객체지향 쿼리 언어
- 여러 데이터를 복잡한 조건으로 조회할 때 사용
- 엔티티 객체와 속성의 대소문자는 구분해야한다.
- 이렇게 하는거 개불편한 방법이다 - jpql은 동적쿼리에 약함
- JPQL 규칙
- params는 :maxPrice 같이 작성
- params binding은 query.setParameter("maxPrice", maxPrice) 이렇게 작성
public List<Item> findAll(ItemSearchCond cond) {
/* jpql
* sql과 95% 유사한, 객체 쿼리 언어 - 쿼리가 복잡할 경우 사용
* Item : 대소문자 주의, Item entity 객체 의미
* Item i : i는 alias(별칭)
* select i : i는 Item entity 자체를 의미 == 아까 alias로 만든 그 i 인것이다.
* Item이라는 entity를 대상으로 entity 객체인 i를 select하겠다는 의미
* */
String jpql = "select i from Item i";
// 동적쿼리 없는 경우 사용 code
// List<Item> result = em.createQuery(jpql, Item.class).getResultList();
// 동적쿼리 넣은 경우 - 아주 불편한 단점 가짐
Integer maxPrice = cond.getMaxPrice();
String itemName = cond.getItemName();
if (StringUtils.hasText(itemName) || maxPrice != null) {
jpql += " where";
}
boolean andFlag = false;
if (StringUtils.hasText(itemName)) {
jpql += " i.itemName like concat('%',:itemName,'%')";
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) {
jpql += " and";
}
jpql += " i.price <= :maxPrice";
}
log.info("jpql={}", jpql);
TypedQuery<Item> query = em.createQuery(jpql, Item.class);
if (StringUtils.hasText(itemName)) {
query.setParameter("itemName", itemName);
}
if (maxPrice != null) {
query.setParameter("maxPrice", maxPrice);
}
return query.getResultList();
}
4. JPA 적용3 - 예외변환
@Slf4j
@Repository
@Transactional
public class JpaItemRepository implements ItemRepository {
private final EntityManager em;
public JpaItemRepository(EntityManager em) {
this.em = em;
}
...
}
- EntityManger는 순수한 JPA 기술이고, 스프링과 관계가 없다. 따라서 엔티티 매너저는 예외가 발생하면 JPA 관련 예외를 발생 시킴
- JPA는 persistenceException과 하위 예외를 발생시킨다. + IllegalStateException, IllegalArgumentException 발생시킴
- JPA 예외를 스프링 예외로 추상화(== DataAccessException)으로 변환하는 방법은 @Repository에 존재한다.
4.1. @Repository 기능
- component scan 대상
- 예외 변환 AOP 적용 대상
- 스프링과 JPA를 함께 사용하는 경우 스프링은 JPA 예외 변환기를 등록한다.
- 예외 변환 AOP 프록시는 JPA 관련 예외가 발생시 JPA 예외 변환기를 통해 발생한 예외를 스프링 데이터 접근 예외로 변환
- 결과적으로 @Repository 만 존재하면 spring이 예외 변환을 처리하는 AOP를 만들어준다