LearneWeek 1,2 Summary!


  1. 미리 할당되어있던 2주 남짓 분량의 프론트엔드 태스크들을 완료했다.
  2. 테스트코드가 사실상 없던 프로젝트에 테스트코드를 도입하고 커버리지를 10% 까지 끌어올렸다
  3. 테스트코드를 작성하던 도중 알게되어 하수라 관련 안전한 트랜잭션이 되도록 로직 보완했다.

전시영역을 담당하는 백엔드 nodejs 서버 프로젝트에 테스트코드 도입하기


“Hasura 라고 있어요 …” (전시영역 관련 독특한 구조)

hasura-img-1

  • 우리회사는 커머스의 전시영역과 전시영역 관련한 데이터(이벤트, 기획전, 배너, 프론트엔드 컴포넌트 등)를 별도의 프로젝트와 DB로 분리해서 관리하고 있다.

  • 그리고 해당 프로젝트는 Hasura라는 오픈소스를 사용하는데, 짧게 요약하면 DB스키마를 보고 자동으로 gql api를 만들어준다.(gql특유의 이슈들도 잘 관리되어 최적화를 해준다)

  • 물론 전시영역 관련된 데이터이지만, 디비 전부를 인터페이스화 할 수 는 없기에 그 앞단에 게이트웨이성 nodejs서버를 두고 하수라 엔진에 질의하며 응답을 포워드 해주는 구조이다.

hasura-img-2

  • 초창기에는 위에서 언급한 정도의 역할만 하며 해당 프로젝트가 잘 지켜졌다. 전시영역 관련된 뷰테이블에 간단한 질의 하는 정도 위주로 코드가 작성되어있었다.

  • 다만 해당 프로젝트 특성상 여러 데이터소스 컴포넌트들이 붙게 되거나 다른 서버랑 통신하는 경우들이 늘어났다.

  • 과정중에 결코 가볍지 않은 비즈니스 로직들도 늘어가기 시작했다.

hasura-img-3

  • 결국 도메인 하나를 무리없이 도입하게 될 정도로 프로젝트에 비즈니스 로직이 추가됐고, 위의 그림처럼 이번에는 커뮤니티 관련 기능들이 해당 프로젝트에 추가되는 수준의 지경에 이르렀다.

  • 그리고 특히 테스트코드에 엄청 자율적인 회사 특성상, 테스트 코드는 없는 상태로 프로젝트가 유지보수 되고 있다.

AS-IS

테스트코드-ASIS

언급한 것처럼 테스트코드에 매우 자율적인 기조를 유지해왔기 때문에, 이 프로젝트에 테스트코드는 jest기준 0%대 였다.

그나마 설정되어있는 것들과 작성되어 있는 테스트코드 역시 입사 완전 초기에 나와 같이 실험적으로 도입하면서 설정해 둔 것 이며, 유일한 테스트코드도 기존에 내가 작성해둔 것 이 전부였다.

해당 테스트코드는 특정 인증관련 해쉬값을 디코드해서 나온 정보대로 잘 캐시를 타는지 정도를 테스트해둔 코드였고, 이게 전부였다.

소스코드-ASIS

디렉토리 구조의 예시는 다음과 같았다.

src/
└── (인가 분리)/
    └── (인가에 따른 client 객체들 설정)/
        ├── client_settings.ts
        └── schemas/
            ├── common.ts
            ├── hasura-banner.ts
            ├── hasura-community.ts  # 신규 프로젝트로 작성/수정 중
            └── (...)

그리고 schemas/some-domain.ts에 외부에 노출되는 gql api들이 정의 되어 있는데, 해당 파일의 구조들은 다음과 같다.

// 타입 정의들
interface IBannerDetailArgs {
  ...
}
interface IBannerSchedule {
  ...
}
interface IBanner {
  ...
}

// gql type defs
const typeDefs = `
  ${defaultTypeDefs}

  type Query {
    ...
  }
  
  type BannerSchedule {
    ...
  }
  type Banner {
    ...
  }
`;

