본문 바로가기
TIL/Spring

23.07.26

by J1-H00N 2023. 7. 26.

ORM이 가지고 있던 문제와 해결 방법

상속 문제 : 매핑정보에 상속정보를 넣어준다. (@OneToMany, @ManyToOne)

관계 문제 : 매핑정보에 방향정보를 넣어준다. (@JoinColumn, @MappedBy)

탐색 문제 : 매핑/조회 정보로 참조탐색 시점을 관리한다. (@FetchType, fetchJoin())

밀도 문제 : 크기가 큰 멤버 객체는 테이블을 분리하여 상속으로 처리한다. (@embedded)

식별성 문제 : PK 를 객체 Id로 설정하고 EntityManager는 해당 값으로 객체를 식별하여 관리 한다. (@Id, @GeneratedValue)

 

ORM이 가지는 장점

1. 1차, 2차 캐시

  • 1차 캐시
    • 영속성 컨텍스트 내부에는 엔티티를 보관하는 저장소가 있는데 이를 1차 캐시라고 한다.
    • 일반적으로 트랜잭션을 시작하고 종료할 때까지만 1차 캐시가 유효하다.
    • 1차 캐시는 한 트랜잭션 계속해서 원본 객체를 넘겨준다.
  • 2차 캐시
    • 애플리케이션 범위의 캐시로, 공유 캐시라고도 하며, 애플리케이션을 종료할 때 까지 캐시가 유지된다.
    • 2차 캐시는 캐시 한 객체 원본을 넘겨주지 않고 복사본을 만들어서 넘겨준다.
    • 복사본을 주는 이유는 여러 트랜잭션에서 동일한 원본객체를 수정하는일이 없도록 하기 위해서이다.

적용 방법

// Team.java

@Entity
@Cacheable
public class Team {
	@Id @GeneratedValue
	private Long id;
	...
}
# application.yml

spring.jpa.properties.hibernate.cache.use_second_level_cache: true
# 2차 캐시 활성화합니다.

spring.jpa.properties.hibernate.cache.region.factory_class: XXX
# 2차 캐시를 처리할 클래스를 지정합니다.

spring.jpa.properties.hibernate.generate_statistics: true
# 하이버네이트가 여러 통계정보를 출력하게 해주는데 캐시 적용 여부를 확인할 수 있습니다.

캐시 모드 설정

# appplication.yml

spring.jpa.properties.javax.persistence.sharedCache.mode: ENABLE_SELECTIVE
ALL
모든 엔티티를 캐시합니다.
NONE
캐시를 사용하지 않습니다.
ENABLE_SELECTIVE
Cacheable(true)로 설정된 엔티티만 캐시를 적용합니다.
DISABLE_SELECTIVE
모든 엔티티를 캐시하는데 Cacheable(false)만 캐시하지 습니다.
UNSPECIFIED
JPA 구현체가 정의한 설정을 따릅니다

 

2. 영속성 컨텍스트(1차 캐시)를 활용한 쓰기지연

  • 영속성 4가지 상태 ( 비영속 > 영속 > 준영속 | 삭제)
  • ① 비영속(new/transient) - 엔티티 객체가 만들어져서 아직 저장되지 않은 상태로, 영속성컨텍스트와 전혀 관계가 없는 상태
  • ② 영속(managed) - 엔티티가 영속성 컨텍스트에 저장되어, 영속성 컨텍스트가 관리할 수 있는 상태
  • ③ 준영속(detached) - 엔티티가 영속성 컨텍스트에 저장되어 있다가 분리된 상태로, 영속성컨텍스트가 더 이상 관리하지 않는 상태
  • ④ 삭제(removed) - 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제하겠다고 표시한 상태
  • 객체의 영속성 상태는 Entity Manager 의 메소드를 통해 전환된다.
  • Raw JPA 관점에서 순서대로 요약정리 해보자면
    • persist(),merge() > (영속성 컨텍스트에 저장된 상태) > flush() > (DB에 쿼리가 전송된 상태) > commit() > (DB에 쿼리가 반영된 상태)

예제)

