GPT ? 너 누구였구나..

나나's Brain·2024년 10월 24일
1

개념Study

목록 보기
14/21
post-thumbnail

➡️ 전의 글에서는 gpt api를 사용하기위한 빌드 과정을 설명해보았다. api key는 다시 못보니까 꼭 복사해서 어딘가에다가 붙여넣기 해놓자 ??

application.yml -> GptConfig -> GptService -> GptController -> GptServiceTest 순으로 플로우를 작성해볼것이다.


📁 Sprigboot로 GPT API활용해보기

먼저, application.yml에 모델명과 key를 작성해줄건데 여기서 모델은 gpt-3.5-turbo를 먼저 사용해보고, 개발이 어느정도 진행되었다면 그때 4로 바꿔보는것을 추천한다.

✅ application.yml

openai:
  model: gpt-4o
  secret-key: sk- api Key 복사하기

처음에는 gpt-3.5-turbo를 사용했었는데 프롬프팅을 시켜도 한계가 존재한다는것을 과정에서 확인해볼 수 있을것이다. 너무 화내진말고 내가 그랬다. 차분하게 천천히 나아간다면 좋은 결과가 나올 수 있다고 자부한다.

⚒️ 위에처럼 나중에 가져다쓸 gpt 정보를 적었다면 이를 불러올 Config 파일을 작성해보자.

✅ GptConfig

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GptConfig {

    @Value("${openai.secret-key}")
    private String secretKey;

    @Value("${openai.model}")
    private String model;

    public String getSecretKey() {
        return secretKey;
    }

    public String getModel() {
        return model;
    }
}

이렇게 작성해놓으면 gptConfig.getModel() 이런식으로 꺼내쓸수 있다.
근데 여기서 잠깐. 우리는 GPT API를 사용하기위해 기획했던것이 존재할 것이다. 다시말해, 응답되는 출력문을 말하는것이다.

➡️ 나는 GPT가 헬스트레이너였으면 좋겠고, 필요하고 도움될 정보만을 출력되기를 원해 이러한 니즈를 충족 시킬 수 있는 운동루틴 페르소나를 먼저 기획해보았다.

하체 운동 루틴: 강력한 다리 만들기!
날짜: 2024년 10월 12일
키: 168cm
몸무게: 55kg
성별: 여성
운동 부위: 하체
운동할 시간: 60분

오늘의 운동 루틴:

1) 바디웨이트 스쿼트
운동 시간: 10분
세트 및 반복 횟수: 3세트 15회
중량: 체중
운동 설명: 어깨 너비로 발을 벌리고 발끝은 약간 바깥쪽을 향하게 합니다. 무릎이 발끝을 넘지 않도록 주의하면서 엉덩이를 뒤로 빼며 앉습니다. 허벅지가 바닥과 평행이 될 때까지 내려갔다가 천천히 일어납니다. 상체는 곧게 펴고, 복부에 힘을 주어 자세를 유지합니다. 이 운동은 허벅지와 엉덩이 근육을 강화하는 데 효과적입니다.
운동 영상: 바디웨이트 스쿼트

2) 레그 프레스
운동 시간: 10분
세트 및 반복 횟수: 3세트 12회
중량: 50kg
운동 설명: 레그 프레스 머신에 앉아 발을 어깨 너비로 벌리고 발판에 놓습니다. 무릎이 90도가 될 때까지 발판을 밀어 올립니다. 다리를 완전히 펴지 않도록 주의하며 천천히 시작 위치로 돌아옵니다. 이 운동은 대퇴사두근과 종아리를 강화하는 데 매우 효과적입니다.
운동 영상: 레그 프레스

n) ....

추천 Music:
Eye of the Tiger - Survivor
Stronger - Kanye West
Can't Stop the Feeling! - Justin Timberlake
Uptown Funk - Mark Ronson ft. Bruno Mars
Shape of You - Ed Sheeran

운동 응원 멘트:
"오늘도 최선을 다하는 당신이 가장 멋집니다! 꾸준히 노력하면 원하는 목표에 도달할 수 있습니다. 파이팅!"

이런식으로 사용자 정보와 운동부위 및 운동시간을 사용자에게 받아온다면, 그걸 기반으로 우리쪽에서 운동기구 데이터 기반으로 이 데이터 바운더리 안에서 운동 루틴을 추천해 주는 방식으로 추진하였다.

🪄 페르소나 형식으로 접해본다면 특정 분야에 대한 질문을 하고 싶을 때 유용하게 쓰일것이다. 답변의 주제를 좁힐 수 있어 답변의 퀄리티가 향상될 수 있다고 생각이 들었다.

