제로 너비 공간이란?
제로 너비 공간(ZWSP)은 텍스트에서 눈에 보이지 않는 공백으로, 그 자체는 아무런 너비도 가지지 않지만 텍스트의 형식을 제어하는 데 사용됩니다.
이 문자들은 텍스트의 가독성을 해치지 않으면서도 데이터 처리 과정에서 문제를 일으킬 수 있습니다.
특히 SQL 데이터베이스에서는 데이터 무결성을 저해하거나 예상치 못한 결과를 초래할 수 있습니다.
주요 제로 너비 문자 종류 및 설명
제로 너비 공간 (Zero Width Space, ZWSP) - U+200B
설명: 두 글자 사이에 보이지 않는 간격을 추가합니다. 줄 바꿈 지점을 표시하거나 텍스트를 구분하는 데 사용됩니다.
용도: 줄 바꿈 허용 지점 표시, 언어별 단어 분리
제로 너비 비조인자 (Zero Width Non-Joiner, ZWNJ) - U+200C
설명: 아랍어나 힌디어와 같은 언어에서 두 문자가 결합되는 것을 방지합니다. 결합되지 않는 상태로 두 문자를 표시합니다.
용도: 문자 결합 방지, 특정 글자 형태 유지
제로 너비 조인자 (Zero Width Joiner, ZWJ) - U+200D
설명: 아랍어, 힌디어 등에서 두 문자가 결합되는 것을 촉진합니다. 결합된 형태로 두 문자를 표시합니다.
용도: 문자 결합 촉진, 이모지 결합
제로 너비 비브레이크 공간 (Zero Width No-Break Space, ZWNBSP) - U+FEFF
설명: 줄 바꿈을 방지하는 보이지 않는 간격입니다. 보통 BOM(Byte Order Mark)로 사용되며, 파일의 바이트 순서를 표시합니다.
용도: 줄 바꿈 방지, 파일의 바이트 순서 표시
왼쪽에서 오른쪽 표시 (Left-to-Right Mark, LRM) - U+200E
설명: 텍스트의 방향을 좌에서 우로 강제합니다.
용도: 텍스트 방향 제어
오른쪽에서 왼쪽 표시 (Right-to-Left Mark, RLM) - U+200F
설명: 텍스트의 방향을 우에서 좌로 강제합니다.
용도: 텍스트 방향 제어
단어 결합자 (Word Joiner, WJ) - U+2060
설명: 줄 바꿈 없이 단어를 연결합니다.
용도: 줄 바꿈 방지
보이지 않는 구분자 (Invisible Separator) - U+2063
설명: 보이지 않는 구분자로 사용됩니다.
용도: 논리적 구분
소프트 하이픈 (Soft Hyphen, SHY) - U+00AD
설명: 줄 바꿈 시 하이픈을 표시하지만, 줄 바꿈이 없을 경우 보이지 않습니다.
용도: 하이픈 표시 제어
제로 너비 문자의 문제점
데이터 무결성 문제: 제로 너비 문자는 보이지 않기 때문에 데이터 입력 시 실수로 포함될 수 있으며, 이는 데이터 무결성에 영향을 줄 수 있습니다.
검색 및 매칭 문제: 검색어에 제로 너비 문자가 포함되면 일치하는 결과가 나오지 않을 수 있습니다.
데이터 처리 문제: 데이터 전처리 과정에서 예상치 못한 동작을 일으킬 수 있습니다.
SQL별 제로 너비 문자 대처법
1. MySQL
REPLACE 함수와 정규 표현식을 사용할 수 있습니다.
SELECT REPLACE(REPLACE(REPLACE(REPLACE(column_name, CHAR(8203), ''), CHAR(8204), ''), CHAR(8205), ''), CHAR(65279), '') AS cleaned_column
FROM table_name;
위 예제에서는 주요 제로 너비 문자(Zero Width Space, Zero Width Non-Joiner, Zero Width Joiner, Zero Width No-Break Space)를 제거하는 방법을 보여줍니다.
2. PostgreSQL
regexp_replace 함수를 사용하여 제로 너비 문자를 제거할 수 있습니다.
SELECT regexp_replace(column_name, '[\u200B\u200C\u200D\uFEFF]', '', 'g') AS cleaned_column
FROM table_name;
이 예제에서는 유니코드 정규 표현식을 사용하여 제로 너비 문자를 찾아 제거합니다.
3. SQL Server
REPLACE 함수를 중첩하여 제로 너비 문자를 제거할 수 있습니다.
SELECT REPLACE(REPLACE(REPLACE(REPLACE(column_name, NCHAR(8203), ''), NCHAR(8204), ''), NCHAR(8205), ''), NCHAR(65279), '') AS cleaned_column
FROM table_name;
위 예제에서는 NCHAR 함수를 사용하여 제로 너비 문자를 제거합니다.
4. Oracle
REGEXP_REPLACE 함수를 사용하여 제로 너비 문자를 제거할 수 있습니다.
SELECT REGEXP_REPLACE(column_name, '[\u200B\u200C\u200D\uFEFF]', '') AS cleaned_column
FROM table_name;
이 예제에서는 유니코드 정규 표현식을 사용하여 제로 너비 문자를 찾아 제거합니다.
SQL 테스트
CREATE TABLE UNI_TEXT_TEST (
ID NUMERIC,
UNI_TEXT_FIELD VARCHAR(4000),
TEXT_FIELD VARCHAR(4000)
);
-- POSTGRESQL
INSERT INTO UNI_TEXT_TEST (ID, UNI_TEXT_FIELD, TEXT_FIELD) VALUES (1, E'다른게뭘까\u200B', '다른게뭘까');
INSERT INTO UNI_TEXT_TEST (ID, UNI_TEXT_FIELD, TEXT_FIELD) VALUES (2, E'다른게\u200B뭘까', '다른게뭘까');
INSERT INTO UNI_TEXT_TEST (ID, UNI_TEXT_FIELD, TEXT_FIELD) VALUES (3, E'\u200B다른게뭘까', '다른게뭘까');
INSERT INTO UNI_TEXT_TEST (ID, UNI_TEXT_FIELD, TEXT_FIELD) VALUES (4, E'다른게뭘까\u200C', '다른게뭘까');
INSERT INTO UNI_TEXT_TEST (ID, UNI_TEXT_FIELD, TEXT_FIELD) VALUES (5, E'다른게\u200C뭘까', '다른게뭘까');
INSERT INTO UNI_TEXT_TEST (ID, UNI_TEXT_FIELD, TEXT_FIELD) VALUES (6, E'\u200C다른게뭘까', '다른게뭘까');
-- ORACLE
INSERT INTO TEST04.UNI_TEXT_TEST (ID, UNI_TEXT_FIELD, TEXT_FIELD) VALUES (1, '다른게뭘까'||UNISTR('\200B'), '다른게뭘까');
INSERT INTO TEST04.UNI_TEXT_TEST (ID, UNI_TEXT_FIELD, TEXT_FIELD) VALUES (2, '다른게'||UNISTR('\200B')||'뭘까', '다른게뭘까');
INSERT INTO TEST04.UNI_TEXT_TEST (ID, UNI_TEXT_FIELD, TEXT_FIELD) VALUES (3, UNISTR('\200B')||'다른게뭘까', '다른게뭘까');
INSERT INTO TEST04.UNI_TEXT_TEST (ID, UNI_TEXT_FIELD, TEXT_FIELD) VALUES (4, '다른게뭘까'||UNISTR('\200C'), '다른게뭘까');
INSERT INTO TEST04.UNI_TEXT_TEST (ID, UNI_TEXT_FIELD, TEXT_FIELD) VALUES (5, '다른게'||UNISTR('\200C')||'뭘까', '다른게뭘까');
INSERT INTO TEST04.UNI_TEXT_TEST (ID, UNI_TEXT_FIELD, TEXT_FIELD) VALUES (6, UNISTR('\200C')||'다른게뭘까', '다른게뭘까');
-- POSTGRESQL OR ORACLE
INSERT INTO UNI_TEXT_TEST (ID, UNI_TEXT_FIELD, TEXT_FIELD) VALUES (1, '다른게뭘까' || chr(8203), '다른게뭘까');
INSERT INTO UNI_TEXT_TEST (ID, UNI_TEXT_FIELD, TEXT_FIELD) VALUES (2, '다른게' || chr(8203) || '뭘까', '다른게뭘까');
INSERT INTO UNI_TEXT_TEST (ID, UNI_TEXT_FIELD, TEXT_FIELD) VALUES (3, chr(8203) || '다른게뭘까', '다른게뭘까');
INSERT INTO UNI_TEXT_TEST (ID, UNI_TEXT_FIELD, TEXT_FIELD) VALUES (4, '다른게뭘까' || chr(8204), '다른게뭘까');
INSERT INTO UNI_TEXT_TEST (ID, UNI_TEXT_FIELD, TEXT_FIELD) VALUES (5, '다른게' || chr(8204) || '뭘까', '다른게뭘까');
INSERT INTO UNI_TEXT_TEST (ID, UNI_TEXT_FIELD, TEXT_FIELD) VALUES (6, chr(8204) || '다른게뭘까', '다른게뭘까');
select * from UNI_TEXT_TEST;
TEXT와 REPLACE 한 값 비교 - 1
WITH RECURSIVE T AS (
SELECT
ID,
UNI_TEXT_FIELD,
TEXT_FIELD,
LENGTH(UNI_TEXT_FIELD) AS UNI_TEXT_FIELD_LENGTH,
LENGTH(TEXT_FIELD) AS TEXT_FIELD_LENGTH,
1 AS LEVEL,
ASCII(SUBSTRING(UNI_TEXT_FIELD FROM 1 FOR 1))::TEXT AS UNI_TEXT_FIELD_ASCII,
ASCII(SUBSTRING(TEXT_FIELD FROM 1 FOR 1))::TEXT AS TEXT_FIELD_ASCII
FROM
UNI_TEXT_TEST
UNION ALL
SELECT
ID,
UNI_TEXT_FIELD,
TEXT_FIELD,
UNI_TEXT_FIELD_LENGTH,
TEXT_FIELD_LENGTH,
LEVEL + 1,
UNI_TEXT_FIELD_ASCII || ' ' || ASCII(SUBSTRING(UNI_TEXT_FIELD FROM LEVEL + 1 FOR 1))::TEXT,
TEXT_FIELD_ASCII || ' ' || ASCII(SUBSTRING(TEXT_FIELD FROM LEVEL + 1 FOR 1))::TEXT
FROM
T
WHERE
LEVEL < GREATEST(LENGTH(UNI_TEXT_FIELD), LENGTH(TEXT_FIELD))
)
SELECT
ID,
UNI_TEXT_FIELD,
TEXT_FIELD,
UNI_TEXT_FIELD_LENGTH,
TEXT_FIELD_LENGTH,
MAX(UNI_TEXT_FIELD_ASCII) AS UNI_TEXT_FIELD_ASCII,
MAX(TEXT_FIELD_ASCII) AS TEXT_FIELD_ASCII,
CASE
WHEN UNI_TEXT_FIELD = TEXT_FIELD THEN 'EQUAL'
ELSE 'NOT EQUAL'
END AS COMPARISON_RESULT,
CASE
WHEN REGEXP_REPLACE(UNI_TEXT_FIELD, CHR(8203) || '|' || CHR(8204), '', 'g') = TEXT_FIELD THEN 'EQUAL'
ELSE 'NOT EQUAL'
END AS CLEANED_COMPARISON_RESULT
FROM
T
GROUP BY
ID,
UNI_TEXT_FIELD,
TEXT_FIELD,
UNI_TEXT_FIELD_LENGTH,
TEXT_FIELD_LENGTH
ORDER BY ID;
TEXT와 REPLACE 한 값 비교 - 2
-- id가 1,2인 데이터 단순 값 비교시 결과 : Y(같지않다)
select CASE WHEN (select uni_text_field from UNI_TEXT_TEST where id=1) != (select uni_text_field from UNI_TEXT_TEST where id=2) THEN 'Y' ELSE 'N' END AS TEST
from UNI_TEXT_TEST;
-- id가 1,2인 데이터 유니코드 replace 후 값 비교시 결과 : N(같다)
SELECT CASE WHEN REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
(SELECT uni_text_field FROM UNI_TEXT_TEST WHERE id = 1),
E'\u200B', ''), -- Zero Width Space
E'\u200C', ''), -- Zero Width Non-Joiner
E'\u200D', ''), -- Zero Width Joiner
E'\u00A0', ''), -- No-Break Space
E'\u200E', ''), -- Left-To-Right Mark
E'\u200F', ''), -- Right-To-Left Mark
E'\uFEFF', ''), -- Zero Width No-Break Space
E'\u2063', '') -- Invisible Separator
!=
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
(SELECT uni_text_field FROM UNI_TEXT_TEST WHERE id = 2),
E'\u200B', ''), -- Zero Width Space
E'\u200C', ''), -- Zero Width Non-Joiner
E'\u200D', ''), -- Zero Width Joiner
E'\u00A0', ''), -- No-Break Space
E'\u200E', ''), -- Left-To-Right Mark
E'\u200F', ''), -- Right-To-Left Mark
E'\uFEFF', ''), -- Zero Width No-Break Space
E'\u2063', '') -- Invisible Separator
THEN 'Y' ELSE 'N' END AS TEST
FROM UNI_TEXT_TEST;