2019년 7월 26일 금요일

How to encode/decode base62 using C# and Create MSSQL CLR Object

개발을 하면서 base10, base26, base36, base62, base64등으로 encoding, decoding을 하는 경우가 있다. 그리고 많은 서비스에서 shorten url을 만들 때는 base62로 처리한다. base64로 처리할 경우 특수문자가 들어가기때문에 숫자+알파벳 조합인 base62를 선호한다.

Base62에서 표현가능한 값
- ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789

가령 어떤 페이지의 원래 주소는 A이며 이 Url이 너무 길어서 base62로 encoding하여 xxx.li/R04R(Shorten url)로 만들었다. 이때 사용자들에게 접속을 유도한 주소는 xxx.li/R04R이고 사용자가 저 주소를 통해 접속하면 서버에서는 R04R을 Decoding하여 6435131(key)를 알아낸다. 그리고 그 key를 DB에서 조회하여 Full Url인 A라는 주소를 찾아낸 후 사용자를 A 주소로 접속하게 한다.

이번 포스팅에서는 C#으로 숏URL을 만들때 사용할 수 있는 base62 인코딩, 디코딩을 구현하고 더 나아가 CLR Obect로 만들어서 DB에서 인코딩/디코딩을 하는 것을 테스트해본다.

자료 링크 : https://github.com/parksuseong/Base62



using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;

using System.Collections.Generic;
using System.Linq;
using System.Text;

public partial class StoredProcedures
{
    private static char Base62Digit(int d)
    {
        if (d < 10){
            return (char)('0' + d);
        }
        else if (d < 36){
            return (char)('A' + d - 10);
        }
        else if (d < 62){
            return (char)('a' + d - 36);
        }
        else{
            throw new ArgumentException("d");
        }
    }
    public static void EncodeBase62(SqlString n)
    {
        int n2 = int.Parse(n.ToString());
        var res = "";
        while (n2 != 0){
            res = Base62Digit(n2 % 62) + res;
            n2 /= 62;
        }
        SqlDataRecord record = new SqlDataRecord(new SqlMetaData("stringcol", SqlDbType.NVarChar, 128));
        record.SetSqlString(0, res);
        SqlContext.Pipe.Send(record);

    }
    private static int Base62Decode(char c)
    {
        if (c >= 'a' && c <= 'z'){
            return 36 + c - 'a';
        }
        else if (c >= 'A' && c <= 'Z'){
            return 10 + c - 'A';
        }
        else if (c >= '0' && c <= '9'){
            return c - '0';
        }
        else{
            throw new ArgumentException("c");
        }
    }
    public static void DecodeBase62(SqlString s)
    {
        SqlString res = "";
        string s2 = s.ToString();
        res = (s2.Aggregate(0, (current, c) => current * 62 + Base62Decode(c))).ToString();
        SqlDataRecord record = new SqlDataRecord(new SqlMetaData("stringcol", SqlDbType.NVarChar, 128));
        record.SetSqlString(0, res);
        SqlContext.Pipe.Send(record);

    }
}


이렇게 만든 프로그램을 실행하여 DLL을 만들었고 그 DLL을 활용하여 MSSQL CLR Object로 만들어보자.

인코딩용/디코딩용 프로시저를 각각 만들기로 한다.
먼저 ASSEMBLY를 생성하하고 각각의 프로시저에서 위에서 생성한 클래스의 알맞은 메서드들을 호출한다.


--drop proc USP_CLR_DECBASE62
--drop proc USP_CLR_ENCBASE62

-- Drop the assembly
IF EXISTS (SELECT * FROM sys.assemblies WHERE name = 'BASE62_CLR')
BEGIN
DROP ASSEMBLY BASE62_CLR
END
GO

-- Create the assembly
CREATE ASSEMBLY BASE62_CLR
FROM 'D:\mssqlClr\BASE62_CLR.dll'
WITH PERMISSION_SET = UNSAFE;
GO



