1. 기본 설정
build.gradle
의존성 설정
//Spring MVC 및 RESTful 웹 서비스를 만들기 위한 기본
implementation 'org.springframework.boot:spring-boot-starter-web'
//Spring Boot 애플리케이션을 테스트하기 위한 기본 의존성
testImplementation 'org.springframework.boot:spring-boot-starter-test'
//Spring Data JPA를 사용하여 데이터베이스와 상호작용하기 위한 의존성
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
//WebSocket을 사용하여 실시간 양방향 통신을 구현하기 위한 의존성
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-websocket', version: '3.3.1'
// mustache 탬플릿을 위한 의존
implementation 'org.springframework.boot:spring-boot-starter-mustache'
application.yml
yml 설정
spring:
datasource:
initialize: false
openai:
api:
key: ${OPEN_API_KEY}
_core/config/WebSocketConfig
웹 소켓 연결 설정
package org.example.chatai._core.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
// 설정 클래
@Configuration
// STOMP 프로토콜을 사용하여 WebSocket을 통해 메시지를 주고받을 수 있음.
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
// 메세지 라우터
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// /topic 이 붙은 목적지를 구독하는 클라이언트에게 메세지 전달
config.enableSimpleBroker("/topic");
// 클라이언트가 메세지를 보낼때 목적지의 접두사
config.setApplicationDestinationPrefixes("/app");
}
// stomp 엔드포인트 등록. 엔드포인트를 통해 WebSocket 연결 시작
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// /chat-websocket 를 엔드포인트로 설정. 웹소켓 연결 시작
registry.addEndpoint("/chat-websocket").withSockJS();
}
}
2. OpenAI 통신 설정
openAI/OpenAIService
openAI 와 통신
package org.example.chatai.OpenAI;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class OpenAIService {
// application.yml 에서 api key 주입받음
@Value("${openai.api.key}")
private String apiKey;
// 요청 api 주소
private final String apiUrl = "https://api.openai.com/v1/chat/completions";
// RESTful 웹 서비스 사용위해
private final RestTemplate restTemplate = new RestTemplate();
// 질문 받고 응답
public String askOpenAI(String prompt) {
// 요청할 모델 지정
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", "gpt-3.5-turbo");
requestBody.put("messages", List.of(
Map.of("role", "user", "content", prompt)
));
// 헤더 설정
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + apiKey);
// 헤더 바디를 포함한 요청 객체 생성
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
// api 요청 후 응답 받음
ResponseEntity<Map> response = restTemplate.postForEntity(apiUrl, entity, Map.class);
// 응답된 메세지 반환
if (response.getBody() != null) {
List<Map<String, Object>> choices = (List<Map<String, Object>>) response.getBody().get("choices");
if (choices != null && !choices.isEmpty()) {
Map<String, Object> message = (Map<String, Object>) choices.get(0).get("message");
return message != null ? (String) message.get("content") : "No response content";
}
}
return "Error: No response from OpenAI";
}
}
3. 채팅 설정
chat/ChatController
클라이언트로부터 오는 메세지 처리
package org.example.chatai.chat;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.stereotype.Controller;
@RequiredArgsConstructor
@Controller
public class ChatController {
private final ChatService chatService;
// 클라이언트가 입력한 메세지 서버로 전달
@MessageMapping("/chat.sendMessage")
public void sendMessage(@Payload ChatRequest.ChatMessageDTO chatMessage) {
chatService.processMessage(chatMessage);
}
// 클라이언트 정보 저장
@MessageMapping("/chat.addUser")
public void addUser(@Payload ChatRequest.ChatMessageDTO chatMessage, SimpMessageHeaderAccessor headerAccessor) {
headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());
}
}
chat/ChatService
클라이언트에게 받은 메세지를 openAI 에 전달, 응답받은 정보를 클라이언트에게 전달
package org.example.chatai.chat;
import lombok.RequiredArgsConstructor;
import org.example.chatai.OpenAI.OpenAIService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
public class ChatService {
// STOMP 메시지를 클라이언트로 보내기 위한 템플릿 객체
private final SimpMessagingTemplate messagingTemplate;
private final OpenAIService openAIService;
// 클라이언트로부터 받은 메시지를 처
public void processMessage(ChatRequest.ChatMessageDTO requestDTO) {
// 클라이언트로부터 입력받은 메세지 저장
String userMessage = requestDTO.getContent();
// userMessage 를 openAI에 전달 후 받은 응답을 aiResponse 에 저장
String aiResponse = openAIService.askOpenAI(userMessage);
// 응답받은 메세지를 DTO에 담
ChatResponse.ChatMessageDTO aiMessage = new ChatResponse.ChatMessageDTO(aiResponse);
// /topic/messages 경로로 전달해 클라이언트가 메세지를 전달받음
messagingTemplate.convertAndSend("/topic/messages", aiMessage);
}
}
chat/ChatRequest
요청 DTO
package org.example.chatai.chat;
import lombok.Data;
public class ChatRequest {
@Data
public static class ChatMessageDTO {
// 요청 내용
private String content;
// 클라이언트 이름
private String sender;
}
}
chat/ChatResponse
응답 DTO
package org.example.chatai.chat;
import lombok.Data;
public class ChatResponse {
@Data
public static class ChatMessageDTO {
private String content;
private String sender;
public ChatMessageDTO(String aiResponse) {
this.content = aiResponse; // 응답받은 내용
this.sender = "AI"; // 발신자를 AI 로 지정
}
}
}
4. 화면
HomeController
package org.example.chatai;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/")
public String index() {
return "index";
}
}
index.mustache 전체 코드
<!DOCTYPE html>
<html>
<head>
<title>AI Chatbot Example</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.4/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<style>
.message {
padding: 5px;
margin: 5px;
border-radius: 5px;
max-width: 80%;
}
.user-message {
background-color: #DCF8C6;
text-align: right;
float: right;
clear: both;
}
.ai-message {
background-color: #EDEDED;
text-align: left;
float: left;
clear: both;
}
#chatArea {
height: 300px;
overflow-y: scroll;
border: 1px solid black;
padding: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>Chatbot Example</h1>
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#chatModal">
Open Chat
</button>
</div>
<!-- Modal -->
<div class="modal fade" id="chatModal" tabindex="-1" aria-labelledby="chatModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="chatModalLabel">Chat with AI</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div>
<label for="username">Username:</label>
<input type="text" id="username"/>
<button id="connectBtn" class="btn btn-secondary">Connect</button>
</div>
<br/>
<div id="chatArea"></div>
<br/>
<div>
<textarea id="message" rows="3" cols="40"></textarea>
<br/>
<button id="sendBtn" class="btn btn-primary">Send</button>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
<script type="text/javascript">
var stompClient = null;
function connect() {
var socket = new SockJS('/chat-websocket');
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/messages', function(response) {
showMessage(JSON.parse(response.body));
});
});
}
function sendMessage() {
var messageContent = $('#message').val();
var username = $('#username').val();
var chatMessage = {
content: messageContent,
sender: username
};
stompClient.send('/app/chat.sendMessage', {}, JSON.stringify(chatMessage));
$('#message').val('');
showUserMessage(chatMessage); // 사용자의 메시지를 바로 표시
}
function showMessage(message) {
var messageElement = $('<div class="message ai-message"></div>');
messageElement.text(message.sender + ': ' + message.content);
$('#chatArea').append(messageElement);
$('#chatArea').scrollTop($('#chatArea')[0].scrollHeight); // 스크롤을 아래로
}
function showUserMessage(message) {
var messageElement = $('<div class="message user-message"></div>');
messageElement.text(message.sender + ': ' + message.content);
$('#chatArea').append(messageElement);
$('#chatArea').scrollTop($('#chatArea')[0].scrollHeight); // 스크롤을 아래로
}
$(function () {
$('form').on('submit', function (e) {
e.preventDefault();
});
$('#connectBtn').click(function() {
connect();
});
$('#sendBtn').click(function() {
sendMessage();
});
});
</script>
index.mustache 자바스크립트 부분
<script type="text/javascript">
var stompClient = null;
// connect 버튼 입력 시 실행, 웹소켓 연결
function connect() {
var socket = new SockJS('/chat-websocket');
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/messages', function(response) {
showMessage(JSON.parse(response.body));
});
});
}
// 메세지 전달
function sendMessage() {
var messageContent = $('#message').val();
var username = $('#username').val();
var chatMessage = {
content: messageContent,
sender: username
};
stompClient.send('/app/chat.sendMessage', {}, JSON.stringify(chatMessage));
$('#message').val('');
showUserMessage(chatMessage); // 사용자의 메시지를 바로 표시
}
// 메세지 표시
function showMessage(message) {
var messageElement = $('<div class="message ai-message"></div>');
messageElement.text(message.sender + ': ' + message.content);
$('#chatArea').append(messageElement);
$('#chatArea').scrollTop($('#chatArea')[0].scrollHeight); // 스크롤을 아래로
}
// 클라이언트 정보 표시
function showUserMessage(message) {
var messageElement = $('<div class="message user-message"></div>');
messageElement.text(message.sender + ': ' + message.content);
$('#chatArea').append(messageElement);
$('#chatArea').scrollTop($('#chatArea')[0].scrollHeight); // 스크롤을 아래로
}
$(function () {
$('form').on('submit', function (e) {
e.preventDefault();
});
$('#connectBtn').click(function() {
connect();
});
$('#sendBtn').click(function() {
sendMessage();
});
});
</script>

Share article