1. AI 챗봇 구현
OpenAI 접속 + Postman 세팅
•
Postman에 다음과 같이 세팅
HTTP Method POST로 설정 후 Send 클릭한다.
•
사용 가능한 모델
◦
gpt-4.1
◦
gpt-4o
◦
o4-mini
•
gpt-4.1
•
gpt-4o
•
o4-mini
코드 작성 및 수정
•
ChatMessage
package org.example.backendproject.stompwebsocket.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessage {
private String message;
private String from;
private String to; // 귓속말을 받을 사람
private String roomId; // 방 id
// 생성자 추가
public ChatMessage(String from, String message) {
this.from = from;
this.message = message;
}
}
Java
복사
•
ChatController
package org.example.backendproject.stompwebsocket.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.example.backendproject.stompwebsocket.dto.ChatMessage;
import org.example.backendproject.stompwebsocket.gpt.GPTService;
import org.example.backendproject.stompwebsocket.redis.RedisPublisher;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
@Controller
@RequiredArgsConstructor
public class ChatController {
@Value("${PROJECT_NAME:web Server}")
private String instanceName;
private final RedisPublisher redisPublisher;
private ObjectMapper objectMapper = new ObjectMapper();
// 서버가 클라이언트에게 수동으로 메세지를 보낼 수 있도록 하는 클래스
private final SimpMessagingTemplate template;
private final GPTService gptService;
// 단일 브로드캐스트 (동적으로 방 생성이 안 됨)
// GPTService 실행하기 위한 sendMessageGPT 메서드 작성
@MessageMapping("/gpt")
public void sendMessageGPT(ChatMessage message) throws Exception {
String getResponse = gptService.gptMessage(message.getMessage());
ChatMessage chatMessage = new ChatMessage("난 GPT", getResponse);
template.convertAndSend("/topic/gpt", chatMessage);
}
// 동적으로 방 생성 가능
@MessageMapping("/chat.sendMessage")
public void sendMessage(ChatMessage message) throws JsonProcessingException {
message.setMessage(instanceName + " " + message.getMessage());
String channel = null;
String msg = null;
if (message.getTo() != null && !message.getTo().isEmpty()) {
// 귓속말
// 내 아이디로 귓속말 경로를 활성화 함
channel = "private." + message.getRoomId();
msg = objectMapper.writeValueAsString(message);
} else {
// 일반 메시지
// message에서 roomId를 추출해서 해당 roomId를 구독하고 있는 클라이언트에게 메세지를 전달
channel = "room." + message.getRoomId();
msg = objectMapper.writeValueAsString(message);
}
redisPublisher.publish(channel, msg);
}
}
Java
복사
•
WebSokcetConfig
package org.example.backendproject.stompwebsocket.config;
import org.example.backendproject.stompwebsocket.handler.CustomHandshakeHandler;
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
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
// ... 중략 ...
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-chat")
.setHandshakeHandler(new CustomHandshakeHandler())
.setAllowedOriginPatterns("*");
// gpt 전용 채팅 EndPoint 설정
registry.addEndpoint("/ws-gpt")
.setAllowedOriginPatterns("*");
}
}
Java
복사
•
localhost:8080 접속 - front 코드는 별도로 구현
•
GPT AI 챗봇 버튼 클릭
•
GPT 답장 확인
•
nginx.conf 에서 gpt 관련 엔드포인트 추가한 이후 docker-compose.yml 파일 실행
2. Jenkins
초기 세팅
•
현재 파일 구조 확인 후 CI/CD 폴더 생성 > jenkins 관련 폴더 및 파일 생성
◦
git 폴더 > Jenkinsfile
◦
local 폴더 > Jenkinsfile
◦
docker-compose.yml
◦
Dockerfile
•
git 폴더 > Jenkinsfile
pipeline {
agent any
environment {
PROJECT_DIR = 'backendProject' // 백앤드 서버 프로젝트 폴더
COMPOSE_DIR = '.' // 프로젝트 루트 폴더에서 docker-compose.yml 실행
IMAGE_NAME = 'backend'
}
stages {
stage('Cleanup Containers') {
steps {
dir("${COMPOSE_DIR}") {
sh 'docker-compose down'
}
}
}
stage('Build') {
steps {
dir("${PROJECT_DIR}") {
sh 'chmod +x gradlew'
sh './gradlew clean build'
}
}
}
stage('Docker Build') {
steps {
dir("${PROJECT_DIR}") {
sh "docker build -t ${IMAGE_NAME} ."
}
}
}
stage('Compose Up') {
steps {
dir("${COMPOSE_DIR}") {
sh 'pwd'
sh 'ls -l ./nginx/nginx.conf'
sh 'docker-compose up -d --build'
}
}
}
stage('Restart Nginx') {
steps {
sh 'docker restart nginx || true'
}
}
}
}
Shell
복사
•
local 폴더 > Jenkinsfile
pipeline {
agent any
environment {
PROJECT_DIR = '/var/jenkins_home/workspace/backend5_Test_local/backendProject' // 백앤드 서버 프로젝트 폴더
COMPOSE_DIR = '/var/jenkins_home/workspace/backend5_Test_local' // 프로젝트 루트 폴더에서 docker-compose.yml 실행
IMAGE_NAME = 'backend'
}
stages {
stage('Cleanup Containers') {
steps {
dir("${COMPOSE_DIR}") {
sh 'docker-compose down'
}
}
}
stage('Build') {
steps {
dir("${PROJECT_DIR}") {
sh 'chmod +x gradlew'
sh './gradlew clean build'
}
}
}
stage('Docker Build') {
steps {
dir("${PROJECT_DIR}") {
sh "docker build -t ${IMAGE_NAME} ."
}
}
}
stage('Compose Up') {
steps {
dir("${COMPOSE_DIR}") {
sh 'pwd'
sh 'ls -l ./nginx/nginx.conf'
sh 'docker-compose up -d --build'
}
}
}
stage('Restart Nginx') {
steps {
sh 'docker restart nginx || true'
}
}
}
}
Shell
복사
•
CI/CD 폴더 > docker-compose.yml
services:
jenkins:
build: .
container_name: jenkins
ports:
- "7070:8080"
- "50000:50000"
user: root
volumes:
- ./volumes/jenkins:/var/jenkins_home
- /var/run/docker.sock:/var/run/docker.sock
# 이 경로는 자신의 PC의 경로로 입력해야 함
# ex) {상위 폴더 경로} - /Users/test/Desktop/project/backend5
- ${상위 폴더 경로}:/var/jenkins_home/workspace/backend5_Test_local # 프로젝트 연결
restart: unless-stopped
YAML
복사
❗️진행 중에 발생한 에러 해결
build: .. ⇒ build: . 으로 변경 후 Dockerfile 내용 정상적으로 동작
•
CI/CD 폴더 > Dockerfile
FROM jenkins/jenkins:lts-jdk17
USER root
# Java 17 + Docker CLI 설치
RUN apt-get update && \
apt-get install -y \
docker.io \
docker-compose && \
apt-get clean
# Docker 명령어가 없다고 나올 수 있으니 /usr/bin에 심볼릭 링크도 설정 (보완책)
RUN ln -s /usr/bin/docker.io /usr/bin/docker || true
# Jenkins 유저도 docker 실행할 수 있도록 그룹 추가
RUN usermod -aG docker jenkins
USER jenkins
Docker
복사
•
진행 순서
◦
Jenkins 파일이 위치한 폴더로 이동
▪
cd cicd > cd jenkins 명령어 입력
▪
docker compose up -d 명령어 입력
•
Jenkins 접속 확인 (localhost:7070)
•
Jenkins 초기 비밀번호 추출하고 입력한 다음 Customize Jenkins 에서 ‘Install suggested plugins’ 클릭
docker exec -it jenkins cat /var/jenkins_home/secrets/initialAdminPassword
Shell
복사
•
필요한 정보들 입력 (계정: root/1234)
•
Instance Configuration > “Save and Finish” 클릭
•
시작!
새로운 Item 생성
파이프라인 실행
•
로컬에서 실행할 파이프라인 스크립트 작성
pipeline {
agent any
environment {
PROJECT_DIR = '/var/jenkins_home/workspace/backend5_Test_local/backendProject' // 백앤드 서버 프로젝트 폴더
COMPOSE_DIR = '/var/jenkins_home/workspace/backend5_Test_local' // 프로젝트 루트 폴더에서 docker-compose.yml 실행
IMAGE_NAME = 'backend'
}
stages {
stage('Cleanup Containers') {
steps {
dir("${COMPOSE_DIR}") {
sh 'docker-compose down'
}
}
}
stage('Build') {
steps {
dir("${PROJECT_DIR}") {
sh 'chmod +x gradlew'
sh './gradlew clean build'
}
}
}
stage('Docker Build') {
steps {
dir("${PROJECT_DIR}") {
sh "docker build -t ${IMAGE_NAME} ."
}
}
}
stage('Compose Up') {
steps {
dir("${COMPOSE_DIR}") {
sh 'pwd'
sh 'ls -l ./nginx/nginx.conf'
sh 'docker-compose up -d --build'
}
}
}
stage('Restart Nginx') {
steps {
sh 'docker restart nginx || true'
}
}
}
}
Shell
복사
•
Jenkins Container에 접속 후 workspace 폴더로 이동
◦
컨테이너 접속: docker exec -it jenkins /bin/bash
◦
cd /var/jenkins_home/workspace/
•
ls 입력 후 이렇게 나와야 함
Jenkins 배포 자동화 진행 (로컬에 배포)
•
Nginx 설정을 위해 다음과 같이 세팅한다.
◦
nginx > nginx.conf 추가
FROM nginx:latest
# 기존 설정 제거 (선택적)
RUN rm /etc/nginx/nginx.conf
# 커스텀 nginx.conf 복사
COPY ./nginx.conf /etc/nginx/nginx.conf
Docker
복사
•
만약에 중간에 에러가 발생한다면 몇 가지를 고려해야 한다.
◦
build.gradle 에서 H2 Database 설정을 추가한다.
▪
Jenkins Test 진행 시 TestDB가 필요하다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// 20250620 - H2 database 추가
testImplementation 'com.h2database:h2'
Java
복사
◦
인텔리제이에서 test폴더 쪽에 resource 폴더를 만든다.
▪
application-test.properties 를 만들고 다음과 같이 작성한다.
•
기존의 redis, mysql 설정을 삭제하고 다음과 같이 작성
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
Shell
복사
▪
Test 클래스에 다음과 같이 작성한다.
package org.example.backendproject;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
@ActiveProfiles("test") // 이 부분 추가
@SpringBootTest
class BackendProjectApplicationTests {
@Test
void contextLoads() {
}
}
Java
복사
▪
Redis 설정 해제
package org.example.backendproject.stompwebsocket.redis;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
@Profile("!test") // 이 부분 추가
@RequiredArgsConstructor
@Configuration
public class RedisConfig {
private final RedisSubscriber redisSubscriber;
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory redisConnectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
container.addMessageListener(new MessageListenerAdapter(redisSubscriber), new PatternTopic("room.*"));
container.addMessageListener(new MessageListenerAdapter(redisSubscriber), new PatternTopic("private.*")); // 귓속말
return container;
}
}
Java
복사
•
“지금 빌드” 클릭
Started by user codesche
[Pipeline] Start of Pipeline
[Pipeline] node
Running on Jenkins in /var/jenkins_home/workspace/backend5_Test_local
[Pipeline] {
[Pipeline] withEnv
[Pipeline] {
[Pipeline] stage
[Pipeline] { (Cleanup Containers)
[Pipeline] dir
Running in /var/jenkins_home/workspace/backend5_Test_local
[Pipeline] {
[Pipeline] sh
+ docker-compose down
Stopping nginx ...
Stopping redis ...
Stopping database ...
Stopping nginx ... done
Stopping redis ... done
Stopping database ... done
Removing nginx ...
Removing backend2 ...
Removing backend1 ...
Removing backend3 ...
Removing redis ...
Removing database ...
Removing nginx ... done
Removing backend3 ... done
Removing database ... done
Removing backend2 ... done
Removing backend1 ... done
Removing redis ... done
Removing network backend5_test_local_default
[Pipeline] }
[Pipeline] // dir
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Build)
[Pipeline] dir
Running in /var/jenkins_home/workspace/backend5_Test_local/backendProject
[Pipeline] {
[Pipeline] sh
+ chmod +x gradlew
[Pipeline] sh
+ ./gradlew clean build
Starting a Gradle Daemon (subsequent builds will be faster)
> Task :clean
> Task :compileJava
> Task :processResources
> Task :classes
> Task :resolveMainClassName
> Task :bootJar
> Task :jar
> Task :assemble
> Task :compileTestJava
> Task :processTestResources
> Task :testClasses
2025-06-20T13:52:06.606Z INFO 996 --- [backendProject] [ionShutdownHook] o.s.m.s.b.SimpleBrokerMessageHandler : Stopping...
2025-06-20T13:52:06.606Z INFO 996 --- [backendProject] [ionShutdownHook] o.s.m.s.b.SimpleBrokerMessageHandler : BrokerAvailabilityEvent[available=false, SimpleBrokerMessageHandler [org.springframework.messaging.simp.broker.DefaultSubscriptionRegistry@69ca71b3]]
2025-06-20T13:52:06.606Z INFO 996 --- [backendProject] [ionShutdownHook] o.s.m.s.b.SimpleBrokerMessageHandler : Stopped.
2025-06-20T13:52:06.623Z INFO 996 --- [backendProject] [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2025-06-20T13:52:06.626Z INFO 996 --- [backendProject] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2025-06-20T13:52:06.627Z INFO 996 --- [backendProject] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
> Task :test
> Task :check
> Task :build
BUILD SUCCESSFUL in 12s
9 actionable tasks: 9 executed
[Pipeline] }
[Pipeline] // dir
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Docker Build)
[Pipeline] dir
Running in /var/jenkins_home/workspace/backend5_Test_local/backendProject
[Pipeline] {
[Pipeline] sh
+ docker build -t backend .
#1 [internal] load build definition from Dockerfile
#1 sha256:b0724739860ca9c121d7a66cc9e0e0b4901bf0ea45b8bd0f808fc13130083cb3
#1 transferring dockerfile: 552B 0.0s done
#1 DONE 0.0s
#2 [internal] load metadata for docker.io/library/openjdk:17-jdk
#2 sha256:ca51d00d9530d240e743cd0ab7d53583e2fed8269e748b1001b3fe78fb46ffcf
#2 DONE 1.4s
#3 [internal] load .dockerignore
#3 sha256:4e8f7968a72b285e40793a2f04bca295384dbf1979acc5a49b4bb4e21f2dc5f6
#3 transferring context: 2B done
#3 DONE 0.0s
#7 [1/3] FROM docker.io/library/openjdk:17-jdk@sha256:528707081fdb9562eb819128a9f85ae7fe000e2fbaeaf9f87662e7b3f38cb7d8
#7 sha256:f73aed2f80bd0e561c39fdfc2e68670e1949d1bfa96639f3c70771849d802a7e
#7 DONE 0.0s
#5 [internal] load build context
#5 sha256:db0eb76c2771456a1e5f7f06923d71280b5457caa6749f3e2c779e45814f554b
#5 transferring context: 64.16MB 0.6s done
#5 DONE 0.6s
#6 [2/3] WORKDIR /app
#6 sha256:ebfac0acd0d242235faaf3af5c14ddf7d85661355fc853ee47f047b0c80672c3
#6 CACHED
#4 [3/3] COPY build/libs/backendProject-0.0.1-SNAPSHOT.jar /app/backendProject-0.0.1-SNAPSHOT.jar
#4 sha256:888be59c1957a19c1d69d1851a63d2a92a4290bf82b15760168735463e30b915
#4 DONE 0.1s
#8 exporting to image
#8 sha256:70a3ede40af2251b6699a41e06acc861b23b262e7bce225e57bdc48f25a0ff07
#8 exporting layers
#8 exporting layers 0.1s done
#8 writing image sha256:125858e8032634dc0432f7b452213a3b9a1c08e1104aaab89ef1a43f981b55e1 done
#8 naming to docker.io/library/backend:latest done
#8 DONE 0.1s
[Pipeline] }
[Pipeline] // dir
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Compose Up)
[Pipeline] dir
Running in /var/jenkins_home/workspace/backend5_Test_local
[Pipeline] {
[Pipeline] sh
+ pwd
/var/jenkins_home/workspace/backend5_Test_local
[Pipeline] sh
+ ls -l ./nginx/nginx.conf
-rw-r--r-- 1 root root 1304 Jun 20 02:39 ./nginx/nginx.conf
[Pipeline] sh
+ docker-compose up -d --build
Creating network "backend5_test_local_default" with the default driver
Building nginx
#1 [internal] load build definition from Dockerfile
#1 sha256:066018aa9577c968431c106f30480985577cb6a69d2b00a873fca48ba8ad2bce
#1 transferring dockerfile: 192B done
#1 DONE 0.0s
#2 [internal] load metadata for docker.io/library/nginx:latest
#2 sha256:9ad844bdd951526fe21f5db65442779c28ef3778b7341f24b3fff4059e36d00e
#2 DONE 0.0s
#3 [internal] load .dockerignore
#3 sha256:a3c3c5da8e00757c2a95d599d25746ec9bf9591f8f7adbe2f96a8a30898cbd6b
#3 transferring context: 2B done
#3 DONE 0.0s
#7 [1/3] FROM docker.io/library/nginx:latest
#7 sha256:d5687ae241157e32429b22dff7fad05be88477a77988fa0517f84408d5fc5efe
#7 DONE 0.0s
#5 [internal] load build context
#5 sha256:b38a9c66c8cd2203ba9c82fa2f0aac40753ddbca1680d1eccc35178c5bcca62c
#5 transferring context: 32B done
#5 DONE 0.0s
#6 [2/3] RUN rm /etc/nginx/nginx.conf
#6 sha256:9c4a004d5c82d81d57602daa5fc87bfbba857d28e6f109f506e0f67cac6f6f6f
#6 CACHED
#4 [3/3] COPY ./nginx.conf /etc/nginx/nginx.conf
#4 sha256:520fb4c2c98b3d2b8861326f90943ddf0d328c31a1941038a80ea2da2dec95d6
#4 CACHED
#8 exporting to image
#8 sha256:1c4ccf01fd4f832c565140d427c45f1bd7108d39f0e3d8b6b7635d6beddf72de
#8 exporting layers done
#8 writing image sha256:0e510e239564322e25b3c3228ee0828a6021c959e78a54a260bbc4324e561632 done
#8 naming to docker.io/library/backend5_test_local_nginx:latest done
#8 DONE 0.0s
Creating redis ...
Creating database ...
Creating redis ... done
Creating database ... done
Creating backend3 ...
Creating backend1 ...
Creating backend2 ...
Creating backend1 ... done
Creating backend3 ... done
Creating backend2 ... done
Creating nginx ...
Creating nginx ... done
[Pipeline] }
[Pipeline] // dir
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Restart Nginx)
[Pipeline] sh
+ docker restart nginx
nginx
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // withEnv
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS
Shell
복사
•
http://localhost 접속하여 확인