이제 서비스 코드를 작성해보자.

✅ GptService

서비스 코드에는 크게 5가지 메소드가 존재한다.

callOpenAI -> GPT 호출하는 메소드
generatePrompt -> 출력 프롬프트 메소드 여기서 프롬프팅 할것이다.
generateRoutine -> callOpenAI를 호출해 사용자 정보와 운동기구 데이터를 불러오고, 운동 부위, 운동 시간을 입력 받아 출력하는 메소드
routineParser -> 출력된 정보를 파싱해 원하는 정보 빼내기
processRoutine -> 출력된 정보를 해당 도메인에 저장

1) callOpenAI 메소드

public class GptServiceImpl implements GptService {

    private final GptConfig gptConfig;
    private final RestTemplate restTemplate;
    private final ExerciseEquipmentService exerciseEquipmentService;
    private final UserService userService;
    private final WorkoutPerRoutineService workoutPerRoutineService;

    private static final String API_URL = "https://api.openai.com/v1/chat/completions";
    private final RoutineService routineService;
    private final WorkoutInfoService workoutInfoService;

    public String callOpenAI(String prompt, int maxTokens) throws JsonProcessingException {

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.setBearerAuth(gptConfig.getSecretKey());

        Map<String, Object> requestBody = new HashMap<>();
        requestBody.put("model", gptConfig.getModel());
        requestBody.put("messages", new Object[]{new HashMap<String, String>() {{
            put("role", "system");
            put("content", String.format("당신은 우주최고유명하고 운동을 잘가르치는 헬스트레이너야, " +
                    "초보자 맞춤형 운동 루틴을 제대로 잘 추천해 줘야해."));
        }},
                new HashMap<String, String>() {{
                    put("role", "user");
                    put("content", prompt);
                }}
        });

        requestBody.put("temperature", 0.3);
        requestBody.put("max_tokens", maxTokens);

        HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);

        try {
            ResponseEntity<String> response = restTemplate.exchange(API_URL, HttpMethod.POST, entity, String.class);
            return response.getBody();
        } catch (Exception e) {
            return "Error: " + e.getMessage();
        }
    }

여기서 String.format("당신은 우주최고유명하고 운동을 잘가르치는 헬스트레이너야, " + "초보자 맞춤형 운동 루틴을 제대로 잘 추천해 줘야해.") 라며 처음에 GPT를 format을 시키는 것이다.

max_tokens는 프롬프팅의 일부로, GPT 모델이 생성하는 출력의 길이를 제어하기 위해 사용되며 우리는 3000자 정도로 잡았다. 즉, API 호출 시 생성되는 응답의 크기(토큰 수)를 의미한다.

temperature는 값이 낮을수록(예: 0.2~0.3), 모델의 응답은 더 결정적이고 예측 가능하게 된다. 보통 0.7이라고 많이 적어서 나는 0.3으로 적어보았다.

2) generateRoutine 과 generatePrompt 메소드

  @Override
    public String generateRoutine(String userCode, String bodyPart, int time) {

        List<ExerciseEquipmentDTO> exerciseEquipment = exerciseEquipmentService.findByBodyPart(bodyPart);
        String prompt = generatePrompt(userCode, bodyPart, time, exerciseEquipment);

        try {
            String response = callOpenAI(prompt, 3000);
            routineParser(response);
            return response;
        } catch (JsonProcessingException e) {
            throw new CommonException(StatusEnum.INTERNAL_SERVER_ERROR);
        }
    }

⬆️ 여기서 사용자가 입력한 운동부위를 넘겨주기 위해 exerciseEquipment 운동기구를 값을 넘겨 받아 운동기구를 찾아 List에 저장하여 담았다.

⬇️ 아래에는 유저 정보에서 키, 몸무게, 나이, 성별의 정보를 가져옴으로써 사용자 맞춤 운동 루틴을 작성하고자 기획하였다.

