메일 발송 서비스를 개발하기 위해서 검토를 진행하였습니다.

메일 발송과 같은 도메인(비지니스) 로직이 없고 복잡한 기술 중심의 기능이 아니면 우선순위가 낮아 일단 안전하게 실행만 된다는 생각으로 별도의 검토 없이 개발을 진행했습니다. 최근에는 마음의 여유가 생겨 우선순위가 낮은 부분에 대해서도 의미를 부여하고 부여된 의미만큼 결과에 만족하는 긍정적인 사이클이 형성되고 있는 것 같습니다.

기존에 해왔던 방식은 System.Net.Mail.SmtpClient을 사용하던 방식입니다. 사용방식이 어려운건 아니라 그냥 사용해도 무방합니다. 다른 방법이나 아이디어를 얻기 위해서 오픈 소스를 검색해보았습니다.


1번째로 살펴볼 FluentEmail의 사용하는 코드는 아래와 같습니다.

 // Using Razor templating package

Email.DefaultRenderer = new RazorRenderer();

var template = "Dear @Model.Name, You are totally @Model.Compliment.";

var email = Email
    .From("bob@hotmail.com")
    .To("somedude@gmail.com")
    .Subject("woo nuget")
    .UsingTemplate(template, new { Name = "Luke", Compliment = "Awesome" });

=> 사용이 편리해 보이지만 String.Format이나 $"{}"을 사용하면 동일하게 템플릿이 작성되는거 같아 잔기술 느낌이 납니다.

=> Fluent라는 이름이 붙으면 Method Chaining(.을 붙여서 연결하는 것)과 기존 클래스들을 랩핑하는 느낌인데 코드의 양이 엄청 줄거나 사용을 쉽게 하는 것외에 다른 핵심적인 내용이 없어 보입니다.


2번째로 살펴볼 오픈 소스는 PreMailer.Net 입니다. stylesheets에 선언된 내용이 inline style attribute로 추가해주는 라이브러리입니다. 일단 제가 원하는 것은 메일 발송이였기 때문에 거리는 있지만 이메일 양식과 관련된 내용이라 좀 더 살펴보았습니다.


[TestMethod]

public void MoveCssInline_RespectExistingStyleElement()

{

string input = "<html><head><style type=\"text/css\">.test { height: 100px; }</style></head><body><div class=\"test\" style=\"width: 100px;\">test</div></body></html>";


var premailedOutput = PreMailer.MoveCssInline(input, false);


Assert.IsTrue(premailedOutput.Html.Contains("<div class=\"test\" style=\"height: 100px;width: 100px"));

}

=> 해당 라이브러리의 기본 기능은 head 사이에 style이 div 태그 사이에 style에 적용되는 것입니다. 그 외에도 HTML태그에 대한 대문자 -> 소문자로 변경해준다던지 유효하지 않은 문법도 확인 할 수 있습니다. 그렇다면 이런 라이브러리는 왜 필요한 것일까요? 앞에 기능을 중심으로 생각해보면 외부 링크로 설정된 스타일이 포함된 경우 메일로 전송을 하게 되면 보안상(메일을 열었을 때 외부 링크에서 스타일을 받아와야 하므로) 좋지 않기 때문으로 추측됩니다.


3번째로 살펴볼 오픈 소스는 MailKit 입니다. 기존에 SmtpClient을 사용한 것이 아니라 RFC(표준)를 준수하며 크로스 플랫폼에서 사용이 가능하도록 구현한 라이브러리입니다. .NET에서 크로스 플랫폼 이야기가 나오면 .NET Core를 떠올리게 되는데 SmtpClient은 .NET Core에 포함되어 있지 않기 때문에 리눅스 환경에서는 MailKit을 사용하면 메일 발송을 할 수가 있습니다. 

MailKit에는 추가적으로 더 의미가 있는 점이 있는데 바로 성능이 우수하다는 것입니다. 성능이 우수한 이유를 PIPELINING 기술을 통해서 설명하고 있습니다.

https://tools.ietf.org/html/rfc2920


아래는 그림은 HTTP PIPELINING에 해당하는 그림으로 해당 용어에 대한 개념을 이해하기가 더 쉽습니다.


