본문 바로가기

Computer Science/Database

MySQL의 ENUM 타입

최근 데이터베이스 설계 프로젝트를 진행하면서 ENUM 타입에 대해 잘못 알고 있던 부분이 있어서 기록을 해두려고 한다. 설계 프로젝트는 MariaDB로 진행했지만 MySQL과 큰 차이가 없을 것이라고 보고 MySQL 기준으로 내용을 작성하려고 한다.

 


MySQL의 ENUM 타입

 

MySQL에는 ENUM 타입이 존재하는데, SET타입과 마찬가지로 내부적으로 숫자 값으로 매핑해서 관리하는 타입이다. 테이블 자체를 설계하면서 그 구조에 나열된 목록 중에 하나의 값을 가질 수 있다. 글로 설명하는 것보다 SQL 쿼리문 자체를 바로 확인해 보자.

CREATE TABLE jdm
(
    engine ENUM ('B16B', 'RB26DETT', '2JZ-GTE')
);

INSERT INTO jdm
VALUES ('B16B'),
       ('RB26DETT');

SELECT * FROM jdm;
+--------+
|engine  |
+--------+
|B16B    |
|RB26DETT|
+--------+

jdm이라는 이름의 테이블을 생성했고 이 테이블의 engine 컬럼은 값으로  'B16B', 'RB26DETT', '2JZ-GTE' 3가지 값만 받을 수 있도록 만들었다.

ENUM  타입은 INSERT, UPDATE, SELECT 등의 쿼리에서 CHAR나 VARCHAR 타입처럼 문자열로 비교하거나 저장하는 것이 가능하다. 그런데, 아래의 예시에서도 살펴볼 수 있듯이 MySQL 서버는 실제 값을 디스크나 메모리에 저장할 때 값에 매핑된 정수값을 사용한다.

SELECT engine * 1 AS real_value
FROM jdm;
+----------+
|real_value|
+----------+
|1         |
|2         |
+----------+

위의 예시처럼 SELECT문에서 1을 곱하거나 0을 더하는 연산을 적용해서 ENUM 타입의 실제 값을 확인하는 것이 가능하다. ENUM 타입에서 매핑되는 정수값은 일반적으로 테이블 정의에 나열된 문자열 순서대로 1부터 할당되고, 빈 문자열은 항상 0으로 매핑된다.

그래서 아래와 같이 WHERE문에 값을 직접 명시하지 않고, 정수값을 전달해서 원하는 값을 찾는 것이 가능하다.

SELECT *
FROM jdm
WHERE engine = 2;
+--------+
|engine  |
+--------+
|RB26DETT|
+--------+

그리고 ENUM 타입의 이러한 특징을 제대로 인지하지 못했기 때문에 이 부분에서 문제를 겪었다.

 


문제 상황

 

먼저, 진행했던 프로젝트는 MariaDB로 진행되었음을 다시 밝힌다.

서비스 내에서 사용되는 모든 주소 정보를 담고 있는 addr_info 라는 테이블을 작성했고, 여기서 주소지가 산간 지역인지, 일반 대지에 해당되는지 확인하기 위해서 mountain 컬럼에 ENUM 타입을 사용했었다.

CREATE TABLE IF NOT EXISTS addr_info
(
    id               BIGINT      NOT NULL AUTO_INCREMENT,
    stat_code        VARCHAR(10) NOT NULL,
    city             VARCHAR(40) NOT NULL,
    sigungu          VARCHAR(40) NOT NULL,
    eupmyeondong     VARCHAR(40) NOT NULL,
    lee              VARCHAR(40),
    mountain         ENUM ('0','1') DEFAULT '0',
    street_code      VARCHAR(12) NOT NULL,
    street_name      VARCHAR(80) NOT NULL,
    zip_code         VARCHAR(5)  NOT NULL,
    building         VARCHAR(40),
    building_details VARCHAR(100),
    PRIMARY KEY (id)
);

ENUM 타입에 굳이 '산', '대지'와 같이 이름을 명시하지 않고 숫자로 표현해도 괜찮을 것 같다는 생각에 '0'은 대지를, '1'은 산간지역을 표시하도록 ENUM  타입을 사용했다.

