(326)

[NestJS] enum과 literal type 중 어떤걸 사용할까? (feat. Tree-shaking, Template Literal Ty

정확한 정보 전달이 아닌, 여러 좋은 포스팅들을 보며 적용해보고개인의 관점에서의 의견 서술입니다. 여러 피드백들을 적극 환영합니다.    요약트리 쉐이킹(Tree Shaking)은 번들링 시 사용하지 않는 불필요한 코드를 제거하는 최적화 작업을 말한다.프론트에서의 트리쉐이킹은 번들의 크기를 최소화해서 UX의 향상에 목적이 있다고 하지만 백엔드 관점에서의 최적화는 코드의 안정성, 유지보수 등에 초점이 맞춰지고, 프로젝트의 특성과 요구사항과 등을 고려하는 것이 좋다고 생각한다.TypeScript4.1에 추가된 Template Literal Type처럼, 명시된 타입들을 조합하는 복잡한 타입 조합이 필요하지 않을 경우, 이넘을 사용하는 것이 어떠한 이넘 값으로 강제되기 때문에 오히려 더 명확한 의도를 전달할 ..

[Grafana Loki] Data source connected, but no labels received. Verify that Loki and Promtail is configured properly

에러 원인 라벨 설정 시 설정했던 라벨이 존재하지 않음. 이는 로키는 제대로 연결되었지만, 로그 파일을 제대로 프롬테일에서 받아오지 못했음을 의미한다. 본인의 경우는 프로젝트의 도커컴포즈 볼륨 설정에서, 경로가 제대로 작성되지 않아 망운트가 제대로 되지 않았음. 걸린 시간 3시간 남짓 에러 해결 프로젝트의 로그생성 docker exec -it containerName /bin/sh 프로젝트 내부에서는, 루트 경로에 logs폴더 내부에서 날짜, 에러레벨에 맞게 분기처리를 하여 로그를 생성했다. 호스트 서버에서, 위 명령어를 통해 컨테이너 내부로 진입하는데, 진입하자마자 logs경로에 로그가 잘 들어오고 있길래 당연히 잘 동작할거라 생각했지만 그라파나에서 로키의 ip:port로 커넥션을 연결하는 과정에서 계..

[NestJS] 코드 리팩토링하기 - 응집도를 높이고 의존성을 명확하게

서론 요즘 좋은 코드 라는 키워드에 대해 특히 변경과 재사용이 용이한, 높은 응집도와 낮은 결합 관계 에 대해 많이 생각하고 있다. 특히 기존 레거시를 모두다 걷어내기에는 시간적으로 애로사항이 있어 틈틈이 관련된 프로젝트에 들어갈 때, 해당 로직에 대한 레거시들을 최대한 바꾸려고 노력하고 있다. [네이버클라우드 개발자 스토리] 좋은 코드란 무엇일까?🤔 #클린코드 이야기 📍 “좋은 코드를 짜야 한다”​ medium.com 특히, 상품의 리뷰를 불러오는 함수를 수정해야 하는 일이 최근에 있었는데, 상품군 7~8개의 하위 상품에 대한 리뷰를 모두 다른 함수에서 불러오는 것을 보고 경악을 금치 못했다. (급한 사항이라 판단되어 우선 프로덕션에 수정해서 반영한 뒤 구조를 수정하였다..) 나도 최근에 신규 프로젝트..

[NestJS] TypeORM 0.3 버전의 CustomRepository 생성, Repository패턴 적용하기

0.2 버전 사내 서비스의 TypeORM버전은 0.2버전대를 사용중이다. 0.2버전대에서는 @EntityRepository 데커레이터를 지원하여, Repository를 커스텀화하여 리파지토리 클래스를 생성할 수 있었고, 이에 따라 Service와 Repository레이어를 분리하여 결합도를 낮출 수 있었다. @Injectable() export class RsvcenterService { constructor( @InjectRepository(CustomRsvRepository) private readonly customRsvRepo: CustomRsvRepository, //DB 관련 로직 예외처리 Provider private readonly customEm: CustomEntityManager, )..

[TypeORM / QueryBuilder] Relation with property path confirms in entity was not found

최근 레거시 코드 중 DB 관련 로직들을 거의 대부분 쿼리빌더로 변경하는 작업을 완료하고, 검수중에 있다. 그 과정에서 발생한 에러들을 하나하나 정리하여 남기려고 한다. 에러 메세지 원인 관계 매핑이 정확하지 않아서 발생했다. 나의 경우는 아래 이유 때문에 발생했는데, TypeORM의 쿼리빌더를 사용하는 과정에서, 커스텀 리파지토리를 생성하여, 해당 리파지토리에서 두 테이블을 조인해서 사용했는데, 처음 INNER JOIN을 시도한 테이블에서, enterprise라는 엔터티에 대한 정의를 내리지 않았기 때문에 발생했다. 해당 엔터티를 살펴보면, export class EasyBookWhichEntEntity { @PrimaryGeneratedColumn({ type: 'int', name: 'no' }) ..

[NestJS] JWT Token 인증 - (3) Refresh Token 사용

이전 글에서 이어집니다. [NestJS] JWT Token 인증(Access Token) 구현하기(with passport) - (2) 서론 지난글에 이어, Nest에서 JWT를 사용한 인증을 진행해보려한다. [NestJS] JWT Token 인증 - (1) JWT 토큰이란? [NestJS] JWT Token 인증 - (1) JWT 토큰이란? 쿠키와 세션을 통한 인증 쿠키와 세션 (Cookie & Sessio mag1c.tistory.com Access Token의 한계 Access Token을 통해 웹사이트를 이용하다, 토큰이 만료되면 다시 재로그인을 해야만 하는 불편함을 겪게 된다. 또한 Access Token 만료 전 토큰을 탈취당하게 된다면 토큰이 만료될 때까지 속수무책이라는 보안상의 문제도 있다..

[NestJS] JWT Token 인증(Access Token) 구현하기(with passport) - (2)

서론 지난글에 이어, Nest에서 JWT를 사용한 인증을 진행해보려한다. [NestJS] JWT Token 인증 - (1) JWT 토큰이란? [NestJS] JWT Token 인증 - (1) JWT 토큰이란? 쿠키와 세션을 통한 인증 쿠키와 세션 (Cookie & Session) HTTP 프로토콜의 특징 비연결성 ( Connectionless ) 클라이언트가 서버에 요청(Request)할 때, 그에 대한 응답(Response)을 한 후, 연결을 끊는다. 비상태 mag1c.tistory.com NestJS에서 제공하는 공식 문서의 인증 관련 페이지를 참조했다. Documentation | NestJS - A progressive Node.js framework Nest is a framework for b..

[NestJS] JWT Token 인증 - (1) JWT 토큰이란?

쿠키와 세션을 통한 인증 쿠키와 세션 (Cookie & Session) HTTP 프로토콜의 특징 비연결성 ( Connectionless ) 클라이언트가 서버에 요청(Request)할 때, 그에 대한 응답(Response)을 한 후, 연결을 끊는다. 비상태성 ( Stateless ) 클라이언트의 상태 정보를 가지지 않는 mag1c.tistory.com 쿠키는 인증이 필요한 요청을 할 때마다 쿠키를 던져 요청하는 동작 구조를 가진다. 쿠키를 통한 인증 시 다음과 같은 단점들이 존재한다. 쿠키의 단점 1. 민감 정보들을 노출당하기 쉽고 조작당하기도 쉽다. 2. 웹 브라우저마다 지원 형태가 다르기 때문에 공유가 불가능하다. 3. 쿠키의 사이즈 제한(4KB)이 있어 충분한 데이터를 담을 수 없다. 4. 서버에 매..

[NestJS] typeorm의 transaction

서론 JWT를 구현하는 도중, 아래와 같은 에러가 발생했다. //회원가입 및 JWT TOKEN 발급 코드 async signup(signInDto: UserSigninDto) { const user = await this.userService.findById(signInDto.id); const exist = user != null; if (exist) { throw new BadRequestException('중복된 아이디 존재'); } else { const valiUser = new UserEntity(); const hashedPassword = this.hash(signInDto.pw); valiUser.id = signInDto.id; valiUser.pw = hashedPassword; co..

[NestJS] enum과 literal type 중 어떤걸 사용할까? (feat. Tree-shaking, Template Literal Ty

Tech/NodeJS 2024. 3. 29. 15:16
728x90
728x90

 
 
 

 
정확한 정보 전달이 아닌, 여러 좋은 포스팅들을 보며 적용해보고
개인의 관점에서의 의견 서술입니다. 여러 피드백들을 적극 환영합니다.

 
 
 
 

요약


트리 쉐이킹(Tree Shaking)은 번들링 시 사용하지 않는 불필요한 코드를 제거하는 최적화 작업을 말한다.

프론트에서의 트리쉐이킹은 번들의 크기를 최소화해서 UX의 향상에 목적이 있다고 하지만 백엔드 관점에서의 최적화는 코드의 안정성, 유지보수 등에 초점이 맞춰지고, 프로젝트의 특성과 요구사항과 등을 고려하는 것이 좋다고 생각한다.
TypeScript4.1에 추가된 Template Literal Type처럼, 명시된 타입들을 조합하는 복잡한 타입 조합이 필요하지 않을 경우, 이넘을 사용하는 것이 어떠한 이넘 값으로 강제되기 때문에 오히려 더 명확한 의도를 전달할 수도 있다.

 
 
 
 

Nest에서의 요청

@ApiTags("product")
@Controller("/api/v1/product")
export class ProductDetailController implements DetailController {

    constructor (private productService: ProductService) {}
    
    @ApiOperation({ summary: "상품 상세" })
    @ApiQuery({ name: "no", description: "상품 번호 PK", required: true })
    @UseGuards(VisitorGuard)
    @Get()
    async getProductDetail(@Query("no") no: number) {
    	return await this.productService.getProductDetail(no);
    }
    
}

 
위와 같은 컨트롤러에, 상품번호로 요청을 보낼 때, no는 number이 아니여도, NaN으로 비즈니스 로직까지 타고 넘어가서 동작하게 된다.


TypeScript에서 타입을 명시해도, 이는 컴파일 단계에서만 적용되는 타입 어노테이션이다.
실제 런타임에서는 데이터 타입을 변환하거나, 검증해주지 않는다는 소리다.
따라서 API 호출 시 쿼리 파라미터로 전달된 값이 숫자가 아닌 문자여서 서버 측에서 이를 숫자로 변환하려할 때,
런타임은 JavaScript환경이기 때문에 NaN으로 흘러가게 되는 것이다.

 
 
그렇기 때문에, 보통은 불필요한 자원 낭비를 피하기위해 Validation Pipe를 사용하거나, 객체로 변환해서 Class-Validator을 사용하곤 한다. 앞 단에서 걸러주지 않으면, 결국 비즈니스 로직에서 DB Connection까지 흘러가서 쿼리 파라미터 에러가 발생할 것이다.
 
어떤 요청이 컨트롤러에 도달하기 전에, Pipes에서 걸러지냐, NaN으로 비즈니스 끝까지 들어가서 쿼리에서 에러를 뱉어내냐는 꽤나 중요한 문제다. 불필요한 리소스를 낭비할 수 있다.

 


 
 


실제로는 number pk값과 같은 것들은 +를 붙여 number로 캐스팅하여 넘기기도 한다.
나는 예시를 들기위해 아래처럼 커스텀파이프를 만들었다.
실제로 간단한 Number검증은 기본적으로 제공하는 ParseIntPipe를 사용해도 된다.
@ApiTags("product")
@Controller("/api/v1/product")
export class ProductDetailController implements DetailController {

    constructor (private productService: ProductService) {}
    
    @ApiOperation({ summary: "상품 상세" })
    @ApiQuery({ name: "no", description: "상품 번호 PK", required: true })
    @UseGuards(VisitorGuard)
    @Get()
    async getProductDetail(@Query("no", ValidationNumberPipe) no: number) {
    	return await this.productService.getProductDetail(no);
    }
    
}
//의존성 주입을 통해 모듈 전역에서도 사용이 가능하다.
export class ValidateNumberPipe implements PipeTransform<string> {
  transform(value: string, metadata: ArgumentMetadata): number {
    const val = parseInt(value, 10);
    if (isNaN(val)) {
      throw new BadRequestException('Validation failed');
    }
    return val;
  }
}

 
종종 위처럼 Pipe의 구현체인 ValidationPipe를 사용하지 않고, 직접 구현해서 사용했었다.
 
 
여러 데이터들이 들어와 DTO를 구현해서 요청을 받을 때는, Class-Validator을 사용하기도 한다.

export class UserInfo {
    @IsString()
    @IsNotEmpty()
    @Length(4, 20)
    id: string;
    @IsString()
    @IsNotEmpty()
    @Length(4, 20)
    pw: string;
    @IsString()
    @IsNotEmpty()
    @Length(10)
    name: string;
}

 
 
 

이넘과 리터럴 타입

하지만, 조금 더 특정 데이터만 들어올 수 있게 강제하기위해 이넘타입을 많이 사용했었다.
이 이유에는, 타입스크립트 전에 스프링을 사용했기 때문도 있는 것 같다.

export enum RsvCategory {
  DEFAULT = '전체',
  WEDDINGHALL = '웨딩홀',
  HANBOK = '한복',  
  DRESS = '예복',
  GIFT = '예물',
  APPLIANCES = '혼수가전'  
}


export enum RsvSort {
  DEFAULT = '전체',
  COMPLETE = '답변완료',
  WAITING = '답변대기'
}

 
 
위의 코드들은 비교적 최근에 작업한 프로젝트 중 일부로, 데이터 조회를 위한 쿼리들에 이넘타입을 선언한 모습이다.
 
 
라인 기술블로그의 타입스크립트에서 이넘을 사용하지 않는게 좋은 이유 라는 포스팅을 보고나서 확인차 실제로 코드를 작성해보았다.
 
아래는 각각 이넘과 리터럴타입의 트랜스파일 코드들이다.

enum COIN {
    BITCOIN, ALTCOIN
}

enum ALTCOIN {
    ETHEREUM, RIPPLE, LITECOIN, DASH, MONERO, ZCASH, NEM, BITCOIN_CASH, DOGECOIN
}
var COIN;
(function (COIN) {
    COIN[COIN["BITCOIN"] = 0] = "BITCOIN";
    COIN[COIN["ALTCOIN"] = 1] = "ALTCOIN";
})(COIN || (COIN = {}));
var ALTCOIN;
(function (ALTCOIN) {
    ALTCOIN[ALTCOIN["ETHEREUM"] = 0] = "ETHEREUM";
    ALTCOIN[ALTCOIN["RIPPLE"] = 1] = "RIPPLE";
    ALTCOIN[ALTCOIN["LITECOIN"] = 2] = "LITECOIN";
    ALTCOIN[ALTCOIN["DASH"] = 3] = "DASH";
    ALTCOIN[ALTCOIN["MONERO"] = 4] = "MONERO";
    ALTCOIN[ALTCOIN["ZCASH"] = 5] = "ZCASH";
    ALTCOIN[ALTCOIN["NEM"] = 6] = "NEM";
    ALTCOIN[ALTCOIN["BITCOIN_CASH"] = 7] = "BITCOIN_CASH";
    ALTCOIN[ALTCOIN["DOGECOIN"] = 8] = "DOGECOIN";
})(ALTCOIN || (ALTCOIN = {}));
//# sourceMappingURL=coin.enum.js.map

 

type Coin = 'BITCOIN' | Altcoin;
type Altcoin = 'ETHEREUM' | 'RIPPLE' | 'LITECOIN' | 'DASH' | 'MONERO' | 'ZCASH' | 'NEM' | 'BITCOIN_CASH' | 'DOGECOIN';

let myCoin: Coin = 'BITCOIN';
let myAltcoin: Altcoin = 'ETHEREUM';
let myCoin = 'BITCOIN';
let myAltcoin = 'ETHEREUM';
//# sourceMappingURL=coin.type.js.map

 
 
라인의 포스팅에서는, 과도한 이넘 타입의 설정은 번들링 시 코드 최적화에 실패하여 유저에게 최적의 UX를 제공하지 못한다. 라고 해석되었다.
 
이를 해소하기위해 위처럼 유니온 타입을 사용하길 권장했고, 더 나아가서 유니온타입을 유연하게 사용하기 위해 템플릿 리터럴 타입도 생각해봄직 한 것 같았다.
 
 
 
 

사용해보기

 
우선 기존 프로젝트 중 분석에 대한 기능을 담당하는 코드들을 조금 손보기로 했다.

type ReadonlyRecord<K extends string, V> = Readonly<Record<K, V>>;
//step1
export type TStudioConcept = '인물 중심' | '다양한 배경' | '인물 + 배경';
export type TDressMood = '심플함' | '화려함';
export type TDressMaterial = '실크' | '레이스' | '비즈';
export type TMakeupStyle = '과즙/생기' | '깨끗/청초/화사' | '윤곽/음영';

//step2
export type TBudget = '100만원대' | '200만원대' | '300만원대' | '400만원대 이상';

//step3
export type TBodyType = '슬림' | '평균' | '통통';
export type TPeriod = '44사이즈' | '55사이즈' | '66사이즈' | '77사이즈 이상';


//step1
export const StudioConcept: ReadonlyRecord<TStudioConcept, TStudioConcept> = {
    '인물 중심': '인물 중심',
    '다양한 배경': '다양한 배경',
    '인물 + 배경': '인물 + 배경'
};
export const DressMood: ReadonlyRecord<TDressMood, TDressMood> = {
    '심플함': '심플함',
    '화려함': '화려함'
};
export const DressMaterial: ReadonlyRecord<TDressMaterial, TDressMaterial> = {
    '실크': '실크',
    '레이스': '레이스',
    '비즈': '비즈'
};
export const MakeupStyle: ReadonlyRecord<TMakeupStyle, TMakeupStyle> = {
    '과즙/생기': '과즙/생기',
    '깨끗/청초/화사': '깨끗/청초/화사',
    '윤곽/음영': '윤곽/음영'
};

//step2
export const Budget: ReadonlyRecord<TBudget, TBudget> = {
    '100만원대': '100만원대',
    '200만원대': '200만원대',
    '300만원대': '300만원대',
    '400만원대 이상': '400만원대 이상'
};

//step3
export const BodyType: ReadonlyRecord<TBodyType, TBodyType> = {
    '슬림': '슬림',
    '평균': '평균',
    '통통': '통통'
};
export const Period: ReadonlyRecord<TPeriod, TPeriod> = {
    '44사이즈': '44사이즈',
    '55사이즈': '55사이즈',
    '66사이즈': '66사이즈',
    '77사이즈 이상': '77사이즈 이상'
};

 
여기서 ReadonlyRecord타입은, as const처럼 변경 불가능한 읽기전용 객체임을 편하게 명시하기 위함이다. 이넘을 타입으로 변경하려다보니, 프로퍼티 값들을 고정시켜 이넘처럼 사용할 수 있게 하여 일관성과 안정성을 유지하고자 했다.
 
 
이제 이를, Reuqest DTO에 각 섹션마다 뿌려주면 되었고, IsEnum을 사용하여 이넘 타입과 똑같이 벨리데이션이 가능했다.
 

export class AnaylsisStep1 {
    @IsNotEmpty()
    @IsEnum(StudioConcept)
    @ApiProperty({ enum: StudioConceptProperty, description: '스튜디오 컨셉', required: true })
    sConcept: TStudioConcept

    @IsNotEmpty()
    @IsEnum(DressMood)
    @ApiProperty({ enum: DressMoodProperty, description: '드레스 분위기', required: true })
    dMood: TDressMood;

    @IsNotEmpty()
    @IsEnum(DressMaterial)
    @ApiProperty({ enum: DressMaterialProperty, description: '드레스 소재', required: true })
    dMaterial: TDressMaterial;

    @IsNotEmpty()
    @IsEnum(MakeupStyle)
    @ApiProperty({ enum: MakeupStyleProperty, description: '메이크업 스타일', required: true })
    mStyle: TMakeupStyle;
}

export class AnaylsisStep2 {
    @IsNotEmpty()
    @IsEnum(Budget)
    @ApiProperty({ enum: BudgetProperty, description: '예산', required: true })
    budget: TBudget;
}

export class AnaylsisStep3 {
    @IsNotEmpty()
    @IsEnum(BodyType)
    @ApiProperty({ enum: BodyTypeProperty, description: '몸매 타입', required: true })
    bodyType: TBodyType;

    @IsNotEmpty()
    @IsEnum(Period)
    @ApiProperty({ enum: PeriodProperty, description: '체형', required: true })
    period: TPeriod;

    @IsNotEmpty()
    @IsBoolean()
    @ApiProperty({ description: '브랜드 인지도', required: true })
    bAwareness: boolean;
}

export class StyleAnalysisRequestDto {
    @ValidateNested()
    @Type(() => AnaylsisStep1)
    @IsNotEmpty()
    @ApiProperty({ type: AnaylsisStep1 })
    step1: AnaylsisStep1;

    @ValidateNested()
    @Type(() => AnaylsisStep2)
    @IsNotEmpty()
    @ApiProperty({ type: AnaylsisStep2 })
    step2: AnaylsisStep2;

    @ValidateNested()
    @Type(() => AnaylsisStep3)
    @IsNotEmpty()
    @ApiProperty({ type: AnaylsisStep3 })
    step3: AnaylsisStep3;
}

 
 
 
 
 

마무리하며

백엔드 관점에서, 빌드된 서버 코드들이 모두 클라이언트에게 넘어가는 것이 아니기 때문에, 반드시 이넘타입을 변경해야 할까? 라는 생각이 들었다. 프론트 개발에서 중요한 번들 크기 최적화 같은 이슈가, 백엔드에서는 크게 중요하지 않을 수 있다.
 
Class-Validator이나, Swagger Response등 객체를 반드시 집어넣어야 동작하는 프레임워크의 내장 모듈이나 외부 라이브러리 등을 사용할 때, 단순 이넘을 선언하는 것이 편리했다. 리터럴 타입은 반드시 객체를 한번 더 선언해야만 동작했기 때문이다. 분명 개발 편의성과 효율성에서 차이가 있다.
 
마지막으로 선호도 차이도 있다. 자바 진영에서 타입스크립트로 넘어왔는데,  Class사용이 가능하고, Enum도 마찬가지로 사용이 가능하기 때문에 둘을 선호해서 사용했던 것 같다.
 
종합해보면, 코드의 안정성과 기타 필수로 갖춰야 할 코드베이스들을 해치지 않으면서, 프로젝트의 성격 / 팀원들의 성향 또는 개인의 성향에 맞게 작성하면 될 것 같다.는 결론이 나왔다.
 
2주일 정도 구현도 직접 해보고, 포스팅을 위한 고민을 했다. 좋은 포스팅을 여러개 모아서 스토리북처럼 읽어가면서 직접 구현해보면서 생각의 풀을 넓힐 수 있는 좋은 시간이였던 것 같다.
 
 
 
 

참조

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

 

TypeScript enum을 사용하지 않는 게 좋은 이유를 Tree-shaking 관점에서 소개합니다.

들어가며 안녕하세요. LINE Growth Technology UIT 팀의 Keishima(@pittanko_pta)입니다. 이번 글에서는 TypeScript의 enum을 사용하지 않는 편이 좋은 이유를 Tree-shaking 관점에서 소개하겠습니...

engineering.linecorp.com

 

Template Literal Types로 타입 안전하게 코딩하기

TypeScript 코드베이스의 타입 안전성을 한 단계 올려줄 수 있는 Template Literal Type의 뜻과 응용에 대해 알아봅니다.

toss.tech

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

[Grafana Loki] Data source connected, but no labels received. Verify that Loki and Promtail is configured properly

Tech/트러블슈팅 2024. 3. 8. 14:05
728x90
728x90

 

 

 

 

에러 원인

라벨 설정 시 설정했던 라벨이 존재하지 않음.

이는 로키는 제대로 연결되었지만, 로그 파일을 제대로 프롬테일에서 받아오지 못했음을 의미한다.

 

본인의 경우는 프로젝트의 도커컴포즈 볼륨 설정에서, 경로가 제대로 작성되지 않아 망운트가 제대로 되지 않았음.

 

 

걸린 시간

3시간 남짓

 

 

 

에러 해결

프로젝트의 로그생성


docker exec -it containerName /bin/sh

 

프로젝트 내부에서는, 루트 경로에 logs폴더 내부에서 날짜, 에러레벨에 맞게 분기처리를 하여 로그를 생성했다.

 

호스트 서버에서, 
위 명령어를 통해 컨테이너 내부로 진입하는데, 진입하자마자 logs경로에 로그가 잘 들어오고 있길래 당연히 잘 동작할거라 생각했지만

그라파나에서 로키의 ip:port로 커넥션을 연결하는 과정에서 계속해서 아래 에러가 발생했다.

 

"Data source connected, but no labels received. Verify that Loki and Promtails is configured properly"

 

라벨을 가져올 수 없는 상황인 것 같았고, 결국 라벨은 로키까지 도달되려면 프로젝트 로그에서 출발한 데이터가 프롬테일을 통해 로키로 전달이 되어야하기 때문에, 프롬테일에서 로그를 받아오기까지의 과정을 확인해볼 수 밖에 없었다.

 

 

프롬테일은 로그를 잘 받아오고 있는가?

 

프롬테일의 로그를보면, 경로는 정확히 추적하고 있는 것으로 보였다.

 

 

로그 파일의 바인드 마운트

프로젝트 내부의 로그는 정상적으로 생성되지만, 호스트 내부의 경로에는 로그파일이 정상적으로 생성되지 않았다.

 

결과론적으로, 프로젝트의 compose설정에서, 볼륨의 마운트 설정이 잘못되어서 해당 위치에 로그파일이 생성되지 않았다.

 

 

컨테이너 내부로 접속했을 때, 바로 logs디렉토리가 있어서 compose 설정을 다음과 같이 작성했기 때문에 마운트가 되지 않았다.

version: '3.7'
services:
  api:
    image: api
    container_name: api
    build: .
    privileged: true
    ports:
      - '45001:5001'
    volumes:
      - /var/log/api:/logs
    environment:
      - PORT=5001
      - NODE_ENV=production

 

 

다른 곳에서 삽질을 많이 했다. 괜히 프롬테일 설정파일, 로키 설정파일도 뜯어보고.

아예 호스트서버에서 프롬테일 로키를 삭제했다 재설치도 해보고.

영문으로된 자료들을 많이 찾아봤다가

 

볼륨 경로가 이상하다는 것을 3시간만에 겨우 파악했고... 볼륨경로를 정확히 지정해주었더니 해결되었다.

volumes:
  - /var/log/api:/app/logs

 

 

 

 

 

참고

 

Troubleshooting | Grafana Loki documentation

Open source Troubleshooting “Loki: Bad Gateway. 502” This error can appear in Grafana when Grafana Loki is added as a datasource, indicating that Grafana in unable to connect to Loki. There may one of many root causes: If Loki is deployed with Docker,

grafana.com

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

[NestJS] 코드 리팩토링하기 - 응집도를 높이고 의존성을 명확하게

Tech/트러블슈팅 2024. 1. 21. 21:59
728x90
728x90

 

 

 

서론

 

요즘 좋은 코드 라는 키워드에 대해

 

특히 변경과 재사용이 용이한, 높은 응집도와 낮은 결합 관계 에 대해 많이 생각하고 있다.

 

특히 기존 레거시를 모두다 걷어내기에는 시간적으로 애로사항이 있어

 

틈틈이 관련된 프로젝트에 들어갈 때, 해당 로직에 대한 레거시들을 최대한 바꾸려고 노력하고 있다.

 

 

개발자라면, 누구나 좋은 코드가 무엇인지는 간략하게라도 알고 있다.

 

 

[네이버클라우드 개발자 스토리] 좋은 코드란 무엇일까?🤔 #클린코드 이야기

📍 “좋은 코드를 짜야 한다”​

medium.com

 

 

 

특히, 상품의 리뷰를 불러오는 함수를 수정해야 하는 일이 최근에 있었는데,

 

상품군 7~8개의 하위 상품에 대한 리뷰를 모두 다른 함수에서 불러오는 것을 보고 경악을 금치 못했다.

 

(급한 사항이라 판단되어 우선 프로덕션에 수정해서 반영한 뒤 구조를 수정하였다..)

 

 


 

 

 

나도 최근에 신규 프로젝트를 진행하면서

 

함수가 많아지고 코드가 길어짐에 따라

 

비즈니스 레이어에 있는 Validation관련 로직이 많아져서

 

Validation관련 함수들만 따로 클래스를 분리하여 Provider로 만들어주는 과정에서 있었던 일들을

 

 

간략하게나마 작성해 이 시기에 이런 고민을 했고, 후에 다른 백엔드 분들과 협업 시에 달라질 코드 스타일과 비교하고자,

마지막으로 지금의 나는 옳은 방향으로 가고 있었는지 미래에 확인해보기 위해 작성 해보려고 한다.

 

 

 


 

 

 

쭉 써내려간 코드

 

아래는, 출퇴 시간을 직접 조정할 수 있는 기능에 대한 코드이다.

 

//출결 등록, 변경 관련
@Injectable()
export class AttendenceService implements AttendanceServiceImpl {

    constructor(
        private readonly offDayPlanRepo: OffDayPlanRepository,
        private readonly offDayPlanCoreRepo: OffDayPlanCoreRepository,
        private readonly offDayPlanOutTimeRepo: OffDayPlanOutTimeRepository,
    ) { }
    
    
    //출퇴근 시간 직접 설정
    async customizeWorkTime(user: User, startTime: Date, endTime?: Date): Promise<CustomRes> {
    
    	//현재 시각의 Date, String
        const { today, todayString } = await dateDataSet();
        
        //오늘 날짜의 Row Check
        await this.existsOffDayPlanCheck(user, 'customIn', today, todayString);

        //부서별 코어 근무시간과 validation
        if (await this.registrationCustomVaildation(user.devision, startTime, endTime)) {
        	
            const result = await this.offDayPlanRepo.updateCustomWorkTime(user.id, startTime, endTime, now());

            if (result.affected) {
                let startMsg = (startTime) ? `${getFullDate(startTime)}` : '';
                let endMsg = (endTime) ? `${getFullDate(endTime)}` : '';
                const returnMsg = startMsg + ' ' + endMsg;
                return TimeRecordSuccess(`update complete : ${returnMsg}`);
            }
        }
    }
    
        
    //출퇴근 시간 직접 설정 시 근무가능시간 Validation
    // 1. 코어근무시간, 2. 근무 가능시간
    async registrationCustomVaildation(devision: string, startTime: Date, endTime?: Date): Promise<boolean> {

        const availableTimeVali = await this.coreTimeValidation(devision, startTime, endTime);
        const coreTimeVail = await this.availableWorkTimeValidation(devision, startTime, endTime);

        if (availableTimeVali && coreTimeVali) {
            return true;
        };

    }

	
    
    //부서별 코어 근무 시간과 비교해
    // 출퇴근 시간 등록 가능하면 바로, 안되면 승인 요청 요구.
    async coreTimeValidation(devision: string, startTime?: Date, endTime?: Date): Promise<boolean> {

        const { startTimeString, endTimeString } = await this.getCoreTimeByDevision(devision);

        if (startTime && startTimeString > getHoursAndMinutes(startTime)) {                        
            throw CoreTimeLangeException(`[request first] request first-1`);
        };
        if (endTime && endTimeString < getHoursAndMinutes(endTime)) {
            throw CoreTimeLangeException(`[request first] request first-2`);
        };

        return true;

    }


    //근무 가능 시간과 비교
    async availableWorkTimeValidation(devision: string, startTime?: Date, endTime?: Date): Promise<boolean> {

        //근무가능시간 시나리오가 나오면 작성 예정
        return true;

    }
    

}

 

 

해당 기능을 포함한 Attendence에 대한 비즈니스 로직, 벨리데이션 로직들을 쭉 써내려가다보니

 

벨리데이션을 분리하여 관리해주고 싶어졌다.

 

 

 


 

 

리팩토링

 

우선, 서비스의 비즈니스 로직에서 벨리데이션을 분리하여 응집도를 높여볼 수 있었다.

 

이는 벨리데이션의 조건이 변경되거나 추가될 때 등

벨리데이션을 관리하고 유지보수하는 데 더 용이하다.

 

//출결 등록, 변경 관련
@Injectable()
export class AttendanceValidationProvider {
    
        
    //출퇴근 시간 직접 설정 시 근무가능시간 Validation
    // 1. 코어근무시간, 2. 근무 가능시간
    async registrationCustomVaildation(devision: string, startTime: Date, endTime?: Date): Promise<boolean> {

        const availableTimeVali = await this.coreTimeValidation(devision, startTime, endTime);
        const coreTimeVail = await this.availableWorkTimeValidation(devision, startTime, endTime);

        if (availableTimeVali && coreTimeVali) {
            return true;
        };

    }

	
    
    //부서별 코어 근무 시간과 비교해
    // 출퇴근 시간 등록 가능하면 바로, 안되면 승인 요청 요구.
    async coreTimeValidation(devision: string, startTime?: Date, endTime?: Date): Promise<boolean> {

        const { startTimeString, endTimeString } = await this.getCoreTimeByDevision(devision);

        if (startTime && startTimeString > getHoursAndMinutes(startTime)) {                        
            throw CoreTimeLangeException(`[request first] request first-1`);
        };
        if (endTime && endTimeString < getHoursAndMinutes(endTime)) {
            throw CoreTimeLangeException(`[request first] request first-2`);
        };

        return true;

    }


    //근무 가능 시간과 비교
    async availableWorkTimeValidation(devision: string, startTime?: Date, endTime?: Date): Promise<boolean> {

        //근무가능시간 시나리오가 나오면 작성 예정
        return true;

    }
    

}

 

 

모듈의 Provider에 ValidationProvider을 추가하고, 서비스로 DI하여 사용하면 되겠다.

 

 

 

 

고 생각하였으나 문제가 발생했다.

 

Validation Provider의 코드를 자세히 보면 아래와 같은 코드가 있다.

 

const { startTimeString, endTimeString } = await this.getCoreTimeByDevision(devision);

 

위의 코드는, 사내 부서별 코어 근무시간과, 근무 가능 시간을 불러오는 코드로, 서비스단에 구현되어 있으며

 

현재는 커스텀 리파지토리를 구현하여 findOneByDevision(devision)을 호출하게 되어있다.

 

/**
 * 부서별 코어 근무시간, 근무 가능시간 조회
 * @param devision 
 * @returns 
 */
async getCoreTimeByDevision(devision: string): Promise<OffDayPlanCoreEntity> {

    const entity = await this.offDayPlanCoreRepo.findOne({ where: { devision: devision } });
    if (!entity) {
        throw CoreTimeRecordNotFoundException([Not Found] `${devision} row data not found`);
    };

    return entity;
}

 

 

Validation Provider에서 다시 서비스단에 의존을 하게 된다면,

 

Validation Provider가 단순 출결 서비스의 벨리데이션을 담당하는 모듈이 아니라 서비스와 동일한 수준의 모듈이 되어버린다. DIP를 위반하게 되는 것이다. (상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안된다)

 

또한 직접 의존하게 될 경우 결합도가 상승하여 로직이 변경될 경우, 서로에게 영향을 끼치게 된다.

최악의 경우 코드를 모두 수정해야할 수도 있다.

 

 

 

또한 Validation 클래스에서 직접 Repository를 호출하는 것 또한 DIP를 위배하고,

Validation만 수행하게 하려고 역할을 분리하여 클래스를 설계했는데, 이에 위배된다고 생각했다.

 

 

 

 

이런 저런 생각들을 통해 아래처럼 코드를 최종 변경할 수 있었다.

//부서별 코어 근무 시간과 비교해
// 출퇴근 시간 등록 가능하면 바로, 안되면 승인 요청 요구.
async coreTimeValidation(entity: CoreTimeEntity, startTime?: Date, endTime?: Date): Promise<boolean> {

    const { startTimeString, endTimeString } = entity;

    if (startTime && startTimeString > getHoursAndMinutes(startTime)) {                        
        throw CoreTimeLangeException(`[request first] request first-1`);
    };
    if (endTime && endTimeString < getHoursAndMinutes(endTime)) {
        throw CoreTimeLangeException(`[request first] request first-2`);
    };

    return true;

}

 

1. coreTimeValidation라는 네이밍에 맞게 코어 근무시간만 Validation하였으며

 

2. entity를 파라미터로 받았다.

 

 

이제 이 Validation 클래스는 이름에 맞게, 출퇴근의 Validation만 담당하게 될 것이며

Validation의 조건이 변경될 경우 해당 Validation만 변경해주거나, 파라미터만 수정해준다면 올바르게 동작할 것이다.

 

 

 

 

 

02.16 추가

감사하게도 댓글의 좋은 피드백을 받아, 한 번의 리팩토링을 더 거칠 수 있게 되었다.

 

1. 사용하지 않는 변수 제거

2. 함수명이 명확한 의미를 가지게 변경,

3. 함수를 boolean타입으로 리턴받아서 추가적으로 핸들링하는 것이 없기 때문에 void

(단순 에러 or 통과)

4. validateCoreTime만 봐도 어떤 부분들을 검증하는지 명확하게 볼 수 있게 내부 함수로 변경

 

특히 2,4번은 간과하고 있던 부분이라고 생각했다.

(다시한번 좋은 피드백을 제공해 주셔서 감사하다는 말씀을 전합니다.)

validateCoreTime(entity: CoreTimeEntity, startTime?: Date, endTime?: Date): void {

    const { startTimeString, endTimeString } = entity;

    if (startTime) {                        
    
    	this.validateStartCoreTime(startTimeString, startTime);
    
    };
    
    if (endTime) {
    
    	this.validateEndCoreTime(endTimeString, endTime);
    
    };
    
}

private validateStartCoreTime(startTimeString: string, startTime: Date): void {

	if (startTimeString > getHoursAndMinutes(startTime)) {
    
    	throw CoreTimeLangeException('[request first] request first-1');
    
    };

};
    

private validateEndCoreTime(endTimeString: string, endTime: Date): void {

	if (endTimeString > getHoursAndMinutes(endTime)) {
    
    	throw CoreTimeLangeException('[request first] request first-2');
    
    };

};

 

 

상위 함수도 변경할 수 있었다.

 

1. 상위 모듈인 서비스에서 entity를 들고오고

2. 네이밍도 명확하게 변경시켜주었다.

3. 또한 하위의 각 함수들이 무슨 역할을 하는지도 명확히 전달될 수 있게 변경해주었고

4. void타입의 함수들이기 때문에 함수의 로직도 변경시켜주었다.

async validateCustomWorkTime(entity: CoreTimeEntity, startTime: Date, endTime?: Date): Promise<void> {

    await this.validateCoreTime(entity, startTime, endTime);
    await this.validateAvailableWorkTime(entity, startTime, endTime);

}

 


 

 

마치며

 

같이 협업하는 백엔드 개발자가 없다보니,

 

코드 구조에 대한 공부를 혼자 해나가며, 이렇게 저렇게 적용해 보는 중이다.

 

 

 

 

또한 완전 새삥 프로젝트이다보니

 

프로젝트 구조며 서버 셋팅 또한 이래저래 해볼 수 있는 시간이 주어졌다.

 

잘 기록해서 기억해두고, 언젠가 누군가에게 피드백받을 수 있는 날이 왔으면 좋겠다.

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

[NestJS] TypeORM 0.3 버전의 CustomRepository 생성, Repository패턴 적용하기

Tech/트러블슈팅 2024. 1. 5. 10:48
728x90
728x90

 

 

0.2 버전

사내 서비스의 TypeORM버전은 0.2버전대를 사용중이다.

 

0.2버전대에서는 @EntityRepository 데커레이터를 지원하여, Repository를 커스텀화하여 리파지토리 클래스를 생성할 수 있었고,

이에 따라 Service와 Repository레이어를 분리하여 결합도를 낮출 수 있었다.

 

@Injectable()
export class RsvcenterService {

    constructor(
        @InjectRepository(CustomRsvRepository)
        private readonly customRsvRepo: CustomRsvRepository,
        //DB 관련 로직 예외처리 Provider
        private readonly customEm: CustomEntityManager,
    ) { }

    async getRsvList(id: string, rsvFilterQueryDto: RsvFilterQueryDto) {   
        //예약내역 조회
        const rsvData = await this.customRsvRepo.findAllById(id);

        //404 validation
        this.customEm.validateEntity(rsvData);

        return rsvData;
    }
}
@EntityRepository(RsvEntity)
export class CustomRsvRepository extends Repository<RsvEntity> {

    async findAllById(id: string) {
    	return await this.find({ where: { id: id }});
    }
}

 

 

 

0.3 버전

사내 서비스의 버전 관리 일정에 앞서 새로운 프로젝트 일정이 잡혔고,

새로운 프로젝트를 위한 서버 및 CICD 구축,, 등등 할 일이 생겼다. 해당 글의 주제와는 맞지 않으니 넘어가도록 하고.

여튼 새로운 프로젝트 구성을 위해 가급적 높은 버전을 사용해보고, 현재 버전과 다른점이 무엇인지 선파악 해보려했다.

 

그리하여 위의 서비스는 0.2.45버전을 사용중이지만, 신규 서비스의 TypeORM은 0.3.19버전을 사용하게 되었다.

 

 

 

Releases · typeorm/typeorm

ORM for TypeScript and JavaScript. Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, SAP Hana, WebSQL databases. Works in NodeJS, Browser, Ionic, Cordova and Electron platforms. -...

github.com

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

 

 

기본적으로 릴리즈 노트를 확인하고, 공식 문서들을 찾아봤는데

공식 문서에서 기본적으로 DB 접근을 할 때, 서비스 레이어에서 데이터 조회에 관련된 로직을 작성하는 것을 볼 수 있다.

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  findAll(): Promise<User[]> {
    return this.usersRepository.find();
  }

  findOne(id: number): Promise<User | null> {
    return this.usersRepository.findOneBy({ id });
  }

  async remove(id: number): Promise<void> {
    await this.usersRepository.delete(id);
  }
}

 

 

더이상 @EntityRepository 데코레이터를 사용할 수 없게 되었고,

따로 커스텀 데커레이터를 만들까 하다가 원래도 상속받는 TypeORM의 Repository 클래스를 참조하여 작성해 보기로 했다.

 

export declare class Repository<Entity extends ObjectLiteral> {
    readonly target: EntityTarget<Entity>;
    readonly manager: EntityManager;
    readonly queryRunner?: QueryRunner;
    get metadata(): import("..").EntityMetadata;
    
    constructor(target: EntityTarget<Entity>, manager: EntityManager, queryRunner?: QueryRunner);
    
    //(...함수 생략...)
}

 

생성자로 Entity와 EntityManager, 선택사항으로 QueryRunner를 필요로한다는 것을 알아냈다.

이제 위의 코드를 0.3버전에 맞게 수정해보자

 

@Injectable()
export class CustomRsvRepository extends Repository<RsvEntity> {
    constructor( private dataSource: DataSource ) {
        super(RsvEntity, dataSource.createEntityManager());
    }
    
    async findAllById(id: string) {
    	return await this.find({ where: { id: id }});
    }
}

 

EntityManager를 생성하기위해 dataSource를 생성자로 사용했고,

QueryRunner는 단순 조회만 하는 예제이기 때문에 추가하지 않았다. 트랜잭션을 사용할 일이 있으면 추가하면 될 것 같아 보인다.

 

이렇게 작성한 후, 서비스를 아래와 같이 수정하고, 모듈의 Provider에 리파지토리를 추가해주고, 엔터티를 Import시키면 정상적으로 동작하는 것을 확인할 수 있었다. 

@Injectable()
export class RsvcenterService {

    constructor(
        private readonly customRsvRepo: CustomRsvRepository,
        //DB 관련 로직 예외처리 Provider
        private readonly customEm: CustomEntityManager,
    ) { }

    async getRsvList(id: string, rsvFilterQueryDto: RsvFilterQueryDto) {   
        //예약내역 조회
        const rsvData = await this.customRsvRepo.findAllById(id);

        //404 validation
        this.customEm.validateEntity(rsvData);

        return rsvData;
    }
}

 

 

 

 

후기

어떤 모듈을 설치한 후, 공식문서와 모듈 내장 클래스만 보고 직접 무언가를 작성하거나 수정하는 경험이 처음이었다.

만약 내가 작성한 커스텀 Repo가 잘못된 것일수도 있지만, 검색 없이도 공식문서와 내장 코드들을 들여다보며 수정할 수도 있구나 라는 생각에 뿌듯한 경험이 되었다.

 

혹시 더 좋은 코드가 있거나, 수정사항이 있으면 누군가 알려주셧으면 감사하겠습니다.. 꾸벅

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

[TypeORM / QueryBuilder] Relation with property path confirms in entity was not found

Tech/트러블슈팅 2023. 12. 8. 11:51
728x90
728x90

 

최근 레거시 코드 중 DB 관련 로직들을 거의 대부분 쿼리빌더로 변경하는 작업을 완료하고, 검수중에 있다.

그 과정에서 발생한 에러들을 하나하나 정리하여 남기려고 한다.

 

 

에러 메세지

 

 

 

원인

관계 매핑이 정확하지 않아서 발생했다.

 

나의 경우는 아래 이유 때문에 발생했는데,

TypeORM의 쿼리빌더를 사용하는 과정에서, 커스텀 리파지토리를 생성하여, 해당 리파지토리에서 두 테이블을 조인해서 사용했는데,

 

처음 INNER JOIN을 시도한 테이블에서, enterprise라는 엔터티에 대한 정의를 내리지 않았기 때문에 발생했다.

 

해당 엔터티를 살펴보면,

export class EasyBookWhichEntEntity {
    @PrimaryGeneratedColumn({ type: 'int', name: 'no' })
    no: number;

    @Column('varchar', { name: 'enterprise_code' })
    enterpriseCode: string;

    @Column('varchar', { name: 'product_no' })
    productNo: string;

    @Column('int', { name: 'easy_book_no' })
    easyBookNo: number;

    @ManyToOne(() => EasyBookEntity, (book) => book.ents)
    @JoinColumn({ name: 'easy_book_no' })
    book: EasyBookEntity;
}

 

EasyBookEntity에 대한 정의만 내려져있지, enterprise에 대한 정의가 내려져있지 않았다.

 

 

해결

이제 해당 엔터티에 조인 컬럼의 정의를 내려주자.

export class EasyBookWhichEntEntity {
    @PrimaryGeneratedColumn({ type: 'int', name: 'no' })
    no: number;

    @Column('varchar', { name: 'enterprise_code' })
    enterpriseCode: string;

    @Column('varchar', { name: 'product_no' })
    productNo: string;

    @Column('int', { name: 'easy_book_no' })
    easyBookNo: number;

    @ManyToOne(() => EasyBookEntity, (book) => book.ents)
    @JoinColumn({ name: 'easy_book_no' })
    book: EasyBookEntity;
    
    @OneToOne(() => WmEnterpriseEntity, (ent) => ent.eBook)
    @JoinColumn({ name: 'enterprise_code' })
    enterprise: WmEnterpriseEntity;
}

 

export class WmEnterpriseEntity {
  @PrimaryGeneratedColumn({ type: 'bigint' })
  no: number;

	(...생략...)

  @OneToOne(() => EasyBookWhichEntEntity, (eBook) => eBook.enterprise)
  eBook: EasyBookWhichEntEntity;
}

 

 

정확히 관계 매핑을 해주고나면, 동작하는 모습을 볼 수 있다.

 

 

 

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

[NestJS] JWT Token 인증 - (3) Refresh Token 사용

Tech/NodeJS 2023. 11. 25. 19:15
728x90
728x90

 

 

이전 글에서 이어집니다.

 

[NestJS] JWT Token 인증(Access Token) 구현하기(with passport) - (2)

서론 지난글에 이어, Nest에서 JWT를 사용한 인증을 진행해보려한다. [NestJS] JWT Token 인증 - (1) JWT 토큰이란? [NestJS] JWT Token 인증 - (1) JWT 토큰이란? 쿠키와 세션을 통한 인증 쿠키와 세션 (Cookie & Sessio

mag1c.tistory.com

 

 

Access Token의 한계

Access Token을 통해 웹사이트를 이용하다, 토큰이 만료되면 다시 재로그인을 해야만 하는 불편함을 겪게 된다.

또한 Access Token 만료 전 토큰을 탈취당하게 된다면 토큰이 만료될 때까지 속수무책이라는 보안상의 문제도 있다.

 

이러한 문제들 때문에, 보통 Acces Token의 유효 시간을 짧게 해두고, Refresh Token의 유효기간을 길게 설정하여

Refresh Token이 만료되기 전에는, Access Token을 갱신하여 사용할 수 있다.

 

출처 : 인파(inpa)

 

정리
1. Access Token의 유효 기간을 짧게 설정, Refresh Token의 유효 기간은 길게 설정
2. 두 토큰 모두 서버에 전송하여 Access Token으로 인증하고, 만료 시 Refresh Token으로 재발급한다.
3. Access Token을 탈취당해도, 짧은 유효 기간이 지나면 사용할 수 없다.

 

 

하지만 만약 Refresh Token이 탈취당한다면..?

 

Is a Refresh Token really necessary when using JWT token authentication?

I'm referencing another SO post that discusses using refresh tokens with JWT. JWT (JSON Web Token) automatic prolongation of expiration I have an application with a very common architecture where my

stackoverflow.com

위 스택오버플로우의 글에서 제안하는 방법은 다음과 같다.

1. 데이터베이스에 Access Token, Refresh Token 쌍을 저장(1:1 매핑)

2. 공격자는 탈취한 Refresh Token으로 새 Access Token을 생성하여 서버에 전송하게 된다. 이 때, 서버에서 데이터베이스에 저장된 Access Token과 공격자에게 받은 Access Token이 다른 것을 확인하게 된다. 이 때, 저장된 토큰이 만료되지 않았을 경우 탈취당한 것으로 간주하고 두 토큰을 모두 만료시킨다.

3. 재로그인을 통해 인증을 다시 수행해야하지만, 공격자의 토큰 역시 만료됐기 때문에 공격자는 접근이 불가능해진다.

 


토큰은 클라이언트나 서버 전역에서 만료시킬 수 있는 개체가 아니기 때문에, 토큰의 유효 기간이 지나기 전까지는 데이터베이스에 저장하여 관리할 필요가 있다.

또한 Refresh Token도 Access Token과 같은 유효기간을 가지게 해서, 사용자가 한 번 Refresh Token으로 Access Token을 재발급을 받았을 때, Refresh Token도 재발급 할 수 있도록 권장하고 있다고 한다. (ietf문서 링크)

 

 

 

 

Refresh Token 이용하기(Refresh Strategy)

import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Request } from 'express';
import { UserService } from 'src/user/user.service';

@Injectable()
export class RefreshStrategy extends PassportStrategy(Strategy, 'refresh') {
  constructor(
    private readonly configService: ConfigService,
    private readonly userService: UserService
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromExtractors([(request: Request) => {
        return request?.cookies?.refreshToken
      }]),
      ignoreExpiration: false,
      passReqToCallback: true,
      algorithms: ['HS256'],
      secretOrKey: configService.get('JWT_SECRET'),
    });
  }

  async validate(request: Request, payload: any) {    
    const refreshToken = request.cookies['refreshToken'];
    return this.userService.refreshTokenMatches(refreshToken, payload.no);
  }
}
//JWT의 추출방법을 지정한다.
//fromAuthHeaderAsBearerToken()을 사용하여 헤더에서
//Bearer Token을 Authorization 헤더에서 추출한다.
jwtFromRequest: ExtractJwt.fromExtractors([(request: Request) => {
    return request?.cookies?.refreshToken
}]),

//토큰 만료 기간을 무시할지? false는 만료된 토큰 자체를 거부한다.
ignoreExpiration: false,

//validate함수의 첫 번째 인자로 request 객체 전달 여부를 결정.
passReqToCallback: true,

//서명에 사용할 알고리즘을 지정한다.
algorithms: ['HS256'],

//서명에 사용되는 비밀키로 환경변수에서 가져왔다.
secretOrKey: configService.get('JWT_SECRET'),

 

 

 

validate의 refreshTokenMatches를 통해, 해당 user의 row에 있는 refresh token이 request의 refresh token과 일치한지 여부를 확인한다.

//JWT 관련 로직
async refreshTokenMatches(refreshToken: string, no: number): Promise<UserEntity> {
    const user = await this.findByNo(no);

    const isMatches = this.isMatch(refreshToken, user.refresh_token);
    if (isMatches) return user;
}

 

 

검증에 성공하면, 다시 accessToken을 반환하여, 클라이언트에서 이를 보관처리하고 다시 인증에 유효하게 사용할 수 있을 것이다.

@Post('/refresh')
@UseGuards(RefreshAuthGuard)
@ApiOperation({ description: '토큰 재발급' })
async refreshToken(@Request() req) {        
    const accessToken = await this.authService.createAccessToken(req.user);
    const user = new UserEntityBuilder()
        .withNo(req.user.no)
        .withId(req.user.id)        
    return { accessToken, user };
}

 

 

 

 

 

참조

https://docs.nestjs.com/security/authentication

https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps-00#section-8

https://stackoverflow.com/questions/32060478/is-a-refresh-token-really-necessary-when-using-jwt-token-authentication

https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-Access-Token-Refresh-Token-%EC%9B%90%EB%A6%AC-feat-JWT

https://velog.io/@park2348190/JWT%EC%97%90%EC%84%9C-Refresh-Token%EC%9D%80-%EC%99%9C-%ED%95%84%EC%9A%94%ED%95%9C%EA%B0%80

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

[NestJS] JWT Token 인증(Access Token) 구현하기(with passport) - (2)

Tech/NodeJS 2023. 11. 18. 18:55
728x90
728x90

 

 

 

서론

지난글에 이어, Nest에서 JWT를 사용한 인증을 진행해보려한다.

 

[NestJS] JWT Token 인증 - (1) JWT 토큰이란?

 

[NestJS] JWT Token 인증 - (1) JWT 토큰이란?

쿠키와 세션을 통한 인증 쿠키와 세션 (Cookie & Session) HTTP 프로토콜의 특징 비연결성 ( Connectionless ) 클라이언트가 서버에 요청(Request)할 때, 그에 대한 응답(Response)을 한 후, 연결을 끊는다. 비상태

mag1c.tistory.com

 

 

 

NestJS에서 제공하는 공식 문서의 인증 관련 페이지를 참조했다.

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

 

설치한 모듈
passport (@nestjs/passport, passport, passport-local, @types/passport-local)
jwt (@nestjs/jwt, passport-jwt, @types/passport-jwt)

 

 

 

Passport

Node에서 인증을 적용할 때 사용할 수 있는 미들웨어로, 클라이언트가 서버에 권한을 요청할 자격이 있는지 인증할 때 passport 미들웨어를 사용한다. Nest에서는 토큰 인증에 있어서, passport의 사용을 권장하고 있다.

 

간단히 살펴보면, 유저의 자격 증명을 통해 사용자를 인증하며, 인증된 상태관리를 JWT와 같은 토큰을 통해 수행하며, Request에서 추가로 사용할 수 있게 사용자의 추가 정보를 첨부할 수 있다고 되어있다.

 

패키지 설치 시 @nestjs/passport 패키지를 반드시 요한다고 공식문서에 나타나 있다.

 

Nest에서는, PassportStrategy 클래스를 확장하여 구성하며, 하위 클래스에서 super() 메서드를 통해 파라미터를 전달하며 하위 클래스에서 validate() 메서드를 구현하여 인증에 대한 콜백을 수행한다고 명시되어 있다.

 

즉, 사용자가 존재하는지, 존재한다면 validate되는지를 따져 원하는 리턴값을 반환시킬 수 있고, 에러도 만들어 낼 수 있다.

 

Passport Strategy

이제 이 미들웨어의 전략을 구상(??)해야하는데, 기본적으로 공식 문서나 기타 개발 블로그 등 자료들에서 볼 수 있듯 기본 전략에서 내가 사용하는 서비스에 맞게 변경해주었다.

 

또한 들어가기에 앞서, @UseGuards 데코레이터를 사용하여 어떤 전략을 사용할 지 명시해주면 @nestjs/passport가 자동으로 명시된 전략을 호출한다.

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

 

예를들어, 위 AuthGuard는 local이라는 파라미터를 Passport에게 전달하여, 해당 Guard사용 시 LocalStrategy를 사용하게 한다.

 

 

 

 

그리하여 세가지 strategy를 만들 수 있었다.

Strategy

1. 로그인 시 사용하는 LocalStrategy
2. 로그인 후 인증 전반을 담당하는 JwtStrategy
3. Access Token 만료 시 Refresh Token을 검증하는 RefreshStrategy

 

 

로그인 (LocalStrategy)

로그인 시 수행하는 전략은 id와 pw를 가지고, 흔히 구현된 로그인 로직으로 보내어 유저 정보가 일치하는지 validate하는 전략이다.

import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from '../auth.service';
import { UserService } from 'src/user/user.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService, private userService: UserService) {
    super({ usernameField: 'id', passwordField: 'pw' });
  }

  async validate(id: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(id, password);
    if (!user) {
      throw new UnauthorizedException('아이디 혹은 비밀번호를 확인해주세요.');
    }
    return user;
  }
}

 

 

유저 검증을 위한 서비스의 validateUser 코드이며, 유저의 비밀번호는 암호화되어 있기 때문에 복호화 메서드를 통해 검증을 추가로 진행하고, 유저 정보를 리턴할 것이기 때문에 유저의 비밀번호는 삭제해주었다.

 

유저 객체 자체를 클라이언트로 반환하는 것은 아닌데, 굳이 비밀번호 삭제가 필요한 지는 의문이긴 하다. 토큰 생성 시에도 user의 PK와 id만 담긴다. 확실한 지식이 없어 혹여 불필요한 비밀번호가 유출 될까 삭제하긴 했는데.. 이 부분을 좀 더 알아봐야겠다. (조언좀 부탁드립니다!!)

 

//validate for stragety
async validateUser(id: string, password: string): Promise<any> {
    const user = await this.userService.findById(id);
    if (!user) {
        throw new UnauthorizedException('가입된 유저가 아닙니다.');
    }
    if (user && (await this.isMatch(password, user.pw))) {
        delete user.pw;
        return user;
    } else {
        throw new BadRequestException('패스워드를 확인해주세요');
    }
}

 

 

 

LocalStrategy에서, 올바르게 유저를 검증하는데 성공했다면, user객체를 반환하게 코드를 만들어놓았기 때문에, 로그인 유저 검증에 성공한다면 Request객체에 user가 담기게되고, 우리는 이를 이용해서 로그인 처리(토큰 생성)를 수행할 수 있게 된다.

@Post('/signin')
@UseGuards(LocalAuthGuard)
@ApiOperation({ description: '로그인' })
@ApiResponse({ status: 200, description: 'JWT_TOKEN을 발급합니다.', type: JwtTokenDto })
signin(@Request() req) {
    return this.authService.signin(req.user);
}

 

async signin(user: UserEntity) {
    //1. user validate with hashed pw        
    if (this.validateUser) {
        user.last_join_date = new Date();
        //1-1. 유저의 마지막 로그인 시점 업데이트
        const signinDateUpdate = this.userService.updateSignin(user);

        //1-2. when validated >> token created
        if (signinDateUpdate) return this.createToken(user);
    }        
}

//access token 발급
createToken(user: UserEntity) {
    const no = user.no;
    const tokenDto = new JwtTokenDto(this.createAccessToken(user), this.createRefreshToken(no));
    this.setCurrentRefreshToken(tokenDto.refresh_token, no);
    return tokenDto;
}

createAccessToken(user: UserEntity) {
    const cu = { no: user.no, id: user.id };
    return this.jwtService.sign(cu, {
        secret: this.configService.get('JWT_SECRET'),
        expiresIn: this.configService.get('expiresIn')
    });
}

createRefreshToken(no: number) {
    return this.jwtService.sign({ no }, {
        secret: this.configService.get('JWT_SECERT'),
        expiresIn: this.configService.get('expiresInRefresh')
    });
}

 

 

토큰 생성시, payload에는 민감정보를 전달하면 안되기 때문에, pk와 id만 담았다. (pk는 좀 애매할 수 있을것 같다.)

생성 시 서명(signature)으로 설정해둔 시크릿 키와, 유효기간을 담았다.

createAccessToken(user: UserEntity) {
    const cu = { no: user.no, id: user.id };
    return this.jwtService.sign(cu, {
        secret: this.configService.get('JWT_SECRET'),
        expiresIn: this.configService.get('expiresIn')
    });
}

createRefreshToken(no: number) {
    return this.jwtService.sign({ no }, {
        secret: this.configService.get('JWT_SECERT'),
        expiresIn: this.configService.get('expiresInRefresh')
    });
}

 

 

정상적으로 로그인이 되면 아래처럼, access_token과 refresh_token이 생성되고 리턴된다.

 

 

 

토큰 검증(JwtStrategy)

토큰 검증을 수행하기로 한 Strategy로, PassportStrategy에 JWT 전략에 필요한 옵션들을 보낸다.

import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      algorithms: ['HS256'],
      secretOrKey: configService.get('JWT_SECRET'),
    });
  }

  async validate(payload: any) {
    return { ...payload };
  }
}

 

