0. 학습 목표

→ 비밀번호를 평문으로 저장하지 않고, 해시값으로 저장한 뒤 로그인할 때 안전하게 비교하는 구조를 학습합니다.

더보기

이번 글에서 다룰 내용

이번 글에서는 로그인 회원 정보 중 비밀번호를 그대로 저장하지 않는 방법을 학습합니다.

6강에서는 회원 정보를 member.json 파일에 저장하고 다시 불러오는 구조를 만들었습니다.

하지만 비밀번호가 1234처럼 그대로 파일에 저장되면 누구나 파일을 열어 비밀번호를 확인할 수 있습니다.

그래서 이번 강의에서는 비밀번호를 Hash해시 값으로 바꾼 뒤 저장합니다.

구분 내용
핵심 개념 비밀번호를 평문으로 저장하지 않고 해시값으로 저장합니다.
실습 준비 Python의 hashlib 모듈, 기존 member.json 저장 구조, PySide6 로그인 화면
최종 목표 member.json에는 pw 대신 password_hash를 저장하고, 로그인할 때 입력 비밀번호를 해시로 바꿔 비교합니다.
평문 비밀번호 저장 방식과 해시 저장 방식 비교

이번 단계의 핵심: 비밀번호는 다시 꺼내서 보여주는 값이 아니라, 입력값과 비교하기 위한 해시값으로 저장해야 합니다.

 

1. 비밀번호를 그대로 저장하면 왜 위험할까?

→ member.json 파일에 비밀번호가 그대로 보이는 문제를 확인합니다.

더보기

1.1 6강에서 만든 member.json 구조

6강에서는 회원 정보를 member.json 파일에 저장했습니다.

그런데 현재 구조에서는 비밀번호가 아래처럼 그대로 보일 수 있습니다.

# 기존 member.json

{
    "id": "test",
    "pw": "1234",
    "name": "홍길동",
    "email": "test@test.com",
    "phone": "010-1234-5678"
}

이렇게 저장된 비밀번호를 평문 비밀번호라고 합니다.

평문은 사람이 바로 읽을 수 있는 원래 문장이나 값을 의미합니다.

저장 방식 문제점
"pw": "1234" 파일을 열면 비밀번호가 그대로 보입니다.
코드 안에 비밀번호 작성 코드를 보는 사람이 비밀번호를 알 수 있습니다.
파일에 비밀번호 그대로 저장 파일이 노출되면 비밀번호도 함께 노출됩니다.

 

1.2 암호화와 해시의 차이

비밀번호를 안전하게 저장한다고 할 때 흔히 “암호화”라는 말을 사용합니다.

하지만 로그인 비밀번호 저장에서는 보통 Encryption암호화보다 해시 방식을 사용합니다.

구분 설명
암호화 비밀번호를 암호문으로 바꾸고, 필요하면 다시 원래 비밀번호로 복호화할 수 있습니다.
해시 비밀번호를 해시값으로 바꾸고, 원래 비밀번호로 되돌리기 어렵습니다.
# 암호화 개념

비밀번호
    ↓
암호문
    ↓
다시 원래 비밀번호로 복호화 가능
# 해시 개념

비밀번호
    ↓
해시값
    ↓
원래 비밀번호로 되돌리기 어려움

 

1.3 이번 강의에서 바꿀 저장 구조

이번 강의에서는 member.json에 pw를 그대로 저장하지 않습니다.

대신 password_hash라는 이름으로 해시값을 저장합니다.

# 새 member.json 구조 예시

{
    "id": "test",
    "password_hash": "03ac674216f3e15c...",
    "name": "홍길동",
    "email": "test@test.com",
    "phone": "010-1234-5678"
}

로그인할 때는 사용자가 입력한 비밀번호를 다시 해시값으로 바꿉니다.

그리고 저장된 password_hash와 비교합니다.

문제의 핵심: 비밀번호 원문을 저장하지 않고, 비밀번호를 변환한 해시값만 저장해야 합니다.

 

2. 비밀번호를 해시로 바꾸는 함수 만들기

→ hashlib를 사용해 입력 비밀번호를 해시 문자열로 변환합니다.

더보기

2.1 프로젝트 구조 수정하기

6강에서 만든 파일 저장 구조를 그대로 사용합니다.

이번 강의에서는 비밀번호 해시 처리를 담당하는 password_utils.py 파일만 추가합니다.

# 프로젝트 구조

tab_login_file_storage/
    ├── main.py
    ├── member_model.py
    ├── member_storage.py
    ├── password_utils.py
    └── tab_window.py
