개발/Project(Spring-쇼핑몰)

Project - (1) Spring Data JPA

잇(IT) 2023. 8. 1. 15:29
728x90
- Maven Setting

- 여러가지의 프로젝트를 동시에 진행하면 메이븐의 의존성이 서로 꼬일 수 있으므로 프로젝트별로 다른 폴더를 Local repository를 지정하기를 권장한다.

 


- application.properties 설정

- server.port = 80 : 애플리케이션 실행할 포트 설정

- application.name = spring-demo : 설정해둔 애플리케이션의 값을 읽어와서 자바 코드에서 사용해야 하면 @Value 어노테이션을 통해서 읽어올 수 있다.


- JPA 사용 시 장점

1. 특정 데이터베이스에 종속되지 않음

2. 객체지향적 프로그래밍

3. 생산성 향상

- JPA 사용 시 단점

1. 복잡한 쿼리 처리

2. 성능 저하 위험

3. 학습 시간

- ORM : Object Relational Mapping의 약자로 객체와 관계형 데이터베이스를 매핑해주는 것을 말한다.

 - 객체지향과 관계형 데이터베이스 간의 패러다임이 불일치하기 때문에 이를 해결하기 위해 나온 기술이 ORM이다.

 - 객체는 객체지향적으로, 데이터베이스는 데이터베이스 대로 설계를 한다. ORM은 중간에서 2개를 매핑하는 역할을 한다. 

 

- JPA 동작 방식

1. 엔티티

 1.1. 데이터베이스의 테이블에 대응하는 클래스

 1.2. @Entity가 붙은 클래스는 JPA에서 관리하며 엔티티라고 한다.

 

2. 엔티티 매니저 팩토리

 2.1. 엔티티 매니저 인스턴스를 관리하는 주체이다.

 2.2. 애플리케이션 실행 시 한 개만 만들어지며 사용자로부터 요청이 오면 엔티티 매니저 팩토리로부터 엔티티 매니저를 생성한다.

 

3. 엔티티 매니저

 3.1. 엔티티 매니저란 영속성 컨텍스트에 접근하여 엔티티에 대한 데이터베이스 작업을 제공한다.

 3.2. 내부적으로 데이터베이스 커넥션을 사용해서 데이터베이스에 접근한다.

 3.3.

  3.3.1. find() : 영속성 컨텍스트에서 엔티티를 검색하고 영속성 컨텍스트에 없을 경우 데이터베이스에서 데이터를 찾아 영속성 컨텍스트에 저장한다.

  3.3.2. persist() : 엔티티를 영속성 컨텍스트에 저장한다.

  3.3.3. remove() : 엔티티 클래스를 영속성 컨텍스트에서 삭제한다.

  3.3.4. flush() : 영속성 컨텍스트에 저장된 내용을 데이터베이스에 반영한다.

 

4. 영속성 컨텍스트

엔티티를 영구 저장하는 환경으로 엔티티 매니저를 통해 영속성 컨텍스트에 접근한다.

 

* 엔티티 매니저는 데이터 변경 시 데이터의 무결성을 위해 반드시 트랜잭션을 시작해야 한다.

 

- 영속성 컨텍스트 사용 시 이점

1. 영속성 컨텍스트라는 중간 계층을 만들면 버퍼링, 캐싱 등을 할 수 있는 장점이 있다.

1. 1차 캐시 : 영속성 컨텍스트에는 1차 캐시가 존재하면 Map<Key, Value>로 저장된다. entityManager.find() 호출 시 영속성 컨텍스트의 1차 캐시를 조회한다. 엔티티가 존재할 경우 해당 엔티티를 반환하고, 엔티티가 없으면 데이터베이스에서 조회 후 1차 캐시에 저장 및 반환한다.

 

2. 동일성 보장 : 하나의 트랜잭션에서 같은 키값으로 영속성 컨텍스트에 저장된 엔티티 조회 시 같은 엔티티 조회를 보장한다. 바로 1차 캐시에 저장된 엔티티를 조회하기 때문에 가능하다.

3. 트랜잭션을 지원하는 쓰기 지연

 3.1. 영속성 컨텍스트에는 쓰기 지연 SQL 저장소가 존재한다. entityManager.persist()를 호출하면 1차 캐시에 저장되는 것과 동시에 쓰기 지연 SQL 저장소에 SQL문이 저장된다.

 3.2. SQL을 쌓아두고 트랜잭션을 커밋하는 시점에 저장된 SQL문들이 flush되면서 데이터베이스에 반영된다.

 3.3. 이렇게 모아서 보내기 때문에 성능에서 이점을 볼 수 있다.

 