//JWT의 추출방법을 지정한다.
//fromAuthHeaderAsBearerToken()을 사용하여 헤더에서
//Bearer Token을 Authorization 헤더에서 추출한다.
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),

//토큰 만료 기간을 무시할지? false는 만료된 토큰 자체를 거부한다.
ignoreExpiration: false,

//서명에 사용할 알고리즘을 지정한다.
algorithms: ['HS256'],

//서명에 사용되는 비밀키로 환경변수에서 가져왔다.
secretOrKey: configService.get('JWT_SECRET'),

 

 

이제, 헤더에서 토큰을 추출하여 토큰이 있을 경우, 디코딩된 payload를 validate로 가져올 수 있다.

여기서는, 유저의 PK와 ID가 담겨있고, iat와 exp가 추가로 담겨있다 (iat : 발급시간, exp : 만료시간)

 

 

마찬가지로, @UseGuard를 이용해서 사용할 수 있으며, 컨트롤러 전역에 토큰 인증이 필요할 경우, 전역으로도 사용할 수 있다. 아래는 토이 프로젝트의 유저 컨트롤러 일부이다.

import { Controller, Get, Post, Body, Patch, Param, Delete, Query, Req, UseGuards, Request, Put } from '@nestjs/common';
import { ApiBody, ApiOperation, ApiTags } from '@nestjs/swagger';
import { UserService } from './user.service';
import { Jwt } from 'src/decorator/CurrentUserDecorator';
import CurrentUser from 'src/auth/dto/currentUser.dto';
import { JwtAuthGuard } from 'src/auth/guard/jwt-auth.guard';
import UserInvestmentDataPutDto from './dto/user-investmentData.dto';

