dev-course day30

2rlokr·2025년 4월 14일

dev-course

목록 보기
30/43
post-thumbnail

오늘 배운 것

Hibernate 실습

Case 1 : JPQL SELECT Test

Test 1 : 객체 자체를 조회 & 원하는 필드만 조회

executeCommit(entityManager, () -> {
	String jpql1 = "select c from Coffee c";
    
    
    // 1. 객체 전체를 조회
    // List resultList = entityManager.createQuery(jpql1).getResultList();
    // 클래스 타입을 지정해주지 않으면 Object 타입으로 결과를 반환한다. 
	List<Coffee> coffeeList = entityManager.createQuery(jpql1, Coffee.class).getResultList();
    
    // 2. 특정 필드만 조회
    String jpql2 = "select c.name from Coffee c";
    List<String> coffeeNameList = entityManager.createQuery(jpql2, String.class).getResultList();
	// 스트링 타입으로 스트링 배열을 받을 수 있게 된다.
  • JPQL에서는 엔티티 클래스의 필드명으로 조회해야 한다. (SQL 필드명 X)

로그에 출력되는 SQL문을 확인해보면, SQL 쿼리 이전에 JPQL이 찍히는 것을 알 수 있다.

JPA를 이용해서 .find() 를 한다거나 하면 ActionQueue에 쌓아뒀다가 트랜잭션 커밋 시 SQL를 날리는데, 그 때 SQL를 바로 보내는 것이 아니라 JPQL을 만들어두고 이것을 SQL로 번역해서 보내는 것이다.

Test 2 : 동적 바인딩

executeCommit(entityManager, () -> {
	
	String jpql1 = "select c from Coffee c where c.name=:name";
    String jpql2 = "select c from Coffee c where c.name= ?1";

    TypedQuery<Coffee> query1 = entityManager.createQuery(jpql1, Coffee.class);
    query1.setParameter("name", "Americano");

	TypedQuery<Coffee> query2 = entityManager.createQuery(jpql2, Coffee.class);
    query2.setParameter(1, "Americano");

	Coffee americano1 = query1.getSingleResult();
	// resultList.get(0);
	Coffee americano2 = query2.getSingleResult();
  • JPQL에서 변수를 동적 바인딩할 때, 변수명을 사용해도 되고, jpql2처럼 위치 기반으로 바인딩해줄 수도 있다.
  • 동적 바인딩을 할 때는 .setParameter(...) 을 사용한다.
  • createQueryQuery를 반환한다. 특정 클래스 타입을 넣어줬을 경우, TypedQuery가 반환된다.

결과 값이 없을 때 (getSingleResult() vs getResultList())

  • getSingleResult()는 결과를 하나만 반환한다. 하지만, 결과가 하나도 없을 때는 예외가 발생한다.
  • getResultList()는 결과 전체를 리스트로 반환한다. 결과가 하나도 없을 때 비어있는 리스트를 반환한다. null 이 담기는 것이다.

Test 3 : DTO를 이용해서 SELECT 결과 받기

executeCommit(entityManager, () -> {
/*		1번 방법
		String jpql = "select c.name, c.price from Coffee c";

		List<Object[]> resultList = entityManager.createQuery(jpql).getResultList();

		for (Object[] row : resultList) {
        	String name = (String) row[0];
            Integer price = (Integer) row[1];
            log.info("name = {}", name);
            log.info("price = {}", price);
            CoffeeDto coffeeDto = new CoffeeDto(name, price);
            log.info("coffeeDto = {}", coffeeDto);
		}*/
	
    // 2번 방법
	String jpql = "select new io.silver.domain.eg4._1.CoffeeDto(c.name, c.price) from Coffee c";
    List<CoffeeDto> resultList = entityManager.createQuery(jpql, CoffeeDto.class).getResultList();
  • 특정 컬럼을 여러 개 받아올 때는 String.class 이렇게 통일해서 받아올 수가 없다.
  • 그래서 이 때는, CoffeeDto와 같은 필요한 컬럼만을 담고 있는 Data Transfer Object (DTO)를 생성해줄 수 있다.
  • 하지만, @Entity가 아니기 때문에 JPA가 자동으로 매핑해주지 못한다.

1번 방법

Object[] 오브젝트 배열의 리스트로 받아올 수 있다. 하지만, 타입변환을 수동으로 해줘야 한다는 번거로움이 있다.

2번 방법

JPQL에서 직접 new 키워드로 DTO를 생성해줄 수 있다. new 키워드를 통해 생성자 기반 매핑을 해줘야 하고, 전체 경로를 다 지정해줘야 한다.

Test 4 : OFFSET, LIMIT

executeCommit(entityManager, () -> {

	String jpql = "select c from Coffee c";

	TypedQuery<Coffee> query = entityManager.createQuery(jpql, Coffee.class);
    query.setFirstResult(5); // OFFSET
    query.setMaxResults(5); // LIMIT

	List<Coffee> resultList = query.getResultList();

	assertThat(resultList.size()).isEqualTo(5);
});
  • OFFSET 설정으로 몇 번째 레코드부터 조회할지, LIMIT 설정으로 몇 개까지만 조회할지 설정할 수 있다.
  • 일반적으로 SQL에서는 쿼리문에서 지정해줄 수 있지만, JPQL에서는 .setFirstResult, .setMaxResults 메서드를 통해 OFFSET과 LIMIT를 정해줄 수 있다.

Hibernate + SpringBootTest 실습

Case 0

@Transactional

@Slf4j
@Repository
@Transactional
@RequiredArgsConstructor
public class HibernateItemRepository {
	private final EntityManager entityManager;
  • @Transactional 을 붙여주면 엔티티매니저에서 트랜잭션을 받아오고 시작한다. 이전 실습에서 getTransaction(), try/catch해서 커밋과 롤백을 진행한 이 반복되는 횡단관심사를 @Transactional 어노테이션이 대신 해주는 것이다.
    • readOnly = true 라는 옵션으로 쓰기를 막아줄 수 있다. (조회만 가능)

SQL 구문 보기

spring:
  jpa:
    properties:
      hibernate:
        show_sql: true
        format_sql: true

    hibernate:
        ddl-auto: create
  • 마찬가지로 위와 같이 설정하여 show_sql, format_sql를 통해 SQL구문이 전달되는 것을 로그에서 확인할 수 있다.

Case 1 : Item 저장

Items item1 = Items.builder()
                .itemCode(TestUtils.genRandomItemCode())
                .price(TestUtils.genRandomPrice())
                .build();
                
Items saved = repository.save(item1);
assertThat(saved.getId()).isNotNull();
  • 객체를 생성할 때 Id 값을 넣어주지 않았지만, persist -> commit (영속성 컨텍스트에서 엔티티 인스턴스로 영속화되어 있다가 커밋)될 때 Id가 자동으로 들어가게 된다.
Items item2 = Items.builder()
                .price(TestUtils.genRandomPrice())
                .build();
                
assertThatThrownBy(
	() -> {
    	repository.save(item2);
    }
).isInstanceOf(DataIntegrityViolationException.class);
  • nullable=falseitemCode를 빼고 객체를 생성하고 repository.save(item2)를 실행하면 INSERT문을 날리는 것을 로그에서 확인할 수 있다.
    즉, persist에서는 문제가 없었고, Commit할 때 DB의 NOT NULL 제약조건 때문에 오류가 난 것이다.
  • 엔티티 영속화 (persist) 시점에는 null이어도 문제가 없다. 여기서 에러는 DB 스키마에 설정된 NOT NULL 제약조건 때문에 오류가 나는 것이다. 그렇기 때문에 @Column에 설정해 둔 nullable, unique 제약조건은 유효성 검사가 아니다.

오늘 궁금했던 것 (1) ❓

Q. 그렇다면 nullable, unique 설정은 왜 해주는 걸까?

A. @Column(nullable = false)는 JPA 수준의 힌트로, 주로 DDL 생성 시점에 사용된다. DDL 생성 시 DB 컬럼에 NOT NULL 제약을 걸도록 유도하는 메타정보인 것이다.

Case 2 : Bulk Insert (Batch Save)

public List<Items> saveAll(List<Items> items) {

// 1번 방법
//        for (Items item : items) {
//            entityManager.persist(item);
//        }
	
	// 2반 빙밥 : buffer
	int batchSize = 50;

	for( int i=0; i < items.size(); i++){
    	entityManager.persist(items.get(i));
        if(i % batchSize == 0 && i > 0){
        	entityManager.flush(); // actionQueue에 있는 거 DB에 반영시키는 것임.
            entityManager.clear();
        }
    }

	entityManager.flush();
	entityManager.clear();
	return items;
}
  • 한 번에 하나의 객체만 저장하지 말고, 여러 개의 데이터를 한 번에 저장할 때 사용할 수 있다.

1번 방법

  • 1번 방법을 이용해서 한 번에 저장해줄 수 있다.
  • 하지만, 저렇게 되면 영속성 컨텍스트에 많은 쿼리들이 쌓여 영속성 컨텍스트 메모리가 과부하될 가능성이 있고, 커밋될 때 한 번에 너무 많은 양의 쿼리가 전송되어 성능이 저하될 가능성도 있다.
  • clear()도 안 했기 때문에 1차 캐시에 엔티티가 계속 남아있어서 GC도 못하게 된다.

2번 방법

  • batchSize를 정해두어 쿼리가 batchSize만큼 쌓이면 커밋하겠다고 설정하는 것이다.
  • batchSize만큼 쌓였을 때, persist()를 하고, flush()clear() 도 해준다.
    • flush()를 안 하면 메모리에 엔티티가 너무 많이 쌓여서 엔티티 컨텍스트에 과부하가 온다. 그래서 flush()로 쿼리를 DB에 전송해주고, clear()로 캐시까지 비우는 것이다.

Spring Data JPA

Case 1 : JpaRepository<>

public interface DataJpaItemRepository extends JpaRepository<Items, Long> {
    // Query Method
    Optional<Items> findByItemCode(String itemCode);
}
  • Spring Data JPA는 레포지토리 인터페이스를 생성하고 JpaRepository 를 상속받아주면 된다.
  • 여기에 들어가는 Generic Type 2개가 중요하다 !
    • 첫 번째 파라미터에는 관리할 Entity 클래스 타입을 넣어준다.
    • 두 번째 파라미터에는 해당 Entity의 @Id 필드 타입을 넣어준다.

그렇게 되면, Spring Data JPA가 이 인터페이스를 보고, findAll, save, delete 등 CRUD 메서드 구현체를 자동으로 생성해준다 ! 또, 이 구현체는 런타임 시 Spring Bean으로 등록되어 @Autowired 등으로 주입이 가능해진다.

기본 구현해주는 메서드를 제외하고 다른 메서드를 구현하고 싶을 때 (PK가 아닌 UNIQUE 키로 데이터를 조회하고 싶을 때 등) Query Method를 이용할 수 있다.

  • Query Method란, 메서드 이름을 분석해서 쿼리를 자동 생성해주는 기능인데, 위의 예시에서는 JPQL로 select i from Items i where i.itemCode = :itemCode와 같은 쿼리를 사용해서 결과를 받아오고, 반환타입도 바꿔서 Optional로 쓸 수 있게 되는 것이다.
// DataJpaOrderRepository.java
@Query("select o from Orders o where o.orderCode=:orderCode")
Optional<Orders> findByOrderCode(String orderCode); 

// DataJpaOrderItemsRepository.java
@Query("select oi from OrderItems oi where oi.orders.orderCode = :orderCode")
List<OrderItems> findAllByOrderCode(String orderCode);

위와 같이, @Query 어노테이션을 사용해 JPQL을 직접 작성해줄 수도 있다. 쿼리 메서드의 매개변수와 JPQL의 :파라미터명을 일치시켜줘야 자동 바인딩돼서 DB에 전달한다.

또, 두 번째의 경우 oi.orders.orderCode처럼 엔티티 간의 연관관계를 따라 들어갈 수 있다. JPA에서는 이렇게 객체 그래프 탐색을 통해 엔티티의 필드도 쿼리에서 자유롭게 사용할 수 있다.

Case 2 : Combine Repository

@Repository
@RequiredArgsConstructor
public class DataJpaOrderRepositoryCombine {

    private final DataJpaOrderRepository orderRepository;
    private final DataJpaOrderItemsRepository orderItemsRepository;
  • OrderOrderItems는 생명주기가 같다. 그래서 Combine된 Repository class를 만들어서 각 레포지토리를 주입받아 사용한다.

Case 3 : @SpringBootTest vs @DataJpaTest

@SpringBootTest

  • 전체 애플리케이션 통합 테스트로, 존재하는 빈을 모두 로딩하기 때문에 비교적 느리다.
  • 직접 @Transactional을 걸어야 한다.

@DataJpaTest

  • JPA 관련 컴포넌트 테스트에 특화되어 있고, JPA 관련 Bean(Repository 등)만 로딩하기 때문에 비교적 빠르다. (Service, Controller는 올라가지 않는다.
  • 기본 설정으로 @Transactional을 가지고 있다. 또, 테스트가 끝나면 자동 롤백되어 DB가 원상복구된다.
  • 보통 Repository 테스트, 쿼리 테스트용으로 잘 사용한다.

QueryDSL

QueryDSL은 정적 타입을 이용한 Query생성에 특화된 언어(Domain Specific Language)의 특징을 갖는 라이브러리이다.

QueryDSL의 등장배경

  1. JPQL의 타입 안정성
  • JPQL은 String 형태의 문자열로 작성하게 되는데, 이럴 경우 띄어쓰기, 오타 등 잘못된 문법이 있다면 실제 서버가 띄워지고 로직이 수행된 후 에러를 발생할 수 있게 된다.
  1. 직관성
  • 함수를 작성할 때, 함수를 보고 어떤 기능을 하는지 이해할 수 있는 것은 중요하다. 하지만, JPQL의 경우에 이러한 직관성이 떨어진다.

QueryDSL 설정 방법

1. 의존성 주입

implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'

annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta'
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
  • QueryDSL을 사용하기 위해서는 위와 같은 의존성을 주입해줘야 한다.

2. Gradle build

  • 빌드 clean -> build 를 수행한다.

3. Q 클래스 생성

  • 그렇게 되면 위와 같이 build/classes/java/....../entity (객체를 보관해둔 패키지) 안에 Q 클래스가 생성된 것을 볼 수 있다.

Q클래스란
QueryDSL을 사용할 때, Entity 기반으로 자동 생성되는 클래스이다. Q 클래스는 Entity 필드들을 타입 안전하게 쿼리로 작성할 수 있도록 도와주는 DSL 객체이다. 즉, 문자열로 JPQL을 쓰는 대신, Q클래스의 필드와 메서드를 통해 자바 코드로 쿼리를 작성하는 것이다.

4. Configuration

@Configuration
public class QueryDslConfig {

    @Bean
    public JPAQueryFactory queryFactory(EntityManager entityManager) {
        return new JPAQueryFactory(entityManager);
    }

}
  • QueryDSL을 사용하기 위한 설정 클래스이다.
  • JPAQueryFactory는 QueryDSL에서 사용하는 쿼리 생성 객체이다. JPAQueryFactory는 내부적으로 이 EntityManager를 사용해서 쿼리를 실행한다.

5. JPAQueryFactory를 이용해서 쿼리 만들기

@Slf4j
@Repository
@Transactional
@RequiredArgsConstructor
public class QueryDslItemRepository {

    private final JPAQueryFactory queryFactory;
	public Optional<Items> findByItemCode(String itemCode) {

        Items findItem = queryFactory.selectFrom(items)
                .where(items.itemCode.eq(itemCode))
                .fetchFirst();// 오류가 안 남. 없으면 Null 1개만 리턴

        return Optional.ofNullable(findItem);
    }
}
  • queryFactory를 이용해서 쿼리를 만든다.
  • selectFrom 로 대상 객체를 정해준다.
  • where로 조건을 지정한다.
  • .fetchFirst() 로 결과를 1개만 반환하도록 한다.
    • 결과가 없더라도 오류가 나지 않고, Null이 반환된다.

주의할 점

QueryDSL은 엔티티가 달라지면 clean() -> build()를 다시 해야 한다.
또한, QueryDSL은 조회에 특화되어 있는 기술이기 때문에, 따로 저장이나 수정, 삭제 작업을 원한다면 EntityManager을 사용해야 한다.


느낀 점

오늘은 굉장히 무난하게 ! (중간에 헷갈리는 부분들은 있었지만) 재미있게 ! 수업을 들었다. 어제 N시간동안 스터디 정리한 게 파사삭 날라간 이슈로 다시 다하고 새벽 4시에 잤더니 살짝 피곤했다. 근데 잠은 안오고 그냥 기운이 좀 없었다. 그런 거치고 집중은 되게 잘됐어서 다행이다..

아니 세상에 오늘 TIL 적는데 뭐라고 벌써 day30 TIL이라고..? 와우.. 대박이다잉.. = 이 프로그램의 30%는 했다는 건데.. 짱 신기 짱 뿌듯하다 ! ˃̵͈̑ᴗ˂̵͈̑ 그리고~~Spring Data JPA까지 진도가 다 나갔다!!! 이것도 너무 신기해... 큰 챕터로 보자면 이제 Spring Security밖에 안 남은 거잖아. 이게 맞냐구. 대박이야 어머어머

근데 나 보안쪽으로는 아는 게 더더욱 없는데.. 또 어렵겠지..? ㅎ.. ㅎ 와아.. ㅎ 그렇지만 모르는만큼 겁없이 들어주마 덤벼. 막이래

내일도 힘내서 해보자구요 파이팅 !!! 30일 수고했다 !!!

0개의 댓글