스케줄 관리 프로젝트 개발 중 엔티티의 논리 삭제(Soft Delete)를 처리하기 위해 @Where 애노테이션을 사용하고 있었습니다.
@SQLDelete(sql = "UPDATE user SET is_deleted = true, deleted_at = now() WHERE id = ?")
@Where(clause = "is_deleted = false")
이 코드는 조회시 삭제되지 않은 데이터만 반환하도록 동작하지만,
커밋 시 경고 문구가 발생하여 일부 버전에서 `@Where`애노테이션 기능이 더이상 권장 되지 않는다는 사실을 알게되었습니다.
해결 방법 - @Filter 사용
`@Filter`와 `@FilterDef`를 사용해 기존 `@Where`를 대체했습니다
(1) 코드 수정 전 (@Where)
@SQLDelete(sql = "UPDATE user SET is_deleted = true, deleted_at = now() WHERE id = ?")
@Where(clause = "is_deleted = false")
public class User extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false, unique = true)
private String email;
@Column(name = "is_deleted")
private Boolean isDeleted = false;
// 생성자
// ...
}
(2) 코드 수정 후 (@Filter)
@FilterDef(name = "deletedFilter", parameters = @ParamDef(name = "isDeleted", type = Boolean.class))
@Filter(name = "deletedFilter", condition = "is_deleted = :isDeleted")
public class User extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false, unique = true)
private String email;
@Column(name = "is_deleted")
private Boolean isDeleted = false;
// 생성자
// ...
}
`@FilterDef`
필터의 메타데이터를 정의하는데 사용된다.
`name` 필터의 고유 이름을 지정하며, 이를 통해 코드에서 이 필드를 참조할 수 있다.
`parameters` 필터가 사용할 파라미터를 정의하며, 각 파라미터는 `@ParamDef`로 지정하고 `name`과 `type` 속성을 가진다.
- `name = "deletedFilter"`
필터의 이름은 deletedFilter로 정의하며, 이를통해 코드에서 이 필터를 참조할 수 있다. - `parameters = @ParamDef(name ="isDeleted", type = Boolean.class)`
필터는 isDeleted이름의 파라미터를 사용하며, 이 파라미터의 타입은 Boolean 타입이다.
`@Filter`
필터의 조건을 정의하는데 사용된다.
데이터베이스 쿼리에 동적으로 추가되며, 이는 condition 속성에 의해 결정된다.
- `name = deletedFilter`
이 필터가 deltedFilter라는 이름으로 참조된다는 것을 의미한다. - `condition = "is_deleted = :isDeleted"`
필터가 적용될 조건을 정의하며, 여기선 엔티티의 `is_deleted`필드가 `isDeleted`파라미터와 일치하는 경우에만 데이터를 조회한다.
필터 활성화 및 적용
@Filter는 자동으로 적용되지 않기 때문에, Hibernate Session을 통해 수동으로 필터를 활성화해야 합니다.
EntityManager 를 사용해 Session을 가져와 필터를 활성화합니다.
(3)Service 계층
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final EntityManager entityManager; // EntityManager 주입
@Transactional
public List<UserResponseDto> findUsers(Long id, String username, String email) {
// Hibernate Session 가져오기
Session session = entityManager.unwrap(Session.class);
// 필터 활성화 (삭제되지 않은 사용자만 조회)
session.enableFilter("deletedFilter").setParameter("isDeleted", false);
List<User> userList = userRepository.findUsers(
id,
sanitizeString(username),
sanitizeString(email)
);
if (userList.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
return userList.stream().map(UserResponseDto::toDto).toList();
}
}
Hibernate Session 사용 이유
Hibernate Session은 동적 필터링 기능을 제공하며,
이를 통해 필터 조건을 런타임에 동적으로 변경할 수 있습니다.
`EntityManager.unwrap(Session.class)`
Spring Data JPA는 기본적으로 EntityManager를 사용하지만,
Hibernate의 고유 기능(Session)을 사용하기 위해 이를 unwrap 메서드를 통해 변환합니다.
`enableFilter`메소드로 Session에서 필터 활성화
deletedFilter가 `isDeleted = false`로 설정되며, 삭제되지 않은 데이터만 조회됩니다.
필터 활성화 시 파라미터 설정
`setParameter("isDeleted",false)`로, `:isDeleted` 값에 false를 전달한다.
위의 코드로 인해,
필터가 활성화되면 Hibernate는 SQL 쿼리 생성 시 `is_deleted = false` 조건을 자동으로 추가합니다.
값이 true로 바뀌면 조건이 `is_deleted = true`로 변경됩니다.
이번 트러블 슈팅을 통해 Hibernate에서 권장하는 방식(`@Filter`)으로 전환할 수 있었고,
동적 필터링의 유연성과 Hibernate Session의 적용을 구현해 볼 수 있어 새로웠습니다.
개발 중 자주 사용하는 애노테이션이나 기능이 Deprecated될 수 있다는 점을 항상 염두에 두는 것이 좋고,
경고 메시지를 무시하지 않는 습관이 중요하겠다라는 것을 알게되었습니다.
또한, 권장 방식으로 리팩토링하면서 코드의 확장성과 유지보수성이 더욱 개선되었습니다.
1️⃣ 동적 필터링 지원으로 유연한 조건 적용
`@Where`는 고정된 조건만 사용할 수 있지만, `@Filter`는 런타임에 조건을 변경할 수 있습니다.
따라서, 비즈니스 로직의 요구사항 변화에 쉽게 대응할 수 있습니다.
2️⃣ 중복 코드 감소 및 코드 재사용성 향상
`@Filter`는 Session 레벨에서 여러 엔티티에 공통 적용할 수 있습니다.
같은 필터 조건을 재사용할 수 있기 때문에 코드 중복이 줄어들고 유지보수가 용이합니다.
앞으로도 Hibernate 공식 문서 및 최신 정보를 꾸준히 확인하며
더 안정적이고 효율적인 코드를 작성하는 개발자가 되어야겠다는 다짐을 하게 되었습니다.