CREATE PROCEDURE USP_CLR_DECBASE62 @STR NVARCHAR(MAX)
WITH EXECUTE AS CALLER AS EXTERNAL NAME BASE62_CLR.StoredProcedures.DecodeBase62
GO


CREATE PROCEDURE USP_CLR_ENCBASE62 @STR NVARCHAR(MAX)
WITH EXECUTE AS CALLER AS EXTERNAL NAME BASE62_CLR.StoredProcedures.EncodeBase62
GO



실제로 잘 동작하는지 확인해보자.

exec USP_CLR_DECBASE62 'R04R'


 --exec USP_CLR_ENCBASE62 '6435131'


잘 동작하는 것을 확인할 수 있다.


C# 코드, DB 스크립트, DLL 파일 다운로드 주소
https://github.com/parksuseong/Base62


2019년 7월 22일 월요일

JavaScript Print CSS Not Working

화면에서 프린트 기능을 구현했는데 글자들은 잘 나오지만 CSS가 안먹는 경우가 간혹 발생했다. 마크업된 CSS를 불러오지 못해 발생하는 문제로 판단했고 약간의 트릭으로 해결할 수 있었다.


아래는 구현된 화면이다.


이 화면을 출력하고자 다음처럼 코드를 구현했다.

코드는 다음과 같다.

function makeHtml(){
        const obj = {html : ''};
        let html =document.getElementById('tbl1').innerHTML;
        obj.html = html;
        return obj;
}

function reportPrint(param){
            const setting = "width=900, height=850";
            const objWin = window.open('', 'print', setting);
            objWin.document.open();
            objWin.document.write('<!DOCTYPE html>');
            objWin.document.write('<head>');
            objWin.document.write('<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0">');
            objWin.document.write('<title>iframe</title>');
            objWin.document.write('<link rel="stylesheet" href="<spring:eval expression="@environment.getProperty('web.jsp.cssPath')"/>/test.css"/>');
            objWin.document.write('</head>');
            objWin.document.write('<body id="contents_body" class="print">');
            objWin.document.write('<div id="test_wrap">');
            objWin.document.write('<div id="cont" class="test_wrap">');
            objWin.document.write('<div></div>');
            objWin.document.write('<div class="test_print_tab" style="margin-left: 30px; margin-right: 30px;>');
            objWin.document.write(param.html);
            objWin.document.write('</div></div></div></div></body></html>');
            objWin.focus();
            objWin.document.close();
            objWin.print();
            objWin.close();

}

function printClick() {
const completeParam = makeHtml();
    reportPrint(completeParam);
};


하지만 이렇게 구현된 출력 기능은 간혹 css가 먹지 않아 다음과 같은 문제가 발생했다.


출력 Window가 로딩될 때 css가 작동하지 않은 상태로 html이 그려진 것이므로 css의 문제임은 분명해보였다. reportPrint(Param) 내부에서 objWin.print(); objWin.close();에 약간의 트릭을 주는 것으로 문제를 해결할 수 있었다.

setTimeout(function() {
     objWin.print();
     objWin.close();
    }, 100);

위와 같이 print()와 close()에 타이머를 걸어줌으로써 css가 동작할 수 있도록 찰나(?)의 시간을 기다려주었고 다음과 같이 출력 Window가 잘 그려지게 되었다.



크롬에서 빈번히 발생하는 문제같고 stackoverflow에도 해당 문제를 겪는 사람이 꽤나 있었다.




2019년 7월 12일 금요일

Postgresql 대량 Insert하기 - Merge로 구현 (OLAP 성격의 테이블 집계하기)

Postgresql을 사용하여 OLAP성 대량의 데이터를 집계(Insert)해보자.

가장 쉬운 OLAP성 테이블을 집계하는 방법은 DELETE(TRUNCATE)-> INSERT일 것이다.
하지만 데이터의 공백이 발생하면 안되는 경우에는 Partition Switching을 통해 순간적으로 집계도 가능하다. 

