TL;DR

  • Hibernate와 Soft Delete의 핵심 개념과 사용 범위를 한눈에 정리
  • 등장 배경과 필요한 이유를 짚고 실무 적용 포인트를 연결
  • 주요 특징과 체크리스트를 빠르게 확인

1. 개념

이 문서에서는 Java/Kotlin 진영의 대표적인 ORM 프레임워크인 Hibernate의 탄생 배경과 핵심 개념, 그리고 데이터 삭제 전략인 Soft Delete의 필요성과 구현 방법을 상세히 설명합니다.

2. 배경

2000년대 초반 Java 개발자들은 심각한 문제에 직면했습니다.

3. 이유

이 주제를 이해하고 적용해야 하는 이유를 정리한다.

4. 특징

  • Hibernate란 무엇인가?
  • Soft Delete란?
  • Hibernate의 Soft Delete 지원
  • Soft Delete 구현 시 주의사항
  • Soft Delete의 단점과 대안

5. 상세 내용

작성일: 2026-02-02 키워드: Hibernate, ORM, JPA, Soft Delete, Hard Delete, @SQLDelete, @SQLRestriction, @SoftDelete, 논리적 삭제, Gavin King


개요

이 문서에서는 Java/Kotlin 진영의 대표적인 ORM 프레임워크인 Hibernate의 탄생 배경과 핵심 개념, 그리고 데이터 삭제 전략인 Soft Delete의 필요성과 구현 방법을 상세히 설명합니다.


1. Hibernate란 무엇인가?

1.1 탄생 배경: 객체-관계 불일치 문제

2000년대 초반 Java 개발자들은 심각한 문제에 직면했습니다.

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Object-Relational Impedance Mismatch                     │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  📚 당시 상황:                                                               │
│     - Java는 객체지향 언어 (클래스, 상속, 다형성, 참조)                     │
│     - 데이터베이스는 관계형 (테이블, 행, 열, 외래키)                        │
│     - 둘 사이에 근본적인 패러다임 불일치 존재                               │
│                                                                             │
│  이것을 "Object-Relational Impedance Mismatch"라고 부름                      │
│  (객체-관계 임피던스 불일치)                                                │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

불일치 문제의 구체적 예시

1️⃣ 상속 표현 문제

┌─────────────────────────────────────────────────────────────────────────────┐
│                    상속을 DB에서 어떻게 표현할까?                            │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  Java 클래스 구조:                 DB 테이블은???                           │
│                                                                             │
│  ┌──────────────┐                 방법 1: 단일 테이블 (Single Table)        │
│  │   Animal     │                 → null 값이 많아짐                        │
│  │  - name      │                                                           │
│  │  - age       │                 방법 2: 조인 전략 (Joined)                │
│  └──────┬───────┘                 → 조회마다 조인 필요 (성능 이슈)          │
│         │                                                                   │
│    ┌────┴────┐                    방법 3: 클래스별 테이블 (Table Per Class) │
│    │         │                    → 다형성 쿼리 어려움                      │
│  ┌─┴──┐   ┌──┴─┐                                                            │
│  │Dog │   │Cat │                  어떤 방법을 선택해야 하지???               │
│  │bark│   │meow│                                                            │
│  └────┘   └────┘                                                            │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

2️⃣ 관계 표현 문제

┌─────────────────────────────────────────────────────────────────────────────┐
│                    객체 참조 vs 외래키                                       │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  Java 객체:                         DB 테이블:                              │
│                                                                             │
│  class User {                       users 테이블                            │
│      Long id;                       ┌────────────────────┐                  │
│      String name;                   │ id │ name         │                  │
│      List<Order> orders; ──┐        └────────────────────┘                  │
│  }                         │                                                │
│                            │        orders 테이블                           │
│  class Order {             │        ┌───────────────────────────┐           │
│      Long id;              └──────→ │ id │ user_id │ product   │           │
│      User user;  ←─────────────────→│    │  (FK)   │           │           │
│      String product;                └───────────────────────────┘           │
│  }                                                                          │
│                                                                             │
│  객체: user.getOrders().get(0)  ← 참조로 직접 탐색                          │
│  DB:   SELECT * FROM orders WHERE user_id = 123  ← 조인 필요                │
│                                                                             │
│  → 탐색 방식이 완전히 다름!                                                 │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

3️⃣ 동일성 vs 동등성 문제

┌─────────────────────────────────────────────────────────────────────────────┐
│                    같은 데이터인데 다른 객체?                                │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  // 두 번 조회하면 어떻게 될까?                                             │
│  User user1 = userDao.findById(1);  // 첫 번째 조회                        │
│  User user2 = userDao.findById(1);  // 두 번째 조회                        │
│                                                                             │
│  // 결과                                                                    │
│  user1 == user2      // false! (서로 다른 객체 인스턴스)                   │
│  user1.equals(user2) // true  (같은 데이터)                                │
│                                                                             │
│  문제점:                                                                    │
│  - user1의 name을 변경해도 user2에는 반영 안 됨                            │
│  - 같은 데이터를 가리키는 객체가 여러 개 → 일관성 문제                      │
│  - HashSet/HashMap에서 예상치 못한 동작                                     │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

