티스토리 뷰

BACKEND

mongoose, nestjs에서 discriminator 사용해보기

나를찾는아이 2023. 4. 20. 18:07
728x90
반응형

discriminator

약간 단어가 어렵습니다

 

해석하자면 판별장치 이런 뜻인데요

 

저장된 데이터가 어떤 종류의 데이터인지 판별하는 그런 의미를 담고 있다고 볼수 있겠습니다

 

https://mongoosejs.com/docs/discriminators.html

 

Mongoose v7.0.4: Discriminators

Discriminators are a schema inheritance mechanism. They enable you to have multiple models with overlapping schemas on top of the same underlying MongoDB collection. Suppose you wanted to track different types of events in a single collection. Every event

mongoosejs.com

 

nodejs 진영에서 가장 유명한 mongodb 용 패키지인 mongoose에 정의되어있는 기능이구요

 

https://docs.nestjs.com/techniques/mongodb#discriminators

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

 

당연히 mongoose 패키지를 내부적으로 사용하는 @nestjs/mongoose 도 사용이 가능합니다

 

 

 

어떤 기능을 구현하기 위해 설계되었는지 한번 알아보겠습니다

 

 

A회사에서는 유저들의 액션을 분석하기 위한 기능을 개발하려고 합니다

매출을 높이기 위해서 유저들이 어떠한 액션들을 수행하는지 알아보려고 하는것이죠

회원가입, 링크 클릭, 구매, 로그인, 좋아요 등등 다양한 액션 데이터를 수집하려고 합니다

그리고 이 모든 유저의 액션 이벤트들을 하나의 컬렉션에 모두 저장하고, 조회하려고 합니다

 

 

모든 액션들을 저장하려고 보니 각각의 액션마다 저장되어야할 데이터가 다르다는것을 알 수 있습니다

 

회원가입이벤트의 경우는

 

어떤 유저가 가입했는지 userId에 대한 정보가 필요하겠구요

 

반면에 링크클릭 이벤트의 경우는

 

어떤 url이 클릭되었는지에 대한 정보가 필요합니다

 

다형성이라고 말할수 있겠네요

 

 

mongodb는 schema less 한 데이터베이스로서 RDB에서처럼 생성되어있는 컬럼에 제한을 받지 않습니다

 

모든 document 형태를 저장할수 있습니다

 

이렇게 말이죠

{ name : "Joe", age : 30, interests : "football" }
{ name : "Kate", age : 25 }
{ name : "Joe", age : 28, country: "america" }

 

RDB 같았으면 name, age, interests, country 모두 컬럼을 생성해야겠지만

 

mongodb는 document 기반의 저장소이기 때문에 이러한 구성이 가능합니다

 

 

자 그러면 우리가 저장하려고 했던 이벤트들도

 

이런식으로 mongodb에 저장될수 있겠죠?

 

{
  "userId": "user1",
  "kind": "SignUpEvent",
  "createdAt": {
    "$date": "2023-04-20T01:46:11.952Z"
  }
},
{
  "url": "https://daum.net",
  "kind": "ClickedLinkEvent",
  "createdAt": {
    "$date": "2023-04-20T01:47:53.836Z"
  }
}

 

kind 라는 필드는 어떤 종류의 이벤트인지를 넣고 각 이벤트마다 필요한 데이터를 추가필드에 저장하는겁니다

 

SignupEvent에는 userId 필드를 추가로 저장하고

 

ClickedLinkEvent에는 url 필드를 추가로 저장하고 이런식으로 말이죠

 

좀 더 타입의 이점을 누려보자 이말입니다

 

 

 

코드로는 어떻게 작성할수 있을까요?

 

mongoose는 typescript를 지원하긴 하지만 완전히 typescript로 구현된 패키지가 아닙니다

 

그리고 nestjs는 typescript로 구현된 프레임워크죠

 

그래서 nestjs와 mongoose를 이용해서 어떻게 discriminator를 사용해야하는지는 각각의 공식문서의 내용만으로는 충분하지 않습니다

 

제가 이 포스팅을 작성하게된 이유기도 합니다

 

nestjs위에서 이 기능을 구현해보기로 해보죠

 

 

먼저 우리가 사용할 스키마들을 정의해볼께요

 

기본형이될 Event 모델과 그리고 ClickedLinkEvent, SignupEvent 를 만들어보겠습니다

 

export enum EventKind {
  ClickedLinkEvent = "ClickedLinkEvent",
  SignUpEvent = "SignUpEvent",
}
// event.schema.ts
export type EventDocument = HydratedDocument<Event>;

@Schema({ timestamps:true, discriminatorKey: 'kind' })
export class Event {
  kind: EventKind;
}

export const EventSchema = SchemaFactory.createForClass(Event);

@Schema 데코레이터에 discriminatorKey를 선언했습니다

 

해당 키 이름을 가진 필드에 어떤 유형의 데이터가 담겼는지 저장이 될거예요

 

DiscriminatorKey를 설정하지 않는 경우 자동적으로 __t 라는 필드에 저장이 됩니다

 

그리고 kind 필드와 타입을 선언하는데요

 

단, 여기서 주의해야할점은 kind에는 @Prop 데코레이터가 없다는 점입니다

 

