회사 내 프로젝트의 배포 시간이 긴 문제가 있었다. 이 문제를 다들 쉬쉬하고 있었지만, 이젠 이를 개선하기 위해 작업을 진행하려고 한다. 현재 평균 배포 시간은 대략 8분에서 10분 사이가 걸리고 있다.
배포가 느린 이유
배포가 느린 이유는 빌드 속도가 매우 오래 걸렸다. ➜ NestJS를 사용하고 있었는데 ts를 js로 컴파일하는 과정이 매우 오래 걸렸다. 그 문제의 파일은 `filter.ts`로 배포시간이 긴 것만이 문제가 아니라 IDE 툴로 작업을 할 때 이 파일만 들어가거나 수정이 일어나면 IDE가 엄청 느려지고 정신을 못 차렸다... (작업하는데 매우 불편함)
간단하게 `filter.ts`를 보자면,
export class typeOfFilter {
@IsOptional()
@IsString()
id?: string[];
@IsOptional()
@IsString()
country?: string;
// ... 중략
@IsOptional()
@IsNotEmpty()
@IsString()
@IsIn(['1', '2', '3'])
stateOffset?: string;
@IsOptional()
@IsNotEmpty()
@IsString()
@IsIn(['0', '1'])
similarity?: string;
}
export function getFilter(
request: typeOfFilter,
ospList?: WebhardOspList[],
// eslint-disable-next-line @typescript-eslint/ban-types
): Object {
const filter = {
...(request.id
? {
_id: Array.isArray(request.id)
? {
$in: request.id.map(
(id: string) => new mongoose.Types.ObjectId(id),
),
}
: new mongoose.Types.ObjectId(request.id),
}
: {}),
...(request.country ? { country: request.country } : {}),
// ...중략
...(request.state
? request.state !== '5'
? request.stateOffset
? {}
: { state: request.state }
: {
$and: [
{
state: '2',
},
{
checkdate: { $ne: null },
},
],
}
: {}),
// ... 중략
...(request.checkState
? request.checkState === '1'
? { checkDate: { $ne: null } }
: { checkDate: null }
: {}),
deletedAt: { $exists: false },
};
logger.info(filter);
return filter;
}
- 이 파일은 주로 데이터 필터링 및 검색 쿼리를 생성하는 역할을 수행한다.
- `typeOfFilter` 클래스를 정의하여 사용자로부터 다양한 필터링 조건을 받을 수 있는 구조를 만들고,
- `getFilter` 함수를 통해 이러한 조건들을 바탕으로 MongoDB 쿼리 객체를 생성한다.
중략이 많이 되었지만 사실 조건이 20개 이상이 되어서 가독성이 매우 안 좋고 성능에도 영향을 미치고 있다. request를 넣으면 바로 쿼리가 만들어진다는 편안함과 귀찮음에 취해 여기까지 끌고 온 결과물이다..출처: https://brunch.co.kr/@toughrogrammer/44
개선해 볼 만한 방법 생각
- 가장 간단한 GitHub Actions에서 빌드파일을 캐시 해보는 방법
- NestJS Hot reload로 빌드 개선
- `filter.ts`를 미리 js로 변경하기
- `filter.ts` 리팩토링 하기
사실 가장 근본적인 해결방법은 마지막 방법으로 `getFilter`라는 함수를 리팩토링 하는 방법이다. 얼마나 걸릴지도 모르고 테스트코드도 없기 때문에 조건을 실수로 빠뜨린다면 데이터가 다르게 보일 수도 있지만 가독성이나 유지보수성을 생각해서 꼼꼼히 검증한다 생각하고 작업을 시작했다.
리팩토링 된 결과
function idFilter(ids?: string[]) {
if (!ids) return {};
return {
_id: Array.isArray(ids)
? { $in: ids.map((id) => new mongoose.Types.ObjectId(id)) }
: new mongoose.Types.ObjectId(ids),
};
}
function countryFilter(country?: string) {
return country ? { country } : {};
}
// ...중략
function similarityFilter(similarity?: string) {
return similarity && similarity === '1'
? { similarity: { $exists: true } }
: {};
}
function stateFilter(request: typeOfFilter) {
if (!request.state) return {};
if (request.state === '5') {
return {
$and: [{ state: '2' }, { checkdate: { $ne: null } }],
};
} else if (request.stateOffset) {
return {};
} else {
return { state: request.state };
}
}
// ...중략
function dateRangeFilter(startDate?: string, endDate?: string) {
return startDate && endDate
? {
regdate: {
$gte: getDate(startDate),
$lt: nextDate(getDate(endDate)),
},
}
: {};
}
function partnershipCompanyFilter(
partnershipCompany?: string,
ospList?: WebhardOspList[],
) {
if (!partnershipCompany) return {};
return partnershipCompany === '제휴사'
? { osp: { $in: ospList } }
: { osp: { $nin: ospList } };
}
function checkStateFilter(checkState?: string) {
return checkState
? checkState === '1'
? { checkDate: { $ne: null } }
: { checkDate: null }
: {};
}
export function getFilter(request: typeOfFilter, ospList?: WebhardOspList[]) {
const idFilterResult = idFilter(request.id);
const countryFilterResult = countryFilter(request.country);
// ...중략
const dateRangeFilterResult = dateRangeFilter(
request.startDate,
request.endDate,
);
const partnershipCompanyFilterResult = partnershipCompanyFilter(
request.partnershipCompany,
ospList,
);
const checkStateFilterResult = checkStateFilter(request.checkState);
const filter: any = {};
Object.assign(filter, idFilterResult);
Object.assign(filter, countryFilterResult);
// ...중략
Object.assign(filter, partnershipCompanyFilterResult);
Object.assign(filter, checkStateFilterResult);
filter.deletedAt = { $exists: false };
logger.info(filter);
return filter;
}
정확한 컴파일러의 작동원리는 알지 못하지만 리팩토링으로 인한 개선 사항을 정리해 보며, 컴파일 시간 단축의 가능한 이유를 추론해 보겠다.
- 코드의 모듈화: 리팩토링 과정에서 각 필터 조건을 별도의 함수로 분리하면, 코드가 더 모듈화 되고 구조화된다. 이는 컴파일러가 각 모듈(함수)을 개별적으로 최적화할 수 있게 만들어, 전체적인 컴파일 과정을 더 효율적으로 만들 수 있다.
- 재사용성과 중복 최소화: 분리된 함수들이 재사용될 가능성이 높아지면, 컴파일러는 이러한 패턴을 인식하고 최적화할 수 있다. 또한, 코드 내에서 중복되는 로직이 줄어들면 컴파일러는 더 적은 양의 코드를 분석하고 처리해야 하므로, 컴파일 시간이 단축될 수 있다.
- 의존성 분석: 함수가 분리됨으로써, 각 함수 간의 의존성이 줄어들 수 있습니다. 컴파일러는 코드의 의존성 그래프를 분석할 때 더 적은 연결점을 고려하게 되어, 컴파일 과정이 빨라질 수 있다.
- 컴파일 캐싱: 모던 컴파일러들은 종종 컴파일 결과를 캐싱하여, 변경되지 않은 모듈은 다시 컴파일하지 않고 이전 결과를 재사용한다. 코드가 더 세분화되고 각 부분이 독립적일수록, 변경이 없는 부분에 대한 컴파일 캐싱의 효율이 올라가 컴파일 시간이 단축될 수 있다.
결과 🔥
빌드 배포 시간이 약 10분에서 1분 8초로 단축됨에 따라, 개선 후의 컴파일 속도는 개선 전에 비해 약 8.8배 더 빨라졌다.
이는 빌드속도지만 IDE에서 컴파일이 너무 오래 걸려 대기하는 개발자들의 시간도 절약할 수 있게 되었다.
기간 만료 전 마지막(?) 선물
![](https://blog.kakaocdn.net/dn/xVEEP/btsFDzAYQu4/DNXfj5nl7PJ3t6nyf46yc0/img.jpg)
언제나 잘못된 설명이나 부족한 부분에 대한 피드백은 환영입니다🤍