1.2 JDBC 직접 사용의 고통

Hibernate 등장 전, JDBC를 직접 사용해야 했습니다.

┌─────────────────────────────────────────────────────────────────────────────┐
│                    2001년경 전형적인 Java 데이터 접근 코드                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  // 단순히 User 하나 조회하는데 이렇게 많은 코드가 필요했음                  │
│                                                                             │
│  public User findUserById(Long id) {                                       │
│      Connection conn = null;                                               │
│      PreparedStatement ps = null;                                          │
│      ResultSet rs = null;                                                  │
│      User user = null;                                                     │
│                                                                             │
│      try {                                                                 │
│          conn = dataSource.getConnection();                                │
│          ps = conn.prepareStatement(                                       │
│              "SELECT id, name, email, created_at FROM users WHERE id = ?" │
│          );                                                                │
│          ps.setLong(1, id);                                                │
│          rs = ps.executeQuery();                                           │
│                                                                             │
│          if (rs.next()) {                                                  │
│              user = new User();                                            │
│              user.setId(rs.getLong("id"));                                 │
│              user.setName(rs.getString("name"));                           │
│              user.setEmail(rs.getString("email"));                         │
│              user.setCreatedAt(rs.getTimestamp("created_at"));             │
│          }                                                                 │
│      } catch (SQLException e) {                                            │
│          throw new RuntimeException(e);                                    │
│      } finally {                                                           │
│          // 리소스 정리 (안 하면 커넥션 누수!)                              │
│          if (rs != null) try { rs.close(); } catch (SQLException e) {}    │
│          if (ps != null) try { ps.close(); } catch (SQLException e) {}    │
│          if (conn != null) try { conn.close(); } catch (SQLException e) {}│
│      }                                                                     │
│      return user;                                                          │
│  }                                                                         │
│                                                                             │
│  🔴 문제점:                                                                 │
│  - 보일러플레이트 코드가 비즈니스 로직보다 많음                              │
│  - SQL을 문자열로 직접 작성 (오타 → 런타임 에러)                            │
│  - 컬럼명과 필드명 수동 매핑 (실수하기 쉬움)                                │
│  - 리소스 관리 실수 → 커넥션 누수 → 서버 다운                              │
│  - 테이블 구조 변경 시 모든 SQL 수정 필요                                   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

1.3 Hibernate의 등장 (2001년)

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Hibernate의 탄생                                         │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  👤 창시자: Gavin King (호주 개발자)                                        │
│  📅 탄생: 2001년                                                            │
│  🏢 현재: Red Hat (JBoss)에서 관리                                          │
│  🎯 목표: "객체를 저장하는 것처럼 DB를 사용하자"                             │
│                                                                             │
│  Gavin King의 핵심 아이디어:                                                │
│  ────────────────────────────                                               │
│  "개발자는 SQL이 아닌 객체에 집중해야 한다"                                 │
│  "DB 접근 코드의 80%는 자동화할 수 있다"                                    │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Hibernate가 해결한 것

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Hibernate (ORM) 의 역할                                  │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   ┌─────────────────────────────────────────────────────────────────┐       │
│   │                                                                 │       │
│   │   Java 객체  ←──── Hibernate (ORM) ────→  관계형 DB           │       │
│   │                                                                 │       │
│   │   // 저장                                   실제 실행 SQL       │       │
│   │   User user = new User();                                      │       │
│   │   user.setName("Kim");          ──→       INSERT INTO users    │       │
│   │   session.save(user);                     (name, email)        │       │
│   │                                           VALUES ('Kim', ...)  │       │
│   │                                                                 │       │
│   │   // 조회                                                       │       │
│   │   User u = session.get(          ←──      SELECT * FROM users  │       │
│   │       User.class, 1                       WHERE id = 1         │       │
│   │   );                                                            │       │
│   │                                                                 │       │
│   │   // 수정 (변경 감지!)                                          │       │
│   │   user.setName("Park");          ──→      UPDATE users         │       │
│   │   // 별도 save 호출 필요 없음              SET name = 'Park'    │       │
│   │                                           WHERE id = 1         │       │
│   │                                                                 │       │
│   └─────────────────────────────────────────────────────────────────┘       │
│                                                                             │
│   ORM = Object-Relational Mapping                                          │
│   객체와 관계형 DB 사이의 자동 변환 기술                                     │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

