디지털 데이터를 압축·암호화해 안전하게 관리하는 법부터, 빅데이터를 수집·전처리·시각화해 문제를 해결하는 법까지.
사진·영상·통화 기록처럼 우리가 매일 만들어 내는 데이터는 폭발적으로 늘고 있다. 이 많은 데이터를 더 작게, 더 빠르게 주고받기 위한 기술이 데이터 압축이다.
데이터를 저장·전송하는 데는 공간과 시간, 그리고 전기가 든다. 전 세계 데이터 센터가 배출하는 이산화탄소량은 2002년 약 7,600만 톤에서 2020년 약 2억 5,900만 톤으로 급증했다 — 우리가 무심코 쓰는 데이터가 실제로 기후 변화에 영향을 준다는 뜻이다. 압축은 이 문제를 줄이는 가장 직접적인 방법이다.
압축이든 암호화든, 컴퓨터가 다루는 모든 데이터는 결국 0과 1(2진수)로 저장돼 있다. 다만 2진수는 자릿수가 너무 길어서 사람이 읽기 불편하니, 같은 비트를 3개씩 묶어 8진수로, 4개씩 묶어 16진수로 줄여 쓰기도 한다. 묶는 방법만 다를 뿐 같은 데이터이므로, 10진수로 환산한 값은 셋 다 똑같다 — 아래에서 버튼을 눌러 직접 확인해 보자.
맨 앞 빨간 버튼은 부호(0=양수, 1=음수)이고, 나머지 15개는 2진수 각 자리다. 버튼을 눌러 0↔1을 바꾸면 아래 8진수·16진수 묶음과 최종 10진수 값이 실시간으로 함께 바뀐다.
0과 1의 나열을 손실 없이 더 짧게 표현했다가, 필요할 때 원본과 똑같이 되돌리는 방식이다. 텍스트 파일, 은행 기록처럼 한 글자도 틀리면 안 되는 데이터에 적합하다.
같은 문자가 몇 번 반복되는지를 기록해 데이터를 짧게 만드는 방식. 단색이 많은 아이콘·로고(BMP 파일 등)에는 효율적이지만, 색이 다양한 사진에는 잘 맞지 않는다.
A3)문자열을 입력하면 결과가 바로 갱신돼요. ▶ 압축하기를 누르면 그 순간의 입력값으로 다시 계산해 아래 결과가 잠깐 반짝여요(다시 압축했다는 표시).
압축할 데이터: 문자열 AAADDBOOOO(총 10글자). 컴퓨터에서 한 글자를 8비트(1바이트)로 저장한다고 하면, 압축하기 전 원본 크기는 10글자 × 8비트 = 80비트(10바이트)다. 이 80비트를 어디까지 줄일 수 있는지, 쉬운 방법부터 차근차근 따라가 보자.
1️⃣ 가장 단순한 방법 — 사전 압축(등장 순서대로 번호 붙이기)
문자열에 등장하는 문자를 나온 순서대로 A→D→B→O로 정리하고, 모두 똑같은 길이(3비트)의 번호를 매긴다. (3비트면 000~111, 즉 최대 8종류의 문자를 구분할 수 있다.)
| 문자 | 등장 순서 | 부호 |
|---|---|---|
| A | 1번째로 등장 | 001 |
| D | 2번째로 등장 | 010 |
| B | 3번째로 등장 | 011 |
| O | 4번째로 등장 | 100 |
문자 10개를 전부 3비트로 바꾸면 10 × 3 = 30비트. 압축비는 80 ÷ 30 ≒ 2.67 (37.5%)이다.
2️⃣ 더 똑똑한 방법 — 허프만 부호화(자주 나올수록 짧게)
허프만 부호화(Huffman coding)의 핵심 아이디어는 단 하나다 — 자주 등장하는 문자에는 짧은 부호를, 드물게 등장하는 문자에는 긴 부호를 준다. 5단계를 하나씩 직접 따라가며 트리를 완성해 보자.
[1단계] 문자별 등장 빈도 세기
| 문자 | 등장 횟수 |
|---|---|
| O | 4번 |
| A | 3번 |
| D | 2번 |
| B | 1번 |
빈도가 높은 순서(내림차순)로 미리 정렬해 두었다.
[2단계] 가장 작은 두 값을 하나로 묶는다
지금 가장 작은 두 값은 D(2)와 B(1)이다. 둘을 묶어 합계 3짜리 새 노드를 만들고, 먼저 나열된 쪽(D)에 0, 나중 쪽(B)에 1을 붙인다.
[3단계] 다시 가장 작은 두 값을 찾아 묶는다
이제 후보는 O(4), A(3), 방금 만든 노드(3) — 세 개다. 가장 작은 두 값은 A(3)와 노드(3)인데, 값이 같으면 정렬 순서가 앞선 쪽을 먼저 처리하므로 A와 [D,B]노드를 묶는다. 합계는 6이고, A에 0, [D,B]노드에 1을 붙인다.
[4단계] 마지막 두 값을 묶어 트리를 완성한다
이제 남은 건 O(4)와 방금 만든 노드(6)뿐이다. 이 둘을 묶으면 전체 합계 10인 맨 위(뿌리, root)가 완성된다. 노드(6)에 0, O에 1을 붙인다.
[5단계] 뿌리부터 각 문자까지 경로를 이어붙여 부호 완성
뿌리에서 출발해 그 문자에 도착할 때까지 지나온 0과 1을 순서대로 이어붙이면, 그것이 그 문자의 부호가 된다.
| 문자 | 빈도 | 경로(뿌리→문자) | 부호 |
|---|---|---|---|
| O | 4 | 뿌리 →(1) O | 1 |
| A | 3 | 뿌리 →(0) 노드6 →(0) A | 00 |
| D | 2 | 뿌리 →(0) 노드6 →(1) 노드3 →(0) D | 010 |
| B | 1 | 뿌리 →(0) 노드6 →(1) 노드3 →(1) B | 011 |
1(O)로 시작하는 부호는 O 하나뿐이고, 00(A)으로 시작하는 부호도 A뿐이며, 010(D)과 011(B)도 앞 두 자리(01)까지는 같지만 마지막 한 자리에서 서로 갈린다. 그래서 비트를 앞에서부터 읽어 나가다가 이미 정해진 부호와 정확히 맞아떨어지는 순간 거기서 끊어 읽으면, 다음에 어디서부터 새 글자가 시작하는지 헷갈릴 일이 없다. (트리에서 모든 문자가 가지 끝(리프)에만 있고, 가지가 갈라지는 중간 지점엔 문자가 없기 때문에 이 규칙이 자동으로 지켜진다.)데이터 변환하기 — 최종 압축 결과
문자열 AAADDBOOOO를 완성된 부호표(O=1, A=00, D=010, B=011)에 따라 한 글자씩 바꿔서 이어붙이면 다음과 같다.
원본 80비트가 19비트로 줄었다. 압축비는 80 ÷ 19 ≒ 4.21 (23.8%)이다.
3️⃣ 두 방법 비교 — 왜 허프만이 더 효율적일까?
| 구분 | 사전 압축(고정 길이) | 허프만 부호화(가변 길이) |
|---|---|---|
| 부호 길이 | 모든 문자 3비트로 동일 | 문자마다 다름 (1~3비트) |
| 압축 후 크기 | 30비트 | 19비트 |
| 압축비 | 80 / 30 ≒ 2.67 (37.5%) | 80 / 19 ≒ 4.21 (23.8%) |
| 핵심 원리 | 등장 순서로만 번호 부여 | 자주 나올수록 짧은 부호 부여 |
이미지·영상·소리처럼 사람의 눈과 귀가 완벽히 구분하지 못하는 영역의 정보는 과감히 버려서 용량을 획기적으로 줄이는 방식이다. 압축 해제한 데이터는 원본과 정확히 같지 않지만, 체감 품질 차이는 크지 않다.
DEFLATE 방식을 쓴다. 게다가 압축 전에 각 픽셀을 바로 옆 픽셀과의 차이값으로 바꿔 저장하는 '필터링'을 거치는데, 색이 단순한 이미지는 이 차이값이 대부분 0에 가까워 허프만 부호화가 아주 짧은 부호를 붙일 수 있다.사람 눈은 색보다 밝기 차이를 더 잘 인식한다는 성질을 이용해, R·G·B 값을 그대로 저장하지 않고 휘도(밝기)와 색 차이 정보 위주로 압축한다. 이미지를 8×8 픽셀 블록으로 나눠 처리한다.
영상은 JPEG로 압축한 이미지(프레임)가 여러 장 이어진 것이다. MPEG는 연속된 프레임의 유사성을 이용해, 이전 프레임과 달라진 부분만 저장한다 — 배경이 그대로인 장면일수록 압축 효율이 크게 오른다.
아날로그 소리를 표본화 → 양자화 → 부호화 순서로 바꿔 디지털 데이터로 만드는 이 과정 전체를 PCM 변환(Pulse Code Modulation)이라 부른다.
전화 통화를 예로, 실제 숫자를 따라가며 목소리가 디지털 데이터가 되는 과정을 확인해 보자.
G.711의 방식)MP3(MPEG-1 Audio Layer III)는 표본화·양자화·부호화를 거쳐 만들어진 디지털 음원(PCM)을 한 번 더 압축하는 손실 압축 방식이다. 핵심 아이디어는 "사람 귀가 애초에 듣지 못하는 소리는 저장할 필요가 없다"는 심리음향(psychoacoustic) 모델이다.
MP3는 이렇게 ① 소리를 여러 주파수 대역으로 나누고 ② 심리음향 모델로 어느 대역이 들리는지/안 들리는지 분석해 ③ 잘 들리는 대역엔 비트를 많이, 안 들리는 대역엔 비트를 적게(또는 아예 0을) 할당한 뒤 ④ 남은 값을 양자화·부호화한다. 이 과정을 거치면 원본 CD 음원(약 1,411kbps)을 1/10 수준(128kbps)까지 줄이면서도 사람 귀에는 거의 같은 소리로 들린다.
| 비트레이트 | 체감 음질 | 파일 크기 |
|---|---|---|
| 128kbps | 보통 (음원 스트리밍 최소 수준) | 작음 |
| 192kbps | 좋음 | 중간 |
| 320kbps | 매우 좋음 (원음과 구분 어려움) | 큼 |
FLAC, ALAC.디지털 데이터 압축의 개념과 필요성, 무손실·손실 압축의 차이를 설명할 수 있나요? 런 길이 부호화·허프만 부호화의 원리를 손으로 계산할 수 있나요? 나이퀴스트율에 따라 표본화 주파수를 정하고, PCM 변환 과정을 예로 들어 설명할 수 있나요? MP3가 청각 마스킹을 이용해 데이터를 줄이는 원리를 설명할 수 있나요?
구글 코랩(Colab)에서 바로 돌려볼 수 있다. 같은 사진을 네 가지 형식으로 저장해 보고, 해상도는 그대로인데 파일 크기만 달라지는 것을 확인해 보자. 마지막에는 무손실과 손실을 눈이 아니라 코드로 판별해 본다.
내려받은 BMP 이미지(.bmp)를 코랩에 업로드한 뒤, 이미지 변환에 필요한 라이브러리를 불러온다.
# os: 파일 크기 확인용 / PIL(Pillow): 이미지 열기·저장용 import os from PIL import Image
im = Image.open('flower_BMP.bmp') print('이미지 확장자', im.format) print('이미지 해상도', im.size) print('파일 크기', os.path.getsize('flower_BMP.bmp'), 'Bytes')
BMP / (800, 600) / 1440054 Bytes# PNG로 저장 후 다시 열어 확인 im.save('flower_PNG.png', 'png') im2 = Image.open('flower_PNG.png') print('이미지 확장자', im2.format) print('이미지 해상도', im2.size) print('파일 크기', os.path.getsize('flower_PNG.png'), 'Bytes') # GIF로 저장 후 다시 열어 확인 im.save('flower_GIF.gif', 'gif') im3 = Image.open('flower_GIF.gif') print('이미지 확장자', im3.format) print('이미지 해상도', im3.size) print('파일 크기', os.path.getsize('flower_GIF.gif'), 'Bytes') # JPG로 저장 후 다시 열어 확인 im.save('flower_JPG.jpg', 'jpeg') im4 = Image.open('flower_JPG.jpg') print('이미지 확장자', im4.format) print('이미지 해상도', im4.size) print('파일 크기', os.path.getsize('flower_JPG.jpg'), 'Bytes')
PNG (800, 600) 940822 Bytes — 원본의 65.3% (압축비 1.5)GIF (800, 600) 418151 Bytes — 원본의 29.0% (압축비 3.4)JPEG (800, 600) 66469 Bytes — 원본의 4.6% (압축비 21.7)# 코랩에서는 변수 이름만 적어도 이미지가 출력된다
im
im2
im3
im4
"똑같아 보인다"와 "똑같다"는 다르다. 두 이미지의 픽셀을 하나하나 빼서 차이가 0인지 확인하면, 정보를 잃었는지 정확히 알 수 있다.
# ImageChops: 두 이미지를 픽셀 단위로 계산해 주는 도구 from PIL import ImageChops origin = im.convert('RGB') for name, file in [('PNG', 'flower_PNG.png'), ('GIF', 'flower_GIF.gif'), ('JPG', 'flower_JPG.jpg')]: other = Image.open(file).convert('RGB') diff = ImageChops.difference(origin, other) # 픽셀끼리 뺀 결과 if diff.getbbox() is None: # 차이가 전부 0이면 None print(name, ': 원본과 완전히 동일 → 무손실') else: print(name, ': 원본과 다름 → 정보가 사라짐')
PNG : 원본과 완전히 동일 → 무손실GIF : 원본과 다름 → 정보가 사라짐JPG : 원본과 다름 → 정보가 사라짐말로만 넘어가지 말고 직접 확인해 보자. 색을 미리 256개로 줄여 둔 이미지를 GIF로 저장하면, ①단계에서 버릴 색이 이미 없다. 남는 것은 ②압축뿐이다. 이때도 원본과 달라질까?
# 색을 미리 256개로 줄인 이미지를 만든다 (여기서 색을 버림) small = im.convert('RGB').quantize(colors=256).convert('RGB') small.save('small.bmp') # 이것을 '원본'으로 삼는다 # 이미 256색이므로, GIF로 저장해도 더 버릴 색이 없다 small.save('small.gif', 'gif') print('BMP', os.path.getsize('small.bmp'), '-> GIF', os.path.getsize('small.gif'), 'Bytes') diff = ImageChops.difference(small, Image.open('small.gif').convert('RGB')) print('원본과 동일한가:', diff.getbbox() is None)
BMP 1440054 -> GIF 418151 Bytes원본과 동일한가: True사진을 고치고 저장하기를 반복하는 일은 흔하다. 형식마다 결과가 다른데, 이 차이가 실제로는 가장 중요하다.
# GIF로 두 번 더 저장해 본다 g1 = Image.open('flower_GIF.gif').convert('RGB') g1.save('g2.gif', 'gif') g2 = Image.open('g2.gif').convert('RGB') g2.save('g3.gif', 'gif') g3 = Image.open('g3.gif').convert('RGB') print('GIF 2번 더 저장 후 그대로인가:', ImageChops.difference(g1, g3).getbbox() is None) # JPG로 두 번 더 저장해 본다 j1 = Image.open('flower_JPG.jpg').convert('RGB') j1.save('j2.jpg', 'jpeg') j2 = Image.open('j2.jpg').convert('RGB') j2.save('j3.jpg', 'jpeg') j3 = Image.open('j3.jpg').convert('RGB') print('JPG 2번 더 저장 후 그대로인가:', ImageChops.difference(j1, j3).getbbox() is None)
GIF 2번 더 저장 후 그대로인가: True — 더 나빠지지 않는다JPG 2번 더 저장 후 그대로인가: False — 또 나빠졌다| 형식 | 저장 전에 정보를 버리나? | 압축 자체는? | 파일 크기 | 색 표현 | 사진을 원본대로 되살릴 수 있나? |
|---|---|---|---|---|---|
| BMP | 안 버림 | 거의 압축 안 함 | 1,440,054 B (100%) | 1,600만 색 | 원본 그대로 |
| PNG | 안 버림 | 무손실 | 940,822 B (65.3%) | 1,600만 색 | 완벽히 가능 |
| GIF | 버림 (색을 256개로) | 무손실 | 418,151 B (29.0%) | 256색만 | 불가능 (버린 색은 못 되찾음) |
| JPG | 안 버림 | 손실 | 66,469 B (4.6%) | 1,600만 색 | 불가능 (압축하며 버림) |
A3D2B1O4처럼 줄이고, 자주 나오는 값에 짧은 코드를 배정하는 것이다 — 원본을 그대로 복원할 수 있으므로 무손실이다.quality 값(1~95)으로 고를 수 있다. 아래를 실행해 크기와 화질을 함께 비교해 보자.for q in [95, 75, 50, 10]:
im.save(f'q{q}.jpg', 'jpeg', quality=q)
print(q, os.path.getsize(f'q{q}.jpg'), 'Bytes')95 → 212,094 · 75 → 66,469 · 50 → 37,791 · 10 → 9,384 Bytes다. quality=10으로 저장한 이미지를 열어 보면 8×8 네모 칸 자국이 눈에 보일 만큼 뭉개져 있다 — JPG가 무엇을 버려서 크기를 줄이는지 직접 확인할 수 있다.네 가지 형식을 비교해 보고, 어떤 상황에 어떤 형식이 가장 알맞을지 이야기 나누어 보자.
① 졸업 사진 원본을 오래 보관할 때 ② 홈페이지에 로고를 올릴 때 ③ SNS에 사진 수십 장을 올릴 때 ④ 사진을 여러 번 고쳐 가며 작업할 때 — 각각 무엇을 고르겠는가? 특히 ④번은 왜 JPG를 피해야 할지 생각해 보자.
앞에서 손으로 만든 허프만 트리를 이번엔 파이썬으로 만들어 본다. 구글 코랩(Colab)에서 그대로 붙여 넣어 실행하면 된다. 설치할 라이브러리는 없다.
압축할 문자열은 앞에서 쓴 AAADDBOOOO 그대로다.
# heapq: 가장 작은 값을 빠르게 꺼내는 도구 (트리를 아래에서부터 쌓을 때 사용) import heapq from collections import Counter data = 'AAADDBOOOO' # 문자별 등장 횟수 freq = Counter(data) print('빈도:', dict(freq))
빈도: {'A': 3, 'D': 2, 'B': 1, 'O': 4}가장 드문 문자 두 개를 꺼내 하나로 묶는 일을, 노드가 하나만 남을 때까지 반복한다. 손으로 D와 B를 먼저 묶었던 것과 같은 과정이다.
# [빈도, 순번, 문자, 왼쪽자식, 오른쪽자식] — 순번은 빈도가 같을 때 순서를 정해 준다 heap = [[w, i, ch, None, None] for i, (ch, w) in enumerate(sorted(freq.items()))] heapq.heapify(heap) counter = len(heap) while len(heap) > 1: left = heapq.heappop(heap) # 가장 드문 것 right = heapq.heappop(heap) # 그 다음으로 드문 것 # 둘을 묶어 부모 노드를 만든다 (빈도는 두 개의 합) heapq.heappush(heap, [left[0] + right[0], counter, None, left, right]) counter += 1 root = heap[0] # 마지막에 남은 하나가 뿌리(root)
뿌리에서 각 문자까지 내려가며 지나온 가지의 0·1을 이어 붙이면 그 문자의 부호가 된다.
def walk(node, prefix=''): if node[2] is not None: # 문자가 들어 있으면 = 가지 끝(리프) codes[node[2]] = prefix or '0' return walk(node[3], prefix + '0') # 왼쪽으로 가면 0 walk(node[4], prefix + '1') # 오른쪽으로 가면 1 codes = {} walk(root) for ch in sorted(codes, key=lambda c: (len(codes[c]), c)): print(f'{ch} ({freq[ch]}회) -> {codes[ch]} ({len(codes[ch])}비트)')
O (4회) -> 0 (1비트)A (3회) -> 10 (2비트)B (1회) -> 110 (3비트)D (2회) -> 111 (3비트)encoded = ''.join(codes[c] for c in data) print('인코딩:', encoded) print('원본 :', len(data) * 8, '비트') # 한 글자 8비트로 저장했다면 print('압축 :', len(encoded), '비트') print('압축비: %.2f' % (len(data) * 8 / len(encoded)))
인코딩: 1010101111111100000원본 : 80 비트 → 압축 : 19 비트 → 압축비: 4.21압축한 비트열을 다시 원래 문자열로 되돌려, 한 글자도 잃지 않았는지 확인한다.
out = [] node = root for b in encoded: node = node[3] if b == '0' else node[4] # 0이면 왼쪽, 1이면 오른쪽 if node[2] is not None: # 가지 끝에 닿으면 한 글자 완성 out.append(node[2]) node = root # 다시 뿌리로 돌아가 다음 글자 print('복호화:', ''.join(out)) print('원본과 일치:', ''.join(out) == data)
복호화: AAADDBOOOO / 원본과 일치: TrueO=1, A=00, D=010, B=011이었는데 코드는 O=0, A=10, B=110, D=111을 내놓았다. 하지만 부호의 길이는 1, 2, 3, 3으로 똑같고 전체도 똑같이 19비트다. 갈림길에서 어느 쪽을 0으로 부를지, 그리고 빈도가 같을 때 누구를 먼저 묶을지는 정하기 나름이라서, 허프만 부호는 여러 가지 정답이 나올 수 있다. 길이와 총 비트 수가 같으면 모두 똑같이 최적이다.data를 바꿔 가며 실행해 보자. 'AAAAAAAAAB'처럼 한 글자가 유난히 많으면 압축비가 크게 오르고, 'ABCDEFGHIJ'처럼 모두 한 번씩만 나오면 거의 줄지 않는다. 이것이 앞에서 배운 "중복이 많을수록 압축이 잘 된다"는 원리다.온라인 쇼핑, 민원 서류 발급처럼 인터넷으로 처리하는 일이 많아질수록 비밀번호와 개인정보를 안전하게 지키는 기술이 중요해진다. 그 핵심이 암호화다.
평문의 각 문자를 약속된 다른 문자로 치환하는 방법이다. 로마 황제 율리우스 카이사르가 전쟁 중 군사 정보를 전달하기 위해 고안했다고 전해져 카이사르 암호라 불린다. 알파벳을 정해진 칸수(키)만큼 밀어서 치환하고, 복호화할 때는 반대 방향으로 밀어 되돌린다.
영문자를 입력하고 키(이동 칸수)를 조절해 보세요. 숫자·공백·한글은 그대로 둡니다.
🔒 암호화를 누르면 입력칸의 글자가 암호문으로 바뀐다. 이어서 🔓 복호화를 누르면 원문으로 돌아온다.
고대에는 문자의 순서를 재배치하는 전치형 암호화도 쓰였다. 그리스 군사들이 사용한 스키테일(scytale)이 대표적인 예로, 정해진 지름의 막대에 종이를 감아 글을 쓰면 풀었을 때 문자가 뒤섞여 보인다 — 같은 지름의 막대를 가진 사람만 다시 감아 해독할 수 있다.
COMPUTER를 키 35164827의 순서로 재배치하면 MUCTPROE가 된다. 그러나 전치형 암호화는 컴퓨팅 기술이 발전하며 쉽게 뚫리게 되어, 더 안전한 방법이 필요해졌다.치환형·전치형 암호화 방법을 설명하고, 그 한계(문자 빈도 분석 등으로 해독 가능)를 말할 수 있나요?
암호화는 되지만 복호화는 불가능한 방식. 같은 평문은 항상 같은 암호문(해시값)을 만들고, 한 글자만 달라져도 완전히 다른 결과가 나온다. 그래서 원문을 노출하지 않고도 일치 여부만 비교할 수 있어 비밀번호 저장, 파일 위·변조 확인, QR코드 신원 인증 등에 쓰인다. 대표 알고리즘은 SHA-256.
| 이름 | 해시값 길이 | 비고 |
|---|---|---|
| SHA-1 | 160비트 | 2017년 실제 충돌(같은 해시값을 갖는 서로 다른 파일) 사례가 발견돼 더 이상 안전하지 않음 — 사용 비권장 |
| SHA-256 | 256비트 | 현재 가장 널리 쓰이는 표준. 비트코인, 인증서, 비밀번호 저장 등에 사용 |
| SHA-512 | 512비트 | SHA-256보다 해시값이 더 길어 안전성이 높고, 64비트 컴퓨터에서는 오히려 더 빠르게 동작하기도 함 |
61쪽 스스로 해결하기 — 문장을 입력하면 진짜 SHA-256 알고리즘으로 해시값을 계산해요. 아래 두 칸에 원문과, 마침표 하나·띄어쓰기 하나만 바꾼 문장을 각각 넣고 해시값이 어떻게 달라지는지 직접 확인해 보자.
송신자와 수신자가 같은 키로 암호화·복호화하는 양방향 방식. 속도가 빠르지만, 사용자마다 키를 개별적으로 안전하게 전달해야 하는 키 배송 문제가 있다 — 사용자가 10만 명이면 키도 10만 개를 관리해야 한다. 인터넷·모바일 뱅킹의 OTP와 보안카드가 대표 사례다.
대칭 암호화의 키 배송 문제를 해결하기 위해 등장했다. 송수신자가 서로 다른 키 쌍(공개키·개인키)을 가지며, 한쪽 키로 암호화한 것은 반드시 짝을 이루는 다른 키로만 풀 수 있다. 처리 속도는 느리지만 키를 안전하게 교환할 필요가 없다. 전자 서명, 공동(공인) 인증서가 대표 사례다.
그럼 이 두 개의 키를 실제로 누가, 어떻게 나눠 갖는 걸까? 우리가 매일 쓰는 은행 거래로 살펴보자.
누가 중간에 가로채도 내용을 못 읽게 하는 것이 목적. 받는 쪽(은행)의 키 쌍을 쓴다.
"진짜 본인이 보냈는지"를 증명하는 것이 목적. 보내는 쪽(고객)의 키 쌍을 쓴다.
| 목적 | 암호화·서명에 쓰는 키 | 복호화·검증에 쓰는 키 | 얻는 것 |
|---|---|---|---|
| Ⓐ 비밀로 보내기 | 받는 사람(은행)의 공개키 | 받는 사람(은행)의 개인키 | 기밀성 |
| Ⓑ 본인 증명 | 보내는 사람(고객)의 개인키 | 보내는 사람(고객)의 공개키 | 인증 · 무결성 · 부인 방지 |
"은행에서 인증서를 받는다"고 흔히 말하지만, 실제로는 은행이 내 키를 만들어 주는 것이 아니다. 키 쌍은 내 기기 안에서 만들어지고, 인증기관은 그중 공개키에만 도장을 찍어 준다.
| 단계 | 실제로 일어나는 일 | 키 관점 |
|---|---|---|
| ① 발급 신청 | 내 PC·휴대폰 안에서 키 쌍이 생성된다 | 개인키·공개키가 내 기기에서 태어남 |
| ② 본인 확인 | 창구 방문 또는 계좌번호 + 보안카드·OTP 입력 | "이 공개키의 주인이 누구인지" 확인 |
| ③ 신청서 전송 | 공개키 + 신원정보를 인증기관에 보냄. 이때 내 개인키로 서명해 "짝 개인키를 갖고 있음"을 증명 | 공개키만 나가고 개인키는 안 나감 |
| ④ 인증서 발급 | 인증기관이 확인 후 자기 개인키로 서명한 인증서를 준다 | 인증서 = 내 신원 + 내 공개키 + CA 서명 |
| ⑤ 저장 | 개인키는 인증서 비밀번호로 암호화되어 기기에 저장 | 개인키는 끝까지 내 기기 안에만 |
있다. 두 키는 남남이 아니라 한 쌍으로 태어난 짝꿍이다. 그런데 이상하다 — 관계가 있다면 공개키를 보고 개인키를 알아낼 수 있어야 하지 않을까? 바로 그 지점을 막아 둔 것이 비대칭 암호의 핵심이다. 세 단계로 나눠 보자.
1단계 · 한쪽으로만 쉬운 계산이 있다
다음 두 문제를 직접 풀어 보자. 계산기 없이 걸리는 시간이 완전히 다르다.
문제 A. 61 × 53 = ? → 몇 초면 된다 (3233) 문제 B. 3233 = ? × ? → 2, 3, 5, 7, 11 … 하나씩 나눠 봐야 한다
둘은 같은 사실을 묻는데 난이도가 다르다. 곱하는 방향은 쉽고, 되돌리는 방향은 어렵다. 이렇게 한쪽으로만 쉬운 계산을 일방향 함수라고 부른다. 공개키는 이미 곱해 놓은 결과(3233)를, 개인키는 그 재료(61과 53)를 아는 사람만 만들 수 있다.
2단계 · 암호는 '시계 산수'를 쓴다
시계를 떠올려 보자. 지금이 10시인데 5시간이 지나면 15시가 아니라 3시다. 12를 넘으면 다시 처음으로 돌아오기 때문이다. 이런 계산을 나머지 연산이라 하고 mod(모드)라고 쓴다.
10 + 5 = 15 → 15 mod 12 = 3 (12시간짜리 시계)
암호에서는 3233짜리 시계를 쓴다고 생각하면 된다. 숫자를 아무리 크게 곱해도 결과는 항상 0~3232 사이 어딘가로 돌아온다. 밖에서 보면 값이 마구 튀어서 원래 숫자를 짐작할 수 없다.
3단계 · 두 키는 서로를 되돌리는 짝이다
이제 키를 실제로 만들어 보자. 순서대로 따라가면 숫자가 어디서 나오는지 보인다.
① 소수 두 개를 고른다 p = 61, q = 53 ← 나만 아는 재료
② 시계 크기를 만든다 n = 61 × 53 = 3233 ← 공개해도 되는 값
③ 시계의 '한 바퀴'를 구한다 (61-1) × (53-1) = 60 × 52 = 3120
④ 잠금 횟수를 고른다 e = 17 ← 내가 정하는 값
⑤ 풀림 횟수를 계산한다 17 × d 를 3120으로 나눠 나머지가 1이 되는 d
d = 2753 (17 × 2753 = 46801 = 3120 × 15 + 1)
17은 우리가 고른 숫자고, 2753은 계산해서 나온 숫자다. ④에서 e를 19나 23으로 골랐다면 d도 다른 값이 됐을 것이다. 그러니 공개키 e는 "17번 곱해서 잠그기", 개인키 d는 "2753번 곱해서 풀기"인 셈이고, 17번 잠그고 2753번 풀면 정확히 제자리로 돌아온다. ⑤를 그렇게 되도록 맞춰 놓았기 때문이다.
φ(n)이라 부른다.여기가 핵심이다. ⑤에서 d를 구하려면 ③의 3120이 필요하고, 3120을 구하려면 ①의 61과 53을 알아야 한다. 그런데 밖으로 공개되는 건 3233과 17뿐이다. 공개키만 아는 사람은 3233을 61과 53으로 되돌릴 수 없으니 3120도, d도 구할 수 없다 — 관계는 분명히 있지만 따라갈 수가 없는 것이다.
φ는 그리스 문자로 "파이"라고 읽는다. 원주율 π도 우리말로 똑같이 "파이"라서 헷갈릴 때는 "피"라고 구분해 읽기도 한다. φ(n)은 오일러 파이 함수라 부르며, "n보다 작으면서 n과 서로소인 수의 개수"를 뜻한다. ≡는 "합동", mod는 "모드"라고 읽고 나머지를 구한다는 뜻이다 — e × d ≡ 1 (mod φ(n))은 "e와 d를 곱해 φ(n)으로 나누면 나머지가 1"이라는 말이다.n = p × q (아주 큰 소수 두 개를 곱한다) φ(n) = (p-1) × (q-1) e × d ≡ 1 (mod φ(n)) ← 개인키 d는 공개키 e의 "역수" 공개키 = (n, e) 암호화: c = me mod n 개인키 = (n, d) 복호화: m = cd mod n
앞에서 쓴 61과 53으로 실제로 맞는지 확인해 보자. 잠근 뒤 풀면 원래 숫자로 정확히 돌아온다.
p = 61, q = 53 → n = 3233, φ(n) = 60 × 52 = 3120 e = 17 → d = 2753 (17 × 2753 = 46801 = 1 + 15 × 3120 ✓) 평문 65 --암호화--> 6517 mod 3233 = 2790 암호문 2790 --복호화--> 27902753 mod 3233 = 65 ✓ 원문 복원
위 숫자를 그대로 쓴다. 공개키 (n=3233, e=17)는 누구나 알고, 개인키 d=2753은 나만 안다.
잠근 키로는 열리지 않는다는 것을 직접 확인해 보자.
공개키 = 개인키 × G (타원곡선 위의 점 덧셈)로 관계가 더 단순해 보이지만, 역산(이산로그 문제)이 지극히 어렵다는 원리는 똑같다. 같은 안전성을 더 짧은 키로 얻을 수 있어 스마트폰·인증서에 널리 쓰인다.| 구분 | 대칭 암호화 | 비대칭 암호화 |
|---|---|---|
| 키 관계 | 암호화 키 = 복호화 키 | 암호화 키 ≠ 복호화 키 |
| 속도 | 빠름 | 느림 |
| 키 교환 | 필요 (문제 발생 가능) | 불필요 (쉬움) |
| 대표 사례 | OTP, 보안카드 | 공동 인증서, 전자 서명 |
단방향·대칭·비대칭 암호화의 차이와 각각의 실제 사용 사례를 설명할 수 있나요?
4차 산업혁명 시대, 우리 주변에는 규모를 가늠할 수 없을 만큼 많은 데이터가 매일 쏟아진다. 이 방대한 데이터를 빅데이터라 하고, 이를 목적에 맞게 다루는 것이 데이터 분석이다.
빅데이터는 기존 방식으로는 처리하기 어려운 막대한 양의 데이터셋(또는 그 처리 기술)을 말하며, 세 가지 특징 3V로 요약된다.
| 수집 방법 | 특징 | 예 |
|---|---|---|
| 공공 데이터 | 공공 기관이 만든 데이터. 개방성이 가장 큰 특징으로, 개인정보·안보를 침해하지 않으면 누구나 자유롭게 쓸 수 있다. 신뢰성이 높다. | 공공데이터포털(data.go.kr), 기상자료개방포털 |
| 민간 데이터 | 기업·개인이 상업적 목적으로 수집. 더 전문적이고 실생활에 도움이 되는 경우가 많다. | 캐글(Kaggle), 데이콘(DACON) |
| 웹크롤링 | 웹 크롤러(web crawler)라는 프로그램으로 웹사이트 정보를 자동 수집하는 방법. | 특정 웹사이트의 실시간 통계 자동 수집 |
| 직접 수집 | 필요한 데이터가 없을 때 직접 얻는 방법. 목적에 딱 맞는 데이터를 얻을 수 있다. | 설문 조사, 카메라 촬영, 센서 측정 |
빅데이터의 개념과 3V 특징을 설명하고, 문제 해결에 필요한 데이터를 수집 방법별로 구분해 고를 수 있나요?
모은 데이터를 그대로 분석에 쓸 수는 없다. 다듬어야(전처리) 하고, 눈에 보이게 그려야(시각화) 비로소 의미를 읽을 수 있다.
데이터 전처리는 원시 데이터를 원하는 형식으로 다듬는 작업이다. 적절히 전처리하면 분석과 기계학습의 정확도·신뢰도가 올라간다.
| 전처리 작업 | 내용 |
|---|---|
| 결측치 제거 | 데이터셋에서 값이 빠진(누락된) 결측치를 채우거나 제거한다. |
| 이상치 제거 | 일반적인 경향에서 크게 벗어난 이상치는 분석 결과를 왜곡할 수 있어 제거한다. |
| 데이터 정리 | 중복 항목 제거, 오류·맞춤법 수정 등. |
| 데이터 통합 | 서로 유사하거나 공통 속성(예: ID)을 가진 데이터를 하나로 합친다. |
| 데이터 정규화 | 속성값을 공통 척도나 일정 범위로 맞춰 분석 일관성을 높인다. |
기상청 기상자료개방포털에서 서울(지점 108)의 일별 기온을 받아 그려 보면, 그래프 한가운데가 뻥 뚫려 있다. 관측 장비가 고장 난 것도, 자료를 잃어버린 것도 아니다. 6·25 전쟁으로 관측 자체가 멈췄던 기간이다.
결측 구간의 앞뒤 실제 기록은 이렇다. 1950년 8월 31일에 끊기고, 1,187일(약 3년 3개월) 뒤인 1953년 12월 1일에 다시 시작된다.
| 날짜 | 최저기온 | 평균기온 | 최고기온 |
|---|---|---|---|
| 1950-08-30 | 18.0 ℃ | 24.6 ℃ | 32.6 ℃ |
| 1950-08-31 | 20.1 ℃ | 25.4 ℃ | 32.5 ℃ |
| ⋯ 1950-09-01 ~ 1953-11-30 · 1,187일 결측 ⋯ | |||
| 1953-12-01 | 7.4 ℃ | 12.2 ℃ | 16.2 ℃ |
| 1953-12-02 | 1.4 ℃ | 5.6 ℃ | 9.0 ℃ |
결측치는 한 가지 모습으로 오지 않는다. 같은 1,187일인데도 자료를 열어 보면 두 가지가 섞여 있다.
| 모습 | 일수 | 실제 자료 |
|---|---|---|
| 행이 아예 없음 | 431일 | 그 날짜가 목록에 등장하지 않는다 |
| 행은 있는데 값이 없음 | 756일 | {"dt":"1950-09-01", "stnNm":"서울(108)"} — 날짜는 있는데 기온 항목이 통째로 빠져 있다 |
그래서 "빈칸을 찾아라"만으로는 결측치를 다 못 찾는다. 날짜를 1일 간격으로 쭉 만들어 놓고 있어야 할 날과 실제 있는 날을 맞춰 봐야 빠진 날이 드러난다.
데이터 시각화는 수치·통계 데이터를 그래픽으로 표현해 분석 결과를 직관적으로 이해하기 쉽게 만드는 기술이다. 데이터 형태에 따라 알맞은 시각화 방법이 다르다.
정형 데이터(수치·문자·날짜·지리)는 표·그래프로, 비정형 데이터(이미지·텍스트·소리)는 이미지 뷰어·워드 클라우드처럼 형태에 맞는 방법으로 시각화한다.
교과서의 ‘운동 시간과 소모 열량’ 예시처럼, 두 변수의 상관관계 강도를 슬라이더로 바꾸면 산점도와 회귀선이 실시간으로 달라져요.
빅데이터 분석 도구로 데이터를 시각화하고, 그 결과의 의미와 가치를 해석할 수 있나요? 정형·비정형 데이터에 맞는 시각화 방법을 구분할 수 있나요?
지금까지 나눠서 배운 수집 → 전처리 → 시각화를 하나의 프로그램으로 이어 본다. 120년 가까운 서울 기온 기록에서 내 생일 하루만 뽑아, 그날의 기온이 해마다 어떻게 달라졌는지 그려 볼 것이다.
matplotlib은 기본 상태에서 한글을 □로 그린다. 한글 폰트를 붙여 주는 도구를 먼저 설치한다.
# 한글 시각화 도구 설치 (matplotlib은 기본 상태로는 한글이 □로 깨진다) !pip install koreanize-matplotlib
import koreanize_matplotlib import matplotlib.pyplot as plt import csv
# 분석할 자료를 코랩으로 바로 내려받는다 (기상청에서 받은 파일 그대로) !wget -q -O weather.csv https://edu.sunnylab.co.kr/data/weather.csv !ls -lh weather.csv
from google.colab import drive drive.mount('/content/drive', force_remount=True) import os os.chdir('/content/drive/MyDrive/data/') # weather.csv 를 올려 둔 폴더로 이동
코드를 쓰기 전에 파일이 어떻게 생겼는지 먼저 본다. 첫 줄은 제목 줄이고, 둘째 줄부터 하루에 한 줄씩이다.
날짜,지점,평균기온(℃),최저기온(℃),최고기온(℃) "→1907-10-01",108,13.5,7.9,20.7 "→1907-10-02",108,16.2,7.9,22 ⋮ "→1950-08-31",108,25.4,20.1,32.5 "→1950-09-01",108,,, ← 값이 비어 있다 (결측치) ⋮ "→2026-09-05",108,24,19.1,29.4 → 는 눈에 보이지 않는 탭 문자를 표시한 것
두 가지를 미리 알아 두면 코드가 쉬워진다.
.strip()으로 떼어 낸다.encoding='cp949'일까? · 기상청 CSV는 윈도우 한글 인코딩으로 저장되어 있다. 코랩(리눅스)의 기본값인 UTF-8로 열면 한글이 깨지거나 오류가 난다. 남이 만든 파일을 열 때 인코딩부터 확인하는 것이 전처리의 첫걸음이다.def 최고온도(): f = open('weather.csv', mode='r', encoding='cp949') data = csv.reader(f, delimiter=',') max_date = '' max_temp = -999 header = next(data) # 첫 줄은 제목 줄이므로 건너뛴다 print(header) for row in data: if row[-1] == '': # ← 결측치 처리 ① 값이 비었으면 row[-1] = -999 # '있을 수 없는 값'으로 바꿔 둔다 row[-1] = float(row[-1]) if row[-1] > max_temp: max_date = row[0].strip() max_temp = row[-1] print("최고기온일", max_date) print("최고온도", max_temp) f.close()
row[-1]은 맨 뒤 열(최고기온), row[0]은 날짜다. 뒤에서부터 세면 가운데에 열이 하나 늘어도 코드를 고칠 필요가 없다.
-999일까? 0은 안 되나? · 안 된다. 이 코드는 가장 큰 값을 찾는다. 빈 값을 0으로 바꾸면 영하로 내려간 겨울날보다 0이 더 커서 순위가 뒤틀린다. 그래서 절대 나올 수 없는 값을 넣어 비교에서 자연스럽게 밀려나게 한다. 다만 이 -999가 평균을 구하는 계산에 섞이면 결과가 망가진다 — 목적에 따라 안전한 값이 달라진다는 뜻이다.def 생일날씨조회(): f = open('weather.csv', mode='r', encoding='cp949') data = csv.reader(f) next(data) high, low, year = [], [], [] birth = input('생일을 입력하세요(2000-01-02)') if len(birth) < 1: birth = "2000-01-03" # 그냥 Enter를 누르면 1월 3일로 [y, m, d] = birth.split("-") for row in data: if row[-1] != '' and row[-2] != '': # ← 결측치 처리 ② 최고·최저가 둘 다 있는 날만 날짜 = row[0].strip() if m == 날짜.split("-")[1] and d == 날짜.split("-")[-1]: high.append(float(row[-1])) low.append(float(row[-2])) year.append(날짜.split("-")[0]) f.close() return high, low, year def show_Plot(high, low, year): plt.figure(figsize=(30, 3)) plt.plot(high, "r", marker='.', label='최고 온도') plt.plot(low, 'b', marker='s', label='최저 온도') plt.xlabel("년도") plt.ylabel("온도") x = range(0, len(year), 5) # 5개마다 눈금 하나 plt.xticks(x, year[::5], rotation=45, fontsize=14) plt.legend() plt.title('내 생일의 기온 변화 그래프') plt.show()
여기서는 최고·최저가 둘 다 있는 날만 골라 담는다. 하나라도 비면 그 해는 아예 빼는 것이다. ④와 ⑤는 같은 결측치를 서로 다른 방법으로 다룬다.
if __name__ == '__main__': 최고온도() high, low, year = 생일날씨조회() show_Plot(high, low, year)
['날짜', '지점', '평균기온(℃)', '최저기온(℃)', '최고기온(℃)'] 최고기온일 2018-08-01 최고온도 39.6 생일을 입력하세요(2000-01-02)
| 결측치 처리 | 방법 | 좋은 점 | 대가 |
|---|---|---|---|
| 바꾸기(④) | 빈 값을 -999로 치환 | 모든 행을 그대로 유지 | 평균·합계에 섞이면 결과가 망가진다 |
| 건너뛰기(⑤) | 빈 값이 있는 행을 제외 | 남은 값은 전부 진짜 값 | 가로축 간격이 실제와 어긋난다 |
① max_temp의 처음 값이 -999인 이유는? ② .strip()을 빼면 출력이 어떻게 달라질까? ③ 내 생일이 2월 29일이면 그래프가 어떻게 달라질까? ④ 건너뛰기 대신 빠진 해를 빈칸으로 남겨 가로축 간격을 실제와 맞추려면 코드를 어떻게 고쳐야 할까?
광산에서 광물을 캐내는 광부처럼, 방대한 데이터에서 의미 있는 정보를 찾아내는 직업이 데이터 마이너(data miner)다. 쇼핑몰 구매 기록, 검색 기록, SNS 데이터 같은 빅데이터를 분석해 미래에 도움이 될 정보를 만든다. 통계·프로그래밍에 대한 이해와, 세상을 바라보는 안목(도메인 지식)이 함께 필요한 직업이다.
| 주제 | 핵심 |
|---|---|
| 데이터 압축 | 무손실(RLE, 허프만) vs 손실(JPEG, MPEG) — 압축비 = 압축 전 용량 ÷ 압축 후 용량 |
| 데이터 암호화 | 치환형·전치형(고전) / 단방향·대칭·비대칭(현대) |
| 빅데이터 | 3V(양·속도·다양성), 수집(공공·민간·웹크롤링·직접) |
| 전처리·시각화 | 결측치·이상치 제거, 정규화, 통합 → 막대·산점도·박스플롯·워드클라우드로 시각화 |