파일 역할
password_utils.py 비밀번호를 해시값으로 바꾸는 함수를 담당합니다.
member_storage.py member.json 파일 저장과 불러오기를 담당합니다.
member_model.py 로그인 검사와 프로필 수정을 담당합니다.
tab_window.py 화면과 버튼 동작을 담당합니다.

 

2.2 password_utils.py 작성하기

먼저 비밀번호를 해시값으로 바꾸는 함수를 만듭니다.

Python의 hashlib 모듈을 사용하면 문자열을 해시값으로 바꿀 수 있습니다.

import hashlib


def hash_password(password):
    """입력한 비밀번호를 SHA-256 해시 문자열로 변환합니다."""
    return hashlib.sha256(password.encode("utf-8")).hexdigest()

이 함수는 사용자가 입력한 비밀번호를 UTF-8 바이트로 바꾼 뒤 SHA-256 해시값으로 변환합니다.

마지막의 hexdigest()는 해시 결과를 사람이 읽을 수 있는 16진수 문자열로 바꿉니다.

코드 역할
password.encode("utf-8") 문자열 비밀번호를 바이트 데이터로 바꿉니다.
hashlib.sha256(...) SHA-256 해시 계산을 수행합니다.
hexdigest() 해시 결과를 16진수 문자열로 반환합니다.

 

2.3 해시 결과 확인하기

예를 들어 비밀번호가 1234라면 아래처럼 해시값으로 바뀝니다.

from password_utils import hash_password

print(hash_password("1234"))

실행 결과는 아래처럼 긴 문자열입니다.

03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4

이제 member.json에는 1234를 그대로 저장하지 않고, 위와 같은 해시 문자열을 저장할 수 있습니다.

핵심 확인: 같은 비밀번호는 같은 해시값으로 바뀝니다. 그래서 로그인할 때 입력한 비밀번호를 다시 해시로 바꿔 저장된 해시값과 비교할 수 있습니다.

 

3. member.json에 password_hash 저장하기

→ 기존 pw 저장 구조를 password_hash 저장 구조로 변경합니다.

더보기

3.1 member_storage.py 수정하기

이제 기본 회원 정보를 만들 때 pw 대신 password_hash를 저장하도록 바꿉니다.

또한 6강에서 만든 member.json 파일에 pw가 남아 있을 수도 있으므로, 기존 pw 값을 password_hash로 바꿔주는 처리도 함께 넣습니다.

import json

from PySide6.QtCore import QFile, QIODevice, QTextStream

from password_utils import hash_password


class MemberStorage:
    """회원 정보를 JSON 파일에 저장하고 불러오는 클래스"""

    def __init__(self, file_path="member.json"):
        self.file_path = file_path

    def default_member(self):
        return {
            "id": "test",
            "password_hash": hash_password("1234"),
            "name": "홍길동",
            "email": "test@test.com",
            "phone": "010-1234-5678",
        }

    def load_member(self):
        if not QFile.exists(self.file_path):
            member = self.default_member()
            self.save_member(member)
            return member

        file = QFile(self.file_path)

        if not file.open(QIODevice.OpenModeFlag.ReadOnly | QIODevice.OpenModeFlag.Text):
            return self.default_member()

        stream = QTextStream(file)
        json_text = stream.readAll()
        file.close()

        try:
            member = json.loads(json_text)
        except json.JSONDecodeError:
            member = self.default_member()
            self.save_member(member)
            return member

        member = self.convert_plain_password(member)
        self.save_member(member)

        return member

    def convert_plain_password(self, member):
        if "pw" in member and "password_hash" not in member:
            member["password_hash"] = hash_password(member["pw"])
            del member["pw"]

        return member

    def save_member(self, member):
        file = QFile(self.file_path)

        if not file.open(QIODevice.OpenModeFlag.WriteOnly | QIODevice.OpenModeFlag.Text):
            return False

        stream = QTextStream(file)
        stream << json.dumps(member, ensure_ascii=False, indent=4)
        file.close()

        return True

 

3.2 default_member() 변경 확인

기존에는 기본 회원 정보에 pw가 들어 있었습니다.

# 기존 방식

"pw": "1234"

이제는 password_hash에 해시값을 저장합니다.

# 새 방식

"password_hash": hash_password("1234")

따라서 member.json 파일에는 비밀번호 1234가 직접 저장되지 않습니다.

 

3.3 기존 pw를 password_hash로 바꾸는 이유

이미 6강을 실행했다면 프로젝트 폴더에 기존 member.json 파일이 있을 수 있습니다.

그 파일에는 아직 pw가 남아 있을 수 있습니다.

# 기존 파일에 남아 있을 수 있는 구조