1.4 Hibernate의 핵심 개념

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Hibernate 핵심 개념                                       │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  1️⃣ 엔티티 (Entity)                                                        │
│  ───────────────────                                                        │
│     - DB 테이블과 매핑되는 Java/Kotlin 클래스                               │
│     - @Entity 어노테이션으로 표시                                           │
│                                                                             │
│     @Entity                                                                │
│     @Table(name = "users")                                                 │
│     class User {                                                           │
│         @Id                                                                │
│         @GeneratedValue(strategy = GenerationType.IDENTITY)                │
│         Long id;                                                           │
│                                                                             │
│         @Column(name = "user_name", length = 100)                          │
│         String name;                                                       │
│                                                                             │
│         @Column(nullable = false)                                          │
│         String email;                                                      │
│     }                                                                      │
│                                                                             │
│  2️⃣ 세션 (Session) / EntityManager                                         │
│  ────────────────────────────────────                                       │
│     - DB 연결을 추상화한 객체                                               │
│     - 1차 캐시 역할 (같은 트랜잭션 내 동일 엔티티는 같은 객체)              │
│     - 트랜잭션 관리                                                        │
│     - Spring에서는 EntityManager로 주로 사용                                │
│                                                                             │
│  3️⃣ 영속성 컨텍스트 (Persistence Context)                                  │
│  ─────────────────────────────────────────                                  │
│     - 엔티티를 영구 저장하는 논리적 환경                                    │
│                                                                             │
│     핵심 기능:                                                              │
│     ┌────────────────────────────────────────────────────────────┐         │
│     │ 1차 캐시      │ 같은 ID 재조회 시 DB 안 가고 캐시 반환    │         │
│     │ 동일성 보장   │ 같은 ID = 같은 객체 인스턴스 (== true)    │         │
│     │ 변경 감지     │ 객체 수정 시 자동으로 UPDATE SQL 생성    │         │
│     │ 지연 로딩     │ 연관 데이터는 실제 사용 시점에 조회       │         │
│     │ 쓰기 지연     │ SQL을 모아서 한 번에 실행 (성능 최적화)  │         │
│     └────────────────────────────────────────────────────────────┘         │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

1.5 JPA와 Hibernate의 관계

┌─────────────────────────────────────────────────────────────────────────────┐
│                    JPA vs Hibernate                                         │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  📅 2006년: JPA (Java Persistence API) 탄생                                 │
│                                                                             │
│  배경:                                                                      │
│  ─────                                                                      │
│  - Hibernate가 너무 인기 → 다른 ORM들도 등장                                │
│    (TopLink, EclipseLink, OpenJPA 등)                                      │
│  - 각 ORM마다 API가 달라서 벤더 종속 문제 발생                              │
│  - Java 진영에서 표준 필요성 대두                                           │
│  - Gavin King(Hibernate 창시자)이 JPA 표준 제정에 참여                      │
│                                                                             │
│  결과: JPA = 표준 인터페이스, Hibernate = 구현체                            │
│  ───────────────────────────────────────────────                            │
│  ┌─────────────────────────────────────────────────────────────────┐       │
│  │                                                                 │       │
│  │                     JPA (표준 인터페이스)                       │       │
│  │                   javax.persistence.* 패키지                    │       │
│  │                              │                                  │       │
│  │            ┌─────────────────┼─────────────────┐                │       │
│  │            │                 │                 │                │       │
│  │            ▼                 ▼                 ▼                │       │
│  │       Hibernate         EclipseLink        OpenJPA              │       │
│  │       (구현체)           (구현체)           (구현체)             │       │
│  │         ⭐                                                       │       │
│  │       가장 많이                                                  │       │
│  │       사용됨                                                     │       │
│  │                                                                 │       │
│  └─────────────────────────────────────────────────────────────────┘       │
│                                                                             │
│  비유:                                                                      │
│  ─────                                                                      │
│  - JPA = JDBC처럼 표준 인터페이스                                           │
│  - Hibernate = MySQL Driver처럼 실제 구현체                                │
│                                                                             │
│  현재 (2026년):                                                             │
│  ──────────────                                                             │
│  - Spring Boot의 기본 JPA 구현체 = Hibernate                               │
│  - 대부분의 프로젝트에서 Hibernate 사용                                    │
│  - JPA 3.0 (Jakarta EE) + Hibernate 6.x가 최신                             │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

2. Soft Delete란?

2.1 배경: 데이터 삭제의 두 가지 방식

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Hard Delete vs Soft Delete                               │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  1️⃣ Hard Delete (물리적 삭제)                                              │
│  ─────────────────────────────                                              │
│                                                                             │
│     DELETE FROM users WHERE id = 123;                                      │
│                                                                             │
│     특징:                                                                   │
│     - 데이터가 DB에서 완전히 사라짐                                         │
│     - 복구 불가능 (백업에서만 복구 가능)                                    │
│     - 스토리지 공간 확보                                                    │
│                                                                             │
│  2️⃣ Soft Delete (논리적 삭제)                                              │
│  ─────────────────────────────                                              │
│                                                                             │
│     UPDATE users SET deleted_at = NOW() WHERE id = 123;                    │
│                                                                             │
│     특징:                                                                   │
│     - 데이터는 그대로 존재                                                  │
│     - "삭제됨" 표시만 추가                                                  │
│     - 조회 시 WHERE deleted_at IS NULL 조건 추가                           │
│     - 언제든 복구 가능                                                      │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

