티스토리 뷰
도메인 주도 개발 혹은 MSA로 갈 때 연관 관계를 다루기 위해서는 객체 자체가 아닌 식별자를 저장하곤 한다
예시만 살짝 맛보기로 하기 위해 극단적으로 간단한 엔티티를 만들었다, Panda -> Bear에 대한 간접 참조를 갖는다
@Entity
@NoArgsConstructor
@Getter
public class Panda {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private Long bearId;
@Builder
public Panda(String name, Long bearId) {
this.name = name;
this.bearId = bearId;
}
}
@Entity
@NoArgsConstructor
@Getter
public class Bear {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String type;
@Builder
public Bear(Long id, String type) {
this.id = id;
this.type = type;
}
}
일반적으로는 Panda, Bear에 양방향 연관 관계를 설정하고 Bear가 지워지는 경우
Bear에 속한 Panda들을 cascade 옵션으로 다 지워버리면 된다
다만 연관 관계가 너무 복잡하게 걸려있거나 다른 시스템에서 사용 중이라면 (ex. Panda -> A 시스템, Bear -> B 시스템)
식별자를 들고 있고 그에 따라 요청 / 응답할 수밖에 없다
위와 같은 상황에서 Bear가 제거되는 경우, Panda를 어떻게 지울 수 있을까?
외부 시스템이라면 메세지 큐, 이벤트 큐를 사용하고 같은 시스템 내라면 다음과 같은 방법이 있을 것 같다
1. 도메인 이벤트 (Bear를 제거하는 도메인 메서드에서 이벤트를 발행)
2. 하이버네이트 이벤트 (Bear 엔티티가 제거되는 상황에서 발생)
특정 엔티티에 대한 후속 처리가 필요할 때
처리가 많이 필요하다면 이벤트를 발행하고 이를 처리하는 다양한 구현체들을 만들면 되니 도메인 이벤트가 나을 것 같고
처리가 많이 필요 없다면 하이버네이트의 PostActionEventListener의 하위 인터페이스를 구현하면 된다
PostActionEventListener
- PostInsertEventListener
- PostUpdateEventListener
- PostDeleteEventListener
도메인 이벤트는 손수 발행시켜줘야 한다는 단점이 있지만 @EventListener, @TransactionalEventListener 등의
애노테이션으로 바인딩 시켜 다양한 구현체를 만들 수 있어 복잡하지만 유연한 방식이다
하이버네이트 이벤트로도 다양한 구현체를 만들어 처리할 수 있지만
그렇게 했을 때 단점은 특정 엔티티에 대해서만 반응하는 게 아니라 등록해놓은 수많은 구현체들을 순회한다는 것이다
Delete 이벤트가 발생하면 PostDeleteEventListener를 구현한 구현체들이 모두 반응하기 때문이다
그렇기 때문에 early return 형식으로 작성을 하게 되는데 그럼에도 구현체가 많아진다면 성능 저하가 뒤따를 수밖에 없다
치명적일 정도의 저하는 아니겠지만 그다지 효율적인 방식이 아님은 분명하다
따라서 하이버네이트 이벤트는 반드시 필요한 경우에만 적정 수준의 리스너들을 만드는 것이 좋을 것 같다
아래 코드는 java17로 작성해 낮은 버전에 복사한다면 entity instanceof Bear bear 부분에서 에러가 날 것이다
pattern variable을 지원하지 않는 낮은 버전에서는 entity instanceof Bear로 바꾸고 그 아래에서
Bear bear = (Bear) entity; 로 직접 캐스팅을 해줘야한다
import org.hibernate.event.spi.EventSource;
import org.hibernate.event.spi.PostDeleteEvent;
import org.hibernate.event.spi.PostDeleteEventListener;
import org.hibernate.persister.entity.EntityPersister;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import com.springanything.jpa.Bear;
import com.springanything.jpa.PandaRepository;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Component
public class BearListener implements JpaEventListener, PostDeleteEventListener {
private static PandaRepository pandaRepository;
@Autowired
public void setPandaRepository(PandaRepository pandaRepository) {
BambooListener.pandaRepository = pandaRepository;
}
@Async
@Override
public void onPostDelete(PostDeleteEvent event) {
final Object entity = event.getEntity();
if (!(entity instanceof Bear bear)) {
return;
}
event.getSession().getActionQueue().registerProcess((success, sessionImplementor) -> {
if (success) {
commitOrRollback(sessionImplementor, session -> pandaRepository.deleteByBearId(bear.getId()));
}
});
}
@Override
public boolean requiresPostCommitHanding(EntityPersister persister) {
return true;
}
}
session.getActionQueue(). registerProcess()는 관용구처럼 쓰이는데 이 사이트에서 사용법을 확인해 볼 수 있다
registerProcess는 ActionQueue에서 success와 session을 담아주는데 그 둘은 무엇일까?
인자로는 AfterTransactionCompleteProcess를 받는데 다음과 같은 FunctionInterface다 (Consumer)
success의 설명을 보면 success – Did the transaction complete successfully? True means it did라고 되어있는데
엔티티를 제거한 후의 상황을 처리하는 지금 상황에서는 엔티티 제거가 성공적으로 됐는지를 의미한다
SharedSessionContractImplementor는 계약 인터페이스로 Session, StatelessSession이 지켜야 할 계약을 담고 있다
간단하게 생각해보면 Hibernate의 주요 런타임 인터페이스인 Session을 추상화해놓은 형태로 사용하기 위함이다
더 자세히는 Session은 지켜야 할 명세로 SharedSessionContract가 있고
SharedSessionContract의 하위 인터페이스로 SharedSessionContractImplementor가 있다
즉 SharedSessionContractImplementor로 Session을 다룰 순 없지만 Session의 구현체들은
SessionImplementor를 구현하고 있으므로 SharedSessionContractImplementor로 캐스팅이 가능하다
글로만 설명을 들으면 아리송하니 전체적인 그림을 살펴보자
저기서 Session, SharedSessionContractImplementor가 같은 계층이 아니며
SessionImplementor로 강제하면 SessionImpl을 캐스팅할 수 있다는 점만 알면 된다
여기까지 내부를 여기저기 돌아다니며 살펴봤다
결과적으로 담아주는 건 내부적으로 알아서 할 테니 이전 트랜잭션이 성공했을 때 어떤 동작할지 작성해주면 되는 것이다
이제 commitOrRollback 메서드를 살펴보면 되는데 생각보다 글이 길어져 다음 글로 이어 작성해야겠다
'Spring > Spring Data' 카테고리의 다른 글
[JPA] 간접 참조 연관 관계 EventListener로 삭제하기 - 2 (0) | 2022.10.02 |
---|---|
[Redis] HATEOAS와 @Cacheable - 3 (0) | 2021.12.25 |
[Redis] HATEOAS와 @Cacheable - 2 (0) | 2021.12.22 |
[Redis] LocalDateTime Serialization / Deserialization 삽질기 - 2 (4) | 2021.12.22 |
[Redis] HATEOAS와 @Cacheable 같이 쓰기 (0) | 2021.10.02 |