Claude API 사용하여 AI 피드백 받기

N’oublie pas de t’aimer·2025년 3월 14일

DIVE

목록 보기
4/10

우선 API 키를 발급받는다.

만들어야 할 API

  • 변환된 텍스트를 받아서 claude의 피드백을 받아 리턴하는 API

Feedback 클래스

@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Feedback {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(nullable = false, name = "FEEDBACK_ID")
    private Long id;

    private String contents;

    private LocalDateTime createdAt;

    @ManyToOne
    @JoinColumn(name = "VIDEO_ID")
    private Video video;
}

AnswerDTO (유저가 답변한 질문과 텍스트로 변환한 답변을 받아올 데이터 구조)

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class AnswerDTO {
    private String question;
    private String answer;
}

FeedbackDTO (Claude 피드백 전달할 데이터 구조)

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class FeedbackDTO {
    private String contents;
}

FeedbackController 클래스

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Log4j2
@RequiredArgsConstructor
@RequestMapping("/feedback")
public class FeedbackController {
    private final FeedbackService feedbackService;

    @PostMapping("/create")
    public FeedbackDTO createFeedback(AnswerDTO answerDTO) throws Exception {
        FeedbackDTO feedbackDTO = null;
        try {
            feedbackDTO = feedbackService.getFeedback(answerDTO);
        } catch (Exception e) {
            log.error(e);
        }
        return feedbackDTO;
    }
}

FeedbackService 클래스

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;


@Log4j2
@Service
@RequiredArgsConstructor
public class FeedbackService {
    @Value("${claude.api.key}")
    private static String API_KEY;

    public FeedbackDTO getFeedback(AnswerDTO answerDTO) throws Exception {
        String question = answerDTO.getQuestion();
        String answer = answerDTO.getAnswer();
        String cmd = answer + "은 CS 면접 질문 \"" + question + "\"에 대한 답변이야. 이 답변을 내용 측면과 전달력 측면에서 피드백해줘. 결과는 한국어로 전달해줘.";

        // Claude API 요청 URL
        URL url = new URL("https://api.anthropic.com/v1/messages");

        // HTTP 연결 설정
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("POST");
        connection.setRequestProperty("x-api-key", API_KEY); // 실제 API 키로 교체
        connection.setRequestProperty("anthropic-version", "2023-06-01");
        connection.setRequestProperty("Content-Type", "application/json");
        connection.setDoOutput(true);

        // JSON 요청 본문 구성
        String jsonInputString = "{"
                + "\"model\": \"claude-3-5-sonnet-20241022\","
                + "\"max_tokens\": 1024,"
                + "\"messages\": ["
                + "  {\"role\": \"user\", \"content\": \"" + cmd + "\"}"
                + "]"
                + "}";

        // 요청 본문 전송
        try (OutputStream os = connection.getOutputStream()) {
            byte[] input = jsonInputString.getBytes("utf-8");
            os.write(input, 0, input.length);
        }

        // 응답 처리
        try (BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), "utf-8"))) {
            StringBuilder response = new StringBuilder();
            String responseLine;
            while ((responseLine = br.readLine()) != null) {
                response.append(responseLine.trim());
            }

            // 응답을 FeedbackDTO로 변환하는 부분
            String responseString = response.toString();
            FeedbackDTO feedbackDTO = new FeedbackDTO(responseString);

            return feedbackDTO;
        }
    }
}

배포 후 api 테스트를 해준다.

비정상 종료가 된 것 같다. 로그를 확인해보자.

Unable to create key store: Error reading certificate or key from file

https 접근에 필요한 letsencrypt 키에 접근할 수 없다는 에러인 것 같다.
인스턴스 재부팅 후에 letsencrypt 키를 재발급 받았기 때문에 경로나 키 파일 접근 권한에 문제가 있는 것 같다.

우선

ls -l /etc/letsencrypt/live/[인스턴스 퍼블릭 ip 주소].nip.io/

를 통해 해당 경로에 실제로 파일이 존재하는지 확인한다.

/etc/letsencrypt/live/[인스턴스 퍼블릭 ip 주소].nip.io/privkey.pem은 심볼릭 링크로 되어 있고, 실제 파일은 /etc/letsencrypt/archive/[인스턴스 퍼블릭 ip 주소].nip.io/privkey1.pem

인 것으로 나온다.

실제 파일도 정상적으로 존재하므로 인증서가 갱신되지 않았을 가능성이 있는 것 같다.

sudo certbot certificates

명령어를 통해 서버에 설치된 인증서의 상태를 확인한다.

  Certificate Name: [인스턴스 퍼블릭 ip 주소].nip.io
    Serial Number: 31deb011afd2c8981e5c36a882074a78938
    Key Type: ECDSA
    Domains: [인스턴스 퍼블릭 ip 주소].nip.io
    Expiry Date: 2025-06-10 01:27:40+00:00 (VALID: 85 days)
    Certificate Path: /etc/letsencrypt/live/[인스턴스 퍼블릭 ip 주소].nip.io/fullchain.pem
    Private Key Path: /etc/letsencrypt/live/[인스턴스 퍼블릭 ip 주소].nip.io/privkey.pem

정상적으로 발급된 것으로 보인다.

키를 재발급 받아보자.

여전히 실행이 되지 않는다.

내 Spring Boot 애플리케이션은 root 사용자에게만 실행이 허용되어 있는데 실행할 때 sudo (root 사용자 권한) 명령어를 붙여주지 않아서 그런 것 같다.

sudo로 다시 실행해보자. 실행이 잘 된다.