Item item = new Item();		// 1
item.setItemNm("테스트 상품");	

EntityManager em = entityManagerFactory.createEntityManager();	// 2
EntityTransaction transaction = em.getTransaction();		// 3
	
transaction.begin();		
em.persist(item);		// 4-1
em.flush(item).     // 4-2 (DB에 SQL 보내기/commit시 자동수행되어 생략 가능함)
transaction.commit();		// 5

em.close();			// 6

1️⃣  영속성 컨텍스트에 담을 상품 엔티티 생성
2️⃣  엔티티 매니저 팩토리로부터 엔티티 매니저를 생성
3️⃣  데이터 변경 시 무결성을 위해 트랜잭션 시작
4️⃣  영속성 컨텍스트에 저장된 상태, 아직 DB에 INSERT SQL 보내기 전
5️⃣  트랜잭션을 DB에 반영, 이 때 실제로 INSERT SQL 커밋 수행
6️⃣  엔티티 매니저와 엔티티 매니저 팩토리 자원을 close() 호출로 반환
  • 쓰기 지연이 발생하는 시점
    • flush() 동작이 발생하기 전까지 최적화한다.
    • flush() 동작으로 전송된 쿼리는 더이상 쿼리 최적화는 되지 않고, 이후 commit()으로 반영만 가능하다.
  • 쓰기 지연 효과
    • 여러개의 객체를 생성할 경우 모아서 한번에 쿼리를 전송한다.
    • 영속성 상태의 객체가 생성 및 수정이 여러번 일어나더라도 해당 트랜잭션 종료시 쿼리는 1번만 전송될 수 있다.
    • 영속성 상태에서 객체가 생성되었다 삭제되었다면 실제 DB에는 아무 동작이 전송되지 않을 수 있다.
    • 즉, 여러가지 동작이 많이 발생하더라도 쿼리는 트랜잭션당 최적화 되어 최소쿼리만 날라가게된다.

 

테이블 매핑 기능

@Entity

  • 객체 관점에서의 이름
  • 디폴트로 클래스명으로 설정됨
  • 엔티티의 이름은 JQL에서 쓰임

@Table

  • RDB 의 테이블 이름
  • @Entity의 이름이 테이블의 기본값.
  • 테이블의 이름은 SQL에서 쓰임

@Id

  • 엔티티의 주키를 맵핑할 때 사용.
  • 자바의 모든 primitive 타입과 그 랩퍼 타입을 사용할 수 있음
    • Date랑 BigDecimal, BigInteger도 사용 가능.
  • 복합키를 만드는 맵핑하는 방법도 있지만 그건 논외로..

@GeneratedValue

  • 주키의 생성 방법을 맵핑하는 애노테이션
  • 생성 전략과 생성기를 설정할 수 있다.
    • 기본 전략은 AUTO: 사용하는 DB에 따라 적절한 전략 선택
    • TABLE, SEQUENCE, IDENTITY 중 하나.

@Column

  • unique
  • nullable
  • length
  • columnDefinition
  • ...

@Temporal

  • 현재 JPA 2.1까지는 Date와 Calendar만 지원.

@Transient

  • 컬럼으로 맵핑하고 싶지 않은 멤버 변수에 사용.

 

필드 타입 매핑 기능

@Column

  • String, Date, Boolean, 과 같은 타입들에 공통으로 사이즈를 제한할 용도로 쓰인다.
  • Class 에 @Entity 가 붙어있으면 자동으로 필드들에 @Column 이 붙음

@Enumerated

  • Enum 매핑용도로 쓰이며 실무에서는 @Enumerated(EnumType.*STRING*) 으로 사용권장
  • Default 타입인 ORDINAL 은 0,1,2.. 값으로 들어가기 때문에 추후 순서가 바뀔 가능성있다.

Composite Value 타입

@Embeddable

@Embedded

@AtrributeOverrides

@AttributeOverride

 

  • 복합키를 선언하는 방법은 2가지가 있습니다.
    1. @IdClass를 활용하는 복합키는 복합키를 사용할 엔티티 위에 @IdClass(식별자 클래스) 사용
    2. @EmbeddedId를 활용하는 복합키는 복합키 위에 @EmbeddedId 사용

