8강 구현 중심 ⏱ 약 35분

 

0. 학습 목표

→ 이번 글에서 무엇을 이해하고, 무엇을 만들고, 무엇을 확인할지 먼저 정리합니다.

더보기

0.1 이번 글에서 다룰 내용

이번 글은 구현 중심 강의입니다.

 

지금까지는 클라이언트가 메시지를 보내면 서버가 자동으로 응답했습니다.

이번 강의에서는 서버와 클라이언트가 터미널에서 다음처럼 대화하는 구조를 만듭니다.

클라이언트: 안녕하세요
서버: 안녕하세요, 반갑습니다
클라이언트: 오늘 테스트 잘 되나요?
서버: 네, 잘 됩니다
클라이언트: exit
채팅 종료

 

이번 강의의 핵심은 세 가지입니다.

1. 클라이언트가 메시지를 입력해서 서버로 보낸다.
2. 서버가 받은 메시지를 출력하고 직접 답장을 입력한다.
3. 둘 중 한쪽이 exit을 입력하면 채팅을 종료한다.

 

이번 강의에서 완성하는 1:1 터미널 채팅은 Part 1~2의 최종 산출물입니다. 이후 9강부터는 이 구조를 기반으로 여러 클라이언트가 동시에 접속하는 다중 접속 서버, 스레드 기반 실시간 채팅, GUI 채팅으로 단계적으로 확장합니다.

 

다만 이번 채팅은 아직 완전한 실시간 채팅은 아닙니다. 클라이언트가 먼저 말하고 서버가 답하는 번갈아 말하기 구조입니다. 이 구조는 1:1 소켓 통신의 send()recv() 순서를 직접 경험하는 가장 작은 완성형 채팅 프로그램입니다.

구분 내용
이해할 것 1:1 채팅은 양쪽이 번갈아 보내고 받는 구조라는 것
만들 것 터미널에서 대화할 수 있는 server.py, client.py
확인할 것 서버와 클라이언트가 서로 직접 입력한 메시지를 주고받는지

 

0.2 이번 강의에서 직접 다루는 구조

이번 강의에서는 7강에서 만든 두 파일을 모두 수정합니다.

chat_server/
└── server.py      ← 이번 강의에서 수정

chat_client/
└── client.py      ← 이번 강의에서 수정
파일 이번 강의 이후 역할
chat_server/server.py 클라이언트 메시지를 받고, 서버 사용자가 직접 답장을 입력해 보낸다
chat_client/client.py 클라이언트 사용자가 메시지를 입력해 보내고, 서버 답장을 받는다

대화 순서는 정해져 있습니다. 클라이언트가 먼저 말하고, 서버가 듣고 답합니다.

 

0.3 📦 Part 1 ~ 2 구현

part1_2_final.zip
├── README.md                  ← 실행 방법 + 핵심 개념 정리
├── chat_server/
│   └── server.py              ← 8강 최종 서버 (1:1 터미널 채팅)
└── chat_client/
    └── client.py              ← 8강 최종 클라이언트
part1_2_final.zip
0.00MB

 

1. 실습 준비하기

→ 7강 코드의 한계를 확인하고 이번 강의의 대화 순서를 정합니다.

더보기

1.1 이전 7강 코드와 이번 강의 구조 비교

7강에서 서버의 응답은 자동으로 만들어졌습니다. 서버 사용자가 직접 답장을 입력하지 않았습니다.

response = f"서버 응답: {message}"  # 7강: 자동 응답
구분 7강 구조 이번 강의 구조
클라이언트 직접 입력 직접 입력
서버 자동 응답 직접 입력
대화 방식 요청과 응답 1:1 대화
종료 방식 클라이언트 중심 exit 양쪽 모두 exit 가능

 

1.2 대화 순서 정하기

이번 강의에서는 가장 단순한 1:1 채팅 구조를 사용합니다. 클라이언트가 먼저 보내면 서버가 받고 답장하는 방식으로, 동시에 입력하지 않습니다.

① 클라이언트가 먼저 보낸다
        ↓
② 서버가 받는다
        ↓
③ 서버가 답장을 입력해서 보낸다
        ↓
④ 클라이언트가 받는다
        ↓
⑤ 이 과정을 반복한다 (while True)

이번 강의의 목표는 완전한 실시간 채팅이 아니라, 서버와 클라이언트가 send()recv() 순서를 맞춰 대화하는 기본 흐름을 익히는 것입니다.

