TL;DR

  • Hibernate와 Soft Delete의 핵심 개념과 용어를 한눈에 정리한다.
  • Hibernate와 Soft Delete이(가) 등장한 배경과 필요성을 요약한다.
  • Hibernate와 Soft Delete의 특징과 적용 포인트를 빠르게 확인한다.

1. 개념

Hibernate와 Soft Delete은(는) 핵심 용어와 정의를 정리한 주제로, 개발/운영 맥락에서 무엇을 의미하는지 설명한다.

2. 배경

기존 방식의 한계나 현업의 요구사항을 해결하기 위해 이 개념이 등장했다는 흐름을 이해하는 데 목적이 있다.

3. 이유

도입 이유는 보통 유지보수성, 성능, 안정성, 보안, 협업 효율 같은 실무 문제를 해결하기 위함이다.

4. 특징

  • 핵심 정의와 범위를 명확히 한다.
  • 실무 적용 시 선택 기준과 비교 포인트를 제공한다.
  • 예시 중심으로 빠른 이해를 돕는다.

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