이유는 아래에서 보여드릴 확장된 이벤트들이 Event를 상속받아 활용하기 때문에 kind에는 @Prop 데코레이터가 없어도 정상동작합니다

 

하지만 discriminatorKey가 아닌 다른 추가적인 필드는 @Prop 데코레이터를 선언해주어야 합니다

 

 

그리고 각각 ClickedLinkEvent와 SignupEvent는 기본형인 Event를 상속받습니다

// clicked-link-event.schema.ts
export type ClickedLinkEventDocument = HydratedDocument<ClickedLinkEvent>;

@Schema()
export class ClickedLinkEvent extends Event {
  @Prop({ type: String, required: true })
  url: string;
}

export const ClickedLinkEventSchema = SchemaFactory.createForClass(ClickedLinkEvent);

 

// signup-event.schema.ts
export type SignUpEventDocument = HydratedDocument<SignUpEvent>;

@Schema()
export class SignUpEvent extends Event {
  @Prop({ type: String, required: true })
  userId: string;
}

export const SignUpEventSchema = SchemaFactory.createForClass(SignUpEvent);

 

이제 MongooseModule을 import 합니다

 

@Module({
  imports: [
    MongooseModule.forFeature([
      {
        name: Event.name,
        schema: EventSchema,
        discriminators: [
          { name: EventKind.ClickedLinkEvent, schema: ClickedLinkEventSchema },
          { name: EventKind.SignUpEvent, schema: SignUpEventSchema },
        ],
      },
    ]),
  ],
  controllers: [EventController],
  providers: [EventService]
})
export class EventModule {}

 

nestjs 공식 문서에는 제가 소개드린 코드와 다소 다른 방식으로 설명이 되어있는데요

 

@Schema({ discriminatorKey: 'kind' })
export class Event {
  @Prop({
    type: String,
    required: true,
    enum: [ClickedLinkEvent.name, SignUpEvent.name],
  })
  kind: string;

  @Prop({ type: Date, required: true })
  time: Date;
}

export const EventSchema = SchemaFactory.createForClass(Event);

nestjs에서 설명대로 해도 동작은 하긴 하지만 몇가지 애로사항이 발생합니다

 

실제로 nestjs 공식 가이드대로 예제를 완성하면

 

하위 자식 Event에서 부모 Event를 상속하는 방식을 사용하는것이 아니라서

 

하위 Event 모델에서 부모 Event 모델이 가지고 있는 필드를 똑같이 선언해줘야 하는 번거로움이 있습니다

 

이 번거로움을 해결하기 위해서 상속을 사용하는 경우

 

원형이 되는 Event와 하위 자식들간에 순환 참조가 발생하는 코드가 작성됩니다

 

 

그래서 제가 이 포스팅을 작성하면서 좀 더 개선을 해보았습니다

 

자 이제 모든 준비를 마쳤습니다 사용만 해보면 됩니다

 

@Injectable()
export class EventService {
  constructor(
    @InjectModel(Event.name) private eventModel: Model<EventDocument>,
    @InjectModel(ClickedLinkEvent.name) private clickedLinkEventModel: Model<ClickedLinkEventDocument>,
    @InjectModel(SignUpEvent.name) private signUpEventModel: Model<SignUpEventDocument>,
  ) {}
  
  createSignUpEvent(event: SignUpEvent): Promise<SignUpEvent> {
    return this.signUpEventModel.create(event);
  }
  
  createClickedLinkEvent(event: ClickedLinkEvent): Promise<ClickedLinkEvent> {
    return this.clickedLinkEventModel.create(event);
  }
  
  getEvents(): Promise<Event[]> {
    return this.eventModel.find().exec();
  }
  
  getSignUpEvents(): Promise<SignUpEvent[]> {
    return this.signUpEventModel.find().exec();
  }
  
  getClickedLinkEvents(): Promise<ClickedLinkEvent[]> {
    return this.clickedLinkEventModel.find().exec();
  }
}

 

signUpEventModel.find().exec()를 실행하면 컬렉션의 모든 이벤트 데이터중 signUpEventModel만 가져오게 되고

 

마찬가지로 clickedLinkEventModel.find().exec() 로는 clickedLinkEventModel 데이터만 가져오게 됩니다

 

물론 eventMode.find().exec()로도 전체 이벤트데이터를 가져올수 있습니다

 

저장할때도 내가 생성한 모델에 맞춰 저장이 됩니다

 

데이터를 가져올때나 저장할때나 좀더 타입의 이점을 살려 코드를 작성할수 있어요

 

 

 

실제로 이렇게 저장이 됩니다

 

 

또 다른 예를 들어보면 이커머스가 있습니다

 

물리적인 상품이 있는가하면 디지털 상품도 있을거예요

 

물리적인 상품은 무게나 크기같은 값을 가질수 있겠죠

 

이 두개가 모두 Product 모델로 구분된다면

 

디지털상품도 코드의 타입상으로는 weight나 size같은 값을 가질수 있게 되겠죠?

// 물리적제품
const physicalProduct = new PhysicalProduct();
physicalProduct.getWeight();


// 디지털제품
const digitalProduct = new DisitalProduct();
digitalProduct.getDownloadUrl();

이렇게 명확하게 데이터 타입을 구분하여 사용할수 있으니 좀더 코드가 쉬워질것같네요

728x90
반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함