반응형

office 문서의  관리 / 편집을 프로그램으로 하기 위해서는 vb나 c#을 이용해야하죠. 특히 다음과 같은 마소의 본연 라이브러리로 하는데요, 기술의 발달로 .Net Framework 가 아닌 .NET core를 넘어 .NET Stardard에서 

 

Microsoft.Office.Interop.Word

Microsoft.Office.Interop.Excel

Microsoft.Office.Interop.Powerpoint

Microsoft.Office.Interop.Outlook 

의 com api를 사용하면 다음과 같은 에러메시지를 맞게 되었죠.

 

일단 이유에 대해서 설명드리겠습니다. 윈도우 전용이었던 .Net Framework 에서 cross platform을 추구하는 .NET core, .NET Standard로 넘어오면서 기본 세팅하에서는 com api를 인식할 수 없게 되었어요. com api는 윈도우에만 존재하기 때문이죠.

 

구글링했을때는 ".net core에서는 사용할 수 없다." 라고 기술되어 포기했었는데, 직접참조하면 된다고해서 도전을 다시 해봤죠. 역시나 안됐어요.

 

그렇다면 어떻게 세팅하면 사용할 수 있을까?

방법은 의외로 간단했어요.

 

platform 세팅을 any 에서 window로 바꾸는 것이죠.

 

1. 솔루션 바로 하단의 어플리케이션을 우클릭 

2. 애플리케이션 -> 일반 

 

3. 대상 OS를 찾아 기본값인 (없음)을 클릭

 

4.Windows를 클릭

 

5. 솔루션 내 종속성 우클릭 후 COM 참조 추가

(Nuget으로 microsoft.office.interop 라이브러리를 추가하면 에러가 나던데, 성공하신분 연락 또는 댓글로 방법 남겨주시기 바랍니다!!)

 

6. 필요한 라이브러리 추가 ( 스크린샷 예제에서는 Microsoft.Office.Interop.Word 를 사용하기 위해 word com object를 추가)

 

word com 객체를 추가하였고 이제 using Microsoft.Office.Interop.Word를 사용하면 라이브러리 사용이 가능하답니다.

 

 

주의할점!!!

1. 플랫폼을 바꾸면 어떻게 되느냐 - 당연히 지정한 플랫폼에서만 사용가능합니다. window10/11 등의 window os가 설치된 PC 또는 window server에서만 사용이 가능한 것이죠.

 

2. 위 예제에서 Microsoft Word 16.0 Object Library가 안보이시는 분들 특히, 이 com 객체를 추가하여 라이브러리를 사용하기 위해서는 개발된 어플리케이션이 설치될 pc에 office가 설치되어 있어야 합니다. 

 

3. 이 포스팅을 업로드하는 시점까지 확정된 microsoft의 com api 지원기간은 2029년까지로 알려져있습니다.

반응형
반응형

github에는 작성해놓았는데 블로그에는 이것조차 없네요. 머쓱

 

20241217 - 불필요부분을 뺀다고 뺐는데도 작성길이가 길어 현재 본문에서는

라이브러리 사용시 알아야할 내용과 Get방식만 설명드립니다.

 

 

참고로 .NET Standard 6.0 부터 사용해보았기 때문에

이전 버전은 다르던데 어떻게하는거냐라고 물으신다면 저도 모를꺼예요.

특히 System.Net.Http.Json는 Newtonsoft.Json이 .NET Standard에 와서 System.Text.Json으로 내장되면서 생긴 것으로 알고 있어서, 이전 버전에 없을 확률이 높습니다.

 

 

 

HTTP기초를 먼저 작성하면 좋았을텐데,,, 이 것 부터 쓰게 되네요.

우선 다른데서 공부하고 오셨다는 전제하에 작성해보겠습니다.

 

 

System.Net.Http / System.Net.Http.Json 라이브러리를 사용하기위해 근간에 되는 HttpClient의 인스턴스를 선언합니다.

예제에서는 단순하게 선언하겠습니다.

 

참고로 실제 서버나 프로그램에서 사용하시게되면 httpClientFactory에 넣어서 CreateClient로 가져오시면 매번 선언하지 않고 자원을 아끼면서 적절하게 사용하실 수 있을텐데요. 

 

여기서는 호출방법 자체에 포인트를 맞추고 있어 따로 다루지 않습니다.

(위 내용도 궁금하신 분들은 IHttpClientFactory 인터페이스를 참조하세요.)

 

HttpClient httpClient = new(){
    // 기본 URL 설정
    BaseAddress = new Uri("https://api.example.com/"),

    // 타임아웃 설정 (예: 30초)
    Timeout = TimeSpan.FromSeconds(30)
};

 

 HTTP 메소드에 따라 의 용법도 조금씩 다릅니다.

 

HTTP 헤더 입력값
httpClient.DefaultRequestHeaders.Add("Key", "Value");

위와 같은 형태로 헤더를 입력합니다. 영어를 보시면 Default Request Headers에서 감이 오시는 분이 있을까요?

이렇게 헤더를 추가하게 되면, 같은 인스턴스의 httpClient를 호출할때 항상 헤더값에 입력되게 됩니다.

 

따라서 하나의 httpClient 인스턴스를 돌려쓰시면서, 헤더값을 다르게 해야한다면,

아래의 첫문단과 같이 Clear를 해줍니다.

 

항상 같게 해줘야한다면 아래의 if문과 같이 추가해주는 것이 안정적인 방법일 것입니다.

httpClient.DefaultRequestHeaders.Accept.Clear();
httpClient.DefaultRequestHeaders.Clear();

if (!httpClient.DefaultRequestHeaders.Contains("Key"))
    httpClient.DefaultRequestHeaders.Add("Key", "Value");

 

GET 방식

본문 상단에서 httpClient를 선언할때 baseAddress를 적어주었기 때문에 호출할때는 뒷부분(endpoint)만 작성하면 됩니다.

 

.NET Standard에 오기전 구 버전에서는 HttpRequestMessage로 작성하여 이것저것 넣어줘야 했다면, 비동기 방식의 System.Net.Http 메소드는 사용이 아주 간편합니다.

string endpoint = "/class/students"
HttpResponseMessage response = await httpClient.GetAsync(endpoint);

 

요청이 성공했는지 보는 방법은 단순합니다.

response.StatusCode에 HTTP status code가 반환됩니다.

 

 

System.Net.Http에서 제공하는 enum type의 HttpStatusCode 중 일부를 정리해보았습니다.
HttpStatusCode.{아래 알파벳}으로 사용하시면 되겠습니다.

다만 목록의 코드별 내용을 자세히 설명하기에는 너무 길어 예제에서 사용하는 것들 정도만 언급드리겠습니다.


ex) if (response.StatusCode ==HttpStatusCode.OK)
또는 if (response.StatusCode == 200)

200번대
OK = 200, Created = 201,  Accepted = 202, NoContent = 204, ResetContent = 205
   
300번대
Ambiguous = 300, Moved = 301, Found = 302, Redirect = 302, RedirectMethod = 303,  Unused = 306,        TemporaryRedirect = 307, PermanentRedirect = 308