2.2 왜 Soft Delete가 필요한가?

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Soft Delete가 필요한 4가지 상황                           │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  1️⃣ 실수 복구 (Accidental Delete Recovery)                                 │
│  ──────────────────────────────────────────                                 │
│                                                                             │
│     사용자: "아 실수로 삭제했어요! 복구해주세요!"                            │
│                                                                             │
│     Hard Delete:                                                           │
│     "죄송합니다, 백업에서 복구하려면 3일 걸리고 DBA 승인 필요합니다..."      │
│                                                                             │
│     Soft Delete:                                                           │
│     "네, 5초만요"                                                          │
│     UPDATE users SET deleted_at = NULL WHERE id = 123;                     │
│     "복구 완료!"                                                           │
│                                                                             │
│  2️⃣ 감사/규정 준수 (Audit & Compliance)                                    │
│  ───────────────────────────────────────                                    │
│                                                                             │
│     법규 요구사항:                                                          │
│     - 금융: "거래 기록은 5년간 보관해야 합니다"                             │
│     - 의료: "진료 기록은 10년간 보관해야 합니다"                            │
│     - GDPR: "삭제 요청 시 개인정보는 삭제, 거래 기록은 익명화하여 보관"     │
│                                                                             │
│     Hard Delete: 법규 위반 위험                                             │
│     Soft Delete: 삭제 표시 + 데이터 보관 = 규정 준수                        │
│                                                                             │
│  3️⃣ 참조 무결성 유지 (Referential Integrity)                               │
│  ──────────────────────────────────────────                                 │
│                                                                             │
│     users 테이블                orders 테이블                               │
│     ┌──────────────┐           ┌──────────────────┐                        │
│     │ id: 123      │     ┌────→│ user_id: 123     │                        │
│     │ name: Kim    │─────┘     │ product: iPhone  │                        │
│     └──────────────┘           │ amount: 1000000  │                        │
│                                └──────────────────┘                        │
│                                                                             │
│     Hard Delete 시나리오:                                                   │
│     - FK 제약 → 삭제 불가 (에러 발생)                                       │
│     - CASCADE → 주문도 함께 삭제 (매출 기록 손실!)                          │
│     - SET NULL → user_id가 NULL (누가 주문했는지 모름)                      │
│                                                                             │
│     Soft Delete:                                                           │
│     - User는 삭제 표시, Order는 그대로 유지                                 │
│     - 주문 히스토리 보존                                                    │
│     - "탈퇴한 사용자" 표시 가능                                             │
│                                                                             │
│  4️⃣ 데이터 분석 (Analytics)                                                │
│  ──────────────────────────                                                 │
│                                                                             │
│     질문: "왜 고객들이 탈퇴하나요? 패턴이 있나요?"                           │
│                                                                             │
│     Hard Delete:                                                           │
│     - 탈퇴 고객 데이터 없음                                                 │
│     - 분석 불가                                                             │
│                                                                             │
│     Soft Delete:                                                           │
│     - 탈퇴 고객 데이터 보존                                                 │
│     - "마지막 활동일", "가입 기간", "사용 패턴" 분석 가능                    │
│     - 이탈 방지 전략 수립에 활용                                            │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

2.3 Soft Delete 구현 방식 비교

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Soft Delete 구현 방식 비교                                │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  방식 1: Boolean 플래그                                                     │
│  ─────────────────────                                                      │
│                                                                             │
│  ┌─────────────────────────────────────────┐                               │
│  │ users 테이블                            │                               │
│  │ id │ name │ email      │ is_deleted    │                               │
│  │  1 │ Kim  │ k@mail.com │ false         │                               │
│  │  2 │ Lee  │ l@mail.com │ true  ← 삭제됨 │                               │
│  └─────────────────────────────────────────┘                               │
│                                                                             │
│  장점: 단순함, 인덱스 효율적                                                │
│  단점: "언제" 삭제되었는지 알 수 없음                                       │
│                                                                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  방식 2: Timestamp ⭐ (권장)                                                │
│  ────────────────────────                                                   │
│                                                                             │
│  ┌──────────────────────────────────────────────────────────┐              │
│  │ users 테이블                                             │              │
│  │ id │ name │ email      │ deleted_at                      │              │
│  │  1 │ Kim  │ k@mail.com │ NULL                            │              │
│  │  2 │ Lee  │ l@mail.com │ 2024-01-15 10:30:00 ← 삭제 시각 │              │
│  └──────────────────────────────────────────────────────────┘              │
│                                                                             │
│  장점: 삭제 시각 기록, 감사 추적 가능, NULL 체크로 간단히 조회              │
│  단점: NULL 허용 컬럼의 인덱스 고려 필요                                    │
│                                                                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  방식 3: Status 컬럼                                                        │
│  ──────────────────                                                         │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────┐               │
│  │ users 테이블                                            │               │
│  │ id │ name │ email      │ status                         │               │
│  │  1 │ Kim  │ k@mail.com │ ACTIVE                         │               │
│  │  2 │ Lee  │ l@mail.com │ DELETED                        │               │
│  │  3 │ Park │ p@mail.com │ SUSPENDED                      │               │
│  └─────────────────────────────────────────────────────────┘               │
│                                                                             │
│  장점: 다양한 상태 표현 가능 (ACTIVE, SUSPENDED, DELETED, BANNED 등)        │
│  단점: 삭제 시각은 별도 컬럼 필요, 상태 관리 복잡해질 수 있음                │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