// resolver(핸들러)
const bannerSchedule = async () => {
  ...
};

// resolver 등록
export const resolvers = {
  Query: {
  }
};

export const schema = makeExecutableSchema({
  typeDefs,
  resolvers
});

TroubleShootings

1 - 하나의 리졸버 핸들러가 여러번 외부 컴포넌트를 호출하는 이슈.

처음에는 어찌됐던 client객체가 종단지점이라고 생각했고, client객체들만 잘 모킹하면 간단하게 해결 될 줄 알았다. 언급한것처럼, 비즈니스 로직이 늘어나면서 하나의 핸들러가, 다른 모듈과 외부 통신을 하거나, 하수라 엔진에 여러번 질의를 진행하는 경우가 많이 있었다.

  • 이부분 때문에 도메인 하나를 전부를 리팩토링했다.

  • 여러번 질의하거나 내부적으로 외부 컴포넌트를 호출하는 경우 무조건적으로 함수를 분리했다.

  • 분리한 함수들 (즉 리졸버에 등록되지 않으면서, 리졸버를 처리하기 위해 질의하는 로직들)을 모듈화 했고, 그 덕분에 쉽게 모킹 할 수 있었다.

const some_resolver = async () => {
  bool isOk = await client.query({ ... }); // 이런 부분을 모듈화해서 분리, 이후 모킹
  if (isOk) {
    const res = await client.query({ ... });
    return res;
  }
}

2 - test env 분리하기

  • 프로젝트 특성상 외부 컴포넌트 혹은 데이터소스에 변경을 유발하는 쿼리들이 정의되어있다.
  • 그리고 기존에는 test를 고려하지 않고 작성되어 있어, 실제 운영 env와 함께 동작하면 안되는 위험한 함수들이 있었다.
  • 결론적으로 jest의 test env를 새로 작성하 테스트 파이프라인에서는 해당 env로 동작하도록 파이프라인을 수정했다.

3 - typedefs와 같은 gql문법을 조금 더 안전하게 보장 할 수 있지 않을까?

기존에는 엄청난 길이의 typedefs를 거의 로우하게 텍스트로 관리하고 있다. 오타에 많이 취약한 구조였고, 더 좋게 관리할 방법이 있지 않을까 고민됐다.

const typeDefs = `
  ${defaultTypeDefs}

  type Query {
    (많은 쿼리를 text로 정리한 내용 ..)
  }
  
  // 타입도 진짜 개많다.
  type BannerSchedule {}
  type Banner {}
`;


export const schema = makeExecutableSchema({
  typeDefs,
  resolvers
});

gql 관련해서 여기저기 찾아보니, makeExecutableSchema()함수에 정말 많은 기능이 있었다.

requireResolversForNonScalar, requireResolversForArgs와 같은 것들이 있는데, 이런 옵션은 기본값이 false로 되어있다. (관련된 사용 예시 레퍼런스들을 찾아 봤을 때, 실용적인 면에서 너무 보수적인 옵션값들이다.)

각각 스칼라객체의 타입이 잘 구현되어있는지, 리졸버의 아규먼트가 전부 있는게 맞는건지 등을 검증하는데, true로 수정해도 커뮤니티쪽에서는 에러가 안잡혔다.

마찬가지로 너무 가혹한 기준 같아서 내가 진행하는 테스트코드에만 적용을 했다.(before에서 한번 검증하고 시작)

TO-BE

  • 가장 큰 도메인이기는 하다고 짐작했지만, 정말로 5년 된 레거시코드의 커버리지를 10% 내외로 끌어올렸다.

  • 같이 진행한 리팩토링 덕분에 코드를 보기 훨씬 편해진 것 같다.

  • 트랜잭션과 관련해서 조금 더 안전한 코드가 된 것 같다.

테스트코드 작성하다 알게된 미흡한 트랜잭션 처리 보완