✔ 확인 기준: 이번 강의의 채팅이 번갈아 말하기 구조라는 것을 설명할 수 있으면 완료입니다. 양쪽이 동시에 input()하거나 동시에 recv()하는 구조로 이해하고 있지 않은지 확인하세요.

💡 강사 팁 — 한계를 글이 아니라 체험으로

설명을 읽는 것보다 직접 막히는 것이 훨씬 강하게 남습니다. 채팅 중에 클라이언트 터미널에서 메시지를 연속으로 두 줄 입력해 보도록 유도해 보세요.

클라이언트: 첫 번째 메시지
클라이언트: ← 두 번째 메시지를 입력하려는데 프롬프트가 나타나지 않는다

서버가 아직 recv()로 기다리고 있기 때문에 클라이언트는 두 번째 입력을 할 수 없습니다. 이 막히는 순간이 "왜 스레드가 필요한가"를 백 마디 설명보다 명확하게 보여줍니다. 9강에서 이 문제를 스레드로 해결합니다.

 

2. 서버 - 대화 구현

→ 서버에 input()을 추가하고 양쪽 모두 exit을 처리하도록 수정합니다.

더보기

chat_server/server.py를 다음과 같이 수정합니다. 자동 응답 대신 input("서버: ")으로 직접 답장을 입력하고, 서버도 exit을 입력해 종료할 수 있도록 추가합니다.

import socket

HOST = "127.0.0.1"
PORT = 5000

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
    server_socket.bind((HOST, PORT))
    server_socket.listen()

    print("서버를 시작합니다.")
    print("클라이언트 접속 대기 중...")

    client_socket, client_address = server_socket.accept()

    with client_socket:
        print(f"클라이언트가 접속했습니다: {client_address}")
        print("채팅을 시작합니다. 종료하려면 exit을 입력하세요.")

        while True:
            data = client_socket.recv(1024)

            # 클라이언트가 갑자기 연결을 끊은 경우 처리
            if not data:
                print("클라이언트 연결이 종료되었습니다.")
                break

            client_message = data.decode("utf-8")

            # 클라이언트가 exit을 보낸 경우 종료
            if client_message == "exit":
                print("클라이언트가 채팅을 종료했습니다.")
                break

            print(f"클라이언트: {client_message}")

            # 서버 사용자가 직접 답장 입력
            server_message = input("서버: ")
            client_socket.send(server_message.encode("utf-8"))

            # 서버도 exit을 입력해 종료 가능
            if server_message == "exit":
                print("서버가 채팅을 종료했습니다.")
                break

print("서버를 종료합니다.")

서버 코드의 핵심 흐름은 다음과 같습니다.

클라이언트 메시지 받기
        ↓
받은 메시지가 exit인지 확인
        ↓
받은 메시지 출력
        ↓
서버 사용자가 답장 입력
        ↓
답장 전송
        ↓
서버 답장이 exit인지 확인

중요한 점은 서버도 exit을 클라이언트에게 먼저 보낸 뒤 종료한다는 것입니다. 그래야 클라이언트가 서버의 종료 요청을 받을 수 있습니다.

 

3. 클라이언트 - 대화 구현

→ 서버에 input()을 추가하고 양쪽 모두 exit을 처리하도록 수정합니다.

더보기

2.2 클라이언트 코드 수정하기

chat_client/client.py를 다음과 같이 수정합니다. 서버의 exit 메시지도 처리하고, if not data: 안전 처리를 추가합니다.

import socket

HOST = "127.0.0.1"
PORT = 5000

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket:
    client_socket.connect((HOST, PORT))

    print("서버에 연결되었습니다.")
    print("채팅을 시작합니다. 종료하려면 exit을 입력하세요.")

    while True:
        client_message = input("클라이언트: ")

        # exit도 서버에 먼저 보내고 나서 종료
        client_socket.send(client_message.encode("utf-8"))

        if client_message == "exit":
            print("클라이언트가 채팅을 종료했습니다.")
            break

        data = client_socket.recv(1024)

        # 서버가 갑자기 연결을 끊은 경우 처리
        if not data:
            print("서버 연결이 종료되었습니다.")
            break

        server_message = data.decode("utf-8")

        # 서버가 exit을 보낸 경우 종료
        if server_message == "exit":
            print("서버가 채팅을 종료했습니다.")
            break

        print(f"서버: {server_message}")

