Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[refactor] : Chatbot 관련 리팩토링을 진행한다 #85

Merged
merged 9 commits into from
Nov 23, 2024
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
@RequiredArgsConstructor
public class ChatbotService {
Expand Down Expand Up @@ -41,29 +38,20 @@ public GetGuideChatbotAnswerResponse getGuideChatbotAnswer(String stadiumName, S
private <T extends Enum<T> & GuideAnswer> GetGuideChatbotAnswerResponse getAnswersByOrderAndStadium(
int orderNumber, String stadiumName, T[] guideAnswers) {

// 1. orderNumber에 맞는 enum 리스트 필터링
List<T> matchingAnswers = new ArrayList<>();
for (T answer : guideAnswers) {
if (answer.getId() == orderNumber) {
matchingAnswers.add(answer);
}
}

// 2. stadiumName이 일치하는 답변 찾기
for (T answer : matchingAnswers) {
if (stadiumName.equals(answer.getStadiumName())) {
return GetGuideChatbotAnswerResponse.of(answer.getAnswers(), answer.getImgUrl());
}
}

// 3. 일치하는 stadiumName이 없을 경우, 기본 응답 반환
for (T answer : matchingAnswers) {
if (answer.getStadiumName() == null) {
return GetGuideChatbotAnswerResponse.of(answer.getAnswers(), answer.getImgUrl());
}
}

// 4. 기본 응답도 없을 경우 예외 처리
throw new CustomException(ChatbotErrorStatus._NOT_FOUND_GUIDE_CHATBOT_ANSWER);
// 1. orderNumber와 stadiumName이 모두 일치하는 답변을 찾기
return java.util.Arrays.stream(guideAnswers)
.filter(answer -> answer.getId() == orderNumber) // orderNumber 필터링
.filter(answer -> stadiumName.equals(answer.getStadiumName())) // stadiumName 필터링
.findFirst()
.or(() ->
// 2. stadiumName이 null인 기본 답변을 찾기
java.util.Arrays.stream(guideAnswers)
.filter(answer -> answer.getId() == orderNumber)
.filter(answer -> answer.getStadiumName() == null)
.findFirst()
)
.map(answer -> GetGuideChatbotAnswerResponse.of(answer.getAnswers(), answer.getImgUrl()))
// 3. 아무 답변도 없으면 예외 처리
.orElseThrow(() -> new CustomException(ChatbotErrorStatus._NOT_FOUND_GUIDE_CHATBOT_ANSWER));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 키워드에 따른 결과 찾을 때 stream 많이 사용했는데 사용하기 간편하고 좋더라구요
filter, and, or 조건으로 쉽게 조건 붙일 수 있어서 사용하는 데 익숙해지면 좋을 것 같아요!!

}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package kusitms.backend.chatbot.application;

import kusitms.backend.chatbot.dto.request.ChatbotRequest;
import kusitms.backend.chatbot.dto.request.ClovaRequest;
import kusitms.backend.chatbot.dto.request.Message;
import lombok.RequiredArgsConstructor;
Expand All @@ -13,10 +14,10 @@ public class ClovaRequestFactory {

private final MessageFactory messageFactory;

public ClovaRequest createClovaRequest() {
public ChatbotRequest createClovaRequest() {
ArrayList<Message> messages = new ArrayList<>();
messages.add(messageFactory.createSystemMessage());

return new ClovaRequest(messages, 0.8, 0.3, 256, 5.0);
}
}
}
20 changes: 10 additions & 10 deletions src/main/java/kusitms/backend/chatbot/application/ClovaService.java
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
package kusitms.backend.chatbot.application;

import kusitms.backend.chatbot.dto.request.ClovaRequest;
import kusitms.backend.chatbot.dto.request.ChatbotRequest;
import kusitms.backend.chatbot.dto.response.GetClovaChatbotAnswerResponse;
import kusitms.backend.chatbot.infrastructure.ClovaApiClient;
import kusitms.backend.chatbot.infrastructure.ChatbotApiClient;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

@Service
@RequiredArgsConstructor
public class ClovaService {
private final ClovaApiClient clovaApiClient;
private final ChatbotApiClient chatbotApiClient;
private final ClovaRequestFactory clovaRequestFactory;
private final MessageFactory messageFactory;

// Clova 챗봇 답변을 가져오는 메서드
public GetClovaChatbotAnswerResponse getClovaChatbotAnswer(String message) {
ClovaRequest request = clovaRequestFactory.createClovaRequest();
request.messages().add(messageFactory.createUserMessage(message));
String answer = clovaApiClient.requestClova(request);
public Mono<GetClovaChatbotAnswerResponse> getClovaChatbotAnswer(String message) {
ChatbotRequest request = clovaRequestFactory.createClovaRequest();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetClovaChatbotAnswerResponseDto로 바꿔주세요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

변경 완료!

request.getMessages().add(messageFactory.createUserMessage(message));

return GetClovaChatbotAnswerResponse.of(answer);
return chatbotApiClient.requestChatbot(request)
.map(GetClovaChatbotAnswerResponse::of);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@ public class MessageFactory {

// 사용자 메시지 생성
public Message createUserMessage(String content) {
return new Message(Role.user, content);
return new Message(Role.USER.getRole(), content);
}

// 시스템 메시지 생성
public Message createSystemMessage() {
return new Message(Role.system, new String(Base64.getDecoder().decode(baseballPrompt)));
return new Message(Role.SYSTEM.getRole(), new String(Base64.getDecoder().decode(baseballPrompt)));
}

// 어시스턴트 메시지 생성
public Message createAssistantMessage(String content) {
return new Message(Role.assistant, content);
return new Message(Role.ASSISTANT.getRole(), content);
}
}
}
16 changes: 12 additions & 4 deletions src/main/java/kusitms/backend/chatbot/domain/enums/Role.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
package kusitms.backend.chatbot.domain.enums;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum Role {
system, // 시스템
user, // 사용자
assistant; // 어시스턴트
}
SYSTEM("system"), // 시스템
USER("user"), // 사용자
ASSISTANT("assistant") // 어시스턴트
;

private final String role;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package kusitms.backend.chatbot.dto.request;

import java.util.List;

public interface ChatbotRequest {
List<Message> getMessages();
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package kusitms.backend.chatbot.dto.request;

import java.util.ArrayList;
import java.util.List;

public record ClovaRequest(
ArrayList<Message> messages,
double topP,
double temperature,
int maxTokens,
double repeatPenalty
) {
}
) implements ChatbotRequest {

@Override
public List<Message> getMessages() {
return messages;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package kusitms.backend.chatbot.dto.request;

import kusitms.backend.chatbot.domain.enums.Role;

public record Message(
Role role,
String role,
String content
) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ public record GetClovaChatbotAnswerResponse(
public static GetClovaChatbotAnswerResponse of(String answer) {
return new GetClovaChatbotAnswerResponse(answer);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package kusitms.backend.chatbot.infrastructure;

import kusitms.backend.chatbot.dto.request.ChatbotRequest;
import reactor.core.publisher.Mono;

public interface ChatbotApiClient {
/**
* 외부 챗봇 API와 통신하여 답변을 가져옵니다.
*
* @param request 추상 챗봇 요청 객체
* @return 챗봇의 응답 메시지
*/
Mono<String> requestChatbot(ChatbotRequest request);
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,38 @@
package kusitms.backend.chatbot.infrastructure;

import kusitms.backend.chatbot.dto.request.ChatbotRequest;
import kusitms.backend.chatbot.dto.request.ClovaRequest;
import kusitms.backend.chatbot.dto.response.ClovaChatbotAnswer;
import kusitms.backend.chatbot.status.ChatbotErrorStatus;
import kusitms.backend.global.exception.CustomException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;

@Component
@RequiredArgsConstructor
public class ClovaApiClient {
public class ClovaApiClient implements ChatbotApiClient {
private final WebClient webClient;

// 외부 CLOVA API와 통신하는 메서드
public String requestClova(ClovaRequest request) {
ClovaChatbotAnswer clovaChatbotAnswer = webClient.post()
.bodyValue(request)
@Override
public Mono<String> requestChatbot(ChatbotRequest request) {
if (!(request instanceof ClovaRequest clovaRequest)) {
return Mono.error(new CustomException(ChatbotErrorStatus._INVALID_CHATBOT_REQUEST));
}

return webClient.post()
.bodyValue(clovaRequest)
.retrieve()
.bodyToMono(ClovaChatbotAnswer.class)
.block();

return clovaChatbotAnswer.result().message().content();
.flatMap(clovaChatbotAnswer -> {
if (clovaChatbotAnswer == null || clovaChatbotAnswer.result() == null || clovaChatbotAnswer.result().message() == null) {
return Mono.error(new CustomException(ChatbotErrorStatus._NOT_FOUND_GUIDE_CHATBOT_ANSWER));
}
return Mono.just(clovaChatbotAnswer.result().message().content());
})
.onErrorMap(WebClientResponseException.class, e -> new CustomException(ChatbotErrorStatus._CHATBOT_API_COMMUNICATION_ERROR))
.onErrorMap(Exception.class, e -> new CustomException(ChatbotErrorStatus._CHATBOT_API_COMMUNICATION_ERROR));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;

@RestController
@RequiredArgsConstructor
Expand Down Expand Up @@ -56,11 +57,10 @@ public ResponseEntity<ApiResponse<GetGuideChatbotAnswerResponse>> getGuideChatbo
* @return Clova 챗봇으로부터 생성된 답변
*/
@PostMapping("/clova")
public ResponseEntity<ApiResponse<GetClovaChatbotAnswerResponse>> getClovaChatbotAnswer(
public Mono<ResponseEntity<ApiResponse<GetClovaChatbotAnswerResponse>>> getClovaChatbotAnswer(
@Valid @RequestBody GetClovaChatbotAnswerRequest request) {

GetClovaChatbotAnswerResponse response = clovaService.getClovaChatbotAnswer(request.message());

return ApiResponse.onSuccess(ChatbotSuccessStatus._GET_CLOVA_CHATBOT_ANSWER, response);
return clovaService.getClovaChatbotAnswer(request.message())
.map(response -> ApiResponse.onSuccess(ChatbotSuccessStatus._GET_CLOVA_CHATBOT_ANSWER, response));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
public enum ChatbotErrorStatus implements BaseErrorCode {

_NOT_FOUND_GUIDE_CHATBOT_ANSWER(HttpStatus.NOT_FOUND, "CHATBOT-001", "챗봇 답변을 찾을 수 없습니다."),
_IS_NOT_VALID_CATEGORY_NAME(HttpStatus.BAD_REQUEST, "CHATBOT-002", "올바른 카테고리명이 아닙니다.")
_IS_NOT_VALID_CATEGORY_NAME(HttpStatus.BAD_REQUEST, "CHATBOT-002", "올바른 카테고리명이 아닙니다."),
_INVALID_CHATBOT_REQUEST(HttpStatus.BAD_REQUEST, "CHATBOT-003", "유효하지 않은 요청 타입입니다."),
_CHATBOT_API_COMMUNICATION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "CHATBOT-004", "외부 챗봇 API와의 통신 중 오류가 발생했습니다.")
;

private final HttpStatus httpStatus;
Expand All @@ -36,4 +38,4 @@ public ErrorReasonDto getReasonHttpStatus() {
.message(message)
.build();
}
}
}
14 changes: 12 additions & 2 deletions src/main/java/kusitms/backend/global/dto/ApiResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,33 @@ public class ApiResponse<T> {
@JsonInclude(JsonInclude.Include.NON_NULL)
private final T payload;

// 성공 응답 - 페이로드 포함
public static <T> ResponseEntity<ApiResponse<T>> onSuccess(BaseCode code, T payload) {
ApiResponse<T> response = new ApiResponse<>(true, code.getReasonHttpStatus().getCode(), code.getReasonHttpStatus().getMessage(), payload);
return ResponseEntity.status(code.getReasonHttpStatus().getHttpStatus()).body(response);
}

// 성공 응답 - 페이로드 없음
public static <T> ResponseEntity<ApiResponse<T>> onSuccess(BaseCode code) {
ApiResponse<T> response = new ApiResponse<>(true, code.getReasonHttpStatus().getCode(), code.getReasonHttpStatus().getMessage(), null);
return ResponseEntity.status(code.getReasonHttpStatus().getHttpStatus()).body(response);
}

// 실패 응답 - 기본 메시지
public static <T> ResponseEntity<ApiResponse<T>> onFailure(BaseErrorCode code) {
ApiResponse<T> response = new ApiResponse<>(false, code.getReasonHttpStatus().getCode(), code.getReasonHttpStatus().getMessage(), null);
return ResponseEntity.status(code.getReasonHttpStatus().getHttpStatus()).body(response);
}

public static <T> ResponseEntity<Object> onFailure(BaseErrorCode code, String message) {
// 실패 응답 - 커스텀 메시지 (페이로드 없음)
public static <T> ResponseEntity<ApiResponse<T>> onFailureWithCustomMessage(BaseErrorCode code, String customMessage) {
ApiResponse<T> response = new ApiResponse<>(false, code.getReasonHttpStatus().getCode(), customMessage, null);
return ResponseEntity.status(code.getReasonHttpStatus().getHttpStatus()).body(response);
}

// 실패 응답 - 오버라이드 메서드용
public static <T> ResponseEntity<Object> onFailureForOverrideMethod(BaseErrorCode code, String message) {
ApiResponse<T> response = new ApiResponse<>(false, code.getReasonHttpStatus().getCode(), message, null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public static <T> ResponseEntity<ApiResponse<T>> onFailureForOverrideMethod(BaseErrorCode code, String message) {

여기 onFailureForOverrideMethod메서드도 ApiResponse로 바꾸면 좋을 것 같아요

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 이부분은

    // HttpMessageNotReadableException 처리 (잘못된 JSON 형식)
    @Override
    public ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex,
                                                               HttpHeaders headers,
                                                               HttpStatusCode status,
                                                               WebRequest request) {
        String errorMessage = "요청 본문을 읽을 수 없습니다. 올바른 JSON 형식이어야 합니다.";
        logError("HttpMessageNotReadableException", ex);
        return ApiResponse.onFailureForOverrideMethod(ErrorStatus._BAD_REQUEST, errorMessage);
    }

위처럼 ResponseEntityExceptionHandler 내에 기본적으로 있는 메서드를 오버라이드하는 경우에 쓰는 거라서 타입을 ResponseEntity<Object>로 고정해야 할 것 같아요!!

return ResponseEntity.status(code.getReasonHttpStatus().getHttpStatus()).body(response);
}
}
}
Loading