{
    "id": "test",
    "pw": "1234",
    "name": "홍길동",
    "email": "test@test.com",
    "phone": "010-1234-5678"
}

그래서 load_member()에서 파일을 읽은 뒤 convert_plain_password()를 호출합니다.

member = self.convert_plain_password(member)
self.save_member(member)

이 코드는 기존 pw 값을 password_hash로 바꾸고, 다시 member.json 파일에 저장합니다.

주의할 점: 기존 member.json 파일이 남아 있으면 새 코드만 작성해도 파일 내용이 바로 바뀌지 않을 수 있습니다. 그래서 기존 pw를 password_hash로 변환하는 처리를 넣어 줍니다.

 

4. 로그인할 때 해시값으로 비교하기

→ 입력한 비밀번호를 해시로 바꾼 뒤 저장된 password_hash와 비교합니다.

더보기

4.1 member_model.py 수정하기

이제 MemberModel의 로그인 검사 방식을 바꿉니다.

입력한 비밀번호를 그대로 비교하지 않고, 해시값으로 바꾼 뒤 비교합니다.

from member_storage import MemberStorage
from password_utils import hash_password


class MemberModel:
    """회원 정보 관리, 로그인 검사, 프로필 수정을 담당하는 클래스"""

    def __init__(self):
        self.storage = MemberStorage()
        self.member = self.storage.load_member()
        self.is_logged_in = False

    def check_login(self, user_id, user_pw):
        input_password_hash = hash_password(user_pw)

        if user_id == self.member["id"] and input_password_hash == self.member["password_hash"]:
            self.is_logged_in = True
            return True

        return False

    def get_member_info(self):
        if not self.is_logged_in:
            return None

        return {
            "id": self.member["id"],
            "name": self.member["name"],
            "email": self.member["email"],
            "phone": self.member["phone"],
        }

    def update_profile(self, name, email, phone):
        if not self.is_logged_in:
            return False

        self.member["name"] = name
        self.member["email"] = email
        self.member["phone"] = phone

        return self.storage.save_member(self.member)

 

4.2 check_login() 코드 흐름

로그인 검사는 아래 순서로 진행됩니다.

# 로그인 검증 흐름

사용자가 ID, PW 입력
    ↓
입력한 PW를 hash_password()로 해시 변환
    ↓
입력 ID와 저장 ID 비교
    ↓
입력 PW 해시값과 저장 password_hash 비교
    ↓
둘 다 같으면 로그인 성공

핵심 코드는 아래 부분입니다.

input_password_hash = hash_password(user_pw)

이 코드는 사용자가 입력한 비밀번호를 해시값으로 바꿉니다.

input_password_hash == self.member["password_hash"]

이 코드는 입력 비밀번호의 해시값과 파일에 저장된 해시값이 같은지 비교합니다.

 

4.3 tab_window.py는 거의 그대로 사용하기

화면 코드인 tab_window.py는 거의 수정하지 않아도 됩니다.

LoginView는 여전히 사용자가 입력한 ID와 PW를 MemberModel에 전달합니다.

if self.member_model.check_login(user_id, user_pw):
    self.message_label.setText("로그인 성공!")

    self.main_view.refresh()
    self.profile_view.refresh()

    self.tab_widget.setCurrentIndex(1)
else:
    self.message_label.setText("로그인 실패! 아이디 또는 비밀번호를 확인하세요.")

LoginView는 비밀번호가 해시로 저장되는지 직접 알 필요가 없습니다.

비밀번호 해시 비교는 MemberModel이 담당합니다.

핵심 확인: View는 입력만 받고, 비밀번호를 어떻게 비교할지는 Model이 처리합니다.

 

5. 실행 결과 확인하기

→ member.json에 pw가 사라지고 password_hash가 저장되는지 확인합니다.

더보기

5.1 실행하기

터미널에서 프로젝트 폴더로 이동한 뒤 아래 명령어를 실행합니다.

python main.py

기존 member.json에 pw가 있었다면, 프로그램 실행 후 password_hash로 변환되어 다시 저장됩니다.

 

5.2 member.json 확인하기

프로그램 실행 후 member.json 파일을 열어 봅니다.

이제 pw가 없어지고 password_hash가 저장되어 있어야 합니다.

# 변경 후 member.json 예시

{
    "id": "test",
    "name": "홍길동",
    "email": "test@test.com",
    "phone": "010-1234-5678",
    "password_hash": "03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4"
}

이제 파일을 열어도 원래 비밀번호인 1234는 보이지 않습니다.

 

5.3 로그인 테스트하기