400번대
BadRequest = 400, Unauthorized = 401, Forbidden = 403, NotFound = 404, MethodNotAllowed = 405,        NotAcceptable = 406, RequestTimeout = 408, RequestEntityTooLarge = 413, RequestUriTooLong = 414,        UnsupportedMediaType = 415,      

500번대
InternalServerError = 500, NotImplemented = 501, BadGateway = 502, ServiceUnavailable = 503,        GatewayTimeout = 504,

 

Get 방식의 http method는 정보의 조회 등인 경우가 많아 대부분 200번 즉 response.StatusCode 가 HttpStatusCode.OK일 거예요. 다만, 구체적으로 반환되는 HTTP Status Code는 API를 제작한 개발사/개발자에 문의 또는 설명문서를 참조하여 정하시는 것이 좋겠습니다.

 

다만, 200번대가 보통 성공을 나타낸다고 약속되어져 있습니다. (제가 약속한거 아닙니다.)

따라서 다음과 같은 방법으로 200번대면 성공이라고 가정, 아니면 실패를 예외를 던지는 속성과 함수도 존재합니다.

// response.IsSuccessStatusCode: response가 200번 대면 true를 반환하는 bool값

if(response.IsSuccessStatusCode) {
// 성공
}



// if문도 필요 없이 200번대면 다음 레코드를 읽고, 200번대가  아니면 예외를 던지는 함수
response.EnsureSuccessStatusCode();

 

응답이 성공했으면 원하는 응답을 읽어와야하고, 그 방법에는 여러가지가 있을텐데요. 두가지를 설명드리겠습니다.

 

1. 응답을 모두 string으로 받아오는 방법

c#의 함수나 속성명은 상당히 직관적입니다.

[응답][내용][문자열]으로 불러오겠습니다.

(response)(content)(string)

var responseContent = await response.Content.ReadAsStringAsync();

 

 

그런데 대부분은 string 자체를 사용하진 않을 것입니다. 약속된 API라면 반환값이 json형태의 문자열이기 때문이죠.

따라서 json형태의 문자열을 다음과 같이 클래스로 바꿔줍니다.( System.Text.Json 필요)

var result =  JsonSerializer.Deserialize<ResultClass>(responseContent, new JsonSerializerOptions());

 

JsonSerializerOptions 옵션 등과 같은 System.Text.Json의 고유 기능은 제가 자세히 설명해놓은 글

