Data pelanggan TokoKita menumpuk dari tahun ke tahun: pesanan tahun 2019 masih tersimpan lengkap dengan email dan nomor telepon “siapa tahu nanti berguna”. Setiap tabel lama memperbesar dampak bila terjadi kebocoran data, dan setiap kolom pribadi yang tidak terpakai adalah kewajiban yang tidak memberi nilai. Regulasi kini menuntut disiplin: di Indonesia, UU PDP (Undang-Undang Pelindungan Data Pribadi, UU No. 27 Tahun 2022) membatasi pemrosesan data pribadi hanya untuk tujuan yang jelas, sementara GDPR di Uni Eropa menuntut hal serupa bagi pemrosesan data warga Eropa. Bab ini membahas dua sisi disiplin tersebut — mengumpulkan sesedikit mungkin (privacy) dan menyimpan sesingkat mungkin (retention) — lalu mengeksekusi keduanya sebagai kode yang bisa dijalankan dan diaudit.

Data Minimization: Kumpulkan Seminimal yang Perlu #

Data minimization berarti setiap kolom dan setiap tingkat detail harus punya alasan eksplisit untuk dikumpulkan. Prinsip ini paling murah diterapkan saat desain, dan paling mahal setelah data menyebar ke sepuluh tabel turunan. Daftar berikut adalah pemeriksaan yang kita jalankan sebelum tabel baru dibuat di TokoKita:

  • Hapus kolom yang jawabannya “mungkin nanti berguna” — kebutuhan bisa diambil saat benar-benar muncul.
  • Pilih granularitas paling kasar yang memadai: rentang usia cukup untuk segmentasi promosi, tanpa tanggal lahir lengkap.
  • Data pribadi hanya hidup di tabel sumbernya; tabel turunan memakai pelanggan_id sebagai pseudonym, bukan menyalin email.
  • Kumpulkan persetujuan (consent) bersama datanya, karena tujuan pemrosesan menjadi tidak sah saat consent dicabut.
  • Tetapkan umur simpan (retention) sejak desain, bukan setelah data menumpuk bertahun-tahun.

Anonymization dan Pseudonymization #

Dua teknik ini sering dianggap sama padahal berbeda secara hukum dan teknis. Anonymization mengubah data sehingga identitas asal tidak dapat dipulihkan lagi; pseudonymization mengganti identitas dengan pengganti (pseudonym) tetapi pemetaan kembali masih ada di tangan pihak tertentu. Karena pemetaan itu tersisa, data hasil pseudonymization tetap dihitung sebagai data pribadi menurut GDPR maupun UU PDP.

AspekAnonymizationPseudonymization
Contoh hasilrentang usia 25-34, kode pos[email protected] menjadi pelanggan_8f3a
Dapat dipulihkantidak, identitas asal tidak tersisaya, oleh pemegang tabel pemetaan
Status hukumbukan lagi data pribaditetap data pribadi
Cocok untukstatistik agregat, dataset publikjoin antar tabel internal, debugging

Teknik penggantian nilainya (masking, tokenization) sudah kita bedah di Chapter Masking dan Tokenization. Kolom berisiko tinggi juga sebaiknya terenkripsi seperti pola pada Chapter Encryption at Rest dan In Transit, sehingga akses fisik ke penyimpanan tidak serta-merta membuka isinya.

Retention Policy sebagai Kode #

Retention policy menyatakan berapa lama tiap dataset boleh hidup dan apa yang terjadi setelahnya. Menuliskannya sebagai YAML memberi keuntungan: aturan bisa di-review seperti kode, divalidasi otomatis, dan perubahannya tercatat di version control. Berikut policy TokoKita — tabel transaksi dihapus setelah 90 hari, log layanan lebih pendek lagi, sementara rekaman audit justru diarsipkan lama karena regulasi menuntut jejak panjang:

yaml
# Retention policy TokoKita: umur maksimum tiap dataset sebelum diproses.
datasets:
  pesanan:
    retention_days: 90
    action: delete
  logs_layanan:
    retention_days: 30
    action: delete
  audit:
    retention_days: 1825
    action: archive

