개인정보 안전성 확보조치 기준, 개발자가 구현할 항목

개인정보 안전성 확보조치 기준의 접근권한, 접속기록, 암호화, 인증 조항을 DB 스키마, 로그 설계, 키 관리 같은 개발 작업으로 옮겨 정리했습니다. 작은 팀에도 적용되는지와 실무 체크리스트를 함께 담았습니다.

보안 점검 의뢰 팀 14분 분량

개인정보 안전성 확보조치 기준(개인정보보호위원회 고시)은 개인정보를 다루는 서비스라면 팀 규모와 상관없이 적용되는 최소 기준입니다. 개발팀이 구현해야 하는 일은 크게 다섯 가지입니다. 직무별 최소 권한을 주고 그 부여·변경·말소 이력을 남길 것, 인증 실패가 반복되면 접근을 막을 것, 관리자 접속기록을 남기고 보관·점검할 것, 비밀번호는 일방향 암호화하고 주민등록번호·계좌번호 같은 정보는 암호화해서 저장할 것, 인터넷 구간 전송을 암호화할 것.

"직원 세 명인 스타트업도 해당되나요?"라는 질문을 자주 받습니다. 해당됩니다. 고시에서 소규모 예외가 명시된 항목은 일부입니다. 예를 들어 1만 명 미만의 정보주체를 처리하는 소상공인·개인·단체는 내부관리계획 수립을 생략할 수 있지만, 접근통제·암호화·접속기록 같은 기술적 조치까지 면제되는 것은 아닙니다. 그래서 이 글은 조문 해설 대신 각 조항을 어떤 테이블, 어떤 로그, 어떤 설정으로 구현하는지에 맞춰 정리했습니다.

먼저 한 가지 짚고 가겠습니다. 이 글은 2023년 9월 22일 시행된 고시(개인정보보호위원회 고시 제2023-6호)와 2024년 10월 공개된 「개인정보의 안전성 확보조치 기준 안내서」를 기준으로 썼습니다. 고시는 개정될 수 있으니 실제 적용 전에는 국가법령정보센터와 개인정보보호위원회 누리집(법령 > 지침(가이드라인))에서 현행 원문과 안내서를 꼭 확인하시기 바랍니다. 아래 기간·조건은 원문 대조용 출발점으로 보시면 됩니다.

먼저 판단할 것: 우리 시스템에서 '개인정보처리시스템'은 어디인가

조항을 구현하기 전에 범위부터 정해야 합니다. 고시가 말하는 개인정보처리시스템은 고객 DB만 가리키지 않습니다. 개발 관점에서는 보통 다음이 모두 들어갑니다.

  • 회원·주문 데이터를 저장하는 운영 DB와 읽기 전용 복제본
  • 고객 정보를 조회하거나 수정할 수 있는 관리자 페이지(백오피스)
  • 개인정보를 내보내는 배치 작업, 데이터 웨어하우스, BI 도구
  • CS 도구나 CRM처럼 고객 정보를 동기화하는 외부 SaaS 연동

범위를 정하면서 처리하는 정보주체 수(5만 명 이상인지)와 고유식별정보·민감정보 처리 여부도 함께 확인해 두세요. 접속기록 보관 기간이 이 두 가지에 따라 달라집니다.

접근권한 관리: 권한 테이블과 '변경 이력' 테이블을 분리한다

고시는 업무에 필요한 최소한의 범위로 권한을 차등 부여하고, 권한의 부여·변경·말소 기록을 최소 3년간 보관하라고 요구합니다. 취급자별로 계정을 발급하고 공유하지 않아야 한다는 내용도 있습니다.

흔히 보이는 문제는 권한을 users.role 컬럼 하나로 관리하는 구조입니다. 값을 덮어쓰면 "누가, 언제, 누구에게, 무슨 권한을 줬는지"가 남지 않습니다.

-- 취약한 설계: 현재 상태만 있고 이력이 없다
CREATE TABLE admin_users (
  id         BIGSERIAL PRIMARY KEY,
  email      TEXT NOT NULL,
  role       TEXT NOT NULL  -- 'admin' 하나를 여러 명이 공유하기도 한다
);
-- 개선한 설계: 현재 권한 + 추가만 가능한(append-only) 이력
CREATE TABLE admin_role_grants (
  admin_id   BIGINT NOT NULL REFERENCES admin_users(id),
  role       TEXT   NOT NULL,       -- 'cs_viewer', 'cs_editor', 'billing' 등 직무 단위
  PRIMARY KEY (admin_id, role)
);