이제 postman으로 claude의 피드백을 받는 api를 테스트 해보자.

Forbidden이라고 뜬다.
아마도 /feedback/create uri를 permitAllUrls에 추가해주지 않아서 그런 것 같다. 수정 후에 다시 테스트 해 보자.


static 변수에 값 주입을 지원하지 않기 때문에 요청 헤더의 api key 자리에 null값을 넣어서 401 unauthorized가 발생한 것 같다.

static 키워드를 제거해주자.

400에러가 뜬다.

요청 본문의 모델 버전이 맞지 않아 수정해준다.

 catch (IOException e) {
            // 에러 응답 처리
            if (connection.getResponseCode() == 400) {
                try (BufferedReader br = new BufferedReader(new InputStreamReader(connection.getErrorStream(), "utf-8"))) {
                    StringBuilder errorResponse = new StringBuilder();
                    String responseLine;
                    while ((responseLine = br.readLine()) != null) {
                        errorResponse.append(responseLine.trim());
                    }
                    log.error("API 400 Error: " + errorResponse.toString());
                }
            }
            throw e;
        }

다음과 같이 에러를 로깅하는 코드도 넣어준다.

API 400 Error: {"type":"error","error":{"type":"invalid_request_error","message":"The request body is not valid JSON: unexpected control character in string: line 1 column 187 (char 186)"}}

문자열 연결 방식으로 JSON을 만들면 이런 문제가 자주 발생한다고 한다. 이를 해결하기 위해 Jackson이나 Gson 같은 JSON 라이브러리를 사용하는 것이 좋다고 한다.

package com.site.xidong.feedback;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;


@Log4j2
@Service
@RequiredArgsConstructor
public class FeedbackService {
    @Value("${claude.api.key}")
    private String API_KEY;

    public FeedbackDTO getFeedback(AnswerDTO answerDTO) throws Exception {
        String question = answerDTO.getQuestion();
        String answer = answerDTO.getAnswer();
        String cmd = answer + "은 CS 면접 질문 [" + question + "]에 대한 답변이야. 이 답변을 내용 측면과 전달력 측면에서 피드백해줘. 결과는 한국어로 전달해줘.";

        // Jackson 객체 매퍼 생성
        ObjectMapper mapper = new ObjectMapper();
        ObjectNode rootNode = mapper.createObjectNode();

        // JSON 구조 설정
        rootNode.put("model", "claude-3-7-sonnet-20250219");
        rootNode.put("max_tokens", 1024);

        ArrayNode messagesNode = mapper.createArrayNode();
        ObjectNode messageNode = mapper.createObjectNode();
        messageNode.put("role", "user");
        messageNode.put("content", cmd);
        messagesNode.add(messageNode);

        rootNode.set("messages", messagesNode);

        // JSON 문자열로 변환
        String jsonInputString = mapper.writeValueAsString(rootNode);
        log.info("Request JSON: " + jsonInputString);

        // Claude API 요청 URL
        URL url = new URL("https://api.anthropic.com/v1/messages");

        // HTTP 연결 설정
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("POST");
        connection.setRequestProperty("x-api-key", API_KEY); // 실제 API 키로 교체
        log.info("API KEY: " + API_KEY);
        connection.setRequestProperty("anthropic-version", "2023-06-01");
        connection.setRequestProperty("Content-Type", "application/json");
        connection.setDoOutput(true);

        // 요청 본문 전송
        try (OutputStream os = connection.getOutputStream()) {
            byte[] input = jsonInputString.getBytes("utf-8");
            os.write(input, 0, input.length);
        }

        // 응답 처리
        try (BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), "utf-8"))) {
            StringBuilder response = new StringBuilder();
            String responseLine;
            while ((responseLine = br.readLine()) != null) {
                response.append(responseLine.trim());
            }

            // 응답을 FeedbackDTO로 변환하는 부분
            String responseString = response.toString();
            log.info("response: " + responseString);

            // Jackson을 사용하여 JSON 응답 파싱
            ObjectNode responseNode = mapper.readValue(responseString, ObjectNode.class);
            String content = responseNode.path("content").path(0).path("text").asText();

            FeedbackDTO feedbackDTO = new FeedbackDTO(content);
            return feedbackDTO;
        } catch (IOException e) {
            // 에러 응답 처리
            if (connection.getResponseCode() >= 400) {
                try (BufferedReader br = new BufferedReader(new InputStreamReader(connection.getErrorStream(), "utf-8"))) {
                    StringBuilder errorResponse = new StringBuilder();
                    String responseLine;
                    while ((responseLine = br.readLine()) != null) {
                        errorResponse.append(responseLine.trim());
                    }
                    log.error("API Error ({}): {}", connection.getResponseCode(), errorResponse.toString());
                }
            }
            throw e;
        }
    }
}

다음과 같이 수정해준다.

피드백을 받아오는 데 성공했다.

이제 FeedbackRepository 클래스도 마저 작성하고 db에 feedback 객체를 저장하도록 FeedbackService 클래스도 수정해주자.

Feedback 클래스의 contents 칼럼에 들어가기에 길이가 너무 길어서 오류가 발생했다.

@Lob // 길이 제한 없이 저장 가능
@Column(columnDefinition = "TEXT") // MySQL 기준 TEXT 타입으로 지정

두 어노테이션을 추가해주자.

feedback 객체가 잘 생성되었다.

profile
매일 1퍼센트씩 나아지기 ୧(﹒︠ ̫ ̫̊ ̫﹒︡)୨

0개의 댓글