4. 변경 감지

 4.1. JPA는 1차 캐시에 데이터베이스에서 처음 불러온 엔티티의 스냅샷 값을 가지고 있다. 그리고 1차 캐시에 저장된 엔티티와 스냅샷을 비교 후 변경 내용이 있다면 UPDATE SQL문을 쓰기 지연 SQL 저장소에 담아준다

 4.2. 그리고 데이터베이스에 커밋 시점에 변경 내용을 자동으로 반영한다. 즉, 따로 update문을 호출할 필요가 없다.


- Item (Entity)

@Entity
@Table(name="item")
//entity 설정 및 @Table 어노테이션을 통해 어떤 테이블과 매핑될지를 지정한다.
//item 테이블과 매핑되도록 name을 item으로 지정한다.
@Getter
@Setter
@ToString
public class Item {

    @Id
    @Column(name="item_id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    //entity로 선언한 클래스는 반드시 기본키를 가져야 한다.
    //기본키가 되는 멤버변수에 @Id 어노테이션을 붙여준다. 테이블에 매핑될 컬럼의 이름을 @Colum 어노테이션을 통해 설정한다.
    //item 클래스의 id 변수와 item테이블의 item_id 컬럼이 매핑되도록 한다.
    //마지막으로 @GeneratedValue 어노테이션을 통해 기본키 생성 전략을 AUTO로 지정한다.
    private Long id;

    @Column(nullable = false, length = 50)
    //nullable을 통해 항상 값이 있어야 하는 필드는 not null 설정을 한다.
    //String 필드는 default 값으로 255가 설정되어 있다.
    private String itemNm;

    @Column(name="price", nullable = false)
    private int price;

    @Column(nullable = false)
    private int stockNumber;

    @Lob
    @Column(nullable = false)
    private String itemDetail;

    @Enumerated
    private ItemSellStatus itemSellStatus;

    private LocalDateTime regTime;

    private LocalDateTime updateTime;
}

 

- ItemSellStatus

public enum ItemSellStatus {
    SELL, SOLD_OUT
}

- enum 타입을 통해 현재 상품이 판매 중인지 품절 상태인지 나타낸다. enum에 정의한 타입만 값을 가지도록 컴파일 시 체크를 할 수 있다는 장점이 있다.

 

- ItemRepository

package com.shop.repository;

import com.shop.entity.Item;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface ItemRepository extends JpaRepository<Item, Long> {
    //JpaRepository는 2개의 제네릭 타입을 사용하는데 첫 번째에는 엔티티 타입 클래스를 넣어주고,
    //두 번째는 기본키 타입을 넣어준다.

    List<Item> findByItemNm(String itemNm);

    List<Item> findByItemNmOrItemDetail(String itemNm, String itemDetail);
    //상품을 상품명과 상품 상세 설명을 OR 조건을 이용하여 조회하는 쿼리 메소드

    List<Item> findByPriceLessThan(Integer price);

    List<Item> findByPriceLessThanOrderByPriceDesc(Integer price);

}

 

- Test 코드

@SpringBootTest
//통합 테스트를 위한 어노테이션, 실제 애플리케이션을 구동할 때처럼 모든 Bean을 IoC 컨테이너에 등록한다.
@TestPropertySource(locations = "classpath:application-test.properties")
//properties 설정이 여러개일 때 우선순위를 둘 properties를 지정한다.
class ItemRepositoryTest {

    @Autowired
    ItemRepository itemRepository;
    //@Autowired를 통해 Bean을 주입한다.

    @Test
    @DisplayName("상품 저장 테스트")
    public void createItemTest() {
        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.createItemTest();
        List<Item> itemList = itemRepository.findByItemNm("테스트 상품1");
        for (Item item : itemList) {
            System.out.println("item.toString() = " + item.toString());
        }
    }

    @Test
    @DisplayName("상품명, 상품상세설명 or 테스트")
    public void findByItemNmOrItemDetailTest() {
        this.createItemTest();
        List<Item> itemList = itemRepository
                .findByItemNmOrItemDetail("테스트 상품1", "테스트 상품 상세 설명5");
        for (Item item : itemList) {
            System.out.println("item.toString() = " + item.toString());
        }
    }

    @Test
    @DisplayName("가격 LessThan 테스트")
    public void findByPriceLessThan() {
        this.createItemTest();
        List<Item> itemList = itemRepository.findByPriceLessThan(10005);
        for (Item item : itemList) {
            System.out.println("item.toString() = " + item.toString());
        }
    }