print("클라이언트 소켓을 닫았습니다.")

클라이언트도 마찬가지로 exit을 서버에 먼저 보내고 종료합니다.

# 올바른 순서
client_socket.send(client_message.encode("utf-8"))   # 먼저 보내고
if client_message == "exit":
    break                                             # 그다음 종료

# 잘못된 순서 — 서버가 exit을 받지 못함
if client_message == "exit":
    break                                             # 보내기 전에 종료
client_socket.send(client_message.encode("utf-8"))

✔ 확인 기준: 서버와 클라이언트 코드 모두 input(), send(), recv(), exit 처리가 들어 있으면 완료입니다. 서버가 여전히 자동 응답을 보내고 있거나, 받은 exit을 처리하지 않는지 확인하세요.

 

3. 실행 결과 확인하기

→ 서버를 먼저 실행하고 두 터미널에서 번갈아 메시지를 입력해 대화를 확인합니다.

더보기

3.1 실행 확인하기

 

터미널 1에서 서버를 먼저 실행합니다.

python chat_server/server.py
서버를 시작합니다.
클라이언트 접속 대기 중...

터미널 2에서 클라이언트를 실행하고 메시지를 입력합니다.

python chat_client/client.py
서버에 연결되었습니다.
채팅을 시작합니다. 종료하려면 exit을 입력하세요.
클라이언트: 안녕하세요
서버: 반갑습니다
클라이언트: 오늘 테스트 잘 되나요?
서버: 네, 잘 됩니다
클라이언트: exit
클라이언트가 채팅을 종료했습니다.
클라이언트 소켓을 닫았습니다.

클라이언트 실행 중 서버 터미널에는 다음이 출력됩니다. 서버: 뒤는 서버 사용자가 직접 입력한 내용입니다.

클라이언트가 접속했습니다: ('127.0.0.1', 52344)
채팅을 시작합니다. 종료하려면 exit을 입력하세요.
클라이언트: 안녕하세요
서버: 반갑습니다          ← input("서버: ") 프롬프트 + 직접 입력
클라이언트: 오늘 테스트 잘 되나요?
서버: 네, 잘 됩니다       ← input("서버: ") 프롬프트 + 직접 입력
클라이언트가 채팅을 종료했습니다.
서버를 종료합니다.

포트 번호 52344는 실행할 때마다 달라질 수 있습니다. 이번 예시는 클라이언트가 exit을 입력해 종료한 경우입니다. 서버가 exit을 입력하는 경우에는 클라이언트 쪽에 서버가 채팅을 종료했습니다.가 출력됩니다.

✔ 확인 기준: 양쪽 터미널에서 서로의 메시지가 보이고, exit 입력 시 양쪽이 모두 종료되면 완료입니다. 서버를 먼저 실행했는지, 포트 번호가 양쪽 모두 5000인지, send()recv() 순서가 맞는지 확인하세요.

 

3.2 서버와 클라이언트 순서 맞추기

순서가 어긋나면 양쪽이 모두 recv()에서 기다리거나, 한쪽은 입력을 기다리고 다른 쪽은 응답을 기다리는 상황이 생길 수 있습니다.

순서 클라이언트 서버
1 input() recv() 대기
2 send() 메시지 수신·출력
3 recv() 대기 input()
4 답장 수신·출력 send()

 

3.3 exit 처리 순서 확인하기

채팅 종료는 양쪽 모두에서 가능합니다. 어느 쪽이 exit을 입력하든 공통 규칙은 같습니다. 내가 exit을 입력해도 먼저 상대에게 보내고 종료합니다.

 

클라이언트가 exit 입력 시

클라이언트 send("exit") → 클라이언트 break → 서버 recv("exit") → 서버 break

서버가 exit 입력 시

서버 send("exit") → 서버 break → 클라이언트 recv("exit") → 클라이언트 break

✔ 확인 기준: 어느 쪽이 exit을 입력하든 먼저 상대에게 보내고 종료한다는 것을 알면 완료입니다. exit을 입력하자마자 send()하지 않고 바로 break하도록 작성하지 않았는지 확인하세요.

 

3.4 흔한 오류와 해결 방법

