끄적끄적

class-transformer 에 의한 Nest.js 성능 문제 본문

개발/js & ts & node.js

class-transformer 에 의한 Nest.js 성능 문제

코리이 2023. 6. 18. 21:49

한국에서 nestjs 로 개발하는 개발자라면 typia 에 대해서 한번쯤은 들어봤을 것 같다. 이 라이브러리에서 문제로 지적하는 부분이 nestjs 공식문서 예시로 제시하는 class-validator 가 성능적으로 문제가 크다는 것이다. 때문에 이러한 느린 라이브러리의 문제로 "typescript(javascript) 가 느리다" 라는 잘못된 인식까지 퍼질 수 있다고도 한다. 특히나 대부분의 nestjs 개발자라면 class-transformer + class-validator 기반으로 request 검증을 할테고, 객체 transform 액션에도 class-transformer 를 적극적으로 활용하고 있을 것이라 느리다는 인식이 더 커질 수 있을 것 같다.

 

여기서 성능이 느려지는 가장 큰 이유중 하나는 class-transformer 가 객체를 변환하는데 매우 느리기 때문이다. 필자의 회사에서도 class-transformer 를 주로 사용고 있었는데 대량 발행 등의 액션을 할 때 조금씩 이슈들이 생겨 최근에 이 라이브러리를 최대한 걷어내는 작업을 진행하고 있다. 평상시에 트래픽이 많이 몰리지 않을때는 크게 문제가 없었지만 확실히 많은 객체를 처리하다 보니 이 라이브러리만 걷어내도 꽤 큰 성능적 이점을 가져갈 수 있었다. 그래서 이번에 간단한 성능테스트를 하면서 단순 transform 작업시 얼마나 차이가 나는지에 대해 테스트를 해보고자 한다.

 

테스트 준비

테스트할 액션은 "변환" 과정이다. 3-layered 아키텍처 혹은 hexagonal 아키텍처 사용시 DB Model ↔ Domain Model 변환은 자주 일어나는 액션 중 하나이므로 이를 테스트해보고자 한다. (API call 또한 비슷한 액션이라 가정할 수도 있다.) 아래와 같이 세 가지 변환 로직을 테스트해보고자 한다.

 

  1. 객체 → 객체
  2. 객체  클래스 (직접 생성)
  3. 객체  클래스 (class-transformer 사용)

변환에 사용한 객체 형태는 아래와 같다. 이 객체는 단순 테스트를 위해 씨아이보드 메뉴얼에 존재하는 Member 객체를 참고했다.

export interface IMember {
    id: string;
    userid: string;
    email: string;
    password: string;
    username: string;
    nickname: string;
    level: number;
    homepage: string;
    phone: string;
    birthday: string;
    sex: string;
    zipcode: string;
    address1: string;
    address2: string;
    address3: string;
    address4: string;
    receiveEmail: boolean;
    receiveSms: boolean;
    useNote: boolean;
    openProfile: boolean;
    denied: boolean;
    emailCert: boolean;
    registeredAt: Date;
    registeredIp: string;
    lastLoginAt: Date;
    lastLoginIp: string;
    isAdmin: boolean;
    profileContent: string;
    adminMemo: string;
    following: number;
    followed: number;
    icon: string;
    photo: string;
}

export class Member implements IMember {
    // IMember 상속 및 contstructor 로직만 존재
}

그 후 Repository 클래스에서는 IMember 객체가 DB 에서 가져오는 로직이라 가정하고 각각의 지정된 형태로 변환하는 과정을 거쳤다. 위에서 테스트하고자 하는 세 가지 형태를 지녔으며 단순 테스트를 위한 용도이므로 각각의 메서드로 만들었다.

export class MemberRepository {
    private getSampleDbMemberModel(id: string): IMember {
        return {
            // 객체에 맞는 랜덤값 지정
            // ...
        }
    }

    async getMemberByIdWithObject(id: string) {
        const member: IMember = this.getSampleDbMemberModel(id);
        return {
            id: member.id,
            userid: member.userid,
            email: member.email,
            // ...
        }
    }

    async getMemberByIdWithClass(id: string) {
        const member: IMember = this.getSampleDbMemberModel(id);
        return new Member({
            id: member.id,
            userid: member.userid,
            email: member.email,
            // ...
        });
    }

    async getMemberByIdWithClassTransformer(id: string) {
        const member: IMember = this.getSampleDbMemberModel(id);
        return plainToInstance(Member, member);
    }
}

그 후 테스트를 통해 시간을 측정해 보았다.

테스트 결과

테스트 결과는 아래와 같다. 각각 테스트마다 100번정도 테스트를 한 후 평균을 낸 값이며 ms 단위로 측정했다.

변환 갯수 interface (Object) new Class class-transformer
10 개 0.11 (ms) 0.12 (ms) 0.74 (ms)
100 개 0.56 (ms) 0.7 (ms) 3.58 (ms)
1000 개 5.38 (ms) 5.25 (ms) 28.26 (ms)
10000 개 44.51 (ms) 50.04 (ms) 249.49 (ms)
100000 개 436.65 (ms) 458.36 (ms) 2451.48 (ms)

단순한 테스트만 해봤음에도 결과는 처참하다. transformer 사용시  6배 정도의 성능 저하가 발생하는 것을 확인할 수 있다. 만약 class-validator 를 사용한다면 결과는 더 처참해질 것이 분명해 보인다. 특히나 이 테스트는 단순 변환 테스트라서 5~6 배 정도밖에 차이가 나지 않았지만 typia 라이브러리에서 진행한 결과를 보면 성능차이가 더 벌어진 것을 확인할 수 있다.

결론

매퍼 라이브러리가 성능이 좋지 않다는 것은 당연히 알고 있었지만 처음 개발할 때는 대부분의 성능 저하의 경우 DB 에서 발생한다고 생각했기 때문에 class-transformer 를 적극적으로 사용했었다. 하지만 최근에 성능 최적화를 해야 하는 상황이 발생했고 class-transformer 에 의해 성능이 떨어질 수 있다는 소리를 들어 실제로 테스트 해보니 class-transformer 만 제거해도 평균적으로 2~3 배 정도 최적화 할 수 있었다.

 

class-transformr 의 경우 여러 다른 기능들도 포함하고 있어 무조건적으로 나쁘다 라는 것은 아니지만 경험상으로 대부분의 경우 매퍼 라이브러리를 사용하지 않아도 충분히 해결할 수 있는 부분이 많았다. 오히려 라이브러리 사용시 디펜던시가 꼬이게 되면서 예상과 다른 transform 을 하는 현상도 발생해서 해결하는데 어려움을 겪기도 했다. 현재 레거시 코드가 많아 request 검증에는 어쩔 수 없이 class-validator + class-transformer 를 사용하고 있지만 다른 대부분의 코드에서는 제거한 상황이며 request 검증의 경우에도 제거하기 위한 사전작업을 진행중이다. 물론 PoC 상황에서는 빠르게 개발해야 하니 사용하는 걸 권장할 수 있지만 어차피 변경해야 하는 라이브러리임이 분명해 보여 프로덕션 레벨에서는 최대한 안쓰는 것이 더 좋은 방향이 아닐까 싶다.