몬그로이

Spring Data JPA 본문

Organizing Docs/Java Docs

Spring Data JPA

Mon Groy 2024. 6. 28. 20:00

Cascade (영속성 전이)

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

 

orphanRemoval (고아 객체 제거)

 

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

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 : 지연 로딩 (자식은 필요할때 따로 조회)

Raw JPA 작동 방식

persist(), merge() > (영속성 컨텍스트에 저장된 상태) > flush() > (DB에 쿼리가 전송된 상태)

> commit() > (DB에 쿼리가 반영된 상태)

 

 

Raw JPA 작동 방식으로 인해

SpringDataJpa 사용시 고려할 점

*SpringDataJpa 는 Raw JPA를 추상화한 것

 

1. @Transactional

메서드1 : save()

  • persist() 까지 수행하고 (업데이트일 경우 merge() )
  • save() 메소드에 달린 내부적인 @Transactional 어노테이션에 의해 flush, commit 까지 수행됩니다.

메서드2 : saveAndFlush()

  • persist(), flush() 까지 수행하고
  • saveAndFlush() 메소드에 달린 내부적인 @Transactional 어노테이션에 의해 commit() 까지 수행됩니다.

이는 Repository 기본 구현체인 SimpleRepository 클래스의 save 메소드에

@Transactional 어노테이션이 달려있기 때문이므로

flush 와 commit 사이 즉, DB에 쿼리가 전송된 상태에서 추가동작이 필요한경우에는 

SimpleRepository 대신 Repository 를 개별로 구현하여

@Transactional 이 달리지 않은 saveAndFlush() 를 구현할 것

 

 

2. 지연 로딩(LAZY)를 우선적으로 사용한다.

  • N+1 에러 방지를 위해 지연 로딩 (LAZY) 모드로 사용을 하고
  • 성능 최적화가 필요한 부분에서는 Fetch 조인을 사용한다.
더보기

N+1 문제란?
특정 엔티티를 조회할 때 연관된 엔티티를 추가로 조회하면서 발생하는 비효율적인 쿼리가 실행되는 것

10명의 Member 엔티티가 데이터베이스에 있다고 가정

첫 번째 쿼리로 10명의 Member 엔티티를 가져옴 (LAZY 설정으로 인해 team 정보 가져오지 않음)

for 문 등으로 따로 각 member에 대한 team 을 조회하려고 한다면

10명의 Member 엔티티에 대해 각각의 Team 엔티티를 조회하기 위해 10번의 쿼리가 추가로 실행되어야만 함

따라서, 총 1 + 10 = 11개의 쿼리가 실행

 

따라서 LAZY 가 설정되어 있을때 각 member 에 대한 팀 조회까지 필요한 경우 fetchJoin을 이용할 것

3. OneToMany 매핑은 양방향으로 해준다.

  • 단방향으로 매핑하면 자식 엔티티가 관리하는 외래 키가 다른 테이블에 있음
    => 작업한 Entity가 아닌 다른 Entity에서 쿼리문이 나가는 경우가 있어 헷갈림
    => 양방향으로 하면 외래 키가 같은 테이블에 생김
  • 단방향으로 매핑하면 불필요한 쿼리문이 발생(update 등)
    => 양방향으로 하면 연관관계 정보를 통해 한번에 쿼리 수행

4. save() 이후에는 반환된 인스턴스를 사용해야 한다.

  • save 할때 넘겨준 객체는 save 이후부터는 EntityManager가 관리하고 있는 대상이 아니기 때문에(준영속성 상태)
    이 객체의 값을 바꿔봐야 쿼리에 영향을 미치지 않는다.
  • save 메소드 응답값으로 오는 객체가 영속성 상태의 객체이니 이것을 사용해야 한다. 

5. 테스트 코드에서 @Transactional 을 사용할때는 주의해야한다.

  • 실제 런타임시에는 @Transactional 이 적용되지 않아서 테스트와 다른 환경에서 다른 동작이 발생할 수 있음
  • 따라서, @Transactional 이 테스트 대상 환경일 경우만 적용하여 사용한다.

