-
Notifications
You must be signed in to change notification settings - Fork 2
연관 행의 빈도 높은 삽입, 삭제가 이뤄지는 시스템일 경우 어떻게 설계해야하나?
연관 행의 빈도 높은 삽입, 삭제가 이뤄지는 시스템일 경우 어떻게 설계해야하나?
퀴즈 플랫폼에서 Class → Quiz → Choice로 이어지는 계층적 데이터 구조를 설계하면서 깊은 고민이 있었다.
어떻게 하면 효율적으로 삭제할 수 있을지에 대한 고민이었다.
주된 고민은 크게 두 가지였다
-
데이터베이스 부하 vs 애플리케이션 부하
- 쿼리문의 효율성과 상반되는 강력한 데이터 정합성을 보장해주는 DB
- 애플리케이션에서 처리하면 서버 부하 증가
- "과연 이 트레이드오프의 최적점은 어디일까?"
-
책임 분리에 대한 철학적 고민
- 데이터 정합성은 DB의 영역인가, 애플리케이션의 영역인가?
- 유효성 검사를 어디서 해야 하는가?
- 책임을 분리하면 개발 복잡도가 증가하지 않을까?
연관된 데이터를 어떻게 입력, 삭제해야 효율적으로 삭제할 수 있을까? 라는 생각이 들었다.
크게 3가지 방법을 떠올리게 되었다.
MySQL 기준으로 외래키 제약조건인 ON CASCADE DELETE, ON DELETE RESTRICT, ON UPDATE CASCADE와 같은 기능을 이용해서 데이터의 자동삭제를 이용해서 처리가 가능하다.
어플리케이션 레벨에서 부모 엔티티만 삭제하면
데이터베이스 레벨에서 연관된 자식 레코드를 모두 삭제해준다.
그러나 데이터베이스의 제약사항을 이용해서 처리할 경우 다음과 같은 단점이 드러난다.
- 대량의 연쇄 삭제/수정 발생
- 트랜잭션 로그 크기 증가
- 테이블 락(Lock) 발생 가능성
- 서버 메모리 사용량 증가
근데 이건 두 개의 방법으로 나뉜다.
데이터베이스 레벨의 제약사항
CREATE TABLE posts (
id INT PRIMARY KEY,
user_id INT
);
어플리케이션 레벨의 제약사항
@Entity()
class User {
@OneToMany(() => Post, post => post.user, {
cascade: true
})
posts: Post[];
}
@Entity()
class Post {
@ManyToOne(() => User, user => user.posts)
user: User;
}
데이터베이스 제약사항을 애플리케이션 코드에서 관리하겠다고 한다면 TypeORM의 cascade
옵션만 사용하면 된다.
이럴 경우 다음과 같은 장점이 존재한다.
- 데이터베이스에는 아무런 제약사항이 없음
- TypeORM의
cascade: true
설정으로 관계 관리 - 삭제/수정 등의 작업이 모두 애플리케이션 레벨에서 처리됨
- TypeORM이 관계를 추적하고 연관된 엔티티들을 함께 처리
데이터베이스 레벨의 제약사항
CREATE TABLE posts (
user_id INT,
FOREIGN KEY (user_id) REFERENCES users(id)
);
어플리케이션 레벨의 제약사항
@Entity()
class Post {
@ManyToOne(() => User, {
onDelete: 'CASCADE'
})
user: User;
}
onDelete: ‘CASCADE’
설정은 데이터베이스 시스템에서 설정한 외래 키 제약 조건에 의해 동작하기에 데이터베이스의 제약조건에서는 외래키 설정만 있으면 된다.
이렇게 사용할 경우, onDelete는 외래키 관계를 확인하여 연관된 레코드를 데이터베이스 레벨에서 삭제할 수 있다. DB에는 제약조건이 존재하기에 나름 하이브리드 선택이라고 볼 수 있다.
기능 | 데이터베이스 레벨 | 애플리케이션 레벨 |
---|---|---|
제약 관리 | 데이터베이스 시스템에 설정된 외래 키 제약 조건 (onDelete, onUpdate 등) | 애플리케이션 코드에서 엔티티 관리 (cascade, eager 등) |
자동화 수준 | 데이터베이스가 직접 삭제, 갱신 등의 연산을 처리 | TypeORM이 데이터베이스 쿼리 전에 처리 |
장점 | 데이터의 무결성 보장, 데이터베이스 내에서 처리되어 속도와 일관성 보장 | 유연한 로직 처리 가능, 애플리케이션의 요구사항에 맞게 맞춤형 구현 가능 |
주 사용 옵션 | onDelete: "CASCADE", onUpdate: "RESTRICT" 등 | cascade: true, eager: true 등 |
2번 방법을 선택했으며, 쿼리문의 효율을 극대화해보는 경험을 가져보고 싶었다. 이를 통해서 벌크 삭제 기능과 더 나아가 벌크 입력의 성능을 극대화할 수 있음을 기대할 수 있었다.
- 데이터베이스에는 최소한의 테이블 구조만 정의
- 모든 관계와 제약조건은 TypeORM 엔티티에서 관리
- Cascade 작업은
@OneToMany
와@ManyToOne
데코레이터를 통해 제어 -
eager: false
설정으로 필요한 경우에만 관련 데이터 로드
-
유연성 극대화
- 데이터베이스 스키마 변경 없이 애플리케이션 로직 수정 가능
- 다양한 비즈니스 요구사항에 빠르게 대응 가능
-
성능 최적화
- eager loading을 false로 설정하여 필요한 경우에만 연관 데이터 로드
- 불필요한 조인 연산 방지
- 메모리 사용량 최적화
-
개발 생산성
- TypeORM의 강력한 타입 시스템 활용
- 코드 레벨에서 모든 관계 파악 가능
- 단일 책임 원칙 준수
-
쿼리 성능 최적화
- 외래 키 제약 조건 검사 오버헤드 제거
- INSERT, DELETE 작업 시 데이터베이스 레벨의 외래 키 검증 과정 생략
- 대량 데이터 입력 삭제 시 성능 향상 기대
- 인덱스 업데이트 부하 감소
- 외래 키 인덱스 유지/관리 비용 절감
- 특히 대량 데이터 입력 시 큰 성능 이점
- 외래 키 제약 조건 검사 오버헤드 제거
어플리케이션 레벨에서만 데이터 유효성 검증을 하기에 비즈니스 로직 부분에서 로직이 복잡해질 여지가 있어보인다.
그리고 아무래도 DB의 막강한 유효성 검증 기능을 사용하지않는 것이 죄책감을 느끼게 만드는 기분이었다.
지속적인 고민을 통해서 어떤 방법이 좋을지 생각해보아야겠다.