외부 컴포넌트를 호출하는 함수들을 리팩토링하면서 확인한, 미흡한 트랜잭션 처리 보완

비즈니스 로직이 붙다 보니 아래와 같은 트랜잭션으로 엮여야 하는 부분들이 확인되었다.

// 운영자 등록 예시

// 1. 커뮤니티 기가입 유저 여부 체크
someIsOutSiteUserQuestionToOurHasura();

// 2. role 테이블 권한 추가
someMutationForAddRoleToOurHasura();

// 3. 권한 매핑
someMutationForMapUserRoleToOurHasura();

// 4. 멀티프로필 계정 생성
someMutationForCreateMultiProfileToOurHasura();

// 5. 멀티프로필 계정 매핑
someMutationForMapMultiProfileToOurHasura();
  • 일단 Hasura 는 단일 요청 내에서 쿼리 결과를 기반으로 뮤테이션을 수행하는 기능은 제공하지 않는다.

  • 아예 지원이 없는건 아니고, db레벨에서 stored procedure를 사용하고, 해당 함수를 호출 할 수 있도록 generate해준다는걸 보니 정말 지원을 안하는 것 같다.

  • 관련해서 논의해봤을 때, 기본적으로 stored procedure를 레거시로 여기는 경향이 있어 해당 stored procedure 도입은 반려되었다.

  • 결국 위에서 진행하며 분리한 데이터 소스를 호출하는 함수들에 대해서, 멱등하게 관리 할 수 있는 부분에 대해서는 보상 트랙잭션성 함수들을 추가해서 보완했다.

try {
  // 1. 기가입 유저 체크
  someIsOutSiteUserQuestionToOurHasura();
  // 2. role 테이블 권한 추가
  someMutationForAddRoleToOurHasura();
  try {
    // 3. 권한 매핑
    someMutationForMapUserRoleToOurHasura();
    try {
      // 4. 멀티프로필 계정 생성
      someMutationForCreateMultiProfileToOurHasura();
      try {
        // 5. 멀티프로필 계정 매핑
        someMutationForMapMultiProfileToOurHasura();
      } catch (error) {
        // 기존 매핑을 확인하고 멱등하게 동작
        someCompensatingMutationForDeleteMultiProfile();
        throw error;
      }
    } catch (error) {
	  // 이미 존재하는지 확인하는 로직 추가
      someCompensatingMutationForUnmapUserRole();
      throw error;
    }
  } catch (error) {
    someCompensatingMutationForRemoveRole();
    throw error;
  }
} catch (error) {
  throw error;
}

잔여 프론트엔드 작업 빠르게 마무리하기


기존 커머스 플랫폼에 커뮤니티 기능을 도입하는 태스크였고, 내가 맡은 부분은 레거시 cms의 뷰 작업과 관련한 api작업, 그리고 프론트엔드 커뮤니티 인가 분리하는 태스크였다.

레거시 cms라 사용성에서 일부 잘못된 부분이 있었다. 예를 들어 router에 검색 조건을 push하고 lazyQuery로 라우터의 변경이 감지되면 쿼리를 실행하는 로직이 있었는데, 이러한 부분들을 리팩토링 하면서 진행했다.

상태값으로 관리해야 할 데이터들을 분리해서 상태값으로 관리하고, appoloClient의 refetch, invalidate query key를 이용해서 특정 상태값(주로 검색 조건)이 변경됨에 따라 다시 호출을 해야 하는 부분들을 정리했다.

그리고 기존 인가 관련 로직이 커뮤니티 기능 안에서 파편화 작업되어 있어서, 별도의 인가를 관리하는 훅을 분리해서 처리했다.

결론적으로 불필요한 깜빡임이나, 불필요한 다수의 쿼리파라미터로 지저분해지는 일을 개선했으며, 커뮤니티 기능 안에서의 인가관리를 한곳에서 처리 할 수 있도록 개선했다.