NestJS 전략 & 데코레이터 패턴에 대해서

·2022년 6월 22일
5

SKILL

목록 보기
5/16
post-thumbnail

이번에 적어볼 것은 여러가지 디자인 패턴 중에서
구조적 설계에 속해있는 데코테이터 패턴행위 디자인에 속해있는 전략 패턴에 대해서 적어보려고 한다.

그리고 NestJS는 어떤식으로 커스텀 데코레이터가 어떻게 작동을 하게되는 것인지
또, 실제 돌아가는 코드를 확인하면서 하나하나 알아가보려고 한다.

원래는 CS 공부해야하는데 네트워크가 머리아파서 정신승리하려고 쓴다..

해당 코드는 모두 여기서 보실 수 있습니다. => https://github.com/Sweet-Salty-Project/Sweet-Salty-Server

전략 패턴 (Strategy)

전략패턴이란 수차례 같은 로직이 돌아가는 것을 방지하기 위하여 특정하는 입력값에 따라 로직을 구현하여 캡슐화를 시켜놓은 것을 이야기한다.

사실 디자인 패턴을 공부하면서 계속 느끼는건데, 전략 패턴은 퍼사드 패턴과도 비슷한게 아닌가? 라는 생각도 든다.
아니 그냥 패턴이 엄청 많은 갈래로 나눠져있는데, 정말 아주 작은 차이로 이름이 달라가지고 헷갈리는 것 같기도 하다(....)

전략 패턴의 핵심은 바로 반복되는 작업량을 줄일 수 있고, 큰 변경점이 생기더라도 로직을 건드는 것으로 해결할 수 있다는 것이다.

이게 무슨 소리인가 도대체 알 수 없으니 코드를 직접 보면서 확인을 해보도록 하자.

jwt-access.strategy.ts (회원 인가를 위한 전략패턴)

이것은 프론트에서 온 토큰이 올바른 것인지 확인하고 맞다면 토큰 속에 있는 정보는 어떤 것인지 검증을 하는 코드다.

전략패턴에서 상당히 많이 쓰이는 Passport 라이브러리와 JWT, 그리고 로그아웃을 구현하기 위하여 Redis를 사용했는데

여기서 중요한 부분은 validate부터다.

validate 코드 한줄 한줄 해석

  • const access = req.headers.authorization.replace('Bearer ', '');
    • 헤더에 담겨온 액세스토큰을 분리하는 코드다.
  • const check = await this.cacheManager.get(access);
    • 해당 액세스토큰이 Redis에 존재하는지 확인을 한다.
    • 현재 프로젝트 상에서는 로그아웃을 눌렀을 경우 해당하는 토큰이 만료시간만큼 Redis에 저장되는 구조로 되어있다.
  • if (check) throw new UnauthorizedException();
    • 존재할 경우에는 로그아웃이 완료된 것이므로 401 에러코드를 던져준다.
  • const user = await this.userRepository (너무길어서 축약)
    • 액세스토큰에 담겨있는 userEmail의 값으로 유저의 정보를 가져온다.
  • return (축약)
    • payload에는 JWT토큰을 디코딩시킨 정보가 존재하는데, 이것으로 userEmail을 가져와서 리턴해준다.
    • userId 또한 위에서 불러온 것으로 가져와서 리턴해준다.

여기서 변경됐던 것은 로그아웃과 user의 정보를 가져오는 것이었다.

원래는 단순히 인가를 해주는 것 의외에는 로직이 존재하지 않았다.

유저 정보를 가져오는 로직이 전략패턴에 추가된 이유

유저의 정보를 가져오는 것을 이야기하면 현재 로그인을 할 때 받아오는 값은 회원의 이메일과 비밀번호다.

하지만 회원의 이메일과 비밀번호 중 유니크한 값은 이메일이 있었지만,
PK는 아니였기 때문에 데이터를 DB에서 불러오는 것에서 속도 문제가 있었다.

