데이터 엔지니어링

패스트캠퍼스 환급챌린지 36일차: 데이터엔지니어링 초격차 강의 후기

Big Byte 2025. 5. 6. 21:38

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.

서버와 클라이언트의 끈끈한 연결고리 🔗, TCP 소켓 프로그래밍 파헤치기!

 

안녕하세요, 여러분! ✨

 

지난 시간에는 앱과 서버 간의 통신을 마법처럼 간단하게 만들어주는 Retrofit 🧙‍♂️ 에 대해 배우면서, 복잡한 API 호출도 우아하게 처리하는 방법을 익혔죠! 인터페이스 정의만으로 네트워크 요청이 뚝딱! 정말 혁신적이었어요. 👍 Retrofit 덕분에 우리는 HTTP라는 잘 닦인 고속도로를 통해 서버와 편안하게 데이터를 주고받을 수 있게 되었죠!

 

하지만 Retrofit 같은 편리한 라이브러리 아래에는, 마치 잘 닦인 도로 밑의 견고한 기반 시설처럼, 네트워크 통신의 핵심 원리들이 숨어있답니다.

 

오늘은 그 기반 중에서도 가장 중요하고 널리 사용되는 TCP 프로그래밍의 세계로 한 걸음 더 깊이 들어가 보려고 해요. 🕵️‍♂️ "Retrofit 쓰면 다 되는데, 굳이 TCP까지 알아야 하나요? 🤔" 라고 생각하실 수도 있지만, TCP를 이해하면 네트워크가 실제로 어떻게 데이터를 주고받는지, 왜 때로는 연결이 느리거나 끊기는지 등 근본적인 원리를 파악하는 데 큰 도움이 됩니다. 또한, 실시간 게임이나 채팅처럼 HTTP 외의 프로토콜이 필요한 경우 TCP/IP 소켓 프로그래밍 지식은 강력한 무기가 될 수 있죠! ⚔️

 

Retrofit이 고급 레스토랑의 잘 차려진 코스 요리라면, TCP는 그 요리를 만드는 셰프의 칼솜씨와 불 조절 같은 기본기랄까요? 🔥 이 기본기를 이해하면 네트워크 문제를 더 깊이 있게 진단하고, 특별한 통신 요구사항에도 유연하게 대처할 수 있게 됩니다!

오늘 우리가 함께 탐험할 TCP 프로그래밍의 세계는 다음과 같아요:

  1. 믿음직한 배송원 TCP vs. 날쌘돌이 UDP (어떤 연결을 선택할까?) 🚚💨
  2. 데이터의 흐름, 자바 I/O 스트림 (InputStream & OutputStream) 💧
  3. 실전! 자바 TCP 소켓 프로그래밍 (서버와 클라이언트 만들기) 💻↔️🖥️

자, 이제 네트워크의 좀 더 깊은 심연으로 함께 다이빙할 준비, 되셨나요? 퐁당! 🌊

 

 

1. 믿음직한 배송원 TCP vs. 날쌘돌이 UDP (어떤 연결을 선택할까?) 🚚💨

네트워크 세상에는 데이터를 보내는 두 가지 주요 방식, TCP와 UDP가 있습니다. 마치 택배를 보낼 때 '등기우편'으로 보낼지 '일반우편'으로 보낼지 선택하는 것과 비슷해요!

  • TCP (Transmission Control Protocol): 연결 지향 프로토콜
    • 특징: 데이터를 보내기 전에 상대방과 먼저 "똑똑, 데이터 보내도 될까요?" 하고 연결(Connection)을 수립합니다. 이를 3-way handshake (SYN -> SYN-ACK -> ACK)라고 불러요. 마치 전화를 걸어 상대방이 받으면 통화를 시작하는 것과 같죠. 📞
    • 장점:
      • 신뢰성: 보낸 데이터가 순서대로, 빠짐없이 잘 도착했는지 확인합니다. 문제가 생기면 재전송 요청도 하고요. (믿음직한 배송원!)
      • 흐름 제어: 받는 쪽이 처리할 수 있는 속도에 맞춰 데이터 전송량을 조절합니다.
      • 혼잡 제어: 네트워크가 혼잡할 때 전송량을 줄여 문제를 피합니다.
    • 단점: 연결 설정과 각종 확인 절차 때문에 UDP보다 상대적으로 느리고, 오버헤드가 있습니다.
    • 주요 사용처: 웹 브라우징(HTTP/HTTPS), 이메일(SMTP), 파일 전송(FTP) 등 신뢰성이 중요한 서비스.
  • UDP (User Datagram Protocol): 비연결 지향 프로토콜
    • 특징: TCP처럼 연결을 맺는 과정 없이 그냥 데이터를 냅다 던집니다! "에라 모르겠다, 일단 보내고 보자!" 스타일이죠. 휙! 💨
    • 장점:
      • 속도: 연결 과정과 확인 절차가 없으니 매우 빠릅니다. (날쌘돌이 퀵서비스!)
      • 낮은 오버헤드: TCP보다 구조가 단순합니다.
    • 단점:
      • 비신뢰성: 데이터가 중간에 사라지거나, 순서가 뒤바뀌거나, 중복되어 도착해도 UDP는 신경 쓰지 않아요. (일반우편처럼 분실 위험!)
      • 흐름 제어나 혼잡 제어 기능이 없습니다.
    • 주요 사용처: 실시간 스트리밍(동영상, 음성), 온라인 게임(일부 데이터), DNS 등 속도가 매우 중요하고 약간의 데이터 손실은 감수할 수 있는 서비스.

