inblog logo
|
spencer-tech
    Problem Solving

    이모지(emoji) 에러(Incorrect string value)

    UTF8MB4, MariaDB, Sequel Ace, Emoji
    Sang Un Lee's avatar
    Sang Un Lee
    Oct 08, 2024
    이모지(emoji) 에러(Incorrect string value)
    Contents
    1. 문제 상황2. 문제 원인 파악가설 1 : mariadb의 버전 차이로 인한 에러가설 2 : 이모지를 쿼리 파라미터로 파싱하는 과정에서 에러가설 3 : MariaDB Node.js Connector에서 이모지를 처리할 때 에러가설 4 : 문자집합 문제3. 문제의 정확한 원인4. 해결5. Sequel Ace에서는 에러가 나지 않았던 이유
     
    1. 문제 상황2. 문제 원인 파악가설 1 : mariadb의 버전 차이로 인한 에러가설 2 : 이모지를 쿼리 파라미터로 파싱하는 과정에서 에러가설 3 : MariaDB Node.js Connector에서 이모지를 처리할 때 에러가설 4 : 문자집합 문제3. 문제의 정확한 원인4. 해결5. Sequel Ace에서는 에러가 나지 않았던 이유

    1. 문제 상황

     
    • 사용자가 메모를 입력하는 API에서 이모지를 입력할 때 에러가 발생했음
      • 개발 서버에서는 에러가 발생하지 않았지만 프로덕션 서버에서 에러가 발생함
      • 에러(보안을 위해 데이터베이스와 테이블 및 컬럼 이름 등 구체적인 정보는 감췄음)
        (conn:512392, no: 1366, SQLState: 22007) Incorrect string value: '\xF0\x9F\x98\x80' for column `database_name`.`table_name`.`column_name` at row 1 sql: UPDATE database_name.table_name SET column_name = '😀', updated_by = :updatedBy, client_ip = :clientIp, ...
     

    2. 문제 원인 파악

     
    • 왜 이런 문제가 발생하는지 알지 못해서 가설을 세우고 문제를 해결했음
     

    가설 1 : mariadb의 버전 차이로 인한 에러

     
    • 우선 개발 서버와 프로덕션 서버에서 눈에 띄는 차이는 MariaDB의 버전 차이
      • ㅤ
        MariaDB 버전
        개발 서버
        10.11.7.
        프로덕션 서버
        10.11.9.
     
    • 따라서 MariaDB의 Change Log를 살펴보았지만 이모지와 관련된 변경사항은 확인할 수 없었음
     
    • 그리고 DB 클라이언트에서 쿼리로 직접 이모지를 넣었을 때, MariaDB 버전 10.11.7.과 10.11.9. 둘 다 잘 들어갔음
      • 실행한 쿼리
        UPDATE database_name.table_name SET column_name = '😀', updated_by = 'sp_procedure_name', client_ip = :clientIp updated_at = NOW()
        화면 캡쳐 : 이모지가 정상적으로 UPDATE 됨
        notion image
     
    • 따라서 DB의 차이보단 백엔드 서버에서 차이가 발생해서 에러가 발생했을 것이라고 생각함
      • 화면 캡쳐 : Swagger에서 이모지를 입력하려고 할 때 에러 발생
        notion image
        notion image
     

    가설 2 : 이모지를 쿼리 파라미터로 파싱하는 과정에서 에러

     
    • 해당 API는 쿼리 파라미터를 사용해서 사용자의 입력을 DB에 UPDATE하고 있음
      • 코드
        export const updateRecord = async (values: { recordIdx: number; description: string; updatedBy: string; clientIp: string }) => { return await query( ` UPDATE database_name.table_name SET column_name = :description, updated_by = :updatedBy, client_ip = :clientIp, updated_at = NOW() WHERE record_idx = :recordIdx `, values ); };
        export const query = async (sql: string, values?: any) => { const connection = await pool.getConnection(); try { if (Array.isArray(values)) { return await connection.batch(sql, values); // Bulk Insert } else { return await connection.query(sql, values); // Query } } catch (e) { throw e; } finally { connection.release(); } };
     
    • 쿼리에 직접 이모지를 넣었지만 같은 에러가 발생했음
      • 따라서 쿼리 파라미터로 파싱할 때 문제가 있기보단 Mariadb Node.js Connector에서 에러가 발생했다고 추정
     

    가설 3 : MariaDB Node.js Connector에서 이모지를 처리할 때 에러

     
    • MariaDB Node.js Connector에서 쿼리를 실행하는 코드를 확인
      • 코드 : connection-promise.js
        // connection-promise.js /** * Execute query using text protocol. * * @param sql sql parameter. Object can be used to supersede default option. * Object must then have sql property. * @param values object / array of placeholder values (not mandatory) * @returns {Promise} promise */ query(sql, values) { // sql: SQL 쿼리 또는 설정 객체를 나타냄. // values: 쿼리에 바인딩할 값들을 가진 객체 또는 배열 (선택 사항) // cmdParam에 sql과 values를 합쳐 SQL 쿼리 명령에 필요한 파라미터 설정 const cmdParam = paramSetter(sql, values); // cmdParam을 캡처하여 내부적으로 기록 this.#capture(cmdParam); // 새로운 Promise를 생성하여 쿼리를 비동기적으로 실행 // `this.#conn.query`를 `this.#conn` 컨텍스트로 바인딩해서 cmdParam을 사용하여 호출 return new Promise(this.#conn.query.bind(this.#conn, cmdParam)); }
      • query 함수는 DB에 연결된 소켓을 통해 쿼리를 전송 → 서버로부터 응답을 받으면 Promise를 통해 처리된 결과를 반환
      •  
    • MariDB Node.js Connector 설정 확인
      • 코드 : charset이 UTF8MB4라서 이상 없음
        import mariadb from 'mariadb'; const pool = mariadb.createPool({ host: process.env.DB_HOST, port: parseInt(process.env.DB_PORT), user: process.env.DB_USER, password: process.env.DB_PASS, database: process.env.DB_DATABASE, connectionLimit: parseInt(process.env.DB_POOL_SIZE), charset: process.env.DB_CHARSET || 'UTF8MB4' });
     
    • MariaDB Node.js Connector의 소스코드에서 이모지를 처리할 때 특별한 코드로 처리하지 않고 문자집합도 UTF8MB4라서 이모지를 처리하는데 문제가 없음
      • 따라서 Connector가 아닌 다른 곳에서 문제가 발생했다고 추정
     

    가설 4 : 문자집합 문제

     
    • MariaDB Node.js Connector, MariaDB의 문자 집합은 UTF8MB4인 것을 확인했음
      • DB의 문자 집합 : UTF8MB4
        notion image
      • 다른 문자 집합도 확인
     
    쿼리 : 시스템 변수들 중 문자 집합과 관련된 설정 조회
    SHOW VARIABLES LIKE 'character_set%';
    실행 결과 : MariaDB 서버의 문자 집합이 UTF8MB3인 것을 발견
    notion image
    • character_set_client : DB 클라이언트가 보낸 요청의 문자 집합
    • character_set_connection : DB 클라이언트 - DB 서버의 연결의 문자 집합
    • character_set_database : MariaDB의 특정 데이터베이스의 문자 집합
    • character_set_filesystem : 파일 시스템에서 사용되는 문자 집합
    • character_set_results : DB 서버가 DB 클라이언트에게 반환하는 결과의 문자 집합
    • character_set_server : MariaDB 전체 서버의 문자 집합
    • character_set_system : MariaDB 시스템 테이블의 문자 집합

    3. 문제의 정확한 원인

     
    • MariaDB Client(ex. MariaDB Node.js Connector) → MariaDB Server → MariaDB의 데이터베이스 순서로 쿼리가 실행됨
      • MariaDB Client는 백엔드 서버에서 설정으로 문자 집합을 UTF8MB4로 설정
      • MariaDB의 해당 데이터베이스는 문자 집합이 UTF8MB4임을 직접 확인함
      • 그러나 MariaDB Server의 문자 집합이 UTF8MB3라서 이모지를 넣을 때 에러가 발생함
     

    4. 해결

     
    • DB 구성은 인프라팀에서 관리함
      • ∴ 인프라팀에 요청해서 MariaDB Server의 문자 집합을 UTF8MB4로 변경
     

    5. Sequel Ace에서는 에러가 나지 않았던 이유

     
    • 문제의 원인을 찾을 때, Sequel Ace라는 DB Client로 이모지를 삽입하는 쿼리에서는 에러가 발생하지 않았음
      • MariaDB Server의 문자 집합이 UTF8MB3 이라서 에러가 났다면, Sequel Ace로 쿼리를 실행했을 때도 에러가 발생했어야함
      • Sequel Ace에서 에러가 발생하지 않아서 MariaDB의 문자 집합에는 문제가 없다고 잘못 판단했음
     
    • Sequel Ace는 MariaDB Server의 문자 집합이 UTF8MB3임에도 UTF8MB4로 덮어써서, 이모지를 삽입하는 쿼리를 실행했을 때 에러가 발생하지 않았음
      • 관련 Sequel Ace 코드
        /** * utf8mb3에 해당하는 Encoding이 없어서 utf8mb4가 반환됨 */ - (NSString *)mysqlEncodingFromEncodingTag:(NSNumber *)encodingTag { NSDictionary *translationMap = [NSDictionary dictionaryWithObjectsAndKeys: @"ucs2", [NSString stringWithFormat:@"%i", SPEncodingUCS2], @"utf8", [NSString stringWithFormat:@"%i", SPEncodingUTF8], @"utf8-", [NSString stringWithFormat:@"%i", SPEncodingUTF8viaLatin1], @"ascii", [NSString stringWithFormat:@"%i", SPEncodingASCII], @"latin1", [NSString stringWithFormat:@"%i", SPEncodingLatin1], @"macroman", [NSString stringWithFormat:@"%i", SPEncodingMacRoman], @"cp1250", [NSString stringWithFormat:@"%i", SPEncodingCP1250Latin2], @"latin2", [NSString stringWithFormat:@"%i", SPEncodingISOLatin2], @"cp1256", [NSString stringWithFormat:@"%i", SPEncodingCP1256Arabic], @"greek", [NSString stringWithFormat:@"%i", SPEncodingGreek], @"hebrew", [NSString stringWithFormat:@"%i", SPEncodingHebrew], @"latin5", [NSString stringWithFormat:@"%i", SPEncodingLatin5Turkish], @"cp1257", [NSString stringWithFormat:@"%i", SPEncodingCP1257WinBaltic], @"cp1251", [NSString stringWithFormat:@"%i", SPEncodingCP1251WinCyrillic], @"big5", [NSString stringWithFormat:@"%i", SPEncodingBig5Chinese], @"sjis", [NSString stringWithFormat:@"%i", SPEncodingShiftJISJapanese], @"ujis", [NSString stringWithFormat:@"%i", SPEncodingEUCJPJapanese], @"euckr", [NSString stringWithFormat:@"%i", SPEncodingEUCKRKorean], @"utf8mb4", [NSString stringWithFormat:@"%i", SPEncodingUTF8MB4], nil]; NSString *mysqlEncoding = [translationMap valueForKey:[NSString stringWithFormat:@"%i", [encodingTag intValue]]]; if (!mysqlEncoding) return @"utf8mb4"; return mysqlEncoding; } /** * 데이터베이스 연결의 인코딩을 설정 */ - (void)setConnectionEncoding:(NSString *)mysqlEncoding reloadingViews:(BOOL)reloadViews { ... // MySQL 연결에 인코딩을 설정하려고 시도 // 인코딩 설정에 실패할 경우, 에러 메시지를 로그로 출력하고 메서드 종료 // mysqlEncoding = utf8mb4 if (![mySQLConnection setEncoding:mysqlEncoding]) { NSLog(@"Error: could not set encoding to %@ nor fall back to database encoding on MySQL %@", mysqlEncoding, [self mySQLVersion]); return; } ...
     
    • Sequel Ace와 같은 DB Client가 DB Server의 문자 집합을 덮어쓰는 것은 일반적이라고 함
      • ∵ 데이터 일관성 및 호환성 문제 방지
    Share article
    Contents
    1. 문제 상황2. 문제 원인 파악가설 1 : mariadb의 버전 차이로 인한 에러가설 2 : 이모지를 쿼리 파라미터로 파싱하는 과정에서 에러가설 3 : MariaDB Node.js Connector에서 이모지를 처리할 때 에러가설 4 : 문자집합 문제3. 문제의 정확한 원인4. 해결5. Sequel Ace에서는 에러가 나지 않았던 이유

    spencer-tech

    RSS·Powered by Inblog