Perhatikan dua aksi yang berbeda: delete menghapus permanen, archive memindahkan ke penyimpanan murah karena data masih wajib ada. Batas retention sebaiknya mengikuti klasifikasi dari Chapter Klasifikasi Data — semakin sensitif datanya, semakin pendek umurnya, dan semakin tegas penghapusannya.

Menjalankan Enforcement Retensi #

Policy tidak berguna tanpa eksekusi. Skrip berikut memvalidasi policy YAML, memindai partisi tanggal=..., menghitung umur tiap partisi terhadap tanggal evaluasi, lalu mencantumkan rencana eksekusi (dry-run) sebelum benar-benar menghapus dan mengarsipkan:

python
"""Terapkan retention policy: validasi YAML, pindai partisi, proses yang kedaluwarsa."""
import shutil
from datetime import date
from pathlib import Path

import yaml

TANGGAL_EVALUASI = date(2026, 8, 1)  # tanggal evaluasi dikunci agar hasil demo konsisten
DASAR = Path(__file__).parent / "data_retensi"
ARSIP = Path(__file__).parent / "arsip_retensi"
KEBIJAKAN = Path(__file__).parent / "retensi_policy.yaml"


def siapkan_data_aset():
    """Bangun ulang partisi contoh agar demo selalu bisa diulang."""
    for direktori in (DASAR, ARSIP):
        if direktori.exists():
            shutil.rmtree(direktori)
    contoh = {
        "pesanan": ["2026-01-15", "2026-05-01", "2026-05-10", "2026-07-30"],
        "logs_layanan": ["2026-06-01", "2026-07-05"],
        "audit": ["2020-03-10"],
    }
    for tabel, tanggalan in contoh.items():
        for tanggal in tanggalan:
            partisi = DASAR / tabel / f"tanggal={tanggal}"
            partisi.mkdir(parents=True)
            (partisi / "part-00000.parquet").write_bytes(b"PARQUET-DEMO")


def muat_kebijakan(path):
    """Muat dan validasi retention policy dari YAML."""
    aturan_set = yaml.safe_load(path.read_text())["datasets"]
    for nama, aturan in aturan_set.items():
        hari = aturan.get("retention_days")
        if not isinstance(hari, int) or hari <= 0:
            raise ValueError(f"{nama}: retention_days harus bilangan bulat positif")
        if aturan.get("action") not in ("delete", "archive"):
            raise ValueError(f"{nama}: action harus 'delete' atau 'archive'")
    return aturan_set


def pindai_partisi():
    """Kumpulkan semua partisi tanggal=... beserta tanggalnya."""
    for tabel in sorted(p for p in DASAR.iterdir() if p.is_dir()):
        for partisi in sorted(tabel.glob("tanggal=*")):
            yield tabel.name, partisi, date.fromisoformat(partisi.name.split("=")[1])


def jalankan():
    siapkan_data_aset()
    kebijakan = muat_kebijakan(KEBIJAKAN)
    semua = list(pindai_partisi())
    print(f"Tanggal evaluasi : {TANGGAL_EVALUASI.isoformat()}")
    print(f"Partisi ditemukan: {len(semua)}")
    print("=== Rencana eksekusi (dry-run) ===")
    kedaluwarsa = []
    for tabel, partisi, tanggal in semua:
        umur = (TANGGAL_EVALUASI - tanggal).days
        aturan = kebijakan[tabel]
        if umur > aturan["retention_days"]:
            aksi = aturan["action"].upper()
            kedaluwarsa.append((partisi, aturan["action"]))
            print(f"  {partisi.relative_to(DASAR)}  umur {umur:4d} hari -> {aksi}")
        else:
            print(f"  {partisi.relative_to(DASAR)}  umur {umur:4d} hari -> simpan")
    print("=== Eksekusi ===")
    for partisi, aksi in kedaluwarsa:
        if aksi == "delete":
            shutil.rmtree(partisi)
        else:
            tujuan = ARSIP / partisi.parent.name / partisi.name
            tujuan.parent.mkdir(parents=True, exist_ok=True)
            shutil.move(str(partisi), tujuan)
    sisa = list(pindai_partisi())
    hapus = sum(1 for _, aksi in kedaluwarsa if aksi == "delete")
    arsip = sum(1 for _, aksi in kedaluwarsa if aksi == "archive")
    print(f"Sebelum: {len(semua)} partisi")
    print(f"Sesudah: {len(sisa)} partisi aktif "
          f"({hapus} dihapus, {arsip} diarsipkan)")