=> 왼쪽 그림은 요청에 대한 응답이 오기 전까지 다른 요청을 할 수 없는 구조이지만 오른쪽 그림은 요청과 응답에 대한 병렬처리가 가능하다는 것이 핵심입니다. HTTP PIPELINING은 HTTP 1.1부터 지원이 된다고 하며 자세한 내용은 아래 위키를 참고 하시면 됩니다.

https://en.wikipedia.org/wiki/HTTP_pipelining


결론적으로 크로스플랫폼 지원과 성능이 우수한 MailKit 을 사용하려고 하였는데 StackExchange.Exceptional에 예외 로그를 저장하면서 메일을 전송해주는 부분이 함께 있어 해당 부분을 사용하기로 결정하였습니다. 해당 부분은 예외 로그에 대한 메일 전송이고 메일 발송 서비스만을 위해서는 MailKit과 PreMailer.NET까지 고려해서 차후에 개발해볼 예정입니다.

P.S : 7월22일에 처음 썼던 글인데 게으름이 생겨 이제야 마무리를 했네요 ^^;


Posted by resisa
,

이번 글에 대한 제목을 정하기가 어렵네요.

상황 설명을 해보자면 DB테이블에 nvarchar 타입에 json 포맷으로 된 값이 있고 이 값을 API로 전달해주면 됩니다.

리턴되는 데이터는 아래와 같습니다.


 "{ \"Key\": 1 " }"

리턴되는 타입이 string이고 그 안에 키 또는 string 값에 "문자를 필요로 하기 때문에 \문자로 string안에 string을 구분해주고 있는 것입니다.

이렇게 리턴을 하였을 경우에도 JSON.NET에서는 클래스(object)로 역직렬화가 가능합니다. 하지만 다른 언어의 다른 json 직렬화 라이브러리의 경우에는 에러가 발생하기도 하며 API 명세서에 노출하기에도 깔끔하지 못해보입니다.


최초 DB에서 받아오는 json string에 대해서 클래스(object)를 만들면 간단하게 해결됩니다. 하지만 여기서는 json string에 대한 포맷이 주기적으로 변경될 수도 있고 그에 따른 변경사항에 대해서 약결합을 만들어야 하는 상황입니다.

다음으로 생각해볼 수 있는 건 API에서 string이 아닌 타입으로 넘기면 되지 않을까 싶었고 검색을 해보았습니다.


https://stackoverflow.com/questions/22906010/deserialize-dynamic-json-string-using-newtonsoft-json-net


ExpandoObject 라는 System.Dynamic 네임스페이스의 클래스가 있었네요. JSON.NET으로 아래와 같이 역직렬화를 해주고 있습니다.


var converter = new ExpandoObjectConverter();

dynamic obj = JsonConvert.DeserializeObject<ExpandoObject>(string, converter);

=> JSON.NET에서 ExpandoObjectConverter를 제공해주고 있습니다. 해결은 아주 간단하게 되었고 ExpandoObject를 살펴보니 IDictionary<string, object>를 상속받고 있습니다. 아하~ json string을 아래처럼 Dictionary로 만들어주고 리턴을 해도 되겠군요.


Dictionary<string object> dic = new Dictionary<string object>();

dic.Add("Key", 1);

=> 그렇다면 Dictionary가 아닌 ExpandoObject 사용하는 것에 대한 장점은 무엇일까요?

1. 복잡한 json string일 경우(특히 계층 관계)

2. ExpandoObject가 INotifyPropertyChanged 인터페이스를 상속받고 있는 걸로 봐서는 프로퍼티 변경에 대한 통지(알림)를 받을 수 있네요

3. 이벤트도 추가할 수 있습니다.


해결은 간단하게 되었고 직렬화에 대한 이해도 살짝 올라갔네요. ㅎㅎ

Posted by resisa
,

DeadLock 시리즈는 끝났지만 실제 프로젝트에서 경험을 통해서 알게된 내용과 해결 과정을 공유하려고 합니다. 이번 글 제목처럼 Insert구문만으로도 DeadLock이 발생하는 경우입니다. 하나의 테이블에 단순히 Insert만 한다고 발생하는 경우는 당연히 아닙니다. 실제 프로젝트에서의 내용을 공유할 수는 없어서 DeadLock이 발생하는 상황을 단순화하여 재현을 해보고자 합니다.

