23년 3월 24일 TIL & 개발노트 (Typescript에서 문자열 상수, 그리고 코드 리팩토링)

Posted by , March 31, 2023

thumbnail





개발하며 얻은 TIL

흑우집합소 개발하며 얻은 내용 및 노션에 작성한 내용을 정리한 포스팅이다.
오늘 개발하면서 배운 내용은 아래와 같다.

  1. 예외처리의 미흡함 (예외처리 부분)
  2. 문자열 상수(String Literal) - Typescript Object 키 문제
  3. 반복적 코드를 줄이는 리팩토링

각 항목은 아래의 내용을 참고하자.

1. 예외처리 부분

오늘도 개발을 하던 도중 코드가 이상한 부분을 찾게 되었다.
백엔드 측의 에러 핸들링 부분이었다.

if (exception instanceof AuthException) {
  //토큰 문제가 발생한 경우
  if (
    exception.errorCode === '8-1' ||
    exception.errorCode === '8-2' ||
    exception.errorCode === '8-3' ||
    exception.errorCode === '8-4' ||
    exception.errorCode === '8-5'
  ) {
    const decodeData = this.decodeJwtOutboundPort.accessJwtDecode(request.header(HEADER_JWT));
    //로그아웃 처리
    await this.logoutAccountInboundPort.excute({
      email: decodeData.email,
      joinType: decodeData.joinType,
    });
    response.setHeader(HEADER_JWT, '');
  }
}

Error Handler 부분의 일부다.
여기서 저기 const decodeData 부분의 로직을 보면 request의 헤더로부터 jwt를 가져오는 부분이 있다.

만약 헤더에 jwt를 안가지고 올 경우 어떻게 될까?
또 버그를 양산하는 모양새가 된다.

물론 jwtStrategy에서 검증을 하겠지만, 혹시 AuthExceptionHandler를 strategy 없이 사용하는 곳에서 발생했다면?
그래서 이 부분은 검사를 한번 더 하는 방향으로 로직을 수정했다.

if (exception instanceof AuthException) {
  //토큰 문제가 발생한 경우
  if (
    exception.errorCode === '8-1' ||
    exception.errorCode === '8-2' ||
    exception.errorCode === '8-3' ||
    exception.errorCode === '8-4' ||
    exception.errorCode === '8-5'
  ) {
    //만약 JWT가 헤더 안에 있다면...
    if (request.header(HEADER_JWT)) {
      const decodeData = this.decodeJwtOutboundPort.accessJwtDecode(request.header(HEADER_JWT));
      //로그아웃 처리
      await this.logoutAccountInboundPort.excute({
        email: decodeData.email,
        joinType: decodeData.joinType,
      });
    }

    response.setHeader(HEADER_JWT, '');
  }
}

이렇게 한번 보호를 함으로써 예외를 막을 수 있었다.

2. Typescript Object 키 문제

개발을 하던 도중 아래와 같은 코드를 작성했다.

const F_HEADER_BKEY = 'test_key' as const;
const defaultH: { [key: string]: string } = {
  'key-str': 'value-str',
  'F_HEADER_BKEY': '111aaa',
};

console.log('show value = ', defaultH);

이렇게 작성할 경우 결과는 아래와 같아 나온다.

[LOG]: "show value = ",  {
  "key-str": "value-str",
  "F_HEADER_BKEY": "111aaa"
}

내가 원하는 그림은 F-HEADER_BKEY의 값이 저 상수 이름 값이 아닌,
test_key 상수의 값이 들어가길 원했다.

이 문제를 해결하려면 아래와 같이 작성한다.

const F_HEADER_BKEY = 'test_key' as const
const defaultH: { [key: string]: string } = {
  'key-str': 'value-str',
}

defaultH[F_HEADER_BKEY] = "aaa"

//결과

[LOG]: "show value = ",  {
  "key-str": "value-str",
  "test_key": "aaa"
}

맨 처음처럼 나타나는 이유는 내가 선언한 defaultH의 경우,
타입이 다음과 같이 { [key: string]: string } 되어 있다.

즉 key는 string으로 되어 있기에 string 자체로 인식을 해버린다.
하지만 두 번째 제대로 나오는 부분에서는 Object형에 접근하기 위한 값으로 사용한 것이,
문자열(string)이 아닌 **상수 문자열(string literal)**를 사용했기에 적용이 된다.

그리고 하나 더 찝어서 이야기 하면 아래와 같은 코드도 동작이 안될 것이다.

defaultH.F_HEADER_BKEY = "aaa"

//결과
[LOG]: "show value = ",  {
  "key-str": "value-str",
  "test_key": "aaa",
  "F_HEADER_BKEY": "a111"
}

이거는 간단한 것이 object에 .(Dot)으로 접근 시 해당 키를 이용해 접근하는 방식이다.
하지만 여기도 F-HEADER_BKEY를 문자열 자체로 받아들인다.

이게 문자열 상수 값으로 받아들여지지 않고, 그냥 문자열 자체로 받아들인단 의미다.
그래서 혹시 선언된 객체형에 키를 동적으로 쓰고 싶다면 [] 형식을 이용해서 접근하는게 좋다.

3. 반복적 코드를 줄이는 리팩토링