우리가 오늘 다룰 TCP 소켓 프로그래밍은 바로 이 신뢰성 있는 TCP를 기반으로 통신하는 방법을 배우는 것이랍니다! 마치 중요한 계약서를 보낼 때 등기우편을 이용하는 것처럼요. ✉️✅

 

2. 데이터의 흐름, 자바 I/O 스트림 (InputStream & OutputStream) 💧

TCP 연결이 수립되면, 이제 그 통로를 통해 데이터를 주고받아야겠죠? 이때 사용되는 것이 바로 자바의 입출력 스트림(I/O Stream) 입니다. 지난번 배운 함수형 프로그래밍의 스트림(Stream API)과는 다른 친구예요! Java I/O 스트림은 파일이나 네트워크 연결 같은 외부 장치로부터 데이터를 읽거나 쓰는 단방향 통로라고 생각하면 쉽습니다.

  • InputStream: 데이터를 읽어오는 빨대 ρου
    • 네트워크 연결, 파일 등으로부터 바이트(byte) 단위로 데이터를 읽어들입니다.
    • 대표적인 메서드: read()
    • 예: Socket.getInputStream()을 통해 소켓으로부터 들어오는 데이터를 읽을 수 있습니다.
  • OutputStream: 데이터를 내보내는 호스 📤
    • 네트워크 연결, 파일 등으로 바이트(byte) 단위로 데이터를 씁니다.
    • 대표적인 메서드: write()
    • 예: Socket.getOutputStream()을 통해 소켓으로 데이터를 보낼 수 있습니다.

이 스트림들은 기본적으로 바이트 단위로 데이터를 처리하기 때문에, 우리가 흔히 사용하는 문자열(String)이나 객체(Object)를 주고받으려면 약간의 가공이 필요해요. 예를 들어, 바이트 스트림을 문자 스트림으로 변환해주는 InputStreamReaderOutputStreamWriter, 그리고 버퍼링을 통해 성능을 향상시키는 BufferedReaderBufferedWriter (또는 편리한 PrintWriter) 등을 함께 사용하곤 합니다.

 

마치 수도꼭지(OutputStream)에서 물을 틀고, 컵(InputStream)으로 받는 모습과 비슷하죠? 🚰🥛 이 스트림을 통해 데이터가 서버와 클라이언트 사이를 오고 가게 됩니다!

 

3. 실전! 자바 TCP 소켓 프로그래밍 (서버와 클라이언트 만들기) 💻↔️🖥️

자, 드디어 TCP/IP 소켓 프로그래밍의 핵심입니다! 소켓(Socket)은 네트워크 통신의 양 끝단을 의미해요. 전화기로 치면 각자의 전화기라고 할 수 있죠. ☎️ TCP 소켓 프로그래밍은 크게 '서버 소켓'과 '클라이언트 소켓'으로 나뉩니다.

 

서버(Server) 측

  1. ServerSocket 생성: "손님을 기다리는 식당 문 🚪"
    • 서버는 특정 포트 번호(예: 8080번)를 열고 클라이언트의 연결 요청을 기다립니다.
    • ServerSocket serverSocket = new ServerSocket(8080);
  2. accept() 호출: "손님 입장! 👋"
    • 클라이언트가 연결을 요청하면, serverSocket.accept() 메서드는 이를 수락하고, 실제 클라이언트와 통신할 새로운 Socket 객체를 반환합니다. 이 Socket 객체가 바로 클라이언트와의 1:1 통신 채널이 됩니다.
    • Socket clientSocket = serverSocket.accept();
    • 주의! accept()는 클라이언트 연결이 들어올 때까지 프로그램 실행을 멈추고 기다립니다(블로킹 방식).
  3. I/O 스트림 얻기:
    • clientSocket으로부터 InputStreamOutputStream을 얻어 데이터를 주고받을 준비를 합니다.
    • InputStream in = clientSocket.getInputStream();
    • OutputStream out = clientSocket.getOutputStream();
    • (보통 BufferedReaderPrintWriter 등으로 감싸서 사용합니다.)
  4. 데이터 송수신:
    • 스트림을 통해 클라이언트와 데이터를 주고받습니다 (예: 클라이언트의 요청을 읽고, 응답을 보냅니다).
  5. SocketServerSocket 닫기:
    • 통신이 끝나면 사용한 리소스를 반드시 닫아줘야 합니다. (clientSocket.close(), serverSocket.close())
    • try-with-resources 구문을 사용하면 편리하게 자동 자원 해제가 가능합니다.