3. Hibernate의 Soft Delete 지원

3.1 @SQLDelete와 @SQLRestriction

Hibernate는 Soft Delete를 위한 전용 어노테이션을 제공합니다.

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Hibernate Soft Delete 기본 구현                          │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  @Entity                                                                   │
│  @Table(name = "users")                                                    │
│  @SQLDelete(sql = "UPDATE users SET deleted_at = NOW() WHERE id = ?")      │
│  @SQLRestriction("deleted_at IS NULL")                                     │
│  class User(                                                               │
│      @Id                                                                   │
│      @GeneratedValue(strategy = GenerationType.IDENTITY)                   │
│      var id: Long? = null,                                                 │
│                                                                             │
│      var name: String,                                                     │
│                                                                             │
│      var email: String,                                                    │
│                                                                             │
│      @Column(name = "deleted_at")                                          │
│      var deletedAt: LocalDateTime? = null                                  │
│  )                                                                         │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

동작 방식

┌─────────────────────────────────────────────────────────────────────────────┐
│                    @SQLDelete + @SQLRestriction 동작                        │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  1️⃣ 삭제 시 (@SQLDelete가 DELETE를 가로챔)                                 │
│  ─────────────────────────────────────────                                  │
│                                                                             │
│  // 개발자 코드                                                             │
│  userRepository.delete(user)                                               │
│  // 또는                                                                   │
│  userRepository.deleteById(123)                                            │
│                                                                             │
│  // 원래 실행되어야 할 SQL                                                  │
│  DELETE FROM users WHERE id = 123                                          │
│                                                                             │
│  // 실제 실행되는 SQL (@SQLDelete로 대체됨)                                 │
│  UPDATE users SET deleted_at = NOW() WHERE id = 123                        │
│                                                                             │
│  → DELETE가 UPDATE로 변환됨!                                                │
│                                                                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  2️⃣ 조회 시 (@SQLRestriction이 WHERE 조건 추가)                            │
│  ───────────────────────────────────────────                                │
│                                                                             │
│  // 개발자 코드                                                             │
│  userRepository.findAll()                                                  │
│  userRepository.findById(123)                                              │
│  userRepository.findByEmail("kim@mail.com")                                │
│                                                                             │
│  // 원래 실행되어야 할 SQL                                                  │
│  SELECT * FROM users                                                       │
│  SELECT * FROM users WHERE id = 123                                        │
│  SELECT * FROM users WHERE email = 'kim@mail.com'                          │
│                                                                             │
│  // 실제 실행되는 SQL (@SQLRestriction 조건 자동 추가)                      │
│  SELECT * FROM users WHERE deleted_at IS NULL                              │
│  SELECT * FROM users WHERE id = 123 AND deleted_at IS NULL                 │
│  SELECT * FROM users WHERE email = 'kim@mail.com' AND deleted_at IS NULL   │
│                                                                             │
│  → 삭제된 데이터 자동 제외!                                                 │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

3.2 Hibernate 6.4+의 @SoftDelete (최신 방법)

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Hibernate 6.4+ @SoftDelete 어노테이션                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  Hibernate 6.4 (2023년 말)부터 더 간단한 방법이 추가됨:                     │
│                                                                             │
│  // 가장 간단한 형태                                                        │
│  @Entity                                                                   │
│  @SoftDelete                     // ← 이것만 추가하면 끝!                   │
│  class User(                                                               │
│      @Id                                                                   │
│      @GeneratedValue                                                       │
│      var id: Long? = null,                                                 │
│                                                                             │
│      var name: String                                                      │
│      // deleted 컬럼이 자동으로 추가/관리됨                                 │
│  )                                                                         │
│                                                                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  커스터마이징 옵션들:                                                       │
│                                                                             │
│  // Boolean 컬럼 사용 (기본값)                                             │
│  @SoftDelete(columnName = "is_deleted")                                    │
│                                                                             │
│  // Timestamp 컬럼 사용                                                    │
│  @SoftDelete(                                                              │
│      columnName = "deleted_at",                                            │
│      converter = DeletedAsTimestampConverter::class                        │
│  )                                                                         │
│                                                                             │
│  // Status Enum 사용                                                       │
│  @SoftDelete(                                                              │
│      columnName = "status",                                                │
│      converter = DeletedAsStatusConverter::class                           │
│  )                                                                         │
│                                                                             │
│  장점:                                                                      │
│  - @SQLDelete + @SQLRestriction 조합보다 간결                              │
│  - 타입 안전한 컨버터 지원                                                 │
│  - Hibernate가 컬럼 관리를 자동으로 처리                                   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

3.3 CCK 프로젝트의 Soft Delete 패턴

CCK Mothership 프로젝트에서 실제 사용하는 패턴입니다.