오류 상황 원인 해결 방법
번갈아 말하기 중 양쪽이 멈춤 클라이언트와 서버 모두 recv()에서 기다리는 순서 오류 클라이언트는 먼저 send(), 서버는 먼저 recv() 구조인지 확인한다
exit을 입력했는데 한쪽만 종료됨 exit을 보내지 않고 바로 break하거나, 받은 exit을 처리하지 않음 send()  break하고, 받은 메시지가 exit인지 확인하는 코드를 양쪽에 넣는다
서버가 exit 입력 시 클라이언트가 계속 입력을 기다림 클라이언트 코드에 서버의 exit 수신 처리가 없음 클라이언트 recv()  if server_message == "exit": break 조건이 있는지 확인한다

이번 강의의 채팅은 번갈아 말하기 구조이므로 다음과 같은 제한이 있습니다. 이는 오류가 아니라 스레드를 사용하지 않아서 생기는 자연스러운 결과입니다.

- 클라이언트가 연속으로 여러 메시지를 보낼 수 없다
- 서버도 자기 차례가 되어야 메시지를 보낼 수 있다
- 한쪽이 입력하지 않으면 다른 쪽은 기다린다
- 여러 사용자가 동시에 접속할 수 없다

 

 

4. 최종 코드 정리하기

→ 이번 강의에서 수정한 두 파일의 최종 코드를 확인합니다.

더보기

4.1 chat_server/server.py

import socket

HOST = "127.0.0.1"
PORT = 5000

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
    server_socket.bind((HOST, PORT))
    server_socket.listen()

    print("서버를 시작합니다.")
    print("클라이언트 접속 대기 중...")

    client_socket, client_address = server_socket.accept()

    with client_socket:
        print(f"클라이언트가 접속했습니다: {client_address}")
        print("채팅을 시작합니다. 종료하려면 exit을 입력하세요.")

        while True:
            data = client_socket.recv(1024)

            # 클라이언트가 갑자기 연결을 끊은 경우 처리
            if not data:
                print("클라이언트 연결이 종료되었습니다.")
                break

            client_message = data.decode("utf-8")

            # 클라이언트가 exit을 보낸 경우 종료
            if client_message == "exit":
                print("클라이언트가 채팅을 종료했습니다.")
                break

            print(f"클라이언트: {client_message}")

            # 서버 사용자가 직접 답장 입력
            server_message = input("서버: ")
            client_socket.send(server_message.encode("utf-8"))

            # 서버도 exit을 입력해 종료 가능
            if server_message == "exit":
                print("서버가 채팅을 종료했습니다.")
                break

print("서버를 종료합니다.")

 

4.2 chat_client/client.py

import socket

HOST = "127.0.0.1"
PORT = 5000

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket:
    client_socket.connect((HOST, PORT))

    print("서버에 연결되었습니다.")
    print("채팅을 시작합니다. 종료하려면 exit을 입력하세요.")

    while True:
        client_message = input("클라이언트: ")

        # exit도 서버에 먼저 보내고 나서 종료
        client_socket.send(client_message.encode("utf-8"))

        if client_message == "exit":
            print("클라이언트가 채팅을 종료했습니다.")
            break

        data = client_socket.recv(1024)

        # 서버가 갑자기 연결을 끊은 경우 처리
        if not data:
            print("서버 연결이 종료되었습니다.")
            break

        server_message = data.decode("utf-8")

        # 서버가 exit을 보낸 경우 종료
        if server_message == "exit":
            print("서버가 채팅을 종료했습니다.")
            break

        print(f"서버: {server_message}")

print("클라이언트 소켓을 닫았습니다.")

 

4.3 최종 확인 표

확인할 코드 위치 의미
input("클라이언트: ") 클라이언트 클라이언트 사용자가 메시지를 입력한다
input("서버: ") 서버 서버 사용자가 직접 답장을 입력한다
send(...encode("utf-8")) 서버, 클라이언트 입력한 메시지를 바이트로 바꿔 전송한다
recv(1024) 서버, 클라이언트 상대방 메시지를 받는다
if message == "exit" 계열 서버, 클라이언트 받은 종료 요청 또는 내가 입력한 종료 요청을 처리한다
if not data: 서버, 클라이언트 상대방이 갑자기 연결을 끊은 경우를 처리한다

→ 다음 강의 (9강): 한 명만 접속할 수 있는 이번 서버의 한계를 해결하기 위해, 여러 클라이언트가 동시에 접속하는 다중 접속 구조가 왜 필요한지 개념적으로 정리합니다.

 

5. 추가 과제로 직접 확장하기