@ApiTags('user')
@Controller('/api/v1/user')
@UseGuards(JwtAuthGuard)
export class UserController {
  constructor(private userService: UserService) {}

  @Post('/check')
  @ApiOperation({ description: '토큰 검증' })
  async checkToken(@Request() req) {
    return req.user;
  }

  @Get('/myInvestmentData')
  @ApiOperation({ description: '투자내역 조회' })
  async getInvestmentDataByYyyyMm(@Jwt() cu: CurrentUser, @Query('yyyymm') yyyymm: string) {
     return await this.userService.getInvestmentDataByYyyyMm(cu.no, yyyymm);
  }

  @Put('/myInvestmentData')
  @ApiOperation({ description: '투자내역 입력(by day) '})
  async putInvestmentData(@Jwt() cu: CurrentUser, @Body() userInvDataPutDto: UserInvestmentDataPutDto) {
    return await this.userService.putInvestmentData(cu.no, userInvDataPutDto);
  }

  @Delete('/myInvestmentData/:yyyymm/:day')
  @ApiOperation({ description: '투자내역 초기화(day)' })
  async deleteInvestmentData(@Jwt() cu: CurrentUser, @Param() params) {
    const { yyyymm, day } = params;
    return await this.userService.deleteInvestmentData(cu.no, yyyymm, day)
  }
}

 

 

 