┌─────────────────────────────────────────────────────────────────────────────┐
│                    CCK BaseEntity 패턴                                      │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  // 공통 BaseEntity 정의                                                   │
│  @MappedSuperclass                                                         │
│  abstract class MothershipBaseEntity {                                     │
│      @Id                                                                   │
│      @GeneratedValue(strategy = GenerationType.IDENTITY)                   │
│      var id: Long? = null                                                  │
│                                                                             │
│      @Column(name = "created_at", nullable = false, updatable = false)     │
│      var createdAt: LocalDateTime = LocalDateTime.now()                    │
│                                                                             │
│      @Column(name = "updated_at", nullable = false)                        │
│      var updatedAt: LocalDateTime = LocalDateTime.now()                    │
│                                                                             │
│      @Column(name = "deleted_at")                                          │
│      var deletedAt: LocalDateTime? = null     // ← Soft Delete용           │
│  }                                                                         │
│                                                                             │
│  // 실제 Entity에서 사용                                                   │
│  @Entity                                                                   │
│  @Table(                                                                   │
│      name = "users",                                                       │
│      indexes = [                                                           │
│          Index(name = "idx_user_deleted_at", columnList = "deleted_at")   │
│      ]                                                                     │
│  )                                                                         │
│  @SQLDelete(                                                               │
│      sql = "UPDATE users SET deleted_at = NOW() " +                        │
│            "WHERE id = ? AND deleted_at IS NULL"                           │
│  )                                                                         │
│  @SQLRestriction("deleted_at IS NULL")                                     │
│  class User(                                                               │
│      var name: String,                                                     │
│      var email: String,                                                    │
│      @Enumerated(EnumType.STRING)                                          │
│      var role: UserRole                                                    │
│  ) : MothershipBaseEntity()                                                │
│                                                                             │
│  특징:                                                                      │
│  - createdAt, updatedAt, deletedAt 3개 타임스탬프 표준화                   │
│  - deleted_at에 인덱스 추가 (조회 성능 최적화)                             │
│  - AND deleted_at IS NULL 조건으로 중복 삭제 방지                          │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

4. Soft Delete 구현 시 주의사항

4.1 Unique 제약 조건 문제

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Soft Delete + Unique 제약의 충돌                         │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  🔴 문제 상황:                                                              │
│                                                                             │
│  @Entity                                                                   │
│  @SQLDelete(...)                                                           │
│  @SQLRestriction("deleted_at IS NULL")                                     │
│  class User(                                                               │
│      @Column(unique = true)   // ← 이메일은 유일해야 함                    │
│      var email: String                                                     │
│  )                                                                         │
│                                                                             │
│  시나리오:                                                                  │
│  ┌────────────────────────────────────────────────────────────────┐        │
│  │ 1. kim@mail.com으로 사용자 생성                                │        │
│  │ 2. 해당 사용자 탈퇴 (Soft Delete)                              │        │
│  │    → DB: deleted_at = '2024-01-15 10:30:00'                   │        │
│  │ 3. 새 사용자가 kim@mail.com으로 가입 시도                      │        │
│  │    → 💥 Unique 제약 위반!                                      │        │
│  │    → 삭제된 데이터도 DB에 존재하므로 중복으로 판단             │        │
│  └────────────────────────────────────────────────────────────────┘        │
│                                                                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ✅ 해결책 1: Partial Unique Index (PostgreSQL 권장)                       │
│                                                                             │
│  -- Flyway 마이그레이션에서 설정                                           │
│  CREATE UNIQUE INDEX unique_user_email_active                              │
│  ON users(email)                                                           │
│  WHERE deleted_at IS NULL;                                                 │
│                                                                             │
│  효과:                                                                      │
│  - deleted_at이 NULL인 (활성) 데이터에만 유일 제약 적용                    │
│  - 삭제된 데이터는 같은 email 허용                                         │
│  - 같은 email로 재가입 가능!                                               │
│                                                                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ✅ 해결책 2: 삭제 시 값 변경 (MySQL/MariaDB)                              │
│                                                                             │
│  MySQL은 Partial Index 미지원, 대안 사용:                                  │
│                                                                             │
│  -- 삭제 시 email에 접미사 추가                                            │
│  @SQLDelete(sql = """                                                      │
│      UPDATE users SET                                                      │
│          deleted_at = NOW(),                                               │
│          email = CONCAT(email, '_deleted_', id)                            │
│      WHERE id = ? AND deleted_at IS NULL                                   │
│  """)                                                                      │
│                                                                             │
│  결과:                                                                      │
│  - 삭제 전: kim@mail.com                                                   │
│  - 삭제 후: kim@mail.com_deleted_123                                       │
│  - 새 가입: kim@mail.com (가능!)                                           │
│                                                                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ✅ 해결책 3: 복합 유니크 (범용)                                            │
│                                                                             │
│  @Entity                                                                   │
│  @Table(uniqueConstraints = [                                              │
│      UniqueConstraint(                                                     │
│          name = "uk_user_email_deleted",                                   │
│          columnNames = ["email", "deleted_id"]                             │
│      )                                                                     │
│  ])                                                                        │
│  class User(                                                               │
│      var email: String,                                                    │
│      var deletedId: Long = 0  // 활성: 0, 삭제: 자신의 id                  │
│  )                                                                         │
│                                                                             │
│  UNIQUE(email, deleted_id)                                                 │
│  - 활성 사용자: (kim@mail.com, 0)                                          │
│  - 삭제된 사용자: (kim@mail.com, 123)                                      │
│  - 새 가입: (kim@mail.com, 0) 가능!                                        │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