로그인 화면에서 기존과 동일하게 입력합니다.

항목 입력값
아이디 test
비밀번호 1234

member.json에는 1234가 없지만 로그인은 성공해야 합니다.

왜냐하면 입력한 1234를 다시 해시로 바꾼 뒤, 저장된 password_hash와 비교하기 때문입니다.

# 로그인 성공 흐름

입력 비밀번호: 1234
    ↓
hash_password("1234")
    ↓
03ac674216f3e15c...
    ↓
member.json의 password_hash와 비교
    ↓
같으면 로그인 성공

 

5.4 확인 체크리스트

아래 항목을 직접 확인합니다.

확인 항목 완료
password_utils.py 파일을 만들었는가?
hash_password() 함수가 비밀번호를 해시 문자열로 바꾸는가?
member.json에서 pw가 사라지고 password_hash가 저장되는가?
로그인할 때 입력 비밀번호를 해시로 바꿔 비교하는가?
비밀번호 1234로 정상 로그인되는가?
틀린 비밀번호를 입력하면 로그인에 실패하는가?

 

6. 평문 저장 방식과 해시 저장 방식 비교하기

→ 비밀번호를 그대로 저장하는 방식과 해시값으로 저장하는 방식을 비교하며 정리합니다.

더보기

6.1 기존 방식

기존 방식은 비밀번호를 그대로 저장하는 구조입니다.

# 기존 방식

member.json
    └── "pw": "1234"

이 방식은 구현은 쉽지만 파일을 열면 비밀번호가 바로 보입니다.

 

6.2 새 방식

새 방식은 비밀번호를 해시값으로 바꿔 저장하는 구조입니다.

# 새 방식

입력 비밀번호
    ↓
hash_password()
    ↓
password_hash
    ↓
member.json에 저장

이 방식에서는 member.json을 열어도 원래 비밀번호가 바로 보이지 않습니다.

구분 평문 저장 방식 해시 저장 방식
저장 값 "pw": "1234" "password_hash": "03ac..."
파일 확인 비밀번호가 그대로 보입니다. 원래 비밀번호가 바로 보이지 않습니다.
로그인 비교 입력 비밀번호와 저장 비밀번호를 직접 비교합니다. 입력 비밀번호를 해시로 바꾼 뒤 password_hash와 비교합니다.
학습 목적 처음 구조 이해에는 쉽습니다. 더 안전한 저장 구조를 이해할 수 있습니다.

 

6.3 실제 서비스에서는 더 안전한 방식이 필요합니다

이번 강의에서는 학습을 위해 SHA-256을 사용했습니다.

하지만 실제 서비스에서는 단순 SHA-256만으로 비밀번호를 저장하는 것은 충분하지 않습니다.

실제 서비스에서는 Salt솔트를 추가하고, 비밀번호 저장에 특화된 더 느린 해시 알고리즘을 사용하는 것이 일반적입니다.

구분 설명
이번 강의 개념 학습을 위해 hashlib.sha256()을 사용합니다.
실제 서비스 salt, bcrypt, scrypt, argon2 같은 비밀번호 저장 전용 방식을 사용하는 것이 좋습니다.

 

6.4 최종 정리

이번 글에서는 비밀번호를 평문으로 저장하지 않고 해시값으로 저장하는 구조를 만들었습니다.

핵심 내용 정리
문제 파악 member.json에 pw가 그대로 저장되면 비밀번호가 노출될 수 있습니다.
문제 해결 hash_password() 함수를 만들어 비밀번호를 해시값으로 바꿨습니다.
저장 구조 변경 pw 대신 password_hash를 member.json에 저장했습니다.
로그인 검증 입력 비밀번호를 해시로 바꾼 뒤 저장된 password_hash와 비교했습니다.
주의점 실제 서비스에서는 salt와 비밀번호 저장 전용 해시 알고리즘이 필요합니다.

기억할 문장: 비밀번호는 그대로 저장하지 않고, 입력값을 해시로 바꿔 저장된 해시값과 비교해야 합니다.

 

6.4 프로젝트 파일

657.zip
0.01MB

 

참고. 공식 문서로 확인하기

→ hashlib와 JSON 저장 방식의 자세한 내용은 공식 문서에서 확인합니다.

더보기

참고 문서

이번 글에서 사용한 hashlib, json 모듈은 아래 문서에서 자세히 확인할 수 있습니다.

참고: 이번 강의의 SHA-256 방식은 해시 개념을 이해하기 위한 학습용입니다. 실제 서비스에서는 salt와 bcrypt, scrypt, argon2 같은 비밀번호 저장 전용 방식을 검토해야 합니다.