티스토리 뷰

728x90

사내 서비스를 이용하던 중 전체 강의 화면에서 화면 로딩이 2~3초가 걸려서 원인이 궁금했다.

화면 로딩이 2~3 초면 사용자가 이용하기에 화면 로딩이 너무 느리다고 체감이 되고, 서비스 사용하기에 불편함을 느낄것 같았다.

그래서 이것을 개선해보기로 했다!

 

 

전체 강의 보기 화면에서 호출되는 api 를 파악하고 제일 응답 속도가 느린 api 를 확인해보았다.

제일 응답 속도가 느린 api 는 filters 였고 이 api 응답값을 통해서 프론트에서 화면을 뿌려주고 있었다.

 

filters API 구조를 확인을 해보자!

server to server 통신을 하고 있고, 내부에서 많은 연산이 진행이 되고 있었다.

 

 

여기서 연산이 가장 오래 걸리는 것을 파악하기 위해서 console.time(), console.timeEnd() 두개를 이용했다.

속도를 확인해보고 싶은 부분 시작점에 console.time 을 추가했고 끝점에 console.timeEnd 를 넣어 연산 속도를 확인할 수있었다.

 

this.server.addHook('onRequest', (request, reply, done) => {
  (request.raw as any).startTime = Date.now();
  done();
});
this.server.addHook('onResponse', (request, reply, done) => {
  const startTime = (request.raw as any).startTime;
  if (startTime) {
    console.log(`Request took ${Date.now() - startTime} ms`);
  }
  done();
});

 

또한 전체 api 응답 시간을 확인 하기 위해서 preHandler 쪽에 코드를 추가하여 확인 할 수 있었다.

 

속도가 오래 걸리는 부분은 두가지가 있었다.

첫번째는 server to server 통신을 하고 있던 부분, course ids 를 통한 course 조회하는 부분이였다.

각각 대략 1s 정도가 걸렸다.

 

server to server 통신하는 부분 속도 개선하기!

 

server to server 통신하는 부분의 기능을 확인해보니 모든 사용자마다 다른 응답값을 주는 것이 아니라 동일한 값을 응답하고 있었다.

요청하는 query 에 따라서 다르게 응답하겠지만 client 에서 요청하는 값을 확인해보니 sortType 만 영향이 있었다.

sortType 도 newest/popularity 두가지 뿐이였다.

 

매번 사용자마다 해당 api 를 호출하는 것은 비효율적인 것으로 판단이 되어 cache 사용하는 방안으로 개선을 해보았다.

 

let filteredCourses;
if (cache) {
	filteredCourses = cache;
} else {
    filteredCourses = (
      (await this.classFinderClient.getClassFinderApi(`url`, {
        site: Site.LXP,
        price,
        totalPlayTime,
        state,
        sortType,
      })) as {
        data: FtsResponseDto;
      }
    ).data as unknown as { courseId: number; popularity?: number }[];
    this.curationCacheService
      .setCache({ ...query, site: Site.LXP }, filteredCourses)
      .catch((err) => console.error('curation cache setting error:', err));
}

 

cache 를 조회하여 있다면 redis 에서 값을 가져와서 사용하였고, 없다면 api 호출을 하는 방안으로 진행하였다.

redis 에 set 할때는 비동기로 처리하였다. redis 에 set 을 하고 그 외의 다른 작업이 없었고, 그 후에 다른 연산 과정이 많기 때문에 동기로 처리할 이유가 없었다.

 

redis key 로 api 호출할때 사용하는 query 를 사용하였다. 윗 부분에 해당 api 호출할때 sortType 만 다르다고 하여 sortType 만 사용해도 될 것 같다고 생각이 들텐데 추후 api 호출할때 다른 값도 사용할 수도 있으니 확장성을 고려하여 query 전체를 사용했다.

 

export class CurationCacheService {
  private cacheService: FastCache;
  readonly ttlInSec: number; //1day

  constructor({ cacheService = defaultCache, ttl = CURATION_CACHE_TTL } = {}) {
    this.cacheService = cacheService;
    this.ttlInSec = ttl;
  }
  static get CURATION_CACHE_KEY() {
    return 'cache';
  }

  async getCache(query: CourseFilterQuery & { site: string }): Promise<filteredCourses[] | null> {
    const session = await this.cacheService.get(`${CurationCacheService.CURATION_CACHE_KEY}:${JSON.stringify(query)}`);
    return session ? (ObjectUtil.deserialize(session) as filteredCourses[]) : null;
  }

  async setCache(query: CourseFilterQuery & { site: string }, value: filteredCourses[]) {
    const key = `${CurationCacheService.CURATION_CACHE_KEY}:${JSON.stringify(query)}`;
    await this.cacheService.set(key, ObjectUtil.serialize(value) as string, this.ttlInSec);
    this.logger.debug(`grant curation cache ok, cacheKey=${key}`);
  }
}

 

윗 부분의 코드는 redis set / get 하는 class 이다.

ttl 은 1 day 로 설정했다. 1 day 를 설정한 이유는 해당 api 호출 값이 빈번하게 바뀔일이 없어서 1 day 로 설정했다.

 

해당 부분으로 변경 후 속도가 줄어든 것을 확인 할수 있었다.

 

많은 ids 로 db 조회 시 응답 속도 개선하기!

 

db 조회하는 부분은 위에 표시한 부분이다.

 

//courseService
async findCoursesByIds(ids: number[]) {
  return this.courseDao.selectByIds(ids);
}

//courseDao
async selectByIds(ids: number[]) {
  return this.repository.findBy({ id: In(ids) });
}

 

이 부분의 코드를 타고 들어가보면 이렇게 typeorm 을 사용하고 있었다.

 

id 에 index 가 걸려있어서 index 를 타서 검색을 할텐데 왜 오래 걸리는지 의문이 들었다.

explain 를 사용해서 실제 쿼리가 index 를 타는지 확인을 해보았는데 full scan 을 하고 있었다.

알고 보니 검색하려는 id 갯수가 많아서 index 를 타지 않는다는 것이였다.

 

this.courseService.searchSelectionIds({
  ids: courseIds,
  select: COURSE_SELECTION_IDS,
}),

//courseService
async searchSelectionIds(query: RequestSelectionQuery) {
  return this.courseDao.searchSelectionIds(query);
}

//courseDao
async searchSelectionIds(query: RequestSelectionQuery) {
  return this.repository
    .createQueryBuilder(this.tableName)
    .select(this.buildSelectQuery(this.tableName, query.select))
    .where((qb) => {
      qb.andWhere(`${this.tableName}.id IN (:ids)`, { ids: query.ids });
    })
    .getRawMany();
}

 

db 조회할때 필요한 값만 select 하는 방안으로 개선을 했더니 속도가 개선 되는 것을 확인했다.

 

 

before / after 을 확인해보았을때 확실히 속도가 개선된 것을 확인 할 수 있었다!!

 

 

 

 

728x90