마무리

Refresh Token관련 내용은 다음 포스팅에서..

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

[NestJS] JWT Token 인증 - (1) JWT 토큰이란?

Tech/NodeJS 2023. 11. 15. 14:54
728x90
728x90

 

 

 

쿠키와 세션을 통한 인증

 

쿠키와 세션 (Cookie & Session)

HTTP 프로토콜의 특징 비연결성 ( Connectionless ) 클라이언트가 서버에 요청(Request)할 때, 그에 대한 응답(Response)을 한 후, 연결을 끊는다. 비상태성 ( Stateless ) 클라이언트의 상태 정보를 가지지 않는

mag1c.tistory.com

 

쿠키는 인증이 필요한 요청을 할 때마다 쿠키를 던져 요청하는 동작 구조를 가진다.

 

쿠키를 통한 인증 시 다음과 같은 단점들이 존재한다.

쿠키의 단점

1. 민감 정보들을 노출당하기 쉽고 조작당하기도 쉽다.
2. 웹 브라우저마다 지원 형태가 다르기 때문에 공유가 불가능하다.
3. 쿠키의 사이즈 제한(4KB)이 있어 충분한 데이터를 담을 수 없다.
4. 서버에 매번 쿠키 값을 넘겨 인증해야하며, 조작된 쿠키가 넘어오더라도 방지할 수 없다.

 

 

 

