Django 베타테스터 일괄 생성 완벽 가이드 (HMAC + Fernet 암호화 적용 + Admin 500 에러 트러블슈팅)
Django 베타테스터 일괄 생성은 어떻게 할 수 있을까요?
프로젝트를 진행하면서 내부 사용자 대상 베타 테스트를 진행할 때, 50명, 100명, 심지어 수백 명의 테스트 계정을 수동으로 생성하는 것은 현실적으로 불가능in the file.
관리자 한 명이 일일이 회원가입을 받고 승인하는 과정은 시간 낭비일 뿐 아니라, 사번(employee_no)과 이름 같은 민감 정보를 평문으로 저장하면 보안 사고로 직결됩니다.
그래서 우리는 Django shell 스크립트 한 방으로 해결했습니다.
HMAC-SHA256 해시 + Fernet(AES-128) 암호화를 적용하고, 즉시 승인(approved) 상태로 생성하는 실전용 Django 베타테스터 일괄 생성 스크립트를 공개합니다.
In this post, we'll use the 초기 스크립트의 문제점, 수정된 완성 스크립트, Admin에서 500 에러가 발생한 트러블슈팅 과정and 안전하게 운영하는 방법까지 모두 다룹니다.

왜 베타테스터를 일괄 생성해야 하는가?
베타 테스트 단계에서는 최소 50명 이상의 실제 사용자(사번 보유 직원)로 테스트를 해야 합니다.
수동으로 50명을 생성하려면:
- 50번의 회원가입 + 이메일 확인
- 관리자 승인 작업
- 비밀번호 초기화 및 전달
이 과정만으로도 반나절 이상이 소요됩니다.
게다가 사번을 평문으로 저장하면 DB 유출 시 1차 피해가 발생합니다.
Solution: 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)}개의 베타테스터 계정 생성 완료")이 스크립트의 문제점:
employee_nofield in the 평문 사번이 그대로 저장됨 (HMAC 해시가 아님)CustomUser모델의EncryptedCharField가 적용되지 않아employee_no_plainandname이 제대로 암호화되지 않음- Admin에서 삭제 시 500 Internal Server Error 발생 (암호화 필드 처리 중 예외)
해결: 완성된 fix_beta_users.py 스크립트

Below is the 실제 운영 중인 검증된 스크립트.
두 단계로 구성되어 있습니다.
# 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_id→ ΒEncryptedCharFieldThe 자동으로 Fernet 암호화status=AccountStatus.APPROVED→ 관리자 승인 없이 즉시 로그인 가능- 예외 처리와 중복 체크로 안정성 확보
실행 방법 (3단계)
- 스크립트 파일 생성
cat <<'EOF' > fix_beta_users.py
# 위 스크립트 전체 붙여넣기
EOF- Django shell에서 실행
cd /home/
python manage.py shell < fix_beta_users.py- 생성 확인
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 (/admin/accounts/customuser/)에서 계정을 선택하고 삭제 버튼을 누르면 500 Internal Server Error가 발생하고, 브라우저 콘솔에 POST ... 500이 찍힘.
원인 분석:
- Admin의
delete_selected액션이 실행될 때,CustomUser모델의save()또는 관련 signal이 트리거됨 employee_no가 평문으로 저장된 상태에서hmac_of()나 암호화 로직이 실행되면서 예외 발생EncryptedCharFieldThefrom_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()}개')
"This command uses the Admin을 거치지 않고 ORM으로 직접 삭제하기 때문에 500 에러가 발생하지 않습니다.
삭제 후 위 fix_beta_users.py를 다시 실행하면 깔끔하게 재생성됩니다.
로그인 테스트 방법
생성된 베타테스터 계정 정보:
| 사번 범위 | 비밀번호 | 상태 |
|---|---|---|
| 10000000 ~ 10000009 | 동일 | APPROVED |
| 20000000 ~ 20000009 | 동일 | APPROVED |
| … | … | … |
| 50000000 ~ 50000009 | 동일 | APPROVED |
로그인 화면In:
- 사번:
10000000 - 비밀번호:
10000000
바로 로그인되어 프로젝트의 모든 기능을 테스트할 수 있습니다.
Django 베타테스터 일괄 생성 시 보안 고려사항 및 확장 팁
비밀번호 정책 강화 (실제 운영 시)
- 베타 단계에서는
사번 = 비밀번호로 편의성을 우선했지만, - 실제 서비스에서는 강력한 초기 비밀번호 + 이메일 전송 방식으로 변경하세요.
대량 생성 시 성능
- 1000명 이상 생성할 때는
bulk_create+set_password를 별도 처리하는 것이 좋습니다. django.db.transaction.atomic()으로 묶어서 DB 부하를 줄이세요.
로그 기록
import logging
logger = logging.getLogger(__name__)
logger.info(f"베타테스터 생성 완료: {emp_id}")- CSV 파일로부터 읽어오기
- 사번 목록을 Excel/CSV로 받아서 처리하는 버전으로 확장 가능합니다.
Conclusion
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 동작 방식)






