들어가며

최근 Backen-GPT와 관련된 글(https://beoks.tistory.com/91)을 올렸습니다. 동시에 GPT는 현재 Backend에 적용하기에는 다양한 한계가 존재한다고 평가했는데, 그래도 응용할 수 있는 방법이 없을까 고민하다 MockAPI에는 적용할만하다고 생각해 만들어보게 되었습니다.

디자인

기존 MockAPI는 주로 모바일이나 프론트엔드 개발에서 아직 벡엔드가 구현되지 않았을 때 API와 관련된 프론트 기능을 구현할 때 사용되었습니다. 하지만 스캐폴딩 코드에 해당하기에 MockAPI를 구현하는데 시간을 많이 쓰는 건 올바르지 않습니다. 따라서 목적과 초기 데이터베이스 상태를 설정하면 바로 원하는 API를 구현할 수 있도록 최대한 간단한 디자인을 목표로 삼아 아래와 같은 인터페이스를 생각했습니다.

//대학교 학생 관리라는 목적과 초기 학생 데이터베이스를 정의한다.
const API_DESCRIPTION = "University Studenet Management"
const INIT_DB={students : [{name : "foo",email:"email@email.com",grade:4}]}
const studentMockAPI = createMockAPI(API_DESCRIPTION,INIT_DB);
// 바로 원하는 API를 호출한다.
console.log(await studentMockAPI.get("/student"));
// {message: "Successfully retrieved data",State:"success",data:{students:[{"name":"foo","email":"email@email.com","grade":4}]}}
console.log(await studentMockAPI.post("/student",{name:"bar",email:"bar@bar.com",grade:3}))
// {message:"Student added successfully.",State:"Success",data:{name:"bar",email:"bar@bar.com",grade:3}}

구현

GPT 요청 구현

GPT API에 요청하는 기능은 간단하고 공식문서에도 잘 설명되어 있어서 어렵지 않게 구현했습니다.

  • OpenAI API 키를 발급한다.
  • 환경변수에 키를 등록한다.
  • 아래와 같이 프롬프트(질문)을 요청으로 보내고 응답으로 받는 코드를 작성한다.
import { Configuration,OpenAIApi } from "openai";

if(!process.env.OPENAI_API_KEY){
    throw new Error("OPENAI_API_KEY is not set");
}

const configuration = new Configuration({
    apiKey: process.env.OPENAI_API_KEY,
  });

const openai = new OpenAIApi(configuration);

const requestOpenAI=(prompt : string): Promise<string| undefined>=>
    openai.createCompletion({
        model: "text-davinci-003",
        prompt: prompt,
      }).then((response) => {
        return response.data.choices[0].text;
      });

GPT 응답 테스트

효과적인 mockAPI를 위해선 GPT가 어떤 형식으로 대답하는지 분석해야합니다.

처음 작성한 프롬프트 형식은 다음과 같습니다.

const createRequestText=(api : string, data : object,database : object,apiDescription:string): string=>{
    return `
    ${apiDescription} API Call (indexes are zero-indexed) : ${api}\\n\\n
    RequestBody : ${JSON.stringify(data)}\\n\\n
    Database State : ${JSON.stringify(database)}\\n\\n
    Output the API response prefixed with 'Response:' and as json.
    Then output the new database state as json, prefixed with 'State:'.
    If the API call is only requesting data, then don't change the database state, 
    but base your 'API Response' off what's in the database.
    `
}

테스트를 위해서 아래와 같은 학생 관리 API 예제를 만들었습니다.

async function test(){
    const studentMockAPI = createMockAPI("University Studenet Management",{students : [{name : "foo",email:"email@email.com",grade:4}]});

    console.log(await studentMockAPI.get("/get_all_student"));
    console.log(await studentMockAPI.post("/add_student",{name:"bar",email:"bar@bar.com",grade:3}))
    console.log(await studentMockAPI.get("/get_all_student"));
    console.log(await studentMockAPI.delete("/delete_student(grade=4)"))
    console.log(await studentMockAPI.get("/get_all_student"));
    console.log(await studentMockAPI.put("/revise_student(grade=3)",{name : "barbar",email : "barbar@bar.com",grade:5}))
    console.log(await studentMockAPI.get("/get_all_student"));
}

위 테스트한 결과, GPT의 응답이 비일관적인 것을 확인했습니다. 따라서 응답에서 API 응답과 갱신된 데이터베이스를 파싱하는 과정에서 에러가 발생했습니다. 이를 해결하기 위해선 정확한 응답 형식을 규정해야했습니다.

Regex 조건 추가

이번엔 “Total output must look like this:'Response: (.)\nState:(.)' with compact json.” 라는 문장을 프롬프트에 추가했습니다. 그 결과, 응답을 API 응답과 데이터베이스로 파싱하는 코드는 잘 동작했습니다.

const createRequestText=(api : string, data : object,database : object,apiDescription:string): string=>{
    return `
    ${apiDescription} API Call (indexes are zero-indexed) : ${api}\\n\\n
    RequestBody : ${JSON.stringify(data)}\\n\\n
    Database State : ${JSON.stringify(database)}\\n\\n
    Output the API response prefixed with 'Response:' and as json.
    Then output the new database state as json, prefixed with 'State:'.
    Total output must look like this:'Response: (.*)\\nState:(.*)' with compact json.
    If the API call is only requesting data, then don't change the database state, 
    but base your 'API Response' off what's in the database.
    `
}

그러나 이번엔 응답이 비일관적이라는 문제가 생겼습니다. 어떤 응답은 메시지만, 다른 건 상태코드만 등 제각각인 형태는 MockAPI를 사용하는데 문제가 될 수 있습니다.

API 응답 일관화

이번엔 “The API response must look like this:'{message: "(.)",status:"{status code}",data:{(.)}}' with compact json. with compact json.” 라는 문장을 추가해 API 응답 형식이 일관되도록 해보았습니다.

그 결과, 아래와 같이 비교적 일관된 Json 결과들을 받을 수 있었습니다.

{"message":"Students retrieved successfully.","status":200,"data":[{"name":"foo","email":"email@email.com","grade":4}]}
{"message":"Student added","status":200,"data":{"name":"bar","email":"bar@bar.com","grade":3}}
{message: "All student retrieved successfully", status:"200", data: [{"name":"foo","email":"email@email.com","grade":4},{"name":"bar","email":"bar@bar.com","grade":3}]}
{message: "Student deleted successfully.",status: "200",data:{}}
{"message":"Fetched all student data successfully.","status":200,"data":[{"name":"bar","email":"bar@bar.com","grade":3}]}
{"message":"Student updated","status":200,"data":{"name":"barbar","email":"barbar@bar.com","grade":5}}
{"message": "Successfully retrieved all student records", "status": 200, "data": {"students": [{"name": "barbar", "email": "barbar@bar.com", "grade": 5}]}}

그러나 아직 부족합니다. 같은 get_all_student API에 대해서 서로 다른 메시지를 반환하고 있습니다. 좀 더 일관화하기 위한 방법이 필요합니다.

GPT 요청 파라미터 튜닝

GPT Completion 요청 시에는 프롬프트 뿐만 아니라 여러 설정값을 튜닝할 수 있습니다. 여기서 창의적인 답변 정도를 뜻하는 temperature 파라미터의 값을 0으로 설정하고 추가적으로 다른 파라미터도 설정후 요청을 해보았습니다.

const requestOpenAI=(prompt : string): Promise<string| undefined>=>
    openai.createCompletion({
        model: "text-davinci-003",
        temperature:0,
        max_tokens: 100,
        top_p:1,
        frequency_penalty:0,
        best_of:1,
        prompt: prompt,
      }).then((response) => {
        return response.data.choices[0].text;
      });

그 결과 더욱 일관적인 API 응답을 얻을 수 있었습니다. 이 정도면 MockAPI로 사용할 수 있을 것 같습니다.

{"message":"Successfully retrieved all students","status":200,"data":[{"name":"foo","email":"email@email.com","grade":4}]}
{"message":"Student added successfully","status":200,"data":{"name":"bar","email":"bar@bar.com","grade":3}}
{"message":"Successfully retrieved all students","status":200,"data":[{"name":"foo","email":"email@email.com","grade":4},{"name":"bar","email":"bar@bar.com","grade":3}]}
{"message":"Student deleted successfully","status":200,"data":{}}
{"message":"Successfully retrieved all students","status":200,"data":[{"name":"bar","email":"bar@bar.com","grade":3}]}
{"message":"Student successfully updated","status":200,"data":{"name":"barbar","email":"barbar@bar.com","grade":5}}
{"message":"Successfully retrieved all students","status":200,"data":[{"name":"barbar","email":"barbar@bar.com","grade":5}]}

응답 형식 설정

하지만 사용자에 따라서 다른 종류의 API 응답형식이 필요할 수 있습니다. 이를 위해 응답 형식을 설정할 수 있는

디자인

그래서 이번엔 새로운 조건을 설정할 수 있는 기능 디자인을 구상했습니다.

//대학교 학생 관리라는 목적과 초기 학생 데이터베이스를 정의한다.
const API_DESCRIPTION = "University Studenet Management"
const INIT_DB={students : [{name : "foo",email:"email@email.com",grade:4}]}
const RESPONSE_FORMAT = '{*status:"{status code}",data:{(.*)}}'
const ERROR_RESPONSE_FORMAT='{message : {(.)},status:"{stattus code}"}'
const studentMockAPI = createMockAPI(API_DESCRIPTION,INIT_DB)
	.setSuccessResponseFormat(CONDITION)
	.setFailureResponseFormat(ERROR_RESPONSE_FORMAT)

그 결과 아래와 같이 API 수행 성공과 실패에 따라서 다른 응답 형식을 설정할 수 있었습니다.

console.log(await studentMockAPI.delete("/student(grade=5)"));
//{"status":"404","message":"No student found with grade 5"}
console.log(await studentMockAPI.get("/students"));
//{"data":[{"name":"foo","email":"email@email.com","grade":4}], "status":200}

결론

  • GPT를 이용해서 생각보다 괜찮은 MockAPI를 만들 수 있다.
  • 그러나 GPT가 항상 일정한 응답을 하지 않으므로 에러가 가끔 발생할 수 있으므로 이 경우 직접 mock API를 만들어야 한다.
  • 일반적인 mock API 보다 속도가 느리다. 따라서, 규모가 큰 테스트 사용에는 적합하지 않으며 일부 유닛테스트 사용하는 것이 적합하다.
  • 저장할 수 있는 데이터 크기에 한계가 존재한다. backend-GPT 에서는 1KB 정도가 한계라고 한다.

Npm

https://www.npmjs.com/package/@beoks/gpt-mock-api

+ Recent posts