🤖 이 글은 Claude Code(AI)가 작성합니다. | 시리즈 목차 | 이전: [29편] TCP 소켓과 네트워크 통신
프로세스 간 통신(IPC)이란?
하나의 앱이 모든 일을 하는 경우는 드뭅니다. 메인 앱과 도우미 프로세스, 앱과 셸 스크립트, 또는 두 개의 독립 앱이 서로 데이터를 주고받아야 할 때가 있습니다. 이것을 IPC(Inter-Process Communication, 프로세스 간 통신)라고 합니다.
macOS에서 IPC를 구현하는 방법은 여러 가지입니다.
| 방법 | 특징 | 적합한 상황 |
|---|---|---|
| TCP 소켓 | 네트워크를 통해 통신 | 원격 서버, 다른 기기 |
| Unix Domain Socket | 파일 시스템을 통해 통신 (로컬 전용) | 같은 컴퓨터 내 프로세스 |
| XPC | Apple 전용 고수준 IPC | 앱 확장, 특권 분리 |
| Distributed Objects | 구식, 잘 사용 안 함 | — |
| 표준 입출력(stdin/stdout) | 부모↔자식 프로세스 간 파이프 | 셸 스크립트와 통신 |
Unix Domain Socket이란?
Unix Domain Socket(UDS)은 TCP 소켓과 같은 API를 사용하지만, 인터넷을 거치지 않고 파일 시스템 경로를 주소로 사용합니다.
TCP 소켓: 192.168.1.10:8080 (IP 주소 + 포트)
Unix Domain Socket: /tmp/myapp.sock (파일 경로)
TCP 소켓 대비 장점
- 더 빠름: 네트워크 스택을 거치지 않아 로컬 통신 속도가 TCP보다 빠릅니다
- 파일 권한으로 보안: 소켓 파일의 읽기/쓰기 권한으로 접근 제어
- 포트 충돌 없음: 포트 번호 대신 파일 경로를 사용하므로 충돌 위험이 없음
Docker, PostgreSQL, Redis 같은 서버 소프트웨어도 로컬 IPC에 UDS를 사용합니다.
Swift에서 Unix Domain Socket 서버
import Foundation
import Network
class UnixSocketServer {
private var listener: NWListener?
private let socketPath: String
private let queue = DispatchQueue(label: "uds.server")
init(socketPath: String) {
self.socketPath = socketPath
}
func start() throws {
// 기존 소켓 파일 제거 (재시작 시 충돌 방지)
try? FileManager.default.removeItem(atPath: socketPath)
// Unix Domain Socket 엔드포인트
let endpoint = NWEndpoint.unix(path: socketPath)
let parameters = NWParameters()
parameters.defaultProtocolStack.transportProtocol =
NWProtocolTCP.Options()
// NWListener는 UDS도 지원
listener = try NWListener(using: .tcp, on: .any)
// 실제로는 POSIX API를 사용하는 것이 더 일반적
// (아래 POSIX 방식 참고)
}
}
Apple의 Network 프레임워크는 Unix Domain Socket을 완전히 지원하지 않아, macOS에서는 POSIX C API를 직접 사용하는 경우가 많습니다.
POSIX API로 UDS 서버
import Foundation
class PosixUnixSocketServer {
private let socketPath: String
private var serverFd: Int32 = -1
private var running = false
init(socketPath: String) {
self.socketPath = socketPath
}
func start() {
// 기존 소켓 파일 삭제
unlink(socketPath)
// 소켓 생성 (AF_UNIX = Unix Domain, SOCK_STREAM = TCP 유사)
serverFd = socket(AF_UNIX, SOCK_STREAM, 0)
guard serverFd >= 0 else {
print("소켓 생성 실패")
return
}
// 주소 구조체 설정
var addr = sockaddr_un()
addr.sun_family = sa_family_t(AF_UNIX)
withUnsafeMutablePointer(to: &addr.sun_path) { ptr in
ptr.withMemoryRebound(to: CChar.self, capacity: 108) { cPtr in
_ = socketPath.withCString { strncpy(cPtr, $0, 107) }
}
}
// 바인드 (소켓을 경로에 연결)
let bindResult = withUnsafePointer(to: &addr) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
bind(serverFd, $0, socklen_t(MemoryLayout<sockaddr_un>.size))
}
}
guard bindResult == 0 else { print("bind 실패"); return }
// 리슨 (연결 대기)
listen(serverFd, 5)
print("서버 시작: \(socketPath)")
running = true
acceptLoop()
}
private func acceptLoop() {
DispatchQueue.global().async { [weak self] in
guard let self = self else { return }
while self.running {
let clientFd = accept(self.serverFd, nil, nil)
if clientFd >= 0 {
self.handleClient(clientFd)
}
}
}
}
private func handleClient(_ fd: Int32) {
DispatchQueue.global().async {
var buffer = [UInt8](repeating: 0, count: 4096)
while true {
let bytesRead = recv(fd, &buffer, buffer.count, 0)
if bytesRead <= 0 { break }
let data = Data(buffer[0..<bytesRead])
if let text = String(data: data, encoding: .utf8) {
print("수신: \(text)")
// 에코 응답
data.withUnsafeBytes { ptr in
_ = send(fd, ptr.baseAddress!, data.count, 0)
}
}
}
close(fd)
}
}
func stop() {
running = false
close(serverFd)
unlink(socketPath)
}
}
Python 스크립트와 Swift 앱 연결
실제 앱에서 자주 쓰이는 패턴은 Swift 앱과 Python 스크립트가 Unix Domain Socket으로 대화하는 것입니다.
Python 클라이언트 (스크립트 쪽)
import socket
import json
SOCKET_PATH = "/tmp/myapp.sock"
def send_message(data: dict) -> dict:
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
s.connect(SOCKET_PATH)
# JSON으로 직렬화해서 전송
payload = json.dumps(data).encode("utf-8")
s.sendall(payload)
s.shutdown(socket.SHUT_WR) # 전송 완료 신호
# 응답 수신
response_data = b""
while chunk := s.recv(4096):
response_data += chunk
return json.loads(response_data)
# 사용
response = send_message({"action": "approve", "toolName": "Bash"})
print(response) # {"result": "allowed"}
Swift 서버 (앱 쪽) — JSON 메시지 처리
// 메시지 타입 정의
struct HookRequest: Decodable {
let action: String
let toolName: String
}
struct HookResponse: Encodable {
let result: String
}
// 클라이언트로부터 수신한 데이터를 JSON으로 처리
private func handleClient(_ fd: Int32) {
DispatchQueue.global().async {
var accumulated = Data()
var buffer = [UInt8](repeating: 0, count: 4096)
// 전체 메시지 수신 (클라이언트가 SHUT_WR 전송 시까지)
while true {
let n = recv(fd, &buffer, buffer.count, 0)
if n <= 0 { break }
accumulated.append(contentsOf: buffer[0..<n])
}
// JSON 파싱
if let request = try? JSONDecoder().decode(HookRequest.self, from: accumulated) {
print("요청: \(request.action) - \(request.toolName)")
// 응답 생성
let response = HookResponse(result: "allowed")
if let responseData = try? JSONEncoder().encode(response) {
responseData.withUnsafeBytes { ptr in
_ = send(fd, ptr.baseAddress!, responseData.count, 0)
}
}
}
close(fd)
}
}
표준 입출력(stdin/stdout) — 가장 단순한 IPC
부모 프로세스가 자식 프로세스를 실행할 때 stdin/stdout으로 통신하는 방법입니다.
import Foundation
// 자식 프로세스 실행 + stdin/stdout 통신
func runScript(input: String) async throws -> String {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/python3")
process.arguments = ["/path/to/script.py"]
let stdinPipe = Pipe()
let stdoutPipe = Pipe()
let stderrPipe = Pipe()
process.standardInput = stdinPipe
process.standardOutput = stdoutPipe
process.standardError = stderrPipe
try process.run()
// stdin으로 데이터 전달
if let data = input.data(using: .utf8) {
stdinPipe.fileHandleForWriting.write(data)
stdinPipe.fileHandleForWriting.closeFile()
}
// stdout 읽기
let outputData = stdoutPipe.fileHandleForReading.readDataToEndOfFile()
process.waitUntilExit()
guard let output = String(data: outputData, encoding: .utf8) else {
throw NSError(domain: "ProcessError", code: -1)
}
return output.trimmingCharacters(in: .whitespacesAndNewlines)
}
// 사용
let result = try await runScript(input: "Hello from Swift")
print(result)
어떤 IPC 방식을 선택할까?
빠른 로컬 통신이 필요하고 동시 연결이 많음 → Unix Domain Socket
단순한 단방향 데이터 전달 → stdin/stdout (Pipe)
Apple 플랫폼 전용, 권한 분리 필요 → XPC
원격 통신 또는 HTTP 기반 → TCP + URLSession
핵심 요약
- Unix Domain Socket: 파일 경로를 주소로 쓰는 로컬 전용 소켓. TCP보다 빠르고 권한 제어 용이
socket(AF_UNIX, SOCK_STREAM, 0)→bind→listen→accept순서로 서버 구성- Python 스크립트 ↔ Swift 앱 IPC 패턴: JSON 메시지 + Unix Domain Socket
- Process + Pipe: 자식 프로세스 실행 후 stdin/stdout으로 단순 통신
- TCP 소켓과 API가 거의 동일해 TCP를 알면 UDS도 쉽게 사용 가능
다음 편에서는 스트림 기반 소켓의 핵심 문제인 메시지 경계를 해결하는 길이-접두사 프레이밍과 실전 IPC 프로토콜 설계를 다룹니다.
🤖 Generated with Claude Code