예를 들어서 오라클에서 A테이블에 데이터를 넣어야 할 경우 다음과 같은 방식으로 처리할 수 있다.
1. A 테이블을 파티션으로 만들고, 동일한 스키마의 A_TEMP 테이블을 만든다.
2. A_TEMP 테이블에 먼저 데이터를 넣고 A의 파티션과 A_TEMP 테이블을 파티션 SWAP한다.
이 처럼하면 순간적으로 데이터를 바꿔치기하여 데이터의 공백을 없앨 수 있다.

하지만 Postgresql은 그런 Partition Switching 기능이 없다. 
그래서 가장 쉬운 Delete 후 Insert 방식을 쓰자니 데이터의 공백이 발생하는 위험이 있다.
Bulk Load 개념의 COPY 명령어가 있기는 하지만 개발할 때 신경쓸게 너무 많아진다. (드라이버 문제라던지, CSV 파일을 저장할 공간/주기 등)


결국 채택한 방법은 Merge이며 Upsert + Delete 형식의 로직을 Function(Procedure)으로 구현기로 하기로 하였다. 물론 ETL Tool(SSIS 등)로 Merge를 구현하는 방법도 있는데 필자는 써보지는 않았고 추후에 시도해보고 싶은 방법이다.

Postgresql Merge Function 구현하기
1. 원본테이블(Destination table)과 같은 스키마의 임시 테이블(Stage Table)을 만들고 그 곳에 데이터 적재하기
2. 원본 테이블에 lock table tbl_nm in exclusive mode 걸기
3. 원본 테이블과 임시테이블을 비교하여 필요없는 데이터 DELETE
4. 원본 테이블과 임시테이블을 비교하여 기존데이터 UPDATE
5. 원본 테이블과 임시테이블을 비교하여 신규데이터 INSERT

샘플은 다음과 같다.
Destination Table은 test_tbl이며 pk는 pk_col이다. 
Stage Table은 tmp_test_tbl이고 스키마는 동일하다.


CREATE OR REPLACE FUNCTION ufn_merge_test_tbl()
 RETURNS void
 LANGUAGE plpgsql
AS $function$
begin

lock table test_tbl in exclusive mode;

/* 삭제된 데이터 삭제  */
    delete
    from  test_tbl
    where not exists ( select 1
                       from tmp_test_tbl
                       where test_tbl.pk_col = tmp_test_tbl.pk_col ) ;

/* 기존 데이터 업데이트 */
    update  test_tbl
    set     pk_col = tmp_test_tbl.pk_col
          , col2    = tmp_test_tbl.col2
          , col3    = tmp_test_tbl.col3
          , col4    = tmp_test_tbl.col4
    from    tmp_test_tbl
    where   test_tbl.pk_col = tmp_test_tbl.pk_col ;

/* 신규 데이터 입력 */
    insert into    test_tbl (
            pk_col
  , col2
  , col3
  , col4
    ) select
            tmp_test_tbl.pk_col
          , tmp_test_tbl.col2
          , tmp_test_tbl.col3
          , tmp_test_tbl.col4
    from    tmp_test_tbl
    left join test_tbl
    on  test_tbl.pk_col = tmp_test_tbl.pk_col
    where test_tbl.pk_col is null;
 end;
$function$
;


이렇게 하면 데이터의 공백이 없이 운영 중에도 재집계가 가능하다. 하지만 역시 Merge는 속도면에서 매우 비효율적이다. DB 하드웨어가 어느 정도 받쳐줘야 대량 INSERT가 가능하다.

위에서 설명한 ETL 도구(SSIS)에서 MERGE를 구현하게 되면 다음과 같은 그림이 나온다.
사진 참조 - mssqltips site

즉 여러 단계의 Step으로 ETL을 그려야하며 ETL Tool에 Logic이 들어가서 조금 복잡해보이지만 Merge Function을 구현하는 것과 내용은 같다. 개인적으로 ETL에 Logic이 들어가면 복잡해보여서 선호하지 않지만 잘 보면 Step6의 Insert-Update-Delete가 병렬로 수행되어 Merge Function보다 성능이 좋을 것으로 보인다.

