이 글은 Chris Komlenic의 글 8 Reasons Why MySQL's ENUM Data Type Is Evil을 번역한 글입니다. 원문은 링크에서 찾아보실 수 있습니다.
얼핏 보면 MySQL의 ENUM 데이터 타입은 여러 값 중 허용된 값만 저장되게끔 제약한다
는 목적에 가장 부합하는 효과적인 방법처럼 보입니다. 이제 다음 케이스를 살펴봅시다.
country
테이블에 continent
칼럼이 있으며,
(물론 언젠가 북미 대륙이 아시아 대륙에 부딪쳐 Noramersia
가 탄생할 수도 있지만, 데이터베이스가 그 정도의 시간을 버텨냈다면 적어도 당신이 테이블 구조 변경 회의에 참여할 일은 없을 것이고, 다른 누군가가 그 문제를 해결할 겁니다.)
만약 ENUM
이외에 사용할 수 있는 수단이 전무하다면 그냥 ENUM
을 사용하고, 다른 주제에 대해 토론하는게 낫습니다. (NoSQL
의 장점이나 Git
vs SVN
혹은 어떤 프레임워크가 나쁜가 같은 것들 말이죠) 하지만 대부분의 상황에서 ENUM
보다 더 나은 옵션이 있습니다. 바로 참조 테이블입니다. 위 구조를 참조 테이블로 표현하면 간단하게 나타낼 수 있습니다.
위키피디아에 따르면:
... 특정 데이터 필드에 대해 가능한 값들이 해당 테이블에 직접 명시되지 않고, 다른 테이블을 통해 참조되는 구조를 참조 테이블이라 한다. 예를 들어, 물류 창고의 관계형 데이터베이스 모델에서 '상품' 엔티티는 '판매됨', '재고', '재고 없음'과 같은 값을 가진 '상태' 필드를 가질 것이다. 순수 관계형 모델에서는 이러한 값들이 데이터베이스 정규화를 위해 별도의 엔티티 혹은 '상태'라는 참조 테이블에 속하게 될 것이다.
이제 참조 테이블을 통해 열거형 데이터(enumeration
)을 표현할 수 있다는 점을 확인했으니 왜 ENUM
타입을 사용하는 것이 나쁜지 알아봅시다.
남/여, Mr/Mrs/Ms, 아프리카/아시아 같은 텍스트 데이터에 대해 ENUM
타입을 사용하게 되면, 엄밀히 따졌을 때 가능한 값에 대한 데이터는 마땅히 데이터가 저장되어야할 곳(테이블)이 아닌 다른 곳(테이블 칼럼 정의)에 남게 됩니다. 즉 ENUM
을 사용하게되면 모델 생성과 관련된 데이터만 담아야할 곳에 레코드와 관련된 데이터를 담는 꼴이 되는 겁니다. 그러므로 ENUM
타입의 칼럼은 데이터베이스 정규화를 위반하게 됩니다.
사람들은 언제나ENUM
칼럼을 추가하면서 대상이 되는 값이 변하지 않을 것이란 기대를 합니다. 하지만 우리의 예측은 언제나 빗나갑니다: R&D팀에서는 새로운 상품을, 회사는 새로운 배송 방법을 추가하고, 북미 대륙은 아시아와 충돌합니다. (역자주: 새로운 대륙이 나타날 수 있다는 의미)
문제는 ALTER TABLE
명령어를 통해 ENUM
의 대상이 되는 값을 변경하는 것이 굉장히 느리다는 겁니다. ENUM('red', 'blue', 'black')
을 ENUM('red', 'blue', 'white')
으로 바꾸려면 MySQL 엔진이 그 테이블 전체를 뒤져 더 이상 유효하지 않은 값인 black
을 찾아야만 합니다. MySQL은 똑똑하지 않기 때문에 ENUM
칼럼의 대상에 새로운 값 하나를 추가하기만 하더라도 테이블 전체를 뒤질겁니다. (역자 주: 이는 MySQL의 버그였으며, 현재는 단순히 리스트 맨 뒤에 값을 추가하는 작업은 느리지 않습니다.)
큰 테이블에서는 이렇게 전체 테이블을 새로 구성하는데 긴 시간이 걸립니다. 그러나 ENUM
대신 참조 테이블을 사용했더라면 단순 INSERT
, UPDATE
혹은 DELETE
만으로 이를 해결할 수 있습니다. 또 하나 중요한 사실은 ENUM
대상 값을 변경할 때 기존의 값이 더 이상 포함되지 않는다면 MySQL이 ''(빈 스트링)으로 변환한다는 것입니다. (역자 주: 이 또한 strict mode
를 사용한다면 예방이 가능합니다. 참조) 참조 테이블을 사용하면 이름을 바꾸거나 대상을 제거하는데 있어서 더 유연하게 대처할 수 있게 되는 겁니다.
ENUM
칼럼에 연관된 정보를 저장할 수 있는 간단한 방법은 없습니다. contry/continent
예시에서 대륙의 육지 면적과 같은 추가적인 정보가 필요하다면 어떻게 할 수 있을까요? 참조 테이블을 사용한 경우에는 대륙 테이블에 간단히 land_area
칼럼만 추가해주면 됩니다. 하지만 ENUM
을 사용했다면? 불가능할겁니다.
이처럼 참조 테이블을 사용할 경우 확장성 덕분에 많은 경우 유연하게 대처할 수 있습니다. 한 가지 예는 어떤 선택지가 더 이상 선택이 불가능함을 나타내는 플래그(flag)가 필요할 때입니다. 만약 회사에서 더 이상 검은색 제품을 팔지 않는다면 참조 테이블에 간단히 is_discontinued
칼럼만 추가해주면 됩니다. 물론 그렇게 하더라도 현재 판매 중인 색상을 조회하는 기능에는 전혀 문제가 없고, 이전에 판매된 검은색 제품에 대한 정보도 전혀 변하지 않습니다! 이제 이 기능을 ENUM
으로 구현했다고 생각해보세요. (역자 주: ENUM
타입을 사용했을 때 연관 데이터를 저장할 수단이 크게 제약된다는 의미)
ENUM
값들만 조회하는 것이 힘들다.아래와 같이 데이터베이스에서 가능한 값들만 가져와서 드롭다운 리스트를 만드는 일은 매우 흔한 요구사항입니다.
색상 선택:
<select name="colors" size="1">
<option value="red">red</option>
<option value="blue">blue</option>
<option value="black">black</option>
</select>
만약 이 값들이 colors
참조 테이블에 저장돼있다면: select * from colors ...
만으로 불러올 수 있습니다. 이 경우 단순히 테이블에 값을 추가하거나 변경하는 것만으로도 드롭다운 리스트의 값이 자동적으로 변경됩니다. 아주 깔끔한 상황이죠.
이제 ENUM
의 경우를 한 번 봅시다: 어떻게 대상 값들만을 추려낼 수 있을까요? 테이블에서 DISTINCT
쿼리를 사용해 조회를 해볼 수 있겠지만 이 경우에는 실제 사용된 값이 아니면 조회할 수 없습니다. 물론 INFORMATION_SCHEMA
를 파싱해서 동적으로 결과를 얻어낼 수도 있겠지만 이는 필요 이상으로 복잡합니다. 사실 아직까지도 순수 SQL만으로 ENUM
칼럼의 대상 값을 가져오는 방법은 찾아내지 못했습니다.
(역자 주: ORM
을 사용하는 경우에는 큰 문제가 되지 않습니다.
한 예로 node.js
의 sequelize
의 테이블 정의를 보면,
// items.js
const statuses = ['sold', 'reserved', 'out of stock']
const Items = sequelize.define('items', {
status: DataTypes.ENUM(statuses)
...
})
// 전체 ENUM 값이 필요하다면 statuses를 사용!
와 같이 애플리케이션 코드 안에ENUM
의 대상이 되는 값을 선언/관리할 수 있기 때문입니다.)
ENUM
을 사용함으로써 얻는 최적화의 효과가 그리 크지 않을 수 있다.ENUM
사용을 정당화하는 논의는 일반적으로 최적화(성능에 대한 최적화 및 복잡한 모델을 간단하게 만드는 최적화)와 관련이 있습니다.
성능에 대한 부분부터 살펴보죠. 비효율적인 쿼리는 데이터의 규모가 일정 수준에 도달하기 전까지는 성능에 큰 영향을 미치지 않습니다. 그러므로 디비 개발자들은 성능에 대한 고려가 정말로 필요한 지점까지는 다른 개발자들이 최대한 정규화된 테이블을 유지하게끔 해야합니다. 반사적으로 테이블 조인이나 참조 테이블이 병목의 원인이라고 생각하는 대신 벤치마크를 돌려서 성능 저하의 진짜 원인이 무엇인지 살펴볼 필요가 있는 것이죠.
두 번째 주장은 ENUM
이 전체 테이블과 외래키의 개수를 줄여준다는 주장입니다. 맞습니다. 참조 테이블과 외래키를 추가하면 테이블 정규화의 결과로 더 이해하기 어렵고 복잡한 쿼리가 나타날 것입니다. 하지만 모델과 추상화는 우리가 해당 문제 영역을 이해하기 위한 수단입니다. 그러므로 참조 테이블 하나를 더 추가하는 것이 구조를 더 복잡하게 만들기때문에 ENUM
을 사용한다는 주장은 좋은 근거라고 볼 수 없습니다.
ENUM
칼럼에 정의된 값들은 다른 테이블에서 재사용할 수 없다.ENUM
칼럼은 한 번 정의하고 나면 다른 테이블에서 이를 재사용할 수 없습니다. 반면 참조 테이블을 사용하면 다른 어떤 테이블에도 쉽게 참조할 수 있습니다. 또 참조 테이블의 데이터를 변경하는 것만으로 연결된 모든 테이블의 값을 쉽게 변경할 수 있습니다.
ENUM
칼럼을 사용할 때는 서로 다른 두 테이블에 동일한 값을 복사하는 것말고는 방법이 없습니다. (그리고 이 방법은 데이터의 동일성을 유지하기 위해 지속적으로 서로 맞춰줘야한다는 단점이 있습니다.)
ENUM
칼럼은 조심해서 사용해야한다.ENUM('blue', 'black', 'red')
에 purple
을 추가하면 새로운 값은 ''(빈 스트링)으로 들어갑니다. (역자 주: 위에서 얘기했듯 strict mode
를 사용하면 이 현상이 발생하지 않습니다.) 반면 참조테이블과 외래키를 사용하면 쉽게 데이터 정합성을 보장할 수 있습니다.
또한 MySQL은 ENUM
값을 내부적으로 정수 키로 참조하기 때문에 값이 아닌 인덱스를 참조하게 되는 일도 흔히 발생합니다. 아래의 경우를 보면
CREATE TABLE test (foobar ENUM('0', '1', '2'));
mysql> INSERT INTO test VALUES ('1'), (1);
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql> SELECT * FROM test;
+--------+
| foobar |
+--------+
| 1 |
| 0 |
+--------+
2 rows in set (0.00 sec)
스트링 값인 '1'과 실수로 정수 값인 1을 넣었음에도 MySQL은 정수 1을 ENUM
의 첫 번째 값인 '0'을 가리킨다고 판단하여 어떤 에러도 발생시키지 않습니다.
ENUM
은 다른 DBMS에서 널리 통용되지 않습니다.ENUM
타입은 표준 SQL 문법에 포함되지 않으며, MySQL을 제외하면 ENUM
을 지원하는 DBMS는 많지 않습니다. PostgreSQL
, MariaDB
, and Drizzle
정도인데, 이 중 둘은 MySQL의 fork입니다. 그렇기 때문에 어떤 이가 데이터베이스를 이전하고자한다면 ENUM
을 변환하는 추가적인 과정이 필요하게 됩니다. 물론 일반적으로 데이터베이스를 이전하는 일은 자주 일어나는 일이 아니기 때문에 이 단점은 가장 아래의 8번에 위치하게 됐습니다.
여기에 대한 좋은 예는 전체 대륙, 호칭(saluations) 혹은 카드 수트(card suit) 같은 것들입니다. 물론 이런 경우에도 호칭에 'Dr.'를 추가한다거나 카드 게임 앱에서 정규 수트가 아닌 조커(Joker)와 같은 새로운 수트가 필요할 수 있죠.
다시 스페이드/하트/다이아몬드/클럽을 생각해봅시다. 클럽과 스페이드는 검은색이고 하트와 다이아몬드는 빨간색이라는 사실을 저장할 필요가 생긴다면 어떻게 해야할까요? 참조 테이블을 사용한 경우 추가적으로 색깔에 대한 정보를 저장할 칼럼만 추가하면 되지만 ENUM
을 사용했다면 이를 표현하기가 훨씬 까다로워지고 결국 애플리케이션 레벨에서 이를 강제할 수밖에 없게 됩니다.
대상이 되는 값이 두 개인 경우 ENUM
보다는 TINYINT(1)
나 BIT(1)
을 사용하는 편이 낫습니다. 예를 들어 ENUM('male', 'female')
은 is_male BIT(1)
으로 바꿀 수 있습니다. (역자 주: 저는 BIT(1)이 ENUM이 가진 의미를 살리지 못한다고 생각해서 ENUM을 사용하는 편이 더 나아보입니다.)
20개보다 많은 값을 ENUM
에 저장할 경우 필요 이상으로 지저분해질 것이고, 50개 이상되는 값을 저장했다가는 다룰 수 없을 정도로 복잡해질 겁니다.
ENUM
사용 문제는 개발/운영의 관점에서 적당한 합의점을 정해서 그에 따르면 됩니다. 성능 문제는 그 문제가 수면 위로 떠오를 때 그 때 최적화를 진행하면 됩니다. 물론 그런 경우에도 참조 테이블 대신 ENUM
타입을 쓸 근거는 크게 없어보입니다.
아래는 Donald Knuth
가 최적화에 대해 남긴 말입니다.
There is no doubt that the grail of efficiency leads to abuse. Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. (성급한 최적화는 만악의 근원이다)
Yet we should not pass up our opportunities in that critical 3%. A good programmer will not be lulled into complacency by such reasoning, he will be wise to look carefully at the critical code; but only after that code has been identified. - Donald Knuth
번역 후기
원글에는 이제는 더이상 유효하지 않은 부분도 많았지만 데이터 설계에 대해 다시 한 번 생각해보게 하는 좋은 글이었습니다. 저는 개인적으로 작은 참조 테이블을 많이 만드는 것에 대해 부정적으로 생각합니다. (단순히 테이블이 많아지는 것이 보기 싫다는 미적인 이유에서) 하지만 애플리케이션의 요구사항은 언제나 달라지기 마련이고 참조 테이블을 적극적으로 활용해서 유연한 구조를 유지하는 것은 긍정적으로 생각합니다. MySQL 8.0 버전부터는 json
데이터 타입에 대한 많은 지원을 추가했는데, 이제는 ENUM
타입과 참조 테이블의 중간정도 성격의 데이터를 저장하기 위해 json
타입을 사용할 수 있지 않을까 싶습니다. MySQL에서 json
타입의 정합성 보장을 위한 유효성 검사도 제공하고, 성능이 필요하다면 json
타입에 대해 간접적으로라도 인덱스를 추가하는 방법도 있으니까요. 번역글을 재밌게 읽으셨다면 원문도 한 번 읽어보시길 바랍니다. 감사합니다.
이 저작물은 크리에이티브 커먼즈 저작자표시-비영리-동일조건변경허락 4.0 국제 라이선스에 따라 이용할 수 있습니다.
잘 읽었습니다. 좋은 글 감사합니다.