그래서 PK였던 userId를 담았어야했는데 JWT보안관련으로 문제가 있다보니 정보를 최대한 담지 않기 위하여 이메일만 담고 있었다.
어떻게 하면 개선을 할 수 있을까 고민을 하던 차에
인가가 필요한 곳에는 언제나 액세스전략이 적용되어있다는 것을 깨달았고
거기에 userId를 추가로 담도록 코드를 변경하게 되었다.

이렇게 코드를 변경하면서 이런저런 수정사항 없이 더욱 빠른 속도로 데이터를 읽어올 수 있게 됐다.

근데 이렇게 글을 쓰면서 보니까, 왜 굳이 이메일을 토큰에 담았나 하는 생각이 든다.
어짜피 검증하면서 user정보 다 훑어오는데 이럴거면 그냥 아이디를 토큰에 담으면 된게 아니였나싶은데....?


로그아웃을 전략패턴에 추가된 이유

로그아웃의 구현에 대한 이야기를 하자면 로그아웃을 성공적으로 만들었지만 어떻게 작동을 시키느냐? 의 문제가 있었다.

그래서 이것도 생각을 해보니 인가를 해주는 액세스전략에 적용을 시키면 된다고 생각이 들어서 바로 적용을 하게 됐다.


로그아웃을 처리하는 blackList의 service 코드. 토큰의 남은 시간을 ttl로 적용하는 것을 하단에서 볼 수 있다.

이렇게 Redis에 해당하는 토큰이 존재하는지 검증하는 것으로 추가적인 구현없이 로그아웃을 완성하게 됐다.


전략패턴 정리

전략패턴을 사용한다는 것은, 비슷한 결과를 내야만 하는 로직이 있을 경우 통합하여 간단하게 처리를 해줄 수 있다.
또한 변경점이 생겼을 경우에는 내부의 알고리즘을 추가, 수정, 삭제 하는 것으로 손쉽게 적용을 할 수 있게 된다.

새로운 전략패턴을 만들기 위해서 나는 @nestjs/passport를 사용하여 아래와 같이 구현을 하였는데, 아마 다양한 방식이 있을 것이다.

@Injectable()
class JwtAccessStrategy extends PassportStrategy(Strategy,'전략패턴의 이름'){로직}

이정도로 전략 패턴의 설명은 끝을 내고 그 다음으로 설명하게 될 것은 데코레이터다.

데코레이터 패턴(Decorator)?

데코레이터란 기존 객체의 동작을 동적으로 증대시키기 위해 구성된 디자인 패턴이다.

데코레이터의 핵심은 한 줄로 정의할 수 있는데, 바로 기존의 값을 가지고 확장을 할 수 있다 는 것이다.

기존의 입력값을 가지고 다양한 것을 적용시켜서 새로운 로직을 만드는 경우도 있고
로그를 쌓기 위해서 사용하는 모습도 보이는 것 같다.

전략패턴과도 상당히 유사한데 어떤 차이가 있는지 이번에는 API를 보면서 알아보도록 하자.

updateProfile

NestJS에서는 앞에 @를 붙이는 것으로 데코레이터를 선언할 수 있다.

updateProfile 코드 한줄 한줄 해석

  • @UseGuards(GqlAuthAccessGuard)
    • Guard라는 미들웨어인데 해당 코드가 실행되기 전에 최우선적으로 작동되는 미들웨어다.
    • 여기서는 이 회원이 로그인 한 회원인지 검증을 해주는 AccessGuard라는 전략패턴과 같이 사용된다.
  • @Mutation(() => User)
    • 그래프큐엘에서 CUD를 위해 사용하는 뮤테이션, 그리고 리턴값으로 User의 Entity를 보낸다.
  • @Args('profile') profile: string,
    • 그래프큐엘에서 입력값을 받기 위하여 @Args라는 데코레이터를 사용하고 이름은 profile이다.
    • 입력값의 타입은 string이다.
  • @CurrentUser() currentUser: ICurrentUser,
    • 커스텀 데코레이터로 위의 액세스전략에 나와있던 이메일과 아이디가 들어가있다.
    • 타입은 미리 지정해놓은 커런트유저의 인터페이스다.
  • return this.userService.updateProfile({ profile, currentUser });
    • 업데이트프로필의 서비스로직을 호출하는 함수, 리턴이 될 경우엔 User의 Entity가 존재한다,

