저는 처음 ORM을 Spring+JPA로 시작하면서 자연스럽게 DTO로 바로 반환하는 작업을 당연하게 여겼습니다. 하지만 최근 NestJS와 TypeORM을 사용하면서, JPA에서 가능했던 방식이 왜 TypeORM에서는 동일하게 적용되지 않는지 궁금해졌고, 이에 대해 스스로 공부한 내용을 정리하고 공유하고자 이 글을 작성하게 되었습니다.
자바에서는 JPA (Java Persistence API)와 같은 ORM(Object-Relational Mapping) 프레임워크에서 JPQL(Java Persistence Query Language)이나 Native Query를 사용할 때, 쿼리 결과를 DTO로 자동으로 매핑하는 기능이 제공됩니다. 이때 쿼리에서 별칭(AS)을 DTO의 필드 이름과 맞춰주면 자동으로 매핑이 이루어집니다.
그러나 TypeScript와 TypeORM에서는 이런 자동 매핑 기능이 기본적으로 제공되지 않습니다. 그 이유는 자바의 JPA와 TypeORM의 설계 방식과 동작 방식이 다르기 때문입니다.
Java의 강력한 리플렉션(reflection) 기능:
TypeORM의 한계:
TypeORM의 설계 목표 차이:
TypeORM에서 자바의 JPA처럼 DTO에 자동으로 매핑하는 기능을 원한다면, 쿼리 결과를 수동으로 DTO에 매핑해야 하며, 이를 위해서는 다음과 같은 방법을 사용할 수 있습니다:
class-transformer와 같은 라이브러리를 사용하여 쿼리 결과를 자동으로 특정 클래스(DTO)로 변환할 수 있습니다.typescript
import { plainToInstance } from 'class-transformer';
const users = await this.userRepository
.createQueryBuilder("user")
.select(["user.id", "user.firstName", "user.lastName"])
.getRawMany();
const userDtos = plainToInstance(UserDto, users);
class-transformer는 쿼리에서 반환된 일반 자바스크립트 객체를 DTO 클래스로 변환해주는 역할을 합니다.
class-transformer와 같은 라이브러리를 사용할 수 있습니다.TypeScript에서 "런타임 시에 타입 정보가 제거된다"는 것은 타입스크립트가 컴파일 시점에서만 타입을 확인하고, 실제 실행되는 자바스크립트 코드에서는 타입 정보가 유지되지 않는다는 의미입니다. 즉, 타입스크립트는 타입 안정성을 제공하기 위해 개발 시에만 타입을 검사하고, 컴파일된 JavaScript는 타입 정보를 전혀 포함하지 않습니다. 이를 이해하기 위해서는 JavaScript와 TypeScript의 작동 방식을 비교해볼 필요가 있습니다.
자바스크립트는 동적 타이핑(dynamic typing) 언어로, 변수에 어떤 타입의 값이 들어갈지 미리 알 수 없습니다. 타입이 명시적으로 선언되지 않으며, 런타임 시에 타입이 결정됩니다. 예를 들어, 변수에 숫자를 넣었다가 나중에 문자열을 넣는 것도 가능하죠.
javascript
let x = 10; // x는 현재 숫자
x = "Hello"; // 이제 x는 문자열
타입스크립트는 정적 타입(static typing) 시스템을 도입하여 코드의 안정성을 높입니다. 개발자는 변수의 타입을 명시적으로 선언하고, 타입스크립트 컴파일러가 코드의 타입 일관성을 검사합니다. 하지만 이 타입 정보는 컴파일 타임에만 유효하며, 런타임에는 사라집니다. 즉, 컴파일된 자바스크립트는 타입 정보를 포함하지 않기 때문에 런타임에서는 타입 검사나 매핑이 불가능합니다.
typescript
let y: number = 10; // y는 숫자
y = "Hello"; // 컴파일 오류: 'y'는 숫자여야 함
하지만 이 타입 정보는 자바스크립트로 컴파일되면 없어지기 때문에 실제로 실행되는 코드는 다음과 같이 단순화됩니다:
javascript
let y = 10;
y = "Hello"; // 자바스크립트에서는 문제 없음
TypeORM은 자바스크립트와 타입스크립트에서 모두 사용할 수 있는 ORM입니다. TypeORM이 런타임에 DTO나 엔티티 클래스와 쿼리 결과를 매핑하기 위해서는 런타임에도 타입 정보를 알아야 하는데, 앞서 설명한 것처럼 타입스크립트는 컴파일된 자바스크립트에서 타입 정보를 유지하지 않습니다. 따라서 TypeORM은 런타임에 타입스크립트의 DTO나 엔티티 타입 정보를 사용하여 자동으로 매핑할 수 없습니다.
Java에서는 리플렉션(reflection)이라는 기능이 있어 런타임 시에도 클래스의 메타데이터(타입 정보, 필드 정보 등)를 유지하고 사용할 수 있습니다. 이 때문에 JPA 같은 ORM은 런타임에 엔티티 클래스와 쿼리 결과를 매핑할 수 있는 메타데이터에 접근할 수 있습니다.
반면, 자바스크립트는 리플렉션 기능이 제한적이며, 특히 런타임에는 타입 정보가 사라지기 때문에 DTO 또는 엔티티 클래스와 쿼리 결과를 자동으로 매핑하는 기능이 부족합니다. 이로 인해 TypeORM에서는 쿼리 결과를 자동으로 DTO에 매핑하는 기능을 제공하지 않으며, 쿼리 결과를 수동으로 매핑해야 합니다.
따라서 자바스크립트의 이 특성 때문에 TypeORM에서 DTO로 자동 매핑이 불가능한 것입니다.