→ 지금까지 만든 1대1 통신 서버를 두 방향으로 확장해 보며 recv/send 반복과 문자열 약속(프로토콜)을 직접 다룹니다.

더보기

지금까지 만든 서버는 메시지를 한 번 받고 한 번 답하면 끝이었습니다. 이번 추가 과제에서는 그 구조를 두 방향으로 넓혀 봅니다. 과제 1은 여러 번 주고받는 채팅으로, 과제 2는 명령어를 약속해 계산 결과를 돌려주는 서버로 확장합니다.

두 과제 모두 막히면 아래 완성 코드와 비교하면 됩니다. 먼저 직접 작성해 보고, 기대 출력이 나오지 않을 때 정답 코드를 펼쳐 보는 순서를 권장합니다.

과제 핵심 학습 포인트 만들 파일
과제 1 닉네임을 메시지에 붙여 보내기, while 반복으로 여러 번 대화, exit 종료 task1_server.py
task1_client.py
과제 2 문자열을 split()으로 분해, int() 형변환, 잘못된 입력 방어 task2_server.py
task2_client.py

5.1 과제 1 — 닉네임 기반 1대1 터미널 채팅

지금 채팅 프로그램은 메시지를 받아도 누가 보냈는지 알 수 없습니다. 클라이언트가 접속하면 닉네임을 입력받고, 이후 보내는 모든 메시지 앞에 닉네임이 붙도록 바꿉니다. 서버 화면에는 홍길동: 안녕하세요처럼 닉네임과 함께 찍히게 만드는 것이 목표입니다.

요구사항은 다음과 같습니다.

1. 클라이언트 접속 후 닉네임 입력
2. 메시지를 "닉네임: 내용" 형식으로 서버에 전송
3. 서버 화면에 수신 메시지 출력
4. 서버에서 답장 입력 후 전송
5. 클라이언트에서 exit 입력 시 채팅 종료
6. 서버에서 exit 입력 시 채팅 종료
7. 종료 시 소켓 정상 종료

가장 중요한 설계 결정은 닉네임을 어떻게 서버에 알려줄 것인가입니다. 이 과제에서는 매 메시지를 보낼 때 닉네임: 내용 형태로 합쳐서 보내는 방식을 씁니다. 그러면 서버는 받은 문자열을 그대로 출력하기만 하면 되고, 4강 서버에서 바뀌는 부분이 가장 적습니다.

클라이언트 코드 — task1_client.py

import socket

HOST = "127.0.0.1"
PORT = 5001

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect((HOST, PORT))                 # 서버에 연결한다
print("서버에 성공적으로 연결되었습니다.")

nickname = input("닉네임 입력: ")                    # 접속 후 닉네임을 먼저 받는다

while True:                                          # 여러 번 대화하기 위해 반복한다
    text = input(f"[{nickname}] 보낼 메시지: ")       # 프롬프트에 닉네임을 보여준다

    if text == "exit":                               # exit이면 종료 신호를 보내고 끝낸다
        client_socket.send(f"{nickname}|exit".encode("utf-8"))
        print("채팅을 종료합니다.")
        break

    full_message = f"{nickname}: {text}"             # 닉네임과 내용을 한 문자열로 합친다
    client_socket.send(full_message.encode("utf-8")) # 합친 메시지를 서버로 보낸다

    data = client_socket.recv(1024)                  # 서버 답장을 받는다
    reply = data.decode("utf-8")

    if reply == "exit":                              # 서버가 종료하면 함께 끝낸다
        print("서버가 채팅을 종료했습니다.")
        break

    print(f"서버: {reply}")

client_socket.close()                                # 소켓을 닫는다

서버 코드 — task1_server.py

import socket

HOST = "127.0.0.1"
PORT = 5001

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
    server_socket.bind((HOST, PORT))                 # 주소에 묶는다
    server_socket.listen()                           # 접속 대기 상태로 만든다
    print("과제 1: 닉네임 채팅 서버가 시작되었습니다. 클라이언트를 기다립니다...")

    client_socket, client_address = server_socket.accept()   # 접속을 받는다
    print(f"클라이언트가 연결되었습니다! 주소: {client_address}")

    while True:                                      # 여러 번 주고받기 위해 반복한다
        data = client_socket.recv(1024)              # 클라이언트 메시지를 받는다
        if not data:                                 # 연결이 끊기면 빈 데이터가 온다
            break
        message = data.decode("utf-8")

        if message.endswith("exit"):                 # "닉네임|exit"이면 종료 요청이다
            print("클라이언트가 종료 요청을 보냈습니다.")
            break

        print(message)                               # "홍길동: 안녕하세요" 그대로 출력

        reply = input("서버: ")                       # 답장을 입력받는다
        client_socket.send(reply.encode("utf-8"))    # 답장을 보낸다

        if reply == "exit":                          # 서버가 exit이면 종료한다
            print("서버에서 종료했습니다.")
            break