    @Test
    @DisplayName("가격 내림차순 조회 테스트")
    public void findByPriceLessThanOrderByPriceDesc() {
        this.createItemTest();
        List<Item> itemList = itemRepository.findByPriceLessThanOrderByPriceDesc(10005);
        for (Item item : itemList) {
            System.out.println("item.toString() = " + item.toString());
        }
    }
}

 

- findByItemNmTest()

 

- findByItemNmOrItemDetailTest()

 

- findByPriceLessThan()

 

- findByPriceLessThanOrderByPriceDesc()


- Spring Data JPA, Querydsl

- Querydsl은 JPQL을 코드로 작성할 수 있도록 도와주는 빌더 API이다.

- Querydsl은 소스코드로 SQL문을 문자열이 아닌 코드로 작성하기 때문에 컴파일러의 도움을 받을 수 있다.

- 또한 동적으로 쿼리를 생성해주는 게 큰 장점이다.

 

- 장점

1. 고정된 SQL문이 아닌 조건에 맞게 동적으로 쿼리를 생성할 수 있다.

2. 비슷한 쿼리를 재사용할 수 있으며 제약 조건 조립 및 가독성을 향상시킬 수 있다.

3. 문자열이 아닌 자바소스코드로 작성하기 때문에 컴파일 시점에 오류를 발견할 수 있다.

4. IDE의 도움을 받아서 자동 완성 기능을 이용할 수 있기 때문에 생산성을 향상시킬 수 있다.

- Querydsl을 설정을 마치게 되면 위와 같이 Q(엔티티명) java 파일이 생성된다.

 

- QItem.java

@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QItem extends EntityPathBase<Item> {

    private static final long serialVersionUID = -1161068493L;

    public static final QItem item = new QItem("item");

    public final NumberPath<Long> id = createNumber("id", Long.class);

    public final StringPath itemDetail = createString("itemDetail");

    public final StringPath itemNm = createString("itemNm");

    public final EnumPath<com.shop.constant.ItemSellStatus> itemSellStatus = createEnum("itemSellStatus", com.shop.constant.ItemSellStatus.class);

    public final NumberPath<Integer> price = createNumber("price", Integer.class);

    public final DateTimePath<java.time.LocalDateTime> regTime = createDateTime("regTime", java.time.LocalDateTime.class);

    public final NumberPath<Integer> stockNumber = createNumber("stockNumber", Integer.class);

    public final DateTimePath<java.time.LocalDateTime> updateTime = createDateTime("updateTime", java.time.LocalDateTime.class);

    public QItem(String variable) {
        super(Item.class, forVariable(variable));
    }

    public QItem(Path<? extends Item> path) {
        super(path.getType(), path.getMetadata());
    }

    public QItem(PathMetadata metadata) {
        super(Item.class, metadata);
    }

}

- 위 코드들을 Querydsl이 자동으로 생성해준다. 

 

@Test
    @DisplayName("Querydsl 조회 테스트1")
    public void queryDslTest() {
        this.createItemList();
        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        QItem qItem = QItem.item;
        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();

        for (Item item : itemList) {
            System.out.println("item.toString() = " + item.toString());
        }
    }


-QuerydslPredicateExecutor를 이용한 상품 조회

- Predicate란 '이 조건이 맞다'고 판단하는 근거를 함수로 제공한다.

 

public interface ItemRepository extends JpaRepository<Item, Long>, QuerydslPredicateExecutor<Item> {

- QueryDslPredicateExecutor 인터페이스 상속을 추가한다.

 

- Test 코드

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();
        //BooleanBuilder는 쿼리에 들어갈 조건을 만들어주는 빌더라고 생각하면 된다. Predicate를 구현하고 있으며 메소드 체인 형식으로 사용한다.
        QItem item = QItem.item;
        String itemDetail = "테스트 상품 상세 설명";
        int price = 10003;
        String itemSellStat = "SELL";

        booleanBuilder.and(item.itemDetail.like("%" + itemDetail + "%"));
        booleanBuilder.and(item.price.gt(price));
        System.out.println(ItemSellStatus.SELL);
        if(StringUtils.equals(itemSellStat, ItemSellStatus.SELL)){
            booleanBuilder.and(item.itemSellStatus.eq(ItemSellStatus.SELL));
        }
// 동적으로 쿼리를 추가 할 수 있다. 위의 코드는 상품의 판매 상태가 SELL일 때만 booleanBuilder에 판매상태 조건을 동적으로 추가하는 것을 볼 수 있다.
        Pageable pageable = PageRequest.of(0, 5);
        Page<Item> itemPagingResult = itemRepository.findAll(booleanBuilder, pageable);
            //QueryDslPredicateExecutor 인터페이스에서 정의한 findAll() 메서드를 이용해 조건에 맞는 데이터를 Page 객체로 받아온다
        //booleanBuilder는 Predicate의 구현체이기 때문에 파라미터로 넘길 수 있다.
        System.out.println("total elements : " + itemPagingResult. getTotalElements ());

        List<Item> resultItemList = itemPagingResult.getContent();
        for(Item resultItem: resultItemList){
            System.out.println(resultItem.toString());
        }
    }
}

728x90