본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 4. · 15 Views
데이터 처리와 암호화 완벽 가이드
분산 시스템의 CAP 정리부터 데이터 압축, 암호화까지 초급 개발자가 반드시 알아야 할 핵심 개념을 다룹니다. 실무에서 자주 마주치는 보안과 성능 최적화의 기초를 탄탄하게 다져봅시다.
목차
1. CAP 정리 이해
어느 날 김개발 씨는 회사에서 새로운 분산 시스템 프로젝트에 투입되었습니다. 아키텍처 회의에서 선배가 "우리 시스템은 AP를 선택했어요"라고 말하는데, 도무지 무슨 뜻인지 알 수 없었습니다.
분산 시스템을 다루려면 반드시 알아야 할 CAP 정리, 지금부터 함께 알아보겠습니다.
CAP 정리는 분산 시스템에서 일관성(Consistency), 가용성(Availability), 분할 내성(Partition Tolerance) 세 가지를 동시에 완벽하게 만족시킬 수 없다는 이론입니다. 마치 "빠르고, 싸고, 좋은 것" 세 가지를 동시에 가질 수 없는 것과 같습니다.
이 정리를 이해하면 시스템 설계 시 어떤 특성을 우선할지 현명한 선택을 할 수 있습니다.
다음 코드를 살펴봅시다.
# CAP 정리를 시뮬레이션하는 간단한 분산 시스템 예제
class DistributedNode:
def __init__(self, node_id):
self.node_id = node_id
self.data = {}
self.is_available = True
# 일관성을 보장하는 쓰기 작업
def write_consistent(self, key, value, other_nodes):
# 모든 노드에 동기화 완료 후 응답 (일관성 우선)
for node in other_nodes:
if not node.is_available:
raise Exception("일관성 보장 불가 - 노드 불가용")
node.data[key] = value
self.data[key] = value
return "쓰기 완료 - 모든 노드 동기화됨"
# 가용성을 보장하는 쓰기 작업
def write_available(self, key, value):
# 즉시 응답 (가용성 우선, 나중에 동기화)
self.data[key] = value
return "쓰기 완료 - 나중에 동기화 예정"
김개발 씨는 입사 6개월 차 주니어 개발자입니다. 최근 회사에서 사용자가 급증하면서 단일 서버로는 감당이 안 되기 시작했습니다.
자연스럽게 분산 시스템 도입 논의가 시작되었고, 김개발 씨도 아키텍처 회의에 참석하게 되었습니다. 회의실에서 선배 개발자 박시니어 씨가 화이트보드에 세 개의 원을 그렸습니다.
"자, 분산 시스템을 설계할 때 우리는 선택을 해야 합니다. CAP 정리라고 들어보셨나요?" 그렇다면 CAP 정리란 정확히 무엇일까요?
쉽게 비유하자면, CAP 정리는 마치 삼각형의 세 꼭짓점과 같습니다. 세 꼭짓점을 모두 동시에 만질 수는 없듯이, 분산 시스템도 세 가지 특성을 완벽하게 동시에 가질 수 없습니다.
**C는 일관성(Consistency)**으로, 모든 노드가 같은 시점에 같은 데이터를 보는 것입니다. **A는 가용성(Availability)**으로, 요청하면 항상 응답을 받을 수 있다는 것입니다.
**P는 분할 내성(Partition Tolerance)**으로, 네트워크가 끊겨도 시스템이 계속 작동한다는 것입니다. CAP 정리가 없던 시절, 아니 정확히는 이 정리를 모르던 개발자들은 어땠을까요?
많은 팀이 "우리 시스템은 완벽해야 해!"라며 세 가지 모두를 달성하려고 시도했습니다. 결과는 참담했습니다.
네트워크 장애가 발생하면 시스템 전체가 멈추거나, 데이터가 꼬이는 심각한 문제가 발생했습니다. 2000년에 에릭 브루어가 이 정리를 발표하고, 2002년에 수학적으로 증명되면서 개발자들은 비로소 "선택"의 중요성을 깨달았습니다.
바로 이런 혼란을 정리하기 위해 CAP 정리가 주목받기 시작했습니다. CAP 정리를 이해하면 시스템 설계 시 명확한 기준이 생깁니다.
"우리 서비스는 데이터 정확성이 최우선이야"라면 일관성을 선택하고, "무조건 응답이 빨라야 해"라면 가용성을 선택할 수 있습니다. 현실에서 분할 내성(P)은 포기할 수 없기 때문에, 실제로는 CA, AP, CP 중 하나를 선택하게 됩니다.
위의 코드를 살펴보겠습니다. write_consistent 메서드를 보면, 다른 모든 노드에 데이터를 동기화한 후에야 응답을 반환합니다.
만약 하나라도 불가용한 노드가 있으면 예외를 발생시킵니다. 이것이 바로 일관성을 우선하는 방식입니다.
반면 write_available 메서드는 자기 노드에만 저장하고 즉시 응답합니다. 나중에 동기화하겠다는 것이죠.
이것이 가용성을 우선하는 방식입니다. 실제 현업에서는 어떻게 활용할까요?
은행 시스템을 생각해봅시다. 계좌 잔액이 각 서버마다 다르게 보인다면 큰 문제가 됩니다.
따라서 은행은 일관성을 우선합니다. 반면 SNS의 좋아요 수는 잠깐 다르게 보여도 큰 문제가 없습니다.
그래서 SNS는 가용성을 우선하는 경우가 많습니다. 하지만 주의할 점도 있습니다.
CAP 정리를 "세 개 중 두 개만 선택 가능"이라고 단순화하는 것은 위험합니다. 현실에서는 네트워크 분할이 항상 발생하는 것이 아니며, 정상 상태에서는 세 가지 모두 어느 정도 만족시킬 수 있습니다.
문제는 장애 상황에서 무엇을 우선할 것인가입니다. 다시 회의실로 돌아가 봅시다.
박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "그래서 우리가 AP 시스템을 선택한 거군요.
사용자 경험을 위해 항상 응답을 주되, 데이터는 나중에 동기화하는 거죠?" CAP 정리를 이해하면 분산 시스템 설계의 첫걸음을 뗄 수 있습니다. 다음 섹션에서는 CA, AP, CP 시스템의 구체적인 예시를 살펴보겠습니다.
실전 팁
💡 - CAP에서 P(분할 내성)는 현실적으로 포기할 수 없으므로, 실제로는 CP vs AP의 선택입니다
- 시스템 전체가 하나의 선택을 할 필요는 없습니다. 기능별로 다른 전략을 적용할 수 있습니다
2. CA AP CP 시스템
김개발 씨가 CAP 정리를 이해한 후, 박시니어 씨가 후속 질문을 던졌습니다. "그럼 우리가 사용하는 MySQL은 뭐고, Redis 클러스터는 뭘까요?" 이론을 배웠으니 이제 실제 시스템에 어떻게 적용되는지 알아볼 차례입니다.
CA 시스템은 네트워크 분할을 허용하지 않고 일관성과 가용성을 보장합니다. CP 시스템은 분할 상황에서 일관성을 우선하여 일부 요청을 거부할 수 있습니다.
AP 시스템은 분할 상황에서도 항상 응답하되 일관성은 나중에 맞춥니다. 각각의 대표 시스템을 알면 아키텍처 선택이 쉬워집니다.
다음 코드를 살펴봅시다.
# 각 CAP 유형별 시스템 동작 시뮬레이션
class CAPSystemSimulator:
# CP 시스템: 일관성 우선 (예: MongoDB, HBase)
def cp_system_read(self, is_network_partitioned):
if is_network_partitioned:
# 분할 상황에서는 응답 거부 (일관성 보장)
raise Exception("서비스 불가 - 데이터 일관성 보장 불가")
return {"data": "일관된 데이터", "guaranteed": True}
# AP 시스템: 가용성 우선 (예: Cassandra, DynamoDB)
def ap_system_read(self, is_network_partitioned, local_data):
# 분할 상황에서도 항상 응답 (로컬 데이터 반환)
return {
"data": local_data,
"possibly_stale": is_network_partitioned,
"message": "응답 완료 - 최신 여부 보장 안됨"
}
# CA 시스템: 단일 노드 (예: 전통적 RDBMS)
def ca_system_read(self, connection_available):
if not connection_available:
raise Exception("연결 불가")
return {"data": "정확한 데이터", "single_source": True}
지난 회의에서 CAP 정리를 배운 김개발 씨는 한 가지 의문이 생겼습니다. "이론은 알겠는데, 실제로 어떤 데이터베이스가 어떤 유형인지 어떻게 알 수 있을까요?" 박시니어 씨가 화이트보드에 세 개의 영역을 그렸습니다.
"자, 우리가 실제로 사용하는 시스템들을 분류해볼까요?" 먼저 CA 시스템에 대해 알아봅시다. CA 시스템은 마치 외줄 타기와 같습니다.
줄이 끊어지면(네트워크 분할) 더 이상 진행할 수 없습니다. 전통적인 단일 서버 RDBMS가 여기에 해당합니다.
MySQL이나 PostgreSQL을 단일 서버로 운영하면 네트워크 분할 자체가 발생하지 않으니, 일관성과 가용성 모두를 보장할 수 있습니다. 하지만 서버가 다운되면 전체 서비스가 중단됩니다.
다음은 CP 시스템입니다. CP 시스템은 은행 금고와 같습니다.
"확실하지 않으면 열어주지 않겠다"는 원칙을 고수합니다. MongoDB, HBase, Redis(클러스터 모드) 등이 여기에 해당합니다.
네트워크가 분할되면 과반수 노드가 있는 쪽만 서비스를 계속하고, 나머지는 요청을 거부합니다. 데이터 정합성이 깨지느니 차라리 에러를 반환하겠다는 것입니다.
마지막으로 AP 시스템을 살펴봅시다. AP 시스템은 24시간 편의점과 같습니다.
"무슨 일이 있어도 문은 열어둔다"는 정책입니다. Cassandra, DynamoDB, CouchDB 등이 대표적입니다.
네트워크가 분할되어도 각 노드는 자신이 가진 데이터로 응답합니다. 나중에 네트워크가 복구되면 충돌을 해결합니다.
사용자는 항상 응답을 받지만, 그 데이터가 최신인지는 보장되지 않습니다. 위 코드에서 각 시스템의 동작 차이를 확인할 수 있습니다.
cp_system_read는 네트워크 분할 시 예외를 발생시킵니다. 일관성을 보장할 수 없다면 차라리 서비스를 거부하는 것입니다.
ap_system_read는 분할 여부와 관계없이 항상 로컬 데이터를 반환합니다. 다만 possibly_stale 플래그로 데이터가 오래됐을 수 있음을 알려줍니다.
실제 서비스 설계에서는 어떻게 선택할까요? 결제 시스템, 재고 관리처럼 정확성이 중요한 곳에는 CP 시스템이 적합합니다.
잠깐 에러가 나더라도 잘못된 데이터보다 낫습니다. 반면 상품 추천, 피드 조회처럼 약간의 지연이 허용되는 곳에는 AP 시스템이 좋습니다.
사용자가 빈 화면을 보는 것보다는 조금 오래된 데이터라도 보여주는 게 낫습니다. 흔히 하는 실수가 있습니다.
많은 개발자가 "우리 서비스는 CP야" 또는 "우리는 AP야"라고 단정짓습니다. 하지만 현실의 서비스는 기능별로 다른 전략을 사용합니다.
같은 쇼핑몰이라도 장바구니는 AP, 결제는 CP로 운영할 수 있습니다. 유연한 사고가 필요합니다.
김개발 씨가 물었습니다. "그럼 우리 서비스에서 사용하는 MongoDB는 CP니까, 장애 시 에러가 날 수 있다는 거네요?" 박시니어 씨가 웃으며 답했습니다.
"정확해요. 그래서 우리가 장애 대응 매뉴얼을 철저히 준비해두는 거예요." 어떤 시스템을 선택할지는 비즈니스 요구사항에 따라 달라집니다.
중요한 것은 각 선택의 트레이드오프를 명확히 이해하는 것입니다.
실전 팁
💡 - 하나의 서비스 내에서도 기능별로 다른 CAP 전략을 적용할 수 있습니다
- 데이터베이스의 설정에 따라 같은 제품도 CP 또는 AP로 동작할 수 있으니 문서를 꼼꼼히 확인하세요
3. 데이터 압축 알고리즘
어느 날 김개발 씨는 로그 파일이 서버 디스크를 가득 채워 장애가 발생하는 것을 목격했습니다. 선배가 "압축 적용하면 용량이 10분의 1로 줄어들어"라고 했는데, 마법처럼 들렸습니다.
데이터 압축의 원리, 지금부터 알아보겠습니다.
데이터 압축은 정보를 더 적은 비트로 표현하는 기술입니다. 마치 이사할 때 옷을 압축팩에 넣어 부피를 줄이는 것과 같습니다.
반복되는 패턴을 찾아 효율적으로 저장하면 저장 공간과 전송 시간을 크게 절약할 수 있습니다.
다음 코드를 살펴봅시다.
import zlib
import gzip
from io import BytesIO
# 압축 전후 크기 비교 함수
def compare_compression(data: bytes) -> dict:
original_size = len(data)
# zlib 압축 (다양한 압축 레벨 지원)
zlib_compressed = zlib.compress(data, level=9) # 최고 압축
# gzip 압축 (파일 저장에 주로 사용)
gzip_buffer = BytesIO()
with gzip.GzipFile(fileobj=gzip_buffer, mode='wb') as f:
f.write(data)
gzip_compressed = gzip_buffer.getvalue()
return {
"원본_크기": original_size,
"zlib_압축": len(zlib_compressed),
"gzip_압축": len(gzip_compressed),
"zlib_압축률": f"{(1 - len(zlib_compressed)/original_size)*100:.1f}%",
"gzip_압축률": f"{(1 - len(gzip_compressed)/original_size)*100:.1f}%"
}
# 테스트: 반복 패턴이 많은 데이터
test_data = b"Hello World! " * 1000
print(compare_compression(test_data))
김개발 씨는 운영 중인 서비스의 로그 파일이 하루에 수십 GB씩 쌓이는 것을 발견했습니다. 디스크 용량 알람이 매일 울리고, 결국 어느 날 새벽 서버가 멈춰버렸습니다.
로그 파일이 디스크를 100% 채운 것이 원인이었습니다. 박시니어 씨가 터미널에서 간단한 명령어를 실행했습니다.
"gzip으로 압축하면 이 10GB 로그 파일이 800MB로 줄어들어요." 김개발 씨는 놀라움을 감추지 못했습니다. "어떻게 그게 가능하죠?
데이터가 사라지는 건 아니잖아요?" 데이터 압축의 핵심 원리는 중복 제거입니다. 생각해보세요.
"AAAAAAAAAA"라는 문자열이 있다면, 이것을 "A가 10번"이라고 표현할 수 있습니다. 10글자가 3글자로 줄었습니다.
로그 파일에는 비슷한 패턴이 엄청나게 많이 반복됩니다. 타임스탬프 형식, IP 주소, 에러 메시지 등이 계속 반복되죠.
압축 알고리즘은 이런 반복을 찾아내어 효율적으로 표현합니다. 압축 알고리즘은 크게 두 가지로 나뉩니다.
무손실 압축은 원본을 완벽하게 복원할 수 있습니다. 텍스트 파일, 프로그램 코드, 데이터베이스 등에 사용됩니다.
손실 압축은 일부 정보를 버리고 더 높은 압축률을 얻습니다. JPEG 이미지, MP3 음악 파일이 대표적입니다.
이번 섹션에서는 무손실 압축에 집중하겠습니다. 위 코드에서 zlib과 gzip을 사용한 압축을 볼 수 있습니다.
zlib.compress 함수는 level 파라미터로 압축 강도를 조절합니다. 1은 빠르지만 압축률이 낮고, 9는 느리지만 압축률이 높습니다.
"Hello World! "가 1000번 반복된 데이터는 90% 이상 압축됩니다.
반복이 많을수록 압축 효과가 극대화되기 때문입니다. 실무에서 압축은 어디에 사용될까요?
API 응답에 gzip 압축을 적용하면 네트워크 트래픽이 크게 줄어듭니다. 대부분의 웹 서버와 브라우저가 이를 지원합니다.
데이터베이스 백업 파일, 로그 파일 보관에도 압축은 필수입니다. 클라우드 스토리지 비용이 비싼 요즘, 압축은 비용 절감의 핵심 도구입니다.
하지만 압축이 만능은 아닙니다. 이미 압축된 파일(JPEG, MP4, ZIP 등)을 다시 압축하면 오히려 용량이 늘어날 수 있습니다.
또한 압축과 해제에는 CPU 자원이 필요합니다. 실시간 처리가 중요한 시스템에서는 압축 오버헤드를 고려해야 합니다.
김개발 씨가 로그 로테이션 스크립트에 gzip 압축을 추가했습니다. "이제 일주일치 로그도 2GB면 충분하네요!" 박시니어 씨가 만족스럽게 고개를 끄덕였습니다.
"데이터의 특성을 파악하고 적절한 압축을 적용하는 것, 그게 엔지니어의 역할이에요."
실전 팁
💡 - HTTP 응답에 Content-Encoding: gzip을 적용하면 네트워크 비용을 크게 줄일 수 있습니다
- 압축률과 속도는 트레이드오프 관계입니다. 실시간 처리에는 낮은 레벨, 아카이브에는 높은 레벨을 사용하세요
4. 무손실 압축 기법
김개발 씨가 압축의 기본을 이해한 후, 호기심이 생겼습니다. "gzip 내부에서는 대체 어떤 일이 일어나는 걸까요?" 박시니어 씨가 화이트보드 앞으로 다가갔습니다.
"허프만 코딩과 LZ77, 두 가지 핵심 알고리즘을 알아야 해요."
허프만 코딩은 자주 나오는 문자에 짧은 코드를, 드물게 나오는 문자에 긴 코드를 할당하는 방식입니다. LZ77은 반복되는 패턴을 "이전 위치 참조"로 대체합니다.
이 두 기법을 결합한 것이 DEFLATE 알고리즘이며, gzip과 PNG가 이를 사용합니다.
다음 코드를 살펴봅시다.
from collections import Counter
import heapq
# 허프만 코딩 간단 구현
class HuffmanNode:
def __init__(self, char, freq):
self.char = char
self.freq = freq
self.left = None
self.right = None
def __lt__(self, other):
return self.freq < other.freq
def build_huffman_tree(text: str) -> dict:
# 1. 문자 빈도 계산
freq = Counter(text)
# 2. 최소 힙으로 트리 구성
heap = [HuffmanNode(char, f) for char, f in freq.items()]
heapq.heapify(heap)
while len(heap) > 1:
left = heapq.heappop(heap)
right = heapq.heappop(heap)
merged = HuffmanNode(None, left.freq + right.freq)
merged.left, merged.right = left, right
heapq.heappush(heap, merged)
# 3. 코드 생성
codes = {}
def generate_codes(node, code=""):
if node.char:
codes[node.char] = code or "0"
return
generate_codes(node.left, code + "0")
generate_codes(node.right, code + "1")
generate_codes(heap[0])
return codes
# 예시: "aaaaabbc" -> a는 짧은 코드, c는 긴 코드
print(build_huffman_tree("aaaaabbc"))
박시니어 씨가 화이트보드에 "AAAAABBC"라고 적었습니다. "이 문자열을 압축한다고 생각해봐요.
일반 ASCII로는 각 문자가 8비트니까 총 64비트가 필요해요." 김개발 씨가 계산해봤습니다. "8개 문자 곱하기 8비트, 맞네요." "하지만 여기서 A가 5번, B가 2번, C가 1번 나와요.
모든 문자에 똑같은 8비트를 주는 게 효율적일까요?" 이것이 허프만 코딩의 핵심 아이디어입니다. 허프만 코딩은 마치 자주 가는 길에 지름길을 만드는 것과 같습니다.
자주 다니는 길은 짧게, 가끔 다니는 길은 길어도 괜찮습니다. 전체 이동 거리가 줄어드니까요.
마찬가지로 자주 나오는 A에는 1비트짜리 코드 "0"을, 드물게 나오는 C에는 3비트짜리 코드 "110"을 할당합니다. 알고리즘의 동작 원리를 살펴봅시다.
먼저 각 문자의 출현 빈도를 계산합니다. 그 다음 가장 빈도가 낮은 두 노드를 합쳐 새 노드를 만듭니다.
이 과정을 하나의 트리가 될 때까지 반복합니다. 트리가 완성되면 왼쪽 가지는 0, 오른쪽 가지는 1로 코드를 생성합니다.
루트에서 해당 문자까지의 경로가 그 문자의 코드가 됩니다. 위 코드에서 "aaaaabbc"를 입력하면 결과는 대략 이렇습니다.
a가 가장 많이 나오므로 "0" 또는 "1" 같은 1비트 코드를 받습니다. b는 중간 빈도라 "10" 같은 2비트, c는 가장 적게 나오니 "11" 같은 코드를 받습니다.
원래 64비트였던 데이터가 5x1 + 2x2 + 1x2 = 11비트 정도로 줄어듭니다. LZ77은 다른 접근법을 사용합니다.
LZ77은 "이전에 나온 패턴을 재활용"합니다. "ABCABCABC"라는 문자열이 있다면, 두 번째 ABC는 "3글자 전으로 가서 3글자를 복사"라고 표현할 수 있습니다.
이 (거리, 길이) 쌍이 원본보다 짧으면 압축 효과가 생깁니다. 실무에서 이 알고리즘들은 어떻게 쓰일까요?
DEFLATE는 LZ77과 허프만 코딩을 결합한 알고리즘입니다. gzip, PNG, ZIP 파일 모두 DEFLATE를 사용합니다.
웹에서 가장 널리 쓰이는 압축 방식이기도 합니다. 최근에는 더 효율적인 Brotli 알고리즘도 등장했습니다.
주의할 점이 있습니다. 허프만 코딩은 빈도 테이블을 파일에 함께 저장해야 합니다.
그래야 압축 해제가 가능하니까요. 파일이 아주 작으면 이 오버헤드 때문에 오히려 압축 후 크기가 커질 수 있습니다.
김개발 씨가 감탄했습니다. "단순해 보이는 gzip 뒤에 이런 알고리즘이 있었군요!" 박시니어 씨가 웃었습니다.
"기초를 알면 도구를 더 잘 활용할 수 있어요. 언제 압축이 효과적인지, 왜 어떤 파일은 압축이 안 되는지 이해하게 되죠."
실전 팁
💡 - 허프만 코딩은 엔트로피에 가까운 최적의 코드를 생성하지만, 테이블 오버헤드가 있습니다
- 실무에서는 직접 구현보다 검증된 라이브러리(zlib, lz4, zstd)를 사용하세요
5. 암호학 기초
어느 날 김개발 씨는 사용자 비밀번호를 데이터베이스에 평문으로 저장한 것을 발견했습니다. 박시니어 씨의 표정이 굳었습니다.
"이건 큰 문제야. 당장 암호화해야 해." 보안의 기본, 암호학의 세계로 들어가 봅시다.
암호학은 정보를 보호하기 위한 수학적 기법입니다. 마치 비밀 편지를 암호로 작성하는 것과 같습니다.
암호화는 읽을 수 있는 데이터를 읽을 수 없게 변환하고, 복호화는 그 반대 과정입니다. 현대 웹 서비스에서 암호학은 필수입니다.
다음 코드를 살펴봅시다.
from cryptography.fernet import Fernet
import base64
import os
# 암호화의 기본 개념을 보여주는 예제
class SimpleCrypto:
def __init__(self):
# 안전한 키 생성 (실제로는 안전하게 보관 필요)
self.key = Fernet.generate_key()
self.cipher = Fernet(self.key)
def encrypt(self, plaintext: str) -> bytes:
"""평문을 암호문으로 변환"""
return self.cipher.encrypt(plaintext.encode())
def decrypt(self, ciphertext: bytes) -> str:
"""암호문을 평문으로 복원"""
return self.cipher.decrypt(ciphertext).decode()
# 사용 예시
crypto = SimpleCrypto()
secret_message = "사용자의 민감한 정보"
encrypted = crypto.encrypt(secret_message)
print(f"암호화됨: {encrypted[:50]}...") # 읽을 수 없는 형태
decrypted = crypto.decrypt(encrypted)
print(f"복호화됨: {decrypted}") # 원본 복원
김개발 씨는 얼굴이 하얗게 질렸습니다. "비밀번호가 그대로 저장되어 있다니...
누가 데이터베이스를 털면 어떡하죠?" 박시니어 씨가 차분하게 말했습니다. "그래서 우리는 암호화를 해야 해요.
설령 데이터가 유출되더라도 읽을 수 없게 만드는 거죠." 암호학의 역사는 인류의 역사만큼이나 오래됐습니다. 고대 로마의 시저 암호는 알파벳을 일정 수만큼 밀어서 암호화했습니다.
A를 D로, B를 E로 바꾸는 식이죠. 단순하지만 핵심 개념은 같습니다.
원본(평문)을 규칙(키)에 따라 변환(암호화)하여 암호문을 만듭니다. 규칙을 아는 사람만 원본을 복원(복호화)할 수 있습니다.
현대 암호학은 수학적으로 훨씬 복잡합니다. 시저 암호는 26가지 경우의 수만 시도하면 깨집니다.
현대 암호 알고리즘은 수학적으로 "깨는 것이 사실상 불가능"하도록 설계됩니다. 열쇠 없이 여는 것이 우주의 나이보다 오래 걸리도록 말이죠.
위 코드에서 Fernet을 사용한 암호화를 볼 수 있습니다. Fernet은 Python의 cryptography 라이브러리가 제공하는 안전한 암호화 도구입니다.
**generate_key()**로 256비트 랜덤 키를 생성합니다. 이 키로 encrypt()를 호출하면 평문이 읽을 수 없는 암호문으로 바뀝니다.
같은 키로 decrypt()를 호출해야만 원본이 복원됩니다. 암호학에는 몇 가지 핵심 원칙이 있습니다.
케르크호프스의 원리는 "알고리즘이 공개되어도 키만 비밀이면 안전해야 한다"입니다. 비밀 알고리즘에 의존하면 안 됩니다.
AES, RSA 같은 검증된 공개 알고리즘을 사용해야 합니다. 또한 키 관리가 암호화의 핵심입니다.
아무리 좋은 자물쇠도 열쇠를 도어매트 밑에 두면 소용없습니다. 실무에서 암호화는 어디에 적용될까요?
HTTPS는 웹 통신을 암호화합니다. 데이터베이스의 민감 정보(개인정보, 결제정보)는 저장 시 암호화합니다.
백업 파일, 로그 파일도 암호화 대상입니다. API 키, 비밀번호 같은 시크릿은 환경변수나 비밀 관리 도구에 암호화하여 보관합니다.
초보자들이 흔히 하는 실수가 있습니다. "직접 암호화 알고리즘을 만들어볼까?"라는 생각은 위험합니다.
전문가들이 수십 년간 검증한 알고리즘을 사용해야 합니다. 또한 키를 코드에 하드코딩하면 안 됩니다.
Git에 올라가는 순간 비밀이 아니게 됩니다. 김개발 씨가 물었습니다.
"그런데 비밀번호는 복호화가 필요한가요?" 박시니어 씨가 미소를 지었습니다. "좋은 질문이에요.
비밀번호는 암호화가 아니라 '해시'를 써야 해요. 다음에 자세히 알아보죠."
실전 팁
💡 - 절대 직접 암호화 알고리즘을 만들지 마세요. 검증된 라이브러리를 사용하세요
- 키는 코드에 하드코딩하지 말고, 환경변수나 비밀 관리 서비스를 사용하세요
6. 대칭 비대칭 암호화
암호화의 기본을 배운 김개발 씨에게 박시니어 씨가 질문했습니다. "암호화 키를 상대방에게 어떻게 안전하게 전달할까요?" 김개발 씨는 잠시 생각에 잠겼습니다.
키를 보내려면 암호화해야 하는데, 그 암호화에도 키가 필요하고... 닭이 먼저냐 달걀이 먼저냐의 문제 같았습니다.
대칭 암호화는 암호화와 복호화에 같은 키를 사용합니다. 빠르지만 키 교환이 문제입니다.
비대칭 암호화는 공개키로 암호화하고 비밀키로 복호화합니다. 느리지만 키 교환 문제를 해결합니다.
현대 시스템은 두 방식을 조합하여 사용합니다.
다음 코드를 살펴봅시다.
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.fernet import Fernet
# 대칭 암호화 예제 (AES 기반 Fernet)
def symmetric_example():
key = Fernet.generate_key() # 송신자와 수신자가 공유해야 함
cipher = Fernet(key)
encrypted = cipher.encrypt(b"비밀 메시지")
decrypted = cipher.decrypt(encrypted)
return decrypted.decode()
# 비대칭 암호화 예제 (RSA)
def asymmetric_example():
# 수신자가 키 쌍 생성
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048
)
public_key = private_key.public_key()
# 송신자가 공개키로 암호화
message = b"Secret Message"
ciphertext = public_key.encrypt(
message,
padding.OAEP(mgf=padding.MGF1(hashes.SHA256()),
algorithm=hashes.SHA256(), label=None)
)
# 수신자만 비밀키로 복호화 가능
plaintext = private_key.decrypt(
ciphertext,
padding.OAEP(mgf=padding.MGF1(hashes.SHA256()),
algorithm=hashes.SHA256(), label=None)
)
return plaintext.decode()
박시니어 씨가 화이트보드에 두 개의 그림을 그렸습니다. "암호화에는 크게 두 가지 방식이 있어요.
마치 열쇠 하나로 잠그고 여는 자물쇠와, 잠그는 열쇠와 여는 열쇠가 다른 특수 자물쇠의 차이예요." 먼저 대칭 암호화를 알아봅시다. 대칭 암호화는 같은 키로 암호화와 복호화를 합니다.
마치 집 열쇠처럼, 잠글 때도 열 때도 같은 열쇠를 사용하죠. AES가 가장 널리 쓰이는 대칭 암호화 알고리즘입니다.
장점은 속도입니다. 대량의 데이터를 빠르게 암호화할 수 있습니다.
하지만 치명적인 문제가 있습니다. 키를 상대방과 어떻게 공유할까요?
인터넷으로 키를 보내면 중간에 누군가 가로챌 수 있습니다. 직접 만나서 전달하면 안전하지만, 전 세계 수백만 사용자와 직접 만날 수는 없습니다.
이 문제를 키 교환 문제라고 합니다. 이 문제를 해결한 것이 비대칭 암호화입니다.
비대칭 암호화는 마법 같은 개념입니다. 두 개의 키가 있는데, 하나로 잠그면 다른 하나로만 열 수 있습니다.
공개키는 누구에게나 공개하고, **비밀키(개인키)**는 나만 갖고 있습니다. 누군가 내게 비밀 메시지를 보내고 싶으면 내 공개키로 암호화합니다.
그러면 나만 가진 비밀키로만 복호화할 수 있습니다. 코드에서 RSA 비대칭 암호화를 볼 수 있습니다.
**rsa.generate_private_key()**로 비밀키를 생성하면, 그로부터 공개키를 추출할 수 있습니다. 송신자는 공개키로 암호화하고, 수신자만 비밀키로 복호화합니다.
핵심은 공개키가 유출되어도 비밀키 없이는 복호화가 불가능하다는 것입니다. 비대칭 암호화에도 단점이 있습니다.
대칭 암호화보다 수백 배 느립니다. 대량의 데이터를 RSA로 직접 암호화하면 시간이 너무 오래 걸립니다.
그래서 실무에서는 하이브리드 방식을 사용합니다. HTTPS가 바로 하이브리드 방식의 대표 예입니다.
처음 연결할 때 비대칭 암호화로 "대칭키"를 안전하게 교환합니다. 이후 실제 데이터는 그 대칭키로 암호화합니다.
비대칭의 안전성과 대칭의 속도, 두 마리 토끼를 잡는 것이죠. 김개발 씨가 정리했습니다.
"그러니까 비대칭 암호화로 대칭키를 안전하게 전달하고, 실제 데이터는 대칭 암호화로 처리하는 거네요?" 박시니어 씨가 고개를 끄덕였습니다. "정확해요.
이게 현대 보안 통신의 핵심 구조예요."
실전 팁
💡 - 대량 데이터는 AES(대칭), 키 교환은 RSA나 ECDH(비대칭)를 사용하세요
- RSA 키 크기는 최소 2048비트, 권장 4096비트입니다
7. 해시 함수와 보안
암호화를 공부하던 김개발 씨가 다시 비밀번호 저장 문제로 돌아왔습니다. "비밀번호를 암호화하면 되지 않나요?" 박시니어 씨가 고개를 저었습니다.
"비밀번호는 암호화가 아니라 해시를 써야 해요. 원본을 복원할 필요가 없으니까요."
해시 함수는 임의 길이의 입력을 고정 길이의 출력으로 변환하는 단방향 함수입니다. 마치 고기를 갈아서 햄버거 패티로 만들면 다시 원래 고기 형태로 돌릴 수 없는 것과 같습니다.
비밀번호 저장, 데이터 무결성 검증 등에 핵심적으로 사용됩니다.
다음 코드를 살펴봅시다.
import hashlib
import bcrypt
import os
# 일반 해시 (비밀번호에 부적합)
def simple_hash(password: str) -> str:
return hashlib.sha256(password.encode()).hexdigest()
# 올바른 비밀번호 해싱 (bcrypt 사용)
def secure_password_hash(password: str) -> bytes:
# salt 자동 생성, work factor로 연산 비용 조절
salt = bcrypt.gensalt(rounds=12)
return bcrypt.hashpw(password.encode(), salt)
def verify_password(password: str, hashed: bytes) -> bool:
return bcrypt.checkpw(password.encode(), hashed)
# 사용 예시
password = "mySecureP@ssw0rd"
# 잘못된 방식 (같은 입력 = 같은 출력)
print(f"SHA256: {simple_hash(password)}")
# 올바른 방식 (같은 입력이어도 매번 다른 해시)
hashed = secure_password_hash(password)
print(f"bcrypt: {hashed}")
print(f"검증 결과: {verify_password(password, hashed)}")
박시니어 씨가 중요한 질문을 던졌습니다. "비밀번호를 왜 암호화하면 안 될까요?" 김개발 씨가 생각해봤습니다.
"복호화해서 비교하면 되지 않나요?" "그게 문제예요. 복호화가 가능하다는 건 관리자도, 해커도 비밀번호를 볼 수 있다는 뜻이에요.
비밀번호는 아예 복원이 불가능해야 해요." 해시 함수는 단방향 함수입니다. 입력을 넣으면 출력이 나오지만, 출력에서 입력을 역산할 수 없습니다.
마치 믹서기로 과일을 갈면 주스가 되지만, 주스에서 원래 과일 형태를 복원할 수 없는 것과 같습니다. 같은 입력은 항상 같은 출력을 만들지만, 출력만 봐서는 입력을 알 수 없습니다.
해시 함수의 핵심 특성이 있습니다. 첫째, 결정성: 같은 입력은 항상 같은 해시를 생성합니다.
둘째, 충돌 저항성: 다른 입력이 같은 해시를 만들기 매우 어렵습니다. 셋째, 눈사태 효과: 입력이 조금만 바뀌어도 해시가 완전히 달라집니다.
"password"와 "Password"의 해시는 전혀 다릅니다. 하지만 SHA-256 같은 일반 해시는 비밀번호에 부적합합니다.
왜일까요? 해시는 빠릅니다.
초당 수십억 번 계산이 가능합니다. 해커는 흔한 비밀번호 목록(rainbow table)을 미리 해시해두고 비교합니다.
또한 같은 비밀번호는 같은 해시이므로, 한 사용자의 해시가 깨지면 같은 비밀번호를 쓰는 모든 사용자가 위험해집니다. 이 문제를 해결하는 것이 **솔트(salt)**와 느린 해시입니다.
솔트는 각 비밀번호에 랜덤 값을 추가합니다. 같은 "1234"도 다른 솔트가 붙으면 다른 해시가 됩니다.
레인보우 테이블이 무력화됩니다. 느린 해시는 의도적으로 연산을 느리게 합니다.
bcrypt의 work factor를 높이면 해시 한 번에 수백 밀리초가 걸립니다. 정상 로그인에는 문제없지만, 수십억 번 시도에는 몇 년이 걸립니다.
위 코드에서 bcrypt 사용법을 볼 수 있습니다. **bcrypt.gensalt(rounds=12)**로 솔트와 작업 계수를 설정합니다.
rounds가 1 증가할 때마다 시간이 2배 늘어납니다. 로그인 시에는 **bcrypt.checkpw()**로 비밀번호를 검증합니다.
원본 비밀번호를 저장하거나 복호화할 필요가 전혀 없습니다. 해시는 비밀번호 외에도 많이 쓰입니다.
파일 다운로드 후 해시를 비교하면 파일이 변조되지 않았는지 확인할 수 있습니다. Git은 커밋마다 SHA-1 해시로 고유 ID를 만듭니다.
블록체인은 해시로 블록을 연결합니다. 김개발 씨가 코드를 수정하기 시작했습니다.
"이제 이해했어요. 비밀번호는 bcrypt로 해시하고, 로그인할 때 checkpw로 검증하면 되네요!" 박시니어 씨가 만족스럽게 웃었습니다.
"바로 그거예요. 보안의 기본을 지키는 것이 가장 중요해요."
실전 팁
💡 - 비밀번호 해시는 bcrypt, Argon2, scrypt 같은 전용 함수를 사용하세요
- MD5와 SHA-1은 보안 목적으로 사용하지 마세요. SHA-256 이상 또는 전용 해시를 권장합니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Helm 마이크로서비스 패키징 완벽 가이드
Kubernetes 환경에서 마이크로서비스를 효율적으로 패키징하고 배포하는 Helm의 핵심 기능을 실무 중심으로 학습합니다. Chart 생성부터 릴리스 관리까지 체계적으로 다룹니다.
보안 아키텍처 구성 완벽 가이드
프로젝트의 보안을 처음부터 설계하는 방법을 배웁니다. AWS 환경에서 VPC부터 WAF, 암호화, 접근 제어까지 실무에서 바로 적용할 수 있는 보안 아키텍처를 단계별로 구성해봅니다.
AWS Organizations 완벽 가이드
여러 AWS 계정을 체계적으로 관리하고 통합 결제와 보안 정책을 적용하는 방법을 실무 스토리로 쉽게 배워봅니다. 초보 개발자도 바로 이해할 수 있는 친절한 설명과 실전 예제를 제공합니다.
AWS KMS 암호화 완벽 가이드
AWS KMS(Key Management Service)를 활용한 클라우드 데이터 암호화 방법을 초급 개발자를 위해 쉽게 설명합니다. CMK 생성부터 S3, EBS 암호화, 봉투 암호화까지 실무에 필요한 모든 내용을 담았습니다.
AWS Secrets Manager 완벽 가이드
AWS에서 데이터베이스 비밀번호, API 키 등 민감한 정보를 안전하게 관리하는 Secrets Manager의 핵심 개념과 실무 활용법을 배워봅니다. 초급 개발자도 쉽게 따라할 수 있도록 실전 예제와 함께 설명합니다.