JPA 시작
스프링과 JPA는 자바 시장의 주력 기술이다.
그 조합은 구글 트랜드에서 글로벌에서는 80%, 국내에서도 50% 이상 사용된다고 측정되는
현대 자바 개발자 시장의 큰 축이다.
다만 JPA는 그 자체는 매우 유용한 기술이지만 러닝 커브가 심하다.
하지만 한 번 배워두면 생산성이 크게 올라가는 것은 사실이기에
계속 자바 개발자로 먹고 살거면 배워두는 것이 좋다.
ORM 개념1- SQL 중심적인 개발의 문제점
개발자가 개발을 하다 보면 반복되는 쿼리를 직접 사용해야 하는 경우가 있다.
물론 MyBatis의 sql 태그처럼 코드 조각을 재사용하는 방법이 있긴 하지만,
그것은 동일한 “쿼리”를 작성하는 것을 줄여주는 것이지
동일한 “행동”을 줄여주는 것은 아니다.
또한 기본적으로 자바는 객체지향언어이지만 DB는 그렇지 않기 때문에
개발자가 일일이 맞지 않는 부분을 맞춰줘야 하는 작업이 반복된다.
특히 객체와 관계형 DB의 경우에는
상속, 연관관계, 데이터 타입, 데이터 식별 방법이라는 큰 4개의 차이점이 존재한다.
그래서 JPA는 객체와 관계형 DB에서 직접 매핑하는 방식으로
객체와 관계형 DB의 차이점을 해결해준다.
ORM 개념2 - JPA 소개
우선 JPA는 자바 진영의 ORM 기술 표준이다.
ORM이란 객체 관계 매핑이라는 뜻으로
ORM 프레임워크는 객체와 관계형 DB 중간에서 매핑해주는 역할을 하는 데 이를 의미한다.
대중적인 언어에는 대부분 ORM 기술이 존재한다.
JPA는 자바 애플리케이션과 JDBC API 사이에서 동작한다.
JVM 내부에서 자바 애플리케이션이 JDBC를 호출하면 중간에서 JPA가 동작하여
쿼리를 생성하거나 패러다임 불일치를 해결하는 등 다양한 역할을 해준다.
만약에 운영 중인 서비스에서 사용 중인 테이블에 컬럼이 3개나 추가된다면 어떨까?
기존에는 해당 테이블을 사용하는 쿼리들을 일일이 찾아서 해당 컬럼들에 대한 처리를 추가해줘야 했다.
하지만 JPA에서는 DB와 매핑되는 역할인 엔티티
클래스에 필드만 추가하면 쿼리가 자동으로 변경된다.
그리고 JPA는 다양한 성능 최적화 기능을 제공하기 때문에
비교적 성능 튜닝을 하기 쉽다는 장점이 있다.
JPA 설정
build.gradle
에 라이브러리를 추가하자.
//JPA, 스프링 데이터 JPA 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
해당 라이브러리를 사용한다면 JPA뿐만 아니라 스프링 데이터 JPA라는 기술도 함께 사용할 수 있다.
그리고 만약 build.gradle
에 org.springframework.boot:spring-boot-starter-jdbc
가 따로 추가되어 있다면 없애도 된다. 왜냐하면 org.springframework.boot:spring-boot-starter-data-jpa
내부에 JDBC가 이미 포함되어 있기 때문이다.
그리고 로그를 확인하고 싶다면 application.properties
에 아래와 같이 추가하자.
#JPA log
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
다만 스프링 부트 3.0 이상을 사용하면 하이버네이트 6 버전이 사용되는데, 이 때는 로그 설정 방식이 달라지기 때문에
logging.level.org.hibernate.type.descriptor.sql.BasicBinder
대신에
logging.level.org.hibernate.orm.jdbc.bind
를 사용하자.
JPA 적용 1 - 엔티티
JPA가 객체와 DB를 매핑시키려면 객체를 엔티티
로 만들어야 한다.
DB에 맞게 설계된 객체가 있다면 @Entity
애노테이션을 추가해서 엔티티로 만들면 된다.
그런 다음에 여러 애노테이션들을 활용해 DB의 각 컬럼과 객체의 각 필드들을 매핑시켜주면 된다.
만약에 아래와 같은 객체가 있다고 가정해보자.
@Data
public class Item {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
우선 클래스에 @Entity
애노테이션을 추가해서 엔티티로 만들어주자.
엔티티는 PK가 존재해야 하기에 이를 나타내는 @Id
애노테이션을 추가해주자.
그리고 PK가 생성되는 전략에 따라 @GeneratedValue
애노테이션을 추가해주자.
전략의 종류에 따라 strategy
속성에 명시해주면 된다.
PK 이외의 컬럼들은 @Column
애노테이션으로 매핑해주면 된다.
MyBatis의 경우에는 카멜 케이스로 자동 변환하는 옵션이 있었다.
다만 JPA는 객체와 테이블이 동일하게 설계되었다는 가정하에 진행되기 때문에
별도의 설정이 없다면 자동으로 컬럼명을 카멜 케이스로 변환해서 가져온다. 다만 스네이크 케이스와 카멜 케이스가 적용된 이름이 완전히 다르다면
@Column
애노테이션에 name
이라는 속성을 통해 매핑할 컬럼명을 명시해준다.
그러면 이름이 완전히 달라도 별도 별칭 없이 매핑해서 데이터를 가져올 수 있다.
그리고 JPA는 public 또는 protected 의 기본 생성자가 필수이기 때문에
기본 생성자를 꼭 넣어줘야 한다.
리포지토리
JPA 적용 2 - 리포지토리
EntityManager
JdbcTemplate에서는 JdbcTemplate
을 통해서 쿼리를 실행했었다.
하지만 JPA에서는 EntityManager
라는 것을 사용한다.
스프링을 통해서 EntityManager
를 주입받는다.
이 EntityManager
는 내부에 데이터 소스를 가지고 있으며 DB에 접근할 수 있다.
스프링 부트의 자동화
원래 스프링 프레임워크에서 JPA를 사용하려면
EntityManagerFactory
나 JpaTransactionManager
등 다양한 설정을 해야 한다.
하지만 스프링 부트에서는 이 과정을 모두 자동화해준다.
데이터 저장
데이터 저장은 매우 단순하다.
EntityManager
의 persist
메소드를 쓰면 된다.
아래는 그 예시다.
public Item save(Item item) {
em.persist(item);
return item;
}
데이터 수정
JPA의 데이터 수정은 매우 신기하다.
우선 아래 코드를 살펴보자.
public void update(Long itemId, ItemUpdateDto updateParam) {
Item findItem = em.find(Item.class, itemId);
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
}
해당 코드를 살펴보면 별도로 저장하는 메소드를 호출하는 것이 없다.
하지만 해당 메소드를 호출하면 실제로 데이터가 수정된다.
이는 JPA에서 사용되는 영속성 컨텍스트
라는 개념때문이다.
자세한 건 생략하면 우선 영속성 컨텍스트라는 것은 엔티티가 저장되는 저장소다.
JPA는 이 영속성 컨텍스트에 존재하는 엔티티를 감시하고 있는데,
여거서 특정 엔티티의 값이 변경된다면 변경 감지
라는 기능을 통해
트랜잭션이 종료되었을 때 오류가 없는한 커밋을 진행하며
동시에 UPDATE 쿼리가 발생해서 DB에는 수정된 데이터가 저장된다.
JPQL
사실 객체의 설계 방식이 달라진거지 순수하게 JPA를 사용하는 것은 JdbcTemplate과 크게 다를 것 없다.
가장 큰 차이점은 쿼리를 작성하는 방식이다.
기존에는 select * from item
이라고 작성했다고 가정해보자.
하지만 JPA는 객체를 활용하기 때문에 쿼리를 작성할 때도 객체를 활용한다.
쿼리를 작성할 때 select i from Item i
처럼 작성해야 한다.
다만 이것은 JPA에 맞게 사용한 쿼리다. 그래서 이런 JPA용 쿼리를 JPQL
이라고 부른다.
JPA가 JPQL을 실행하면 JPA가 해당 JPQL을 실제 연동된 DB의 문법에 맞는 실제 쿼리로 번역해준다.
조회
우선 아래 코드를 살펴보자.
public List<Item> findAll(ItemSearchCond cond) {
String jpql = "select i from Item i";
//동적 쿼리 생략
TypedQuery<Item> query = em.createQuery(jpql, Item.class);
return query.getResultList();
}
일단 JPA가 읽을 수 있는 JPQL을 만들었다.
이제 이 JPQL을 EntityManager
의 createQuery
메소드를 통해서 실행할 쿼리 정보를 TypedQuery
에 저장한다.
그런 다음에 TypedQuery
의 getResultList
메소드를 실행하면 실제 DB에서 데이터를 조회한다.
파라미터 바인딩
당연히 JPQL에도 파라미터 바인딩이 존재한다.
JPQL 작성 시 매핑할 파라미터명에 :
을 추가하면 된다. select i from Item i where i.itemName like concat('%',:itemName,'%') and i.price <= :maxPrice
처럼 작성하면 된다.
이 때 JPQL을 살펴보면 2가지를 알 수 있다.
우선 concat
이라는 키워드다. 일반적인 DBMS에서 사용하는 concat
함수를 사용할 수 있게 해주는 키워드다.
물론 해당 애플리케이션과 연동되어 있는 DBMS와 그 버전이 concat
함수를 지원한다는 가정하에 사용할 수 있다.
하지만 지원하지 않아도 사용하는 방법이 있긴 한데 지금은 넘어가자.
추가로 MyBatis와 다르게 비교 연산자를 바로 사용할 수 있다.
MyBatis에서는 XML을 사용하기 때문에 <=
나 <![CDATA[ <= ]]>
처럼 비교 연산자를 다른 무언가로 치환했어야 했다.
하지만 JPA는 순수 자바 코드로 이루어져 있기 때문에 그런 번거로운 과정이 없다.
JPA 적용 3 - 예외 변환
JPA에서는 예외가 발생하면 JPA 예외가 발생하게 된다.
일단 중요한 건 EntityManager
는 스프링과 관련 없는 순수한 JPA 기술이라는 것이다.
그래서 예외가 발생하면 PersistenceException
과 그 하위 예외를 발생시킨다.
다만 스프링은 JPA를 사용할 때 @Repository
애노테이션이 붙은 클래스를 컴포넌트 스캔의 대상으로 인식한다.
그래서 @Repository
애노테이션이 붙은 클래스는 예외 변환 AOP의 적용 대상이 된다.
그러면 스프링은 JPA 예외 변환기인 PersistenceExceptionTranslator
를 통해서
JPA 관련 예외가 발생하면 스프링 데이터 접근 예외로 변환해준다.