CREATE TABLE admin_role_history (
  id          BIGSERIAL PRIMARY KEY,
  admin_id    BIGINT      NOT NULL,
  role        TEXT        NOT NULL,
  action      TEXT        NOT NULL CHECK (action IN ('GRANT','CHANGE','REVOKE')),
  actor_id    BIGINT      NOT NULL, -- 권한을 변경한 사람
  reason      TEXT,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 애플리케이션 계정에서는 이력의 수정·삭제를 막는다
REVOKE UPDATE, DELETE ON admin_role_history FROM app_user;

구현 포인트는 이렇습니다.

  • 역할은 admin 하나로 두지 말고 조회와 수정, 마스킹 해제, 다운로드를 나눕니다. 다운로드 권한은 별도로 떼어 두는 편이 접속기록 점검에도 유리합니다.
  • 퇴사·부서 이동은 HR 이벤트나 SSO 그룹 변경과 연동해 자동으로 REVOKE되게 합니다. 사람이 손으로 하면 빠지는 경우가 생깁니다.
  • 공용 계정([email protected] 하나로 여럿이 로그인)은 없애고 1인 1계정으로 바꿉니다.

권한 체크를 API마다 서버에서 하는지도 함께 확인하세요. 역할을 잘게 나눠도 API가 객체 소유권을 확인하지 않으면 의미가 없습니다. 이 부분은 IDOR 취약점 방어법 글에서 자세히 다뤘습니다.

인증과 접근통제: 로그인 실패 제한, 외부 접속, 세션 만료

고시는 일정 횟수 이상 인증에 실패하면 접근을 제한하라고 하고, 외부에서 개인정보처리시스템에 접속할 때 인증서·보안토큰·일회용 비밀번호(OTP) 같은 안전한 인증수단을 쓰라고 요구합니다. 일정 시간 이상 업무 처리를 하지 않으면 접속을 자동으로 차단하는 조치, 개인정보가 홈페이지나 공유 설정으로 공개·노출되지 않도록 하는 조치도 들어 있습니다. 안내서는 인증수단의 예시를 문자메시지, 전화 인증, 소셜 로그인 등으로 넓혔고, 반복 실패 시 계정 잠금 외에 캡차나 재시도 시간 제한 같은 방법도 제시합니다.

// 취약한 예: 실패 횟수 제한 없음, 세션 만료 없음
app.post("/admin/login", async (req, res) => {
  const user = await findAdmin(req.body.email);
  if (user && await verify(req.body.password, user.hash)) {
    req.session.adminId = user.id; // 무기한 유지
    return res.redirect("/admin");
  }
  res.status(401).send("fail");
});
// 개선한 예: 실패 횟수 제한 + 2차 인증 + 유휴 만료
const MAX_FAIL = 5;
const LOCK_MINUTES = 15;

app.post("/admin/login", async (req, res) => {
  const key = `login_fail:${req.body.email}`;
  const fails = Number(await redis.get(key) ?? 0);
  if (fails >= MAX_FAIL) return res.status(429).send("잠시 후 다시 시도하세요");

  const user = await findAdmin(req.body.email);
  const ok = user && await verify(req.body.password, user.hash);
  if (!ok) {
    await redis.multi().incr(key).expire(key, LOCK_MINUTES * 60).exec();
    await auditLog({ type: "LOGIN_FAIL", email: req.body.email, ip: req.ip });
    return res.status(401).send("이메일 또는 비밀번호가 올바르지 않습니다");
  }
  await redis.del(key);
  req.session.pendingMfaAdminId = user.id; // OTP 확인 전에는 권한 없음
  res.redirect("/admin/mfa");
});

// 세션 설정: 유휴 30분 만료(rolling), 쿠키 보안 속성
app.use(session({
  rolling: true,
  cookie: { maxAge: 30 * 60 * 1000, httpOnly: true, secure: true, sameSite: "lax" },
}));

실패 횟수(5회)나 유휴 시간(30분)은 예시값입니다. 고시는 구체적인 숫자를 정해 두지 않았으니, 내부관리계획이나 보안 정책 문서에 우리 팀의 기준값을 적어 두는 것이 좋습니다. 관리자 페이지를 IP 허용 목록이나 VPN 뒤에 두는 것도 접근통제 조항과 직접 맞닿아 있습니다. 출시 전에 볼 항목은 관리자 페이지 보안 체크리스트에 정리해 두었습니다.

암호화: 무엇을, 어디서, 어떻게

암호화 조항은 개발팀에 가장 직접적인 영향을 주는 부분입니다. 고시 내용을 개발 작업으로 옮기면 다음과 같습니다.

대상고시 요구개발 작업
비밀번호복호화되지 않도록 일방향 암호화해 저장솔트가 들어가는 느린 해시(Argon2id, bcrypt 등)
주민등록번호, 여권·운전면허·외국인등록번호, 계좌번호, 카드번호 등저장 시 암호화컬럼 단위 암호화(AES-GCM 등) + 별도 키 관리
인터넷 구간 송수신안전한 암호 알고리즘으로 암호화전 구간 HTTPS, 내부 API·DB 연결도 TLS 검토
취급자 PC·모바일·보조저장매체저장 시 암호화디스크 암호화, 엑셀 다운로드 파일 관리

저장 위치(인터넷 구간, DMZ, 내부망)와 정보 종류에 따라 내부망 저장 시 영향평가·위험도 분석 결과로 암호화 여부를 판단할 수 있는 예외가 있습니다. 다만 주민등록번호는 그 예외에서 빠집니다. 우리 데이터가 어디에 해당하는지는 원문 조문으로 확인하세요.

비밀번호: 빠른 해시는 일방향이어도 부족하다

# 취약한 예: 솔트 없는 빠른 해시
import hashlib
pw_hash = hashlib.sha256(password.encode()).hexdigest()
# 개선한 예: Argon2id (argon2-cffi), 솔트는 라이브러리가 자동 생성
from argon2 import PasswordHasher
ph = PasswordHasher()
pw_hash = ph.hash(password)        # 저장
ph.verify(pw_hash, input_password) # 검증 (실패 시 예외)

SHA-256도 일방향이긴 하지만 솔트가 없으면 같은 비밀번호가 같은 해시가 되고, 계산이 빨라 대량 대입에 약합니다. 안내서가 솔트 추가를 고려하라고 명시한 것도 같은 이유입니다.

주민등록번호·계좌번호: 키를 DB 옆에 두지 않는다

컬럼을 암호화해도 키가 소스 코드나 같은 DB에 있으면 실질적인 보호 효과가 크게 줄어듭니다.

// 취약한 예: 키가 코드에 하드코딩되어 Git에 올라감
const KEY = "my-secret-key-1234";
// 개선한 예: 키는 KMS/Secret Manager에서 주입, 암호문에 키 버전 기록
import { createCipheriv, randomBytes } from "node:crypto";

const key = await secrets.get("pii-encryption-key"); // 32바이트, 런타임 주입
export function encryptPII(plain) {
  const iv = randomBytes(12);
  const c = createCipheriv("aes-256-gcm", key.value, iv);
  const enc = Buffer.concat([c.update(plain, "utf8"), c.final()]);
  return `v${key.version}:${iv.toString("base64")}:${c.getAuthTag().toString("base64")}:${enc.toString("base64")}`;
}

환경변수를 쓰더라도 .env 파일이 저장소, 도커 이미지, CI 로그에 섞여 들어가지 않는지 확인하세요. 키 버전을 암호문에 같이 남겨 두면 나중에 키를 교체할 때 다시 암호화하기 쉬워집니다. 검색이 필요한 컬럼이라면 원문 대신 HMAC 값을 별도 컬럼에 두는 방식을 검토합니다.

접속기록: 무엇을 남기고, 얼마나 보관하고, 언제 보는가

고시는 접속기록을 "식별자, 접속일시, 접속지 정보, 처리한 정보주체 정보, 수행업무"를 전자적으로 기록한 것으로 정의합니다. 보관 기간은 1년 이상이고, 5만 명 이상의 정보주체를 처리하거나 고유식별정보·민감정보를 처리하는 시스템(또는 등록·신고한 기간통신사업자)은 2년 이상입니다. 월 1회 이상 점검해야 하고, 개인정보를 다운로드한 것이 확인되면 내부관리계획에 정한 기준에 따라 그 사유를 확인해야 합니다. 위·변조와 도난, 분실을 막는 조치도 요구합니다.

일반 애플리케이션 로그(GET /admin/users/123 200)만으로는 "처리한 정보주체"와 "수행업무"를 알기 어렵습니다. 감사 로그는 따로 설계하는 편이 낫습니다.

{
  "ts": "2026-10-26T10:15:32+09:00",
  "actor_id": "admin_42",
  "actor_ip": "203.0.113.10",
  "action": "EXPORT",
  "resource": "customer",
  "subject_ids": ["cust_1001", "cust_1002"],
  "subject_count": 2,
  "fields": ["name", "phone"],
  "reason": "CS-2026-1033 환불 처리 대상 확인",
  "request_id": "req_8f2a"
}

설계할 때 지킬 것:

  • 행위자는 개인 식별자로 남깁니다. 공용 계정이면 기록이 있어도 누가 했는지 알 수 없습니다.
  • 수행업무는 조회·수정·삭제·다운로드처럼 구분합니다. 다운로드는 사유 입력을 필수로 받고 로그에 함께 남깁니다.
  • 로그에 개인정보 원문을 넣지 않습니다. 정보주체는 내부 ID로 남기고, 전화번호나 주민등록번호를 그대로 쓰지 않습니다.
  • 변경되지 않는 저장소에 둡니다. 별도 계정의 스토리지에 Object Lock(WORM) 같은 기능을 켜거나, 애플리케이션 계정에는 쓰기 권한만 주는 식으로 위·변조를 막습니다.
  • 보관 기간은 수명주기 정책으로 강제합니다. 1년이나 2년보다 짧게 지워지는 설정이 있는지 확인하고, 반대로 기간이 지난 로그를 지울 정책도 정합니다.
  • 월간 점검을 자동화합니다. 근무시간 외 대량 조회, 평소보다 많은 다운로드, 퇴사 예정자 계정의 활동 같은 조건으로 리포트를 만들고, 점검한 사람과 날짜를 기록합니다.

작은 팀을 위한 우선순위 체크리스트

한 번에 다 하기 어렵다면 아래 순서를 권합니다. 위쪽일수록 구현 비용 대비 효과가 큽니다.

  1. 비밀번호를 Argon2id 또는 bcrypt로 저장하고 있다
  2. 서비스 전 구간이 HTTPS이고, 관리자 페이지는 IP 제한 또는 VPN 뒤에 있다
  3. 관리자 계정은 1인 1계정이고, 2차 인증이 켜져 있다
  4. 로그인 실패 횟수 제한과 유휴 세션 만료가 있다
  5. 주민등록번호·계좌번호·카드번호 등은 컬럼 암호화하고, 키는 코드·DB 밖에 있다
  6. 권한 부여·변경·말소 이력이 append-only로 3년 이상 남는다
  7. 관리자 감사 로그에 식별자·일시·접속지·정보주체·수행업무가 남고, 기준 기간 이상 보관된다
  8. 다운로드 시 사유를 입력받고, 접속기록을 월 1회 이상 점검한 기록이 있다
  9. 퇴사 시 계정 회수 절차가 자동화되어 있다
  10. 원문 고시와 안내서를 대조해 우리 팀 기준값(실패 횟수, 유휴 시간, 보관 기간)을 문서로 남겼다

Supabase나 Firebase처럼 클라이언트가 DB에 직접 붙는 구조라면 2번과 6번의 의미가 조금 달라집니다. 이 경우 접근통제의 핵심은 행 단위 정책이 됩니다. 관련 내용은 Supabase RLS 보안 가이드를 참고하세요.

자주 묻는 질문

회원이 수백 명뿐인 초기 서비스도 지켜야 하나요?

네. 고시는 개인정보처리자 전반에 적용되는 최소 기준입니다. 1만 명 미만을 처리하는 소상공인·개인·단체는 내부관리계획 수립을 생략할 수 있는 등 일부 완화 규정이 있지만, 비밀번호 일방향 암호화나 전송 구간 암호화 같은 기술적 조치는 그대로 적용된다고 보고 준비하는 것이 안전합니다.

클라우드 DB의 저장 암호화(디스크 암호화)를 켜면 암호화 조항을 충족하나요?

저장 매체 수준의 보호는 되지만, DB에 접속할 수 있는 사람이나 애플리케이션에는 평문으로 보입니다. 주민등록번호나 계좌번호처럼 고시가 직접 지목한 정보는 컬럼 단위 암호화와 키 분리를 함께 검토하는 편이 좋습니다. 저장 위치에 따른 예외가 있는지는 원문 조문으로 확인하세요.

일반 사용자가 자기 정보를 조회한 기록도 접속기록으로 남겨야 하나요?

고시의 접속기록 보관 의무는 개인정보처리시스템에 접속하는 취급자를 대상으로 하며, 정보주체 본인의 접속은 제외됩니다. 따라서 우선순위는 관리자·운영자·배치 계정의 기록입니다. 다만 일반 사용자 로그도 보안 사고 분석에는 쓸모가 있으니, 별도 정책으로 운영하면 됩니다.

정리

  • 개인정보 안전성 확보조치 기준은 팀 규모와 상관없이 적용되며, 소규모 예외는 일부 관리적 항목에 한정됩니다.
  • 접근권한은 이력 테이블로, 접속기록은 위·변조를 막은 감사 로그로, 암호화는 느린 해시와 키를 분리한 컬럼 암호화로 구현합니다.
  • 실패 횟수나 유휴 시간 같은 숫자는 고시에 정해져 있지 않으니 팀 기준을 문서로 남기고, 보관 기간과 조건은 현행 고시 원문과 안내서로 반드시 대조하세요.
  • 우리 서비스가 이 기준에 얼마나 맞는지 직접 점검하기 어렵다면 무료 분석 신청으로 현재 상태를 확인해 볼 수 있습니다.

참고 자료

함께 읽으면 좋은 글