@IdClass

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class UserChannelId implements Serializable {
  private Long user;   // UserChannel 의 user 필드명과 동일해야함
  private Long channel; // UserChannel 의 channel 필드명과 동일해야함

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    UserChannelId userChannelId = (UserChannelId) o;
    return Objects.equals(getUser(), userChannelId.getUser()) && Objects.equals(getChannel(), userChannelId.getChannel());
  }

  @Override
  public int hashCode() {
    return Objects.hash(getUser(), getChannel());
  }
}
@Entity
@IdClass(UserChannelId.class)
public class UserChannel {
  ....

  @Id
  @ManyToOne
  @JoinColumn(name = "user_id")
  User user;

  @Id
  @ManyToOne
  @JoinColumn(name = "channel_id")
  Channel channel;
  ...
}

 

@EmbeddedId

@Entity
public class UserChannel {

  @EmbeddedId
  private UserChannelId userChannelId;

	...

  @ManyToOne
  @MapsId("user_id")
  User user;

  @ManyToOne
  @MapsId("channel_id")
  Channel channel;

	...

}

 

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Embeddable
public class UserChannelId implements Serializable {

  @Column(name = "user_id")
  private Long userId;

  @Column(name = "channel_id")
  private Long channelId;

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    UserChannelId userChannelId = (UserChannelId) o;
    return Objects.equals(getUser(), userChannelId.getUser()) && Objects.equals(getChannel(), userChannelId.getChannel());
  }

  @Override
  public int hashCode() {
    return Objects.hash(getUser(), getChannel());
  }
}

 

 

Cascade(영속성 전이)

  • 사용 위치
    • 연관관계의 주인 반대편 - 부모 엔티티(다대일에서 일)
    • 즉, @OneToMany 가 있는 쪽 또는 @OneToOne 도 가능
    • 예를들어, 게시글과 첨부파일이라면 일에 해당하는 게시글에 설정한다.
  • 사용 조건
    • 양쪽 엔티티의 라이프사이클이 동일하거나 비슷해야한다.
      • 예를들어, 게시글이 삭제되면 첨부파일도 같이 삭제 되어야 한다.
    • 대상 엔티티로의 영속성 전이는 현재 엔티티에서만 전이 되어야 한다. (다른곳에서 또 걸면 안됨)
      • 예를들어, 첨부파일을 게시글이 아닌 다른곳에서 영속성 전이를 하면 안된다.
  • 옵션 종류
    • ALL : 전체 상태 전이
    • PERSIST : 저장 상태 전이
    • REMOVE : 삭제 상태 전이
    • MERGE : 업데이트 상태 전이
    • REFERESH : 갱신 상태 전이
    • DETACH : 비영속성 상태 전이

 

orphanRemoval (고아 객체 제거)

  • 사용 위치
    • @OneToMany 또는 @OneToOne 에서 사용 - 부모 엔티티
  • 사용법
    • Cascade.REMOVE 와 비슷한 용도로 삭제를 전파하는데 쓰인다.
    • 부모 객체에서 리스트 요소삭제를 했을경우 해당 자식 객체는 매핑정보가 없어지므로 대신 삭제해준다.
      • 요건 DB 에서는 절대 알 수 없는 행동이다. (부모가 자식의 손을 놓고 버리고 간 고아 객체)
  • 옵션
    • true
    • false

Cascade.REMOVEorphanRemoval 차이점

Cascade.REMOVE의 경우 일에 해당하는 부모 엔티티를 em.remove를 통해 직접 삭제할 때,그 아래에 있는 다에 해당하는 자식 엔티티들이 삭제되는 것입니다.

orphanRemoval=true는 위 케이스도 포함하며,일에 해당하는 부모 엔티티의 리스트에서 요소를 삭제하기만 해도 해당 다에 해당하는 자식 엔티티가 delete되는 기능까지 포함하고 있다고 이해하시면 됩니다.