(링크:  https://pichen.tistory.com/52 ) 을 참조하시기 바랍니다.

 

[.NET 라이브러리] System.Text.Json

안녕하세요. 예전에 NewtonSoft.Json 라이브러리를 설명드렸었는데요. 이번엔 System안에 속한 Json 컨버팅 라이브러리를 설명드리도록 하겠습니다. NewtonSoft.Json( [c# dotnet nuget] Newtonsoft.Json (tistory.com) )

pichen.tistory.com

저의 경우 NewtonSoft.Json은 .Net Framework에서는 많이 사용했지만. Net Standard에 와서는 내장된 System.Text.Json을 적극 사용하고 있습니다. 특히 아래 2번 방법과 JsonSerializerOptions 을 공유하고 있기 때문에 사용하시는 것을 권장드립니다.

 

 

 

2. 응답내용을 불러올때 클래스로 만드는 방법입니다. (using System.Net.Http.Json 필요)

ResultClass result = await response.Content.ReadFromJsonAsync<ResultClass>();

저는 보통 클라이언트 호출부를 함수로 만들어

return await response.Content.ReadFromJsonAsync<ResultClass>();

형태로 사용하거나 var로 선언하긴하지만, 여러분에게 명시적으로 보여드리기 위해 클래스로 선언부를 작성하였습니다.

개발 초기에는 응답내용을 전부 로그를 찍기 위해 1번으로 시작할때도 있지만, 주로 2번을 사용하고 있습니다.

ReadFromJsonAsync의 정의를 보면, System.Text.Json의 JsonSerializerOptions 옵션을 공유합니다.

null처리, 기본값 처리 등을 손쉽게 할 수 있고, 그에 따라 api의 상세한 요구조건을 컨트롤 할 수 있으니 반드시 알아두시면 좋겠습니다.

 

ResultClass가 무엇인가요? 라고 물으실 수 있을 것 같아요. 아무렇게나 작성한 임의의 클래스입니다.

{"number": 1} 이라는 형태의 json이 왔고 약속된 형태라면,

ResultClass은 아래와 같이 정의가 되었을 것입니다.

class ResultClass{

	public int Number {get; set;}

}

// 또는

class ResultClass{

	[JsonPropertyName("number")]
	public int Sequence {get; set;}
    
}

 

serialize했을때 json이 되는 클래스를 만들었다라고 보시면 되겠습니다.

이 포스트는 json, json과 클래스의 관계에 대한 설명글이 아니기 때문에 이 정도만 작성하도록하고, 여태까지 설명 내용을 한 번에 보실 수 있도록 담아보겠습니다.

 

// json문자열을 역직렬화할 Model 클래스
class ResultClass{

	[JsonPropertyName("number")]
	public int Sequence {get; set;}
    
}


// HttpClient 선언
HttpClient httpClient = new(){
    BaseAddress = new Uri("https://api.example.com/"),
    Timeout = TimeSpan.FromSeconds(30)	// 굳이 적지 않아도 되나 기본값이 30초
};

// 필요시 헤더 추가
if (!httpClient.DefaultRequestHeaders.Contains("Key"))
    httpClient.DefaultRequestHeaders.Add("Key", "Value");

// Get 방식 호출
HttpResponseMessage response = await httpClient.GetAsync("/class/students");

// 성공 여부 검증
response.EnsureSuccessStatusCode();

// 성공시 json을 역직렬화하여 변수에 담는다.
var result = await response.Content.ReadFromJsonAsync<ResultClass>();
반응형
반응형

폴더 형태의 구조를 담은 DB 또는 그러한 형태를 반환하는 API를 경험할 일이 많아졌다.

 

[상위폴더 여부 | 상위 폴더 Id | 폴더 Id | 폴더 이름]

 

간추려 위와 같은 컬럼이 있는 DB가 있거나,

유사형태의 값을 반환하는 API를 만났다.

 

DB만 있었다면 DB에서 recurrsive 테이블로 재귀로 뽑아 내는 것도 방법이겠지만

API를 사용해야만 하는 시점에서는 위 형태의 반환 값을 폴더구조로 풀어내는 것에 대한 고민이 있었다.

 

(공부 뒤 블로그나 github에 올리지 못한 내용이 더 많아서 공부를 뜨문뜨문한다고 보실 분들도 있겠지만...

필자는 계속 책을 구매하거나 인강을 듣거나 아주 작은 단위의 프로젝트를 혼자 진행하며

공부하고 찾아보면서 학습중이었다는 점....)

 

 

그러다 DFS를 여기 적용할 수 있겠다는 생각이 들었다.

이 포스트에서는 그러한 예제를 풀어가려한다.

 

 

재직회사에서 C#을 쓰고 있어 C#으로 개발하였으니 다른언어를 하셨더라도 원리로 가져가면 될 것 같다.

 

 

특히,

폴더명으로 검색하기에는

a \ b

a \ c \ b 

의 경우 처럼 이름은 같지만 다른 위치에 있는 것을 프로그램은 구분해서 조회할 수 없기 때문에 폴더구조로 뽑아오거나 id를 뽑아오는 것은 매우 필연적이기 떄문에 이 작업이 필요할때가 많다.

 

 

먼저 아래와 같은 클래스를 작성했다.

DB에서 4개 컬럼을 읽어다 넣었거나, API 반환값이거나, 혹은 읽거나 받은 정보를 여기에 담았다고 가정하자.

class Folder
{
    public int ParentId { get; set; }
    public int Id { get; set; }
    public bool HasParent { get; set; }
    public string Name { get; set; }
    
    public string NameHierarchy { get; set; }
    public string IdHierarchy { get; set; }
}

 

 

Folder 클래스의 인스턴스에 HasParent, ParentId, Id, Name이 기입 되어있는 List가 있다고 가정하자.

(NameHierarchy, IdHierarchy는 폴더구조를 \ 구분자로 만들 필드로 처음엔 빈칸으로 시작한다고 가정한다.)

List<Folder> wholeFolders = new List<Folder>();

 

 

DFS 알고리즘을 적용하기 위해 시작 노드를 정리할 필요가 있다.

다만, 폴더구조를 로그를 찍었을때 직관적으로 더 잘 보이게 하기위해 visited는 생성하지 않는다.

 

HasParent 같은 친절한 필드 또는 컬럼이 없는 경우가 있겠지만 그런경우에는 parent id가 null이거나 0 등 특별히 최상위 폴더를 알 수 있는 조건이 있을 것이다. 여기서는 HasParent가 true가 아니면 최상위 폴더이다.

List<Folder> rootfolders = wholeFolders.Where(f => !f.HasParent)
				.ToList();

 

 

위에서 filtering한  인스턴스들을 Stack을 선언하여 노드로 push한다.

최상위 폴더는 폴더이름, 폴더 id 자체가 폴더구조 이므로 이 값을 Hierarchy류 필드에 넣는다.

Stack<Folder> stack = new Stack<Folder>();
foreach (var rootfolder in rootfolders)
{             
    rootfolder.IdHierarchy = rootfolder.Id.ToString();
    rootfolder.NameHierarchy = rootfolder.Name;
    stack.Push(rootfolder);
}

 

 

이제 루프를 돌린다.

첫 번째 순회에서는 root folder들을 stack에 쌓았으므로

가장 마지막 node를 pop하여, 전체 폴더 중에서 parent id가 root folder의 id와 동일한 폴더들을 찾아 리스트로 반환하게 된다.

그렇게 조회한 하위 폴더를 상위폴더\현재폴더 값으로 Hierarchy류에 입력하고 stack에 push한다.

다음루프에서는 방금 중 가장마지막에 push한 폴더를 꺼내 다시 한번 절차를 거치게 된다.

이렇게. 최상위폴더에서 최하위 폴더까지 순회 후 다음 노드로 넘어가게 된다.

Folder folder = new Folder();
while (stack.Count > 0)
{
    folder = stack.Pop();               
    foreach (var child in wholeFolders.Where(f => f.ParentId == folder.Id))
    {
        child.NameHierarchy = $@"{folder.NameHierarchy}\{child.Name}";
        child.IdHierarchy = $@"{folder.IdHierarchy}\{child.Id}";
        stack.Push(child);
    }
}

 

한번에 작성해보면

List<Folder> wholeFolders = new List<Folder>(); // 무언가 조회된 것이라 가정한다.

Stack<Folder> stack = new Stack<Folder>();
foreach (var rootfolder in wholeFolders.Where(f => !f.HasParent)) // 1회 순회 - IEnumerable사용
{             
    rootfolder.IdHierarchy = rootfolder.Id.ToString();
    rootfolder.NameHierarchy = rootfolder.Name;
    stack.Push(rootfolder);
}


Folder folder = new Folder();
while (stack.Count > 0)
{
    folder = stack.Pop();               
    foreach (var child in wholeFolders.Where(f => f.ParentId == folder.Id))  // 1회 순회 - IEnumerable사용
    {
        child.NameHierarchy = $@"{folder.NameHierarchy}\{child.Name}";
        child.IdHierarchy = $@"{folder.IdHierarchy}\{child.Id}";
        stack.Push(child);
    }
}


class Folder
{
    public int ParentId { get; set; }
    public int Id { get; set; }
    public bool HasParent { get; set; }
    public string Name { get; set; }
    
    public string NameHierarchy { get; set; }
    public string IdHierarchy { get; set; }
}

 

DFS를 폴더구조화에 적용한 예제였다. DFS가 나같은 비전공자에게는 한번에 와닿지 않으나, DFS의 개념을 이해는 못한체 외우고만 있다가 필요할때 써보려고하니 어떤 로직으로 어떻게 적용되는지 이해가 되어 기뻤던 개발이었다.

 

 

반응형
반응형

프로그램을 개발할때 많이 쓰는 것 중에는 사용자에게 어떤 알림메시지를 보내는 것입니다.

사용자에게 메시지를 보내는 방법은 크게

SMS(문자) 발송, 이메일 발송, 우리나라 기준 한정으로 카카오 메시지 발송이 있을 것입니다.

 

이번 포스트에서는 그중에서도 이메일 발송에 대해 알아보도록 하겠습니다.

 

대개 email을 발송하기 위해서는 

이메일을 발송하는 이메일 서버(SMTP)와 이메일을 발송할 이메일 계정이 필요합니다.

 

C#에서는

.Net 라이브러리 중 System.Net.Mail 라이브러리 내

 

SmtpClient 클래스로 SMTP와 연결하고,

MailMessage 클래스로 이메일을 작성할 수 있습니다.

다만 이 포스트에서는 메일서버 구축이 아닌 보내기에 초점을 두었기 때문에 SMTP서버의 구축 관련한 내용은 다루지 않겠습니다.

 

참고로 무료로 풀려있는 SMTP 서버들이라 하더라도 일정 갯수가 넘어가게 발송하게되면 상업적용도로 간주되어 결제를 요구하기도 하니, 상업적용도로 사용하실 분들은 SMTP서버를 제공하는 각 기업에 절차에 맞게 결제 등을 하여 사용하시면 됩니다. c# 개발자 또는 엔지니어라면 아마도 Microsoft, 한국에서는 역시 naver, 그리고 kakao도 daum을 인수했기 때문에 smtp서버가 있을 것이라 판단됩니다. 

 

SqlClient도 그렇고 호출을 담당하는 클래스들은 ~Client로 명명하는 것이 규칙으로 보입니다.

먼저, SmtpClient를 선언합니다.

    using SmtpClient SmtpServer = new(smtpAddress)
    {
        Port = smtpPort,
        EnableSsl = true,
        UseDefaultCredentials = false,
        DeliveryMethod = SmtpDeliveryMethod.Network,
        Credentials = new NetworkCredential(mailSender, mailSenderPassword)
    };

smtpAddress 자리에는 SMTP Server의 주소를 입력하면되고

smtpPort에는 SMTP Server의 열려있는 포트를 이용하면 됩니다. 보통 암호화 보안연결을 사용하면 465나 587, 사용하지 않으면 25가 기본값으로 알려져있습니다.(https가 443, http가 80 인 것과 동일하게 생각하시면 되겠습니다.)

SMTP서버를 직접 구축하신 경우에는 네트워크에 지정한 포트번호를 이용하시면 되겠습니다.

 

SMTP에 연결할 계정은 NetworkCredential() 클래스에 (계정, 비밀번호) 형태의 생성자가 있으므로, SmtpClient 클래스의 인스턴스 필드 중 Credentials로 입력해주면 됩니다.

 

SmtpClient는 IDisposabled이 구현되어있어서 선언부 앞에 using을 붙여주면 사용하지 않을때 메모리를 해체할 수 있습니다.

 

Visual Studio 기준으로 ctrl버튼을 누른채로 작성한 SmtpClient를 클릭하면, 해당 클래스를 모두 보실 수 있는데요.

이외에도 여러 필드가 있지만 포스트의 예제에서는 저 정도만 사용하도록 하겠습니다.

 

SMTP를 연결했으면 이제는 이메일을 작성해야겠죠.

    using MailMessage email = new()
    {
        From = new MailAddress(mailSender),
        Subject = emailSubject,
        BodyEncoding = Encoding.UTF8,
        IsBodyHtml = true,
        Body = "본문 내용",
    };

 

만일 받는이가 1명이라면 MailMessge(string from, string to)라는 생성자가 존재하기 때문에 아래와 같이 선언하는 방법도 있습니다.

    using MailMessage email = new(mailSender, mailReceiver)
    {
        Subject = emailSubject,
        BodyEncoding = Encoding.UTF8,
        IsBodyHtml = true,
        Body = "본문 내용",
    };

 

 

왜 선언부에 To, Cc, Bcc를 안적으셨냐고 물으실 수 있는데요, 여러명한테 보낼 수 있다보니 세 변수는 컬렉션으로 구성되어있어요. (class MailAddressCollection : Collection ) 한명한테만 보낸다면 모르겠지만, 여러명한테도 보낼 상황이 있는 작업물이라면 선언하고 추가하는 방식이 좋습니다.

email.To.Add(받을사람1);
email.To.Add(받을사람2);
email.Cc.Add(참조1);
email.Bcc.Add(숨은참조1);

 

실제로 사용하실때는 받을 사람 자체가 컬렉션에 있어서 루프문을 통해서 Add하는 방식으로 가게 되곤 합니다.

 

BodyEncoding을 UTF.8 로 해줘야 영어가 아닌 문자가 깨지는 것을 방지할 수 있고,

IsBodyHtml = true 를 사용하게 되면, <div></div> <p></p> 등 html을 이메일 본문 내에 사용하면 랜더링 해줍니다.

false를 사용하면 글자 그대로 나오게 됩니다.

 

이메일을 다 작성했다면 메일 보내기를 해야겠죠?

 

SmtpServer.Send(email);

비동기 방식의 SendMailAsync(MailMessage email) 도 존재하고, cancellationToken을 사용하신다면, SendAsync(MailMessage email, CancellationToken  cancellationToken) 도 있습니다.

 

저같은 경우에는 email Body에 html을 넣어서 테이블로 어떤 데이터를 정리해서 보내주는 형식으로 개발을 많이 해서 Body에 엄청난 노가다를 하곤 했었는데요. 여러분의 상황에 맞게 잘 활용하시길 바라겠습니다.

반응형
반응형

 

 

"[C#] 초성을 포함한 StartWith 함수 만들기" (링크: https://pichen.tistory.com/56 ) 글에서 ㄱ부터 ㅎ까지를 모두 직접 적어서 switch 돌리는 것에 계산식이 있지 않겠냐고 하는 반응을 보았습니다.

 

[C#] 초성을 포함한 StartWith 함수 만들기

개발자 중 누군가 초성을 포함하여 검색하는 방법을 물었는데, 시간이 남아서 개발을 했다가 초안이 맘에 안들어서 퇴근 후 공부를 마친 시간에 보완작업을 해보았습니다. 현재 다니는 회사에

pichen.tistory.com

 

결론적으로 말하면 1편에서 쓰는 방법을 쓰는 것이 성능면에서 월등해보입니다만, char에 대한 이해도를 높이기 위해 계산을 하는 과정을 보여드리려고 합니다. 먼저 각 글자들의 유니코드를 외우고 다니고 있지 않기 때문에 지난 글의 소스인

아래 이미지 부분을 복사하여 엑셀을 열어 붙여넣기합니다. 엑셀을 활용하는 방법은 다른 글 또는 검색으로 찾아보세요!

(이 글을 작성하는 24년 5월 9일까지는 엑셀 관련글은 블로그에 없긴 합니다.)

 

 

 

엑셀에 붙여 넣기 후 [텍스트 나누기] 기능으로 텍스트 중 유니코드가 궁금한 '' 들을 분리해냅니다.

 

그리고 나서, 컬럼만 복사 후 값으로 붙여넣기한 다음에 로그를 찍을 모양을 만들어 줍니다.

아래 이미지 처럼 만들때 당연하게 손으로 쓰는 부분은 L 컬럼 뿐이어야 엑셀을 쓰는 보람이 있습니다.

M열 중 첫행 하나의 값을 만들고 + 눌러서 누르면 됩니다.

참고로 M1의 셀에 입력한 값은 =$L$1&I1&$L$2&$L$3&I1&$L$4&$L$1&J1&$L$2&$L$3&J1&$L$4&$L$1&K1&$L$2&$L$3&K1&$L$2

이건데요, 실제로는 셀 클릭 -> & -> 셀클릭 방식으로 만든거예요. 열과행에 $가 붙는 것은 F4를 누르면 만들 수 있고 자동완성시 고정될 셀에 지정하면 됩니다.(L컬럼의 값들)

로그를 찍어보려고 하는거라 결국은 복사하여 IDE에 붙여넣기 합니다!

Console.Write($@"          요기에 붙여넣기 하세요!       ");

 

$@를 둘다 붙이는 이유는 $는 보간문자열을 이용하기 위해서고 (그래서 엑셀에서 굳이 {와 }를 붙여서 가져왔어요.

@를 안쓰면 문장 띄어쓰기할때마다

$" ~~~~ " +

$" ~~~~ " +

$" ~~~~ " 이렇게 붙여줘야하는데요, @를 쓰면 이 과정을 생략해도 일관된 값으로 처리됩니다! @의 다른 모든 기능들은 검색해보세요!

 

출력값은 이렇게 나옵니다.

 

이제 초성 검색을 위해 필요한 유니코드들을 알았죠!

유니코드간의 규칙을 찾기 위해 다시 엑셀로 붙여넣기 하고 앞서 설명드렸던 텍스트 나누기 - 기준 "공백"을 적용하여

숫자만 분리해낸 뒤, 제가 ㄲㄸㅃㅆㅉ를 제일 뒤에 적었어서, 자음의 유니코드 순서에 맞게 정렬합니다. 엑셀의 필터기능을 이용하면 쉬운데, 방법을 모르신다면 수동으로 정렬하셔도 좋습니다.

 

 

유니코드가 비어있는 부분을 혹시 캐치하셨나요? ㄲ과 ㄴ 사이 12595가 없고, ㄹ과 ㅁ 사이에 12602~12608이 없죠,

위에 로그를 찍던 방식으로 찍어보시면 알겠지만 ㄳ, ㄺ,ㄻ,ㄼ,ㄽ,ㄾ,ㄿ,ㅀ 로 초성으로는 올 수 없는 애들이 껴있어요.

가-힣에는 해당 초성이 받침 이외에서 쓰이는 글자는 없죠!

그래서 이 부분들만 남기면 약간의 규칙이 생긴답니다.

 

 

ㄱ 과 가 간에는 31439 차이가 있는데, 가 -> 까, 까 -> 나 등등 은 유니코드 차이가 정확히 588이라는 얘기입니다.

또한 가와 깋, 그러니까 ㄱ으로 시작하는 가장 첫 글자와 ㄱ으로 시작하는 가장마지막 글자의 차이가 587,

어떤 초성을 고르더라도 그 차이는 동일하게 587이라는 결과가 위 엑셀 캡처에서 드러났습니다.

 

따라서, char 'ㄱ'의 int 값 12593에 31439를 더하면 '가'의 int값이 되고, 여기서 587을 더하면 '깋'의 int 값이 됩니다.

char'ㄴ'의 int 값은, 12593 ('ㄱ') + 31439 + 588, 여기서 582을 더하면 '닣'의 int값이 됩니다.

 

이해가 되시나요?

 

이 내용을 c#에서 적어보면, 아래와 같습니다. 규칙이 보이시나요?

char gi_eok = 'ㄱ';
char ga = (char)((int)gi_eok + 31439);
char gih = (char)((int)ga + 587);


char ni_en = 'ㄴ';
char na = (char)((int)gi_eok + 31439 + 588); 
char nih = (char)((int)na + 587);

char di_geut = 'ㄷ';
char da = (char)((int)gi_eok + 31439 + 588*2); 
char dih = (char)((int)da + 587);

 

이 규칙을 토대로 초성일때는 초성이 들어가는 글자에서 모두 true를 리턴할 수 있도록 함수를 작성하였습니다.

bool IsChosung2(char OriginWordChar, char keywordChar)
{
    List<char> chosungs = new() { 'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ' };
    if (chosungs.Contains(keywordChar))
    {
        int startCharacter = (int)'ㄱ' + 31439 + 588 * chosungs.IndexOf(keywordChar);
        int endCharacter = startCharacter + 587;
        return startCharacter <= OriginWordChar && OriginWordChar <= endCharacter;
    }
    else
    {
        return OriginWordChar == keywordChar;
    }
}


///////////////////////////////////////////////////
// 참고로 지난 글에서 이부분의 함수는 아래와 같습니다.
bool IsChosung1(char OriginWordChar, char keywordChar) => keywordChar switch
{
    'ㄱ' => '가' <= OriginWordChar && OriginWordChar <= '깋',
    'ㄴ' => '나' <= OriginWordChar && OriginWordChar <= '닣',
    'ㄷ' => '다' <= OriginWordChar && OriginWordChar <= '딯',
    'ㄹ' => '라' <= OriginWordChar && OriginWordChar <= '맇',
    'ㅁ' => '마' <= OriginWordChar && OriginWordChar <= '밓',
    'ㅂ' => '바' <= OriginWordChar && OriginWordChar <= '빟',
    'ㅅ' => '사' <= OriginWordChar && OriginWordChar <= '싷',
    'ㅇ' => '아' <= OriginWordChar && OriginWordChar <= '잏',
    'ㅈ' => '자' <= OriginWordChar && OriginWordChar <= '짛',
    'ㅊ' => '차' <= OriginWordChar && OriginWordChar <= '칳',
    'ㅋ' => '카' <= OriginWordChar && OriginWordChar <= '킿',
    'ㅌ' => '타' <= OriginWordChar && OriginWordChar <= '팋',
    'ㅍ' => '파' <= OriginWordChar && OriginWordChar <= '핗',
    'ㅎ' => '하' <= OriginWordChar && OriginWordChar <= '힣',
    'ㄲ' => '까' <= OriginWordChar && OriginWordChar <= '낗',
    'ㄸ' => '따' <= OriginWordChar && OriginWordChar <= '띻',
    'ㅃ' => '빠' <= OriginWordChar && OriginWordChar <= '삫',
    'ㅆ' => '싸' <= OriginWordChar && OriginWordChar <= '앃',
    'ㅉ' => '짜' <= OriginWordChar && OriginWordChar <= '찧',
    _ => keywordChar == OriginWordChar
};

 

 

지난 글에서 작성한 부분 중 뒷부분에 대한 설명이라 앞부분은 그냥 가져왔습니다.

방금 작성한 IsChosung2()가 아닌 다른 부분이 궁금하시다면

지난글인 "[C#] 초성을 포함한 StartWith 함수 만들기" (링크: https://pichen.tistory.com/56 ) 를 참고해주세요!

//초성 검색 테스트
string keyword1 = "ㄱ나ㄷ";
string keyword2 = "aㄱ나";
List<string> words = new List<string> { "가나다", "가노", "가난", "가나두", "가나라", "고리", "기체", "깋체", "나다라", "다라마", "가나a", "a가나", "a가나다" };
List<string> list1 = words.FindAll(w => StartWithKeyword(keyword1, w));
List<string> list2 = words.FindAll(w => StartWithKeyword(keyword2, w));
Print(keyword1, list1);
Print(keyword2, list2);



void Print(string keyword, List<string> papers) => papers.ForEach(p => Console.WriteLine($"검색어: {keyword} - {p}"));

bool StartWithKeyword(string keyword, string word)
{
    if (keyword.Length > word.Length)
    {
        return false;
    }

    for (var i = 0; i < word.Length && i < keyword.Length; i++)
    {
        //bool charResult = IsChosung1(word[i], keyword[i]);
        bool charResult = IsChosung2(word[i], keyword[i]);
        if (!charResult)
        {
            return false;
        }
    }
    return true;
}



bool IsChosung2(char OriginWordChar, char keywordChar)
{
    List<char> chosungs = new() { 'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ' };
    if (chosungs.Contains(keywordChar))
    {
        int startCharacter = (int)'ㄱ' + 31439 + 588 * chosungs.IndexOf(keywordChar);
        int endCharacter = startCharacter + 587;
        return startCharacter <= OriginWordChar && OriginWordChar <= endCharacter;
    }
    else
    {
        return OriginWordChar == keywordChar;
    }
}

 

IsChosung함수의 길이는 짧아졌는데, 성능도 과연 그럴까?

정답은 아닙니다.

먼저 방금 작성된 함수는 숫자를 매번계산해야 합니다. => 따라서 성능을 개선하기 위해서는 변화하는 값이 아니기 때문에 미리 계산하거나 하드코딩으로 클래스로 초성, 시작글자, 마지막글자의 int나 char값을 작성 후 컬렉션에 넣어서 관리해야 첫번째 함수와 유사한 퍼포먼스를 낼 수 있을 것입니다.

 

그러나 이러한 개발과정과 검토가 개발실력에 많은 도움이 될 것이라 믿습니다. 감사합니다.

반응형
반응형

개발자 중 누군가 초성을 포함하여 검색하는 방법을 물었는데, 시간이 남아서 개발을 했다가 초안이 맘에 안들어서 퇴근 후 공부를 마친 시간에 보완작업을 해보았습니다.

 

현재 다니는 회사에서 사용하는 언어가 C#인데, 회사에 적용해도 좋을 것 같아서 C#을 사용했습니다.

 

일단 전체 검색대상 목록을 List<string>에 담았다고 가정하여, 

다음과 같은 List를 선언합니다.

List<string> words = new List<string> { "가나다", "가노", "가난", "가나두", "가나라", "고리", "기체", "깋체", "나다라", "다라마", "가나a", "a가나", "a가나다" };

 

그리고 검색어를 전체 구현이라면 앞단에서 받아야 겠지만, 콘솔을 통한 입력도 받지 않고, 선언을 하겠습니다.

 

검색어는 

string keyword1 = "ㄱ나ㄷ";
string keyword2 = "aㄱ나";

이렇게 두개로 하겠습니다.

 

컬렉션에서 어떤 조건을 만족하는 모든 값을 컬렉션으로 반환하는 함수인 FindAll()을 사용하도록 할텐데요,

words.FindAll(w => 어떤함수(w))로 사용할 어떤함수를 작성해야겠죠.

 

words.FindAll(word => 어떤함수( word )) 는 아래와 같은 기능을 한다고 이해하지면 됩니다. 실제로는 변수에 델리게이트를 사용하는 것이지만, 이해를 돕기 위해 작성한 것입니다.

List<string> 가짜FindAll(List<string> words){
    List<string> a = new();
    foreach(string word in words) 
    {
         if(어떤함수(word))
             a.Add(word)
    }
    return a;
}

 

그러면 어떻게 해야 초성에 맞는 한글들을 표현해낼 것인가를 고민해야겠죠.

 

우리는 char라는 자료형을 알고있죠. 유니코드로 숫자 범위로서 한글에 대한 범위를 지정해볼 수 있습니다.

따라서 검색어 중 어떤 글자가 'ㄱ', 'ㄴ' 등의 자음 초성일때, 가~깋, 나 ~닣가 포함되어있는지를 범위로 지정해볼 생각을 할 수 있습니다.

 

간단하게 검색어와 실제단어를 비교한다고 생각하고, n번째 글자가 ㄱ이면 원래 글자가 '가'와 '깋' 사이에 있는지 여부를 반환합니다.

if(검색어 중 n번째 글자 == 'ㄱ') {

  	원래 글자 중 n번째 글자 >= '가' && 원래 글자 중 n번째 글자 <= '깋'

}

 

받침 등이 자음마다 달라서 아주 일정한 규칙이 있는 것이 아니라서 이 작업을 ㄱ~ㅎ까지 모두 해주어야 합니다.

엑셀을 이용해도 됩니다.

 

그런데 우리는 switch 변수구문을 작성한 적이 있기 때문에 아래와 같이 작성합니다.

저 같은 경우는 ㄱ~ㅎ, 가~하, 깋~힣은 gpt에게 한글을 작성해달라고 하고, 엑셀에서 목록을 만들어서 다시 vs에 붙여넣기 했답니다. 초성이 아닌 경우에는 검색어와 원래 단어의 n번째 글자가 같으면 되기 때문에 _ => unicode == OriginChar로 작성하였습니다. OriginChar는 원래 목록에 있던 단어의 n번째 글자이고 unicode는 검색어의 n번째 글자입니다. 지금보니 변수명을 잘못지었지만, 이미 개발했으니 그대로 쓰겠습니다.

 

bool IsChosung(char OriginChar, char unicode) => unicode switch 
{
    'ㄱ' => '가' <= OriginChar && OriginChar <= '깋',
    'ㄴ' => '나' <= OriginChar && OriginChar <= '닣',
    'ㄷ' => '다' <= OriginChar && OriginChar <= '딯',
    'ㄹ' => '라' <= OriginChar && OriginChar <= '맇',
    'ㅁ' => '마' <= OriginChar && OriginChar <= '밓',
    'ㅂ' => '바' <= OriginChar && OriginChar <= '빟',
    'ㅅ' => '사' <= OriginChar && OriginChar <= '싷',
    'ㅇ' => '아' <= OriginChar && OriginChar <= '잏',
    'ㅈ' => '자' <= OriginChar && OriginChar <= '짛',
    'ㅊ' => '차' <= OriginChar && OriginChar <= '칳',
    'ㅋ' => '카' <= OriginChar && OriginChar <= '킿',
    'ㅌ' => '타' <= OriginChar && OriginChar <= '팋',
    'ㅍ' => '파' <= OriginChar && OriginChar <= '핗',
    'ㅎ' => '하' <= OriginChar && OriginChar <= '힣',
    'ㄲ' => '까' <= OriginChar && OriginChar <= '낗',
    'ㄸ' => '따' <= OriginChar && OriginChar <= '띻',
    'ㅃ' => '빠' <= OriginChar && OriginChar <= '삫',
    'ㅆ' => '싸' <= OriginChar && OriginChar <= '앃',
    'ㅉ' => '짜' <= OriginChar && OriginChar <= '찧',
    _ => unicode == OriginChar
};

 

이 작업을 검색어와 단어목록의 단어중 하나에서 글자단위로 진행해야하므로, foreach(char a in string값) 또는 for문에 대입하겠습니다.

 

여기서는 검색어와 단어를 index단위로 비교해야하므로, for문이 적절하고, 둘 중 짧은 곳에 맞추어 돌려야 하므로 조건에 둘다 작성합니다.

 

IsChosung의 값이 false이면 더이상 뒷부분을 확인하지 않아도 되므로, false를 반환하고 루프를 끝냅니다.

bool 어떤함수?(string keyword, string word){
    for (var i = 0; i < word.Length && i < keyword.Length; i++)
    {
        bool charResult = IsChosung(word[i], keyword[i]);
        if (!charResult)
        {
            return false;
        }
    }
    return true;
}

여기까지만 하면 문제가 발생하는데 찾으셨나요?

 

바로 검색어가 단어보다 길어도 앞부분까지만 매칭이 되면 true를 반환하는 문제가 생기죠.

 

그래서 앞에 길이를 비교하는 조건이 필요합니다.

bool 어떤함수?(string keyword, string word){
	if (keyword.Length > word.Length)
    {
        return false;
    }
    
    for (var i = 0; i < word.Length && i < keyword.Length; i++)
    {
        bool charResult = IsChosung(word[i], keyword[i]);
        if (!charResult)
        {
            return false;
        }
    }
    return true;
}

 

이제 출력의 시간입니다.

독자분들의 편의를 위해서 앞서 작성한 부분들도 기입하겠습니다.

 

string keyword1 = "ㄱ나ㄷ";
string keyword2 = "aㄱ나";
List<string> words = new List<string> { "가나다", "가노", "가난", "가나두", "가나라", "고리", "기체", "깋체", "나다라", "다라마", "가나a", "a가나", "a가나다" };
List<string> list1 = words.FindAll(w => StartWithKeyword(keyword1, w));
List<string> list2 = words.FindAll(w => StartWithKeyword(keyword2, w));
Print(keyword1, list1);
Print(keyword2, list2);


// foreach console.writeline 두번안쓰려고 선언한 함수
void Print(string keyword, List<string> papers) => papers.ForEach(p => Console.WriteLine($"검색어: {keyword} - {p}"));

bool StartWithKeyword(string keyword, string word)
{   
    if (keyword.Length > word.Length)
    {
        return false;
    }

    for (var i = 0; i < word.Length && i < keyword.Length; i++)
    {
        bool charResult = IsChosung(word[i], keyword[i]);
        if (!charResult)
        {
            return false;
        }
    }
    return true;
}

bool IsChosung(char OriginChar, char unicode) => unicode switch 
{
    'ㄱ' => '가' <= OriginChar && OriginChar <= '깋',
    'ㄴ' => '나' <= OriginChar && OriginChar <= '닣',
    'ㄷ' => '다' <= OriginChar && OriginChar <= '딯',
    'ㄹ' => '라' <= OriginChar && OriginChar <= '맇',
    'ㅁ' => '마' <= OriginChar && OriginChar <= '밓',
    'ㅂ' => '바' <= OriginChar && OriginChar <= '빟',
    'ㅅ' => '사' <= OriginChar && OriginChar <= '싷',
    'ㅇ' => '아' <= OriginChar && OriginChar <= '잏',
    'ㅈ' => '자' <= OriginChar && OriginChar <= '짛',
    'ㅊ' => '차' <= OriginChar && OriginChar <= '칳',
    'ㅋ' => '카' <= OriginChar && OriginChar <= '킿',
    'ㅌ' => '타' <= OriginChar && OriginChar <= '팋',
    'ㅍ' => '파' <= OriginChar && OriginChar <= '핗',
    'ㅎ' => '하' <= OriginChar && OriginChar <= '힣',
    'ㄲ' => '까' <= OriginChar && OriginChar <= '낗',
    'ㄸ' => '따' <= OriginChar && OriginChar <= '띻',
    'ㅃ' => '빠' <= OriginChar && OriginChar <= '삫',
    'ㅆ' => '싸' <= OriginChar && OriginChar <= '앃',
    'ㅉ' => '짜' <= OriginChar && OriginChar <= '찧',
    _ => unicode == OriginChar
};

 

결과는 다음과 같습니다.

 

영어를 포함해도 뒤에 초성이 바르게 검색되는 것을 확인했습니다.

 

 

 

반복되는 형태의 문장들인데 계산식으로 만들 수 없을까? 궁금하신 분들은 2편을 참고해주세요.

https://pichen.tistory.com/57

 

[C#] 초성을 포함한 StartWith 함수 만들기 2편, char의 유니코드 값으로 계산해보자.

"[C#] 초성을 포함한 StartWith 함수 만들기" (링크: https://pichen.tistory.com/56 ) 글에서 ㄱ부터 ㅎ까지를 모두 직접 적어서 switch 돌리는 것에 계산식이 있지 않겠냐고 하는 반응을 보았습니다. [C#] 초성

pichen.tistory.com

 

 

반응형
반응형

파일을 읽는 방법은 여러가지가 있다. 필자가 최근에 가장 많이 사용하고 있는 방법은 커스터마이징 한 것으로 다른 글에서 잠깐 다뤄 볼 예정이고, 

 

본문에서는 .json 파일과 .ini 파일을 읽는 방법에 대해 알아보자.

 

 

 

먼저 [.ini 파일을 읽는 방법]

 

 

아래와 같은 Location.ini 파일을 바탕화면에 생성한다.

[Lo_1]
name=Jeju
stationID=A
PID=1234

[Lo_2]
name=Busan
stationID=2
PID=7654

 

 

ini 파일은 아주 구시대적일 수 있는 정보파일로 보통은 시작할때 읽어들이는 정보를 담은 파일을 뜻한다.

 

using System.Runtime.InteropServices; 라이브러리를 사용하여

kernel32.dll을 import하고, 그중에서도 GetPrivateProfileString()을 사용한다.

 

이를 사용하기 위한 세팅은 다음과 같다.

using System.Runtime.InteropServices;

[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
static extern uint GetPrivateProfileString(
        string lpAppName,
        string lpKeyName,
        string lpDefault,
        StringBuilder lpReturnedString,
        uint nSize,
        string lpFileName
);

 

 

가져올때에는 ini 파일내 변수들을 List나 Dictionary에 넣어두는 것이 루프문을 통해 읽어올 수 있어서 편하다.

 

List<string> iniKeys = new ()
{
	"name",  "stationID", "PID"
};

 

 

 

설정파일은 바탕화면에 있는 것으로 가정하였으므로 파일경로는 

Environment.GetFolderPath를 사용해도 좋다.

string iniPath = Path.Combine(
	Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
    "Location.ini"
);

Path.Combine()은 파일경로를 \가 없으면 붙이고 있으면 그대로 잇는 등

자동으로 파일경로를 결합시켜주는 역할을 한다.

 

Environment.GetFolderPath()는 운영체제 환경에서 특정 폴더경로를 가져오는 함수로

Environment.SpecialFolder 내에 바탕화면(Desktop), 즐겨찾기, appdata 등 여러 경로가 존재하고 저는 그 중에서 바탕화면을 썼던 것이다.

 

 

해당 ini파일이 존재하면 읽어서 출력해보자.

만일 없는 변수가 있으면 Warning으로 보이게 하면 구분이 쉽다.

 

아래와 같은 방법 말고도 특정 변수에 집어 넣어야만 확실한 사용이 될 것인데, for문의 i와 foreach의 key 값에 따라 변수에 집어 넣으면 된다.

Log.Information("iniPath:" + iniPath);

if (File.Exists(iniPath))
{
    for (int i = 1; i <= 2; i++)
    {
        StringBuilder sb = new StringBuilder(255);
        foreach (var key in iniKeys)
        {
            uint result = GetPrivateProfileString($"Lo_{i}", key, "", sb, (uint)sb.Capacity, iniPath);
            if (result > 0)
            {
                Log.Information("Value: " + sb.ToString());
            }
            else
            {
                Log.Warning("Value: " + sb.ToString());
            }
        }
    }
}

 

 

kernel32.dll의 GetPrivateProfileString() 함수는

1. ini 파일 내 [와 ] 사이값

2. ini 파일 내  [] 바로 아래 변수명 값

3. ini 파일 내  읽을 변수에 값이 없을때 대체 값

4. StringBuilder 변수값

5. 글자의 길이 제한

6. ini파일의 경로

 

로 구성되어있다.

 

 

 

(참고) 제 포스팅을 읽으실때 Log.Information()을 사용하지 못하시는분은 없겠죠?

만일 있다면 Serilog 포스팅도 읽어보기를 추천한다.

 

다만 부수적이라 생각하고 계속 진행할 독자라면 여기서 사용한 방법만 간단하게 설명할테니 따라하면 된다. 

nuget 패키지로 Serilog, Serilog.Sink.File을 설치한 후 

 

소스 최상단에 아래 내용을 추가하면된다. Serilog에 대해서는 자세한 설명은 하지 않겠다.

using Serilog;

string appName = "only_for_test";

Log.Logger = new LoggerConfiguration()
    // .WriteTo.Console()
    .WriteTo.File(
        $"{Environment.GetFolderPath(Environment.SpecialFolder.Desktop)}/{appName}_Logs/{appName}_log-.txt",
        rollingInterval: RollingInterval.Day,
        retainedFileCountLimit: null,
        fileSizeLimitBytes: 50 * 1024 * 1024,
        rollOnFileSizeLimit: true,
        outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level}] {Message}{NewLine}{Exception}")
    .MinimumLevel.Is(Serilog.Events.LogEventLevel.Information)
    .CreateLogger();

 

 

 

 

이번엔 [.json 파일을 읽는 방법]

 

json을 읽는 방법은 아주 간단하다.

우선 바탕화면에 Location.json 파일이 다음과 같다고 하자.

ini 파일과 준위를 맞추고자 아래 처럼 작성하였다.

{ 
	"Lo_1": {
		"name":"incheon", 
		"stationID": "A", 
		"PID": "1234"
	},
	"Lo_2": {
		"name":"busan", 
		"stationID": "1", 
		"PID": "1256"
	}
}

 

파일 경로는 다음과 같이 가져온다.

string jsonPath = Path.Combine(
	Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
    "Location.json"
);

 

 

File.ReadAllText(string filePath) 로 파일내의 모든 문자를 받아와

System.Text.Json 라이브러리에 속한 json을 클래스로 Deserialize 해준다.

 

Deseiralize를 제네릭으로 바로 인스턴스로 만들 수 있도록, .json 정보에 맞게 클래스를 선언해준다.(아래)

그러면 소스 내에서 바로 사용이 가능하다.

 

if (File.Exists(jsonPath))
{
    try
    {
        // 파일의 내용을 문자열로 읽어옴
        string jsonString = File.ReadAllText(jsonPath) ?? "";
        var Locations = JsonSerializer.Deserialize<Dictionary<string, Location>>(jsonString);
        if (Locations is not null)
        {
            foreach(var key in Locations.Keys)
            {
                Log.Information($"{key}= {Locations[key]}");
                if (Locations[key] is not null)
                {
                    Log.Information($"{key}=> name: {Locations[key].Name}");
                    Log.Information($"{key}=> stationID: {Locations[key].StationId}");
                    Log.Information($"{key}=> PID: {Locations[key].Pid}");
                }         
            }  
        }
    }
    catch (Exception e)
    {
        Log.Error($"{e}");
    }
}


class Location
{
    [JsonPropertyName("name")]
    public string? Name { get; set; }

    [JsonPropertyName("stationID")]
    public string? StationId { get; set; }

    [JsonPropertyName("PID")]
    public string? Pid { get; set; }
}

 

 

설정파일을 읽는 방법들에 대해서 다뤄봤다. xml은 있는 것을 유지보수 할때 말고 직접 개발을 초기부터할때는 사용하지 않아서 다루지 않았다.

반응형
반응형

안녕하세요. 

예전에 NewtonSoft.Json 라이브러리를 설명드렸었는데요.

이번엔 System안에 속한 Json 컨버팅 라이브러리를 설명드리도록 하겠습니다.

 

NewtonSoft.Json( [c# dotnet nuget] Newtonsoft.Json (tistory.com) ) 글에 설명을 자세히 적어놨기 때문에

기본적인 시리얼라이즈, 디시시리얼라이즈 원리 자체는 동일하기때문에 여기서는 설명은 최소화 합니다.

 

[c# dotnet nuget] Newtonsoft.Json

이번에는 Newtonsoft.Json에 대해서 소개하도록 하겠습니다. System.Text.Json으로도 json데이터를 처리할 수 있지만 너무나 편리하고 기존에 만들어진 소스에서 많이 사용하고 있어서 사용하게 되었고

pichen.tistory.com

 

 - 인스턴스를 json형태의 문자열로 바꾸는 메소드(직렬화)

// 그냥
string jsonString = JsonSerializer.Serialize(어떤 인스턴스);

// 제네릭을 사용
string jsonString = JsonSerializer.Serialize<어떤 클래스>(어떤 클래스의 인스턴스);

 

 

- System.Text.Json에도 컨버팅 옵션이 존재합니다!

// 명확하게 써놓은것
var options = new JsonSerializerOptions { WriteIndented = true };
string jsonString = JsonSerializer.Serialize(인스턴스, options);

// 한줄로
string jsonString = JsonSerializer.Serialize(
	인스턴스, 
    new JsonSerializerOptions { WriteIndented = true }
);

    JsonSerializerOptions에 쓰일 수 있는 변수를 설명드리겠습니다. Newtonsoft.Json을 보고 오신분이라면 WriteIndented가 뭔지 감이 오실텐데요.

 

    WriteIndented =true :  를 설정하면 엔터등 공백을 포함하여 json문자열을 만듭니다.

 

    PropertyNameCaseInsensitive=true :  대소문자를 구분하여 비교를 사용합니다.

    Newtonsoft.Json에서는 대소문자를 구분하지 않아 false와 동일하며 System.Text.Json에서는 구분이 가능하다는 장점이 있는겁니다.

    

  IgnoreReadOnlyFields=true : 프로퍼티가 readonly인 경우 이를 무시하고 serialize합니다.

 

  DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull : null값을 무시합니다.

 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault : 기본값을 무시합니다.

 

 

   - json형태의 문자열을 Oject또는 특정 클래스로 바꾸는 메소드(역직렬화)

var oject = JsonSerializer.Deserialize(json);

클래스 인스턴스 = JsonSerializer.Deserialize<클래스>(json);

 

 

 - 인스턴스를 db 등이 아닌 파일에 기록할때 사용하는 메소드

 string fileName = "아무파일명.json";
using FileStream createStream = File.Create(fileName);
await JsonSerializer.SerializeAsync(createStream, 파일화하고싶은 인스턴스);
await createStream.DisposeAsync();

 

json파일로 만든 것은 ReadAllText로 읽어와서 Desirialize하여 사용하시면 됩니다.

 

 

제가 사용하는 기능 위주로 작성했기 때문에 어떤 특정한 내용을 찾고자 오셨다면 내용이 부족하다 느낄 수 있습니다.

 

Newtonsoft.Json에서 System.Text.Json으로 마이그레이션 - .NET | Microsoft Learn

 

Newtonsoft.Json에서 System.Text.Json으로 마이그레이션 - .NET

로 마이그레이션하는 Newtonsoft.JsonSystem.Text.Json 방법과 차이점에 대해 알아봅니다 System.Text.Json.

learn.microsoft.com

 

microsoft 공식 링크를 통해 자세한 부분을 찾아보시기 바립니다.

반응형

+ Recent posts