세션은 개인 민감 정보를 노출하는 쿠키의 단점을 막을 수 있다.

세션은 인증 정보 자체를 특정 세션에 저장하고, 쿠키에 담아 클라이언트가 쿠키를 요청할 때 마다 세션에 저장되어 있는 정보와 동일한 지 인증한다.

 

아래의 코드는 개발자 첫 입문 프로젝트 시 사용했던 스프링 로그인 관련 코드이다.

로그인 시 인증 객체를 통해 세션 ID를 만들어 user_email, nickname세션에 저장하고, 세션 ID를 쿠키에 담아 반환할 것이다.

@RequestMapping("signin_check")
public ModelAndView signin_check(@RequestParam String email, String password, UserDto userDto,
                    HttpSession session, ModelAndView mv) {
    String str = userService.login(userDto);
    if(str != null) {
        session.setAttribute("user_email", userDto.getEmail());
        session.setAttribute("nickname", str);
        session.setMaxInactiveInterval(60*30);
        mv.setViewName("redirect:/");
    }else {
        mv.setViewName("user/signin");
    }
    return mv;
}

//로그아웃
@RequestMapping("signout")
public String logout(HttpSession session) {
    session.invalidate();		
    return "redirect:/";
}

 

 

세션에 식별할 수 있는 인증 값을 넣어두고, 비교할 수 있기 때문에 보완이 쿠키에 비해서 좋다. 만약 보안 문제가 발생할 경우, 서버의 세션을 지워버리면 된다. 하지만 반대로 이야기하면, 세션의 장애가 발생할 경우, 인증 전반에 문제가 생겨 사용자의 인증이 불가능한 상황을 야기시킨다.

 