즉, orphanRemoval=true 는 리스트 요소로써의 영속성 전이도 해준다는 뜻

 

<영속성 전이 최강 조합 : orphanRemoval=true + Cascade.ALL

위 2개를 함께 설정하면 자식 엔티티의 라이프 사이클이 부모 엔티티와 동일해지며, 직접 자식 엔티티의 생명주기를 관리할 수 있게 되므로 자식 엔티티의 Repository 조차 없어도 된다. (따라서, 매핑 테이블에서 많이 쓰임)

 

Fetch(조회 시점)

  • 사용 위치
    • Entity 에 FetchType 으로 설정할 수 있다.
      • @ElementCollection, @ManyToMany, @OneToMany, @ManyToOne, @OneToOne
    • Query 수행시 fetch Join 을 통해서 LAZY 인 경우도 즉시 불러올 수 있다.
  • 사용법
    • 기본 LAZY를 설정한 뒤에 필요할때만 fetch Join 을 수행한다.
    • 항상 같이 쓰이는 연관관계 일 경우만 EAGER 를 설정한다.
  • 옵션(FetchType)
    • EAGER : 즉시 로딩 (부모 조회 시 자식도 같이 조회)
    • LAZY : 지연 로딩 (자식은 필요할때 따로 조회)

 

SpringData 구조

SpringData 기능 목록

  • 강력한 리포지토리 및 사용자 지정 객체 매핑 추상화
  • 리포지토리 메서드 이름에서 동적 쿼리 파생
  • 기본 속성을 제공하는 구현 도메인 기본 클래스
  • 명료한 추적기능 지원(생성일시, 마지막 변경일시, 생성자, 마지막 변경자)
  • 사용자 지정 리포지토리 코드 통합 가능성
  • JavaConfig 및 사용자 지정 XML 네임스페이스를 통한 간편한 Spring 통합
  • Spring MVC 컨트롤러와의 고급 통합
  • 교차 스토어 지속성에 대한 실험적 지원

 

SpringData와 JpaRepository 원리

  • Repository 는 MarkerInterface 로 특별한 기능은 없음
  • Repository ~ JpaRepository 까지는 @NotRepositoryBean 이 붙어있는 인터페이스이다.
    • JpaRepository<Entity,ID> 붙이면 알맞은 프로그래밍 된 SimpleJpaReository 구현체 빈이 등록된다.
      • 어떻게? @SpringBootApplication 을 통해 자동으로 붙여지는 @EnableJpaRepositories 의 JpaRepositoriesRegistrar 를 통해서 등록된다.
        • JpaRepositoriesRegistrar 는 ImportBeanDefinitionRegistrar 의 구현체이다
        • ImportBeanDefinitionRegistrar 는 프로그래밍을 통해 빈을 주입해준다.
기존 Repository vs 새로운 JpaRepository

기존 Repository
@Repository 을 클래스에 붙인다.
앞서배운 RawJPA의 Repository 기능만 가진 구현체가 생성된다. (DB별 예외처리 등)

새로운 JpaRepository JpaRepository<Entity,ID> 인터페이스를 인터페이스에 extends 붙인다. @NotRepositoryBean 된 상위 인터페이스들의 기능을 포함한 구현체가 프로그래밍된다. (@NotRepositoryBean = 빈생성 막음)
SpringDataJpa 에 의해 엔티티의 CRUD, 페이징, 정렬 기능 메소드들을 가진 빈이 등록된다. (상위 인터페이스들의 기능)

 

실무에서 많이 쓰이는 JpaRepository 팁!

JpaRepository 에서 사용할 메소드 제한하기

1. @RepositoryDefinition 을 인터페이스에 붙이는법 (가장 많이 쓰임)

  • 어노테이션을 붙이면 BeanDefinition 에 직접 접근하여 프로그래밍으로 주입받을 구현체 메소드들을 지정해서 요청할 수 있다.
@RepositoryDefinition(domainClass = Comment.class, idClass = Long.class)
public interface CommentRepository {

    Comment save(Comment comment);

    List<Comment> findAll();
    
}

