2. JPA
1. JPA란?
자바를 사용할 때, SQL의 문제점
- 자바 객체를 SQL로 DB를 관리하고, DB를 다시 SQL을 통해 자바 객체로 반환하는 과정이 귀찮음.
- 객체와 RDBMS 간의 패러다임의 불일치
RDBMS와 객체지향 간의 중간에서 2개를 매핑하는 역할을
ORM
이라고 한다.JPA
는 이런 ORM의 표준 명세로 자바에서 제공하는API
이다.JPA
는 인터페이스이고, 이를 구현한 구현체가 대표적으로Hibernate
등이 있다.
[ JPA 장점 ]
1. 특정 DB에 종속되지 않음
- 설정 파일에서 사용할 DB 변경 가능
2. 객체지향 프로그래밍
3. 생산성 향상
- DB에 새로운 칼럼 추가 시, DTO 클래스의 필드도 모두 변경해야 함.
- JPA는 매핑 클래스에 필드만 추가하면 끝!
( DAO : DB의 data에 접근하기 위한 객체 )
( DTO : 계층 간 데이터 교환을 하기 위한 객체 )
[ JPA 단점 ]
1. 복잡한 쿼리 처리
- 복잡하면 SQL 사용 권장
- 이를 보완한 JPQL이 존재
2. 성능 저하 위험
3. 학습 시간
2. JPA 동작 방식
Entity
- 데이터베이스의 테이블에 대응하는 클래스
- DB에 item 테이블 만들고, Item.java에 @Entity 붙이면 해당 클래스가 엔티티가 됨.
Entity Manager Factory
- Entity Manager 인스턴스를 관리하는 주체
- 실행시, 한 개만 만들어짐.
Entity Manager
- 영속성 컨텍스트에 접근하여 Entity에 대한 DB 작업을 제공
- DB에 접근하기 위한 메소드를 제공
메소드 | 내용 |
---|---|
find() |
영속성 컨텍스트에 Entity 검색 후, 없으면 DB에서 해당 데이터 찾아 영속성 컨텍스트에 저장 |
persist() |
Entity를 영속성 컨텍스트에 저장 |
remove() |
Entity 클래스를 영속성 컨텍스트에서 삭제 |
flush() |
영속성 컨텍스트에 저장된 내용을 DB에 반영 |
Persistence Context
[ 생명주기 용어 설명 ]
생명주기 | 내용 |
---|---|
new |
PC와 관련 없는 상태 |
managed |
Entity가 PC에 저장되어 관리되는 상태 / 커밋 시점에 DB 반영 |
detached |
PC에 저장되었다가 분리된 상태 |
removed |
PC와 DB에서 삭제된 상태 |
예시
Item item = new Item(); // PC와 관련없음
item.setItemNm("테스트 상품");
EntityManager em = entityManagerFactory.createEntityManager();
// EntityManager 생성
EntityTransaction transaction = em.getTransaction();
// 데이터 변경 시, 무결성 위해 트랜잭션 필수!
em.persist(item);
// PC에 저장된 상태. 아직 SQL X
transaction.commit(); // 트랜잭션을 DB에 반영
em.close(); // 자원 반환
emf.close();
영속성 컨텍스트 사용 시 이점
- 1차 캐시
- 위 사진 처럼, find() 시 1차 캐시를 조회함
- 동일성 보장
- 1차 캐시를 조회하기 때문에 하나의 트랜잭션에서 같은 키값으로 조회 가능
- 트랜잭션을 지원하는 쓰기 지연
- persist() 시, 쓰기 지연 저장소에도 SQL을 쌓아 커밋시점에 SQL문들을 flush 하면서 DB에 반영됨.
- 이 때문에 성능면에서 좋음.
- 변경 감지
- 1차 캐시에 처음 DB에서 불러온 Entity의
스냅샷
을 가짐. - 1차 캐시 저장된 Entity와 스냅샷 사이에 변경이 있으면
UPDATE
를쓰기 지연 저장소
에 보관함. - 그리고 커밋 시점에 해당 변경을 자동으로 반영
다음 챕터에선 본격적으로 프로젝트를 만들어보자
2. 프로젝트 생성하기
application.properties 설정
server.port = 80
#MySQL
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/shop?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=1254mlm
#실행되는 쿼리 콘솔 출력
spring.jpa.properties.hibernate.shop_sql=true
#콘솔창에 출력되는 쿼리를 가독성이 좋게 포맷팅
spring.jpa.properties.hibernate.format_sql=true
#쿼리에 물음표로 출력되는 바인드 파라미터 출력
logging.level.org.hibernate.type.descriptor.sql=trace
spring.jpa.hibernate.ddl-auto=create // DB 초기화 전략
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect // SQL 방언 설정
SQL 방언
SQL은 표존 SQL과 DBMS 벤더에서 제공하는 SQL이 존재.
각 공급업체에서 만든 SQL을 방언(Dialect)이라고 함.
DB 초기화 전략 - DDL AUTO 옵션
- 애플리케이션 구동 시 JPA의 DB 초기화 전략을 설정 가능.
- 총 5가지의 옵션을 제공한다
옵션 | 내용 |
---|---|
none | 사용하지 않음 |
create | 기존 테이블 삭제 후 테이블 생성 |
create-drop | 기존 테이블 삭제 후 테이블 생성. 종료 시점에 테이블 삭제 |
update | 변경된 스키마 적요 |
validate | 엔티티와 테이블 정상 매핑 확인 |
- update는 삭제 시 문제 발생을 고려해, 칼럼 추가만 가능
- 초반에는 create, update 익숙해지고, 추후 validate 설정
- 스테이징, 운영환경에선 create, create-drop, update 사용 금지
- 스테이징, 운영환경에선 none이나 validate 사용.
스테이징 환경이란?
운영 환경
은 실제 서비스를 운영하는 환경.
운영환경과 거의 동일한 환경 구성하여, 배포하기 전 여러 가지 기능을 검증하는 환경
3. 상품 엔티티 설계하기
엔티티를 만들기 위해 테이블에 어떤 데이터가 저장될지 설계해야함
그전에 엔티티 매핑 관련 어노테이션을 알아보자
엔티티 매핑 관련 어노테이션
@Lob에 대해
@Column 속성
DDL이란?
테이블, 스키마, 인덱스, 뷰, 도메인을 정의, 변경, 제거할 때 사용하는 용어.
( CREATE, DROP 등이 이에 해당 )
https://cbw1030.tistory.com/71
@GeneratedValue
- Entity 클래스에는 기본키가 있어야 한다.
- 기본적으로 @Id 를 통해 기본키로 사용할 칼럼 설정.
- @GeneratedValue를 통해서도 기본키 생성 전략을 설정.
- 총 4가지 방법이 존재
생성 전략 | 내용 |
---|---|
GenerationType.AUTO (default) | JPA 구현체가 자동으로 생성 전략 결정 |
GenerationType.IDENTITY | 기본키 생성을 DB에 위임 (MySQL은 AUTO_INCREMENT로 기본키 생성) |
GenerationType.SEQUENCE | DB 시퀀스 오브젝트를 이용해 기본키 생성. @SqeuenceGenerator로 시퀀스 등록 필요 |
GenerationType.TABLE | 키 생성요 테이블 사용. @TableGenerator 필요 |
기본키와 DB 시퀀스 오브젝트의 의미
기본키 : DB에서 조회시 구분할 수 있는 유일한 기준
시퀀스 : 순차적으로 증가하는 값을 반환해주는 DB 객체
예시
-> com.shop.entity.item.java
@Entity
@Table(name="item")
@Getter
@Setter
@ToString
public class Item {
@Id
@Column(name="item_id")
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id; // 상품 코드
@Column(nullable = false, length = 50)
private String itemNm; // 상품명
@Column(name="price", nullable = false)
private int price; // 가격
@Column(nullable = false)
private int stockNumber; // 재고 수량
@Lob // BLOB, CLOB 타입 매핑
@Column(nullable = false)
private String itemDetail; // 상품 상세 설명
@Enumerated(EnumType.STRING) // enum 타입 매핑
private ItemSellStatus itemSellStatus; // 상품 판매 상태 (enum 타입 클래스)
private LocalDateTime regTime; // 등록 시간
private LocalDateTime updateTime; // 수정 시간
}
EnumType.ORDINAL : enum 순서 값을 DB에 저장 EnumType.STRING : enum 이름을 DB에 저장 |
---|
enum ItemSellStatus {
SELL, SOLDOUT
}
4. Repository 설계
- EntityManager를 이용해 엔티티 저장하지 않고, Spring Data JPA에선 EntityManager를 직접 설정하지 않아도 된다.
- 그대신 DAO(Data Access Object)의 역할을 하는 Repository 인터페이스를 설계한 후 사용하면 된다.
-> com.shop.repository.ItemRepository.java
public interface ItemRepository extends JpaRepository<Item, Long> { }
- JPARepository는 CRUD 및 페이징 처리를 위한 메소드를 제공.
메소드 | 기능 |
---|---|
엔티티 저장 및 수정 | |
void delete(T entity) | 엔티티 삭제 |
count() | 엔티티 총 개수 반환 |
Iterable | 모든 엔티티 조회 |
h2DBMS를 이용해서 테스트 코드를 작성해보자.
테스트 코드용 프로퍼티 파일을 만들어 준다.
-> application-test.properties
# Datasource 설정
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:test
spring.datasource.username=sa
spring.datasource.password=
# H2 DB 방언 설정
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
직접적인 테스트 코드를 만든다.
-> com.shop.repository.ItemRepositoryTest.java
@SpringBootTest // 실제 애플리케이션 구동 할 때처럼 모든 빈을 IoC 컨테이너에 등록
@TestPropertySource(locations="classpath:application-test.properties") // 기본 프로퍼티보다 우선적으로 설정하게 함
class ItemRepositoryTest {
@Autowired
ItemRepository itemRepository;
@Test
@DisplayName("상품 저장 테스트")
public void createItemTest(){
Item item = new Item();
item.setItemNm("테스트 상품");
item.setPrice(10000);
item.setItemDetail("테스트 상품 상세 설명");
item.setItemSellStatus(ItemSellStatus.SELL);
item.setStockNumber(100);
item.setRegTime(LocalDateTime.now());
item.setUpdateTime(LocalDateTime.now());
Item savedItem = itemRepository.save(item);
System.out.println(savedItem.toString());
}
실행 결과
- Spring Data JPA는 인터페이스만 작성하면 런타임 시점에 자바의 Dynamic Proxy를 이용해서 객체를 동적으로 생성해줌.
- 따라서 DAO와 xml에 쿼리를 작성하지 않아도 된다.
5. 쿼리 메소드
Repository 인터페이스에 간단한 네이밍 룰을 이용해 메서드를 작성해서 원하는 쿼리를 실행할 수 있다
[ 규칙 ]
find + (엔티티 이름) + By + 변수이름
- 엔티티 이름은 생략이 가능하다.
-> ItemRepository의 예시
public interface ItemRepository extends JpaRepository<Item, Long> {
List<Item> findByItemNm(String itemNm);
- 이제 해당 메서드를 테스트 코드에서 사용해보자!
-> ItemRepositoryTest.java
// 아이템 리스트 만드는 메서드
public void createItemList(){
for(int i=1;i<=10;i++){
Item item = new Item();
item.setItemNm("테스트 상품" + i);
item.setPrice(10000 + i);
item.setItemDetail("테스트 상품 상세 설명" + i);
item.setItemSellStatus(ItemSellStatus.SELL);
item.setStockNumber(100); item.setRegTime(LocalDateTime.now());
item.setUpdateTime(LocalDateTime.now());
Item savedItem = itemRepository.save(item);
}
}
@Test
@DisplayName("상품명 조회 테스트")
public void findByItemNmTest(){
this.createItemList(); // 1. 아이템들 생성
List<Item> itemList = itemRepository.findByItemNm("테스트 상품1"); // ItemNm이 테스트 상품1인 아이템을 받기
for(Item item : itemList){
System.out.println(item.toString()); // 해당 리스트 출력
}
}
실행 결과
Hibernate:
select
item0_.item_id as item_id1_0_,
item0_.item_detail as item_det2_0_,
item0_.item_nm as item_nm3_0_,
item0_.item_sell_status as item_sel4_0_,
item0_.price as price5_0_,
item0_.rec_time as reg_time6_0_,
item0_.stock_number as stock_nu7_0_,
item0_.update_time as update_t8_0_,
from
item item0_
where
item0_.item_nm=?
binding parameter [1] as [VARCHAR] - [테스트 상품1]
여러 조건 처리하기
- 여러 개의 조건을 이용한 검색 및 정렬도 가능하다.
- 이 때에는 JPQL을 이용하기도 한다. (2.6절에서..)
1. OR 조건
// 인터페이스
List<Item> findByItemNmOrItemDetail(String itemNm, String itemDetail);
// 테스트 코드
@Test
@DisplayName("상품명, 상품상세설명 or 테스트")
public void findByItemNmOrItemDetailTest(){
this.createItemList();
List<Item> itemList = itemRepository.findByItemNmOrItemDetail("테스트 상품1", "테스트 상품 상세 설명5");
for(Item item : itemList){
System.out.println(item.toString());
}
}
2. LessThan 조건
// 인터페이스
List<Item> findByPriceLessThan(Integer price);
// 테스트 코드
@Test
@DisplayName("가격 LessThan 테스트")
public void findByPriceLessThanTest(){
this.createItemList();
List<Item> itemList = itemRepository.findByPriceLessThan(10005);
for(Item item : itemList){
System.out.println(item.toString());
}
}
3. 내림차순 정렬
오름차순 정렬
: OrderBy + 속성명 + Asc` 를 이용하면 된다!
내림차순 정렬
: OrderBy + 속성명 + Desc` 를 이용하면 된다!
// 인터페이스
List<Item> findByPriceLessThanOrderByPriceDesc(Integer price); // 내림차순
// 테스트 코드
@Test
@DisplayName("가격 내림차순 조회 테스트")
public void findByPriceLessThanOrderByPriceDesc(){
this.createItemList();
List<Item> itemList = itemRepository.findByPriceLessThanOrderByPriceDesc(10005);
for(Item item : itemList){
System.out.println(item.toString());
}
}
6. @Query
- JPARepository의 메서드를 이용해서 조회 시, 조건이 많아지면 이름만으론 구분하기 힘들다.
- 그렇기 때문에 직접 쿼리를 작성할 수 있도록
@Query
를 지원한다. - @Query는
JPQL
을 이용해서 작성해야한다.
JPQL
- 특정 DB SQL에 의존하지 않음.
예시) ->ItemRepository
@Query("select i from Item i where i.itemDetail like " + // like 다음에 띄어쓰기 해야됨
"%:itemDetail% order by i.price desc")
List<Item> findByItemDetail(@Param("itemDetail") String itemDetail);
- @Param : 파라미터로 넘어온 값을 JPQL에 들어갈 변수로 지정.
- like % % 사이에 :itemDetail로 값이 들어감
-> ItemRepositoryTest
@Test
@DisplayName("@Query를 이용한 상품 조회 테스트")
public void findByItemDetailTest(){
this.createItemList();
List<Item> itemList = itemRepository.findByItemDetail("테스트 상품 상세 설명");
for(Item item : itemList){
System.out.println(item.toString());
}
}
- "테스트 상품 상세 설명"을 포함하는 상품 데이터 10개가 가격이 높은 순부터 조회가 된다.
기존 DB에서 사용하던 쿼리 그대로 사용하기
@Query
의 nativeQuery
속성을 사용.
-> ItemRepository
@Query("select i from Item i where i.itemDetail like " +
"%:itemDetail% order by i.price desc", nativeQuery = true)
List<Item> findByItemDetail(@Param("itemDetail") String itemDetail);
-> ItemRepositoryTest
@Test
@DisplayName("nativeQuery를 이용한 상품 조회 테스트")
public void findByItemDetailTest(){
this.createItemList();
List<Item> itemList = itemRepository.findByItemDetail("테스트 상품 상세 설명");
for(Item item : itemList){
System.out.println(item.toString());
}
}
실행 결과
7. Querydsl
@Query
는 JPQL 문법을 문자열로 입력하기 때문에, 컴파일 시점에 에러가 발견되지 않는다.- 이를 보완하기 위한
Querydsl
를 알아보자.
Querydsl
@Query("select i from Item wheere ...)
위 코드의 JPQL의 문법 중 where를 wheere로 잘못 입력되었다.
이때 Querydsl이 오타 발생시 바로 알려주고, 동적으로 쿼리를 생성해준다!
Querydsl 장점
- 고정된 SQL문이 아닌 조건에 맞게 동적으로 쿼리 생성
- 비슷한 쿼리 재사용 가능 및 가독성 향상
- 문자열이 아닌 자바 소스코드로 작성됨 (컴파일 시 오류 발견 ㅆㄱㄴ)
- IDE 도움을 받아 자동 완성까지 지원!
Querydsl 설정하기
JPAQueryFactory로 상품 조회하기
-> ItemRepositoryTest
@PersistenceContext
EntityManager em;
@Test
@DisplayName("Querydsl 조회 테스트1")
public void queryDslTest(){
this.createItemList();
JPAQueryFactory queryFactory = new JPAQueryFactory(em); // 쿼리를 동적으로 생성
QItem qItem = QItem.item; // 쿼리 생성위해 자동생성된 QItem 객체 사용
JPAQuery<Item> query = queryFactory.selectFrom(qItem)
.where(qItem.itemSellStatus.eq(ItemSellStatus.SELL))
.where(qItem.itemDetail.like("%" + "테스트 상품 상세 설명" + "%"))
.orderBy(qItem.price.desc());
List<Item> itemList = query.fetch(); // 쿼리 결과를 리스트로 반환, fetch(0 시점에 쿼리문 실행
for(Item item : itemList){
System.out.println(item.toString());
}
}
@PersistenceContext
: 영속컨텍스트 사용하기 위해 EntityManager 빈을 주입QItem
: QueryDSL전용 엔티티 (간단한 쿼리는 (Spring Data)JPA를 사용하고, 동적쿼리는 QueryDSL을 사용하여 개발하곤 합니다.)
JPAQuery 데이터 반화 메소드
메소드 | 내용 |
---|---|
List fetch() | 조회 결과 리스트 반환 |
T fetchOne | 조회 대상이 1건인 경우 제네릭으로 지정한 타입 반환 |
T fetchFirst() | 조회 대상 중 1건만 반환 |
Long fatchCount() | 조회 대상 개수 반환 |
QueryResult fetchResults() | 조회한 리스트와 전체 개수를 포함한 QueryResults 반환 |
실행결과
Hibernate:
select
item0_.item_id as item_id1_0_,
item0_.item_detail as item_det2_0_,
item0_.item_nm as item_nm3_0_,
item0_.item_sell_status as item_sel4_0_,
item0_.price as price5_0_,
item0_.rec_time as reg_time6_0_,
item0_.stock_number as stock_nu7_0_,
item0_.update_time as update_t8_0_,
from
item item0_
where
item0_.item_sell_status=?
and(
item-_.item_detail like ? escape '!'
)
order by
item0_.price desc
QuerydslPredicateExecutor를 이용한 상품 조회를 해보자
Predicate란?
'이 조건이 맞다'고 판단하는 근거를 함수로 제공하는 것
-> ItemRepository
public interface ItemRepository extends JpaRepository<Item, Long>,
QuerydslPredicateExecutor<Item> {
- Pepository에 Predicate를 파라미터로 전달하기 위해 코드 수정
QueryDslPredicateExecutor 인터페이스 제공 메소드
메소드 | 내용 |
---|---|
long count(Predicate) | 조건에 맞는 데이터 총 개수 반환 |
boolean exists(Predicate) | 조건에 맞는 데이터 존재 여부 반환 |
Iterator findAll(Predicate) | 조건에 맞는 모든 데이터 반환 |
Page findAll(Predicate, Pageable) | 조건에 맞는 페이지 데이터 반환 |
Iterator findAll(Predicate, Sort) | 조건에 맞는 정렬된 데이터 반환 |
T findOne(Predicate) | 조건에 맞는 데이터 1개 반환 |
-> ItemRepository
public void createItemList2(){
for(int i=1;i<=5;i++){
Item item = new Item();
item.setItemNm("테스트 상품"+i);
item.setPrice(10000 + i);
item.setItemDetail("테스트 상품 상세 설명"+i);
item.setItemSellStatus(ItemSellStatus.SELL);
item.setStockNumber(100);
item.setRegTime(LocalDateTime.now());
item.setUpdateTime(LocalDateTime.now());
itemRepository.save(item);
}
for(int i=6;i<=10;i++){
Item item = new Item();
item.setItemNm("테스트 상품"+i);
item.setPrice(10000 + i);
item.setItemDetail("테스트 상품 상세 설명"+i);
item.setItemSellStatus(ItemSellStatus.SOLD_OUT);
item.setStockNumber(0);
item.setRegTime(LocalDateTime.now());
item.setUpdateTime(LocalDateTime.now());
itemRepository.save(item);
}
}
@Test
@DisplayName("상품 Querydsl 조회 테스트2")
public void queryDslTest2(){
this.createItemList2();
BooleanBuilder booleanBuilder = new BooleanBuilder(); // 2: 쿼리에 들어갈 조건을 만들어주는 빌더
QItem item = QItem.item;
String itemDetail = "테스트 상품 상세 설명";
int price = 10003;
String itemSellStat = "SELL";
booleanBuilder.and(item.itemDetail.like("%" + itemDetail + "%")); // 3: 조회 시 조건
booleanBuilder.and(item.price.gt(price));
if(StringUtils.equals(itemSellStat, ItemSellStatus.SELL)){
booleanBuilder.and(item.itemSellStatus.eq(ItemSellStatus.SELL));
}
Pageable pageable = PageRequest.of(0, 5); // 4: 데이터를 페이징해 조회하도록 Pageble 객체 생성
Page<Item> itemPagingResult =
itemRepository.findAll(booleanBuilder, pageable); // 5: 조건에 맞는 데이터를 Page 객체로 받아옴.
System.out.println("total elements : " +
itemPagingResult. getTotalElements());
List<Item> resultItemList = itemPagingResult.getContent();
for(Item resultItem: resultItemList){
System.out.println(resultItem.toString());
}
}
createItemList2
: 110까진 SELL, 1120까진 SOLDOUTBooleanBuilder
: 동적으로 쿼리를 생성해주면서 조건까지 넣을 꺼임.and
: 조회 시 사용할 조건들을 설정Pageable
: 페이지를 어떻게 보여줄지 정보를 전달하는 APIPageRequest
: 몇 페이지, 한 페이지의 사이즈, Sorting 방법(Option)을 가지고, Repository에 Paging을 요청할 때 사용하는 것
[ 첫번째 인자 : 조회할 페이지 번호, 두번째 : 한 페이지당 조회할 데이터 개수]
실행결과
Hibernate:
select
item0_.item_id as item_id1_0_,
item0_.item_detail as item_det2_0_,
item0_.item_nm as item_nm3_0_,
item0_.item_sell_status as item_sel4_0_,
item0_.price as price5_0_,
item0_.rec_time as reg_time6_0_,
item0_.stock_number as stock_nu7_0_,
item0_.update_time as update_t8_0_,
from
item item0_
where
(
item0_.item_detail like ? escape '!'
)
and item0_.price>?
and item0_.item_sell_status=? limit ?
'🛠 백엔드 > 쇼핑몰 클론코딩' 카테고리의 다른 글
[8] Cart (1) | 2023.01.11 |
---|---|
[7] Order (0) | 2023.01.07 |
[6] Item 등록 및 조회 (0) | 2023.01.04 |
[5] 연관매핑 (0) | 2022.12.30 |
[4] Spring Security (1) | 2022.12.30 |