soft delete를 이용하여 사용자 삭제를 진행할 때,
조회 시, 엔티티에 걸어둔 @Filter어노테이션이 작동하지 않는 문제가 있었다.
@Filter(name = "activeUserFilter", condition = "deleted_at is null")
@FilterDef(name = "activeUserFilter")
🔍 문제 원인
Hibernate의 @Filter은 엔티티에 선언되더라도 모든 세션이나 쿼리에 자동 적용되지 않는다.
필터는 세션 단위로 작동한다. 따라서 해당 세션에서 필터를 명시적으로 활성화해야 한다.
엔티티 매니저는 JPA 표준 인터페이스로 데이터 베이스와 상호작용할 수 있다.
하지만 Hibernate는 JPA 구현체로, JPA 표준과, Hibernate 고유 기능도 제공한다.
@Filter는 Hibernate의 고유 기능으로, Hibernate가 제공하는 Session 객체에 접근하여 기능을 사용할 수 있다.
따라서 entityManager.unwrap(Session.class) 를 통해서
JPA EntityManager 내부의 Hibernate Session 객체를 추출하여 사용할 수 있다.
즉, Hibernate 고유 기능을 사용하기 위해 `entityManager.unwrap(Session.class)`를 통해 Hibernate 세션을 얻어야 한다.
서비스 계층에서 코드를 작성하였다.
@Service
@RequiredArgsConstructor
public class UserService {
@PersistenceContext
private EntityManager entityManager;
// 아이디, 닉네임으로 사용자 조회 (단건 + 다건)
public List<UserResponseDto> findUsers(Long id, String nickname) {
Session session = entityManager.unwrap(Session.class);
session.enableFilter("activeUserFilter");
...
}
`@PersistenceContext` 는 JPA 표준 어노테이션이다.
이를 사용하면 특정 구현체에 종속 되지 않고, 표준 방식으로 엔티티 매니저를 주입받아 사용할 수 있다.
즉, 엔티티 매니저의 생성과 관리를 직접 할 필요 없이, 컨테이너가 자동으로 주입해준다.
🤔 고민 사항
여러 도메인에서 필터를 적용해야 했기 때문에,
엔티티 매니저를 주입받아 필터를 활성화하는 로직을 별도의 클래스로 분리하여 주입하는 방식을 시도하였다.
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.hibernate.Session;
import org.springframework.stereotype.Component;
@Component
public class HibernateFilterHelper {
@PersistenceContext
private EntityManager entityManager;
/**
* activeUserFilter 필터를 활성화합니다.
* 해당 필터는 "deleted_at is null" 조건을 적용합니다.
*/
public void enableActiveUserFilter() {
Session session = entityManager.unwrap(Session.class);
session.enableFilter("activeUserFilter");
}
/**
* activeUserFilter 필터를 비활성화합니다.
*/
public void disableActiveUserFilter() {
Session session = entityManager.unwrap(Session.class);
session.disableFilter("activeUserFilter");
}
}
튜터님께서 별도의 헬퍼 클래스로 분리하는 방식은 구조적으로 깔끔해 보일 수 있으나, 실무에서 잘 사용되지 않는다고 하였다.
- 필터 활성화 코드는 비교적 간단한 코드이기에 별도의 클래스로 분리하면 과도한 추상화가 되어 코드가 산만해 질 수 있다.
- 필터 기능은 Hibernate의 고유 기능으로, 각 도메인에서 직접 처리하는 것이 흐름을 이해하고, 유지보수 하기에 용이하다.
💡 추가 개선
Hibernate 세션, 필터 활성화 등의 데이터 접근에 관련된 세부 구현은 리포지토리 계층에서 처리하는 것이 역할 분리에 적합하므로,
서비스 계층에서 리포지토리 계층으로 이동하여 구현하였다.
JPARepository는 기본적인 CRUD 및 간단한 쿼리 기능을 제공하지만, 복잡한 로직을 수행하는 메소드는 제공하지 않는다.
그렇기 때문에 확장을 위해 커스텀 리포지토리를 생성하였다.
1️⃣ 커스텀 리포지토리 인터페이스
package com.example.nbcnewsfeed.user.repository;
import com.example.nbcnewsfeed.user.entity.User;
import java.util.List;
public interface UserRepositoryCustom {
List<User> findUsersWithActiveFilter(Long id, String nickname);
}
2️⃣ 커스텀 리포지토리 구현체
`enableFilter()` 메소드를 사용하여 필터를 활성화 하였다.
`disableFilter()` 메소드를 사용하면 필터를 비활성화 할 수 있다.
package com.example.nbcnewsfeed.user.repository;
import com.example.nbcnewsfeed.user.entity.User;
import org.hibernate.Session;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
@Repository
@Transactional(readOnly = true)
public class UserRepositoryImpl implements UserRepositoryCustom {
@PersistenceContext
private EntityManager entityManager;
@Override
public List<User> findUsersWithActiveFilter(Long id, String nickname) {
// Hibernate Session을 통해 필터 활성화
Session session = entityManager.unwrap(Session.class);
session.enableFilter("activeUserFilter");
// JPQL 쿼리 실행
String jpql = "SELECT u FROM User u WHERE " +
"(:id IS NULL OR u.id = :id) AND " +
"(:nickname IS NULL OR u.nickname LIKE CONCAT('%', :nickname, '%'))";
return entityManager.createQuery(jpql, User.class)
.setParameter("id", id)
.setParameter("nickname", nickname)
.getResultList();
}
}
3️⃣ 기존 리포지토리 인터페이스
userRepositoryCustom 인터페이스를 추가로 상속
package com.example.nbcnewsfeed.user.repository;
import com.example.nbcnewsfeed.user.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Repository;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {
// 기타 메소드
...
}