Backend
home

2025-6-20 (금)

생성일
2025/06/20 00:03
태그
WebSocket
AI챗봇
Jenkins
Redis

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 접속하여 확인