4.2 Cascade와 Soft Delete

┌─────────────────────────────────────────────────────────────────────────────┐
│                    orphanRemoval vs CascadeType.REMOVE                      │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ⚠️ 매우 중요한 차이점:                                                    │
│                                                                             │
│  1️⃣ orphanRemoval = true                                                   │
│  ─────────────────────────                                                  │
│     - 컬렉션에서 제거된 자식 엔티티를 자동으로 삭제                         │
│     - ⚠️ @SQLDelete를 무시함!                                              │
│     - 항상 물리적 DELETE 실행                                               │
│     - Soft Delete와 충돌!                                                   │
│                                                                             │
│  2️⃣ CascadeType.REMOVE (또는 CascadeType.ALL에 포함)                       │
│  ───────────────────────────────────────────────                            │
│     - 부모 삭제 시 자식도 삭제                                              │
│     - ✅ @SQLDelete를 존중함!                                              │
│     - UPDATE 실행 (Soft Delete 동작)                                        │
│     - Soft Delete와 호환!                                                   │
│                                                                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ❌ 잘못된 예 (orphanRemoval이 Soft Delete를 무시):                         │
│                                                                             │
│  @Entity                                                                   │
│  @SQLDelete(sql = "UPDATE projects SET deleted_at = NOW() WHERE id = ?")   │
│  class Project(                                                            │
│      @OneToMany(                                                           │
│          mappedBy = "project",                                             │
│          cascade = [CascadeType.ALL],                                      │
│          orphanRemoval = true   // ← 문제! @SQLDelete 무시됨               │
│      )                                                                     │
│      var members: MutableList<ProjectMember> = mutableListOf()             │
│  )                                                                         │
│                                                                             │
│  // 이렇게 하면...                                                         │
│  project.members.remove(member)                                            │
│  projectRepository.save(project)                                           │
│                                                                             │
│  // 의도: UPDATE project_members SET deleted_at = NOW() ...                │
│  // 실제: DELETE FROM project_members WHERE id = ???  💥                   │
│                                                                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ✅ 올바른 예 (Soft Delete와 호환):                                         │
│                                                                             │
│  @Entity                                                                   │
│  @SQLDelete(sql = "UPDATE projects SET deleted_at = NOW() WHERE id = ?")   │
│  class Project(                                                            │
│      @OneToMany(                                                           │
│          mappedBy = "project",                                             │
│          cascade = [CascadeType.ALL],   // ← @SQLDelete 존중              │
│          orphanRemoval = false          // ← 자동 삭제 비활성화            │
│      )                                                                     │
│      var members: MutableList<ProjectMember> = mutableListOf()             │
│  )                                                                         │
│                                                                             │
│  // 삭제가 필요할 때는 명시적으로                                           │
│  projectMemberRepository.delete(member)  // → Soft Delete 실행             │
│  project.members.remove(member)          // → 컬렉션에서만 제거            │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

4.3 삭제된 데이터 조회하기

┌─────────────────────────────────────────────────────────────────────────────┐
│                    삭제된 데이터도 조회해야 할 때                            │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  @SQLRestriction은 모든 조회에 적용되므로,                                  │
│  삭제된 데이터를 보려면 별도 방법 필요                                      │
│                                                                             │
│  방법 1: Native Query 사용                                                 │
│  ────────────────────────                                                   │
│                                                                             │
│  @Query(                                                                   │
│      value = "SELECT * FROM users WHERE id = :id",                         │
│      nativeQuery = true   // @SQLRestriction 우회                          │
│  )                                                                         │
│  fun findByIdIncludingDeleted(id: Long): User?                             │
│                                                                             │
│  @Query(                                                                   │
│      value = "SELECT * FROM users WHERE deleted_at IS NOT NULL",           │
│      nativeQuery = true                                                    │
│  )                                                                         │
│  fun findAllDeleted(): List<User>                                          │
│                                                                             │
│  방법 2: 별도 View Entity 생성                                             │
│  ────────────────────────────                                               │
│                                                                             │
│  // Soft Delete 필터 없는 별도 Entity                                      │
│  @Entity                                                                   │
│  @Table(name = "users")                                                    │
│  @Immutable  // 읽기 전용                                                  │
│  class UserWithDeleted(                                                    │
│      @Id                                                                   │
│      var id: Long,                                                         │
│      var name: String,                                                     │
│      var deletedAt: LocalDateTime?                                         │
│  )                                                                         │
│  // @SQLRestriction 없음 → 삭제된 데이터도 조회                            │
│                                                                             │
│  방법 3: Hibernate Filter 사용 (동적 필터링)                               │
│  ──────────────────────────────────────────                                 │
│                                                                             │
│  @Entity                                                                   │
│  @FilterDef(name = "deletedFilter",                                        │
│             parameters = [@ParamDef(name = "isDeleted", type = Boolean)])  │
│  @Filter(name = "deletedFilter",                                           │
│          condition = "deleted_at IS NULL = :isDeleted")                    │
│  class User { ... }                                                        │
│                                                                             │
│  // 사용                                                                   │
│  session.enableFilter("deletedFilter")                                     │
│         .setParameter("isDeleted", false)  // 삭제된 것도 보기             │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