2. @NoRepositoryBean 인터페이스로 한번더 감싸는법

  • 상위 인터페이스 개념을 하나 더 만들어서 열어줄 메소드만 선언해준다.
@NoRepositoryBean
public interface MyRepository<T, ID extends Serializable> extends Repository<T, ID> {

    <E extends T> E save(E entity);

    List<T> findAll();

}

 

JpaRepository에 기능 추가하기

ex) delete()

메소드의 내부 기능 확인하기

  • delete 호출시 영속성 상태인지 확인한다.
    • 영속성 컨텍스트에 없다면(!em.contains(entity)) 엔티티를 조회해서 영속성 상태로 바꾼다.
      • Cascade, orphanRemoval 에 의한 자식도 삭제가 누락되지 않도록!!!
  • JpaRepository 의 delete() 는 해당 엔티티를 바로 삭제하지 않는다.
    • remove() 메소드를 통해 remove 상태로 바꾼다.
public interface MyRepository {
	...여기다가 추가할 메소드 선언...
}

1. delete 쿼리가 바로 날아가도록 개선

@Repository
@Transactional
public class MyRepositoryImpl implements MyRepository {

	@Autowired
	EntityManager entityManager;


	@Override
	public void delete(User user) {
		entityManager.remove(user);
  }

}

2. findAll 할 때 이름만 가져오도록 개선

@Repository
@Transactional
public class MyRepositoryImpl implements MyRepository {

	@Autowired
	EntityManager entityManager;

	@Override
	public List<String> findNameAll() {
    return entityManager.createQuery("SELECT u.username FROM User AS u", String.class).getResultList();
  }

}

 

 

페이징 처리 프로세스

  1. PageRequest 를 사용하여 Pageable에 페이징 정보를 담아 객체화 한다.
  2. Pageable을 JpaRepository가 상속된 인터페이스의 메서드에 T(Entity)와 함꼐 파라미터로 전달한다.
  3. 2번의 메서드의 return 으로 Page<T>가 응답 된다.
  4. 응답된 Page<T>에 담겨진 Page 정보를 바탕으로 로직을 처리하면 된다.

Pageable 요청/ 응답

요청

org.springframework.data.domain.Pageable

Pageable 만드는법

PageRequest.of(int page, int size) : 0부터 시작하는 페이지 번호와 개수. 정렬이 지정되지 않음
PageRequest.of(int page, int size, Sort sort) : 페이지 번호와 개수, 정렬 관련 정보
PageRequest.of(int page int size, Sort sort, Direction direction, String ... props) : 0부터 시작하는 페이지 번호와 개수, 정렬의 방향과 정렬 기준 필드들

Pageable 메서드

 

pageable.getTotalPages() : 총 페이지 수
pageable.getTotalElements() : 전체 개수
pageable.getNumber() : 현재 페이지 번호
pageable.getSize() : 페이지 당 데이터 개수
pageable.hasnext() : 다음 페이지 존재 여부
pageable.isFirst() : 시작페이지 여부
pageable.getContent(), PageRequest.get() : 실제 컨텐츠를 가지고 오는 메서드. getContext는 List<Entity> 반환, get()은 Stream<Entity> 반환

응답

org.springframework.data.domain.Page

 

Page<T> 타입

  • 게시판 형태의 페이징에서 사용된다.
  • 전체 요소 갯수도 함께 조회한다. (totalElements)

Slice<T> 타입

  • 더보기 형태의 페이징에서 사용된다.
  • 전체 요소 갯수 대신 offset 필드로 조회할 수 있다.
    • 따라서 count 쿼리가 발생되지 않고 limit+1 조회를 한다. (offset 은 성능이 안좋아서 현업에서 안씁니다)

List<T> 타입

  • 전체 목록보기 형태의 페이징에서 사용된다.
  • 기본 타입으로 count 조회가 발생하지 않는다.
  • 가장 많이 쓰임

 

정렬

컬럼 값으로 정렬하기

  • Sort 클래스를 사용한다.
