1. 네트워크 인프라스트럭처 이해
1.1 네트워크 장비의 역할
컴퓨터 간 데이터 교환을 위해서는 다양한 네트워크 장비들이 필요합니다. 각 장비의 특성과 동작 원리를 살펴보겠습니다.
스위치 (L2 Switch)
여러 대의 컴퓨터를 연결하여 로컬 네트워크를 구성하는 핵심 장비입니다. 각 호스트에는 고유한 IP 주소가 할당되어야 통신이 가능합니다.
호스트 구성 예시:
- 워크스테이션 A: 192.168.10.1
- 워크스테이션 B: 192.168.10.2
- 워크스테이션 C: 192.168.10.3
- 워크스테이션 D: 192.168.10.4
ARP 프로토콜 동작 방식:
IP 주소만 알고 MAC 주소를 모르는 상황에서 통신을 시작하면, 스위치는 브로드캐스트 방식으로 모든 포트에 데이터를 전송합니다. 대상 호스트만 응답하고 나머지는 패킷을 버립니다.
이 과정에서 각 호스트는 ARP 테이블을 유지하여 IP-MAC 매핑 정보를 캐싱합니다. 이후 통신에서는 유니캐스트로 직접 전송이 가능합니다.
ARP 테이블 예시:
네트워크 주소 물리적 주소
192.168.10.1 a4-5f-3c-2b-11-9e
192.168.10.3 a4-5f-3c-2b-11-c0
스위치 내부에도 MAC 주소 테이블이 존재하여 포트와 연결된 호스트의 물리적 주소를 관리합니다.
라우터 (Router)
서로 다른 네트워크 세그먼트 간 통신을 가능하게 하는 장비입니다. 브로드캐스트 도메인을 분리하여 네트워크 성능을 향상시킵니다.
네트워크 구성 예시:
A동 사무실:
- PC1: 192.168.10.1 게이트웨이: 192.168.10.254
- PC2: 192.168.10.2 게이트웨이: 192.168.10.254
B동 사무실:
- PC1: 192.168.20.1 게이트웨이: 192.168.20.254
- PC2: 192.168.20.2 게이트웨이: 192.168.20.254
라우터 인터페이스 설정:
eth0: 192.168.10.254 (192.168.10.0/24 네트워크)
eth1: 192.168.20.254 (192.168.20.0/24 네트워크)
게이트웨이는 외부 네트워크로 나가는 관문 역할을 합니다. 라우팅 테이블을 참조하여 패킷을 적절한 인터페이스로 전달합니다.
L3 스위치
스위칭과 라우팅 기능을 통합한 장비로, VLAN을 활용하여 논리적으로 네트워크를 분리합니다.
VLAN 기반 구성:
VLAN 10 (A동):
- 포트 1-3 할당
- IP: 192.168.10.254
VLAN 20 (B동):
- 포트 4-6 할당
- IP: 192.168.20.254
1.2 핵심 네트워크 개념
IP 주소와 서브넷 마스크
IPv4 주소는 32비트로 구성되며, 점으로 구분된 10진수 표기법을 사용합니다.
이진 표현: 11000000.10101000.00000001.11001000
10진 표현: 192.168.1.200
서브넷 마스크는 네트워크 부분과 호스트 부분을 구분합니다:
예시 1:
IP: 192.168.1.200 11000000.10101000.00000001.11001000
서브넷: 255.255.255.0 11111111.11111111.11111111.00000000
네트워크: 192.168.1.0/24 (앞 24비트)
예시 2:
IP: 172.16.50.100 10101100.00010000.00110010.01100100
서브넷: 255.255.240.0 11111111.11111111.11110000.00000000
네트워크: 172.16.48.0/20 (앞 20비트)
같은 네트워크에 속한 호스트끼리는 직접 통신이 가능하고, 다른 네트워크와는 라우터를 거쳐야 합니다.
DHCP (Dynamic Host Configuration Protocol)
IP 주소를 수동으로 설정하는 대신, 네트워크 장비의 DHCP 서버에서 자동으로 할당받을 수 있습니다. IP 주소, 서브넷 마스크, 게이트웨이, DNS 정보가 함께 제공됩니다.
사설 IP와 공인 IP
사설 IP 대역 (RFC 1918):
- 10.0.0.0 ~ 10.255.255.255
- 172.16.0.0 ~ 172.31.255.255
- 192.168.0.0 ~ 192.168.255.255
인터넷 통신을 위해서는 ISP에서 할당한 공인 IP가 필요합니다. NAT 기술을 통해 여러 사설 IP가 하나의 공인 IP를 공유하기도 합니다.
포트와 서비스 식별
단일 서버에서 여러 애플리케이션을 운영할 때 포트 번호로 구분합니다.
예시:
- 웹 서비스 A: 203.0.113.45:8080
- 웹 서비스 B: 203.0.113.45:9090
- API 서버: 203.0.113.45:5000
권장 사용자 정의 포트 범위: 5000-65535 (Well-known 포트 제외)
DNS와 도메인 이름
사람이 기억하기 쉬운 도메인을 IP 주소로 변환하는 시스템입니다.
도메인 → IP 매핑:
api.example.com → 203.0.113.45
db.internal.net → 10.0.0.15
DNS 조회 과정:
- 브라우저 캐시 확인
- 운영체제 캐시 및 hosts 파일 확인
- 로컬 DNS 서버 (resolver) 조회
- 필요시 루트 → TLD → 권한 네임서버 순차 조회
2. Python 소켓 프로그래밍
Python의 socket 모듈을 활용한 네트워크 애플리케이션 개발 방법입니다.
2.1 기본 TCP 통신 예제
에코 서버:
import socket
def run_echo_server(host='0.0.0.0', port=9001):
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listener.bind((host, port))
listener.listen(5)
print(f"서버 시작: {host}:{port}")
while True:
conn, client_addr = listener.accept()
print(f"연결 수락: {client_addr}")
try:
payload = conn.recv(4096)
if payload:
text = payload.decode('utf-8')
print(f"수신: {text}")
conn.sendall(f"에코: {text}".encode('utf-8'))
finally:
conn.close()
if __name__ == '__main__':
run_echo_server()
에코 클라이언트:
import socket
def run_echo_client(server_ip, server_port):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.connect((server_ip, server_port))
message = "안녕하세요, 서버!"
sock.sendall(message.encode('utf-8'))
response = sock.recv(4096)
print(f"서버 응답: {response.decode('utf-8')}")
finally:
sock.close()
if __name__ == '__main__':
run_echo_client('127.0.0.1', 9001)
2.2 대화형 서비스 구현
간단한 문의 응대 시스템:
import socket
def inquiry_server():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 9002))
server.listen(5)
print("문의 시스템 대기 중...")
while True:
session, addr = server.accept()
# 초기 환영 메시지
session.sendall("고객 지원 시스템에 오신 것을 환영합니다.\n문의사항을 입력하세요 (종료: exit):\n".encode('utf-8'))
while True:
query = session.recv(2048).decode('utf-8').strip()
if not query or query.lower() == 'exit':
break
# 간단한 키워드 매칭 응답
if '가격' in query:
reply = "요금 정보는 홈페이지에서 확인 가능합니다."
elif '배송' in query:
reply = "배송은 2-3일 소요됩니다."
else:
reply = "담당자에게 전달하겠습니다. 잠시만 기다려주세요."
session.sendall((reply + "\n").encode('utf-8'))
session.close()
print(f"세션 종료: {addr}")
if __name__ == '__main__':
inquiry_server()
대화형 클라이언트:
import socket
def inquiry_client():
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 9002))
# 초기 메시지 수신
print(client.recv(2048).decode('utf-8'), end='')
while True:
user_input = input("> ")
if user_input.lower() == 'exit':
break
client.sendall(user_input.encode('utf-8'))
response = client.recv(2048).decode('utf-8')
print(response, end='')
client.close()
if __name__ == '__main__':
inquiry_client()
2.3 파일 전송 시스템
파일 수신 서버:
import socket
import os
def file_receiver(save_dir='./received', port=9003):
os.makedirs(save_dir, exist_ok=True)
receiver = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
receiver.bind(('127.0.0.1', port))
receiver.listen(3)
print(f"파일 수신 서버: {port}")
conn, addr = receiver.accept()
# 메타데이터 수신 (파일명:크기)
meta = conn.recv(256).decode('utf-8')
filename, filesize = meta.split(':')
filesize = int(filesize)
filepath = os.path.join(save_dir, filename)
received = 0
with open(filepath, 'wb') as f:
while received < filesize:
chunk = conn.recv(min(8192, filesize - received))
if not chunk:
break
f.write(chunk)
received += len(chunk)
print(f"진행률: {received}/{filesize} bytes")
print(f"저장 완료: {filepath}")
conn.close()
receiver.close()
if __name__ == '__main__':
file_receiver()
파일 전송 클라이언트:
import socket
import os
def file_sender(filepath, server='127.0.0.1', port=9003):
if not os.path.exists(filepath):
print("파일이 존재하지 않습니다.")
return
filename = os.path.basename(filepath)
filesize = os.path.getsize(filepath)
sender = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sender.connect((server, port))
# 메타데이터 전송
sender.sendall(f"{filename}:{filesize}".encode('utf-8'))
# 파일 내용 전송
sent = 0
with open(filepath, 'rb') as f:
while sent < filesize:
data = f.read(8192)
sender.sendall(data)
sent += len(data)
print(f"전송 중: {sent}/{filesize} bytes")
print("전송 완료")
sender.close()
if __name__ == '__main__':
file_sender('./sample.pdf')
3. 애플리케이션 아키텍처 패턴
3.1 C/S (Client/Server) 구조
전용 클라이언트 프로그램을 설치하여 서버와 통신하는 방식입니다. 데스크톱 애플리케이션, 모바일 네이티브 앱 등이 해당됩니다.
특징:
- 풍부한 UI/UX 제공 가능
- 로컬 리소스 직접 활용
- 설치 및 업데이트 필요
3.2 B/S (Browser/Server) 구조
웹 브라우저를 클라이언트로 사용하여 서버와 통신하는 방식입니다. 별도 설치 없이 URL로 접근 가능합니다.
특징:
- 플랫폼 독립적
- 즉시 배포 및 업데이트
- 브라우저 호환성 고려 필요
4. 실습 과제: 인증 기반 접속 시스템
사용자 인증 후 접속하는 보안 채널을 구현합니다.
인증 서버:
import socket
import json
import hashlib
class AuthServer:
def __init__(self, port=9004):
self.port = port
self.credentials = self._load_users()
def _load_users(self):
# 실제 환경에서는 데이터베이스 사용 권장
return {
'admin': '5f4dcc3b5aa765d61d8327deb882cf99', # 'password'의 MD5
'user01': '0acf4539a14b3aa27deeb4cbdf6e989f' # 'hello123'의 MD5
}
def verify(self, userid, password):
hashed = hashlib.md5(password.encode()).hexdigest()
return self.credentials.get(userid) == hashed
def start(self):
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.bind(('127.0.0.1', self.port))
srv.listen(5)
print(f"인증 서버 실행 중: {self.port}")
while True:
conn, addr = srv.accept()
self._handle_session(conn)
def _handle_session(self, conn):
conn.sendall(json.dumps({
'type': 'info',
'message': '인증 시스템 v1.0\n아이디와 비밀번호를 |로 구분하여 입력하세요'
}).encode('utf-8'))
while True:
data = conn.recv(1024).decode('utf-8').strip()
if '|' not in data:
conn.sendall(json.dumps({
'type': 'error',
'message': '형식: 아이디|비밀번호'
}).encode('utf-8'))
continue
uid, pwd = data.split('|', 1)
if self.verify(uid, pwd):
conn.sendall(json.dumps({
'type': 'success',
'message': f'환영합니다, {uid}님!',
'token': hashlib.sha256(uid.encode()).hexdigest()[:16]
}).encode('utf-8'))
break
else:
conn.sendall(json.dumps({
'type': 'fail',
'message': '인증 실패. 다시 시도하세요.'
}).encode('utf-8'))
conn.close()
if __name__ == '__main__':
AuthServer().start()
인증 클라이언트:
import socket
import json
def secure_client():
cli = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
cli.connect(('127.0.0.1', 9004))
# 서버 정보 수신
info = json.loads(cli.recv(2048).decode('utf-8'))
print(info['message'])
while True:
uid = input("아이디: ")
pwd = input("비밀번호: ")
cli.sendall(f"{uid}|{pwd}".encode('utf-8'))
result = json.loads(cli.recv(2048).decode('utf-8'))
print(f"[{result['type'].upper()}] {result['message']}")
if result['type'] == 'success':
print(f"발급된 토큰: {result.get('token')}")
break
cli.close()
if __name__ == '__main__':
secure_client()