추가적으로 세션의 단점을 정리해보자면 다음과 같다.

세션의 단점

1. 위에서 언급했듯이, 세션 자체에 문제가 발생하면, 인증 체계가 무너진다.
2. stateful하기 때문에 http의 stateless에 위배된다.
   > (scale out 시 걸림돌이 될 수 있다. 세션 ID를 또 다른 서버에 저장해야함)
3. 인증 요청 시 매번 세션 저장소를 조회해야한다.
4. 세션 저장소를 사용하는 비용. 사용자가 증가할수록 메모리 사용량도 증가.

 

 

 

JWT

JWT(Json Web Token)은 인터넷 표준 인증 방식으로, Json 객체에 인증이 필요한 정보들을 담은 후 비밀키로 서명한 토큰이다. 말 그대로 인증에 필요한 정보들을 Token에 담아 암호화시켜 사용하는 토큰이다.

 

인증 구조 자체는 쿠키와 크게 다르지 않지만, 서명된 토큰이라는 점이 주요 차이점이다. public/private key를 쌍으로 사용하여 토큰에 서명할 경우, 서명된 토큰은 개인 키를 보유한 서버가 이 서명된 토큰이 정상적인 토큰인지 인증할 수 있다.

 

 

JWT의 동작 과정

 

동작 과정은 위와 같다.