JpaRepository 쿼리 기능

 

1. Repository ~ JpaRepository 까지는 @NotRepositoryBean 이 붙어있는 인터페이스

2. JpaRepository<Entity,ID> 붙이면 알맞은 프로그래밍 된 SimpleJpaReository 구현체 빈이 등록된다.

*@EnableJpaRepositories 의 JpaRepositoriesRegistrar 를 통해서 빈이 등록된다.

(@EnableJpaRepositories  는 @SpringBootApplication 을 통해 자동으로 붙는 어노테이션)
(JpaRepositoriesRegistrar 는 ImportBeanDefinitionRegistrar 의 구현체)
(ImportBeanDefinitionRegistrar 는 프로그래밍을 통해 빈을 주입해줌)


ImportBeanDefinitionRegistrar 를 사용해 직접 프로그래밍 해보기

// MyRepository.java

 

@Setter

public class MyRepository {

 

private HashMap<Long, String> dataTable; // DB 테이블을 의미

 

public String find(Long id) {

return dataTable.getOrDefault(id, "");

}

 

public Long save(String data) {

var newId = Long.valueOf(dataTable.size());

this.dataTable.put(newId, data);

return newId;

}

}

 

// MyRepositoryRegistrar.java

 

public class MyRepositoryRegistrar implements ImportBeanDefinitionRegistrar {

 

@Override

public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,

BeanDefinitionRegistry registry) {

 

// 주입할 빈에 대해 프로그래밍 하는 부분!!

GenericBeanDefinition beanDefinition = new GenericBeanDefinition();

beanDefinition.setBeanClass(MyRepository.class);

beanDefinition.getPropertyValues().add("dataTable", Map.of(1L, "data"));

// 여기까지!

 

registry.registerBeanDefinition("myRepository", beanDefinition);

}

}

 

// TestApplication.java

 

@Import(MyRepositoryRegistrar.class) // 주입!

@SpringBootTest

public class MyRepositoryTest {

 

@Autowired

MyRepository myRepository;

 

@Test

void myRepositoryTest() {

// given

var newData = "NEW DATA";

var savedId = myRepository.save(newData);

 

// when

var newDataList = myRepository.find(savedId);

 

// then

System.out.println(newDataList);

}

}


JpaRepository 쿼리 사용법

규칙

리턴타입 {접두어}{도입부}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|Desc
리턴 타입 E, Optional<E>, List<E>, Slice<E>, Stream<E>
매개변수 Pageable, Sort

 

// 페이징

Page<User> findByName(String name, Pageable pageable); // Page 카운트쿼리 수행됨

Slice<User> findByName(String name, Pageable pageable); // Slice 카운트쿼리 수행안됨

List<User> findByName(String name, Sort sort);

List<User> findByName(String name, Pageable pageable);

 

Pageable vs. Sorting

 

Pageable

  • Pageable 인터페이스를 구현한 PageRequest 객체를 만들거나 얻습니다.
  • PageRequest 객체를 repository 메소드에 우리가 의도한대로 인자로 전달합니다.
  • PageRequest 객체는 요청된 페이지 숫자와 페이지 사이즈를 넘김으로서 만듭니다.
    (페이지 숫자는 0부터 시작)

// 페이지 (페이지 사이즈 = 2)

Pageable firstPageWithTwoElements = PageRequest.of(0, 2);

// 두번째 페이지 (페이지 사이즈 = 5)

Pageable secondPageWithFiveElements = PageRequest.of(1, 5);

 

// 페이지 사용

List<Product> allTenDollarProductsproductRepository.findAllByPrice(10, secondPageWithFiveElements);

 

  • findAll(Pageable pageable) 메소드는 기본적으로 Page 객체를 리턴합니다.
  • 그러나, 우리는 또한 커스텀 메소드를 통해 페이지네이션된 데이터를
    Page , Slice 또는 List 의 타입으로 받을 수 있습니다.
  • Page 인스턴스는 Product 의 목록 뿐 아니라 페이징할 수 있는 전체 목록의 숫자도 알고 있습니다.
    이를 실행하기 위해 추가적으로 쿼리 작업이 들어갑니다.
  • 이러한 작업에 대한 비용을 방지하기 위해, 우리는 대신 Slice나 List로 반환 받을 수 있습니다.
    Slice 는 단지 다음 slice가 가능한지 아닌지만 알고 있습니다.

