프로젝트 진행 중, 엔티티 특정 필드의 상태가 'N'인 모든 데이터가 조회되지 않도록 일괄 적용할 방법이 필요하여 @Where를 적용하게 되었고 사용 방법 및 적용 이후 발생한 문제와 다양한 테스트 경험에 대해 공유하고자 한다.
- 사용 방법
@Where의 사용방법은 굉장히 단순하다.
엔티티 클래스에 org.hibernate.annotations 패키지의 @Where를 클래스 단위에 작성하면 된다.
(해당 어노테이션은 클래스뿐 아니라 메서드, 필드 단위도 설정은 가능하지만 테스트를 진행하지는 않았다.)
만약, 입양되지 않은 상태의 시바견 데이터는 항상 조회 조건에 포함이 안되었으면 좋겠다.라고 한다면
is_adopted 필드의 상태가 'N'이 아닌 데이터만 조회할 수 있도록 설정한다.
@Where(clause = "is_adopted <> 'N'")
- SQL
- Shiba
@Entity
@Table(name = "shiba")
@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Where(clause = "is_adopted <> 'N'")
public class Shiba extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer age;
@Enumerated(EnumType.STRING)
private Adopt isAdopted;
public Shiba(String name, Integer age, Adopt isAdopted) {
this.name = name;
this.age = age;
this.isAdopted = isAdopted;
}
public void updateAge(Integer age) {
this.age = age;
}
@OneToOne(mappedBy = "shiba")
private House house;
}
[TEST]
@Test
@DisplayName("입양되지 않은 시바견은 자동으로 확인되지 않는다.")
@Transactional
void findAllByOnlyAdoptedShiba() {
List<Shiba> shibaList = shibaRepository.findAll();
boolean isPresent = shibaList.stream()
.filter(shiba -> shiba.getIsAdopted() == Adopt.N)
.findAny()
.isPresent();
assertEquals(false, isPresent);
}
JPA에서 기본으로 제공하는 findAll 메서드를 사용했는데 자동으로 is_adopted 상태가 'N'인 시바견은 검색되지 않도록 조회 조건이 추가되었다.
- 문제점 1. 특정 조건이 필요한 경우에 접근하기 불편하다.
이처럼 자동으로 특정 조건이 추가되면 다른 개발자가 해당 엔티티를 다루게 될 때 특정 조건 존재 여부( ex) 입양 여부가 'N'인 시바견만 조회한다.)를 알지 못하고 개발해도 자동으로 추가되어 실수를 줄이는 분명한 장점이 있다.
하지만, 특정 조건이 필요한 경우가 간혹 생길 수 있다. 필자의 경우는 특정 필드가 'N'인 상태의 데이터를 삭제하는 배치를 개발해야 했는데 이때 자동 조건 추가로 인해 해당 데이터에 접근할 수 없었다.
- 해결방안 = 네이티브 쿼리(nativeQuery)를 사용한다.
해당 문제점은 nativeQuery를 사용하여 접근하면 해결할 수 있다.
nativeQuery를 사용하는 방법은 단순하다.
@Query에 nativeQuery 조건을 true로 추가하고 value에 순수 SQL 문을 직접 작성하면 된다. (JPQL 아님)
- ShibaRepository
@Query(value = "select * from shiba", nativeQuery = true)
List<Shiba> findAllByNQ();
[TEST]
@Test
@DisplayName("Native Query를 통해 입양되지 않은 시바견 데이터에 접근한다.")
@Transactional
void findAllShibaByNativeQuery() {
List<Shiba> shibaList = shibaRepository.findAllByNQ();
boolean isPresent = shibaList.stream()
.filter(shiba -> shiba.getIsAdopted() == Adopt.N)
.findAny()
.isPresent();
assertEquals(true, isPresent);
}
이처럼 네이티브 쿼리를 사용하면, @Where에 설정된 조건을 무시하고 정상적으로 원하는 데이터를 가져올 수 있다.
하지만 직접 SQL문을 작성하는 불편함을 감수해야 한다.
- 문제점 2. 연관 관계에서 접근할 때 예외가 발생할 수 있다.
@Where를 설정한 곳에서 조회 조건을 추가하면 잘 작동하는 것은 알겠다!
그런데 연관 관계를 가진 외부 엔티티에서 해당 엔티티를 접근할 때도 자동으로 조회 조건이 추가가 될까..?
- House
@Entity
@Table(name = "shiba_house")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class House {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Enumerated(EnumType.STRING)
private Color color;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "shiba_id")
private Shiba shiba;
public House(String name, Color color, Shiba shiba) {
this.name = name;
this.color = color;
this.shiba = shiba;
}
@Override
public String toString() {
return "House{" +
"id=" + this.id +
", name='" + this.name + '\'' +
", color=" + this.color +
", shiba=" + ShibaDto.entityToDto(this.shiba) +
'}';
}
Shiba 엔티티와 1:1 연관 관계를 가진 House 엔티티를 추가했다.
- SQL
[TEST]
@Test
@DisplayName("연관관계를 통해 입양되지 않은 시바견 데이터에 접근한다.")
@Transactional
void findNotAdoptedShibaByMappedHouse() {
List<House> houseList = houseRepository.findAll();
boolean isPresent = houseList.stream()
.filter(house -> house.getShiba().getIsAdopted() == Adopt.N)
.findAny()
.isPresent();
assertEquals(true, isPresent);
}
테스트 결과, 해당 시바견 데이터를 찾을 수 없어서 EntityNotFoundException 예외가 발생하게 된다.
해당 예외가 발생한 이유는 지연 로딩을 통해 접근하는 데이터의 조회 조건에 자동으로 설정한 조건(is_adopted <> 'N')이 추가되었기 때문이다.
1. shiba_house 테이블에서 외래키(shiba_id)로 연결된 데이터를 조회하려고 시도하는데
2. 추가로 is_adopted가 'N'인 데이터는 조회하지 않는다는 조건이 붙어서 결국은 가져오지 못하게 되었고
3. 존재하지 않는 데이터에서 메서드를 호출하려고 시도했기 때문에 예외가 발생하게 된 것이다.
다른 개발자 입장에서는 데이터를 실제로 가져오지 못할 수도 있으니, 해당 엔티티에 접근해서 데이터를 사용하는 게 불안해지게 된다.
참고로 지연 로딩(LAZY)이라면 해당 엔티티를 접근하지 않는 경우 예외가 발생하지 않지만, 즉시 로딩(EAGER)이면 바로 예외가 발생한다.
- 해결 방안 = 해당 엔티티에 접근하는 모든 외부 엔티티에 페치 조인을 걸자
해결 방안은 의외로 간단하다. @Where 조건이 있는 엔티티에 접근하는 외부 엔티티에 페치 조인(fetch join)을 추가하여 데이터를 가져오면 된다. 페치 조인을 거는 방법은 @EntityGraph를 추가하거나 @Query JPQL 등을 통해서 추가하면 된다.
- HouseRepository
@EntityGraph(attributePaths = {"shiba"})
List<House> findAll();
페치 조인을 사용하면 동일한 테스트에서 무난하게 성공을 받을 수 있다.
하지만 단순히 페치 조인만 거는 것은 진짜로 문제를 해결했다고 볼 수 없다. 상단의 이미지와 같이 @Where 조건에 해당하지 않는 데이터도 모두 가지고 오기 때문에 결국 isAdopted가 N인 데이터도 가져오게 되기 때문이다.
그러므로 엔티티에 명시된 @Where 조건도 확인하고 조건에 추가해야 한다.
- HouseRepository
@Query("select h from House h join fetch h.shiba hs where hs.isAdopted <> :isAdopted")
List<House> findAllWithShiba(@Param("isAdopted") Adopt isAdopted);
[TEST]
@Test
@DisplayName("페치조인을 활용해서 입양되지 않은 시바견 데이터에 접근한다.")
@Transactional
void findNotAdoptedShibaByJPQL() {
List<House> houseList = houseRepository.findAllWithShiba(Adopt.N);
boolean isPresent = houseList.stream()
.filter(house -> house.getShiba().getIsAdopted() == Adopt.N)
.findAny()
.isPresent();
assertEquals(false, isPresent);
}
이처럼 연관관계가 있으며 해당 엔티티에 접근할 필요가 있는 외부 엔티티는 반드시 fetch join + where 조건 처리를 해주어야 한다.
- 결론
@Where는 특정 조건을 자동으로 추가해 주므로 분명히 편리하고 생산성을 높인다는 확실한 장점이 있다.
하지만 단점 또한 명확하다.
연관관계를 갖는 경우는 불안요소를 제거하기 위해 해당 엔티티에 접근하는 모든 외부 엔티티에서 페치 조인 및 @Where에 해당하는 조건을 추가해야 한다. 자주 사용되지 않는 데이터라면 괜찮을 수도 있겠지만, 많은 곳에서 접근하는 데이터라면 굉장히 크리티컬 할 것이다.
또한, 나중에 다른 개발자가 해당 필수 조회 조건에 대한 정보를 공유받지 못했을 때 충분히 실수할 수 있는 여지를 남길 수 있다.
그러므로 사용하기 전에 사용하는 쪽과 사용하지 않는 쪽 어느 쪽이 더 생산성에 유리할지 충분히 고민한 다음 추가하도록 하자. 그리고 반드시 @Where를 사용했을 때는 팀원들에게 사용 이유를 공유하고 주석을 남기자!
'Spring > JPA' 카테고리의 다른 글
Spring Boot 3.0 QueryDsl Maven 설정하기 (0) | 2023.04.09 |
---|---|
JPA 동일 트랜잭션에서 update와 insert 동시에 수행할 때 문제 해결하기 (쿼리 실행 순서 문제) (0) | 2023.02.05 |
JPA @Query @Modifying 벌크 삭제 연산시 연관관계 문제 해결하기 (with SQLIntegrityConstraintViolationException) (0) | 2023.01.22 |
JPA @Query @Modifying 벌크 연산시 자동 업데이트(Auditing) 주의사항 (0) | 2023.01.15 |
JPA, ORM 그리고 패러다임의 불일치 (0) | 2022.07.24 |
댓글