유저의 프로필사진을 업데이트하기 위해서는 무엇이 필요할까?

  1. 변경이 될 유저의 정보
  2. 변경될 프로필의 정보

이렇게 두가지가 필요할 것이다.

2번의 경우에는 그냥 스트링으로 입력값이 들어오기 때문에 문제가 없겠지만, 유저의 정보는 어떻게 가져와야할까?

팀프로젝트에 사용된 JWT를 기반으로 생각한다면
프론트에서 보내주는 액세스토큰에 내가 명시해놓은 유저를 판가름할 수 있는 유저의 정보가 들어있을 것이다.

하지만 프론트에서 보내주는 Context중에서 액세스 토큰이 존재하는지, 이것이 정상적인 액세스 토큰인지 검증하는 과정이 필요할 것이다.

그래서 NestJS에서는 UseGuards라는 데코레이터를 통하여 인가를 시켜주는데 이것에 대해서 알아보도록 하고
Context에는 수많은 데이터가 있을텐데, 이것을 필요한 것 만 보내주는 커스텀 데코레이터에 대해서 알아보겠다.

UseGuards에 대해서(데코레이터에 대해서)

UseGuards는 nestjs에서 자체적으로 제공해주는 데코레이터 중 하나로 인가를 위해 사용되는 옵션이다.

(진짜 여기까지 벌써 공부하게 될 줄은 정말 몰랐는데요...네)

이것은 nestjs에서 제공하는 설정파일로 node_modules/@nestjs/common/decorators/core/user-guards.decorator.js에 있는 코드다.

리턴을 잘보면, 화살표 함수를 사용하여 들어온 값의 클래스와 메소드를 인자로 사용해서 리턴하는 모습을 보여주고 있는데

메소드가 존재한다면 메소드의 결과값을 리턴해주고
존재하지 않는다면 클래스 그 자체를 리턴하는 것으로 보인다.

유심히 보면 if(descriptor)에 target.constructor(클래스 생성자)를 사용하는 모습을 볼 수 있기도 하다.

즉 입력된 값을 통하여 작업을 한 후, 그 결과값을 돌려주는 것의 모습을 볼 수 있다.
(이게 정확한가....? 틀리다면 바로잡아주시면 감사하겠습니다.)

하지만(또) 가드에서 원하는 ExecutionContext의 모양이 RestAPI의 기준으로 맞춰져 있어서
GraphQL에서 사용하기 위해서는 변경사항이 필요하다.

GqlAuthAccessGuard

공식문서의 설명 => https://jakekwak.gitbook.io/nestjs/graphql/tooling

위에서 언급했던 것처럼 GraphQL은 컨텍스트의 모양이 달라 변환 작업을 해줘야한다.

그래서 GqlExecutionContext를 통하여 새로운 컨텍스트를 만들고, 리턴을 해주는 작업이 필요하다.

구글 소셜 로그인의 API, RestAPI로 되어있어서 그냥 호출하는 것으로 사용이 가능하다.

즉 UseGuards이라는 데코레이터를 사용하여 GqlAuthAccessGuard라는 인가 과정이 포함되어있는 전략패턴을 실행하고 결과값을 볼 수 있었다.


전략패턴과 데코레이터패턴의 차이