클라이언트(Client) 측

  1. Socket 생성: "식당에 전화 걸기/방문하기 📞"
    • 클라이언트는 서버의 IP 주소와 포트 번호를 알고 있어야 하며, 이를 이용해 서버에 연결을 시도합니다.
    • Socket socket = new Socket("서버_IP_주소", 8080);
    • 이 과정에서 TCP 3-way handshake가 일어납니다.
  2. I/O 스트림 얻기:
    • 마찬가지로 socket으로부터 InputStreamOutputStream을 얻습니다.
    • InputStream in = socket.getInputStream();
    • OutputStream out = socket.getOutputStream();
  3. 데이터 송수신:
    • 스트림을 통해 서버와 데이터를 주고받습니다 (예: 서버에 요청을 보내고, 응답을 받습니다).
  4. Socket 닫기:
    • 통신이 끝나면 socket.close()로 연결을 종료합니다.

 

간단한 예시 (개념 코드):

// 서버 측 (간단화된 예시)
try (ServerSocket serverSocket = new ServerSocket(12345)) {
    System.out.println("서버: 클라이언트 연결 대기 중...");
    try (Socket clientSocket = serverSocket.accept(); // 클라이언트 접속 대기
         PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
         BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {

        System.out.println("서버: 클라이언트 연결됨!");
        out.println("서버에 오신 것을 환영합니다!"); // 클라이언트로 메시지 전송

        String clientMessage = in.readLine(); // 클라이언트 메시지 수신
        System.out.println("서버: 클라이언트로부터 받은 메시지 - " + clientMessage);
    }
} catch (IOException e) {
    e.printStackTrace();
}

// 클라이언트 측 (간단화된 예시)
try (Socket socket = new Socket("localhost", 12345); // 서버에 연결 시도
     PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
     BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {

    String serverMessage = in.readLine(); // 서버 메시지 수신
    System.out.println("클라이언트: 서버로부터 받은 메시지 - " + serverMessage);

    out.println("안녕하세요, 서버님!"); // 서버로 메시지 전송
    System.out.println("클라이언트: 서버로 메시지 전송 완료.");

} catch (IOException e) {
    e.printStackTrace();
}

 

(실제 운영 환경에서는 서버가 여러 클라이언트를 동시에 처리하기 위해 스레드를 사용하는 등의 추가적인 구현이 필요합니다! Thread per client, Thread Pool 등)

이렇게 서버와 클라이언트가 소켓과 스트림을 통해 서로 데이터를 주고받는 것이 TCP 소켓 프로그래밍의 기본이랍니다! 마치 보이지 않는 파이프라인을 통해 메시지를 주고받는 것 같지 않나요? 📨

정리하며 📝

오늘은 네트워크 통신의 근간이 되는 TCP 프로그래밍의 세계를 탐험해 보았습니다! 🌍

  • TCP vs. UDP: 신뢰성이 중요한 통신에는 TCP (연결형, 순서보장, 꼼꼼이 배송원 📦), 속도가 생명이고 약간의 손실은 괜찮다면 UDP (비연결형, 날쌘돌이 퀵서비스 🚀)를 사용합니다.
  • Java I/O 스트림: InputStream은 데이터를 읽어오는 통로(빨대 ρου), OutputStream은 데이터를 내보내는 통로(호스 📤)입니다. 이들을 통해 바이트 단위의 데이터가 오고 갑니다.
  • TCP 소켓 프로그래밍:
    • 서버: ServerSocket으로 클라이언트의 접속을 기다리고(accept()), 연결되면 Socket을 통해 통신합니다. 마치 문을 열고 손님을 맞는 가게 주인 같아요! 🧑‍🍳
    • 클라이언트: Socket으로 서버에 접속을 요청하고, 연결되면 통신을 시작합니다. 가게에 찾아가는 손님과 같죠! 🚶‍♀️
  • 핵심 클래스: ServerSocket, Socket, InputStream, OutputStream (그리고 이들을 보조하는 BufferedReader, PrintWriter 등)

Retrofit이 편리한 통역사였다면, TCP 소켓 프로그래밍은 직접 외국어를 구사하는 것과 같아요. 더 많은 제어권을 갖지만, 더 많은 디테일을 신경 써야 하죠! 🗣️ 하지만 이 원리를 이해하면 Retrofit과 같은 고수준 라이브러리들이 내부적으로 어떻게 동작하는지 더 잘 이해할 수 있고, 다양한 네트워크 환경에 더욱 능동적으로 대처할 수 있는 힘을 기를 수 있습니다!

 

 

이 지식을 바탕으로 여러분의 앱이 더욱 견고하고 안정적으로 세상과 소통할 수 있기를 바랍니다!

오늘 모두 정말 수고 많으셨습니다! 다음 시간에도 더 흥미로운 주제로 만나요! 👋

https://abit.ly/lisbva