지금까지 회사에서 LLM 서비스하면
웹에서 ChatGPT, Gemini 등을 사용하는 게 다였지만,
한 발 더 앞서나갈 계기가 되는 일이 있었습니다.
시스템을 사용하는 고객사에서 이런 요청이 왔더라구요?
어떤 내용을 작성하고 AI수정? 버튼을 누르면 AI가 수정한 내용을 보여달라는 것인데..
회사 차원에서 첫시도를 하게 된 AI 접목 건이라 기대됐습니다 🙊

그렇게 대형 프로젝트급은 아니라서 개발은 저 혼자 진행하게 됐고
LLM API 토큰의 구매와 사내 AI 서버 관리를 담당하는 팀과 협업했습니다..
개요

전반적인 서비스의 형태는 대충 이런 느낌인데..
간단하죠?
저거 내용은 더미이고,, 디자인도 블로그 게시용으로 급조한 거라 좀 구립니다..
사용자가 원래 본인이 사용하던 화면에서 문서 작성 업무를 진행하고 AI 수정 버튼을 누르면 우측에 동적으로 페이지가 밀어나오면서 LLM이 작성한 내용이 보이게 되는 것이죠..
진짜 별 건 없긴한데 쌩판 처음하는 거라
+ 도구 이슈로 저 우측 동적 페이지 만드는 데 시간 좀 썼네요..
C에서 HTTP 요청 구현하기
그렇지만 오늘 말씀드릴 건 동적 페이지 작성에 관한 건은 아니고
C에서.. HTTP 요청 보내본 건 처음이라.. 한 번 써보려 합니다.
근데 또 웃긴 건 아무리 회사 사업 방향이 그렇게 다각적이지 않다해도
HTTP 요청을 보낸 레퍼런스가 한 손에 꼽네요,,
진짜 다 뒤져봤는데 이번 케이스에 가용할 만한 케이스가 전무한 상황!
결국 사내에 구비된 HTTP 라이브러리와 그렇지 않은 부분부터 리서치가 시작되었습니다! 😂
음,, 그런데,, 왜 다 구비되어 있습니까..?
한 15년 전에 SI회사가 만들고 갈 때 진짜 잘 만들고 가셨네요..! 😃
라고 생각을 했던 때가 있었죠..
첫 시도) socket 트기
C에서는 매우 간단하게 통신을 할 수 있습니다.
거기다 저희는 이번에 클라이언트 역할에 단일 요청이라 더더욱 평이한 작업이 될 수 있었습니다.
#include <string.h>
#include <netdb.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
int open_tcp_client_socket(char *host, int port)
{
struct hostent *server_host;
struct sockaddr_in server_info;
int num_sock;
// hostent 구조체를 리턴해주는 변환 함수 사용 (netdb.h)
server_host = gethostbyname(host);
if ( server_host == NULL ) {
// 에러 리턴
};
// server_host 변수와 주어진 인자로 sockaddr_in 변수 설정
memcpy((char *)&server_info.sin_addr, server_host->h_addr, server_host->h_length);
server_info.sin_port = htons(port);
server_info.sin_family = AF_INET;
errno = 0;
// 소켓을 열고 해당 파일 디스크립터를 받아온다.
// 자세한 내용은 OS 레벨로,,
num_sock = socket(AF_INET, SOCK_STREAM, 0);
if ( num_sock < 0 ){
// 에러 리턴, 필요하면 errno를 활용 예) strerror(errno)
};
errno = 0;
// 우리의 소켓과 서버(대상)의 소켓을 연결 시도
// 당연히 서버 쪽 소켓도 오픈된 상태여야 겠죠?
if ( connect(num_sock, (struct sockaddr*) &server_info, sizeof(server_info)) < 0 ) {
// 에러 리턴, 필요하면 errno를 활용 예) strerror(errno)
};
}
그냥 진짜 튜토리얼 대로만 해서 소켓 정보를 받아왔습니다,,
그렇다면 여길 통해 HTTP 요청을 보내기만 하면 됩니다!
HTTP 요청 Header&Body 작성
정상적인 HTTP 요청을 보내려면 몇가지 조건을 지켜야 하는데,,
write_string_with_realloc(&http_header
, strlen(http_header)
, "POST %s HTTP/1.0\r\n"
"Host: %s:%d\r\n"
"Content-Type: application/json; charset=euc-kr\r\n"
"Accept-Charset: euc-kr\r\n"
"Content-Length: %zu\r\n"
"Authorization: Bearer %s\r\n"
"\r\n"
"%s"
, PATH
, HOST
, PORT
, strlen(http_body)
, getenv("AI_API_KEY")
, http_body);
write_string_with_realloc ← 이건 제가 만든 커스텀 함수인데..
그냥 snprintf랑 realloc을 합친 함수입니다..
C의 문자열 컨트롤이 너무 귀찮아서.. 그냥 만들었네요;; 😂🥹
맨 하단에 함수 소스 첨부해두겠습니다.
아무튼 보시는 것처럼
Header 명세를 지켜줘야합니다.
요청 방식) POST HTTP /1.0
요청 주소) HOST: 127.0.0.1:8080
나머지 항목은 상황 따라 다른데
Body 길이) Content-Length: 1025
이거 빼먹으면 안돼요,, 이거 빼먹어서 자꾸 에러나고,,
아무튼 HTTP 요청이 완성이 되었습니다.
요청 전송 select, send
일단,, 회사 소스 내에 준비된 대로 최대한 진행했습니다..
다른 곳은 어떤지 모르겠는데 저희는 일단 select→send로
select로 통신 상태보고 send로 작성해여 보냅니다..
이것도 별 건 없어용
#include <sys/time.h>
#include <sys/select.h>
#include <errno.h>
int send_http_request(int num_sock, char *request_content, int request_size)
{
int left_size, written_size;
char *request_ptr;
fd_set fds;
FD_ZERO(&fds);
FD_SET(num_sock, &fds);
// 소켓 내용 작성에 사용할 로컬 문자 포인터를 설정해준다.
request_ptr = request_content;
// 최초로 주어진 요청 내용의 크기를 초기화해준다.
left_size = request_size;
while (left_size > 0)
{
// 여기서 리턴값 0은 시간 초과이고,
// 우리 소켓이 준비된지 확인하는 거라 timeval 0으로 즉시 반환을 설정함.
// 그러므로 리턴값 0도 에러 처리를 한다.
if ( select(num_sock+1, (fd_set *)0, &fds, (fd_set *)0, (struct timeval*)0) <= 0 ) {
// 에러 리턴
}
if ( (written_size = send(num_sock, request_ptr, left_size, 0)) <= 0 ) {
if ( written_size < 0 ) {
// 에러 처리 역시 errno 사용 가능
};
// 이유는 잘 모르겠지만 쓰기 중에도 인터럽트가 발생할 수 있는 듯 하다.
// 그냥 이렇게 구현했길래 가져왔다.
if (errno == EINTR) {
written_size = 0;
}
};
// 요청 보낸 만큼 남은 내용 수를 줄여주고
left_size -= written_size;
// 요청 내용에서의 포인터 위치도 옮겨준다.
request_ptr += written_size;
}
}
응답 수신 poll, recv
그러고 나서 응답 수신을 딱 하면 되는데..
응답 수신용 사내 소스를 보니까 이게..
2가지 문제가 있었습니다..
1. HTTP용 응답 수신 함수가 완성되어 있지 않았음. (일부 요청만 정상 처리 가능한,,)
2. write의 경우처럼 단발성 읽기 후에 응답의 상태와 관계없이 통신을 끝내버림
그런데 저희가 지금 사용할 건..
LLM의 답변 생성 기능인데.. 이게 시간이 걸린단 말이에요?
그렇다고 저희 시스템 구조상 사용자가 시스템을 통해 요청을 넣고 그거 다되면 알림이 오는 방식으로는 할 수 없었습니다..
즉,
무조건 요청 넣으면 LLM 응답에 30초가 걸리든 60초가 걸리든
그 자리(해당 스레드의 동작 안)에서 응답을 받아내야 된다는 겁니다;;