이덕분에 데코레이터 패턴과 전략 패턴이 상당히 유사한 것처럼 느낄 수 있는데
약간의 차이가 있다는 것을 볼 수 있다.

  • 전략 패턴
    • 들어오는 값을 통하여 알고리즘을 통해 새로운 값을 만들어냄
  • 데코레이터 패턴
    • 들어오는 값을 통하여 다른 값으로 리턴함

커스텀 데코레이터를 만드는 방법

다시 코드로 돌아와서, 커스텀 데코레이터라 언급했던 @CurrentUser()는 어떻게 만드는 것인지 알아보자.

이것은 @CurrentUser() 라는 데코레이터를 만든 로직이다.

GqlAuthAccessGuard에서 보였던 특징처럼 GqlExecutionContext도 볼 수 있기도 하다.
이것은 토큰 정보를 가져오는 가드단의 데이터를 사용하기 때문에 필요한 것이고, 일반적으로는 필요가 없다.

어떤식으로 만들어지는지 확인하기 위하여 createParamDecorator()를 확인해보았는데

NestJS에서 자체적으로 이렇게 데코레이터를 만들어주는 것을 지원한다.

express의 경우라면 데코레이터 팩토리라는 것을 구축해서 만들어야했지만
NestJS에서는 그러한 과정이 프레임워크상에 전부 있기 때문에 손쉽게 데코레이터를 생성할 수 있다.

그리고 NestJS에서는 새로운 데코레이터를 만들어내기 위하여 reflect-metadata 라는 라이브러리를 사용하는데, 한 줄이 제일 중요하다.

 Reflect.defineMetadata(constants_1.ROUTE_ARGS_METADATA, (0, assign_custom_metadata_util_1.assignCustomParameterMetadata)
        (args, paramtype, index, factory, paramData, ...paramPipes), target.constructor, key);

라이브러리에서 제공하는 Reflect.defineMetadata사용하는 것으로
새로운 메타데이터를 생성하는 것으로 커스텀 데코레이터가 만들어진다.

그런데 찾다보니 더욱 더 단순하게 만드는 것도 있어서 한번 가져와봤다;

SetMetadata()라는 것으로 사용할 수 있는 메소드인데, 앞에는 이름 뒤에는 값을 넣음으로 커스텀데코테이터가 생성이 된다고 적혀있다.

결국 데코레이터라는 것은 메터데이터로 구현이 되어있고, 해당하는 메타데이터를 호출하면서 작동이 되는 것이라고 보면 될 것 같다.

커스텀 데코레이터의 호출

그럼 이건 또 어떻게 호출을 하나 찾아봤더니 이 부분이 어디서 나와있는지 확인이 되지 않았다.

분명 DNS와 비슷한 개념으로 @CurrentUser()를 호출할 경우
CurrentUser라는 이름으로 명시되어있는 데코레이터를 가져오면서 값을 실행시켜줄 것이라 생각을 하고 있는데

위에 보이는 것은 decorators.module.js의 코드인데 음... 여기서는 그런 작업을 한다기보단
메타데이터를 가져와서 데코레이터를 만드는 느낌이랄까....?

그래서 데코레이터를 호출해서 사용하게 만드는 코드가 적혀있는 곳을 찾아봐야할 것 같다.

내가 참고를 했던 유투브에는 아래와 같은 코드가 적혀있던데, 왜 나는... 안보이지? 뭔가 라이브러리를 덜깠나 싶다;

출처 : https://www.youtube.com/watch?v=Zs_werD3bP8

이렇게 전략 패턴과 데코레이터 패턴을 정리해봤는데 느낀 것은
공부를 하고 싶다면 깊게 해야한다는 것을 느꼈다.

진짜 전부 node_modules에는 모든 것이 적혀있고 그저 내가 모를 뿐이라는 것이다...
시간을 늘여서 전부 다 정리를 하고 싶긴 한데, 얼마나 오래 걸릴지는 장담을 못하겠다(...)

profile
물류 서비스 Backend Software Developer

0개의 댓글