[JPA] @Where로 기본 조건을 설정했을 때 장단점
@Where
을 사용해서 엔티티에 적용해주면 해당 엔티티를 조회할 때마다 기본 조건을 추가해줄 수 있다.
(참고로 hibernate 6.3부터 @Where
는 deprecated 됐고 @SQLRestriction
를 사용할 수 있다.
하지만 이 글에서는 @Where
로 설명한다.)
예를 들면 삭제되지 않은 User를 조회하고 싶다면 아래처럼 적용해주면 된다.
@Entity
@Table(name = "users")
@Where(clause = "is_deleted = 0") // 적용
class User(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
val name: String,
)
- User 조회하는 쿼리에는 where절에
is_deleted = 0
을 추가해주지 않아도 알아서 적용된다.
User 조회하는 모든 쿼리에 is_deleted = 0
를 추가해주지 않아도 된다는건 큰 장점이다.
실수로 조건을 누락해서 삭제된 User 데이터를 User 객체로 불러오는 경우가 있다면 치명적일 수 있기 때문이다.
일반적으로 비즈니스상 유의미한 조건을 @Where
에 적용하므로 조심해야한다.
하지만 장점만큼 단점도 명확하다. 반대로 @Where
에 명시된 조건을 일부 케이스에서 적용하지 않아야하는 상황이 있다고 해보자.
즉, 삭제된 User도 조회해야하는 상황이 생겼다고 해보자.
그러면 @Where
를 걷어낸다면 User를 사용하는 쿼리 중에서 is_deleted = 0
를 사용해야하는 곳만 찾아서 적용해야한다.
굉장히 번거롭다.
물론 @Where
를 걷어내지 않고 해결하는 방법도 있다.
nativeQuery 사용하기
네이티브 쿼리는 지정해준 쿼리 그대로 날아가기 때문에 @Where
처럼 JPA를 통해서 적용되는 것들을 모두 무시한다.
기존 로직은 수정하고 싶지 않은데 일부에만 적용하고 싶다면 적용하기 좋은 방법이라고 생각한다.
하지만 적용할 곳이 너무 많거나, 새로운 쿼리를 많이 작성해야한다면 유지보수하기 어려운 코드가 될 수 있다.
(JPA를 쓰면 유지보수가 쉽다는 말은 아니다. 생짜 쿼리를 작성했을 때 오는 단점을 말한 것이다.)
interface UserRepository : JpaRepository<User, Long> {
@Query(value = "SELECT * FROM users WHERE name = :name", nativeQuery = true)
fun findByName(name: String): List<User>
}
새 엔티티 만들기
JPA는 쓰고싶고 네이티브 쿼리는 쓰기 싫다면 새 엔티티를 만들어도 된다.
위의 User
에서 @Where만 제거한 AllUser
를 만들었다. repository에서 AllUser
를 조회하면 삭제된 User도 조회할 수 있다.
User 엔티티와 어떤 관계를 맺고있지 않기 때문에 만약 기존 로직에서 사용돼야 한다면 그 로직도 한벌 더 만들어줘야 한다.
네이티브 쿼리에서 설명한 것처럼 사용하는 곳이 많이 제한적일 때 사용하는게 좋다.
@Entity
@Table(name = "users")
class AllUser(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
val name: String,
)
엔티티 구조 분리하기
삭제된 User도 기존 로직에서 활용을 해야한다면, 로직의 수정을 최소화하고 싶을 것이다.
그러면 interface로 User를 만들고 구현체를 만들어주면 된다.
interface User {
val id: Long
val name: String
}
@Entity
@Table(name = "users")
@Where(clause = "is_deleted = 0")
class ActiveUser(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long = 0,
override val name: String,
) : User
@Entity
@Table(name = "users")
class AllUser(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long = 0,
override val name: String,
) : User
- User는 구현체가 갖는 필드를 모두 사용할 수 있도록 정의해야 한다.
- interface를
User
를 만들고 구현체로ActiveUser
,AllUser
를 갖는다. ActiveUser
는 삭제되지 않은 User만,AllUser
는 모든 User가 들어올 수 있다.
물론 구현체별로 repository는 분리해서 사용해야한다.
기존 로직에서는 interface인 User
를 사용하면 된다.
하지만 이 방법은 엔티티 구조가 복잡해진다는 단점이 있다. 왜 이렇게 분리됐는지에 대한 설명을 남겨둬야 할 필요가 있다.
interface에서 모든 구현체가 갖는 필드를 접근할 수 있도록 모두 정의해줘야 한다는건 적어도 구현체에서 갖는 모든 필드는 오버라이드
돼야 한다는 점도 있다.
각 방법은 모두 장단점이 있으므로 상황에 맞게 사용하면 되겠다.
댓글남기기