Sorting

  • 유사하게, 우리의 쿼리 결과를 정렬하기 위해선 Sort 객체를 메소드에 전달하면 됩니다.
  • 만약, 정렬과 페이지네이션을 둘다 하고 싶다면
    정렬에 대한 디테일 정보를 PageRequest 객체에 전달하면 됩니다.
  • 페이징 없이 정렬만 하려는 경우
    위에 언급한 findAll(Sort sort) 메서드와 같이 Sort 객체만 파라미터로하는 메서드를 작성하면 된다.

Pageable sortedByName = PageRequest.of(0, 3, Sort.by("name"));

Pageable sortedByPriceDesc = PageRequest.of(0, 3, Sort.by("price").descending());

Pageable sortedByPriceDescNameAsc = PageRequest.of(0, 5, Sort.by("price").descending().and(Sort.by("name")));

 

Page<Product> allProductsSortedByName = productRepository.findAll(Sort.by("name").accending());

 


JpaRepository 효율적 사용법

 

Optional 제거하기

Spring Data JPA의 findByXX 메서드는 기본적으로 Optional을 반환한다. 이로 인해 비즈니스 로직에서 Optional 처리를 위한 추가적인 작업이 필요하게 되는데, 이럴 때 default 메서드를 활용하면 이 문제를 우아하게 해결할 수 있다.

public interface UserRepository extends JpaRepository<User, Long> {

// Default 메소드를 사용하여 findById의 Optional을 내부적으로 처리

default User findUserById(Long id) {

return findById(id).orElseThrow(() -> new DataNotFoundException("User not found with id: " + id));

}

}

 

메서드명 간소화하기

Spring Data JPA를 사용하다 보면 복잡한 쿼리 때문에 메서드명이 길어져 가독성을 해치는 경우가 있다. 이럴 때도 default 메서드를 활용하면 긴 메서드명을 간결하고 명확하게 표현할 수 있다.

public interface ProductRepository extends JpaRepository<Product, Long> {

// 기존의 쿼리 메소드

List<Product> findAllByCategoryAndPriceGreaterThanEqualAndPriceLessThanEqualOrderByPriceAsc(String category, BigDecimal minPrice, BigDecimal maxPrice);

 

// Default 메소드를 사용하여 간결한 메소드명 제공

default List<Product> findProductsByCategoryAndPriceRange(String category, BigDecimal minPrice, BigDecimal maxPrice) {

return findAllByCategoryAndPriceGreaterThanEqualAndPriceLessThanEqualOrderByPriceAsc(category, minPrice, maxPrice);

}

}

 

비즈니스 로직 통합

여러 기본 제공 메서드를 하나의 고차 작업으로 결합할 수도 있다. 다만 Spring Data JPA의 Repository는 Data Access Layer의 일부로, 데이터베이스와의 상호작용만을 담당하는 것이 일반적이기 때문에 이 부분은 서비스 레이어에서 처리하는 것이 일반적이다.

public interface UserRepository extends JpaRepository<User, Long> {

 

// 사용자 ID로 사용자를 찾고, 존재할 경우 연락처 정보를 업데이트하는 메소드

default void updateUserContact(Long userId, String newContact) {

findById(userId).ifPresent(user -> {

user.setContact(newContact);

save(user);

});

}

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'Organizing Docs > Java Docs' 카테고리의 다른 글

JPQL - 복잡한 쿼리를 수동으로 작성하고 실행하는 방법  (0) 2024.06.30
ORM  (0) 2024.06.28
페이징  (0) 2024.06.28
MyBatis  (0) 2024.06.27
데이터베이스Driver >> JDBCTemplate  (0) 2024.06.26