이전 포스트들과 이어집니다
1. [Spring/WebSocket] 순수 WebSocket으로 채팅방 만들기
2. [Spring/WebSocket] WebSocket + STOMP으로 귓속말 가능한 채팅방 만들기
3. [Spring/WebSocket] WebSocket + STOMP + DOCKER 실습 : 여러 서버 띄우기
4. [Spring/WebSocket] 웹소켓을 활용한 GPT AI 챗봇 만들기

❓브라우저와 GPT 사이에 웹소켓이 중간에 끼는 이유
- 브라우저에서 바로 GPT에 직접 보내게 되면 클라이언트에 API 키가 노출되며, 로깅과 제어가 불가하다.
- 중간에 웹소켓이 끼게 되면 API 키 노출을 방지할 수 있고, 실시간 응답이 가능하다.

curl https://api.openai.com/v1/responses \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
"model": "gpt-4.1",
"input": "Write a one-sentence bedtime story about a unicorn."
}'



@Service
public class GPTService {
//json 문자열 <-> 자바객체, json 객체
private final ObjectMapper mapper = new ObjectMapper();
//@Value("${openai.api-key}")
private String openaiApiKey;
public String getMessage(String message) throws Exception {
try {
//API 호출을 위한 본문 작성
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", "gpt-4o");
requestBody.put("input", message);
//http 요청 작성
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.openai.com/v1/responses"))
.header("Authorization", "Bearer {API키}")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(requestBody))) //본문 삽입
.build();
//요청 전송 및 응답
HttpClient client = HttpClient.newHttpClient();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
//응답을 JSON으로 파싱
JsonNode jsonNode = mapper.readTree(response.body());
System.out.println("get 응답 : " + jsonNode);
//메시지 부분만 추출하여 반환(응답형태가 Json인데 json문자열 복잡함 -> 거기서 지피티의 대답만 추출)
String gptMessageResponse = jsonNode.get("output").get(0).get("content").get(0).get("text").asText();
return gptMessageResponse;
//응답이 오지않으면 예외처리
} catch (Exception e) {
return "❗문제가 발생했습니다. 다시 시도해주세요.";
}
}
}
//환경 변수를 받아서 화면에 출력
@Value("${PROJECT_NAME:web Server}")
private String instansName;
private final RedisPublisher redisPublisher;
private ObjectMapper objectMapper = new ObjectMapper();
private final GPTService gptService;
//단일 브로드 캐스트(방 동적 생성 불가)
@MessageMapping("/gpt")
public void sendMessageGPT(ChatMessage message) throws Exception{
template.convertAndSend("/topic/gpt",message);//내가 보낸 메시지 출력
//사용자가 보낸 메시지를 받음. gpt 목적지 반환
String getResponse = gptService.getMessage(message.getMessage());
ChatMessage chatMessage = new ChatMessage("난 GPT ", getResponse);
template.convertAndSend("/topic/gpt", chatMessage);
}
public ChatMessage(String message, String from) {
this.from =from;
this.message = message;
}
public void registerStompEndpoints(StompEndpointRegistry registry) {
//채팅방 엔드포인트
registry.addEndpoint("/ws-chat")
.setHandshakeHandler(new CustomHandshakeHandler()) //귓속말 가능하게 해줌
.setAllowedOriginPatterns("*");
//지피티 엔드포인트
registry.addEndpoint("/ws-gpt")
.setAllowedOriginPatterns("*");
}


worker_processes 1;
events {
worker_connections 1024;
}
http {
upstream spring_backend {
server backend1:8080;
server backend2:8080;
server backend3:8080;
}
server {
listen 80;
location / {
proxy_pass http://spring_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /ws-chat {
proxy_pass http://spring_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /ws-gpt {
proxy_pass http://spring_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