public String generatePrompt(String userCode, String bodyPart, int time, List<ExerciseEquipmentDTO> exerciseEquipmentDTO) {
        UserDTO user = userService.findById(userCode);

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일");
        String formattedDate = LocalDateTime.now().format(formatter);

        StringBuilder prompt = new StringBuilder();
        prompt.append("여기에 이제 원하는 프롬프트 생성을 위해 잘 프롬프팅 시키기");

        prompt.append(String.format(" 날짜 : %s, 키: %.0fcm, 몸무게: %.0fkg, 성별: %s, 나이: %d세, 운동 부위: %s, 운동할 시간: %d분"
                , formattedDate
                , user.getUserHeight()
                , user.getUserWeight()
                , user.getUserGender()
                , user.getUserAge()
                , bodyPart
                , time));
        String equipmentList = exerciseEquipmentDTO.stream()
                .map(ExerciseEquipmentDTO::getExerciseEquipmentName)
                .collect(Collectors.joining(", "));

        prompt.append("운동 추천을 위해 사용할 수 있는 운동 기구 리스트도 제공할게");
        prompt.append(String.format(" 운동에 필요한 기구: %s", equipmentList));

        prompt.append("아래에 제공된 형식을 사용해 줘 꼭!!!");
        prompt.append("여기에 오늘의 운동 루틴형식을 작성하였음");

        return prompt.toString();
    }

prompt.append()를 활용하여 GPT에게 정보를 제공해줄 수 있다.

3) processRoutine과 routineParser 메소드

@Override
    public Map<String, Object> routineParser(String response) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode rootNode = objectMapper.readTree(response);
        JsonNode messageNode = rootNode.path("choices").get(0).path("message").path("content");
        String contents = messageNode.asText();


        String title = extractTitle(contents);
        int totalTime = extractTotalTime(contents);


        String exercisesContent = contents.split("오늘의 운동 루틴을 추천해 드립니다:")[1].trim();
        String[] exercises = exercisesContent.split("\n\n");

        Map<String, Object> workoutData = new HashMap<>();
    
    // workoutData.put(파싱한 값을 workoutData에 넣기);..
    .
    .
    .
  
        

workoutData 의 맵을 형성하여 GPT 생성 한 출력문에 각 파싱 메소드를 추가하여 값을 추가 해 주었다.

 @Override
    public Long processRoutine(String response) throws JsonProcessingException {
        // 파싱된 데이터를 가져옴
        Map<String, Object> workoutData = routineParser(response);
        Long routineId = null;
        
         // 1. 중복된 루틴 확인
        // 2. 새로운 루틴 등록
        // 3. 루틴별 운동 저장
        // 4. 운동 정보 저장
       

여기에서는 위에서 데이터 별로 파싱한것을 각 도메인별로 데이터 저장이 가능하게 기능을 구현하였다. (기능당 유효성검사 필수)

이 과정이 가장 오래 걸렸던 것 같다.. 😭😭😭😭😭😭

✅ GptController

컨트롤러는 생성과 저장으로 두개만 구현하였으며 나중에 프론트에서 생성과 저장이 가능하게 경로를 지정해 주었다.

 @PostMapping("/generate-routine")
    @Operation(summary = "GPT 운동 루틴 생성")
    public ResponseEntity<String> generateRoutine(@RequestBody RequestRegisterRoutineVO request){
    
            //GPT 운동 루틴 생성 
}
@PostMapping("/process-routine")
    @Operation(summary = "GPT 운동 루틴 저장")
    public ResponseEntity<Map<String, Object>> processRoutine(@RequestBody String response) {
    
                //GPT 운동 루틴 저장 
    }

✅ GptServiceTest

@BeforeEach
    public void setUp() {
        userCode = "20241007-05bfb06b-8eda-4857-8681-40d1eccb829d";
        bodyPart = "하체";
        workoutTime = 90;
    }

사용자가 입력 받았다는것을 가정하고, 운동부위와 운동시간을 입력받아 테스트 하는 식으로 하였다.

테스트 해보면 이렇게 유저정보와 운동기구 데이터를 기반으로 운동 루틴을 생성해주는 GPT API 활용을 해보았.......다.... 생각보다 힘들었던것 같으면서도 어떻게 프롬프트를 시키느냐에 따라서 결과물이 달라지는 모습을 보고 얼마나 프롬프팅이 중요한지 깨달았..다... 🤨..

( DB 모델링도.. 정말 중요하구나 라고 10000000000% 와닿았었다. )

왜냐. 삽질을 열심히 하였었... 🛠️⚒️⛏️🪓🔧🔩🪛 굉장해.. 엄청나..


이를 바탕으로 Vue.js에서 프론트엔드를 구현하고, 서버에서 CORS 정책을 설정하여 API와 통신하며 프로젝트를 마무리 하였다. 그래도 정말 재밌었던 기능 구현이였다.

🖥️ GPT 활용 기능 부분 화면

➡️ 루틴 생성

➡️ 루틴 기록 저장

profile
"로컬에선 문제없었는데…?"

1개의 댓글

comment-user-thumbnail
2024년 10월 27일

나나님의 GPT 감동입니다 :)

답글 달기