먼저 테이블은 아래와 같습니다.


/****** Object:  Table [dbo].[Master]    Script Date: 2017-07-20 오후 1:15:55 ******/

CREATE TABLE [dbo].[Master](

[MasterId] [bigint] IDENTITY(1,1) NOT NULL,

[MasterName] [nvarchar](50) NULL,

 CONSTRAINT [PK_Master] PRIMARY KEY CLUSTERED 

(

[MasterId] ASC

)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

) ON [PRIMARY]


GO

/****** Object:  Table [dbo].[Slave]    Script Date: 2017-07-20 오후 1:15:55 ******/

CREATE TABLE [dbo].[Slave](

[SlaveId] [bigint] IDENTITY(1,1) NOT NULL,

[MasterId] [bigint] NOT NULL,

[SlaveName] [nvarchar](50) NULL,

 CONSTRAINT [PK_Slave_1] PRIMARY KEY CLUSTERED 

(

[SlaveId] ASC

)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

) ON [PRIMARY]


GO

ALTER TABLE [dbo].[Slave]  WITH CHECK ADD  CONSTRAINT [FK_Slave_Master] FOREIGN KEY([MasterId])

REFERENCES [dbo].[Master] ([MasterId])

GO

ALTER TABLE [dbo].[Slave] CHECK CONSTRAINT [FK_Slave_Master]

GO

=> 테이블은 2개입니다. Master, Slave라는 테이블이고 1:N의 관계를 가지는 구조입니다.

=> 노란색으로 표시한 부분이 외래키(FK) 관련 설정입니다.


DeadLock 상황을 설명하기 위한 표입니다.


 시간순서

 프로세스1

 프로세스2 

 비고

 1

 Insert : Master 테이블

 

 

 2

 

 Insert : Master 테이블

 

 3

 

 Select : Slave FK 관계 확인

 Insert : Slave 테이블 전에

 4

 Select : Slave FK 관계 확인 

 Insert : Slave 테이블 전에

=> DeadLock이 2개의 프로세스에서 Master테이블에 X, S Lock이 순환되어 발생합니다.

=> Lock 관점에서 보면 X, S Lock 호환이 되지 않는 Insert Select 구문으로 인한 것이며 여기서 Select에 해당 하는 부분이 FK 관계에 대한 확인이라는 점입니다. FK 확인 때문에 Master에 S Lock으로 블럭이 발생한다는 점이 의문입니다.

=> 어쨌든 FK와 관련된 것으로 보여 FK를 삭제하니 해결은 되었습니다. 일반적으로 테이블에 대한 관계, 제약조건을 빼고 하는 경우도 실전에서 은근히 많이 보게되어서 잠시 접어두었습니다. 

=> 고도화 하는 과정에서 다시 해당 내용을 살펴보았는데 DeadLock graph 내용을 보아도 특이한 점을 발견할 수가 없었습니다. 