사용자가 서버에 로그인 요청을 보내면, 서버는 secret key를 사용해 암호화한 JWT 토큰을 발급하여 내보낸다.

이렇게 받은 토큰을 클라이언트의 로컬에 저장해두고, 인증이 필요한 요청 시 토큰을 헤더에 실어서 보내 토큰 검증 과정을 거친다.

 

 

 

JWT의 구조

JWT는 Header, Payload, Signature의 세 구조로 되어있다.

 

 

 

Header

Header는 토큰의 타입, 서명 생성에 어떤 알고리즘이 사용되었는지를 저장한다.

 

 

 

 

Payload

Payload에는 Claim을 Key-Value 형태로 저장한다.

 

여기서 Claim이란 사용자 혹은 토큰에 대한 프로퍼티이며, 토큰에서 사용할 정보의 조각을 의미한다.

여기에는, 커스텀하여 값들을 넣을 수 있지만,  민감한 정보를 payload에 담지 않는 것이 중요하다. header와 payload는 json이 디코딩되어 있을 뿐, 특별한 암호화가 걸려있지 않기 때문에 누구나 값을 알 수 있기 때문이다.

JWT 표준 Claim

1. iss(issuer) - 토큰 발급자
2. sub(subject) - 토큰 제목(사용자에 대한 식별 값)
3. aud(audience) - 토큰 대상자
4. exp(expiration time) - 토큰 만료 시간
5. nbf(not before) - 토큰 활성 날짜
6. iat(issued at) - 토큰 발급 시간
7. jti(jwt id) - 토큰 식별자 (issuer가 여러 명일 때를 위한 구분 값)

 

 

 

 

