|

Django 베타테스터 일괄 생성 완벽 가이드 (HMAC + Fernet 암호화 적용 + Admin 500 에러 트러블슈팅)

Django 베타테스터 일괄 생성은 어떻게 할 수 있을까요?

프로젝트를 진행하면서 내부 사용자 대상 베타 테스트를 진행할 때, 50명, 100명, 심지어 수백 명의 테스트 계정을 수동으로 생성하는 것은 현실적으로 불가능합니다.

관리자 한 명이 일일이 회원가입을 받고 승인하는 과정은 시간 낭비일 뿐 아니라, 사번(employee_no)과 이름 같은 민감 정보를 평문으로 저장하면 보안 사고로 직결됩니다.

그래서 우리는 Django shell 스크립트 한 방으로 해결했습니다.
HMAC-SHA256 해시 + Fernet(AES-128) 암호화를 적용하고, 즉시 승인(approved) 상태로 생성하는 실전용 Django 베타테스터 일괄 생성 스크립트를 공개합니다.

이 포스트에서는 초기 스크립트의 문제점, 수정된 완성 스크립트, Admin에서 500 에러가 발생한 트러블슈팅 과정, 그리고 안전하게 운영하는 방법까지 모두 다룹니다.

Django 베타테스터 일괄 생성 스크립트 실행 중 IntegrityError 및 평문 저장 문제 터미널 로그

왜 베타테스터를 일괄 생성해야 하는가?

베타 테스트 단계에서는 최소 50명 이상의 실제 사용자(사번 보유 직원)로 테스트를 해야 합니다.
수동으로 50명을 생성하려면:

  • 50번의 회원가입 + 이메일 확인
  • 관리자 승인 작업
  • 비밀번호 초기화 및 전달

이 과정만으로도 반나절 이상이 소요됩니다.
게다가 사번을 평문으로 저장하면 DB 유출 시 1차 피해가 발생합니다.

해결책: Django shell + 암호화 로직을 결합한 자동화 스크립트입니다.

문제 발생: 초기 스크립트의 치명적 버그

처음에 작성한 스크립트는 다음과 같았습니다.

# create_beta_users.py (문제 버전)
from django.contrib.auth import get_user_model
from django.db import IntegrityError

User = get_user_model()

prefixes = [10000000, 20000000, 30000000, 40000000, 50000000]
count_per_prefix = 10
created_accounts = []

for prefix in prefixes:
    for i in range(count_per_prefix):
        emp_id = str(prefix + i)
        try:
            user_kwargs = {
                User.USERNAME_FIELD: emp_id,   # ← 여기서 평문 사번이 그대로 저장됨
            }
            # name 필드가 NOT NULL이면 주석 해제 필요
            # user_kwargs['name'] = f'베타테스터_{emp_id}'

            user = User(**user_kwargs)
            user.set_password(emp_id)          # 비밀번호 = 사번 (평문)
            user.save()

            created_accounts.append(emp_id)
            print(f" 생성 완료: 사번 {emp_id} / 비번 {emp_id}")

        except IntegrityError:
            print(f" 이미 존재하는 사번입니다: {emp_id}")
        except Exception as e:
            print(f" 생성 실패 ({emp_id}): {e}")

print(f" 총 {len(created_accounts)}개의 베타테스터 계정 생성 완료")

이 스크립트의 문제점:

  1. employee_no 필드에 평문 사번이 그대로 저장됨 (HMAC 해시가 아님)
  2. CustomUser 모델의 EncryptedCharField가 적용되지 않아 employee_no_plainname이 제대로 암호화되지 않음
  3. Admin에서 삭제 시 500 Internal Server Error 발생 (암호화 필드 처리 중 예외)

해결: 완성된 fix_beta_users.py 스크립트

Django shell에서 fix_beta_users.py 실행 후 50개 베타테스터 계정 생성 완료 로그 화면

아래는 실제 운영 중인 검증된 스크립트입니다.
두 단계로 구성되어 있습니다.

# fix_beta_users.py (최종 검증 완료 버전)
from apps.accounts.models import CustomUser, AccountStatus
from apps.accounts.encryption import hmac_of
from django.utils import timezone

# ============================================
# 1단계: 잘못 생성된 계정(평문 저장) 삭제
# ============================================
# employee_no가 64자리 hex 문자열(0-9a-f)이 아닌 경우 = 평문으로 잘못 저장된 계정
bad = CustomUser.objects.exclude(employee_no__regex=r'^[0-9a-f]{64}$')
print(f"잘못 생성된 계정 {bad.count()}개 삭제 중...")
bad.delete()
print("삭제 완료\n")

# ============================================
# 2단계: 올바른 방식으로 재생성 (HMAC + Fernet + 즉시 승인)
# ============================================
prefixes = [10000000, 20000000, 30000000, 40000000, 50000000]
created = []

for prefix in prefixes:
    for i in range(10):
        emp_id = str(prefix + i)           # 예: 10000000, 10000001 ...
        hashed = hmac_of(emp_id)           # HMAC-SHA256 해시 생성 (64자 hex)

        # 이미 존재하는 경우 건너뛰기
        if CustomUser.objects.filter(employee_no=hashed).exists():
            print(f"이미 존재: {emp_id}")
            continue

        # CustomUser 생성 (암호화 필드 자동 처리)
        user = CustomUser(
            employee_no=hashed,                    # HMAC 해시 저장 (조회용)
            role='employee',                       # 기본 역할
            status=AccountStatus.APPROVED,         # 즉시 승인 상태
            approved_at=timezone.now(),            # 승인 일시 기록
        )
        user.employee_no_plain = emp_id   # EncryptedCharField → 자동 Fernet 암호화
        user.name = f'베타테스터'         # EncryptedCharField → 자동 Fernet 암호화
        user.set_password(emp_id)         # 비밀번호 = 사번 (PBKDF2 해시)
        user.save()

        created.append(emp_id)
        print(f"생성 완료: 사번 {emp_id} / 비번 {emp_id}")