Sort sort1 = Sort.by("name").descending();     // 내림차순
Sort sort2 = Sort.by("password").ascending();  // 오름차순
Sort sortAll = sort1.and(sort2);      // 2개이상 다중정렬도 가능하다
Pageable pageable = PageRequest.of(0, 10, sortAll);  // pageable 생성시 추가

컬럼이 아닌값으로 정렬하기

  • @Query 사용시 Alias(쿼리에서 as 로 지정한 문구) 를 기준으로 정렬할 수 있다.
// 아래와 같이 AS user_password 로 Alias(AS) 를 걸어주면
@Query("SELECT u, u.password AS user_password FROM user u WHERE u.username = ?1")
List<User> findByUsername(String username, Sort sort);
// 이렇게 해당 user_password 를 기준으로 정렬할 수 있다.
List<User> users = findByUsername("user", Sort.by("user_password"));

SQL 함수를 사용해서 정렬하기

  • JpaSort 를 사용해서 쿼리 함수를 기준으로 정렬할 수 있다.
// 아래와 같이 일반적인 쿼리에서
@Query("SELECT u FROM user u WHERE u.username = ?1") // 이건 없어도됨
List<User> findByUsername(String username, Sort sort);
// 이렇게 쿼리함수 LENGTH() 조건을 걸어서 password 문자길이 기준으로 정렬할 수 있다.
List<User> users = findByUsername("user", JpaSort.unsafe("LENGTH(password)"));

 

 

페이징 & 정렬 실무 팁

1. List<T>가 필요하면 응답을 Page<T>로 받지말고 List<T> 로 받아라!

  • 전체 count 쿼리가 추가로 발생하는 Page<T> 보다는 List<T>가 대용량 처리할때 더 안정적이고 빠르다!
//전체셀러 목록 조회
    @Override
    @Transactional(readOnly = true)
    public List<SellerProfileResponseDto> allSellerList(PageDTO pageDTO){

				// 이부분은 List로 받아도됨 (userRepository 도 수정)
        Page<User> usersByUserRole = userRepository
                .findUsersByUserRole(UserRole.SELLER, pageDTO.toPageable());

        List<SellerProfileResponseDto> sellerProfileResponseDtos = new ArrayList<>();

        for(User user:usersByUserRole){
            if(!user.getProfile().getIntroduce().isEmpty()) {
                sellerProfileResponseDtos.add(new SellerProfileResponseDto(user.getUserName(), user.getProfile().getIntroduce()));
            }
        }

        return sellerProfileResponseDtos;

    }

2. Pageable 과 실제 페이지사이의 -1 문제 해결하기

  • JPA 페이지는 0부터인데 화면은 1부터시작하는 문제
    • -1 처리를 중복으로 해줘야하는 이슈
  • PageDTO 를 만들어서 toPageable() 메소드를 사용해보자
public class PageDTO {
  @Positive // 0보다 큰수
  private Integer currentPage;
  private Integer size;
  private String sortBy;

  public Pageable toPageable() {
    return PageRequest.of(currentPage-1, size, Sort.by(sortBy).descending());
  }
}
// UserService 일부
public List<User> findAll(PageDTO pageDTO){
	return userRepository.findUsers(pageDTO.toPageable());
}

3. Pageable 을 GET API의 요청필드로 받아오기

  • Pageable 을 API 요청필드에서 바로 받아올 수 있다.
@GetMapping("/users")
public Page<User> getAllUsers(Pageable pageable) {
    return userRepository.findAll(pageable);
}
  • http://localhost:8080/users?page=0
    • 0번 페이지 부터 20개(default) 조회한다.
  • http://localhost:8080/users?page=0&size=5
    • 0번 페이지부터 5개 조회한다.
  • http://localhost:8080/users?page=0&size=5&sort=id.desc
    • 0번 페이지부터 5개 조회 하는데, id 의 역순으로 조회한다.

 

 

SpringData 쿼리

기능

  • SprintData Common 의 CRUDRepository + PagingAndSortingRepository 이 쿼리기능을 제공

