JPA @Filter 적용 문제와 해결: Hibernate 세션 활용법

2025. 2. 24. 10:02·Framework/Spring

 

 

 

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");
    }
}

튜터님께서 별도의 헬퍼 클래스로 분리하는 방식은 구조적으로 깔끔해 보일 수 있으나, 실무에서 잘 사용되지 않는다고 하였다.

  1. 필터 활성화 코드는 비교적 간단한 코드이기에 별도의 클래스로 분리하면 과도한 추상화가 되어 코드가 산만해 질 수 있다.
  2. 필터 기능은 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 {
	// 기타 메소드	
	...
}

 

 

 

 

728x90
저작자표시 비영리 변경금지 (새창열림)
'Framework/Spring' 카테고리의 다른 글
  • 모든 HTTP 요청 헤더 정보 출력하기
  • 연관관계 매핑의 기본 전략 : 단방향, 양방향, 연관관계 유형 (1:1, 1:N, N:1, N:M)
  • ORM(Object-Relational Mapping) 개요
  • 연관관계 매핑의 개념과 이해 : 정의, 임피던스 불일치, 장점
leonie.
leonie.
  • leonie.
    leveloper
    leonie.
  • 글쓰기 관리
    • 분류 전체보기 N
      • Language
        • Java
      • Git
      • CS
      • CodingTest
        • [프로그래머스] 자바
      • Information
      • Framework
        • Spring
      • DBMS
        • Redis
        • SQL
      • AWS
      • OS
        • Mac
      • 자격증 N
        • 정보처리기사 N
      • 회고
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    알고리즘
    springboot
    Java
    프로그래머스
    정처기
    스프링
    정보처리기사
    자바
    정처기필기
    코딩테스트
  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.3
leonie.
JPA @Filter 적용 문제와 해결: Hibernate 세션 활용법
상단으로

티스토리툴바