🤖 이 글은 Claude Code(AI)가 작성합니다. | 시리즈 목차 | 이전: [28편] SQLite와 데이터베이스
네트워크 통신의 기초 — TCP/IP
앱이 인터넷을 통해 서버와 대화하거나, 같은 컴퓨터 안에서 두 프로세스가 대화할 때 사용하는 가장 기본적인 메커니즘이 소켓(Socket)입니다.
소켓은 두 프로그램이 데이터를 주고받기 위해 여는 “연결 창구”입니다. 편지함에 비유하면, 소켓은 주소가 붙은 우편함이고, 데이터는 그 안에 넣는 편지입니다.
TCP(Transmission Control Protocol)는 가장 널리 쓰이는 소켓 유형입니다.
- 신뢰성: 데이터가 반드시 도착하고, 순서도 보장됩니다
- 스트림 기반: 바이트 흐름으로 통신합니다
- 연결 지향: 통신 전 연결(핸드셰이크)이 필요합니다
URLSession — HTTP 통신
대부분의 앱은 REST API를 통해 서버와 통신합니다. Swift에서는 URLSession이 이를 담당합니다.
import Foundation
// GET 요청
func fetchPosts() async throws -> [Post] {
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
let (data, response) = try await URLSession.shared.data(from: url)
// HTTP 상태 코드 확인
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode([Post].self, from: data)
}
// POST 요청
func createPost(title: String, body: String) async throws -> Post {
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let payload = ["title": title, "body": body, "userId": 1] as [String: Any]
request.httpBody = try JSONSerialization.data(withJSONObject: payload)
let (data, _) = try await URLSession.shared.data(for: request)
return try JSONDecoder().decode(Post.self, from: data)
}
Network 프레임워크 — TCP 소켓 직접 사용
HTTP가 아닌 커스텀 프로토콜이나 낮은 레벨의 TCP 통신이 필요할 때는 Apple의 Network 프레임워크를 사용합니다.
TCP 클라이언트
import Network
class TCPClient {
private var connection: NWConnection?
private let queue = DispatchQueue(label: "tcp.client")
func connect(host: String, port: UInt16) {
let endpoint = NWEndpoint.hostPort(
host: NWEndpoint.Host(host),
port: NWEndpoint.Port(rawValue: port)!
)
connection = NWConnection(to: endpoint, using: .tcp)
connection?.stateUpdateHandler = { state in
switch state {
case .ready:
print("연결됨")
self.receive()
case .failed(let error):
print("연결 실패: \(error)")
case .cancelled:
print("연결 취소")
default:
break
}
}
connection?.start(queue: queue)
}
// 데이터 송신
func send(_ text: String) {
guard let data = text.data(using: .utf8) else { return }
connection?.send(content: data, completion: .contentProcessed { error in
if let error = error {
print("전송 실패: \(error)")
}
})
}
// 데이터 수신 (반복 호출)
private func receive() {
connection?.receive(minimumIncompleteLength: 1, maximumLength: 65536) { data, _, isComplete, error in
if let data = data, !data.isEmpty {
let text = String(data: data, encoding: .utf8) ?? "(바이너리)"
print("수신: \(text)")
}
if isComplete {
print("연결 종료")
} else if error == nil {
self.receive() // 다음 데이터 대기
}
}
}
func disconnect() {
connection?.cancel()
}
}
TCP 서버
class TCPServer {
private var listener: NWListener?
private let queue = DispatchQueue(label: "tcp.server")
private var connections: [NWConnection] = []
func start(port: UInt16) throws {
let parameters = NWParameters.tcp
listener = try NWListener(using: parameters,
on: NWEndpoint.Port(rawValue: port)!)
listener?.stateUpdateHandler = { state in
switch state {
case .ready:
print("서버 시작됨 — 포트 \(port)")
case .failed(let error):
print("서버 오류: \(error)")
default:
break
}
}
// 새 클라이언트 연결 수락
listener?.newConnectionHandler = { [weak self] connection in
print("새 클라이언트 연결")
self?.connections.append(connection)
self?.handleConnection(connection)
}
listener?.start(queue: queue)
}
private func handleConnection(_ connection: NWConnection) {
connection.stateUpdateHandler = { state in
if case .ready = state {
self.receiveLoop(connection)
}
}
connection.start(queue: queue)
}
private func receiveLoop(_ connection: NWConnection) {
connection.receive(minimumIncompleteLength: 1, maximumLength: 4096) { data, _, isComplete, error in
if let data = data, let text = String(data: data, encoding: .utf8) {
print("클라이언트로부터 수신: \(text)")
// 에코 응답
connection.send(content: data, completion: .idempotent)
}
if !isComplete && error == nil {
self.receiveLoop(connection)
}
}
}
func stop() {
listener?.cancel()
connections.forEach { $0.cancel() }
}
}
TCP 스트림의 핵심 문제 — 메시지 경계
TCP는 바이트 스트림입니다. “1000바이트 메시지를 보냈다”고 해서 받는 쪽이 한 번에 1000바이트를 받는다는 보장이 없습니다. 200바이트씩 5번에 나눠서 올 수도 있고, 다른 메시지와 합쳐서 올 수도 있습니다.
이 문제를 해결하는 방법이 메시지 프레이밍(Framing)입니다. 다음 편에서 자세히 다룹니다.
// 잘못된 방식 — TCP는 경계를 보장하지 않음
connection.send(content: messageData, ...) // 보낸 쪽
connection.receive(...) { data in
// 받는 쪽: data가 전체 메시지라는 보장이 없음!
}
// 올바른 방식 — 길이 접두사로 경계 표시 (다음 편에서 상세히)
var frame = Data()
var length = UInt32(messageData.count).bigEndian
frame.append(Data(bytes: &length, count: 4)) // 앞 4바이트: 길이
frame.append(messageData) // 나머지: 실제 데이터
async/await와 Network 프레임워크
// Swift Concurrency와 함께 사용하는 래퍼
actor TCPClientActor {
private var connection: NWConnection?
private let queue = DispatchQueue(label: "tcp.actor")
func connect(host: String, port: UInt16) async throws {
let endpoint = NWEndpoint.hostPort(
host: NWEndpoint.Host(host),
port: NWEndpoint.Port(rawValue: port)!
)
let conn = NWConnection(to: endpoint, using: .tcp)
// continuation으로 콜백 → async 변환
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
conn.stateUpdateHandler = { state in
switch state {
case .ready:
continuation.resume()
case .failed(let error):
continuation.resume(throwing: error)
default:
break
}
}
conn.start(queue: self.queue)
}
self.connection = conn
}
func send(_ data: Data) async throws {
guard let connection else { throw URLError(.notConnectedToInternet) }
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
connection.send(content: data, completion: .contentProcessed { error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
})
}
}
}
다른 언어와 비교
| 언어 | TCP 소켓 API |
|---|---|
| Swift (macOS) | Network 프레임워크 (NWConnection) |
| Python | socket 모듈 (BSD 소켓 래핑) |
| Node.js | net 모듈 |
| Java/Kotlin | java.net.Socket |
Python 예시와 비교:
# Python TCP 클라이언트
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("localhost", 8080))
s.send(b"Hello")
data = s.recv(1024)
s.close()
Swift의 Network 프레임워크는 Python의 socket보다 추상화 수준이 높고, TLS/연결 상태 관리가 더 편리하게 통합되어 있습니다.
핵심 요약
- TCP: 신뢰성 있는 바이트 스트림 프로토콜. 연결 후 양방향 통신
- URLSession: HTTP/HTTPS 통신에 적합한 고수준 API
- Network 프레임워크: TCP/UDP 소켓 직접 제어.
NWConnection,NWListener - TCP는 메시지 경계를 보장하지 않음 — 프레이밍이 필요
withCheckedThrowingContinuation으로 콜백 기반 API를 async/await로 변환
다음 편에서는 같은 컴퓨터 내 프로세스 간 통신에 더 적합한 Unix Domain Socket과 메시지 경계 문제를 해결하는 길이-접두사 프레이밍을 배웁니다.
🤖 Generated with Claude Code