또한 Merge function 방식은 DB 서버에서 많은 리소스가 필요하고 ETL Merge 방법은 ETL Server에서 많은 리소스가 필요할 것이다.

결국 필자는 개발하기에 시간관계상, 그리고 유지보수 하는 입장에서 ETL보다는 아무래도 Script가 편하기 때문에 Function으로 구현하였지만 다시 처음부터 개발할 기회가 온다면 ETL로 Merge를 구현하는 것이 성능에 더 좋을 것 같다는 생각이 든다.

2019년 7월 9일 화요일

SQL Server Min/Max Memory 설정 변경하기

과거 DB서버(서버에 DB만 설치된 단독 DB서버)에서 이따금씩 배치 오류가 난 적이 있다.
서버 메모리가 65GB라면 DB메모리를 약  60GB 정도를 할당한 경우였다.

딱히 원인을 알 수 없었는데 MS 전문가의 도움을 받아 로그분석을 한 결과 특정 시간대에 OS의 메모리 부족 현상에 의해 배치가 중단되었고 원인은 MSSQL에서 호출하는 CLR Object로 밝혀졌다. CLR Object로 인해 OS가 사용할 메모리마저 부족했던 것이다.

즉시 MSSQL의 MIN/MAX MEMORY를 살짝 낮춰주었고 이후에는 재발하는 경우가 없었다.

아래 그림처럼 SSMS에서 서버 속성에 들어가서 변경할 수도 있다.


변환 스크립트는 다음과 같다.

-- 먼저 show advanced option을 1로 설정하면 고급 구성 옵션이 표시된다.
-- RECONFIGURE WITH OVERRIDE 옵션을 사용하면 시스템 재기동을 하지 않고 OVERRIDE가 가능하다.
EXEC SP_CONFIGURE 'SHOW ADVANCED OPTIONS', '1' RECONFIGURE WITH OVERRIDE
GO
-- 구성 확인
EXEC SP_CONFIGURE
GO

-- MIN MEMOERY 48GB로 변경
-- RECONFIGURE WITH OVERRIDE 옵션을 사용하면 시스템 재기동을 하지 않고 OVERRIDE가 가능하다.
EXEC SP_CONFIGURE 'MIN SERVER MEMORY (MB)', 49152
RECONFIGURE WITH OVERRIDE
GO

-- MAX MEMOERY 48GB로 변경
-- RECONFIGURE WITH OVERRIDE 옵션을 사용하면 시스템 재기동을 하지 않고 OVERRIDE가 가능하다.
EXEC SP_CONFIGURE 'MAX SERVER MEMORY (MB)', 49152
RECONFIGURE WITH OVERRIDE
GO

-- 먼저 show advanced option을 0로 설정하면 고급 구성 옵션을 닫는다.
-- RECONFIGURE WITH OVERRIDE 옵션을 사용하면 시스템 재기동을 하지 않고 OVERRIDE가 가능하다.
EXEC SP_CONFIGURE 'SHOW ADVANCED OPTIONS', '0' RECONFIGURE WITH OVERRIDE
GO
-- 구성 확인
EXEC SP_CONFIGURE
GO


아래 사진은 임시로 캡처한 것이며 min/max server memory의 config_value값을 보면 현재 7168MB로 설정되어있다. 실행값 run_value도 동일하다.
이를 SP_CONFIGURE 명령어로 변경이 가능하며 RECONFIGURE WITH OVERRIDE를 통해 DB 재시작없이 바로 적용이 가능하다.




2022년 회고

 올해는 블로그 포스팅을 열심히 못했다. 개인적으로 지금까지 경험했던 내용들을 리마인드하자는 마인드로 한해를 보낸 것 같다.  대부분의 시간을 MLOps pipeline 구축하고 대부분을 최적화 하는데 시간을 많이 할애했다. 결국에는 MLops도 데이...