5. Soft Delete의 단점과 대안

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Soft Delete의 단점                                       │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  1️⃣ 스토리지 증가                                                          │
│     - 삭제해도 데이터가 남아있음                                            │
│     - 시간이 지날수록 테이블 크기 증가                                      │
│     - 인덱스 크기도 증가                                                    │
│                                                                             │
│  2️⃣ 쿼리 복잡도 증가                                                       │
│     - 모든 쿼리에 WHERE deleted_at IS NULL 필요                            │
│     - @SQLRestriction 없으면 실수로 삭제된 데이터 조회 가능                │
│     - JOIN 시에도 양쪽에 조건 필요                                         │
│                                                                             │
│  3️⃣ Unique 제약 복잡도                                                     │
│     - 앞서 설명한 Partial Index 등 추가 처리 필요                          │
│                                                                             │
│  4️⃣ 성능 영향                                                              │
│     - 삭제된 데이터도 스캔해야 함 (인덱스 사용 시 완화)                     │
│     - 통계가 왜곡될 수 있음                                                │
│                                                                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  💡 해결책: 정기적인 데이터 정리 (Purge)                                    │
│                                                                             │
│  -- 90일 이상 지난 삭제 데이터는 완전 삭제                                 │
│  DELETE FROM users                                                         │
│  WHERE deleted_at IS NOT NULL                                              │
│    AND deleted_at < NOW() - INTERVAL 90 DAY;                               │
│                                                                             │
│  -- 또는 아카이브 테이블로 이동                                            │
│  INSERT INTO users_archive SELECT * FROM users                             │
│  WHERE deleted_at IS NOT NULL                                              │
│    AND deleted_at < NOW() - INTERVAL 90 DAY;                               │
│                                                                             │
│  DELETE FROM users                                                         │
│  WHERE deleted_at IS NOT NULL                                              │
│    AND deleted_at < NOW() - INTERVAL 90 DAY;                               │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

6. 요약

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Hibernate & Soft Delete 요약                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  📚 Hibernate                                                               │
│  ─────────────                                                              │
│     - 2001년 Gavin King이 개발한 ORM 프레임워크                             │
│     - 객체-관계 불일치(Impedance Mismatch) 문제 해결                        │
│     - JDBC 보일러플레이트 코드 제거                                         │
│     - JPA 표준의 대표적인 구현체                                            │
│     - 영속성 컨텍스트: 1차 캐시, 변경 감지, 지연 로딩                       │
│                                                                             │
│  📚 Soft Delete                                                             │
│  ───────────────                                                            │
│     - 물리적 삭제(DELETE) 대신 논리적 삭제(UPDATE deleted_at)              │
│     - 장점:                                                                 │
│       ✓ 복구 가능                                                          │
│       ✓ 감사/규정 준수                                                     │
│       ✓ 참조 무결성 유지                                                   │
│       ✓ 데이터 분석 가능                                                   │
│                                                                             │
│  📚 Hibernate에서 Soft Delete 구현                                          │
│  ───────────────────────────────                                            │
│     - 전통적: @SQLDelete + @SQLRestriction 조합                            │
│     - 최신 (6.4+): @SoftDelete 어노테이션                                  │
│                                                                             │
│  ⚠️ 주의사항                                                               │
│  ────────────                                                               │
│     - Unique 제약: Partial Index 또는 복합 유니크로 해결                    │
│     - orphanRemoval=true는 @SQLDelete 무시 → false 권장                    │
│     - 삭제된 데이터 조회: Native Query 또는 별도 Entity 사용               │
│     - 스토리지 관리: 정기적 Purge 또는 Archive 정책 필요                   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

참고 자료

  • Hibernate ORM 공식 문서: https://hibernate.org/orm/documentation/
  • JPA 3.0 명세 (Jakarta Persistence): https://jakarta.ee/specifications/persistence/
  • Hibernate @SoftDelete (6.4+): https://docs.jboss.org/hibernate/orm/6.4/userguide/html_single/Hibernate_User_Guide.html#soft-delete
  • CCK Mothership CLAUDE.md: mothership/backend/src/CLAUDE.md

관련 키워드

Hibernate, ORM, JPA, Soft Delete, Hard Delete, @SQLDelete, @SQLRestriction, @SoftDelete, 논리적 삭제, 물리적 삭제, Gavin King, 영속성 컨텍스트, 변경 감지, 지연 로딩, orphanRemoval, Cascade, Partial Index