except KeyboardInterrupt:
    print("\n서버를 직접 종료했습니다.")

finally:
    client_socket.close()                            # 클라이언트 소켓을 닫는다
    server_socket.close()                            # 서버 소켓을 닫는다
    print("소켓이 안전하게 닫혔습니다.")

실행은 터미널 두 개를 엽니다. 먼저 서버를 켜고, 그다음 클라이언트를 켭니다.

python task1_server.py
python task1_client.py

기대하는 클라이언트 터미널 화면입니다.

서버에 성공적으로 연결되었습니다.
닉네임 입력: 홍길동
[홍길동] 보낼 메시지: 안녕하세요
서버: 반갑습니다.
[홍길동] 보낼 메시지: 오늘 테스트 되나요?
서버: 네, 정상입니다.
[홍길동] 보낼 메시지: exit
채팅을 종료합니다.

기대하는 서버 터미널 화면입니다.

과제 1: 닉네임 채팅 서버가 시작되었습니다. 클라이언트를 기다립니다...
클라이언트가 연결되었습니다! 주소: ('127.0.0.1', 53210)
홍길동: 안녕하세요
서버: 반갑습니다.
홍길동: 오늘 테스트 되나요?
서버: 네, 정상입니다.
클라이언트가 종료 요청을 보냈습니다.
소켓이 안전하게 닫혔습니다.
채점 체크리스트 통과 기준
닉네임 입력 접속 직후 닉네임을 한 번 입력받는다
메시지 형식 서버 화면에 닉네임: 내용으로 출력된다
반복 대화 메시지를 두 번 이상 주고받을 수 있다
양방향 종료 클라이언트·서버 어느 쪽이 exit을 입력해도 끝난다
소켓 종료 종료 시 "소켓이 안전하게 닫혔습니다." 출력

 

5.2 과제 2 — 명령어 기반 원격 계산기

클라이언트가 ADD 15 30처럼 명령어를 입력하면, 서버가 계산한 뒤 결과를 돌려주는 프로그램을 만듭니다. 클라이언트와 서버는 약속된 형식의 문자열로만 대화합니다. 이렇게 미리 정한 통신 규칙을 프로토콜이라고 부릅니다.

요구사항은 다음과 같습니다.

1. 서버 포트 번호 5002 사용
2. 클라이언트에서 "명령어 숫자1 숫자2" 형식으로 입력
3. 서버에서 문자열을 공백 기준으로 분리
4. ADD, SUB, MUL, DIV 네 가지 연산 지원
5. 잘못된 입력은 ERROR 메시지 반환
6. 클라이언트에서 QUIT 입력 시 종료

서버가 받은 문자열을 처리하는 핵심은 다음 세 줄입니다. 문자열을 쪼개고, 명령어를 대문자로 통일하고, 숫자를 정수로 바꿉니다.

parts = message.split()        # "ADD 15 30" → ['ADD', '15', '30'] 리스트로 분리
command = parts[0].upper()     # 'add', 'Add' 모두 'ADD'로 통일
num1 = int(parts[1])           # 문자열 '15'를 정수 15로 형변환
num2 = int(parts[2])           # 문자열 '30'을 정수 30으로 형변환

클라이언트 코드 — task2_client.py

import socket

HOST = "127.0.0.1"
PORT = 5002

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect((HOST, PORT))                 # 서버에 연결한다
print("서버에 연결되었습니다.")

while True:                                          # 여러 번 계산하기 위해 반복한다
    command = input("명령어와 숫자를 입력하세요 (예: ADD 5 3): ")

    if command.upper() == "QUIT":                    # QUIT이면 종료한다
        client_socket.send("QUIT".encode("utf-8"))
        print("계산기 프로그램을 종료합니다.")
        break

    client_socket.send(command.encode("utf-8"))      # 명령어 문자열을 그대로 보낸다

    data = client_socket.recv(1024)                  # 서버의 계산 결과를 받는다
    response = data.decode("utf-8")
    print(f"[서버 응답]: {response}")