백엔드 측을 개발하면서 다음과 같은 코드가 있었다.
(아래 코드는 예시를 위해 사용하여 일부 변수 및 내용이 코드 문법에 어긋날 수 있습니다.)

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'Jwt') {
  constructor(
    @Inject(VERIFY_JWT_OUTBOUND_PORT)
    private readonly verifyjwtOutboundPort: VerifyJwtOutboundPort,
    @Inject(DECODE_JWT_OUTBOUND_PORT)
    private readonly decodJwtOutboundPort: DecodeJwtOutboundPort,
    @Inject(CREATE_JWT_OUTBOUND_PORT)
    private readonly createJwtOutboundPort: CreateJwtOutboundPort,
    @Inject(FIND_ACCOUNT_EMAIL_JOINTYPE_STATUS_OUTBOUND_PORT)
    private readonly findAccountEmailJoinTypeOutboundPort: FindAccountEmainJoinTypeStatusOutboundPort,
    @Inject(LOGOUT_ACCOUNT_INBOUND_PORT)
    private readonly logoutAccountInboundPort: LogoutAccountInboundPort,
  ) {
    super();
  }

  async validate(req: Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>, options?: any) {
    const res = req.res;
    const jwt = req.header(HEADER_JWT);

    /**
     * JWT가 존재하지 않는 경우 에러 처리
     */
    if (!jwt) {
      throw new AuthException(AuthJwtErrorCode.jwtNotFound());
    }

    const account: Account = await this.getAccountInJwt(jwt, JWT_TYPE.ACCESS_TOKEN);
    //...Some work
  }

  //JWT 내에서 계정 정보를 가져오는 함수
  private async getAccountInJwt(jwt: string, tokenType: JwtType): Promise<Account> {
    //Access가 만료된 경우 리프레시 토큰도 체크해줘야 한다.
    const jwtData = this.decodJwtOutboundPort.accessJwtDecode(jwt);

    //계정 정보를 가져온다.
    return await this.findAccountEmailJoinTypeOutboundPort.excute({
      u_email: jwtData.email,
      ua_joinType: jwtData.joinType,
      uStatus: ACCOUNT_STATUS_TYPE.ACTIVE,
      uAccAuth: jwtData.auth,
    });
  }
}

근데 코드를 작성하다보니...getAccountInJwt 함수의 경우 다른 곳에서도 비슷한 방식으로 사용되고 있었다.
쉽게말해 중복적으로 코드가 작성되고 있었다.

이 부분은 Inbound Port 성향의 서비스 로직으로 분리할 수 있었다.
Port & Adaptor 아키텍쳐 방식을 사용할 때 In과 Out을 저번에 받은 강의 내요을 토대로 사용한다.

In 성향은 외부 세계에서 요청이 오는 경우로 나누고,
Out은 백엔드 내에서 처리하여 인프라 쪽과 연계되는 경우로 나눈다.

지금처럼 Jwt를 받아서 그 안에 담겨있는 정보를 요청해야 하는 경우,
이 성향은 Inbound 성향이라 할 수 있다.

설령 내부에서 호출한다 해도 jwt라는 속성 자체가 외부에서 오기 때문이다.
물론 인프라 측의 DB 로직을 한번 타긴 하는데, 난 이런 경우 처음 접근하는 변수를 기준으로 잡는다.

그래서 이 부분은 Inbound Port로 만들었다.

//Inbound Port 정의
import { JwtType } from 'src/domain/auth/types/jwt.enums';

export const GET_ACCOUNT_IN_JWT_INBOUND_PORT = 'Get_Account_In_Jwt_Inbound_Port' as const;

export type GetAccountInJwtInputDto = {
  jwt: string;
  jwtType: JwtType;
};
export type GetAccountInJwtOutputDto = {};

export interface GetAccountInJwtInboundPort {
  excute(params: GetAccountInJwtInputDto);
}

//인바운드 인터페이스 구현체 서비스
export class GetAccountInJwtService implements GetAccountInJwtInboundPort {
  constructor(
    @Inject(DECODE_JWT_OUTBOUND_PORT)
    private readonly decodJwtOutboundPort: DecodeJwtOutboundPort,
    @Inject(FIND_ACCOUNT_EMAIL_JOINTYPE_STATUS_OUTBOUND_PORT)
    private readonly findAccountEmailJoinTypeOutboundPort: FindAccountEmainJoinTypeStatusOutboundPort,
  ) {}

  async excute(params: GetAccountInJwtInputDto) {
    //Access가 만료된 경우 리프레시 토큰도 체크해줘야 한다.
    const jwtData = this.decodJwtOutboundPort.accessJwtDecode(params.jwt);

    //계정 정보를 가져온다.
    return await this.findAccountEmailJoinTypeOutboundPort.excute({
      u_email: jwtData.email,
      ua_joinType: jwtData.joinType,
      uStatus: ACCOUNT_STATUS_TYPE.ACTIVE,
      uAccAuth: jwtData.auth,
    });
  }
}

위와 같이 별도 Inbound Service로 분리할 경우 아래와 같이 간단하게 작성할 수 있었다.

constructor(@Inject()) {} //여기서 아래의 getAccountInJwtService 인터페이스 주입을 해준다.

//Get Account
const account: Account = await this.getAccountInJwtService.excute({ jwt, jwtType: JWT_TYPE.ACCESS_TOKEN });

//기존 코드
//const account: Account = await this.getAccountInJwt(jwt, JWT_TYPE.ACCESS_TOKEN);

이렇게 함으로써 다른 영역에서도 해당 로직을 재사용할 수 있게 되었다.

정리

개발을 하면서 계속 더 나은 코드를 작성하는 건 뭐랄까...
완벽할 수 없지만, 완벽에 가까워지도록 하는 형태가 된다고 해야 할까?

그런 의미로 리팩토링은 참 재미있는 것 같다.