오랜만에 블로그 포스팅을 써보는 것 같다. 조금 바빴다는 핑계로..ㅎㅎ 이제는 다시 포스팅을 조금씩 써보려고 한다.
오늘은 지금까지 애를 많이 먹었던 NestJS 프레임워크를 활용해서 Microsoft Azure OpenAI 연동 방법과 연동 후 기대에 부응하지 못했던 점을 공유드려보려고 한다. NestJS를 가지고 Azure OpenAI를 연동하는 자료는 크게 없는 것 같아서 틈새시장을 노려보겠다 '__'
1. GPT 모델을 가지고 어떤 기능을 구현하려고 했는지?
기획 유출이 될 수 있어 자세하게는 설명할 수 없지만, 민감한 사진과 프롬프트를 가지고 AI 모델에게 분석요청을 요청해서 해당 사진에 대해 분석 결과를 도출해내고, 해당 결과의 변화를 추적할 수 있는 기능을 만들고자 했다.
참고로, GPT 4모델 이상부터는 Vision 기능이 있어서 모델이 이미지를 입력받고 이에 대한 프롬프트에 응답을 할 수 있다.
대표적으로는 GPT-4o, GPT-4o-mini, GPT-4-turbo 등이 있다고 한다.
2. OpenAI (ChatGPT) vs Microsoft Azure OpenAI => Azure OpenAI
처음 입문해서 모델을 생성해야 하는 분들은 해당 링크를 참고하면 좋을 것 같다. 여기서 추가적으로 필자가 설명해보자면..
위의 블로그를 참고해서 잘 모델을 생성하면 다음과 같은 화면을 볼 수 있을 것이다.
여기서 우리는 Azure OpenAI Studio로 이동을 클릭해서 Studio로 들어가고, 배포 탭으로 들어가서 모델 배포 -> 기본 모델 배포를 클릭해보자.
그러면 모델을 선택할 수 있는 화면이 나온다. 필자는 GPT-4o 를 선택했다.
선택하고 나면 아래 화면을 볼 수 있다.
배포 유형은 Azure의 공식문서에 잘 설명이 되어있으니 참고해주시고, 필자는 표준 유형을 선택했다.
그리고 여기서 한가지 중요한점은 초반 무료 계정인 개발자분의 화면은 분당 토큰 속도 제한이 1K까지로 제한이 되어있을 것이다.
이는 무료계정이기 때문에 그렇다. 필자는 1K 이상의 토큰을 사용하기 때문에 해당 부분을 해결해야 했다.
해결하지 못하면 아래 사진과 같이 에러가 발생하는데 해당 에러를 마주쳤다면 아래 내용을 참고해서 해결해보자!
이제 설명할 내용은 100% 정확한 정보는 아니니까 참고해주길 바란다.
결론은 무료계정에서 종량제 계정으로 업그레이드를 해야한다. 계정을 업그레이드하면 필자가 올린 위 사진과 같이 1K 이상으로 설정할 수 있다. 종량제 계정으로 업그레이드를 하면 돈이 나가지 않냐? 돈 나간다. 그렇지만 우리는 처음 계정을 생성하면 200달러의 크레딧을 받기 때문에 해당 크레딧을 먼저 사용하고 나서 돈이 청구가 되니까 참고해주길 바란다.
위의 문서만 참고해도 충분히 코드를 작성할 수 있다. 해당 문서에는 API_TOKEN 방식과 Microsoft Entra ID 방식을 소개하고 있는데 API_TOKEN 모드는 보안상 프로덕션 모드에서는 크게 추천하지는 않는다고 한다. 하지만 필자는 현재 개발 모드이기 때문에 API_TOKEN방식으로도 충분할 것 같아서 API_TOKEN 방식을 선택했다.
(1) Package Install
openai에서 AzureOpenAI 생성자를 지원해주는것 같다.
npm i openai
(2) Service source code
import { AzureOpenAI } from 'openai';
@Injectable()
export class OpenAIService {
private client: AzureOpenAI;
constructor() {
this.client = new AzureOpenAI({
deployment: '<사용하려는 모델>',
apiVersion: '<OpenAI API VERSION>',
endpoint: '<Azure OpenAI Studio에 있는 endpoint 주소>',
apiKey: '<Azure OpenAI API KEY>'
});
}
public async test(): Promise<void> {
const response = await this.client.chat.completions.create({
model: 'gpt-4o-2024-05-13',
messages: [
{
role: 'user',
content: [
{ type: 'text', text: '안녕하세요! JavaScript의 ES6에 대해서 설명해주세요.'},
// 만약 이미지를 추가로 넣어야 한다면?
// { type: 'image_url', image_url: { url: '<image url>', detail: 'auto' }}
]
}
],
temperature: 0 // 낮으면 낮을수록 모델의 응답 결정도가 높아짐.
});
console.log(response);
console.log(response.choices[0].message.content);
}
}
JavaScript의 ES6에 대해 물어봤더니 정상적으로 답변을 받았다.
5. 필자는 민감한 사진을 핸들링한다고 했다. 어떻게 해결하는지?
아마 민감한 사진이 담겨있는 사진의 URL을 위의 코드에 삽입하여 실행을 시키면 다음 에러가 발생할 것이다.
안녕하세요! 현재 NodeJS로 백엔드 취업을 희망하고 있는 이강욱이라고 합니다. 최대한 블로그 포스팅을 보시는 분들에게 자세한 정보와 즐거움을 드리기 위해 노력하며 포스팅을 쓰고 있습니다 ㅎㅎ
읽어보시고 문제가 있거나 잘못되었다고 생각되는 부분들은 언제든 댓글로 작성해주시면 최대한 빠르게 조치하겠습니다!
감사합니다.
forFeature를 통해 ConfigModule에 설정 파일을 등록하는 것이다. 그러면 configService로 접근을 할 수가 있다는 것이다.
그리고 필자는 getAppConfig라는 유틸 함수를 따로 만들어서 관리를 했다.
getAppConfig 메서드과 AppConfigType
/**
* Get App Config.
* If you use useFactory in module level, you can use this function with applying
* return type.
*
* @param configService
* @returns
*/
export const getAppConfig = (configService: ConfigService): AppConfigType => {
const config = configService.get('app');
if (!config) {
throw new InternalServerErrorException('앱 설정에 문제가 있습니다.');
}
return config;
};
// AppConfigType?
type AppConfigType = ConfigType<typeof appConfig>;
만약에 저렇게 유틸함수로 빼지 않으면 Dynamic Module 등록할 때 저 로직들을 계속 작성해주어야 한다.
중복되는 부분을 제거하기 위해서 유틸 함수로 뺐다고 생각하자.
그리고 AppConfigType은 위와 같이 작성해주면 된다.
이렇게 Config를 Type-Safe하게 관리할 수 있도록 해봤다.
아무래도 TS를 사용하다 보니까 Type Checking이 안되는데에 민감한 것 같다 ㅋㅋ
안녕하세요! 현재 NodeJS로 백엔드 취업을 희망하고 있는 이강욱이라고 합니다. 최대한 블로그 포스팅을 보시는 분들에게 자세한 정보와 즐거움을 드리기 위해 노력하며 포스팅을 쓰고 있습니다 ㅎㅎ
읽어보시고 문제가 있거나 잘못되었다고 생각되는 부분들은 언제든 댓글로 작성해주시면 최대한 빠르게 조치하겠습니다!
감사합니다.
[NestJS] Request에 user 정보를 넣으실 때 any 타입으로 하시는 분들 많으시죠?
Framework/NestJS2024. 1. 23. 05:30
오늘은 포스팅을 2개나 작성한다..ㅎㅎ 그만큼 몰랐던 것들을 오늘은 좀 많이 알게 된 것 같아서 뿌듯해서 그런 것 같다 (사실 별건 아니지만)
이전 포스팅에서 strict 모드를 추가하면서 많은 에러가 발생했다고 언급했다. null parameter가 들어올 수 있는 가능성이 있는 코드들 등등을 수정하는 가운데 request의 user 변수의 type이 지정되지 않아서 발생한 문제가 있었다.
어떻게 request 안에 넣어놓은 user 데이터에 type을 부여할 까 고민하다가 좋은 글을 참고하게 되어서 조금 참고하고 작성해보려고 한다.
문제 상황
strict 모드에는 any 타입이 사용되는 경우 에러를 뱉는 옵션이 있다. 필자는 TypeScript로 개발을 하기 때문에 any 타입을 사용하게 되면 그 이점이 사라지기 때문에 진짜 웬만하면 any 타입을 절대 사용하지 않으려고 한다.
key의 req 파라미터를 보면 타입이 Request이다. 이 Request는 express 라이브러리에서 제공하는 타입이다. 그리고 함수 내부에서 userId를 req 변수의 user에서 id를 가져오는 부분이 있는데 이 부분이 지금 문제다!
req의 user가 any 타입이라는 것이다. 우리는 any 타입을 지양하고 있기 때문에 req.user 변수에 custom type을 설정해주어야 한다.
해결 방법
물론 이거보다 더 좋은 방법은 있을 것이지만 필자가 판단했을 땐 해당 방법도 따로 문제가 없어보이고 간단하기 때문에 선택했다.
// request-with-user.type.ts
import { Request } from 'express';
import { DecodedUserToken } from 'src/modules/api/user/types';
// Request에 user를 넣을 때 custom-type을 지정해주는 인터페이스
export interface IRequestWithUserType extends Request {
user: DecodedUserToken;
}
필자는 types 디렉토리에 해당 파일을 생성했다. 하나의 인터페이스를 만들었는데 우리는 Request에 접근을 하고싶기 때문에 extends를 통해 Request를 적용시켰다. 그리고 우리는 결국 로그인 또는 회원가입 과정을 거치고 나서 request 변수에 user 라는 key로 사용자 정보를 request에 넣을 것이기 때문에 인터페이스 내부에 user라는 키를 추가했다.
그리고 DecodedUserToken은 필자가 설계한 User Entity에서 민감한 정보를 제외한 나머지 Property들의 Interface다.
그리고 위의 사진과 같이 req 변수의 타입을 Request가 아닌 위에서 생성한 IRequestWithUserType을 통해 req.user의 타입이 DecodedUserToken 이라는 것을 명시해줬다.
안녕하세요! 현재 NodeJS로 백엔드 취업을 희망하고 있는 이강욱이라고 합니다. 최대한 블로그 포스팅을 보시는 분들에게 자세한 정보와 즐거움을 드리기 위해 노력하며 포스팅을 쓰고 있습니다 ㅎㅎ
읽어보시고 문제가 있거나 잘못되었다고 생각되는 부분들은 언제든 댓글로 작성해주시면 최대한 빠르게 조치하겠습니다!
감사합니다.
안녕하세요! 현재 NodeJS로 백엔드 취업을 희망하고 있는 이강욱이라고 합니다. 최대한 블로그 포스팅을 보시는 분들에게 자세한 정보와 즐거움을 드리기 위해 노력하며 포스팅을 쓰고 있습니다 ㅎㅎ
읽어보시고 문제가 있거나 잘못되었다고 생각되는 부분들은 언제든 댓글로 작성해주시면 최대한 빠르게 조치하겠습니다!
감사합니다.
최근에 몸이 너무 안좋고 무기력해서 아예 1주일 넘게 쉬어버렸다. 여러분 모두 감기조심... 자세조심.. 허리조심 하시길..!
그래서 그런지 오랜만에 블로그 포스팅을 시작으로 다시 열심히 준비해서 내년 초반기에 꼭 취업을 해보려고 한다!
Nest에서 400번대 에러, 정확히는 Class-Validator의 에러 메세지는 배열로 나오는 것을 알고 계실 것이다. 그런데 프로젝트에서도 에러코드 + 단일 메세지 조합으로 Response를 달라고 하시는 개발자분이 계시고, 커뮤니티에서도 몇몇 분이 질문으로 올려주시더라.
그런데 구글링을 몇개 해봤는데 그런 자료들이 없는 것 같아서.. 필자가 글을 한번 올려볼까 한다.
참고로 아래 작성되는 방법은 필자의 뇌피셜로 구현을 한 것이라서,, 부정확할 수도 있다는 점 참고해주었으면 좋을 것 같고, 잘못된 부분이 있다면 많이많이 피드백을 해주시면 좋을 것 같다.
catch 메서드는 Exception이 발생했을 때 딱 잡는 곳이라고 생각하면 될 것 같다. 안의 코드들을 세부적으로 살펴보자면..
exceptionResponse 변수
getResponse()를 통해 우리는 에러 Response를 가져올 수 있다. TExceptionResponse는 ExceptionCodeDto 또는 ValidationError 타입이 될 수 있다. ValidationError는 Class-Validator를 통해 생긴 에러 객체라고 생각하면 될 것 같다. 그리고 ExceptionCodeDto는 code와 message로 구성이 되어있는데 예시를 들어주겠다.
// common.exception-code.ts
MultipartJsonFormatError: setExceptionCode(
'8011',
'올바른 JSON 형식으로 입력해주시고 Multipart에서 이미지를 제외한 데이터는 항상 data라는 키로 JSON 형태로 보내주세요.',
),
// main.ts
throw new BadRequestException(
CommonExceptionCode.MultipartJsonFormatError,
);
필자는 저렇게 커스텀으로 에러코드를 만드는 편이다. setExceptionCode의 첫번째 요소는 커스텀 에러 코드고, 2번째 요소는 그에 해당하는 메세지이다. 이후 애플리케이션 내에서 저렇게 throw를 통해 에러를 보내야 할 경우가 생기는데 이 경우에 ExceptionCodeDto를 사용하는 것이다.
exceptionJson 변수
generateBasicExceptionResponse 메서드는 그냥 기본 에러 골격을 잡아주는 기능이라고 생각하면 된다. statusCode와 요청 경로를 넣어주면 포함시켜서 기본 에러 골격을 만들어준다.
export class ExceptionResponseDto {
public readonly isSuccess: boolean;
public readonly statusCode: number;
public readonly timestamp: string;
public readonly path: string;
public target: string;
public message: string;
public errorCode: string;
public errorStack: string;
private constructor(statusCode: number, path: string) {
this.isSuccess = false;
this.statusCode = statusCode;
this.errorCode = '';
this.target = '';
this.message = '';
this.errorStack = '';
this.timestamp = getKoreaTime();
this.path = path;
}
static from(statusCode: number, path: string): ExceptionResponseDto {
return new ExceptionResponseDto(statusCode, path);
}
}
/**
* @summary ValidationError OR ExceptionCodeDto인지 확인 후 예외 결정하는 함수
* @author Jason
*
* @param { ExceptionResponseDto } exceptionJson
* @param { TExceptionResponse } exceptionResponse
*/
decideBadRequestExceptionByInstance(
exceptionJson: ExceptionResponseDto,
exceptionResponse: TExceptionResponse,
): void {
// Class-Validator를 통해 나온 에러 객체
if (exceptionResponse instanceof ValidationError) {
const validationResult = this.getExceptionObj(exceptionResponse); // 첫 에러 가져오기
this.setBadRequestException(
exceptionJson,
validationResult.code,
validationResult.message,
validationResult.target,
);
return;
}
this.setBadRequestException(
exceptionJson,
exceptionResponse.code,
exceptionResponse.message,
);
}
매개변수에서 exceptionJson은 아까 우리가 위에서 만든 기본 에러 골격이다. 그리고 exceptionResponse는 filter에서 catch한 에러 리스폰스이다. 그래서 우리는 로직에서 ValidationError인지 아닌지를 판단해서 exception을 다르게 한다. 보면 차이점은 target을 넣느냐마냐 + getExceptionObj 메서드의 유무이다. target을 넣은 이유는 디버깅 + 클라분들 에러가 어디서 발생하는지 확인하는 용도라고 생각하면 될 것 같다. 그리고 getExceptionObj는 추후 설명하도록 하겠다.
이후에 공통적으로 setBadRequestException을 통해 에러 기본 골격에 세팅을 해준다.
그러면 이제 getExceptionObj 메서드가 왜 ValidationError인 경우에만 있는지 설명하겠다. 필자가 위에서 말했듯이 ValidationError가 아닌 경우는 개발자가 직접 애플리케이션 단에서 throw를 통해 에러를 보내준 경우이다. 이 경우에는 가공할 필요가 없기 때문에 따로 전처리 과정 없이 바로 내보내줘도 된다. 그러면 이제 getExceptionObj 메서드를 살펴보자.
getExceptionObj 메서드와 getExceptionInChildren 메서드
/**
* @summary Exception을 뱉어줄 메서드
* @author Jason
* @param { ValidationError } validationError
* @returns { TBadRequestException }
*/
getExceptionObj(validationError: ValidationError): TBadRequestException {
const errorChildren = validationError.children;
// Error Children이 없을 시 ==> 바로 반환해주면 됨
if (isEmptyArray(errorChildren)) {
const target = validationError.property;
const errorContext = validationError.contexts as TValidationErrorContext;
const key = errorContext ? Object.keys(errorContext)[0] : '';
return {
...errorContext?.[key],
target,
} as TBadRequestException;
}
return this.getExceptionInChildren(errorChildren);
}
먼저, TBadRequestException 타입은 { code, message, target? } 이라고 생각하자. 여기서 일단 먼저 알아둬야 할 부분은 children 부분이다. children은 class-validator에서 nested한 부분을 처리할 때 여기에 넣곤 한다. 예를 들어 보겠다.
"area": [
{
"date": "2023-11-22"
}
],
위와 같은 객체가 있다고 했을 때 필자는 각 배열의 원소에 value라는 key가 있는지 확인해야 한다. 이렇게 key-value 구조에서 value가 nested할 때 children에 들어간다고 알면 되겠다.
이제 위 코드를 보면, children 배열이 비어있으면 따로 추가 처리 없이 반환해주게 된다. 이 때 context는 다음과 같다.
위 사진에서 contexts를 볼 수 있을 것이다. 보면 IsNotEmpty를 key로 code와 message가 있다. 그래서 key는 context가 있는 경우에 object.keys의 0번째 원소를 가져온다. 0번째를 가져오는 이유는 nest에서 에러 메세지는 배열로 구성되어 있는데 맨 위의 메세지를 보여주려고 하기 때문이다. 그리고 그 첫번째 key를 가지고 code와 message를 추출한다. 그리고 target까지 구성해서 return 해준다.
children 배열이 비어있지 않은 경우를 살펴보겠다.
위의 사진과는 다르게 children 객체가 들어가있다. children 객체에 데이터가 들어가있다는 것은 nested 객체에 유효성 에러가 발생했다는 뜻이고 거기에 접근해야 에러처리가 가능하다는 것이다. 그래서 children 객체에 들어가보겠다.
children 객체를 들어가보니 위에서 보던 ValidationError와 동일한 구조이다. 즉, children도 ValidationError 객체로 이루어져있다. 위의 사진을 보면 children 배열은 비어있고 context가 들어가있는 것을 볼 수 있다. context에 에러 내용이 포함되어 있다.
매개변수로 children 배열을 받으면 일단 가장 첫번째 children을 가져온다. 그리고 children 내에 context가 있는 경우에 우리가 getExceptionObj에서 처리한 것 처럼 처리한다.
그런데 만약에 context도 없고, children도 비어있는 배열이라면 context가 설정되어 있지 않다고 500번대 에러를 발생시킨다. 이 것은 필자가 Class-Validator쪽에 context를 지정해주지 않았기 때문이다.
만약 children이 있다면 context를 지정해 준 것이기 때문에 다시 재귀함수로 호출해준다. 왜냐면 children 안에 또 children이 있을 수 있기 때문이다. nested 안에 또 nested 객체가 들어갈 수 있다는 의미이기 때문에 재귀를 통해 처리하기로 했다.
위와 같이 처리하게 되면 다음과 같이 에러를 받을 수 있다.
원래 Nest의 Response와는 다르게 message를 단일로 받을 수 있게 되었다. 위 코드를 작업한 뒤 main.ts에 Filter를 Global하게 적용해주면 된다.
참고로, 재귀함수를 호출하는 과정에서 코드를 개선할 수 있을 것 같다고 생각이 드는데.. 일단 필자의 머리로는 이것이 최선인 것 같다.
안녕하세요! 현재 NodeJS로 백엔드 취업을 희망하고 있는 이강욱이라고 합니다. 최대한 블로그 포스팅을 보시는 분들에게 자세한 정보와 즐거움을 드리기 위해 노력하며 포스팅을 쓰고 있습니다 ㅎㅎ
읽어보시고 문제가 있거나 잘못되었다고 생각되는 부분들은 언제든 댓글로 작성해주시면 최대한 빠르게 조치하겠습니다!
감사합니다.