그래서 어떤 방식으로 했냐면,, 이것도 약간 막무가내지만..
polling 방식으로 구성해서 응답 다 받을 때까지 계속 체크해주는 것입니다.. 😂😂

다 받으면 다 받았다고 이벤트가 뜨는데
그때가 되어야,, 수신을 종료하는 방식이죠..
#include <poll.h>
#include <sys/socket.h>
#include <errno.h>
int recv_http_response(int num_sock, char **response_content)
{
int ret, read_size;
char buffer[1];
// 소켓 하나만 넣을 거라 하나면 선언
struct pollfd pfd[1];
errno = 0;
// 소켓이 읽힐 준비가 됐는지 확인,,
while (1)
{
// 입력 대기 파일을 커널에 심어 놓고 2초 동안 깨우길 기다림
ret = poll(&pfd, 1, 2000);
// 아직 끝난 게 아니라면 다시 재움
if ( ret == 0 ) continue;
else if ( ret < 0 ) {
// 에러 리턴
}
// 소켓에 입력 이벤트가 발생하면 상태 체크 종료
if ( pfd[0].revents & POLLIN ) break;
else {
// 에러 리턴
}
}
do {
errno = 0;
// 한자한자 소켓으로부터 읽어들임
// 이것도 내부 표준,,
read_size = recv(num_sock, buffer, 1, 0);
// 수신 중 에러가 났을 때
if ( read_size < 0 ) {
// 에러 리턴
}
// 커스텀 문자열 확장 함수를 사용해준다.
ret = write_string_with_realloc(response_content
, strlen(*response_content)
, "%s"
, buffer);
if ( ret < 0 ) {
// 에러 리턴
}
} while (read_size > 0);
}
네.. 아무튼,, 원래는 아까 말씀드린 1번의 HTTP 수신 방식으로 하는 것도 추가해야하는데,
그것도 그거대로 소스가 길어서,,
여기선 못 담겠네요.. 😂😂
아무튼 적당히 좋게좋게 잘 마무리가 되었습니다..
후기
실전에서 소켓 프로그래밍을 해보는 건 처음이라..
인프라가 갖춰있다보니까 생각보다 너무 쉬워서 어어?? 이거 맞니? 😯
이런 생각도 들었고,,
AI 툴로 이슈 트래킹이나 해결도 너무 쉬워졌고,
이런 걸 보면,,
(이 정도도 매우 얕은 수준이지만)
기술은 이제 정말 도구/수단에 불과한 것 같은 생각이 들긴하네요.
굳이 거대한 IT 업력이 있지 않아도 마음만 먹으면 다들 구축할 수 있는,,
팀장님은 이런 것들을 구현해내려면 ㅠㅠ
옛날에는 책 뒤져가면서 꼬박 며칠이 걸렸다 하셨는데 😂
번외) write_string_with_realloc 함수
제가 이번 기회에 얻은 수확 중 하나인 C 커스텀 문자열 함수입니다~
그 동안의 C 계열 문자열 처리가 너무 까다로워서,,
좀 더 편안한 방식이 없을까 고민하다 만들었네요..
가능하다면 올해나 내년 정도에,
C에서 이보다 더 편한 문자열 처리가 가능하게 해보고 싶네요..
#include <stdarg.h>
// 문자열에 문자열을 더해주는 함수
// 정적문자열이 아니라 동적문자열을 넣어줌으로써 계속 확장 적용할 수 있다.
// dest : 대상 문자열의 맨 앞 위치 포인터
// dest_len : 문자열 추가가 시작될 위치
// fmt : snprintf에 적용될 포맷 "%d %s" 같은;;
// 그리고 그 fmt에서 지정한 각 변수들
// return : 최종 문자열 사이즈
int write_string_with_realloc(char **dest, size_t dest_len, const char *fmt, ...)
{
int required_size;
va_list args;
va_start(args, fmt);
required_size = vsnprintf(NULL, 0, fmt, args);
va_end(args);
if (required_size < 0)
{
// 에러 리턴
}
char *temp_ptr = realloc(*dest, dest_len + required_size + 1);
if (!temp_ptr)
{
// 에러 리턴
}
*dest = temp_ptr;
va_start(args, fmt);
vsnprintf(*dest + dest_len, required_size + 1, fmt, args);
va_end(args);
return dest_len + required_size;
}'회사생활' 카테고리의 다른 글
| [C/C++] 런타임 동적 테이블로 Oracle 쿼리 최적화하기 (0) | 2025.11.09 |
|---|---|
| [C/C++] 런타임 임시 저장용 동적 테이블 만들기 (0) | 2025.10.12 |
| [XPlatform] Grid selecttype trick (making cell work like row) (0) | 2025.10.06 |
| [ActiveX] Excel.Application 속도 개선2 - 동적 행 및 열 추가 (8) | 2025.08.16 |
| [ActiveX] Excel.Application 속도 개선 (2) | 2025.05.24 |