print(f"\n총 {len(created)}개 계정 생성 완료")
print("=" * 50)

이 스크립트의 핵심 포인트:

  • hmac_of(emp_id)복호화 불가능한 해시로 저장 (보안 핵심)
  • user.employee_no_plain = emp_idEncryptedCharField자동으로 Fernet 암호화
  • status=AccountStatus.APPROVED → 관리자 승인 없이 즉시 로그인 가능
  • 예외 처리와 중복 체크로 안정성 확보

실행 방법 (3단계)

  1. 스크립트 파일 생성
   cat <<'EOF' > fix_beta_users.py
   # 위 스크립트 전체 붙여넣기
   EOF
  1. Django shell에서 실행
   cd /home/
   python manage.py shell < fix_beta_users.py
  1. 생성 확인
   python manage.py shell -c "
   from apps.accounts.models import CustomUser
   print(f'총 계정 수: {CustomUser.objects.count()}')
   print('샘플 5명:')
   for u in CustomUser.objects.all()[:5]:
       print(f'  {u.employee_no_plain} / {u.name} / {u.status}')
   "

트러블슈팅: Admin에서 500 에러가 발생할 때

Django Admin 500 Internal Server Error 화면과 shell 명령어로 해결하는 전후 비교 인포그래픽

증상:
Django Admin (/admin/accounts/customuser/)에서 계정을 선택하고 삭제 버튼을 누르면 500 Internal Server Error가 발생하고, 브라우저 콘솔에 POST ... 500이 찍힘.

원인 분석:

  • Admin의 delete_selected 액션이 실행될 때, CustomUser 모델의 save() 또는 관련 signal이 트리거됨
  • employee_no가 평문으로 저장된 상태에서 hmac_of()나 암호화 로직이 실행되면서 예외 발생
  • EncryptedCharFieldfrom_db_value / get_prep_value에서 복호화 실패

해결 방법 (가장 빠르고 안전):

# 1. 잘못 생성된 계정만 정확히 삭제 (정규식으로 64자 hex가 아닌 것만)
python manage.py shell -c "
from apps.accounts.models import CustomUser
bad = CustomUser.objects.exclude(employee_no__regex=r'^[0-9a-f]{64}$')
count = bad.count()
print(f'삭제 대상: {count}개')
bad.delete()
print(f'{count}개 삭제 완료')
print(f'남은 계정: {CustomUser.objects.count()}개')
"

이 명령어는 Admin을 거치지 않고 ORM으로 직접 삭제하기 때문에 500 에러가 발생하지 않습니다.

삭제 후 위 fix_beta_users.py를 다시 실행하면 깔끔하게 재생성됩니다.

로그인 테스트 방법

생성된 베타테스터 계정 정보:

사번 범위비밀번호상태
10000000 ~ 10000009동일APPROVED
20000000 ~ 20000009동일APPROVED
50000000 ~ 50000009동일APPROVED

로그인 화면에서:

  • 사번: 10000000
  • 비밀번호: 10000000

바로 로그인되어 프로젝트의 모든 기능을 테스트할 수 있습니다.

Django 베타테스터 일괄 생성 시 보안 고려사항 및 확장 팁

비밀번호 정책 강화 (실제 운영 시)

    • 베타 단계에서는 사번 = 비밀번호로 편의성을 우선했지만,
    • 실제 서비스에서는 강력한 초기 비밀번호 + 이메일 전송 방식으로 변경하세요.

    대량 생성 시 성능

      • 1000명 이상 생성할 때는 bulk_create + set_password를 별도 처리하는 것이 좋습니다.
      • django.db.transaction.atomic()으로 묶어서 DB 부하를 줄이세요.

      로그 기록

           import logging
           logger = logging.getLogger(__name__)
           logger.info(f"베타테스터 생성 완료: {emp_id}")
        1. CSV 파일로부터 읽어오기
        • 사번 목록을 Excel/CSV로 받아서 처리하는 버전으로 확장 가능합니다.

        결론

        Django 베타테스터 일괄 생성은 단순한 편의 기능이 아닙니다.
        보안(암호화)과 운영 효율성(자동화)을 동시에 잡는 필수 기술입니다.

        내부 보안이 중요한 시스템에서는
        HMAC + Fernet 조합으로 “DB가 유출되어도 안전한” 구조를 만드는 것이 핵심입니다.

        위 스크립트를 그대로 복사해서 사용하시고,
        Admin 500 에러가 발생하면 shell 명령어로 직접 삭제하는 트러블슈팅을 기억하세요.

        이제 50명의 베타테스터가 준비되었습니다.
        실제 사용자 피드백을 받아 프로젝트를 더 완성도 있게 만들어 보세요!


        출처 및 참고 자료

        • Django 공식 문서: Customizing authentication (AbstractBaseUser, USERNAME_FIELD)
        • Django 공식 문서: django.db.models.query.QuerySet.exclude() 및 regex lookup
        • cryptography 라이브러리 문서: Fernet (symmetric encryption)
        • OWASP: Password Storage Cheat Sheet (PBKDF2 권장)
        • Django Admin actions 소스 코드 분석 (delete_selected 동작 방식)

        유사한 게시물

        답글 남기기

        이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다