포스트

2023-12-09-TIL

2023-12-09-TIL

Today I Learned

Soft Delete and Unique Constraint

Soft Delete를 사용하는데, “삭제되지 않은것들은 하나만 있도록” 강제할 수는 없을까?

soft delete는 행을 지우지 않고 deleted_at이나 deleted 같은 컬럼으로 삭제 표시만 하는 방식이다. 문제는 유니크 제약이다. email에 유니크 제약을 걸면 삭제 표시된 행도 그 값을 계속 차지해서, 같은 이메일로 다시 가입할 수 없다. 원하는 것은 “살아 있는 행 중에서만 유일”이라는 조건이다.

DB마다 방법이 다르다.

  • PostgreSQL: 부분 인덱스(partial index)를 쓴다. PostgreSQL 문서는 WHERE 조건을 만족하는 행 사이에서만 유일성을 강제하고 나머지는 제약하지 않는 부분 유니크 인덱스를 예로 든다. CREATE UNIQUE INDEX ... ON users (email) WHERE deleted_at IS NULL처럼 쓰면 된다.
  • MySQL: 부분 인덱스가 없다. 대신 MySQL 문서대로 UNIQUE 인덱스는 NULL을 허용하는 컬럼에서 NULL 값을 여러 개 허용한다는 점을 이용한다. 살아 있는 행에서는 값을 갖고 삭제된 행에서는 NULL이 되는 생성 컬럼(예: 삭제되지 않았으면 email, 삭제됐으면 NULL)을 만들고 거기에 유니크 인덱스를 건다. (email, deleted_at) 복합 유니크로 푸는 방법도 있지만, 살아 있는 행의 deleted_at이 NULL이면 NULL끼리는 중복으로 보지 않으므로 살아 있는 행이 여러 개 생길 수 있다.

JPA 쪽에서는 Hibernate 6.4부터 @SoftDelete가 들어와 삭제를 표시 컬럼 갱신으로 바꾸고 조회에서 삭제된 행을 자동으로 거른다. 다만 유니크 제약 문제는 애플리케이션이 아니라 스키마에서 풀어야 한다.

  • https://gusiol.medium.com/soft-delete-and-unique-constraint-da94b41cff62
  • https://oraange.tistory.com/26
  • https://www.mastertheboss.com/hibernate-jpa/quickstart-tutorials-hibernate-jpa/hibernate-softdelete-step-by-step-guide/

Event Journaling

이벤트 저널은 상태를 바꾼 동작을 순서대로 쌓아 두는 기록이다. Hazelcast 문서는 맵이나 캐시의 내용을 바꾸는 put, remove 같은 동작의 이력을 저장하는 분산 자료구조로 설명한다. 용량과 만료 시간이 정해진 링버퍼로 구현되어, 오래된 이벤트는 밀려난다. 이 이벤트 흐름을 Hazelcast Jet 같은 스트림 처리기가 읽어 다른 곳에 반영한다.

같은 생각을 애플리케이션 설계 전체로 넓힌 것이 이벤트 소싱이다. 이벤트 소싱 패턴 글은 최신 상태만 저장하는 대신 모든 이벤트를 순서대로 이벤트 저장소에 덧붙이고, 이를 진실의 원천(source of truth)으로 삼아 조회용 DB를 따로 만드는 구조를 설명한다.

링크 중 MySQL에서 MongoDB로 데이터를 동기화하는 글들도 같은 맥락이다. 원본 DB의 변경을 이벤트처럼 흘려 다른 저장소에 반영하는 문제이고, Airbyte나 Tapdata 같은 도구가 이 파이프라인을 제공한다.

  • https://medium.com/design-microservices-architecture-with-patterns/event-sourcing-pattern-in-microservices-architectures-e72bf0fc9274
  • https://docs.hazelcast.com/imdg/4.2/data-structures/event-journal
  • https://stackoverflow.com/questions/23699513/data-synchronization-between-mysql-and-mongodb
  • https://airbyte.com/how-to-sync/mysql-to-mongodb-destination
  • https://www.mongodb.com/community/forums/t/how-can-i-sync-few-tables-from-mysql-db-to-mongodb/111786
  • https://tapdata.github.io/docs/Use%20Cases/ETL/mysql-to-mongodb.html
  • https://hightouch.com/integrations/mysql-to-mongodb

JPA Auditing Trigger

엔티티의 생성·수정 이력을 남기는 방법은 DB 트리거 말고도 애플리케이션 쪽에 여럿 있다. Baeldung 글은 세 가지 접근을 비교한다.

  • JPA 콜백: @PrePersist, @PreUpdate, @PreRemove를 붙인 메서드가 해당 DML 직전에 실행된다. 생성·수정 시각을 채우는 정도에 적당하다.
  • Hibernate Envers: 엔티티의 변경 이력을 별도 감사 테이블에 버전별로 저장한다. 과거 시점의 엔티티 상태를 조회해야 할 때 쓴다.
  • Spring Data JPA Auditing: @EnableJpaAuditing과 AuditingEntityListener를 설정하면 @CreatedDate, @LastModifiedDate, @CreatedBy 같은 필드를 자동으로 채운다.

애플리케이션 쪽 방식은 JPA를 거치는 변경만 잡는다는 한계가 있다. 네이티브 쿼리나 다른 시스템이 직접 DB를 바꾸면 기록되지 않으므로, 모든 변경을 빠짐없이 잡아야 하면 DB 트리거나 binlog 기반 방식이 필요하다.

  • https://www.baeldung.com/database-auditing-jpa

Daily Tech News

Google Testing Blog는 Google이 테스트와 코드 품질에 관한 글을 올리는 블로그다. 짧은 한 편짜리 팁 형식의 “on the Toilet”(TotT) 에피소드가 꾸준히 올라와, 한 번에 하나씩 읽기 좋다.

  • https://testing.googleblog.com/
이 글은 저작권자의 CC BY 4.0 라이선스를 따릅니다.

댓글

아직 댓글이 없습니다