관계형 데이터베이스를 다뤄봤다면, 누구라도 Foreign Key의 존재와 그 용도를 알 것이다. 그리고 그 Foreign Key는 데이터의 무결성을 보장하기도 한다.
그러나 웹 서비스에 테이블 설계를 하고 실제 개발을 하다보면, Foreign Key를 정말로 걸어주는 것이 최선의 방법인가 싶을 때가 있다. 어떤 경우에 그런 생각이 들었었는지, 그에 따른 좋은 방법은 무엇인지, 이번 포스팅에서 그에 대한 고찰해보려고 한다.
참조 무결성은 데이터 무결성 중 하나다. 데이터 무결성
에 대한 위키백과 정의는 다음과 같다.
데이터 무결성(영어: data integrity)은 컴퓨팅 분야에서 완전한 수명 주기를 거치며 데이터의 정확성과 일관성을
유지하고 보증하는 것을 가리키며 데이터베이스나 RDBMS 시스템의 중요한 기능이다. - 위키백과
데이터베이스의 테이블 간 관계를 설계함에 있어서 적재될 데이터는 무결해야 한다는 것이다. 그 무결을 판단하는 데엔 데이터의 정확함과 일관됨이 기준이 된다는 말이기도 하다.
이 중 참조 무결성
은 Foreign key와 연관이 되어 있다. 다음과 같이 테이블이 있다고 하자.
학생 테이블이 있다.
[학생]
이름 | 학번 | 학과 |
---|---|---|
김태영 | 21327 | 수학과 |
이지연 | 32719 | 철학과 |
남준호 | 33828 | 기계공학과 |
그리고 학과 테이블이 있다고 하자.
[학과]
이름 | 단과대학 | 학과장 |
---|---|---|
수학과 | 자연과학대학 | 이철호 |
철학과 | 문과대학 | 문애리 |
기계공학과 | 공과대학 | 강훈정 |
학생의 테이블의 학과
컬럼은 학과 테이블의 이름
를 참조할 수 있다. 이러한 참조 관계를 이용하면 어떠한 학생이 속한 과의 학과장은 누구인지 등을 쉽게 알 수 있다.
이런 참조 관계에 있는 경우에는, 학과 테이블의 정보를 그냥 지우면 안된다. 학생 테이블에서 사용하는 학과 컬럼에 대한 정보를 더이상 알 수 없게 되기 때문이다. 그렇다는 것은 참조 무결성에 위배(=참조 무결성이 깨짐)이라고 표현할 수 있다.
이에 따라 Foreign Key 제약 조건을 이용한다. 이 제약 조건에서 가장 중요한 것은 Foreign Key로 참조하는 값이 해당 테이블에서 유일한 값이어야 한다
는 것이다. 이는 곧 학과 테이블에서 이름 컬럼의 값은 각각 unique해야 한다는 것이다. 다음과 같은 레코드가 존재 하면 안된다.
[학과]
이름 | 단과대학 | 학과장 |
---|---|---|
수학과 | 자연과학대학 | 이철호 |
수학과 | 자연과학대학 | 정인태 |
수학과
라는 이름의 레코드가 두 개가 존재함으로써 수학과 학생의 학과장을 찾을 수 없다.이는 곧 이름 컬럼이 기본키 컬럼
이거나 유일키 컬럼
이어야 한다는 말이다.
제약 조건을 사용하면 학과 테이블(참조 테이블)의 변동 사항이 있을 때, 학생 테이블의 어떻게 변동을 줄 지까지 결정할 수도 있다. 이는 삭제 될 때(ondelete), 기본키/유일키가 변경될 때(onupdate)에 적용할 수 있다.
수학과 정보를 학과 테이블에서 삭제를 시도한다면(ondelete),
1) 김태영 학생 레코드도 삭제한다. -> CASCADE
2) 김태영 학생 정보 중 학과 값은 null로 변경한다. -> SET NULL
3) 아무 일도 일어나지 않게 한다(수학과 레코드 삭제도 금지한다.) -> RESTRICT
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://username:password@localhost:5432/postgres'
db = SQLAlchemy(app)
# 학과 테이블
class Major(db.Model):
id = db.Column(
db.Integer,
primary_key=True,
autoincrement=True,
nullable=False
)
name = db.Column(db.String)
std_no = db.Column(db.Integer)
major = db.Column(db.String)
# 학생 테이블
class Student(db.Model):
id = db.Column(
db.Integer,
primary_key=True,
autoincrement=True,
nullable=False
)
name = db.Column(db.String)
std_no = db.Column(db.Integer)
# 테이블 예시에서는 이름 자체를 썼지만, 실제 서비스에서는 id를 이용하여 하는 경우가 더 많음
# Foreign key 제약 조건을 설정
major_id = db.Column(
db.Integer,
db.ForeignKey(Major.id, ondelete="SET NULL", onupdate="CASCADE")
)
그러나 실제 서비스를 개발하다 보면 참조 무결성을 지키기가 어려울 때가 있다. 참조 무결성을 지키기 어려운 것은 Foreign key 제약 조건을 사용으로 인한 불편으로부터 기인한다.
불편한 이유는 다음과 같다.
ON DELETE
나 ON UPDATE
조건을 설정하지 않았을 때, 레코드를 지우려면 삭제/변경 순서를 고려해야 한다. 테이블끼리 연쇄 참조하는 경우나 한 테이블에서 여러 테이블을 참조하고 있다면 더더욱 고려하기 복잡해지는 등, 테이블 구조가 복잡해질수록 고려해야 할 사항이 많아진다. 그래서 무결성을 포기하더라도 Foreign key 제약 조건을 걸지 않는 경우가 존재한다.
참조 무결성을 포기했다는 것은, 학과 테이블의 정보가 변경되어도 학생 테이블의 레코드는 아무런 영향을 주지 않는다는 것이다.
그렇다는 것은 논리적으로 존재하면 안되는 데이터가 존재할수도 있다는 것이고,
[학생]
이름 | 학번 | 학과 |
---|---|---|
김태영 | 21327 | 수학과 |
이지연 | 32719 | 철학과 |
남준호 | 33828 | 기계공학과 |
[학과]
이름 | 단과대학 | 학과장 |
---|---|---|
철학과 | 문과대학 | 문애리 |
기계공학과 | 공과대학 | 강훈정 |
(수학과 레코드는 지워졌지만, 김태영 학생 정보는 그대로 있다.)
학과 테이블 정보가 변경되더라도 학생 테이블에서는 적용이 되지 않는다는 것이다.
[학생]
이름 | 학번 | 학과 |
---|---|---|
김태영 | 21327 | 수학과 |
이지연 | 32719 | 철학과 |
남준호 | 33828 | 기계공학과 |
[학과]
이름 | 단과대학 | 학과장 |
---|---|---|
응용수학과 | 자연과학대학 | 이철호 |
철학과 | 문과대학 | 문애리 |
기계공학과 | 공과대학 | 강훈정 |
(수학과 레코드의 이름 값이 응용수학과로 변경됐지만, 김태영 학생 정보는 그대로 있다.)
이럴 때는 join 조건을 잘 걸어서 정보를 취하거나, 정보 변경을 같이 하거나 해야 한다.
data = db.session.query(
Student,
Major,
).join( # outerjoin을 사용하면, 논리적으로 삭제된 데이터를 볼 수 있기 때문에 무조건 join으로 한다.
Major,
Major.id == Student.major_id,
).all()
입사 초기에는 무결성을 생각하면서 Foreign key를 설정하는 것은 당연하다고 생각했다. 그러나 서비스 기능이 점차 복잡해지고 테이블 구조도 복잡해지면서 Foreign key 제약 조건 설정이 오히려 불편을 초래하기도 했다. 이런 경우는 무결성을 포기하기도 했다.
그러나 무결성을 포기하는 만큼 프로덕트 서비스 레벨에서도 고려해야 하는 부분이 반드시 존재하며, 어떤 부분이 프로덕트 개발에 있어 더 효율적인지는 데이터 구조의 복잡성과 기능의 복잡성을 고려해서 판단해야 한다.