특히 기술주 중심의 NASDAQ이 가장 큰 낙폭을 보였는데, 2024년~2025년 AI 기대감으로 20000만을 돌파했던 고점에 비해 14%가량 떨어졌습니다. 이 날 주요 기술주 또한 큰 폭으로 하락하였는데, 마이크로소프트(-3.34%),메타(-4.42%), 알파벳(-4.49%),애플(-4.85%),엔비디아(-5.07%)으로 하락하였고 트럼프 당선과 함께 큰 폭으로 올랐었던 테슬라가 15.43%나 폭락하였습니다.
2. 미국 증시와 함께 약세를 보이는 가상 화폐
현재 가상화폐 또한 약세를 보이고 있습니다. 고점에서 100K USD를 넘어섰던 비트코인(BTC)은 현재 80K선이 붕괴되서 79,000달러 선에서 거래되고 있습니다. 이더리움(ETH) 또한 1900선 아래로 약세를 보이고 있습니다.
BTC / USD 시세
ETH / USD 시세
3. "경기 침체 감수" 시사에 급락한 증시
2025년 03월 10일자 뉴욕 증시가 일제히 급락한 배경에는 트럼프가 경기 둔화를 감수하겠는다는 발언이 그 원인으로 지목됩니다. 2025년에 들어 트럼프가 관세 정책을 강행하면서, 증시는 불안정성으로 약세를 보이고 있었습니다. 또한 2024년까지 강세장을 보였기에 조정 국면도 들어가는 상황이였습니다. 이 와중에서 도널드 트럼프 미국 대통령은 03월 09일자 폭스 뉴스 인터뷰에서 현재 추진 중인 경제 정책(관세 정책 등)이 단기적으로 변동성을 초래하며, 미국 경제의 경기 침체 가능성 또한 감수한다고 밝혔습니다. 현재는 과도기이며 장기적으로는 미국의번영을 가져올 것이라고 주장했습니다. 또한 케빈 해셋 백악관 국가경제위원회 위원장도 CNBC와 인터뷰에서 "미국 경제에 대해 매우 낙관적이지만 1분기 GDP 감소도 염두에 두고 있다"며 경제지표 후퇴 가능성을 언급했으며, 이는 일시적인 현상일 것이라고 말했습니다.
작년 트럼프 당선 이후 불안정 해소로 급등했던 증시와 가상화폐는 트럼프 관세 정책에 의해 무역 전쟁의 긴장감이 고조되며, 약세로 돌아선 상태입니다. 트럼프의 관세 정책에 따라 캐나다·멕시코 등 상대국은 보복 관세 방침을 밝혔습니다. 과거 2018년에 미중 무역 전쟁 이슈로 증시가 큰 폭으로 하락한 과거가 있는데, 무역 전쟁의 긴장이 돌면서 투자자들의 불안이 증가되고 있습니다. 이 배경에서 경기 침체를 감수하면서까지 관세 폭탄 정책을 유지하겠다는 발언은 투자자들의 불확실성을 급증시켰으며, 이로 인해 폭락한 것으로 비추어집니다.
이마트 다음으로 규모가 큰 대형마트 홈플러스가 2025.03.03일자에 서울회생법원에 갑작스럽게 기업회생절차를 신청하여, 많은 사람들에게 충격을 주고 있습니다. 특히 작년에 티메프 사태로 큰 파장이 있었기 때문에 소비자와 협력사의 근심이 굉장히 큰 상태입니다.
홈플러스 회생 절차 배경
먼저 회생 절차에서는 사모펀드 MBK파트너스의 홈플러스 인수 후, 실적 및 재무상황의 악화가 그 배경이 됩니다.
※ 경영 구조 배경 홈플러스는 1997년 삼성물산 유통 부분에서 출발하였습니다. 하지만 1999년 IMF 외환위기 당시 영국의 최대 유통업체인 테스코가 상당수의 지분과 경영권을 인수하였고, 2011년에는 테스코가 지분의 100%를 인수하게 되었습니다. 그러나 테스코의 분식회계 스캔들 및 영업실적 하락으로 자금압박에 의해 매물로 나왔고, 결국 2015년에는 국내 사모펀드(PEF) 운용사인 MBK 파트너스가 약 7조 2천억 원에 홈플러스를 인수되었습니다. 하지만 인수 당시 MBK 파트너스는 4조3000억원을 금융권에 빌려 인수하여서 무거운 부채를 가지고 있습니다.
설상가상으로 MBK 파트너스의 인수 후 매출은 감소하였고, 최근 3년 연속 적자와 부채가 늘어나게 됩니다. 이런 배경에서 회생 절차를 진행하게 된 것으로 비춰집니다. 현재 부채와 재무 상황은 이렇습니다. - 과거에 비해 감소된 매출 :2014년 홈플러스의 매출액은 약 8조 5,681억 원이었으나, 2023년에는 약 6조 9,314억 원으로 19.1% 감소했습니다 - 적자 누적 :2021년 이후 3년 연속 적자를 기록하며, 누적 적자는 5,930억 원에 달합니다. - 금융부채 규모: 운영자금 차입을 포함한 실제 금융부채는 약 2조원 규모입니다. - 자산 규모: 현재는 전국 각지 홈플러스 매장으로 약 4조 7천억 원 상당의 부동산 자산을 보유하고 있습니다.
MBK파트너스는 인수 당시4조원에 달하는 빚을 20여개 점포를 폐점이나 매각 후 재임차로 자산을 처분하여 갚았습니다. 하지만 수익성 악화에 따른 폐점은 여전히 진행중입니다.
홈플러스 실적 악화의 원인
홈플러스 경영 실적 악화의 원인은 국내 e-커머스(쿠팡,SSG닷컴)와 값싸게 물건을 구할 수 있는 중국 커머스(알리 익스프레스 등)에 의해 경쟁력이 밀려난 것이 원인으로 유추됩니다. 특히 코로나와 엔데믹 이후 온라인 커머스가 더욱 발달하면서, 오프라인 매장보다는 로켓프레쉬 등 온라인을 통해 장보는 사람들이 많이 늘어나게 되었습니다.
또한 일부 매점을 폐점하고 매각하여 현금을 확보하는것으로 부동산 자산을 유동화하였지만, 매장수 감소는 한편 매출의 감소로 이어지게도 되었습니다. 폐점되는 일부 매장 중에선 알짜 매장들도 있어서 매출 하락의 우려가 큽니다.
[ 폐점 예정인 점포들 ] - 서울 : 강동점, 금천점, 동대문점, 방학점, 잠실점 - 경기 : 동수원점, 부천상동점, 부천소사점, 수원영통점, 안산선부점 - 인천 : 작전점 - 충북 : 동청주 - 충남 : 천안점 - 세종 : 조치원점 - 경북 : 죽도점 - 대구 : 대구칠곡 내당점 - 부산 : 부산반여 센텀시티점 - 경남 : 삼천포점 진주점 - 전남 : 순천풍덕
다만 잠실점과 같은 일부 매점은 주상복합 완공 후 지하에 재입점할 가능성도 있다고 합니다.
홈플러스 회생 신청 영향
회생 신청으로 인하여 홈플러스의 신용등급은 크게 하락하였습니다. 지난달 28일 홈플러스 기업어음과 단기사채 신용등급을 A3에서 A3-등급으로 하향되었습니다. 하지만, 이번 회생 신청으로 인하여 신용등급은 D로 추락하였습니다.
기존의 매점 폐쇄와 부동산 자산 매각, 회생 신청으로 인해 노사간의 갈등도 심화될 것으로 예정됩니다.
이전 포스팅에서 소켓의 블로킹(Blocking)/논블로킹(Non-blocking) 여부를 설정하기 위해 ioctlsocket 함수를 사용하였다.
이번 포스팅에선 간략하게 해당 함수에 대해서 다루고자 한다.
1) ioctlsocket 함수란? ioctlsocket 함수는 소켓을 제어하기 위해 호출하는 함수이다. ioctlsocket 함수의 인자는 다음과 같다.
intioctlsocket(
SOCKET s,
long cmd,
u_long *argp
);
- 1번째 인자(SOCKET s): 제어할 소켓 - 2번째 인자(long cmd): 명령어 - 3번째 인자(u_long *argp): 명령어(2번째 인자)에 대한 매개 변수에 대한 포인터
2) ioctlsocket 명령어 ioctlsocket함수에서 2번째 인자로 오는 명령어에는 여러 종류가 있다. 해당 기능들은 BSD 소켓 기반에서 지원하는 명령 중 몇몇 옵션을 지원한다.
cmd명
기능
FIONBIO
논블로킹 여부를 활성화/비활성화 한다.
- argp가 0이 아님: 소켓이 논블로킹 모드가 된다. - argp가 0임: 소켓이 블로킹 모드가 된다.
FIONREAD
현재 수신 대기중이며 바로 읽을 수 있는 byte수를 가져온다. 보통 recv()함수로 데이터를 읽기 전, 얼마만큼 받아올 수 있는지 체크하는데 쓴다.
- argp에 결과값을 저장한다.
SIOCATMARK
긴급 데이터(Out-of-band data,OOB)가 포함된 위치를 찾는다. ※ Out-of-band data(OOB): TCP프로토콜에서만 사용. 인터럽트 신호, 중요 공지 등 긴급 신호를 보내기 위해 사용된다.
- argp에 결과값을 저장한다.
3) ioctlsocket을 통해 논블로킹 모드로 변경하기 소켓은 생성하면 기본적으로 블로킹 모드로 설정되어있다. 따라서 ioctlsocket함수를 사용하여 논블로킹 모드를 사용해야한다. 아래는 논블로킹 모드로 변경하는 예시이다.
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
// 논블로킹 모드 설정
u_long nb_mode = 1;
ioctlsocket(sock, FIONBIO, &nb_mode);
여기에서, 논블로킹 모드로 변경하였다면 한가지 주의할 점이 있다. 논블로킹 소켓은 각종 소켓 처리함수(accept, recv등)에서 현재 처리할 작업이 없거나, 즉시 완료할 수 없는 상태면WSAEWOULDBLOCK 오류를 반환한다. 해당 오류는 소켓의 문제되는 오류가 아니므로 예외처리를 진행하며, 실제로 소켓에서 처리할 작업이 올때까지 대기하도록 해야한다.
아래는 간단하게 논블로킹 모드로 설정된 소켓통신을 하는 예시이다.
#include<winsock2.h>#include<iostream>#pragma comment(lib, "ws2_32.lib")intmain(){
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
char buf[1024];
// 논블로킹 모드 설정
u_long mode = 1;
ioctlsocket(sock, FIONBIO, &mode);
std::cout << "논블로킹 모드 설정됨" << std::endl;
while(1) {
int recvBytes = recv(sock, buf, sizeof(buf), 0);
if (recvBytes > 0) {
// 소켓을 통해 데이터가 정상적으로 수신되었을때
std::cout << "[수신 데이터]: " << buf << std::endl;
}
elseif (recvBytes == 0) {
std::cout << "[소켓 연결 종료됨]" << std::endl;
closesocket(sock);
break;
}
elseif (WSAGetLastError() != WSAEWOULDBLOCK) { // 논블로킹의 경우 소켓에 읽은 데이터가 큐에 없으면 반환된다.if (WSAGetLastError() != WSAECONNRESET) { // 상대방이 먼저 연결을 종료한 경우는 오류 제외
std::cerr << "recv() 오류: " << WSAGetLastError() << std::endl;
break;
}
}
}
closesocket(sock);
WSACleanup();
return0;
}
논블로킹 모드로 설정된 소켓을 다시 블로킹 모드로 돌려놓고 싶다면 아래와 같이 설정하면 된다.
// (소켓 정의 및 연결 부분은 생략)// 1. FIONREAD로 수신 대기 중인 데이터 크기 확인
u_long dataSize = 0;
if (ioctlsocket(clientSocket, FIONREAD, &dataSize) != 0) {
std::cerr << "ioctlsocket(FIONREAD) 실패: " << WSAGetLastError() << std::endl;
// 실패에 따른 작업 추가return1;
}
if (dataSize > 0) {
// 2. 수신 대기 중인 데이터만큼 버퍼 동적 할당char* buffer = newchar[dataSize + 1];
memset(buffer, 0, dataSize + 1); // 버퍼 초기화// 3. 수신int bytesReceived = recv(clientSocket, buffer, dataSize, 0);
if (bytesReceived > 0) {
buffer[bytesReceived] = '\0'; // 널 문자 추가
std::cout << "수신된 데이터: " << buffer << std::endl;
} elseif (bytesReceived == 0) {
std::cout << "클라이언트 연결 종료!" << std::endl;
} else {
std::cerr << "recv() 실패: " << WSAGetLastError() << std::endl;
}
// 4. 동적 할당 해제delete[] buffer;
} else {
std::cout << "수신 대기 중인 데이터가 없습니다." << std::endl;
}
5) 기타 ioctlSocket 함수는 BSD소켓의 일부 기능만 제공한다. 예를들면 FIONREAD는 제공하지만, 송신가능한 데이터를 확인하는 FIONSEND는 제공하지 않는다. 더 많은 기능들이 들어간 소켓제어 함수를 Windows에서 사용한다면 WSAIoctl함수를 사용해보자.
6) Non-blocking EchoServer 만들기 이전 포스팅에서 기록하였던 블로킹 모드의 echoServer 코드를 개선하여, 논블로킹 모드로 echoServer를 작성하였다.
EchoServer.cpp
#pragma comment(lib, "ws2_32")#define _WINSOCK_DEPRECATED_NO_WARNINGS#include<iostream>#include<stdio.h>#include<winsock2.h>#include<memory>#include<vector>#define BUF_SIZE 1024usingnamespace std;
intmain(){
printf("Server Start!\n");
// 1) WinSock 라이브러리를 초기화한다
WSAData wsaData; //WinSock 초기화 구조체int initWinSockRes = WSAStartup(MAKEWORD(2, 0), &wsaData);
if (initWinSockRes != 0) {
printf("윈도우소켓 초기화 실패");
return0;
}
// 2) 서버와 클라이언트 소켓을 생성하고 서버소켓을 bind, listen한다.
SOCKET servSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in servAddr, clntAddr;
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
servAddr.sin_port = htons(11000);
if (bind(servSocket, (sockaddr*)&servAddr, sizeof(servAddr)) != 0) {
printf("bind() 오류 [오류코드: %d]\n", WSAGetLastError());
return0;
}
// 크기 3의 대기열을 만든다.// 클라이언트의 연결요청이 왔을때 서버에서 준비된 소켓이 없다면 대기열에서 연결을 기다리게된다. if (listen(servSocket, 3) != 0) {
printf("listen() 오류 [오류코드: %d]\n", WSAGetLastError());
return0;
}
char buf[BUF_SIZE] = {};
int clntAddrLen = sizeof(clntAddr);
int socketCnt = 0;
std::vector<SOCKET> clntSockets = vector<SOCKET>(0);
// ★ 추가: 서버 소켓을 논블로킹으로 한다.
u_long mode = 1;
ioctlsocket(servSocket, FIONBIO, &mode);
while (1) {
// 3) 클라이언트가 연결을 요청하면 수락한다.
SOCKET clntSocket = accept(servSocket, (sockaddr*)&clntAddr, &clntAddrLen);
if (clntSocket == INVALID_SOCKET) {
if (WSAGetLastError() != WSAEWOULDBLOCK) {
printf("accept() 오류 [오류 코드: %d]\n", WSAGetLastError());
return0;
}
}
else {
// ★ 추가: 클라이언트 소켓을 논블로킹으로 한다.ioctlsocket(clntSocket, FIONBIO, &mode);
clntSockets.push_back(clntSocket);
printf("accept client\n");
}
// 연결된 소켓들을 순차적으로 처리한다.for (auto it = clntSockets.begin(); it != clntSockets.end();) {
SOCKET clntSocket = *it;
memset(buf, 0, sizeof(buf));
// ★ 추가: 수신 대기중인 데이터를 확인해본다.
u_long r_size = 0;
ioctlsocket(clntSocket, FIONREAD, &r_size);
if (r_size > 0) { // 읽어들일 데이터가 없을땐 0으로 반환한다. (출력이 많아지는것을 방지하기위해 0 이상일때만 출력하도록 함)printf("readablie size : %ld\n", r_size);
}
int recvBytes = recv(clntSocket, buf, r_size, 0);
if (recvBytes > 0) {
// 4) 클라이언트로부터 온 메세지를 받는다.
std::cout << "[ 클라이언트 " << clntSocket << "] 수신: " << buf << std::endl;
// 5) 클라이언트로부터 온 메세지를 그대로 클라이언트에게 전송한다.send(clntSocket, buf, recvBytes, 0);
}
elseif (recvBytes == 0) {
std::cout << "[ 클라이언트 (소켓: " << clntSocket << ") 연결 종료 ]" << std::endl;
closesocket(clntSocket);
it = clntSockets.erase(it);
continue;
}
elseif (WSAGetLastError() != WSAEWOULDBLOCK) { // 논블로킹의 경우 소켓에 읽은 데이터가 큐에 없으면 반환된다.if (WSAGetLastError() != WSAECONNRESET) { // 클라이언트가 먼저 연결을 종료한 경우는 오류 제외
std::cerr << "recv() 오류: " << WSAGetLastError() << std::endl;
}
}
++it;
}
// TODO: 서버에서 수행할 다른 작업들을 실행할 수 있다.
}
WSACleanup();
return0;
}
위의 이미지는 ibm 문서에서 정의된 유명한 2x2매트릭스이다. 여기서 각 개념들이 조합된 I/O모델의 예시를 알아보자
1) 동기 블로킹 I/O (Sync Blocking I/O)
동기 블로킹 방식은 I/O 호출을 하면, 응용프로그램은 멈추며, I/O를 요청한 함수가 완료될 때까지 스레드는 아무런 작업을 하지않고 기다린다. 작업이 완료되면 이후 정의된 작업을 순차적으로 진행하게 된다. winsock에서 기본적으로 제공하는 send(), recv() 함수가 이러한 모델들이다. 아래는 동기 블로킹 소켓모델에 대한 예시이다. 이전에 구현했던 클라이언트에서 단 한번만 send()/recv()작업을 하는 코드로 변경하였다.
intmain(){
WSAData wsaData;
SOCKET clientSocket;
// ... 중략 ... //// 중략된 내용은 이전 포스팅(https://dodo000.tistory.com/29)의 클라이언트 코드와 동일합니다.
clientSocket = socket(AF_INET, SOCK_STREAM, 0);
if (clientSocket == INVALID_SOCKET) {
printf("윈도우소켓 초기화 실패");
return0;
}
if (connect(clientSocket, (sockaddr*)&servAddr, sizeof(servAddr)) != 0) {
printf("connect() 오류 :%d\n", WSAGetLastError());
return0;
}
// ... 중략 ... //if (send(clientSocket, input, sizeof(input), 0) == SOCKET_ERROR) {
printf("send() 오류 : %d\n", WSAGetLastError());
return0;
}
printf("send 함수가 완료된 후, 다음 작업이 실행됩니다.\n");
if (recv(clientSocket, received, BUF_SIZE, 0) == SOCKET_ERROR) {
printf("recv() 오류 : %d\n", WSAGetLastError());
return0;
}
printf("recv 함수가 완료된 후, 다음 작업이 실행됩니다.\n");
printf("[message from server] %s \n", received);
return0;
}
send()함수 이후 printf작업이 진행되며, recv()함수가 완전히 실행된 이후 다음 작업들이 실행된다. 동기-블로킹 모델은 구현에 있어서 간편하다는 장점이 있지만, 한 소켓의 send()/recv() 작업동안은 아무것도 할 수 없기에 다수의 클라이언트의 동시요청을 서비스하기 어렵다.
2) 동기 논블로킹I/O (Synchronous Non blocking I/O)
동기-논블로킹 방식은 I/O 함수가 호출되자마자 전송/수신이 완료여부와 상관없이 반환한다. 이때 send나 recv 작업이 완료되지 않았다면 반환되는 응답값은 WSAEWOULDBLOCK라는 에러코드이다. 하지만 동기방식을 택하게 되면, 응용프로그램은 입출력 작업에 뒤이어서 오는 작업을 수행하기전에 send()/recv() 작업이 온전하게 데이터를 받을때까지 체크를 해야한다. 그래서 여러번 소켓의 함수를 호출하여 체크를 하고 완전히 입출력이 완료되었을 때, 다음 작업을 수행하는 방식이다.
여기서 ioctlsocket 함수는 소켓의 모드를 설정할 수 있는 함수이다. FIONBIO는 소켓의 블로킹/논블로킹 여부를 지정하겠다는 옵션이며, mode 변수로 온 값이 0이면 블로킹, 0이 아니면 논블로킹 모드로 세팅된다.
해당 소스코드를 실행한 결과이다.
사실 이 방식은 꽤 비효율적일 수 있다. 논블로킹으로 소켓이 바로 반환되지만 순차적으로 작업들을 처리하기 위해 계속해서 recv()함수의 결과를 확인해서 다음 작업을 진행하지 못하고 있다. 동기-블로킹 방식의 경우 입출력 작업을 위해 한 스레드가 blocking될 경우, CPU는 다른 스레드에게 제어권을 넘기면서 병렬처리를 할 수 있다. 하지만 동기-논블로킹은 계속해서 CPU가 해당 스레드를 실행하면서 입출력 작업을 확인하므로 busy-wait라는 자원낭비가 된다.
3) 비동기 블로킹 I/O(Asynchronous Blocking I/O)
비동기 블로킹 방식은 각각의 소켓들의 입출력이 완료되지 않아도 다음 작업을 실행할 수 있지만, 이후 블로킹 모드로 작동하는 select 함수를 통해 하나 이상의 소켓에서 입출력 처리가 완료되기 전까지 응용프로그램의 작동이 잠깐 멈추는 방식이다. ※ select 입출력 모델에 대해서는 이전포스팅에서 다루었다
select 함수를 통해 다수의 소켓들을 묶어서 처리하기 때문에, 단일 스레드에서 다수의 클라이언트를 실시간 서비스할 수 있는 장점이 있다. 하지만 select함수가 완료될 동안 해당 스레드는 아무런 작업을 할 수 없으며, select 함수 내부적으로 각각 소켓(리눅스에서는 파일디렉토리)들을 하나하나 순회하며 체크하기 때문에 C10K문제(10,000명의 동시접속자의 요청을 처리하는 문제)에서는 효율이 떨어지는 한계를 보인다.
4) 비동기 논블로킹 I/O(Asynchronous Non blocking I/O)
비동기 논블로킹 모델은 각 소켓들이 입출력 처리가 완료되지 않아도 바로 반환되며, 스레드는 다른 작업들을 진행한다. 이후 특정 시점에서 입출력 작업이 완료되었을 때, 이를 통지받고 이에 따른 작업을 진행한다. 효율적으로 다른 프로세스들을 처리할 수 있고 여러 클라이언트에서 동시다발적으로 오는 요청을 처리하기에 용이하다. 하지만 구현이 보다 복잡해진다. 이 모델의 예시로는 Overlapped I/O와 IOCP 모델이 존재한다. 이 모델을 채택한 서버에 대해서는 다음 포스팅들에서 다룰 예정이다.
XCode에서 Swift UI를 생성하면 다음과 같은 초기 코드가 존재한다. App, Scene, View가 어떤 역할을 하는지 살펴보도록 한다.
@mainstructMyApp: App{
var body: someScene {
ContentView()
}
}
1. @main 앱을 시작하였을때 진입점을 명시해준다. 이 프로토콜은 앱을 시작하면 main()함수를 호출하게 해준다. 이 main은 모든 앱의 파일중에서 단 하나만 존재해야한다.
2. App 어플리케이션의 구조와 동작을 나타내는 프로토콜. App의 작동을 정의하는 body 변수를 필수로 가져야한다. body는 하나 이상의 Scene에 의해서 구성되는 인스턴스이다. app은 런타임동안 body에 가지고 있는 Scene들을 표현한다.
- App의 life cycle 관리 : iOS 앱에 있어서 life cycle(생명주기)이란 어플리케이션의 활성/비활성 및 Foreground(화면에 띄워짐)/Background(화면에 띄워지지않은 상태에서 실행되고있음), 시스템이 발생시키는 이벤트에 의해 App의 상태가 전환되는 과정을 말한다. iOS14 이전의 버전에서는 App Delegate와 SceneDelegate를 통해 life cycle을 관리하였다. (과거 버전에서는 App의 생명주기만 존재했지만, 이후 Scene개념이 도입되면서 Scene만의 생명주기도 추가되었다) Delegate 클래스는 생명주기 관리외에도 코드 Clean-up, 상태 변경, 이벤트에 필수적인 작업들을 진행하였다. 하지만 iOS 14부터의 Swift UI버전에서는 이런 요소들이 App인스턴스가자동적으로 관리하며, 작업자는 진입점(@main)만 정의하면 된다.
※ App Delegate와 SceneDelegate 최신버전에서는 필수적이지 않기에, 프로젝트를 생성해도 기본적으로 내장되어 있지 않다. 다만 아직도 일부 SDK기능등을 이용하기 위해선 필요하여 수동적으로 도입한다.
3. Scene Scene은 어플리케이션에 그려지는 화면을 표현하는 역할을 담당하는 인스턴스. 각각의 UI요소를 담당하는 View들의 계층구조에서 뿌리 역할을 담당한다. - 계층구조
- Scene Phase: Scene을 활성·비활성·백그라운드 실행 상태를 나타내는 값이다. 이 값이 변경될 때 작업들을 정의하여 Scene의 life cycle을 관리할 수 있다.
@mainstructMyApp: App{
// 백슬래쉬(\)는 변수가 아닌, property값을 나타낸다는 의미이다. keypath라고도 한다.@Environment(\.scenePhase) privatevar scenePhase
var body: someScene {
WindowGroup {
Text("Hello, world!")
}
}
.onChange(of: scenePhase) {(newScenePhase) inswitch newScenePhase {
case .active:
// 활성 상태가 되었을때 작동 정의case .inactive:
// 비활성 상태가 되었을때 작동 정의case .background:
// 백그라운드 실행 상태가 되었을때 작동 정의
}
}
}
active: Scene이 foreground 상태이며, 유저 이벤트를 받을 수 있는 상태
inactive: Scene이 foreground 상태이지만, 이벤트를 받지 못하는 비활성 상태
background: Scene이 현재 존재하고있지만, UI에서는 보여지지 않는 상태
여기서 Swift UI가 기본적으로 제공하는 여러타입의 Scene이 존재한다. 표현하고자 하는 Scene에 알맞은 타입을 적용하거나, 혹은 사용자가 직접 Scene을 커스텀하여 정의할 수 있다.
WindowGroup: 동일한 계층구조를 가지는 그룹들을 여러개의 독립된 윈도우들로 구성하여 관리하는 Scene
Window: 단 하나의 유일한 윈도우만 보여주는 Scene
DocumentGroup: 파일 등의 document들을 Read/Write할 수 있도록 지원하는 Scene
Settings: App의 설정값들을 보여주고 수정할 수 있게하는 Scene
WKNotificationScene: 원격 혹은 로컬에서 알림을 받았을 때 나타나는 Scene
4. View view는 앱의 UI를 표현하는 프로토콜이다. view 프로토콜은 커스텀한 view의 내용물을 담는 body를 정의해야한다. body는 swift ui에서 제공하는 하나 이상의 built-in view를 조합해서 작성할 수 있다. 이렇게 view 안에 하위 view들이 구성되는 형태, 즉 view끼리의 계층구조가 구성된다. - modifier: view는 modifier을 정의하여 크기, 색상, 폰트 등 여러가지 요소들을 수정할 수 있다.
위의 예시 코드에서 .font, .foreground 등의 설정값이 각 view들의 modifier가 된다. UI를 구성하는 built-in view는고정 텍스트(Text), 수정가능한 텍스트 영역(TextEditor), 버튼(Button), 이미지(Image) 등 다양한 종류를 지원하고 있고, 이들의 스타일을 편집하는 modifier도 굉장히 다양한 옵션들이 존재한다.
지난 포스팅에서 select 모델의 특징을 알아보며 블로킹(Blocking)과 비동기(Asynchronous) 입출력 모델이라 설명을 하였다. 컴퓨터공학 분야에서는 기술적 구현에 따라 블로킹과 논블로킹, 그리고 호출된 함수와 호출된 함수의 순차를 신경쓰는가 아닌가에 따라 동기와 비동기로 나뉘어진다. 이번 포스팅에서는 이 개념에 대해서 상세히 다뤄보겠다.
1. 블로킹(Blocking) vs 논블로킹(Non-Blocking)
블로킹과 논블로킹은 기술적인 구현을 통해 나뉘는 개념이다. 코드를 통해 블로킹으로 구현하였는가, 논블로킹을 구현하였는가에 따라 프로세스의 CPU 유휴상태가 달라진다.
1) 블로킹(Blocking)
Blocking방식
- 블로킹은 함수A가 함수B를 호출하였을때, 제어권을 함수 B에게 넘겨준다. A는 B의 결과가 반환되기 전까지 아무런 작업도 하지않고 대기한다. - 마찬가지로 입출력 요청이 발생했을때, 응용프로그램(Application)은 커널(Kernel)에게 시스템 요청(System call)을 하며 커널의 입출력 처리가 마칠때까지 입출력을 요청한 응용프로그램의 스레드는 아무런 작업을 진행하지 않고 기다린다.
※ 시스템 호출(system call): 응용프로그램에서는 수행할 수 있는 기능이 제약적이다. 따라서 특정 기능의 수행이 필요할 때 커널의 도움이 필요하다. 이때 커널의 기능을 사용하기 위해 요청하는것이 시스템 호출이다.
- 단일 스레드로 블로킹 방식의 입출력을 한다면, 입출력 작업이 완료될 때 까지 응용프로그램이 멈춰있는다. (이전에 구현했던 send()/recv()함수를 통한 서버와 클라이언트가 그 예시이다.) - 멀티 스레드로 다수의 클라이언트를 서비스하는 환경에서 문맥 교환(Context Switching)이 잦다. CPU는 실행중인 스레드가 입출력 작업으로 인해 일시적으로 중지되면 다른 스레드에게 CPU자원을 할당하여 실행시킨다. 이때 CPU의 자원을 할당받아 실행시키는 작업이 문맥 교환이라고 한다. 문맥 교환 작업이 일어나는 동안은 프로세스 처리하는 작업을 못하게 되므로 오버헤드가 발생하여 효율이 떨어진다.
※ 오버헤드(overhead): 어떤 처리를 하기 위해 들어가는 간접적인 처리 시간·메모리
이렇게 블로킹 I/O는 비효율성이 있기때문에 이를 개선하기 위한 논블로킹 모델이 고안되었다.
2) 논블로킹(Non-Blocking)
Non-Blocking 방식
- 논블로킹은 함수A가 함수B를 호출하였을때, B는 데이터가 준비되지 않아도 곧바로 반환을 한다. 이때 함수A가 가지고 있는 제어권도 B에게 넘겨준 즉시 A가 돌려받는 것이다. - 함수 B가 반환한 데이터는 신뢰성이 없기때문에, A의 작업을 수행하면서 주기적으로 B가 원하는 결과값이 구해졌는지 체크가 필요하다. 이 작업을 폴링(polling)이라고 한다
- 논블로킹 입출력 모델은 응용 프로그램이 커널에게 시스템 요청을 했을때, 곧바로 응답을 반환한다. (예를들어 windows에서는 WSAEWOULDBLOCK라는 응답을 바로 반환한다) - 멀티 스레드 환경에서 입출력 요청이 발생했을때 다른 스레드를 실행시키기 위한 문맥교환이 필요없다. 입출력을 요청한 스레드는 중단없이 실행되기 때문이다. - 블로킹 형식보다 개선된 효율성을 보이지만 논블로킹이 만능은 아니다. 다수의 클라이언트로부터 동시다발적인 요청이 온다면, 커널에서 원하는 응답값이 준비되었는지 확인하는 과정에서 많은 비용이 들기 때문이다. - 운영 체제들은 polling작업을 커널에서 지원하여 효율을 높인 I/O 모델들을 지원한다. 리눅스의 epoll이 그 예이다.
2. 동기(Synchronous) vs 비동기(Asynchronous)
동기와 비동기는 기술로 구분되는 것이 추상적인 개념이다. 블로킹 논블로킹은 제어권이 누구에게 있느냐가 관심이였다면, 동기와 비동기는 호출한 함수가 작업들의 순서가 보장되느냐 아니냐가 관심사이다
1) 동기(Synchronous)
Synchronous 방식
- 동기방식은 함수A가 호출되었을때, 함수B는 먼저 호출된 함수A의 종료까지 기다린다. 함수A가 종료된 후에서야 함수B가 실행된다. - 함수 작업A가 완료되는동안 함수B는 기다리고 있기때문에 얼핏 블로킹과 유사해보이지만, 동기모델의 관심사는 순차적인 작업의 순서를 보장한다는 것이다.
- 동기 입출력 모델에서는 입출력이 발생하였을 때, 응용프로그램의 스레드는 커널에게 입출력의 입출력 작업이 진행되는 동안 아무런 작업을 하지않고 기다린다. 이후 커널의 작업이 끝나면 응용프로그램의 스레드는 다음 작업을 진행한다. - 동기 입출력 모델에서 응용프로그램이 입출력을 마칠때까지 기다린다는 점에서 마치 블로킹과 유사하지만, 블로킹 형태의 입출력은 CPU자체의 처리를 입출력 작업이 끝나기 전까지 막는것을 뜻하며, 동기-논블로킹 형태로 구현도 가능하다. 입출력함수를 논블로킹으로 설정하면 CPU자체는 곧바로 반환하지만 동기형식이기 때문에 입출력 결과를 계속해서 확인하는 polling작업을 하면서 CPU는 busy-wait(CPU가 계속 프로세스를 작업하며 대기하는 상태)가 될 수 있다.
- 동기방식은 순서를 기다리기 때문에 효율성이 떨어질때도 있지만, 간단한 작업들이 순차적으로 필요할때는 효율적일 수 있다.
2) 비동기(Asynchoronous)
Asynchronous 방식
- 비동기방식은 함수A가 먼저 호출되었다고 해도, 뒤이어 호출되는 함수B와 함수C는 앞서 호출된 함수의 완료여부에 상관없이 실행된다. - 각 함수들의 호출 순서와 함수들의 완료가 같은 순서가 된다고 보장할 수 없다. - 함수B와 함수C의 시점에서는 선행된 작업을 기다리지 않아서 논블로킹과 유사해보이지만, 비동기는 순서를 지키지않고 작업을 진행한다는 것이 관심사이다.
- 비동기 입출력 모델에서는 응용프로그램이 입출력 요청이 발생했을때, 응용프로그램 스레드는 커널에게 작업을 맡긴 후 완료를 기다리지 않고 다음 작업들을 진행한다. - 비동기와 논블로킹이 유사해보이지만, 입출력 요청이 비동기형태로 구현되어도 블로킹 형태로 처리될 수 있다.
- 여러 작업들을 병행해서 가능하므로 효율을 높일 수 있다. 하지만 구현에 있어서 보다 더 복잡한 문제를 고민해야할 가능성이 동기방식보다 높다.
이번 포스팅을 통해 블로킹(Blocking)과 논블로킹(Non Blocking) 그리고 동기(Synchronous)와 비동기(Asynchronous)에 대해 상세히 알아봤다. 사실 동기방식은 블로킹, 비동기방식이 논블로킹이 구현에 있어서 편리함이 있기에 두 개념이 묶여질 때가 많다. 하지만 이들은 별개의 개념이다. 다음 포스팅에서는 이 개념들이 조합된 형태에 대해 다뤄볼 예정이다.
1) Select I/O 모델 먼저 앞선 포스팅에서 멀티플렉싱 I/O모델을 알아보았다. 멀티플렉싱 구조의 서버는 다수의 클라이언트의 입출력 정보를 한꺼번에 묶어서 검사하고 이에 따른 처리를 진행하는 모델인데, 이 역할을 select() 함수가 지원해준다. select함수는 다수의 소켓으로부터 읽기/쓰기/예외사항 별 이벤트를 감지하는 역할을 수행한다.
※ read_fds / write_fds / except_fds의 인자가 오는 자리에 대신 NULL을 기입할 수 있으며, NULL을 기입할 경우 해당 이벤트는 감시하지 않는다.
- 1번째 인자: BSD socket과의 호환을 위해 형식적으로 남겨둔 인자다. Winsock에서는 사용하지는 않는 값이다. (Linux에서는 해당 값을 입력하면, fd배열에서 해당 인자의 값까지의 갯수만 체크한다) - 2번째 인자: 읽기 작업이 필요한 소켓이 있는지 검사한다. 해당 소켓으로부터 클라이언트가 연결되어 accept가 필요하거나, 클라이언트로부터 데이터가 도착하여 읽기 작업이 필요한 경우 읽기 관련 작업이 필요하다는 이벤트가 발생한다. - 3번째 인자: 쓰기가 가능한 소켓이 있는지 검사한다. 클라이언트 소켓이 connect() 함수호출을 완료하고 데이터를 보낼 수 있을때나, send()함수의 작업을 완료하고 다시 데이터를 송신할 수 있는 상태가 되면 이벤트가 발생된다. - 4번째 인자: 예외사항이 발생한 소켓이 있는지 검사한다. - 5번째 인자: select 함수가 커널에 머물며 체크를 하는 최대시간을 지정한다. 만일 NULL로 설정하면 결과값을 반환하기 전까지 서버는 blocking 상태가 된다. - 반환값: select 함수에서 이벤트가 발생하여 작업을 대기중인 소켓들의 갯수를 반환한다. 만일 timeout이 되었는데 감지된 이벤트가 없다면 0을 반환한다. select함수 처리도중 오류가 발생하였다면 SOCKET_ERROR를 반환한다.
2) 파일 디스크립터(FD, File Descriptor) 란? select함수에 fd_set이라는 구조체가 객체의 포인터가 인자로 온다. 이것은 파일 디스크립터(fd)들을 저장한 객체인데, select 서버 전체의 코드를 살펴보기 전에 파일 디스크립터가 무엇인지 알아보도록 하자. Windows Socket은 Berkeley 소프트웨어 배포 (BSD, 릴리스 4.3)의 UNIX 소켓 구현을 기반하여 구현되었으며, BSD Unix와의 호환을 위해 Unix/Linux의 소켓함수와 유사한 구조를 가지는 것이 많다. Unix/Linux는 소켓을 file단위로 취급한다(Unix/Linux 하드웨어를 포함한 시스템의 모든것을 파일단위로 관리한다). 여기서 프로세스가 작업을 수행하기 위해 파일을 참조해야하는데, 이때 각 파일을 구분하게 해주는 값이 파일 디스크립터이다. 파일을 open()/read()/write() 등의 작업을 할때 인자로 이 파일 디스크립터 정보를 넘겨주며 작업을 진행한다. Unix/Linux에서는 각 파일의 디스크립터를 음이 아닌 정수값으로 구성되어있다. (음수는 NULL혹은 오류코드를 기술하는데 사용된다). 여기서 0,1,2은 표준 입출력을 위해 예약된 정수여서 파일이 생성되었을때 파일 디스크립터는 3부터 가장 작은 숫자에서 다른 파일이 사용하지 않은 숫자를 할당한다.
※ 표준 입출력을 나타내는 파일 디스크립터
표준 입력(Standard Input): 0
표준 출력(Standard Output): 1
표준 오류(Standard Error): 2
3) Unix/Linux의 fd_set 구조체 여러개의 파일 디스크립터의 입출력 정보를 묶은 구조가 fd_set이다. Unix/Linux에서 fd_set은 비트값을 통하여 각 파일의 상태를 나타낸다. 그 이유는 만일 fd가 가지는 고유한 정수값을 그대로 저장하게 된다면 한 파일의 정보당 4byte의 크기를 저장하게 된다. 하지만 bit값으로 구성된 배열에 각 파일의 fd값을 인덱스로 정보를 나타낸다면, 한 파일당 1bit의 크기만으로 표현할 수 있어서 저장공간이 절약된다.
초기의 fd_setfd=3인 파일의 상태변화가 있을때의 fd_set
4) Windows의 fd_set 구조체 Unix/Linux의 구조와 호환을위해 fd_set 구조체를 select 함수의 인자로 넘겨주고 있지만, 실제로 Windows는 Unix와는 다른 형식으로 소켓을 취급한다. Windows는 소켓을 파일이 아닌 커널 객체로 취급하며 handle을 통해 다룬다. 실제 Windows의 fd_set 구조체는 이 소켓 핸들을 가르키는 포인터 배열과 배열의 크기정보를 가지고있다. 이 정보들은 BSD Unix와의 호환을 위해 unsigned int형태로 재관리된다.
※ 핸들(handle) : 스레드,파일,그래픽과 같은 시스템 자원을 응용 프로그램이 직접 접근할 수 없다. 따라서 이러한 시스템 자원들을 접근할 수 있도록 돕는것이 핸들이다.
typedefstructfd_set {
u_int fd_count; /* how many are SET? */
SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
} fd_set;
4) fd_set 매크로 fd_set의 데이터를 변경하거나 체크하기 위한 매크로 함수가 존재한다.
FD_ZERO(*set) : fd_set을 초기화한다. fd_set사용하기전에 반드시 해야하는 작업이다.
#pragma comment(lib, "ws2_32")#define _WINSOCK_DEPRECATED_NO_WARNINGS#include<iostream>#include<stdio.h>#include<winsock2.h>#include<memory>#define MAX_LEN 1024 #define BACKLOG 5intmain(){
WSADATA wsaData;
// 서버
SOCKET serverSocket;
structsockaddr_inlisten_addr, accept_addr;char buf[MAX_LEN];
fd_set temp_fds, read_fds;
int readn, addr_len;
unsignedint i, fd_num = 0;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
return1;
serverSocket = socket(AF_INET, SOCK_STREAM, 0);
if (serverSocket == INVALID_SOCKET)
return1;
memset((void*)&listen_addr, 0x00, sizeof(listen_addr));
listen_addr.sin_family = AF_INET;
listen_addr.sin_port = htons(11000);
listen_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
if (bind(serverSocket, (struct sockaddr*)&listen_addr, sizeof(listen_addr)) == SOCKET_ERROR)
return1;
if (listen(serverSocket, BACKLOG) == SOCKET_ERROR)
return1;
FD_ZERO(&read_fds);
FD_SET(serverSocket, &read_fds);
printf("클라이언트로부터의 접속을 대기중입니다\n");
while (1)
{
//select 함수가 호출완료되면, 변화한 fd정보 이외의 것은 초기화된다.//따라서 기존의 fd_set 데이터를 temp_fds에 저장해야한다. (최초에는 빈 fd_set값이 저장된다)
temp_fds = read_fds;
// 별다른 타임아웃이 없으므로, select 함수로 입력이 오기전까지 Blocking되어 대기한다.
fd_num = select(0, &temp_fds, NULL, NULL, NULL);
for (i = 0; i <= temp_fds.fd_count; i++)
{
SOCKET currentSocket = temp_fds.fd_array[i];
if (FD_ISSET(currentSocket, &temp_fds))
{
// 새로 연결이 들어온 경우if (currentSocket == serverSocket )
{
addr_len = sizeof(struct sockaddr_in);
printf("새로운 소켓의 연결이 왔습니다\n");
SOCKET clientSocket = accept(serverSocket, (struct sockaddr*)&accept_addr, &addr_len);
if (clientSocket == INVALID_SOCKET)
{
printf("잘못된 소켓입니다\n");
continue;
}
// 연결온 클라이언트 소켓정보를 read_fds에 추가FD_SET(clientSocket, &read_fds);
} else { //기존에 이미 연결된 소켓으로부터 메시지를 받을때memset(buf, 0x00, MAX_LEN);
readn = recv(currentSocket, buf, MAX_LEN, 0);
if (readn <= 0)
{
printf("클라이언트와의 연결이 종료되었습니다\n");
closesocket(currentSocket);
// 연결이 끊긴 소켓을 read_fds에서 제외FD_CLR(currentSocket, &read_fds);
}
else
{
printf("%s 메시지 수신함\n", buf);
send(currentSocket, buf, readn, 0);
}
}
if (--fd_num <= 0) break;
}
}
}
closesocket(serverSocket);
WSACleanup();
return0;
}
6) Select Server 모델의 특징과 장단점 Select Server는 블록킹(Blocking)방식의 비동기(Asynchronous)식 통신을 가진다는 특징이 있다.
블록킹(Blocking) 방식: 요청한 작업이 완료되어 응답을 받을 때 까지 대기하는 방식
비동기적(Asynchronous) 방식: 요청된 순서대로 작업의 처리순서를 보장하지 않는 방식
따라서 SelectServer는 Select요청을 통해 응답을 받을때까진 블록킹이 된 채로 기다린다. 이후 Select서버에서 응답을 기다리는동안 변화한 파일들의 정보가 있다면, 한꺼번에 받아들여 처리한다. 때문에 클라이언트에서는 요청한 순서대로 응답을 보낸다는 보장이 없다는 특징을 가진다.
Select Server는 select()함수를 통해 하나의 프로세스로 여러개의 파일(네트워크에서는 클라이언트 소켓)의 상태를 조회하며 처리할 수 있다. 따라서 싱글 스레드로 동시성을 보장한다. 하지만 fd_set구조체에 저장된 파일디스크립터들의 상태를 하나하나 체크해야하므로, 수많은 파일(소켓)들을 대상으로 진행할때에는 성능이 저하된다는 단점이 있다.
이번 포스팅을 통해 하나의 I/O Model에 대해 알아보았다. 입출력모델은 Select Server외에도 Overlapped I/O 모델, IOCP 모델들이 존재하는데 이후 포스팅에서 다뤄볼 예정이다.
이전 포스팅에서는 echoServer를 통해 상호간의 통신을 하는 서버와 클라이언트를 구현해보았다. 다만 이전의 서버는 하나의 서버프로세스는 하나의 클라이언트의 처리만 진행하였다. 하지만 실제 서버는 여러개의 클라이언트를 동시다발적으로 처리해야한다. 때문에 이번 포스팅에서는 다중 접속 서버의 모델들의 종류에 대해 설명을 한다.
1) 멀티 프로세스 서버 부모 프로세스가 클라이언트의 접속이 오면, 해당 클라이언트와 데이터를 송수신할 담당할 자식 프로세스를 생성한다. 자식 프로세스는 클라이언트와의 연결이 종료되면 종료된다. 하나의 프로세스가 클라이언트에 대해 통신을 전담하기에 대용량의 데이터를 전송하기에 유리하지만, 프로세스의 생성은 큰 비용이 들기때문에 시스템 자원을 크게 사용한다는 단점이 있다.
2) 멀티 플렉싱 서버 여러개의 클라이언트로 부터 오는 입출력 정보를 하나의 전송로에 묶어서 한꺼번에 데이터를 송수신한다. 송수신하는 데이터의 크기가 작은 경우에 용이하며, 멀티 프로세스 서버보다 다수의 클라이언트를 처리하는데 유리하다. 하지만 멀티스레드 방식에 비해 많은 접속을 받지 못하며 접속자가 많을수록 클라이언트의 데이터를 체크하기위한 작업에 소요되는 시간이 길어져 부하가 생기는 단점이 있다. 멀티 플렉싱 서버는 다수의 클라이언트 정보를 배열로 묶어 관리하는 select함수를 이용하여 구현할 수 있다. 이 부분은 다음 포스팅에서 다뤄보겠다.
3) 멀티 스레드 서버
하나의 프로세스에서 각 클라이언트의 송수신을 담당하는 스레드(thread)를 생성한다. 스레드는 프로세스에 비해 생성비용이 적어 멀티 프로세스 방식의 서버보다 더 많은 클라이언트의 접속을 처리하는데 용이하다. 하지만 스레드간의 교착상태나 race컨디션 등의 위험이 있다.
한꺼번에 1명의 클라이언트를 받을 수 있으며, 한 개의 클라이언트와 통신중에 다른 클라이언트의 요청이 온다면 해당 클라이언트는 이전에 먼저 연결된 클라이언트가 종료될때까지 연결을 기다려야 한다. 서버는 연결된 클라이언트가 종료될때까지 클라이언트로부터 오는 메세지를 받고, 그대로 전송하는 작업을 한다. 3번까지 클라이언트에게 echoServer로써 기능하는 역할을 마무리하면 서버는 종료된다.
다음은 클라이언트의 코드이다. 서버와 마찬가지로 WinSock라이브러리를 초기화한 뒤 서버에 연결한다. 서버와 연결되면 클라이언트에 보내고싶은 문자열을 입력해 전송이 가능하다. 전송된 문자열은 곧 서버로부터 똑같이 되돌려받는다. 만일 quit라고 입력한다면, 클라이언트는 종료되며 서버는 다음 클라이언트를 받을 준비를 한다(3번의 서비스를 했다면 서버도 종료된다)
3) 실행 결과
먼저 서버에 첫번째 클라이언트를 연결한 뒤 메시지를 보낸 결과다
서버
1번 클라이언트
1번 클라이언트에게 메세지를 보낸대로 서버는 대답을 보내준다. 여기에서 1번 클라이언트가 종료되지 않은 시점에서 2번과 3번 클라이언트를 연결시키고, 메세지를 보낸다.
2번 클라이언트
3번 클라이언트
2번과 3번 클라이언트는 메세지를 송신해도 이미 1번 클라이언트가 선점되어 처리되고 있으므로, 메세지에 대한 답변을 받지못한채 블록킹(다음 처리를 하지못하고, 현재 작업이 끝날때까지 기다림) 되고있다.
여기에서 1번클라이언트를 종료시킨다.
서버
1번 클라이언트를 종료시키면 대기하고 있던 2번 클라이언트의 메세지가 보내진다
2번 클라이언트
3번 클라이언트
2번 클라이언트는 메세지를 받고 다음 메세지를 받을 수 있지만, 3번 클라이언트는 여전히 recv함수에서 블록킹 된 상태다.
서버
3번 클라이언트
2번 클라이언트가 종료되고 나서야 마지막으로 3번 클라이언트가 메세지를 보내고 받을 수 있다.
※ 클라이언트의 코드에서 send함수 뒤에 send함수가 완료되었다는 로그를 남기면, 2번 3번 클라이언트가 블록킹 되는 시점은 send가 아닌 recv라는 것을 알 수 있다.
윈도우 소켓으로 서버와 클라이언트의 통신을 구현해보고 알아보았다. 하지만 지금 간단하게 구현된 서버는 서비스에서 거의 사용하지 못한다. 클라이언트를 한명씩 순차적으로만 처리할 수 있다면 기다리는 다른 클라이언트들은 긴 대기시간을 가지게 된다.
서버는 빠른 응답도 중요하기 때문에 이후 포스팅에서는 지금의 구조를 개선해서, 다수의 클라이언트를 동시에 처리할 수 있는 서버(병렬 처리서버)를 다뤄볼 것이다.