본문 바로가기

# GraphQL/TypeGraphQL

[TypeGraphQL] 상속, Inheritance

상속

TypeGraphQL에서는 extends키워드를 통해 기존 타입을 상속받아 사용할 수 있습니다. 여기서는 아래의 3가지 관점에서 상속을 활용하는 방법을 설명하겠습니다.


주요 관점:

  • 베이스 타입이 일반 클래스인 경우
  • 베이스 타입이 추상 클래스인 경우
  • 오버라이딩된 필드는 어떻게 되나?

일반 클래스 상속

위에서 이미 설명하였듯이, 평범하게 extends키워드를 사용하면 기존 타입을 확장할 수 있습니다. 예를 들어, 아래와 같이 2차원 좌표를 표현하는 ObjectType을 정의하면.

@ObjectType()
class Point2D {
    @Field(() => Int)
    x!: number;

    @Field(() => Int)
    y!: number;
}

상속을 사용하여 3차원 좌표로 쉽게 확장할 수 있습니다.

@ObjectType()
class Point3D extends Point2D {
    @Field(() => Int)
    z!: number;
}

위와 같은 방식으로 아래의 타입들을 확장시킬 수 있습니다.

  • ObjectType
  • InputType
  • ArgsType
  • Resolver

단, 같은 타입에 대해서만 상속할 수 있다는 것을 기억해주세요. 다른 타입을 상속해도 문법적인 에러는 없지만, 스키마를 빌드할 때 에러가 발생하게 됩니다.


추상 클래스 상속

추상 클래스는 일반적으로 ObjectType에만 사용되기 때문에, 다른 타입에 대해서는 생각하지 않겠습니다. 즉, ObjectType에 대해서만 생각하겠습니다.


추상 메서드를 통해 정수형 배열을 매핑하는 오브젝트 타입을 정의하겠습니다. 단, TypeScript에서는 아직 AbstractMethod에 데코레이터를 사용할 수 없기 때문에, 추상 메서드가 아닌 함수를 별도로 만들어 사용해야 합니다.

@ObjectType()
abstract class DataMapper {
    protected items: number[] = [];

    //
    // 자식 클래스마다 각기다른 로직을 정의.
    // 추상 메소드에는 데코레이터를 사용할 수 없음.
    abstract map(): number[];

    //
    // 추상 메서드는 데코레이터를 사용할 수 없으므로,
    // 추상 메서드가 아닌 함수를 프록시로 세워야 함.
    @Field(() => [Float])
    getMapped(): number[] {
        return this.map();
    }
}

이제 DataMapper를 상속받아 각 배열의 값을 2배로 늘리는 TwiceMapper와, 각 배열의 값을 절반으로 낮추는 HalfMapper를 구현합니다. getMapped() 필드는 부모 클래스에서 상속받기 때문에 따로 적어주지 않아도 괜찮습니다.

@ObjectType()
class TwiceMapper extends DataMapper {
    constructor() {
        super();
        this.items = [1, 2, 3, 4, 5];
    }

    map() {
        return this.items.map((v) => v * 2);
    }
}
@ObjectType()
class HalfMapper extends DataMapper {
    constructor() {
        super();
        this.items = [1, 2, 3, 4, 5];
    }

    map() {
        return this.items.map((v) => v * 0.5);
    }
}

오버라이딩

만약 부모에게 상속받은 Field를 오버라이딩하면 어떻게 될까요? 먼저, 이항 연산자와 같은 역할을 하는 ObjectType을 정의하겠습니다.

@ObjectType()
class BinaryOperatorObject {
    @Field(() => String)
    operatorName(): string {
        throw new Error("'operatorName()' not implemented.");
    }

    @Field(() => Float)
    exec(
        @Arg("a", () => Int) a: number,
        @Arg("b", () => Int) b: number
    ): number {
        throw new Error("'exec()' not implemented.");
    }
}

위의 결과로 아래의 스키마가 생성됩니다. exec가 2개의 인자를 요구하는 것에 주목해주세요.

type BinaryOperatorObject {
  operatorName: String!
  exec(a: Int!, b: Int!): Float!
}

단순 오버라이딩

이제 BinaryOperatorObject를 상속받아 2개의 정수의 합을 구하는 AdderObject를 만들어보겠습니다.

@ObjectType()
class AdderObject extends BinaryOperatorObject {
    operatorName(): string {
        return "add";
    }

    exec(a: number, b: number): number {
        return a + b;
    }
}

각각의 필드에 데코레이터가 사용되지 않은것에 주목해주세요. 오버라이딩된 필드는 부모객체에서 데코레이터 정보를 가져오기 때문에, 각각의 데코레이터 정보가 손상되지 않습니다. 즉, 아래와 같이 부모와 동일한 스키마가 생성됩니다.

type AdderObject {
  operatorName: String!
  exec(a: Int!, b: Int!): Float!
}

데코레이터 오버라이딩

이제 BinaryOperatorObject를 상속받아 2개의 정수의 차를 구하는 SubtractorObject를 만들어보겠습니다. 이 때, exec에 데코레이터가 새로 정의된 것에 주목해주세요.

@ObjectType()
class SubtractorObject extends BinaryOperatorObject {
    operatorName(): string {
        return "subtract";
    }

    //
    // 잘못된 사용법.
    // 기존의 데코레이터 정보를 가져올 수 없음.
    @Field(() => Float)
    exec(a: number, b: number): number {
        return a - b;
    }
}

오버라이딩된 필드에 데코레이터를 적용하면, 기존의 부모 객체에서 데코레이터 정보를 가져올 수 없게 됩니다. 즉, 반환형을 제외한 모든 정보가 손실되므로 Arg에 대한 정보가 사라지게 됩니다. 결과 스키마를 보면 바로 파악할 수 있습니다.

type SubtractorObject {
  operatorName: String!
  exec: Float!
}

만약 부모가 사용한 정의를 그대로 따라야 한다면, 자식에서는 데코레이터를 명시적으로 다시 적지 말아야 합니다.


예제 다운로드

이 포스팅에 사용된 전체 코드는 여기에서 확인할 수 있습니다.