client_socket.close()                                # 소켓을 닫는다

서버 코드 — task2_server.py

import socket

HOST = "127.0.0.1"
PORT = 5002

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
    server_socket.bind((HOST, PORT))
    server_socket.listen()
    print("과제 2: 원격 계산기 서버가 시작되었습니다. 클라이언트를 기다립니다...")

    client_socket, client_address = server_socket.accept()
    print(f"클라이언트가 연결되었습니다! 주소: {client_address}")

    while True:
        data = client_socket.recv(1024)
        if not data:
            break
        message = data.decode("utf-8")

        if message.upper() == "QUIT":                # QUIT이면 종료한다
            print("클라이언트가 종료 요청을 보냈습니다.")
            break

        print(f"받은 명령어: {message}")

        parts = message.split()                      # 공백 기준으로 분리한다
        if len(parts) != 3:                          # 토막이 3개가 아니면 잘못된 입력
            client_socket.send("ERROR: Invalid Command".encode("utf-8"))
            continue

        command = parts[0].upper()                   # 명령어를 대문자로 통일한다
        try:
            num1 = int(parts[1])                     # 숫자가 아니면 ValueError가 난다
            num2 = int(parts[2])
        except ValueError:
            client_socket.send("ERROR: Invalid Command".encode("utf-8"))
            continue

        if command == "ADD":
            result = f"RESULT: {num1 + num2}"
        elif command == "SUB":
            result = f"RESULT: {num1 - num2}"
        elif command == "MUL":
            result = f"RESULT: {num1 * num2}"
        elif command == "DIV":
            if num2 == 0:                            # 0으로 나누기를 막는다
                result = "ERROR: Division by zero"
            else:
                result = f"RESULT: {num1 / num2}"
        else:                                        # 모르는 명령어는 오류 처리
            result = "ERROR: Invalid Command"

        client_socket.send(result.encode("utf-8"))   # 결과 문자열을 보낸다

except KeyboardInterrupt:
    print("\n서버를 직접 종료했습니다.")

finally:
    client_socket.close()
    server_socket.close()
    print("소켓이 안전하게 닫혔습니다.")

기대하는 클라이언트 터미널 화면입니다.

서버에 연결되었습니다.
명령어와 숫자를 입력하세요 (예: ADD 5 3): ADD 10 20
[서버 응답]: RESULT: 30
명령어와 숫자를 입력하세요 (예: ADD 5 3): sub 50 30
[서버 응답]: RESULT: 20
명령어와 숫자를 입력하세요 (예: ADD 5 3): ADD 5
[서버 응답]: ERROR: Invalid Command
명령어와 숫자를 입력하세요 (예: ADD 5 3): DIV 10 0
[서버 응답]: ERROR: Division by zero
명령어와 숫자를 입력하세요 (예: ADD 5 3): QUIT
계산기 프로그램을 종료합니다.
채점 체크리스트 통과 기준
포트 번호 서버가 5002번 포트를 사용한다
대소문자 무시 sub 50 30도 정상 계산된다
네 가지 연산 ADD·SUB·MUL·DIV가 모두 동작한다
오류 방어 인자 부족·숫자 아님·0 나누기에서 서버가 죽지 않는다
종료 QUIT 입력 시 양쪽이 정상 종료된다

 

5.3 두 과제 핵심 정리

- 메시지를 여러 번 주고받으려면 클라이언트·서버 양쪽에 while 반복이 필요하다
- 닉네임처럼 추가 정보는 메시지 문자열에 함께 담아 보낼 수 있다
- 약속된 형식(프로토콜)으로 보내면 서버가 split()으로 분해해 처리할 수 있다
- 서버는 어떤 입력이 와도 죽지 않도록 잘못된 입력을 미리 검사해야 한다
- 종료 신호(exit, QUIT)를 정해 두면 양쪽이 깔끔하게 연결을 끝낼 수 있다

→ 확장 아이디어: 과제 1에 접속자 이름 목록을 출력해 보거나, 과제 2에 POW(거듭제곱)·MOD(나머지) 명령어를 추가해 보세요. 두 과제 모두 다음 단계인 다중 접속 서버로 넘어가기 전 좋은 연습이 됩니다.