그래서 원래의 의도대로 동작한다면 '0' 또는 '1'을 제외한 다른 값을 입력했을 때 입력에 실패하는 것이 맞지만 실제 결과는 그렇지 않았다.

INSERT INTO addr_info (stat_code, city, sigungu, eupmyeondong,
                       mountain, street_code, street_name,
                       zip_code, building, building_details)
VALUES ('대전B마트', '대전광역시', '동구', '홍도동',
        '2', '302004301033', '홍도로 11',
        '34558', '아리랑프라자', '205~215호');

SELECT * FROM addr_info;

예상과는 다르게 실제 입력된 결과를 조회해 보니 mountain에 '2'도 아니고 '1'이 저장된 것을 확인할 수 있었다.

한편, '0' 또는 '1'을 입력했을 때는 정상적으로 INSERT에 성공했고, '-1' 또는 '3'을 입력했을 때 정상적으로 INSERT에 실패했다.

2행과 3행은 각각 '0'과 '1'을 넣은 경우

 


문제의 원인

 

앞서 ENUM 타입에 대해서 다루었던 것처럼 ENUM은 사용자가 ENUM에 어떤 값을 넣어놓던 내부적으로 숫자값으로 매핑해서 관리하기 때문에 발생한 문제였다.

그래서 '0'은 내부적으로 정수 1에 매핑된 것이고, '1'은 내부적으로 정수 2에 매핑된 상태였기 때문에 INSERT문에서 mountain 컬럼에 '2'를 입력했다는 것은 ENUM value 중 '1'을 사용하겠다는 것이고, 따라서 정상적으로 INSERT가 수행되었던 것이다.

여기서 신기했던 점은 '2'는 작은따옴표로 묶여 있는 상태였는데, 그럼에도 숫자 2에 매핑된 실제 ENUM value를 가져오는데 문제가 없었다는 점이다.

이러한 문제가 있기 때문에 실제 MariaDB 공식문서상에서도 ENUM에 숫자값을 넣는 것을 권장하지 않는다고 되어 있다.

 

ENUM

Enumeration, or string object that can have one value chosen from a list of values.

mariadb.com

 

ENUM의 이러한 특징을 알게 된 뒤 0과 1이 아닌 '대지'와 '산'을 명확하게 나타내는 방향으로 수정했다.

-- AS-IS
mountain ENUM ('0','1') DEFAULT '0',

-- TO-BE
mountain ENUM ('대지', '산') DEFAULT '대지',

 


 

ENUM 쓰면 뭐가 좋음?

 

ENUM 타입에 사용할 수 있는 최대 아이템의 개수는 65,535개인데, ENUM에 속한 아이템의 개수가 255개 미만이면 ENUM 타입은 저장 공간으로 1바이트를 사용하고, 그 이상의 경우에는 2바이트까지 사용한다. 즉, 데이터베이스 서버의 디스크 저장 공간의 크기를 적게 사용할 수 있다는 장점을 가지고 있는 것이다.

팀원들과 함께한 작은 프로젝트와 같이 테스트 데이터만 들어가는 경우에는 이 장점을 크게 느낄 수 없겠지만, 실제로 거래가 많이 발생하는 배달의 민족과 같은 대규모 서비스 상에서는 레코드가 억 단위를 넘어갔을 때 문자열 컬럼보다 ENUM 타입이 1~2GB의 저장공간을 줄일 수 있다고 한다.

디스크의 데이터는 InnoDB 버퍼 풀로 적재되어야 쿼리에서 사용할 수 있기 때문에 디스크 데이터가 커질수록 메모리도 동시에 많이 필요해진다. 따라서 ENUM을 사용해 1~2GB의 디스크 공간을 아낀다는 것은 메모리 사용량도 줄일 수 있다는 것을 뜻한다. 또한, 디스크 사용량이 적다면 덤프파일을 만들어 백업이나 복구하는데 걸리는 시간도 줄일 수 있게 된다.

결과적으로 ENUM을 사용한다면 공간과 시간 측면에서 이득을 볼 수 있다.

 

Reference.

- Real MySQL 8.0