사용 방법

  • 프로그래밍되어 제공되는 쿼리명 규칙
    • 리턴타입 {접두어}{도입부}By{프로퍼티 표현식}(조건식)[(And|Or){프로퍼티 표현식}(조건식)](OrderBy{프로퍼티}Asc|Desc) (매개변수...)
접두어  Find, Get, Query, Count, ...
도입부 Distinct, First(N), Top(N)
프로퍼티 표현식 Person.Address.ZipCode => find(Person)ByAddress_ZipCode(...)
조건식 IgnoreCase, Between, LessThan, GreaterThan, Like, Contains, ...
정렬 조건 OrderBy{프로퍼티}Asc
리턴 타입 E, Optional<E>, List<E>, Page<E>, Slice<E>, Stream<E>
매개변수 Pageable, Sort

 

QueryDSL

기능

  • QueryDSL의 Predicate 인터페이스로 조건문을 여러개를 구성하여 따로 관리할 수 있다.
    • findOne(Predicate), findAll(Predicate) 주로 이 2개 메소드가 사용된다.
      • findOne = Optional<T> 리턴
      • findAll = List<T> | Page<T> | Iterable<T> | Slice<T> 리턴
  • Type Safe 기능
    • 조건문 구성시에 사용되는 객체, 필드 조건이 실제 타입과 일치한지 체크해준다.

장점

  1. 문자가 아닌 코드로 쿼리를 작성함으로써, 컴파일 시점에 문법 오류를 쉽게 확인할 수 있다.
  2. 자동 완성 등 IDE의 도움을 받을 수 있다.
  3. 동적인 쿼리 작성이 편리하다.
  4. 쿼리 작성 시 제약 조건 등을 메서드 추출을 통해 재사용할 수 있다.

원리

  • QueryDSL 의존성을 추가하면 SpringData에 의해 QueryDslPredicateExecutor 인터페이스가 추가된다.
    • QueryDslPredicateExecutor 는 Repository가 QueryDsl 을 실행할 수 있는 인터페이스를 제공하는 역할을 합니다.

 

Spring 3.X 버전부터는 의존성만 추가하면 빌드에 자동으로 포함되서 실행된다

// application.yml

dependencies {
		....

		// 9. QueryDSL 적용을 위한 의존성 (SpringBoot3.0 부터는 jakarta 사용해야함)
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

 

 

QuerydslPredicateExecutor 활용

Join이 필요한 쿼리일 경우 (불가능)

  • 전체 채널에서 내가 멘션된 쓰레드 목록 조회 기능 만들기
    1. Mention 엔티티 생성
    2. User - Mention - Thread 다대다 연관관계 설정
    3. User, Mention 조건으로 Thread 목록 조회 쿼리수행
  • QuerydslPredicateExecutor 로는 Join 연산이 불가능하여 구현 불가능!
    • 멤버 컬렉션까지만 조회 가능하며 이것을 묵시적 조인(1 Depth 자동 조인) 이라고 한다.
      • 즉, channel 의 threads 까지만 접근 가능한것이 묵시적 조인
    • 반면에, Join 연산이 수행되는건 명시적 조인 이라고 한다. (2 Depth 이상 조인)
  • QuerydslPredicateExecutor 의 단점!
    • oin을 수행하지 못한다.
    • 그래서 다음강의때 JPQL 과 JPAQueryFactory 을 배워보자!!!!

Join 이 없는 대신 조건이 많은 쿼리 (가능)

 

해당 채널에서 메세지가 있는 쓰레드 목록 조회 기능 만들기

    • Channel 조건으로 메세지 본문이 있는 Thread 목록 조회 쿼리수행

이처럼 Join 없이 조건이 많이 추가될수록 QuerydslPredicateExecutor 를 활용할 수 있다.

    • 사실 현업에서는 요건 잘 안쓰고 JPAQueryFactory 를 주로 쓴다.

 

'TIL > Spring' 카테고리의 다른 글

23.07.28  (0) 2023.07.28
23.07.27  (0) 2023.07.27
23.07.25  (0) 2023.07.25
23.07.12  (0) 2023.07.12
23.07.11  (0) 2023.07.11