if __name__ == "__main__":
    jalankan()

Output yang Dihasilkan:

text
Tanggal evaluasi : 2026-08-01
Partisi ditemukan: 7
=== Rencana eksekusi (dry-run) ===
  audit/tanggal=2020-03-10  umur 2335 hari -> ARCHIVE
  logs_layanan/tanggal=2026-06-01  umur   61 hari -> DELETE
  logs_layanan/tanggal=2026-07-05  umur   27 hari -> simpan
  pesanan/tanggal=2026-01-15  umur  198 hari -> DELETE
  pesanan/tanggal=2026-05-01  umur   92 hari -> DELETE
  pesanan/tanggal=2026-05-10  umur   83 hari -> simpan
  pesanan/tanggal=2026-07-30  umur    2 hari -> simpan
=== Eksekusi ===
Sebelum: 7 partisi
Sesudah: 3 partisi aktif (3 dihapus, 1 diarsipkan)

Mari kita telaah baris-baris kode di atas:

  1. Baris 14-28 (siapkan_data_aset): membangun ulang tujuh partisi contoh dari awal sehingga demo selalu bisa diulang dengan hasil yang sama — di produksi, bagian ini digantikan oleh data asli.
  2. Baris 31-40 (muat_kebijakan): memuat YAML lewat yaml.safe_load, lalu menolak policy yang cacat — retention_days harus bilangan bulat positif dan action hanya boleh delete atau archive. Policy yang salah gagal keras sebelum menyentuh data.
  3. Baris 43-47 (pindai_partisi): generator ini berjalan atas direktori tabel, mencocokkan pola tanggal=*, dan mengurai tanggalnya dengan date.fromisoformat — pola partisi tanggal inilah yang membuat enforcement retensi cukup murah.
  4. Baris 58-66 (fase dry-run): menghitung umur tiap partisi terhadap TANGGAL_EVALUASI, lalu hanya mencetak rencana — partisi kedaluwarsa dikumpulkan ke daftar kedaluwarsa tanpa dihapus dulu.
  5. Baris 68-80 (fase eksekusi dan laporan): baru pada fase ini shutil.rmtree menghapus partisi dan shutil.move memindahkannya ke direktori arsip terpisah, ditutup laporan sebelum/sesudah — tujuh menjadi tiga partisi aktif, tiga dihapus, satu diarsipkan.

Partisi pesanan/tanggal=2026-05-01 berumur 92 hari — hanya dua hari melewati batas — tetap dihapus; retention yang tegas tidak mengenal “hampir”. Sebaliknya audit/tanggal=2020-03-10 berumur 2.335 hari tidak dihapus melainkan diarsipkan, karena aksinya archive.

Hak penghapusan (right to be forgotten) menuntut mekanisme serupa pada level individu: pelanggan berhak meminta data pribadinya dihapus, dan kita harus bisa membuktikan penghapusan itu terjadi. Rujukan resmi prinsip minimization ada di teks GDPR di EUR-Lex dan halaman perlindungan data Komisi Eropa ; bab ini menjelaskan praktik teknis secara umum, bukan nasihat hukum.

Eksekusi retensi seperti di atas adalah tindakan sensitif: sekali dijalankan, data tidak kembali. Kita butuh jejak yang menjawab siapa yang menjalankan kebijakan, kapan, dan apa saja yang terpengaruh — persis pekerjaan Chapter Audit Logging.