Signature

헤더와 페이로드의 문자열을 합친 후, 헤더에서 선언한 알고리즘과 key를 통해 암호화한 값이다.

 

위에서 언급했듯, Header와 Payload는 단순히 Base64로 인코딩되어있어 누구나 쉽게 복호화할 수 있지만, Signature는 암호화 시 사용된 key가 없으면 복호화 할 수 없다. 이를 통해 JWT는 보안상 안전하다는 특성을 가질 수 있게 된 것이다.

 

 

 

정리

장점

1. 로컬에 저장하기 때문에 서버 스펙에 영향을 받지 않고, 네트워크 부하가 적다 (http 헤더를 통한 전송)
2. 공통 스펙으로 사용 가능한 확장성이 뛰어난 토큰
3. 여러 플랫폼 및 기기에서 동작 가능하며 서로 다른 도메인에서도 통신이 가능
4. 별도의 세션 저장소를 강제하지 않아 stateless하다.

 

단점

1. 토큰 만료 처리를 구현해야한다.
2. 토큰의 크기를 신경써야한다. (트래픽에 영향)

 

 

JWT Token을 활용해서, 개발하는 서비스의 성격에 맞게 보안과 사용자의 편의를 고려하여 조율하면서 좋은 인증 정책을 결정할 수 있도록 해야겠다.

 

 

 

 

참조

https://jwt.io/introduction

https://brunch.co.kr/@jinyoungchoi95/1

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

[NestJS] typeorm의 transaction

Tech/NodeJS 2023. 11. 9. 09:50
728x90
728x90

 

 

서론

JWT를 구현하는 도중, 아래와 같은 에러가 발생했다.

//회원가입 및 JWT TOKEN 발급 코드
async signup(signInDto: UserSigninDto) {
    const user = await this.userService.findById(signInDto.id);
    const exist = user != null;
    if (exist) {
        throw new BadRequestException('중복된 아이디 존재');
    }
    else {
        const valiUser = new UserEntity();
        const hashedPassword = this.hash(signInDto.pw);
        valiUser.id = signInDto.id;
        valiUser.pw = hashedPassword;
        const savedUser = await this.userService.saveUser(valiUser);
        return this.removePasswordFromUserData(savedUser);
    }
}

 

회원가입과 동시에 JWT Token을 발급하는 그런 로직을 짜고 있었는데, DB에는 들어왔지만, 에러는 발생한 그런 상황이다. 토큰은 발급되지 않았다.

 

 

 

 

@Transactional

자바를, 스프링을 사용했을 당시 AOP를 활용한 선언적 트랜잭션인 @Transactional 애너테이션을 사용하곤 했었다.

//게시판의 게시글을 삭제하는 메서드 
@Transactional
public void removeBoard(Long id)throws Exception{
    replyDAO.removeAll(id); //삭제할 게시글의 답글 삭제
    boardDAO.deleteBoard(id); //게시글 삭제 
}

 

 

 

typeorm에서는?

데커레이터는 권장하지 않는다고 하여 getManager().transaction()과 queryRunner중 queryRunner을 사용했다.

async signup(signInDto: UserSigninDto) {
    //typeorm transaction start
    const queryRunner = this.datasource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();
    try {
        const user = await this.userService.findById(signInDto.id);
        const exist = user != null;
        if (exist) {
            throw new BadRequestException('중복된 아이디 존재');
        }
        else {
            const hashedPassword = this.hash(signInDto.pw);

            const valiUser = new UserEntity();                
            valiUser.id = signInDto.id;
            valiUser.pw = hashedPassword;

            const savedUser = await this.userService.saveUser(valiUser);                
            const delPwUserData = this.removePasswordFromUserData(savedUser);               

            const token = this.createToken(delPwUserData)                
            await queryRunner.commitTransaction();

            return token;
        }
    } catch (error) {
        await queryRunner.rollbackTransaction();
        console.error(error);
    } finally {
        await queryRunner.release();
    }

}

 

 

 

 

 

 

트랜잭션 다루기 (feat. NestJS, TypeORM)

Nest JS에서 트랜잭션을 올바르게 다루기 위해 겪었던 고민과 문제에 대한 해결방법을 정리한 글입니다

www.timegambit.com

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

방명록