=> SQL Server DeadLock 3편(http://resisa.tistory.com/187)에서 소개해드렸던 테스트 코드에서 특이한 점을 발견하였습니다.


Task.WaitAll(Enumerable.Range(0, 3).Select(t =>

   Task.Run(() =>

    {

        // 비지니스 로직

    })).ToArray()); 

=> 일반적인 상황에서는 2개만 돌려도 DeadLock이 발생하였는데 2개에서는 나오지 않고 3개 이상이 될 경우에 발생하였습니다. 다음으로 SP를 2개의 세션에서 커밋하지 않고 실행을 하니 잠금이 발생하는 부분이 있다는 것을 알 수 있었습니다. 잠금이 발생한다는 상황이 이해는 되지 않았지만 Select에서 잠금이 발생하고 3번째 프로세스에서 DeadLock이 발생하는 것 같다는 추측 정도는 되었습니다.


잠금이 발생하는 부분과 이유는 무엇일까 확인을 해보기 위해서 SP의 내용을 확인해보았습니다.


BEGIN TRAN


-- 실제로는 파라미터로 TVP로 받았지만 테스트를 위해서 임시 테이블로 가정

-- 1. Master테이블에 넣을 데이터

DECLARE @MasterTemp AS TABLE

(

MasterName nvarchar(100) NOT NULL

)


INSERT INTO @MasterTemp VALUES (N'resisa')


INSERT INTO [dbo].[Master]

SELECT T.MasterName

FROM @MasterTemp AS T


DECLARE @MasterId bigint = CAST(SCOPE_IDENTITY() AS bigint)


-- 2. Slave에 넣을 데이터

DECLARE @SlaveTemp AS TABLE

(

JoinId bigint NOT NULL,

SlaveName nvarchar(100) NOT NULL

)


INSERT INTO @SlaveTemp VALUES (1, N'sisatoss')

INSERT INTO @SlaveTemp VALUES (2, N'resisa1982')


DECLARE @SlaveTemp2 AS TABLE

(

JoinId bigint NOT NULL,

MasterId bigint NOT NULL

)


INSERT INTO @SlaveTemp2 VALUES (1, @MasterId)

INSERT INTO @SlaveTemp2 VALUES (2, @MasterId)


-- 3. SlaveTemp와 SlaveTemp2를 조인하여 Slave 테이블에 데이터를 넣어준다.

INSERT INTO [dbo].[Slave] (SlaveName, MasterId)

SELECT T1.SlaveName, T2.MasterId

FROM @SlaveTemp AS T1

INNER JOIN @SlaveTemp2 AS T2

ON T1.JoinId = T2.JoinId


ROLLBACK TRAN

=> 문제의 원인이 될것이라고 생각한 부분은 3번 주석의 Slave에 데이터를 넣기 위해서 2개의 임시 테이블을 조인하고 그 결과를 넣어주는 부분이였습니다. 그래서 INNER JOIN을 사용하지 않고 실행을 해보니 DeadLock이 사라졌습니다;;

=> 왜 이러한 현상이 발생하는 알기 위해서 예상실행 계획을 살펴보았습니다.


1. 잠금 현상 없음



2. 잠금 발생


=> 차이점이 보이시나요? 결국 Loop Join과  Merge Join에 대한 차이점으로 보입니다.

=> 잠금이 발생하는 상황을 유추해보면 Insert Select INNER JOIN에서 Sort로 인하여 Master테이블(아마 PAG)에 S Lock을 획득하려고 하기 때문입니다. Loop Join일 경우에는 Master테이블에 KEY에 S Lock을 획득하려고 합니다. 결국 차이점은 FK에 대한 Select에 대하여 어떤 리소스에 S Lock를 획득하려고 하느냐이며 전자의 경우에는 다른 트랜잭션에서 접근하는 리소스에 S Lock을 획득하려고 하기 때문에 블럭이 발생하는 것입니다.

=> 리소스를 PAG라고 예상하는 이유는 Slave Insert구문에 WITH(PAGLOCK) 힌트를 주면 해당 문제가 해결되기 때문입니다. 하지만 당연히 성능이 느려지기 때문에 첨언 정도로 봐주시면 될 것 같습니다.


정리를 해보면 아래와 같습니다.

- 상황 : Insert 구문으로 인한 DeadLock

- 조건

1. 2개의 테이블이 관계(FK)를 가지고 있다.

2. Slave 테이블에 Insert하는 구문에 Join 구문을 사용한다.

- 해결

1. Slave 테이블에 Insert하는 구문에 조인 힌트(OPTION (LOOP JOIN))를 사용한다.

2. Slave 테이블에 Insert하는 구문에 Join을 사용하지 않는다. (임시테이블을 사용하여 단순히 Insert한다)

3. FK 제약 조건을 제거한다.


조인을 할 경우에 OPTION (LOOP JOIN)처럼 힌트를 줄 수 있다는 것을 배웠습니다. 하지만 역시나 힌트는 정확하게 알고 써야한다는 것과 SQL 옵티마이저가 해당 임시테이블에 대한 조인 구문에서 Merge Join 비용이 Loop Join보다 높음에도 불구하고 Merge Join으로 판단한 근거(아니면 다른 이유 때문에???)는 무엇일까를 생각하며 잘 볼줄은 모르지만 예상 실행 계획을 통해서 추측을 해보니 DB 전문가?!가 된 느낌입니다. ㅎㅎ

Posted by resisa
,