본문 바로가기
JavaScript & TypeScript

TypeORM에서 연관관계 유지한채 FK만 제거하기 (w. NestJS)

by 향로 (기억보단 기록을) 2021. 10. 10.
반응형

데이터베이스의 FK (Foreign Key) 는 데이터 일관성을 적용하여 데이터베이스를 깨끗하게 유지한다는 큰 장점을 가지고 있습니다.

다만, 서비스의 규모가 커져 테이블당 row가 1억건이 돌파하는 시점부터는 FK는 많은 변경의 병목이 되는데요.

이유는 1억건 이상일 경우 alter table 로는 3~5시간씩 수행 되기 때문입니다.
이 시간동안 테이블 Lock이 발생할 수 있으며, 이를 회피하기 위해 아래와 같은 여러 장치들이 지원됩니다.

다만, FK가 있을 경우 테이블 복사, OnlineDDL를 비롯해서 클러스터링 / 샤딩 / 파티셔닝 등 여러 대량의 데이터 상황에서 제약들이 발생하여서 일정 규모 이상의 서비스에서는 DBA분들의 요청으로 FK를 제거해서 사용하기도 하는데요.

TypeORM Issue 에서도 이에 대해 이야기 나누고 있습니다.

물론 서비스가 작고 DBA분들이 없다면 FK를 통해서 데이터 무결성을 지키는게 훨씬 좋다고 봅니다.

그래서 TypeORM을 사용할 경우에도 마찬가지로 FK가 제거된 형태의 Entity를 구성해야만 합니다.
N:1 관계를 유지하면서 FK 제약조건만 제거된 형태를 구성하는 방법을 알아보겠습니다.

사용법

사용법 자체는 간단합니다.
아래와 같이 createForeignKeyConstraints 옵션을 false로 두는 것입니다.

@ManyToOne(type => Person, {
  createForeignKeyConstraints: false
})

실제 Entity로 한다면 다음과 같이 구성할 수 있습니다.

@Entity()
@Index('idx_user_1', ['group']) // (1)
export class User extends BaseTimeEntity {
  @PrimaryGeneratedColumn()
  id: number;

  ...

  // (2)
  @ManyToOne(() => Group, {
    createForeignKeyConstraints: false,
    nullable: false,
  })
  @JoinColumn({ name: 'group_id', referencedColumnName: 'id' })
  group: Group;

}

(1) @Index('idx_user_1', ['group'])

  • MySQL, PostgreSQL 같은 경우에는 FK를 생성하면 인덱스도 함께 생성 됩니다.
  • 그러다보니 FK를 생성하지 않으면 해당 컬럼의 인덱스를 꼭 필수로 생성해야 ManyToOne 등의 연관관계 조회에서 성능 이슈를 겪지 않습니다.
  • 인덱스 대상은 테이블 컬럼이 아닌 Entity 필드를 사용합니다 (group_id가 아닌 group)
  • 이렇게 하더라도 아래와 같이 마이그레이션에서는 정상적으로 group_id 컬럼으로 지정됩니다.
index

(2) @ManyToOne ~ @JoinColumn

  • 위에서 언급한대로 createForeignKeyConstraints: false, 를 통해 FK 제약조건을 제거합니다.
  • 저같은 경우엔 @OneToOne 외에도 @ManyToOne 에서도 꼭 @JoinColumn을 선언해줍니다.
  • 실제 생성될 테이블의 모습이 어느정도 예측 가능 범위 내에 있어야하고, 이후 인덱스 튜닝등이 필요해 복합 컬럼 인덱스를 만들어야할때가 있어 이때 어느 컬럼을 잡아야할지 알 수 있습니다.
  • nullable: false가 없을 경우 FK임에도 nullable 컬럼이 생성됩니다.
    • 이 부분을 꼭 선언하셔야 null 값이 들어가는것을 방지할 수 있습니다. 
    • 물론 기능상 null이 들어갈 수 있다면 선언하지 않으셔도 됩니다.

실제로 잘 작동하는지 테스트 코드로 검증해보겠습니다.

테스트코드

FK 제약 조건이 없어도 연관관계가 잘 작동하는지 검증 하기 위해 아래와 같이 lazy:true 를 추가해 연관관계 기능을 검증해봅니다.

@Entity()
@Index('idx_user_1', ['group'])
export class User extends BaseTimeEntity {
  ...

  @ManyToOne(() => Group, {
    createForeignKeyConstraints: false,
    lazy: true, // 연관관계 테스트를 위한 lazy 설정
    nullable: false,
  })
  @JoinColumn({ name: 'group_id', referencedColumnName: 'id' })
  group: Group;

  ...
}

테스트 코드는 다음과 같습니다.

it('user와 group lazy load', async () => {
  //given
  const groupName = 'testGroup';
  const group = await groupRepository.save(Group.of(groupName, 'desc'));
  const firstName = 'donguk';
  const user = User.byName(firstName, 'lee');
  user.updateGroup(group);

  await userRepository.save(user);

  //when
  const savedUser = await userRepository.findOne({ firstName });
  const savedGroup = await savedUser.group;

  console.log(`savedGroup = ${JSON.stringify(savedGroup)}`);

  //then
  expect(savedUser.firstName).toBe(firstName);
  expect(savedGroup.name).toBe(groupName);
});
  • console.log 로 하는 테스트는 좋은 테스트는 아닙니다.
  • 다만, 실제로 잘 조회되는지 포스팅에서 보여주기 위함이며, 실제로는 //when 에서 하듯이 expect 로 셀프 검증 (SelfTestingCode) 이 되어야합니다.

이렇게 테스트 코드를 작성후 검증을 해보면?

result

Lazy Loading 에서도 FK 제약 조건 없이 잘 작동하는 것을 확인할 수 있습니다.

 

반응형