[ { "title": "C#, Sliding Window, Two Pointers 구현", "url": "/20260502-1/", "date": "2026-05-02", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "csharp, algo", "content": "슬라이딩 윈도우(Sliding Window)는 고정된 크기의 창(Window)을 옆으로 밀면서 데이터를 처리하는 알고리즘 기법입니다. 중복 계산을 줄여 시간 복잡도를 $O(N)$으로 최적화하는 데 매우 유용하다.1 또한, 매번 처리되는 중복된 요소를 버리지 않고 재사용함으로써 낭비되는 계산을 하지 않음으로써 효율적으로 처리하는 방법이다.2 문자열 처리에서 Dictionary&lt;char, int&gt;나 int[] 배열을 활용해 투 포인터(Two Pointers) 방식으로 직접 구현하면 IndexOf보다 훨씬 효율적인 $O(N)$ 성능의 함수를 만들 수 있다. Substring 사용은 메모리 할당(GC 과부하)이 발생하여 윈도우가 움직일 때마다 Substring을 호출하면 성능이 급격히 떨어진다. 이때 Span&lt;T&gt; 또는 ReadOnlySpan&lt;char&gt;를 사용하면 메모리 복사 없이 문자열의 특정 구간을 가리킬 수 있다. 1 ReadOnlySpan&lt;char&gt; window = s.AsSpan().Slice(left, right - left + 1); 사용 예제 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // 최소 구간 찾기 (Minimum Window Substring) // 출력: BANC const string text1 = \"ADOBECODEBANC\"; ReadOnlySpan&lt;char&gt; minWin = SlidingWindow.GetMinWindow(text1, \"ABC\"); LogHelper.Debug($\"최소 구간 찾기 : {minWin.ToString()}\"); // 중복 없는 가장 긴 문자열 길이 // 출력: 3 (abc) const string text2 = \"abcabcbb\"; int longLen = SlidingWindow.GetLongestUniqueLength(text2); LogHelper.Debug($\"중복 없는 최장 길이: {longLen}\"); // 애너그램 위치 찾기 // 출력: 0, 6 const string text3 = \"cbaebabacd\"; List&lt;int&gt; anagrams = SlidingWindow.FindAnagrams(text3, \"abc\"); LogHelper.Debug($\"애너그램 인덱스: {string.Join(\", \", anagrams)}\"); // K개 고유 문자 포함 최장 길이 // 출력: 3 (ece) const string text4 = \"eceba\"; int kLen = SlidingWindow.GetMaxKUniqueLength(text4, 2); LogHelper.Debug($\"최대 2개 고유문자 최장 길이: {kLen}\"); Sliding Window 함수 함수명 핵심 목표 윈도우 크기 핵심 데이터 구조 GetMinWindow 조건 만족하는 최소 길이 가변 (수축 중심) int[] (필요 빈도수) GetLongestUnique 중복 없는 최대 길이 가변 (확장 중심) int[] (마지막 위치) FindAnagrams 구성이 일치하는 모든 위치 고정 int[] (고정 빈도수) GetMaxKUnique 종류가 K개 이하인 최대 길이 가변 (확장 중심) int[] (현재 종류수) 최대 K개의 고유 문자(Unique Characters)를 포함하는 가장 긴 부분 문자열 찾기 예시 (K = 2 일 때) : 문자열 : a a b a c b e [a a] : 고유 문자 1종 (a) - 통과 [a a b] : 고유 문자 2종 (a, b) - 통과 [a a b a] : 고유 문자 2종 (a, b) - 통과 (현재 최장 길이: 4) [a a b a c] : 고유 문자 3종 (a, b, c) - 실패! (K=2를 초과함) 이제 왼쪽에서 하나씩 뺀다. [a b a c] (3종), [b a c] (3종), [a c] (2종) … 다시 조건을 만족할 때까지 줄인다 결과적으로 위 문자열에서 K=2일 때 가장 긴 구간은 aaba로 길이는 4가 된다. 모든 문자를 포함하는 최소 구간 찾기 (Minimum Window Substring) 데이터 필터링 및 요약 로그 분석: 세 키워드 모두 등작하는 가장 짧은 타임라인 추출 문서 요약: 검색한 키워드들이 모두 포함된 가장 짧은 문장 찾기) Left 최대 수축 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 public static ReadOnlySpan&lt;char&gt; GetMinWindow(string s, string t) { if (string.IsNullOrWhiteSpace(s) || string.IsNullOrWhiteSpace(t)) { return ReadOnlySpan&lt;char&gt;.Empty; } try { int[] targetMap = new int[128]; foreach (char c in t) { targetMap[c]++; } int[] windowMap = new int[128]; int left = 0, count = 0, minLen = int.MaxValue, startIndex = 0; int required = t.Distinct().Count(); for (int right = 0; right &lt; s.Length; right++) { char rChar = s[right]; windowMap[rChar]++; if (targetMap[rChar] &gt; 0 &amp;&amp; windowMap[rChar] == targetMap[rChar]) { count++; } while (count == required) { if (right - left + 1 &lt; minLen) { minLen = right - left + 1; startIndex = left; } char lChar = s[left]; if (targetMap[lChar] &gt; 0 &amp;&amp; windowMap[lChar] == targetMap[lChar]) { count--; } windowMap[lChar]--; left++; } } return minLen == int.MaxValue ? ReadOnlySpan&lt;char&gt;.Empty : s.AsSpan().Slice(startIndex, minLen); } catch (Exception ex) { LogHelper.Error($\"GetMinWindow Error : {ex.Message}\"); return ReadOnlySpan&lt;char&gt;.Empty; } } 중복 문자가 없는 가장 긴 부분 문자열 (Longest Substring Without Repeating) 고유성 검사 및 패턴 인식 보안 인증: 일회용 비밀번호(OTP)나 난수 생성기에서 문자가 겹치지 않고 연속해서 나올 수 있는 최대 성능 측정 데이터 압축: 반복되지 않는 문자열 구간을 찾아 효율적인 압축 사전을 만들 때 중복 발생 시 포인터 점프 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public static int GetLongestUniqueLength(string s) { if (string.IsNullOrWhiteSpace(s)) { return 0; } try { int[] lastSeen = new int[128]; Array.Fill(lastSeen, -1); int maxLength = 0, left = 0; for (int right = 0; right &lt; s.Length; right++) { if (lastSeen[s[right]] &gt;= left) { left = lastSeen[s[right]] + 1; } lastSeen[s[right]] = right; maxLength = Math.Max(maxLength, right - left + 1); } return maxLength; } catch (Exception ex) { LogHelper.Error($\"GetLongestUniqueLength Error : {ex.Message}\"); return 0; } } 애너그램 시작 위치 모두 찾기 (Find All Anagrams) 패턴 매칭 및 암호 해석 유전자 분석: DNA 염기 서열(ATGC)에서 순서는 다르지만 구성 성분이 같은 특정 유전자 마커를 찾을 때 철자 검사기: 사용자가 입력한 단어의 철자를 조합해서 만들 수 있는 단어가 본문에 어디어디 숨어있는지 찾을 때 대상(p.Length)으로 고정 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public static List&lt;int&gt; FindAnagrams(string s, string p) { List&lt;int&gt; indices = []; if (s.Length &lt; p.Length) { return indices; } try { int[] pCount = new int[128]; int[] sCount = new int[128]; foreach (char c in p) { pCount[c]++; } for (int i = 0; i &lt; s.Length; i++) { sCount[s[i]]++; if (i &gt;= p.Length) { sCount[s[i - p.Length]]--; } if (i &lt; p.Length - 1) { continue; } if (AreCountsEqual(pCount, sCount)) { indices.Add(i - p.Length + 1); } } return indices; } catch (Exception ex) { LogHelper.Error($\"FindAnagrams Error : {ex.Message}\"); return indices; } // Span을 이용한 빈도 배열 비교 bool AreCountsEqual(int[] a, int[] b) { return a.AsSpan().SequenceEqual(b.AsSpan()); } } 최대 K개의 고유 문자를 포함하는 가장 긴 구간 자원 제한 조건에서의 최대 효율 탐색 스트리밍 최적화: “서로 다른 화질(Resolution) 종류를 최대 2개까지만 유지하면서 끊김 없이 보낼 수 있는 가장 긴 시간대” 계산. 마케팅: 고객의 구매 이력에서 “최대 3종류의 카테고리 물건만 사면서 가장 길게 이어진 쇼핑 세션”을 분석하여 고객 성향 파악. 윈도우 내부의 ‘종류(Count)’ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public static int GetMaxKUniqueLength(string s, int k) { if (string.IsNullOrWhiteSpace(s) || k == 0) { return 0; } int[] counts = new int[128]; int left = 0, distinctCount = 0, maxLen = 0; for (int right = 0; right &lt; s.Length; right++) { if (counts[s[right]] == 0) { distinctCount++; } counts[s[right]]++; while (distinctCount &gt; k) { counts[s[left]]--; if (counts[s[left]] == 0) distinctCount--; left++; } maxLen = Math.Max(maxLen, right - left + 1); } return maxLen; } Rust, windows() 예제 Rust는 C#과 달리 표준 라이브러리의 slice 유니티에 windows()라는 메서드가 내장되어 있다. C#과 의 가장 큰 차이점은 Rust의 windows()는 데이터를 복사하지 않고 참조(Reference)만 넘겨준다는 점이다. 1 2 3 4 5 6 7 8 9 fn main() { let words = vec![\"A\", \"B\", \"C\", \"D\", \"E\"]; // 크기가 3인 창(Window)을 만들어서 한 칸씩 이동 for window in words.windows(3) { // window는 순서로 참조, [\"A\", \"B\", \"C\"], [\"B\", \"C\", \"D\"] println!(\"{:?}\", window); } } 인접한 요소 비교하기 1 2 3 4 5 6 7 8 9 10 fn main() { let prices = vec![100, 150, 120, 200, 180]; // 크기가 2인 윈도우를 사용해 가격 변동 분석 for w in prices.windows(2) { let diff = w[1] - w[0]; let status = if diff &gt; 0 { \"상승\" } else { \"하락\" }; println!(\"{} -&gt; {}: {} ({})\", w[0], w[1], diff.abs(), status); } } 애너그램 : C#과 비교해서 코드가 간결 함. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 fn find_anagrams(s: &amp;str, p: &amp;str) -&gt; Vec&lt;usize&gt; { let mut result = Vec::new(); let p_len = p.len(); let s_bytes = s.as_bytes(); // 정렬된 target (비교용) let mut p_sorted = p.as_bytes().to_vec(); p_sorted.sort(); // windows(p_len)을 사용해 슬라이딩 윈도우 자동 생성 for (i, window) in s_bytes.windows(p_len).enumerate() { let mut temp = window.to_vec(); temp.sort(); if temp == p_sorted { result.add(i); } } result } Rust의 내장 windows(n)는 윈도우 크기가 n으로 고정된 경우에만 사용할 수 있다. &amp;str에서 바로 사용할 수 없고 바이트 단위나 유니코드 스칼라(chars()) 단위로 변환 후 사용해야 한다. 슬라이딩 윈도우 vs 투 포인터3 구분 슬라이딩 윈도우 투 포인터 윈도우 크기 고정됨 (K) 가변적임 (조건에 따라 변화) 주요 목적 연속된 구간의 합/평균 최적화 구간 내 특정 조건을 만족하는 쌍 찾기 Reference “윈도잉(windowing) 기법을 적용한 고성능 표 컴포넌트 개발기”, &lt;NAVER D2&gt;, 2025.07.24, https://d2.naver.com/helloworld/1450243, 2026.05.02. “슬라이딩윈도우 알고리즘(feat.Javascript)”, &lt;endmoseung&gt;, 2022.10.12, https://velog.io/@endmoseung/슬라이딩윈도우-알고리즘feat.Javascript, 2016.05.02. “단순화된 슬라이딩 윈도우 기법”, &lt;Linkedin&gt;, 2024.10.24,https://kr.linkedin.com/pulse/sliding-window-technique-simplified-c-rishabh-singh-1b3te, 2026.05.02." } , { "title": "C#, Result Pattern 구현", "url": "/20260421-1/", "date": "2026-04-21", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "csharp, helper", "content": "C# Result Pattern은 메서드가 성공 또는 실패(오류) 상태와 관련 데이터를 포함한 객체를 반환하도록 하여, 예외(Exception) 대신 명시적으로 결과를 처리하는 디자인 패턴이다.1 작업의 성공 또는 실패를 명시적으로 반환하여 예외(Exception) 대신 구조화된 객체를 통해 에러를 처리하는 디자인 패턴이다. 주로 함수형 프로그래밍 접근 방식을 활용하여 코드의 가독성, 유지보수성, 예측 가능성을 높이며, ‘성공 시 데이터’와 ‘실패 시 에러 정보’를 하나의 객체로 캡슐화한다.2 이걸 구현하는 비슷한 패키지로는 OneOf가 있지만 오래되었고 좀 더 Customizing 할 수 있게 직접 구현해 보았다. 아래에 전체 소스를 추가하였다. 활용 예제 1 2 3 4 5 6 7 8 9 public Result&lt;string&gt; GetUsername(int id) { return id switch { &lt; 0 =&gt; Result&lt;string&gt;.Failure(\"잘못된 접근입니다.\", 400), 0 =&gt; None.Value, _ =&gt; \"DebugJO\" }; } 1 2 3 4 5 6 7 8 Result&lt;string&gt; result = GetUsername(-1); string message = result.Match( success: name =&gt; $\"사용자: {name}\", none: () =&gt; \"사용자를 찾을 수 없습니다.\", failure: (msg, code) =&gt; $\" 오류({code}): {msg}\" ); Console.WriteLine($\"message = {message}\"); 서비스 등록 예제 1 2 3 4 5 6 7 8 9 10 11 12 public interface IUserService { Result&lt;string&gt; GetUserName(int id); } public class UserService : IUserService { public Result&lt;string&gt; GetUserName(int id) { if (id == 0) return None.Value; return \"DebugJO\"; } } 1 builder.Services.AddScoped&lt;IUserService, UserService&gt;(); 1 2 3 4 5 6 7 8 9 10 11 12 public class UserController(IUserService userService) : ControllerBase { public IActionResult Get(int id) { // 서비스에서 Result&lt;string&gt;을 받아와서 Match로 처리 return userService.GetUserName(id).Match&lt;IActionResult&gt;( success: name =&gt; Ok(name), none: () =&gt; NotFound(), failure: (msg, code) =&gt; StatusCode(code, msg) ); } } 1 2 3 4 5 6 7 8 9 10 11 public IActionResult Process(int id) { var userResult = _userService.GetUserName(id); // 패턴 매칭을 통해 아주 선언적으로 작성 가능 return userResult.Match&lt;IActionResult&gt;( success: name =&gt; Ok($\"어서오세요, {name}님\"), none: () =&gt; NotFound(\"사용자를 찾을 수 없습니다.\"), failure: (msg, code) =&gt; StatusCode(code, msg) ); } 추가 예제, 리턴 타입 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public Result&lt;string&gt; ProcessPayment(decimal amount) { switch (amount) { // if (!User.IsAuthenticated) case &gt; 30: return ResultExtensions.ToUnauthorized&lt;Result&lt;string&gt;, string&gt;(); case &lt;= 0: return ResultExtensions.ToBadRequest&lt;Result&lt;string&gt;, string&gt;(\"금액이 올바르지 않습니다.\"); default: { string receipt = $\"결제 완료: {amount}원\"; return receipt.ToSuccess&lt;Result&lt;string&gt;, string&gt;(); } } } 1 2 3 4 5 6 Result&lt;string&gt; result = ProcessPayment(1000); result.Match( success: s =&gt; Console.WriteLine($\"결과 : {s}\") none: () =&gt; Console.WriteLine(\"결과 없음\"), failure: (msg, code) =&gt; Console.WriteLine($\"Error {code}: {msg}\") ); 코드 추가 1 2 3 4 5 6 7 public static class ErrorCodes { public const int InvalidInput = 400; public const int Unauthorized = 401; public const int NotFound = 404; public const int ServerError = 500; } 1 2 3 4 5 6 7 8 9 10 11 12 13 public Result&lt;User&gt; GetUserAccount(int id) { if (id &lt; 0) return Result&lt;User&gt;.Failure(\"아이디는 음수일 수 없습니다.\", ErrorCodes.InvalidInput); if (id == 999) // 특정 금지된 아이디 가정 return Result&lt;User&gt;.Failure(\"접근 권한이 없는 계정입니다.\", ErrorCodes.Unauthorized); var user = _repository.Find(id); if (user == null) return None.Value; return user; // 성공 (자동 형변환) } 전체 소스 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 public interface IResult&lt;out TSelf, in TValue&gt; where TSelf : IResult&lt;TSelf, TValue&gt; { static abstract TSelf Success(TValue value); static abstract TSelf Failure(string message, int errorCode = 500); } public record Result&lt;T&gt; : IResult&lt;Result&lt;T&gt;, T&gt; { public T? Value { get; } public string? ErrorMessage { get; } public int ErrorCode { get; } public ResultState State { get; } public enum ResultState { Success, None, Failure } private Result(ResultState state, T? value, string? error, int code) { State = state; Value = value; ErrorMessage = error; ErrorCode = code; } public static Result&lt;T&gt; Success(T value) =&gt; new(ResultState.Success, value, null, 0); public static Result&lt;T&gt; Failure(string message, int errorCode = 500) =&gt; new(ResultState.Failure, default, message, errorCode); public static Result&lt;T&gt; None() =&gt; new(ResultState.None, default, null, 0); public static implicit operator Result&lt;T&gt;(T value) =&gt; Success(value); public static implicit operator Result&lt;T&gt;(None _) =&gt; None(); public TResult Match&lt;TResult&gt;(Func&lt;T, TResult&gt; success, Func&lt;TResult&gt; none, Func&lt;string, int, TResult&gt; failure) =&gt; State switch { ResultState.Success =&gt; success(Value!), ResultState.None =&gt; none(), ResultState.Failure =&gt; failure(ErrorMessage!, ErrorCode), _ =&gt; throw new InvalidOperationException() }; public void Match(Action&lt;T&gt; success, Action none, Action&lt;string, int&gt; failure) { switch (State) { case ResultState.Success: success(Value!); break; case ResultState.None: none(); break; case ResultState.Failure: failure(ErrorMessage!, ErrorCode); break; default: throw new InvalidOperationException(); } } } public readonly struct None { public static None Value =&gt; new(); } public static class ResultExtensions { // 공통 실패 처리 (400 Bad Request) public static TResult ToBadRequest&lt;TResult, TValue&gt;(string message) where TResult : IResult&lt;TResult, TValue&gt; =&gt; TResult.Failure(message, 400); // 공통 실패 처리 (401 Unauthorized) public static TResult ToUnauthorized&lt;TResult, TValue&gt;(string message = \"인증되지 않았습니다.\") where TResult : IResult&lt;TResult, TValue&gt; =&gt; TResult.Failure(message, 401); // 공통 성공 처리 (단순 값을 결과 객체로 변환) public static TResult ToSuccess&lt;TResult, TValue&gt;(this TValue value) where TResult : IResult&lt;TResult, TValue&gt; =&gt; TResult.Success(value); } 서비스 등록 인스턴스 생성 시점 및 공유 범위 AddSingleton 애플리케이션 시작 후 최초 요청 시 딱 한 번 생성하며, 모든 사용자가 동일한 인스턴스를 공유 AddScoped HTTP 요청(Request) 하나당 하나 생성. 같은 요청 내에서는 동일한 인스턴스를 공유하지만, 다른 요청과는 공유하지 않음 AddTransient 서비스가 주입될 때마다 항상 새로운 인스턴스를 생성. 가장 수명이 짧다 Reference “C# The Result Pattern in C#: A comprehensive guide”, &lt;Linkedin&gt;, 2024.11.27, https://www.linkedin.com/pulse/result-pattern-c-comprehensive-guide-andre-baltieri-wieuf, 2026.04.21. “C# Result Pattern in .NET and C#”, &lt;Github&gt;, 2024.07.01, https://github.com/karanraj-tech/result-pattern, 2026.04.21." } , { "title": "C#, 고성능 Tag Search", "url": "/20260411-2/", "date": "2026-04-11", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "csharp, helper", "content": "태그(tag) 조회 성능을 위해 Aho-Corasick 알고리즘을 사용하는데 이를 위해 .NET 9 이상에서 제공하는 SearchValues&lt;string&gt; 함수를 이용하는 예제이다. 최적화 핵심은 그룹화, 길이 내림차순 정렬, FrozenDictionary (읽기 전용 최적화 컬렉션) 자료구조이다. IndexOfAny로 특정 단어로 시작하는 단어들만 루프를 돌게 한다. Greedy Matching (긴 단어 우선), ReadOnlySpan&lt;char&gt;으로 메모리(GC) 최적화 등을 포함한다. 성능을 위해 Parallel.ForEach로 문서를 문단 단위로 나누어 병렬처리할 수 있다. 유의어(Alias) 사전 운영은 {\"ID\": 1, \"Name\": \"LG\", \"Aliases\": [\"LG전자\", \"LG디스플레이\", \"LGCNS\", \"LG CNS\"]}으로 관리하고, SearchValues 엔진에는 Aliases에 있는 모든 단어를 다 때려 넣고 무엇이 걸리든 결과는 \"ID\": 1(LG)로 리턴하게 만드는 방식을 사용한다. Program.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 using System; using System.Collections.Generic; using System.Threading.Tasks; namespace ConsoleApp; internal class Program { private static async Task Main() { // tag dictionary는 DB에서 불러온다고 가정 string[] myDictionary = [\"삼성전자\", \"현대차\", \"LG\", \"SK하이닉스\", \"C#\", \".NET\", \"삼성\"]; const string document = \"삼성전자는 이번 분기 실적 발표에서 현대차와의 협업을 발표했습니다. 또한 C#과 NET 환경에서의 소프트웨어 최적화가 중요해지고 있으며, SK하이닉스의 반도체 기술도 언급되었습니다.\"; // ~ 10,000 태그 가정 TagProcessor tagProcessor = new(myDictionary); List&lt;string&gt; tags = tagProcessor.ExtractAllTags(document); Console.WriteLine($\"추출된 태그 개수 : {tags.Count}\"); foreach (string tag in tags) { Console.WriteLine($\"- {tag}\"); } Console.WriteLine(\"----------------------------\"); // 10,000 ~ 100,000개 태그 가정 Console.WriteLine(\"태그 추출 시작 ...\"); List&lt;string&gt; extractTags = await Task.Run(() =&gt; { HighDensityTagProcessor hightProcessor = new(myDictionary); return hightProcessor.ExtractTags(document); }); Console.WriteLine($\"추출된 태그 개수(High) : {extractTags.Count}\"); foreach (string tag in extractTags) { Console.WriteLine($\"- {tag}\"); } Console.WriteLine(\"태그 추출 완료\"); } } TagProcessor.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 using System; using System.Buffers; using System.Collections.Frozen; using System.Collections.Generic; using System.Linq; namespace ConsoleApp; public class TagProcessor { // 빠른 위치 점프 private readonly SearchValues&lt;string&gt; mTagEngine; // 첫 글자로 그룹화하여 검사 대상 최소화 private readonly FrozenDictionary&lt;char, string[]&gt; mTagLookup; public TagProcessor(IEnumerable&lt;string&gt;? tags) { string[] myDictionary = tags?.Distinct().ToArray() ?? []; if (myDictionary.Length == 0) { throw new ArgumentException(\"태그 리스트가 비어 있어 프로세서를 생성할 수 없습니다.\", nameof(tags)); } mTagEngine = SearchValues.Create(myDictionary, StringComparison.OrdinalIgnoreCase); mTagLookup = myDictionary .GroupBy(t =&gt; char.ToLowerInvariant(t[0])) .ToDictionary( g =&gt; g.Key, g =&gt; g.OrderByDescending(t =&gt; t.Length).ToArray() ) .ToFrozenDictionary(); } public List&lt;string&gt; ExtractAllTags(string document) { if (string.IsNullOrEmpty(document)) { return []; } HashSet&lt;string&gt; foundTags = new(StringComparer.OrdinalIgnoreCase); ReadOnlySpan&lt;char&gt; span = document.AsSpan(); while (true) { // 하드웨어 가속 점프 int index = span.IndexOfAny(mTagEngine); if (index &lt; 0) { break; } span = span[index..]; char firstChar = char.ToLowerInvariant(span[0]); if (mTagLookup.TryGetValue(firstChar, out string[]? candidates)) { bool matched = false; // 현재 남은 문장 길이 int spanLength = span.Length; foreach (string tag in candidates) { // 남은 문장보다 태그가 길면 절대 매칭 검사 생략 if (tag.Length &gt; spanLength) { continue; } if (!span.StartsWith(tag, StringComparison.OrdinalIgnoreCase)) { continue; } foundTags.Add(tag); span = span[tag.Length..]; matched = true; break; } if (matched) { if (span.IsEmpty) { break; } continue; } } // 매칭 실패 시 1글자 전진 span = span[1..]; if (span.IsEmpty) { break; } } return foundTags.ToList(); } } HighDensityTagProcessor.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 using System; using System.Buffers; using System.Collections.Frozen; using System.Collections.Generic; using System.Linq; namespace ConsoleApp; public class HighDensityTagProcessor { private readonly SearchValues&lt;string&gt; mTagEngine; private readonly FrozenDictionary&lt;string, string[]&gt; mTagLookup; private readonly int mMinTagLength; private readonly bool mUseDoublePrefix; // 2글자 인덱스 플래그 public HighDensityTagProcessor(IEnumerable&lt;string&gt;? tags, bool useDoublePrefix = true) { string[] tagList = tags?.Distinct().ToArray() ?? []; if (tagList.Length == 0) { throw new ArgumentException(\"태그 리스트가 비어 있어 프로세서를 생성할 수 없습니다.\", nameof(tags)); } mUseDoublePrefix = useDoublePrefix; mMinTagLength = tagList.Min(t =&gt; t.Length); mTagEngine = SearchValues.Create(tagList, StringComparison.OrdinalIgnoreCase); mTagLookup = tagList .GroupBy(t =&gt; GetKey(t, mUseDoublePrefix)) .ToDictionary( g =&gt; g.Key, g =&gt; g.OrderByDescending(t =&gt; t.Length).ToArray() ) .ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); } private static string GetKey(string tag, bool useDouble) { int len = (useDouble &amp;&amp; tag.Length &gt;= 2) ? 2 : 1; return tag[..len].ToLowerInvariant(); } public List&lt;string&gt; ExtractTags(string document) { if (string.IsNullOrEmpty(document) || document.Length &lt; mMinTagLength) { return []; } HashSet&lt;string&gt; foundTags = new(StringComparer.OrdinalIgnoreCase); ReadOnlySpan&lt;char&gt; span = document.AsSpan(); while (span.Length &gt;= mMinTagLength) { int index = span.IndexOfAny(mTagEngine); if (index &lt; 0) { break; } span = span[index..]; if (span.Length &lt; mMinTagLength) { break; } bool matched = false; if (mUseDoublePrefix &amp;&amp; span.Length &gt;= 2) { if (TryMatch(span[..2].ToString(), span, foundTags, out int matchedLength)) { span = span[matchedLength..]; matched = true; } } if (!matched) { if (TryMatch(span[..1].ToString(), span, foundTags, out int matchedLength)) { span = span[matchedLength..]; matched = true; } } if (!matched) { span = span[1..]; } } return foundTags.ToList(); } private bool TryMatch(string key, ReadOnlySpan&lt;char&gt; span, HashSet&lt;string&gt; foundTags, out int matchedLength) { matchedLength = 0; if (!mTagLookup.TryGetValue(key, out string[]? candidates)) { return false; } foreach (string tag in candidates) { if (span.Length &lt; tag.Length || !span.StartsWith(tag, StringComparison.OrdinalIgnoreCase)) { continue; } foundTags.Add(tag); matchedLength = tag.Length; return true; } return false; } } throw 대표적인 예외 1.매개변수(인자) 값이 잘못되었을 때 ArgumentNullException: 전달된 인자가 null이면 안 될 때 ArgumentOutOfRangeException: 숫자가 범위를 벗어났을 때 ArgumentException: 그 외에 값이 잘못되었을 때 (포괄) 2.객체의 상태가 올바르지 않을 때 InvalidOperationException: 메서드를 호출하기 위한 전제 조건이 맞지 않을 때 ObjectDisposedException: 리소스가 해제(Dispose)된 객체를 다시 사용하려고 할 때 3.아직 구현하지 않았거나 지원하지 않을 때 NotImplementedException: 메서드 틀만 있고 코드는 없을 때(메모용) NotSupportedException: 해당 기능이 지원되지 않을 때 Debug.Assert vs if (throw)  차이 구분 Debug.Assert if (tagList.Length == 0) throw 작동 환경 Debug 모드에서만 작동 Debug/Release 모두 작동 실행 결과 개발 중 팝업창이 뜨거나 로그가 찍힘 프로그램이 예외를 발생시키며 중단됨 용도 개발자의 실수를 잡을 때 (버그 방지) 사용자의 입력값이나 데이터가 잘못되었을 때" } , { "title": "C#에서 C++, Rust, Zig 라이브러리 사용하기", "url": "/20260411-1/", "date": "2026-04-11", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "csharp, rust, zig, cpp", "content": "이전 글 C# AOT 라이브러리 활용에서 C++ constexpr, zig comptime, rust의 매크로 함수인 obfstr! 함수를 이용하여 컴파일 타임에 문자열을 난독화하는 예제를 소개했는데 이번 글에서 Native 컴파일 언어로 각각 구현하였다. CppNative.dll , rust_native.dll , zig_native.dll (Zig는 0.16dev 버전) graph LR A[\"Native Library&lt;br/&gt;(Cpp, Rust, Zig)\"] --&gt;B[\"Unmanaged C#(AOT)&lt;br/&gt;(ConsoleAppNative.dll)\"] B--&gt;C[\"Manged C#&lt;br/&gt;(ConsoleApp.exe)\"] 개발 환경 Cpp : Clion + 툴체인(Visusql Studio 2022) Rust : RustRover + rust 1.94.1 Zig : VSCode + zig 0.16.0-dev.3133+5ec8e45f3 C# : Rider + .net 10 전체 예제는 Github에서 볼 수 있다. CPP Example 1. Cpp : CMakeList.txt 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 cmake_minimum_required(VERSION 4.2) project(CppNative) set(CMAKE_CXX_STANDARD 20) add_compile_options(/utf-8) # 정적 컴파일 set(CMAKE_MSVC_RUNTIME_LIBRARY \"MultiThreaded$&lt;$&lt;CONFIG:Debug&gt;:Debug&gt;\") # 동적 컴파일 : dumpbin /dependents CppNative.dll 확인 # set(CMAKE_MSVC_RUNTIME_LIBRARY \"MultiThreaded$&lt;$&lt;CONFIG:Debug&gt;:Debug&gt;DLL\") add_library(CppNative SHARED library.cpp ) set_target_properties(CppNative PROPERTIES OUTPUT_NAME \"CppNative\" PREFIX \"\") # 동적 컴파일 시 사용 #if (CMAKE_BUILD_TYPE STREQUAL \"Debug\") # set(CMAKE_INSTALL_DEBUG_LIBRARIES ON) #endif () #include(InstallRequiredSystemLibraries) set(DEPLOY_DIR \"${CMAKE_SOURCE_DIR}/deploy/$&lt;CONFIG&gt;\") add_custom_command(TARGET CppNative POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory \"${DEPLOY_DIR}\" COMMAND ${CMAKE_COMMAND} -E copy_if_different $&lt;TARGET_FILE:CppNative&gt; \"${DEPLOY_DIR}\" # 동적 컴파일 시 사용 #COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS} \"${DEPLOY_DIR}\" COMMENT \"Deploying CppNative.dll and system dependencies to ${DEPLOY_DIR}\" ) 2. Cpp : library.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #pragma once #include &lt;string&gt; #include &lt;array&gt; template&lt;size_t N&gt; class ObfuscatedString { public: template&lt;size_t... Is&gt; constexpr ObfuscatedString(const char *str, char key, std::index_sequence&lt;Is...&gt;) : m_key(key), m_data{static_cast&lt;char&gt;(str[Is] ^ key)...} { } [[nodiscard]] std::string decrypt() const { std::string result; result.reserve(N); for (size_t i = 0; i &lt; N; ++i) { result += static_cast&lt;char&gt;(m_data[i] ^ m_key); } return result; } private: char m_key; std::array&lt;char, N&gt; m_data; }; #define OBFUSCATE(str) ([]() { \\ constexpr size_t _len = sizeof(str); \\ return ObfuscatedString&lt;_len&gt;(str, 0x5A, std::make_index_sequence&lt;_len&gt;{}); \\ }()) #if defined(_WIN32) #define MY_API __declspec(dllexport) #else #define MY_API __attribute__((visibility(\"default\"))) #endif extern \"C\" { MY_API int GetSecretData(char *buffer, int obfuscatedKey); } 3. Cpp : library.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include \"library.h\" #include &lt;iostream&gt; #define FIXED_BUFFER_SIZE 1024 #define SALT 0x1234 #define VALID_KEY 0x7777 int GetSecretData(char *buffer, const int obfuscatedKey) { if ((obfuscatedKey ^ SALT) != VALID_KEY) { return -1; } static ObfuscatedString&lt;sizeof (\"cpp 보안 문자열\")&gt; secret = OBFUSCATE(\"cpp 보안 문자열\"); const std::string decrypted = secret.decrypt(); if (buffer != nullptr) { const size_t len = (decrypted.length() &lt; FIXED_BUFFER_SIZE) ? decrypted.length() : FIXED_BUFFER_SIZE - 1; memcpy(buffer, decrypted.c_str(), len); buffer[len] = '\\0'; return static_cast&lt;int&gt;(len); } return 0; } RUST Example 1. Rust : Cargo.toml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [package] name = \"rust_native\" version = \"0.1.0\" edition = \"2024\" [lib] crate-type = [\"cdylib\"] [dependencies] obfstr = \"0.4.4\" [profile.release] opt-level = \"z\" # 크기 최적화 (s 또는 z) lto = true # 전체 프로그램 최적화 codegen-units = 1 # 최적화 품질 향상 panic = \"abort\" # 예외 발생 시 즉시 종료 (FFI 안전성) 2. Rust : config.toml 1 2 3 4 5 [target.x86_64-pc-windows-msvc] rustflags = [\"-C\", \"target-feature=+crt-static\"] [target.i686-pc-windows-msvc] rustflags = [\"-C\", \"target-feature=+crt-static\"] 3. Rust : lib.rs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 use obfstr::obfbytes; use std::ptr; const SALT: i32 = 0x1234; const VALID_KEY: i32 = 0x7777; const MAX_INTERNAL_BUFFER: usize = 1024; #[unsafe(no_mangle)] pub extern \"C\" fn get_secret_data(buffer: *mut u8, buffer_len: i32, obfuscated_key: i32) -&gt; i32 { if buffer.is_null() || buffer_len &lt;= 0 { return 0; } if (obfuscated_key ^ SALT) != VALID_KEY { return -1; } let bytes: &amp;[u8; 21] = obfbytes!(\"rust 보안 문자열\".as_bytes()); let safe_limit: usize = (buffer_len as usize).min(MAX_INTERNAL_BUFFER); let copy_len: usize = bytes.len().min(safe_limit - 1); unsafe { ptr::copy_nonoverlapping(bytes.as_ptr(), buffer, copy_len); ptr::write(buffer.add(copy_len), 0); } copy_len as i32 } #[cfg(test)] mod tests { #[test] fn check_string_len() { let s: &amp;str = \"rust 보안 문자열\"; println!(\"바이트 길이: {}\", s.len()); //assert_eq!(s.len(), 21); } } // cargo test -- --nocapture // cargo build --release ZIG Example 1. Zig : build.zig 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const std = @import(\"std\"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const lib = b.addLibrary(.{ .name = \"zig_native\", .linkage = .dynamic, .root_module = b.createModule(.{ .root_source_file = b.path(\"src/main.zig\"), .target = target, .optimize = optimize, }), }); b.installArtifact(lib); } // zig build -Doptimize=ReleaseSmall 2. Zig : main.zig 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 const std = @import(\"std\"); const SALT: i32 = 0x1234; const VALID_KEY: i32 = 0x7777; const XOR_KEY: u8 = 0x5A; const MAX_INTERNAL_BUFFER: usize = 1024; fn obfuscate(comptime str: []const u8) [str.len]u8 { var res: [str.len]u8 = undefined; for (str, 0..) |c, i| { res[i] = c ^ XOR_KEY; } return res; } const secret_data = obfuscate(\"zig 보안 문자열\"); export fn get_secret_data(buffer: [*]u8, buffer_len: i32, obfuscated_key: i32) callconv(.c) i32 { if (buffer_len &lt;= 0) { return 0; } if ((obfuscated_key ^ SALT) != VALID_KEY) { return -1; } const safe_limit = @as(usize, @intCast(buffer_len)); const internal_limit = if (safe_limit &lt; MAX_INTERNAL_BUFFER) safe_limit else MAX_INTERNAL_BUFFER; const copy_len = if (secret_data.len &lt; internal_limit) secret_data.len else internal_limit - 1; var i: usize = 0; while (i &lt; copy_len) : (i += 1) { buffer[i] = secret_data[i] ^ XOR_KEY; } buffer[copy_len] = 0; return @as(i32, @intCast(copy_len)); } C# Example 1. C# : ConsoleApp.csproj 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 &lt;Project Sdk=\"Microsoft.NET.Sdk\"&gt; &lt;PropertyGroup&gt; &lt;OutputType&gt;Exe&lt;/OutputType&gt; &lt;TargetFramework&gt;net10.0&lt;/TargetFramework&gt; &lt;PublishSingleFile&gt;true&lt;/PublishSingleFile&gt; &lt;SelfContained&gt;true&lt;/SelfContained&gt; &lt;RuntimeIdentifier&gt;win-x64&lt;/RuntimeIdentifier&gt; &lt;ImplicitUsings&gt;disable&lt;/ImplicitUsings&gt; &lt;Nullable&gt;enable&lt;/Nullable&gt; &lt;AllowUnsafeBlocks&gt;true&lt;/AllowUnsafeBlocks&gt; &lt;ApplicationIcon&gt;main.ico&lt;/ApplicationIcon&gt; &lt;Version&gt;1.0.0&lt;/Version&gt; &lt;Copyright&gt;Copyright © 2026 devsight.kr&lt;/Copyright&gt; &lt;!-- 복사 충돌 시 --&gt; &lt;!-- &lt;ErrorOnDuplicatePublishOutputFiles&gt;false&lt;/ErrorOnDuplicatePublishOutputFiles&gt; --&gt; &lt;/PropertyGroup&gt; &lt;!-- managed 라이브러리 PublishSingleFile 빌드할 때 EXE에 포함하지 않음 --&gt; &lt;ItemGroup&gt; &lt;ProjectReference Include=\"..\\ConsoleAppLib\\ConsoleAppLib.csproj\"&gt; &lt;ExcludeFromSingleFile&gt;true&lt;/ExcludeFromSingleFile&gt; &lt;/ProjectReference&gt; &lt;/ItemGroup&gt; &lt;!-- AOT 라이브러리 빌드 --&gt; &lt;ItemGroup&gt; &lt;ProjectReference Include=\"..\\ConsoleAppNative\\ConsoleAppNative.csproj\" ReferenceOutputAssembly=\"false\"&gt; &lt;ExcludeFromSingleFile&gt;true&lt;/ExcludeFromSingleFile&gt; &lt;/ProjectReference&gt; &lt;/ItemGroup&gt; &lt;PropertyGroup&gt; &lt;AotDllSourcePath&gt;..\\ConsoleAppNative\\bin\\$(Configuration)\\$(TargetFramework)\\win-x64\\publish\\ConsoleAppNative.dll&lt;/AotDllSourcePath&gt; &lt;/PropertyGroup&gt; &lt;Target Name=\"PublishNativeAotLibrary\" BeforeTargets=\"BeforeBuild;ComputeFilesToPublish\"&gt; &lt;Message Importance=\"High\" Text=\"[AOT 빌드 시작] ConsoleAppNative 프로젝트를 네이티브 DLL로 게시(Publish) 중...\"/&gt; &lt;Exec Command=\"dotnet publish &amp;quot;..\\ConsoleAppNative\\ConsoleAppNative.csproj&amp;quot; -c $(Configuration) -r win-x64\" StdOutEncoding=\"utf-8\" StdErrEncoding=\"utf-8\"/&gt; &lt;/Target&gt; &lt;Target Name=\"CopyAotDllForBuild\" AfterTargets=\"PublishNativeAotLibrary\" Condition=\"'$(PublishProtocol)' == ''\"&gt; &lt;Message Importance=\"High\" Text=\"[빌드 모드] 생성된 네이티브 DLL을 출력 폴더로 복사합니다.\"/&gt; &lt;Copy SourceFiles=\"$(AotDllSourcePath)\" DestinationFolder=\"$(OutDir)\" SkipUnchangedFiles=\"true\"/&gt; &lt;/Target&gt; &lt;Target Name=\"ExcludeAotDllFromSingleFile\" AfterTargets=\"PublishNativeAotLibrary\" BeforeTargets=\"ComputeFilesToPublish\"&gt; &lt;Message Importance=\"High\" Text=\"[배포 모드] 네이티브 DLL을 단일 파일에서 제외하고 외부 배포 목록에 추가합니다.\"/&gt; &lt;ItemGroup&gt; &lt;ResolvedFileToPublish Include=\"$(AotDllSourcePath)\"&gt; &lt;RelativePath&gt;ConsoleAppNative.dll&lt;/RelativePath&gt; &lt;CopyToPublishDirectory&gt;PreserveNewest&lt;/CopyToPublishDirectory&gt; &lt;ExcludeFromSingleFile&gt;true&lt;/ExcludeFromSingleFile&gt; &lt;/ResolvedFileToPublish&gt; &lt;/ItemGroup&gt; &lt;/Target&gt; &lt;ItemGroup&gt; &lt;None Remove=\"main.ico\"/&gt; &lt;Resource Include=\"main.ico\"/&gt; &lt;/ItemGroup&gt; &lt;!-- 메인(ConsoleApp)프로젝트에 이미 포함되어 있을 때 --&gt; &lt;ItemGroup&gt; &lt;None Update=\"CppNative.dll\"&gt; &lt;CopyToOutputDirectory&gt;Always&lt;/CopyToOutputDirectory&gt; &lt;ExcludeFromSingleFile&gt;true&lt;/ExcludeFromSingleFile&gt; &lt;/None&gt; &lt;None Update=\"rust_native.dll\"&gt; &lt;CopyToOutputDirectory&gt;Always&lt;/CopyToOutputDirectory&gt; &lt;ExcludeFromSingleFile&gt;true&lt;/ExcludeFromSingleFile&gt; &lt;/None&gt; &lt;None Update=\"zig_native.dll\"&gt; &lt;CopyToOutputDirectory&gt;Always&lt;/CopyToOutputDirectory&gt; &lt;ExcludeFromSingleFile&gt;true&lt;/ExcludeFromSingleFile&gt; &lt;/None&gt; &lt;!-- &lt;None Update=\"Database\\test.db\"&gt; &lt;CopyToOutputDirectory&gt;Always&lt;/CopyToOutputDirectory&gt; &lt;/None&gt; --&gt; &lt;/ItemGroup&gt; &lt;/Project&gt; 1 2 3 # 싱글 파일 배포 dotnet publish -c Release : csproj에서 설정한 경우 dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true C#에 사용된 전체 예제는 Github에서 확인할 수 있다. 폴더명은 csProject / ConsoleProject / 3개 프로젝트. 참고로 Zig 컴파일러는 아직 정식 버전으로 확정하기 전이므로 버전별로 호환에 문제가 발생할 수 있다." } , { "title": "C#에서 Function Pointer 사용하기", "url": "/20260410-1/", "date": "2026-04-10", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "csharp", "content": "C#의 함수 포인터(Function Pointer)는 C# 9.0부터 도입된 기능으로, 고성능 연산이나 네이티브(C++, Delphi) 라이브러리와의 연동을 위해 만들어진 가장 로우레벨한 함수 호출 방식이다. C++의 void (*func)(int)와 동일하게 함수의 주솟값을 직접(참조) 가리킨다. 메모리 주소를 직접 다루기 때문에 반드시 unsafe 키워드가 필요하다. csproj에서 AllowUnsafeBlock를 true로 설정한다. Delegate나 Action 같은 객체를 생성하지 않으므로, 호출 시 가비지 컬렉터(GC) 오버헤드가 없고 CPU가 즉시 해당 주소로 점프하므로 제로 오버헤드로 볼 수 있다. 함수 포인터의 기본 문법은 delegate* 키워드를 사용하여 선언하며, 마지막 파라미터가 리턴 타입이 된다. 1 2 3 4 5 6 7 8 // 선언: &lt;매개변수1, 매개변수2, 리턴타입&gt; delegate*&lt;int, int, int&gt; addPtr; // 할당: &amp; 연산자로 정적(static) 함수의 주소를 가져옴 addPtr = &amp;MyStaticFunction; // 호출: 일반 함수처럼 호출 int result = addPtr(10, 20); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // Program.cs using System; namespace ConsoleApp; internal class Program { private static void Main() { MathEngine math = new(); double resulMath = math.Execute(OpType.Divide, 3, 0); if (double.IsNaN(resulMath)) { Console.WriteLine($\"Error: {resulMath}\"); } else { Console.WriteLine(resulMath); } } } MathEngine.cs (Function Pointer) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 namespace ConsoleApp; public unsafe class MathEngine { private readonly delegate*&lt;double, double, double&gt;[] mOperationTable; public MathEngine() { mOperationTable = new delegate*&lt;double, double, double&gt;[4]; mOperationTable[(int)OpType.Add] = &amp;Add; mOperationTable[(int)OpType.Subtract] = &amp;Subtract; mOperationTable[(int)OpType.Multiply] = &amp;Multiply; mOperationTable[(int)OpType.Divide] = &amp;Divide; } private static double Add(double a, double b) =&gt; a + b; private static double Subtract(double a, double b) =&gt; a - b; private static double Multiply(double a, double b) =&gt; a * b; private static double Divide(double a, double b) =&gt; b != 0 ? a / b : double.NaN; public double Execute(OpType op, double a, double b) { int opIndex = (int)op; if ((uint)opIndex &gt;= (uint)mOperationTable.Length) { return 0; } delegate*&lt;double, double, double&gt; func = mOperationTable[opIndex]; return func != null ? func(a, b) : 0; // if (func != null) // { // return mOperationTable[opIndex](a, b); // } // // return 0; } } public enum OpType { Add = 0, Subtract = 1, Multiply = 2, Divide = 3 } Dictionary vs. Function Pointer 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 using System; using System.Collections.Generic; public class DictionaryExample { private readonly Dictionary&lt;string, Action&lt;string&gt;&gt; mCommandMap = new(); public DictionaryExample() { mCommandMap.Add(\"Print\", (msg) =&gt; Console.WriteLine($\"[Print] {msg}\")); mCommandMap.Add(\"Log\", ShowLog); } private static void ShowLog(string message) { Console.WriteLine($\"[Log] {DateTime.Now}: {message}\"); } public void Execute(string commandName, string data) { if (mCommandMap.TryGetValue(commandName, out var action)) { action(data); } else { Console.WriteLine(\"명령어를 찾을 수 없습니다.\"); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 using System; public unsafe class FunctionPointerExample { private readonly delegate*&lt;int, void&gt;[] mFunctionTable; public FunctionPointerExample() { mFunctionTable = new delegate*&lt;int, void&gt;[2]; mFunctionTable[0] = &amp;DoubleValue; mFunctionTable[1] = &amp;SquareValue; } private static void DoubleValue(int x) =&gt; Console.WriteLine($\"Double: {x * 2}\"); private static void SquareValue(int x) =&gt; Console.WriteLine($\"Square: {x * x}\"); public void Execute(int index, int value) { if (index &gt;= 0 &amp;&amp; index &lt; mFunctionTable.Length) { mFunctionTable[index](value); } } } Dictionary는 Execute(\"Print\", \"Hello\")와 같이 이름으로 호출하므로 코드가 직관적이지만 내부적으로 문자열 비교와 해시 계산이 발생한다. Function Pointer는 Execute(0, 10)와 같이 인덱스로 호출하므로 매우 빠르다." } , { "title": "CLion에서 Qt 개발환경 설정하기", "url": "/20260327-1/", "date": "2026-03-27", "categories": "【 cppㆍqtㆍc3 】, QT", "tags": "qt, cpp", "content": "Qt Framework를 사용할 때 개발툴로 Qt Creator를 사용하는데 이것을 대신하여 Jetbrains사의 C++ 개발 환경인 CLion을 이용하여 윈도 위젯을 만들 수 있다. Qt는 기본으로 제공하며 여기에 CMakeLists.txt를 수정하여 개발 환경을 구축한다. 프로젝트를 생성하고 빌드하면 cmake 설정 에러가 날 수 있는데 cmake 파일을 수정하기 않아서이다. graph LR A[\"New Project\"] --&gt; B[\"Qt Widgets Excutable\"] B --&gt; C[\"CMakeLists.txt\"] C --&gt; D[\"Qt UI Class\"] 추가로 프로그램 아이콘(타이틀바, 실행파일)을 설정한다. CMakeLists.txt 툴체인 샘플 1 2 3 4 5 6 7 8 9 10 11 set(QT_ROOT_PATH \"C:/Qt/6.8.3\") set(MSYS2_UCRT_PATH \"C:/msys64/ucrt64\") if (MSVC) set(CMAKE_PREFIX_PATH \"${QT_ROOT_PATH}/msvc2022_64\") elseif (EXISTS \"${MSYS2_UCRT_PATH}\") list(APPEND CMAKE_PREFIX_PATH \"${MSYS2_UCRT_PATH}\") set(CMAKE_FIND_ROOT_PATH \"${MSYS2_UCRT_PATH}\") else () set(CMAKE_PREFIX_PATH \"${QT_ROOT_PATH}/mingw_64\") endif () 개발환경 : CLion 2025.3.4, Qt 6.8.3, 툴체인 MinGW/Visual Sutio CMakeLists.txt 기본설정 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 cmake_minimum_required(VERSION 4.1) project(qtproject) set(QT_ROOT_PATH \"C:/Qt/6.8.3\") if (MSVC) set(CMAKE_PREFIX_PATH \"${QT_ROOT_PATH}/msvc2022_64\") else () set(CMAKE_PREFIX_PATH \"${QT_ROOT_PATH}/mingw_64\") endif () set(CMAKE_CXX_STANDARD 20) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) find_package(Qt6 COMPONENTS Core Gui Widgets REQUIRED) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_AUTOUIC_SEARCH_PATHS ${CMAKE_SOURCE_DIR}/ui ${CMAKE_SOURCE_DIR}/src ) add_executable(${PROJECT_NAME} main.cpp resources.qrc app.rc src/mainwindow.cpp include/mainwindow.h ui/mainwindow.ui) target_include_directories(${PROJECT_NAME} PRIVATE include ${CMAKE_BINARY_DIR} ) target_link_libraries(${PROJECT_NAME} Qt::Core Qt::Gui Qt::Widgets) CMakeLists.txt 빌드 환경설정 위의 코드에 아래의 코드를 더하여 빌드 환경을 구축한다. 각 빌드 기본 폴더 안의 deploy 폴더에 배포 시 필요한 최소화된 파일들이 생성된다. MinGW, MSVC 같이 적용한 파일이다. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 if (WIN32 AND NOT DEFINED CMAKE_TOOLCHAIN_FILE) set_target_properties(${PROJECT_NAME} PROPERTIES WIN32_EXECUTABLE ON) if (TARGET Qt6::qmake) get_target_property(_qmake_executable Qt6::qmake IMPORTED_LOCATION) get_filename_component(_qt_bin_dir \"${_qmake_executable}\" DIRECTORY) find_program(WINDEPLOYQT_EXECUTABLE windeployqt HINTS \"${_qt_bin_dir}\") endif () if (WINDEPLOYQT_EXECUTABLE) set(DEPLOY_DIR \"${CMAKE_BINARY_DIR}/deploy\") if (MSVC AND CMAKE_BUILD_TYPE STREQUAL \"Debug\") set(DEPLOY_MODE \"--debug\") else () set(DEPLOY_MODE \"--release\") endif () set(MINIMIZE_FLAGS \"--no-translations\" \"--no-opengl-sw\" \"--no-system-d3d-compiler\" \"--no-svg\" \"--no-quick-import\" \"--no-sql\" \"--no-network\" ) add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND \"${WINDEPLOYQT_EXECUTABLE}\" ${DEPLOY_MODE} --compiler-runtime --no-translations --verbose 0 \"$&lt;TARGET_FILE:${PROJECT_NAME}&gt;\" COMMAND ${CMAKE_COMMAND} -E rm -rf \"${DEPLOY_DIR}\" COMMAND ${CMAKE_COMMAND} -E make_directory \"${DEPLOY_DIR}\" COMMAND ${CMAKE_COMMAND} -E copy \"$&lt;TARGET_FILE:${PROJECT_NAME}&gt;\" \"${DEPLOY_DIR}/\" COMMAND \"${WINDEPLOYQT_EXECUTABLE}\" ${DEPLOY_MODE} --compiler-runtime ${MINIMIZE_FLAGS} --verbose 1 \"${DEPLOY_DIR}/$&lt;TARGET_FILE_NAME:${PROJECT_NAME}&gt;\" COMMENT \"build 실행 환경, deploy 최소 DLL 구성 완료\" ) endif () endif () 프로그램 아이콘 설정 cmake 파일의 add_executable 부분에 넣어준다. 예제에는 포함하였다. app.rc 1 IDI_ICON1 ICON \"main.ico\" resources.qrc 1 2 3 4 5 &lt;RCC&gt; &lt;qresource prefix=\"/\"&gt; &lt;file&gt;main.ico&lt;/file&gt; &lt;/qresource&gt; &lt;/RCC&gt; main.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include \"mainwindow.h\" #include &lt;QApplication&gt; #include &lt;QScreen&gt; #include &lt;QDebug&gt; int main(int argc, char *argv[]) { QApplication a(argc, argv); qDebug() &lt;&lt; \"현재 기기 픽셀 비율:\" &lt;&lt; a.devicePixelRatio(); qDebug() &lt;&lt; \"적용된 라운딩 정책:\" &lt;&lt; QGuiApplication::highDpiScaleFactorRoundingPolicy(); MainWindow m; m.show(); return QApplication::exec(); } mainwindow.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #ifndef QTPROJECT_MAINWINDOW_H #define QTPROJECT_MAINWINDOW_H #include &lt;QMainWindow&gt; QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACE class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); ~MainWindow() override; private: Ui::MainWindow *ui; }; #endif //QTPROJECT_MAINWINDOW_H mainwindow.cpp 1 2 3 4 5 6 7 8 9 10 11 #include \"mainwindow.h\" #include \"ui_mainwindow.h\" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui-&gt;setupUi(this); } MainWindow::~MainWindow() { delete ui; } main.cpp를 제외한 소스코드는 src,inclue,ui 폴더로 이동하여 작성한다. 코드 작성 후 reset cache and Reload project를 실행하여 설정이 꼬인 부분을 해결한다. 참고로 UI 파일을 클릭하면 Qt 설치 시 제공된 디자이너가 실행된다. msys64/ucrt64 QT예제 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 cmake_minimum_required(VERSION 4.2) project(msys2_qt) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_PREFIX_PATH \"C:/msys64/ucrt64\") find_package(Qt6 REQUIRED COMPONENTS Core Sql) find_package(SQLite3 REQUIRED) add_executable(${PROJECT_NAME} main.cpp db_raii.h ) target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Core Qt6::Sql SQLite::SQLite3 ) CMakeLists.txt 전체 내용 및 예제는 GitHub에서 확인할 수 있다." } , { "title": "C#에서 AOT로 NLog사용하기", "url": "/20260324-1/", "date": "2026-03-24", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "csharp, aot, helper", "content": "이전 글 C# AOT 라이브러리 활용에 이어 이번 글에서는 로그 시스템으로 많이 사용하는 NLog를 AOT (Ahead-of-Time)로 빌드하여 라이브러리로 사용하는 법을 알아본다. 실행파일과 같은 폴더에 AppLogs 폴더를 만들고 여기에 연월/날짜/로그타입별파일로 기록하고 디버그 모드로 빌드 후 실행 했을때 DebugViewPP로 실시간 로그를 볼 수 있도록 하였다. 기본 패키지의 리플랙션(Reflection)1을 피해서 작성해야 하므로 기본 사용법과 차이가 있을 수 있다. 참고로 Avalonia + MVVM로 구성된 프로젝트를 AOT로 빌드할 수 있도록 주요 패키지를 하나씩 옮겨보려고 한다. LogHelper(AOT) 프로젝트(LogHelper) 구성 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 &lt;Project Sdk=\"Microsoft.NET.Sdk\"&gt; &lt;PropertyGroup&gt; &lt;TargetFramework&gt;net10.0&lt;/TargetFramework&gt; &lt;ImplicitUsings&gt;disable&lt;/ImplicitUsings&gt; &lt;Nullable&gt;enable&lt;/Nullable&gt; &lt;PublishAot&gt;true&lt;/PublishAot&gt; &lt;IsAotCompatible&gt;true&lt;/IsAotCompatible&gt; &lt;AllowUnsafeBlocks&gt;true&lt;/AllowUnsafeBlocks&gt; &lt;OptimizationPreference&gt;Speed&lt;/OptimizationPreference&gt; &lt;/PropertyGroup&gt; &lt;ItemGroup&gt; &lt;PackageReference Include=\"NLog\" Version=\"6.1.1\"/&gt; &lt;PackageReference Include=\"NLog.OutputDebugString\" Version=\"6.1.1\"/&gt; &lt;/ItemGroup&gt; &lt;Target Name=\"CopyLogHelperDebug\" AfterTargets=\"Publish\"&gt; &lt;Copy Condition=\"'$(Configuration)' == 'Debug'\" SourceFiles=\"$(PublishDir)LogHelper.dll\" DestinationFiles=\"$(PublishDir)LogHelper.Debug.dll\" SkipUnchangedFiles=\"true\"/&gt; &lt;Copy Condition=\"'$(Configuration)' == 'Release'\" SourceFiles=\"$(PublishDir)LogHelper.dll\" DestinationFiles=\"$(PublishDir)LogHelper.Release.dll\" SkipUnchangedFiles=\"true\"/&gt; &lt;/Target&gt; &lt;/Project&gt; 소스 AOT(LogHelper.cs) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 using NLog; using NLog.Config; using NLog.Targets; using NLog.Targets.Wrappers; using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace LogHelper; public static class LogHelper { private static readonly Logger? _logger; private static readonly LogLevel[] _nLogLevels = [ LogLevel.Trace, LogLevel.Info, LogLevel.Warn, LogLevel.Error, LogLevel.Fatal, LogLevel.Debug ]; static LogHelper() { ISetupBuilder logFactory = LogManager.Setup().LoadConfiguration(builder =&gt; { LoggingConfiguration config = builder.Configuration; const string layout = @\"[${date:format=HH\\:mm\\:ss} ${level:uppercase=true:padding=5} : PID ${processid:Padding=5}] ${message} (${event-properties:File}::${event-properties:Member}[${event-properties:Line}][${threadid}])\"; builder.ForLogger().FilterMinLevel(LogLevel.Trace); #if DEBUG OutputDebugStringTarget debugTarget = new(\"debug\") { Layout = layout }; LoggingRule debugRule = new(\"*\", LogLevel.Trace, LogLevel.Fatal, debugTarget) { Final = false }; config.LoggingRules.Add(debugRule); #endif FileTarget fileTarget = new(\"FileTarget\") { FileName = \"${basedir}/AppLogs/${date:format=yyyyMM}/${date:format=dd}/${level}_${date:format=yyyyMMdd}.txt\", Layout = layout, KeepFileOpen = true, MaxArchiveDays = 90 // 선택사항 ArchiveFileName = \"${basedir}/AppLogs/${date:format=yyyyMM}/${date:format=dd}/${level}_${date:format=yyyyMMdd}.{#}.txt\", ArchiveAboveSize = 10485760, // 10MB마다 새 파일 생성 ArchiveEvery = FileArchivePeriod.Day }; AsyncTargetWrapper asyncWrapper = new(fileTarget) { QueueLimit = 10000, OverflowAction = AsyncTargetWrapperOverflowAction.Discard, TimeToSleepBetweenBatches = 0 }; LoggingRule rule = new(\"*\", LogLevel.Trace, LogLevel.Fatal, asyncWrapper) { Final = true }; config.LoggingRules.Add(rule); }); _logger = logFactory.GetLogger(\"NativeLogger\"); } [UnmanagedCallersOnly(EntryPoint = \"LogWrite\", CallConvs = [typeof(CallConvCdecl)])] public static void NativeLog_Write(int level, IntPtr msgPtr, IntPtr fileNamePtr, IntPtr memberPtr, int line) { try { LogLevel nLevel = (uint)level &lt; (uint)_nLogLevels.Length ? _nLogLevels[level] : LogLevel.Trace; if (_logger != null &amp;&amp; !_logger.IsEnabled(nLevel)) { return; } if (msgPtr == IntPtr.Zero) { return; } string? msg = Marshal.PtrToStringUTF8(msgPtr); if (string.IsNullOrWhiteSpace(msg)) { return; } string member = memberPtr != IntPtr.Zero ? Marshal.PtrToStringUTF8(memberPtr) ?? \"Unknown\" : \"Unknown\"; string file = fileNamePtr != IntPtr.Zero ? GetFileNameWithoutExtension(fileNamePtr) : \"Unknown\"; LogEventInfo logEvent = LogEventInfo.Create(nLevel, _logger?.Name, msg); logEvent.Properties[\"Member\"] = member; logEvent.Properties[\"Line\"] = line; logEvent.Properties[\"File\"] = file; _logger?.Log(logEvent); } catch { // Native 영역으로 예외가 전파되지 않도록 차단 (Access Violation 방지) } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe string GetFileNameWithoutExtension(IntPtr ptr) { ReadOnlySpan&lt;byte&gt; utf8Span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)ptr); int lastSlash = utf8Span.LastIndexOf((byte)'\\\\'); if (lastSlash == -1) { lastSlash = utf8Span.LastIndexOf((byte)'/'); } if (lastSlash &gt;= 0) { utf8Span = utf8Span[(lastSlash + 1)..]; } int dot = utf8Span.LastIndexOf((byte)'.'); if (dot &gt;= 0) { utf8Span = utf8Span[..dot]; } return System.Text.Encoding.UTF8.GetString(utf8Span); } [UnmanagedCallersOnly(EntryPoint = \"LogShutdown\")] public static void NativeLog_Shutdown() { try { LogManager.Flush(TimeSpan.FromSeconds(2)); LogManager.Shutdown(); } catch { // Native 영역으로 예외가 전파되지 않도록 차단 } } } dotnet publish -r win-x64 -c Release, dotnet publish -r win-x64 -c Debug를 LogHelper.csproj 프로젝트 안에서 실행한 후 LogHelper.Release.dll, LogHelper.Debug.dll을 publish 폴더에서 복사하여 메인 프로그램에서 사용한다. 메인프로그램(콘솔) 프로젝트 구성(NativeTest) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 &lt;Project Sdk=\"Microsoft.NET.Sdk\"&gt; &lt;PropertyGroup&gt; &lt;OutputType&gt;Exe&lt;/OutputType&gt; &lt;TargetFramework&gt;net10.0&lt;/TargetFramework&gt; &lt;ImplicitUsings&gt;disable&lt;/ImplicitUsings&gt; &lt;Nullable&gt;enable&lt;/Nullable&gt; &lt;AllowUnsafeBlocks&gt;true&lt;/AllowUnsafeBlocks&gt; &lt;/PropertyGroup&gt; &lt;!-- aot가 아닌 콘솔에서 테스트용도, aot만 불러오면 필요없음 --&gt; &lt;ItemGroup&gt; &lt;PackageReference Include=\"NLog\" Version=\"6.1.1\" /&gt; &lt;PackageReference Include=\"NLog.OutputDebugString\" Version=\"6.1.1\" /&gt; &lt;/ItemGroup&gt; &lt;/Project&gt; 메인프로그램 소스(Program.cs) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 using System; using System.Threading.Tasks; namespace NativeTest; internal class Program { private static async Task Main() { LogHelper.Info(\"info\"); LogHelper.Warn(\"Warn\"); LogHelper.Error(\"Error\"); LogHelper.Fatal(\"Fatal\"); LogHelper.Debug(\"Debug\"); LogHelper.Trace(\"Trace\"); Console.WriteLine(\"헬로우월드\"); Console.WriteLine(\"종료 중...\"); bool isLogClose = await Task.Run(()=&gt; { LogHelper.Shutdown(); return true; }); Console.WriteLine(isLogClose ? \"로그 Flush 완료\" : \"로그 Flush 에러\"); Console.WriteLine(\"프로그램 종료\"); } } 콘솔에서 사용하는 Helper클래스 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace NativeTest; public partial class LogHelper { #if DEBUG private const string DLL_NAME = \"LogHelper.Debug.dll\"; #else private const string DLL_NAME = \"LogHelper.Release.dll\"; #endif private static readonly bool _isNativeLoaded; static LogHelper() { _isNativeLoaded = NativeLibrary.TryLoad(DLL_NAME, Assembly.GetExecutingAssembly(), null, out _); } private enum LogLevel { TRACE = 0, INFO = 1, WARN = 2, ERROR = 3, FATAL = 4, DEBUG = 5 } [LibraryImport(DLL_NAME, EntryPoint = \"LogWrite\", StringMarshalling = StringMarshalling.Utf8)] private static partial void LogWrite(int level, string msg, string file, string member, int line); [LibraryImport(DLL_NAME, EntryPoint = \"LogShutdown\")] private static partial void LogShutdown(); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void Write(LogLevel level, string msg, string file, string member, int line) { if (!_isNativeLoaded) { return; } LogWrite((int)level, msg, file, member, line); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Trace(string msg, [CallerFilePath] string f = \"\", [CallerMemberName] string m = \"\", [CallerLineNumber] int l = 0) =&gt; Write(LogLevel.TRACE, msg, f, m, l); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Info(string msg, [CallerFilePath] string f = \"\", [CallerMemberName] string m = \"\", [CallerLineNumber] int l = 0) =&gt; Write(LogLevel.INFO, msg, f, m, l); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Warn(string msg, [CallerFilePath] string f = \"\", [CallerMemberName] string m = \"\", [CallerLineNumber] int l = 0) =&gt; Write(LogLevel.WARN, msg, f, m, l); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Error(string msg, [CallerFilePath] string f = \"\", [CallerMemberName] string m = \"\", [CallerLineNumber] int l = 0) =&gt; Write(LogLevel.ERROR, msg, f, m, l); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Fatal(string msg, [CallerFilePath] string f = \"\", [CallerMemberName] string m = \"\", [CallerLineNumber] int l = 0) =&gt; Write(LogLevel.FATAL, msg, f, m, l); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Debug(string msg, [CallerFilePath] string f = \"\", [CallerMemberName] string m = \"\", [CallerLineNumber] int l = 0) =&gt; Write(LogLevel.DEBUG, msg, f, m, l); public static void Shutdown() { if (!_isNativeLoaded) { return; } try { LogShutdown(); } catch { // 종료 시의 예외는 무시해도 안전한 경우가 많음 } } } 위의 소스는 AOT의 LogHelper가 아니고 콘솔에서 라이브러리를 사용하기 위한 헬퍼클래스 이다. 이름이 같으므로 혼동하지 말 것. AOT없이 않고 직접 NLog 사용하기 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 using NLog; using NLog.Config; using NLog.Targets; using NLog.Targets.Wrappers; using System; using System.Runtime.CompilerServices; namespace NativeTest; public static class LogTest { private static readonly Logger _logger; private const string EX = \" &gt;&gt; \"; static LogTest() { LoggingConfiguration config = new(); const string common_layout = @\"[${date:format=HH\\:mm\\:ss} ${level:uppercase=true:padding=5} : PID ${processid:Padding=5}] ${message}${onexception:${exception:format=message}} (${callsite:className=true:includeNamespace=false:methodName=true}[${callsite-linenumber}][${threadid}])\"; const string log_folder = \"${basedir}/AppLogs/${date:format=yyyyMM}/${date:format=dd}\"; FileTarget fileTarget = new(\"FileTarget\") { FileName = log_folder + \"/${level}_${date:format=yyyyMMdd}.txt\", ArchiveFileName = log_folder + \"/${level}_${date:format=yyyyMMdd}.{#}.txt\", Layout = common_layout, Encoding = System.Text.Encoding.UTF8, KeepFileOpen = true, OpenFileCacheTimeout = 30, AutoFlush = false, BufferSize = 65536, ArchiveEvery = FileArchivePeriod.Day, MaxArchiveDays = 90, ArchiveAboveSize = 10485760 }; AsyncTargetWrapper asyncWrapper = new(fileTarget, 10000, AsyncTargetWrapperOverflowAction.Grow) { TimeToSleepBetweenBatches = 0 }; config.AddRule(LogLevel.Trace, LogLevel.Fatal, asyncWrapper); #if DEBUG OutputDebugStringTarget debugOutput = new(\"debugOutput\") { Layout = common_layout }; AsyncTargetWrapper asyncDebug = new(debugOutput, 5000, AsyncTargetWrapperOverflowAction.Discard); config.AddRule(LogLevel.Trace, LogLevel.Fatal, asyncDebug); #endif LogManager.Configuration = config; _logger = LogManager.GetCurrentClassLogger(); } [MethodImpl(MethodImplOptions.NoInlining)] private static void WriteLog(LogLevel level, string message, Exception? ex = null) { if (!_logger.IsEnabled(level)) { return; } try { LogEventInfo logEvent = new(level, _logger.Name, message) { Exception = ex }; _logger.Log(typeof(LogTest), logEvent); } catch { // ignored } } public static void Trace(string msg) =&gt; WriteLog(LogLevel.Trace, msg); public static void Debug(string msg) =&gt; WriteLog(LogLevel.Debug, msg); public static void Info(string msg) =&gt; WriteLog(LogLevel.Info, msg); public static void Warn(string msg) =&gt; WriteLog(LogLevel.Warn, msg); public static void Error(string msg) =&gt; WriteLog(LogLevel.Error, msg); public static void Error(Exception ex, string msg) =&gt; WriteLog(LogLevel.Error, msg + EX, ex); public static void Fatal(string msg) =&gt; WriteLog(LogLevel.Fatal, msg); public static void Fatal(Exception ex, string msg) =&gt; WriteLog(LogLevel.Fatal, msg + EX, ex); public static void Shutdown() { try { LogManager.Flush(TimeSpan.FromSeconds(2)); LogManager.Shutdown(); } catch { // ignored } } } 위의 소스는 AOT가 아닌 프로젝트에서 바로 NLog를 사용할 때 필요한 소스코드이고 LogTest.Error(\"Error\"); 또는 LogTest.Error(ex, \"Error\"); 형태로 필요한 곳에서 바로 사용한다. 즉, AOT용 하나, 메인에서 바로 사용하는 파일 하나 이렇게 2개로 예제를 작성한 것이다. 애플케애션 설정(App.axml.cs) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); // UI 스레드 예외 this.DispatcherUnhandledException += (s, args) =&gt; { LogHelper.Critical($\"[UI Error] {args.Exception.Message}\\n{args.Exception.StackTrace}\"); args.Handled = true; // 에러메시지 후 앱은 실행 }; //작업 스레드 및 도메인 예외(task.run) AppDomain.CurrentDomain.UnhandledException += (s, args) =&gt; { if (args.ExceptionObject is Exception ex) { LogHelper.Fatal($\"[Fatal Error] {ex.Message}\\n{ex.StackTrace}\"); } LogHelper.Shutdown(); }; // 비동기 Task 예외(await없는 task) TaskScheduler.UnobservedTaskException += (s, args) =&gt; { LogHelper.Error($\"[Async Error] {args.Exception.Message}\"); args.SetObserved(); // 앱 종료 방지 }; } // private static async void OnClosing(object? sender, CancelEventArgs e) protected override void OnExit(ExitEventArgs e) { LogHelper.Shutdown(); base.OnExit(e); } } 이 부분은 프로그램 만들 때 필수로 처리해야 하는 코드의 예이다. Reference C#에서 리플랙션(Reflection)은 실행 중(Runtime)에 객체의 형식(Type), 메서드, 필드, 프로퍼티 등의 메타데이터를 조사하거나 조작할 수 있게 해주는 기능인데 컴파일 시점에 실행될 코드를 예측할 수 없어서 AOT에서는 피해야 하며 소스 생성기(Source Generators)를 통하여 빌드될 수 있도록 해야 한다. 주로 partial 키워드와 필요한 객체 위에 Attribute를 사용한다." } , { "title": "C# AOT 라이브러리 활용", "url": "/20260322-1/", "date": "2026-03-22", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "csharp, aot, helper", "content": "C#은 관리형 언어(Managed Language)이다. 개발자가 메모리 할당과 해제를 하는 것을 CLR(런타임 환경)이 이를 대신해 주고 가비지 컬렉션(Garbage Collection)으로 사용하지 않는 메모리를 시스템이 자동으로 회수하여 메모리 누수를 방지한다. 대신에 빌드하면 소스코드가 바로 기계어로 바뀌는 것이 아니라 중간 언어(IL, Intermediate Language)로 컴파일된 실행 시점에 JIT(Just-In-Time)에 의해 기계어로 번역된다. 그래서 파일(exe, dll)을 ILSpy 같은 툴로 보면 소스코드가 훤히 보여 중요한 문자열을 숨기지 못하는 단점이 있다. C#의 AOT(Ahead-of-Time) 컴파일은 애플리케이션을 실행하기 전(빌드 시점)에 코드를 해당 운영체제와 CPU가 이해할 수 있는 네이티브 기계어로 미리 번역해 두는 기술이다. 이것 때문에 이를 활용하면 ILSpy툴로 내용(소스)을 볼 수 없다. 다만 문자열은 헥사에니터 같은것으로 충분히 볼 수 있기 때문에 이를 보완해야 한다. C++ constexpr, zig comptime, rust의 매크로함수인 obfstr!함수를 이용하여 컴파일 타임에 문자열을 난독화한다. 여기에서는 AOT만을 이용하여 기본을 학습한 후 이것을 토대로 zig, rust, c++를 라이브러리로 만들어 c#에서 활용해 보려고 한다. AOT 라이브러리 프로젝트(NativeTestLib) 구성 1 2 3 4 5 6 7 8 9 10 &lt;Project Sdk=\"Microsoft.NET.Sdk\"&gt; &lt;PropertyGroup&gt; &lt;TargetFramework&gt;net10.0&lt;/TargetFramework&gt; &lt;ImplicitUsings&gt;disable&lt;/ImplicitUsings&gt; &lt;Nullable&gt;enable&lt;/Nullable&gt; &lt;PublishAot&gt;true&lt;/PublishAot&gt; &lt;IsAotCompatible&gt;true&lt;/IsAotCompatible&gt; &lt;AllowUnsafeBlocks&gt;true&lt;/AllowUnsafeBlocks&gt; &lt;/PropertyGroup&gt; &lt;/Project&gt; 소스(NativeTestLib.cs) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace NativeTestLib; public static class NativeMain { private static readonly byte[] _encryptedData = [ 0xBA, 0xCF, 0xF5, 0xB1, 0xFA, 0xC0, 0xB1, 0xC4, 0xEF, 0x8C, 0xFA, 0xF6, 0x88, 0xF7, 0xF9, 0x4B, 0x34, 0x0D, 0x0A, 0x1F, 0x19, 0x09, 0x29, 0x0F, 0x1B, 0x11 ]; private const byte BASE_KEY = 0x57; [UnmanagedCallersOnly(EntryPoint = \"GetSecureData\")] public static unsafe int GetSecureData(byte* buffer, int bufferLen) { try { int dataLen = _encryptedData.Length; if (buffer == null) { return dataLen; } if (bufferLen &lt; dataLen) { return -1; } for (int i = 0; i &lt; dataLen; i++) { buffer[i] = DecryptByte(_encryptedData[i], i); } return dataLen; } catch { return -2; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static byte DecryptByte(byte b, int index) { return (byte)(b ^ (BASE_KEY + index)); } } 작성 후 NativeTestLib.csproj에서 dotnet publish -r win-x64 -c Release를 실행하면 publish 폴더에 NativeTestLib.dll 이 만들어지는데 이 파일을 메인 프로그램에서 사용한다. 참고로 여기에 소스는 unsafe 블록으로 포인터 개념을 사용한다. 메인 프로그램 프로젝트 구성(NativeTest) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 &lt;Project Sdk=\"Microsoft.NET.Sdk\"&gt; &lt;PropertyGroup&gt; &lt;OutputType&gt;Exe&lt;/OutputType&gt; &lt;TargetFramework&gt;net10.0&lt;/TargetFramework&gt; &lt;ImplicitUsings&gt;disable&lt;/ImplicitUsings&gt; &lt;Nullable&gt;enable&lt;/Nullable&gt; &lt;AllowUnsafeBlocks&gt;true&lt;/AllowUnsafeBlocks&gt; &lt;/PropertyGroup&gt; &lt;PropertyGroup&gt; &lt;RuntimeIdentifier Condition=\"'$(RuntimeIdentifier)' == ''\"&gt;win-x64&lt;/RuntimeIdentifier&gt; &lt;/PropertyGroup&gt; &lt;ItemGroup&gt; &lt;None Include=\"..\\NativeTestLib\\bin\\$(Configuration)\\$(TargetFramework)\\$(RuntimeIdentifier)\\publish\\NativeTestLib.dll\"&gt; &lt;Link&gt;NativeTestLib.dll&lt;/Link&gt; &lt;CopyToOutputDirectory&gt;Always&lt;/CopyToOutputDirectory&gt; &lt;/None&gt; &lt;/ItemGroup&gt; &lt;/Project&gt; 아이템 그룹으로 설정하면 되는데 동작이 안 될 때는 라이브러리(dll) 파일을 실행파일 있는 곳에 복사하여 사용한다. 메인프로그램 소스(Program.cs) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 using System; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; namespace NativeTest; internal partial class Program { [LibraryImport(\"NativeTestLib.dll\", EntryPoint = \"GetSecureData\", StringMarshalling = StringMarshalling.Utf8)] private static partial int GetSecureData(ref byte buffer, int bufferLen); [LibraryImport(\"NativeTestLib.dll\", EntryPoint = \"GetSecureData\", StringMarshalling = StringMarshalling.Utf8)] private static partial int GetSize(IntPtr buffer, int bufferLen); private static async Task&lt;string&gt; FetchSecureStringAsync() { try { return await Task.Run(() =&gt; { int requiredSize = GetSize(IntPtr.Zero, 0); if (requiredSize &lt;= 0) { return string.Empty; } Span&lt;byte&gt; buffer = stackalloc byte[requiredSize]; int actualLen = GetSecureData(ref MemoryMarshal.GetReference(buffer), buffer.Length); if (actualLen &lt;= 0) { return string.Empty; } string result = Encoding.UTF8.GetString(buffer[..actualLen]); buffer.Clear(); return result; }); } catch (DllNotFoundException ex) { Console.WriteLine($\"NotFound: {ex.Message}\"); return string.Empty; } catch (Exception ex) { Console.WriteLine($\"Exception: {ex.Message}\"); return string.Empty; } } private static async Task Main() { const string str = \"헬로우월드-SecureData\"; string temp = Helper.GenerateXorHex(str); Helper.Clipboard(temp); Console.WriteLine(temp); string secureKey = await FetchSecureStringAsync(); Console.WriteLine(string.IsNullOrWhiteSpace(secureKey) ? \"데이터없음\" : secureKey); } } 위의 소스 중 아래의 소스는 실제 프로젝트에 포함하지 말아야 한다. Helper.cs파일도 마찬가지. 난독화한 바이트 배열을 만들고 이것을 AOT 라이브러리 소스에 복사하여 사용한다. 1 2 3 4 const string str = \"헬로우월드-SecureData\"; string temp = Helper.GenerateXorHex(str); Helper.Clipboard(temp); Console.WriteLine(temp); Helper 클래스 메인에서 사용하는 Helper.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 using System; using System.Diagnostics; using System.Text; namespace NativeTest; public class Helper { public static string GenerateXorHex(string plainText, byte baseKey = 0x57) { byte[] bytes = Encoding.UTF8.GetBytes(plainText); StringBuilder hexBuilder = new(); hexBuilder.AppendLine($\"원본 문자열: {plainText}\"); hexBuilder.Append(\"private static readonly byte[] EncryptedData = { \"); for (int i = 0; i &lt; bytes.Length; i++) { // 가변 XOR 적용: (원본 바이트) ^ (기본키 + 인덱스) byte encoded = (byte)(bytes[i] ^ (baseKey + i)); hexBuilder.Append($\"0x{encoded:X2}\"); if (i &lt; bytes.Length - 1) hexBuilder.Append(\", \"); } hexBuilder.Append(\" };\"); return hexBuilder.ToString(); } public static void Clipboard(string text) { if (string.IsNullOrWhiteSpace(text)) { return; } string base64Text = Convert.ToBase64String(Encoding.Unicode.GetBytes(text)); string script = $\"[System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String('{base64Text}')) | Set-Clipboard\"; using Process process = new(); process.StartInfo.FileName = \"powershell\"; process.StartInfo.Arguments = $\"-NoProfile -ExecutionPolicy Bypass -Command \\\"{script}\\\"\"; process.StartInfo.CreateNoWindow = true; process.StartInfo.UseShellExecute = false; process.Start(); process.WaitForExit(); } } 클립보드 함수는 복사 붙여넣기를 바로 하기 위하여 포함하였다 암튼 이 helper 클래스는 실제 프로젝트에는 배포 시 제외해야 한다. 이것으로 AOT를 이용하여 간단한 문자열 숨김을 해보았다. 실무에서는 더 복잡하게 작성해야 하는데 이 부분은 다음 글 작성 시 zig, rust, c++로 라이브러리를 만들어 c#에서 호출해 볼 것이다. 바로 호출하기 보다는 중간에 AOT를 매개로 연결한다. 참고로 AOT로 빌드한 네이티브 파일도 GC의 영향을 받는다." } , { "title": "Rust Web Frameworks 비교", "url": "/20250903-2/", "date": "2025-09-03", "categories": "【 RustㆍZigㆍGo 】, Rust", "tags": "rust, api", "content": "Rust는 dotnet(C#)에 비해 성능이 우수하다고 알려져 있다.1 대표적인 Rust의 Web Frameworks에는 대표적으로 Actix Web, Rocket, Warp, Axum, Poem 등이 있는데 각각의 특징을 간략하게 예제(Axum)와 더불어 정리하였다. 학습에 도움이 될 만한 youtube 강좌 Mike Code, “Actix-web-tutorial” Mike Code, “Axum-tutorial” Smart Contract Programmer, “Learn Rust” TechEmpower 벤치마크 결과 (Round 21) 프레임워크 언어 요청/초 지연시간(ms) CPU 사용률 Actix-web (Rust) Rust 748,051 1.7 98% Axum (Rust) Rust 723,892 1.8 97% ASP.NET Core C# 692,345 2.1 95% Spring Boot Java 153,846 6.5 92% Express.js Node.js 58,824 17.0 85% dotNET의 경우 Rust에 비해 떨어지지 않는 성능을 보여준다. Rust Web Framework vs. .NET WebAPI 성능 비교2 항목 Rust Web Framework .NET WebAPI 동시 처리 매우 우수. Rust의 zero-cost abstraction과 안전한 메모리 모델 덕분에 수천~수만 동시 연결 처리 가능 Kestrel 서버 기반의 .NET Core도 수천 동시 연결 가능. 스레드풀과 async/await로 효율적 처리 동시성 Rust는 기본적으로 데이터 레이스를 방지. Send/Sync 트레이트로 안전하게 동시성 구현 .NET도 스레드 안전성 제공. Task 기반 비동기 모델로 동시성 구현. 그러나 메모리 리스크는 비교적 높음 비동기 처리 async/await, non-blocking IO, Tokio/async-std 등 강력한 지원 async/await, non-blocking IO, 스레드풀 기반 비동기 처리 지원 성능 벤치마크 Actix Web 기준: wrk, techempower benchmarks에서 가장 빠른 성능 자주 기록 (수백만 req/s) ASP.NET Core: wrk, techempower benchmarks에서 상위권. Rust보다 약간 낮거나 비슷한 수준 메모리 사용량 매우 낮음. Rust의 ownership 시스템 덕분에 GC 오버헤드 없음 GC 기반이라 Rust보다 메모리 사용량 많음. 하지만 최적화로 많이 개선됨 안정성 컴파일 타임에 대부분의 오류 검출. 런타임 오류 적음 런타임에서 오류 발견 가능성 더 있음. C# 타입 시스템으로 어느 정도 방지 커뮤니티/생태계 안정적이고 빠르게 성장 중. 크지는 않음 매우 크고 성숙. 다양한 라이브러리와 툴링 지원 기술적 차이점의 요인으로 ‘메모리 관리’, ‘동시성 모델’, ‘네트워킹 스택’을 보면 Rust가 dotNET에 비해 상당히 성능이 우수하다. 그러나 극한의 성능과 안정성보다 생산성, 툴링, 윈도우/엔터프라이즈 환경친화성이 우선이라면 dotNET WebAPI가 더 우수하다고 볼 수 있다. Rust 프레임워크 비교3 프레임워크 주된 특징 성능 사용 편의성 에코시스템 / 확장성 Actix Web 매우 높은 성능, Actor 기반 아키텍처, WebSocket 및 다양한 기능 내장 최상위권 (최고 처리량, 낮은 레이턴시) 다소 복잡 (액터 모델, 매크로 활용 등) 가장 성숙하고 확장성 높은 생태계 (ORM, 인증, 미들웨어 등) Rocket 간결하고 타입 안전한 라우팅, Request Guards, Fairings(미들웨어) 좋은 성능, Actix에는 약간 밀림 초보자에게 매우 친숙한 API, 매크로 기반으로 간단 점진적으로 성장 중, 폼 처리 및 DB 등의 지원도 증가 중 Warp 필터 기반의 컴포저형 설계, 하이퍼 기반, 매우 유연함 가볍고 빠름, 오버헤드 최소화 필터 체이닝이 직관적이지만 초기 학습 곡선 존재 에코시스템은 작지만 비동기 유틸리티 중심으로 확장 중 Axum Tokio + Hyper + Tower 기반, macro-free, 라우터 중심 설계 강력한 비동기 성능, 높은 처리량 직관적이고 러스트 초심자에게도 친숙한 API 빠르게 성장 중인 생태계, 웹 관련 크레이트 늘어남 Poem 간결하고 쉬운 사용성, minimalism 지향 빠름 (경량 기반), 규모에 따라 다름 단순하고 배우기 쉬움, 복잡 기능은 외부 크레이트 사용 필요 초기 단계지만 성장 중이고 커뮤니티 활성화 중 사용하기 쉽고 성능에 최적화된 비동기 프레임워크는 Axum이라고 볼 수 있다. 비동기 특화: Tokio + Hyper + Tower 기반 → async/await 친화적 성능: Actix Web과 큰 차이 없는 높은 처리량, 낮은 레이턴시 사용 편의성: 매크로 대신 함수/라우터 기반 API → 러스트 초보자도 직관적으로 사용 가능 에코시스템: Tower 미들웨어, tokio 생태계와 완벽 호환 → 확장성 뛰어남 현대적 설계: 최근 Rust async 웹 개발의 사실상 표준으로 자리잡는 추세 Axum 예제 1 2 3 4 5 6 7 8 9 10 11 12 # Cargo.toml [package] name = \"hello\" version = \"0.1.0\" edition = \"2024\" [dependencies] axum = \"0.8.1\" tokio = { version = \"1.47.1\", features = [\"full\"] } serde = { version = \"1.0.219\", features = [\"derive\"] } serde_json = \"1\" http = \"1.3.1\" 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 // main.rs use axum::{ extract::Json, http::StatusCode, response::IntoResponse, routing::post, Router, }; use serde::{Deserialize, Serialize}; use std::net::SocketAddr; use tokio::time::{sleep, Duration}; #[derive(Serialize)] struct User { id: u32, name: String, } #[derive(Serialize)] struct ErrorResponse { error: String, } #[derive(Deserialize)] struct UserRequest { id: u32, } async fn root() -&gt; impl IntoResponse { Json(serde_json::json!({\"message\": \"Hello, World!\"})) } async fn get_user(Json(req): Json&lt;UserRequest&gt;) -&gt; impl IntoResponse { // DB 조회 (비동기 처리) 가정 sleep(Duration::from_millis(300)).await; if req.id == 0 { let err = ErrorResponse { error: \"User not found\".to_string(), }; (StatusCode::NOT_FOUND, Json(err)).into_response() } else { let user = User { id: req.id, name: format!(\"User{}\", req.id), }; (StatusCode::OK, Json(user)).into_response() } } #[tokio::main] async fn main() { let app = Router::new() .route(\"/\", post(root)) .route(\"/users\", post(get_user)); let addr = SocketAddr::from(([0, 0, 0, 0], 3000)); println!(\"Server listening on {}\", addr); if let Err(e) = axum::serve(tokio::net::TcpListener::bind(addr).await.unwrap(), app).await { eprintln!(\"server error: {e:?}\"); } } 예제 테스트 1 2 3 # root API curl -X POST http://localhost:3000/ # {\"message\":\"Hello, World!\"} 1 2 3 # User조회 API(정상) curl -X POST http://localhost:3000/users -H \"Content-Type: application/json\" -d \"{\\\"id\\\": 1}\" # {\"id\":1,\"name\":\"User1\"} 1 2 3 # User조회 API(없음) curl -X POST http://localhost:3000/users -H \"Content-Type: application/json\" -d \"{\\\"id\\\": 0}\" # {\"error\":\"User not found\"} Reference techempower.com, “TechEmpower Benchmarks” Github Copilot, https://dev.to/leapcell/rust-web-frameworks-compared-actix-vs-axum-vs-rocket-4bad 기반 ChatGPT, https://dev.to/leapcell/rust-web-frameworks-compared-actix-vs-axum-vs-rocket-4bad 기반" } , { "title": "Free the memory used by a collection", "url": "/20250124-1/", "date": "2025-01-24", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "csharp", "content": "C# .NET은 Managed Code이다. .NET의 구성요소인 CLR(Common Language Runtime)에서 실행되는 코드 유형이다. 상대적으로 Unmanaged Code는 CLR의 개입 없이 운영체제나 하드웨어에서 직접 실행되는 코드 유형이다. C#보다 머신 수준에 더 가까운 C, C++ 와 같은 언어로 작성된다.1 Managed Code인 C#은 런타임 환경에서 자동으로 메모리 관리(GC, Garbage Collection)를 관리한다. IDisposable을 구현하는 개체를 사용할 때 using StreamReader와 같이 사용하면 함수의 블록이 끝날 때 자동으로 메모리에서 개체를 메모리에서 해제한다.2 간혹 개발자 중에 직접 메모리를 해제하고자 할 때 아래와 같이 코드를 작성하곤 하는 데 이러한 방법은 GC를 오해한 까닭에서 나온 결과이다.3 개선 전 코드 1 2 3 4 5 6 public static void ClearMemory&lt;T&gt;(this List&lt;T&gt; list) { int id = GC.GetGeneration(list); list.Clear(); GC.Collect(id, GCCollectionMode.Forced); } GC.Collect()를 호출하면 가비지 컬렉션이 실행될 가능성을 높이지만 Unmanaged 언어와 같이 바로 메모리가 반환되는 것을 보장하지 않는다. 즉, 힌트를 준 정도의 역할만 할 뿐이고 특히 GC를 자주 호출하면 CPU 시간을 소모하는 작업이기 때문에 오히려 성능에 악영향을 미칠 수 있다. 아래의 예제는 권장하는 코드와 함께 공부 차원에서 직접 작성한 몇 가지 클래스이다. 권장하는 코드 1 2 3 4 5 6 public static void ClearAll&lt;T&gt;(this List&lt;T&gt; list) { list.Clear(); list.Capacity = 0; // 또는 단순하게 list = null; 참조가 없다고 알림 } 위에서처럼 Clear()를 사용하여 리스트가 차지하고 있던 메모리 공간을 가비지 컬렉션의 대상이 되게 한다. 그리고 Capacity = 0을 적용하여 리스트가 내부적으로 가지고 있는 배열의 메모리도 가비지 컬렉션의 대상이 되게 한다. 닷넷에서는 이렇게 하여 자동으로 GC가 일어나도록 힌트를 줄 수 있다. 나머지는 .NET이 알아서 자동 처리한다. 확장 함수 구현 Disposable한 컬렉션은 DisposeAll()을 사용하고 그렇지 않은 개체는 ClearAll()을 사용한다. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public static class DisposableExtensions { public static void DisposeAll&lt;T&gt;(this List&lt;T&gt; list) where T : IDisposable { list.ForIn(x =&gt; x.Dispose()); } public static void DisposeAll&lt;T&gt;(this T[] array) where T : IDisposable { array.ForIn(x =&gt; x.Dispose()); } public static void DisposeAll&lt;T&gt;(this IEnumerable&lt;T&gt; collection) where T : IDisposable { collection.ForIn(x =&gt; x.Dispose()); } public static void ClearAll&lt;T&gt;(this List&lt;T&gt; list) { list.Clear(); list.Capacity = 0; } private static void ForIn&lt;T&gt;(this IEnumerable&lt;T&gt;? seq, Action&lt;T&gt; act) where T : IDisposable { if (seq == null) { return; } List&lt;Exception&gt; exceptions = []; foreach (T item in seq) { try { act(item); } catch (Exception ex) { exceptions.Add(ex); } } if (exceptions.Count &gt; 0) { throw new AggregateException($\"Disposing Item ERROR : {exceptions}\"); } } } IDisposable 패턴 적용 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public sealed class DisposeList&lt;T&gt; : List&lt;T&gt;, IDisposable { private bool mDisposedValue; private void Dispose(bool disposing) { if (mDisposedValue) { return; } if (disposing) { foreach (IDisposable item in this.OfType&lt;IDisposable&gt;()) { try { item.Dispose(); } catch (Exception ex) { LogHelper.Logger.Error($\"Disposing Item ERROR : {ex.Message}\"); } } Clear(); Capacity = 0; } mDisposedValue = true; } ~DisposeList() =&gt; Dispose(false); public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 public sealed class DisposeArray&lt;T&gt;(uint initialLength = 0) : IDisposable { private T[]? mArray = initialLength &gt; 0 ? new T[initialLength] : null; private bool mDisposedValue; public T this[int index] { get { ObjectDisposedException.ThrowIf(mDisposedValue, nameof(DisposeArray&lt;T&gt;)); if (mArray == null || index &lt; 0 || index &gt;= Count) { throw new IndexOutOfRangeException(); } return mArray[index]; } set { ObjectDisposedException.ThrowIf(mDisposedValue, nameof(DisposeArray&lt;T&gt;)); if (index &lt; 0) { throw new IndexOutOfRangeException(); } if (mArray == null) { mArray = new T[Math.Max(index + 1, 4)]; } else if (index &gt;= mArray.Length) { Array.Resize(ref mArray, Math.Max(index + 1, mArray.Length * 2)); } mArray[index] = value; Count = Math.Max(Count, index + 1); } } public int Count { get; private set; } public int Capacity { get =&gt; mArray?.Length ?? 0; } public T[] ToArray() { ObjectDisposedException.ThrowIf(mDisposedValue, nameof(DisposeArray&lt;T&gt;)); if (mArray == null || Count == 0) { return []; } T[] newArray = new T[Count]; Array.Copy(mArray, newArray, Count); return newArray; } public void Clear() { ObjectDisposedException.ThrowIf(mDisposedValue, nameof(DisposeArray&lt;T&gt;)); if (mArray != null) { Array.Clear(mArray, 0, mArray.Length); } Count = 0; } private void Dispose(bool disposing) { if (mDisposedValue) return; if (disposing) { if (typeof(IDisposable).IsAssignableFrom(typeof(T)) &amp;&amp; mArray != null) { List&lt;Exception&gt; exceptions = []; foreach (IDisposable item in mArray.OfType&lt;IDisposable&gt;()) { try { item.Dispose(); } catch (Exception ex) { exceptions.Add(ex); } } if (exceptions.Count != 0) { throw new AggregateException($\"Disposing Item ERROR : {exceptions}\"); } } else { mArray = null; } } mDisposedValue = true; } ~DisposeArray() =&gt; Dispose(false); public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } } 1 2 3 // 사용 예제 using DisposeList&lt;string&gt; comList = []; using DisposeArray&lt;string&gt; comArray = new(10); //완벽하지 않지만 동적 배열 가능 위의 클래스는 ‘확장 함수 구현’에서 보여준 것을 IDisposable 패턴을 적용하여 자동으로 처리하도록 구현하였다. 굳이 이렇게 사용할 필요는 없고 Clear(), Capacity = 0으로 충분하며, 당연하지만 StreamReader, Network의 Connection과 같이 IDisposable이 적용된 개체는 using을 사용해야 한다. 참고로 위에서 직접 작성한 소스를 deepseek에 질문했더니 아래와 같은 답변을 해주었다. 비교해서 학습하는 데 도움이 되길 바라면서 소스코드를 아래에 옮겨본다. 옮기는 과정에서 약간의 소스 추가(IEnumerable&lt;T&gt;,GetEnumerator 관련)및 정리는 하였다. 1 2 3 4 5 6 7 // 사용 예제 // List using DisposeList&lt;string&gt; comList = new(); using DisposeList&lt;string&gt; comList = []; // Array using DisposeArray&lt;string&gt; comList = new(10); using DisposeArray&lt;string&gt; comList = new(); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 public sealed class DisposeList&lt;T&gt; : IDisposable, IEnumerable&lt;T&gt; { private readonly List&lt;T&gt; mList = []; private bool mDisposedValue; public T this[int index] { get =&gt; mList[index]; set =&gt; mList[index] = value; } public int Count { get =&gt; mList.Count; } public int Capacity { get =&gt; mList.Capacity; } public void Add(T item) =&gt; mList.Add(item); public bool Remove(T item) =&gt; mList.Remove(item); public void Clear() =&gt; mList.Clear(); public IEnumerator&lt;T&gt; GetEnumerator() =&gt; mList.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() =&gt; GetEnumerator(); private void Dispose(bool disposing) { if (mDisposedValue) return; if (disposing) { List&lt;Exception&gt; exceptions = []; foreach (T item in mList) { if (item is not IDisposable disposable) { continue; } try { disposable.Dispose(); } catch (Exception ex) { exceptions.Add(ex); } } if (exceptions.Count &gt; 0) { throw new AggregateException(\"DisposeList disposal failed\", exceptions); } mList.Clear(); } mDisposedValue = true; } ~DisposeList() =&gt; Dispose(false); public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 public sealed class DisposeArray&lt;T&gt; : IDisposable { private T[]? mArray; private bool mDisposedValue; public DisposeArray(uint initialLength = 0) { if (initialLength &gt; 0) mArray = new T[initialLength]; } public T this[int index] { get { ObjectDisposedException.ThrowIf(mDisposedValue, this); if (mArray == null || index &lt; 0 || index &gt;= Count) { throw new IndexOutOfRangeException(); } return mArray[index]; } set { ObjectDisposedException.ThrowIf(mDisposedValue, this); if (index &lt; 0) { throw new IndexOutOfRangeException(); } if (mArray == null) { mArray = new T[Math.Max(index + 1, 4)]; } else if (index &gt;= mArray.Length) { Array.Resize(ref mArray, Math.Max(index + 1, mArray.Length * 2)); } if (mArray[index] is IDisposable disposable) { disposable.Dispose(); } mArray[index] = value; Count = Math.Max(Count, index + 1); } } public int Count { get; private set; } public int Capacity { get =&gt; mArray?.Length ?? 0; } public T[] ToArray() { ObjectDisposedException.ThrowIf(mDisposedValue, this); return mArray == null || Count == 0 ? [] : mArray[..Count]; } private void Dispose(bool disposing) { if (mDisposedValue) return; if (disposing &amp;&amp; mArray != null) { List&lt;Exception&gt; exceptions = []; foreach (T item in mArray) { if (item is not IDisposable disposable) { continue; } try { disposable.Dispose(); } catch (Exception ex) { exceptions.Add(ex); } } if (exceptions.Count &gt; 0) { throw new AggregateException(exceptions); } mArray = null; } mDisposedValue = true; } ~DisposeArray() =&gt; Dispose(false); public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } } Reference c-sharpcorner.com, “Managed vs. Unmanaged Code in .NET” C++에서 smart pointer를 사용하고, Rust 언어에서는 언어 수준 자체의 기능이다. stackoverflow.com, “How free memory used by a large list in C#?, Answer:Oswaldo Junior”" } , { "title": "C#, Button UI 이벤트 제어하기", "url": "/20250105-1/", "date": "2025-01-05", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "csharp, helper", "content": "버튼을 클릭하고 이벤트 핸들러에서 내용을 처리 중일 때는 버튼을 잠시 잠그고 처리 완료 후 버튼의 잠금을 해제하여 이중 클릭 방지와 처리 과정이 동작 중임을 가시적으로 표현하곤 한다. 이 때에 필요한 방법을 4가지 정도로 소개할까 한다. 테스트 환경은 .NET 8.0 WPF 이다. 일반적으로 간단하게 처리하고자 한다면 아래와 같이 코드를 작성할 것이다. 개선 전 코드 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public static void ButtonOnTest(object sender, RoutedEventArgs e) { if (sender is not Button btn) { return; } string? orgContent = btn.Content?.ToString(); try { btn.Content = \"처리중...\"; btn.IsEnabled = false; // 해당 작업이 있다고 가정 Thread.Sleep(5000); } catch (Exception ex) { Console.WriteLine(ex.Message); // Log처리 가정 } finally { btn.Content = orgContent; btn.IsEnabled = true; } } 위의 코드는 문제가 2가지 정도 있는데 하나는 동작 중에 화면이 잠기는 것이고 두 번째는 이중 클릭을 방지할 수 없다는 것이다. 아래의 코드는 비동기 방식으로 처리하여 간단하게 개선한 것이다. 일반적인 비동기로 처리 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public static async Task ButtonOnTest(object sender, RoutedEventArgs e) { if (sender is not Button btn) { return; } string? orgContent = btn.Content?.ToString(); try { btn.Content = \"처리중...\"; btn.IsEnabled = false; // 해당 작업이 있다고 가정 await Task.Delay(5000); } catch (Exception ex) { Console.WriteLine(ex.Message); // Log처리 가정 } finally { btn.Content = orgContent; btn.IsEnabled = true; } } 문제는 해결된 듯 보이지만 여기에 진행 과정이라든지 finally 부분을 필수적으로 사용해야 한다든지 등의 불편한 점을 해결해 보고 굳이 아래의 4가지 정도의 방법은 사용하지 않더라도 학습에 도움이 될 만한 것들이다. IDisposable 활용 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public sealed class DisposeButton : IDisposable { private readonly Button mButton; private readonly string mOrgContent; public DisposeButton(Button? button, string? spinText = null) { ArgumentNullException.ThrowIfNull(button); mButton = button; mOrgContent = button.Content?.ToString() ?? string.Empty; mButton.Content = string.IsNullOrWhiteSpace(spinText) ? \"처리중...\" : spinText; mButton.IsEnabled = false; } ~DisposeButton() =&gt; Dispose(false); private void Dispose(bool disposing) { if (disposing) { mButton.Dispatcher.Invoke(() =&gt; { mButton.Content = mOrgContent; mButton.IsEnabled = true; }); } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } } 클릭 후 작업이 완료되면 자동으로 잠금 해제 1 2 3 4 5 6 public static async Task ButtonOnTest(object sender, RoutedEventArgs e) { using DisposeButton _ = new(sender as Button, \"동작중...\"); // 해당 작업이 있다고 가정 await Task.Delay(5000); } IAsyncDisposable 활용 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public class DisposeButtonAsync : IAsyncDisposable { private readonly Button mButton; private readonly Func&lt;Task&gt;? mFunc; private readonly string mOrgContent; public DisposeButtonAsync(Button? button, Func&lt;Task&gt;? fn = null) { ArgumentNullException.ThrowIfNull(button); mButton = button; mFunc = fn; mOrgContent = button.Content?.ToString() ?? string.Empty; mButton.Content = \"처리중...\"; mButton.IsEnabled = false; } #pragma warning disable CA1816 public async ValueTask DisposeAsync() #pragma warning restore CA1816 { try { await (mFunc?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); } catch (Exception ex) { LogHelper.Logger.Error($\"DisposeButtonAsync(DisposeAsync) : ERROR : {ex.Message}\"); } finally { try { await mButton.Dispatcher.InvokeAsync(() =&gt; { mButton.Content = mOrgContent; mButton.IsEnabled = true; }); } catch (Exception ex) { LogHelper.Logger.Error($\"DisposeButtonAsync(Dispatcher.InvokeAsync) : ERROR : {ex.Message}\"); } } } } Func 델리게이트 사용 1 2 3 4 5 6 7 8 public static async Task ButtonOnTest(object sender, RoutedEventArgs e) { await using DisposeButtonAsync _ = new(sender as Button, async () =&gt; { // 해당 작업이 있다고 가정 await Task.Delay(5000); }); } 참고로 IDisposable, IAsyncDisposable, GC.SuppressFinalize(this)의 관계를 아래에 요약(Gemini 2.0)했다. Microsoft에서 설명한 방법과 비교해 보길 바란다.1 인터페이스 파이널라이저 GC.SuppressFinalize(this) IDisposable 필요할 수 있음 Dispose()에서 호출 필요 IAsyncDisposable 거의 필요 없음 사용하면 안 됨 비동기 개선 추가 예제 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static class FrameworkElementExtensions { /// &lt;summary&gt; /// sender(object)가 Button일 경우 로딩 상태로 전환합니다. /// &lt;/summary&gt; public static DisposeButtonAsync BeginLoading(this object sender, string loadingText = \"처리중...\") { if (sender is Button button) { return new DisposeButtonAsync(button, loadingText); } // Button이 아닐 경우 아무것도 하지 않는 'Dummy' Disposable 반환 // (NullReferenceException 방지) return DisposeButtonAsync.Empty; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public sealed class DisposeButtonAsync : IAsyncDisposable { private readonly Button? _button; private readonly object? _originalContent; private bool _isDisposed; // 빈 객체를 위한 정적 프로퍼티 public static DisposeButtonAsync Empty =&gt; new(null, string.Empty); public DisposeButtonAsync(Button? button, string loadingText) { if (button == null) return; // 버튼이 없으면 아무 작업도 안 함 _button = button; _originalContent = _button.Content; _button.IsEnabled = false; _button.Content = loadingText; } public async ValueTask DisposeAsync() { if (_isDisposed || _button == null) return; try { if (_button.Dispatcher.CheckAccess()) { Restore(); } else { await _button.Dispatcher.InvokeAsync(Restore); } } finally { _isDisposed = true; GC.SuppressFinalize(this); } } private void Restore() { if (_button == null) return; _button.Content = _originalContent; _button.IsEnabled = true; } } 1 2 3 4 5 6 7 8 9 public async void OnClick(object sender, RoutedEventArgs e) { // sender에서 바로 호출! (Button이 아니면 내부적으로 무시됨) await using (sender.BeginLoading(\"서버 전송 중...\")) { await Task.Delay(2000); // 비즈니스 로직 작성... } } 아래의 두 가지 방법은 Singleton으로 클래스를 만들어서 사용하고 내용 중에 프로그래스바의 형태로 표현하기 위해 [\" ⦁ \", \" ⦁⦁ \", \" ⦁⦁⦁ \", \" ⦁⦁⦁⦁ \", \" ⦁⦁⦁⦁⦁\"] 형태를 버튼 Content에 표현한다. 이 부분을 실제 프로그래스바로 바꾸어도 무방하다. ProgressButtonAlone class 이 클래스를 적용한 모든 버튼은 하나의 버튼이 동작 중일 때는 다른 모든 버튼은 ‘사용중…’ 이라는 내용이 버튼의 Content 영역에 나타난다. 그리고 동작이 완료된 다음에 다른 버튼도 동일한 동작 방식으로 처리한다. 1 2 3 4 5 6 7 8 public static async Task ButtonOnTest(object sender, RoutedEventArgs e) { await ProgressButtonAlone.Go.ExecuteAsync(sender as Button, async () =&gt; { // 해당 작업이 있다고 가정 await Task.Delay(5000); }); } ProgressButtonAlone.cs 소스보기 ProgressButton class 이 클래스는 위와 다르게 모든 버튼이 각각 독립적으로 동작한다. 1 2 3 4 5 6 7 8 public static async Task ButtonOnTest(object sender, RoutedEventArgs e) { await ProgressButton.Go.ExecuteAsync(sender as Button, async () =&gt; { // 해당 작업이 있다고 가정 await Task.Delay(5000); }); } ProgressButton.cs 소스보기 ProgressButtonAlone, ProgressButton 소스는 Github에 올려두었으니 해당 링크를 참고하기 바란다. Reference learn.microsoft.com, “Implement a DisposeAsync method”" } , { "title": "C#, IAsyncEnumerable yield return", "url": "/20250104-1/", "date": "2025-01-04", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "csharp, sql", "content": "데이터베이스는 기본적으로 ‘select’와 같이 조회했을 때 그 결과가 운반 단위(array)에 도달하면 호출한 쪽으로 그 결과를 바로 리턴해준다. 예를 들어 어떤 테이블에 row 개수가 1000만 개가 있든 10만 개가 있든지 상관없이 select * from tablename을 실행하면 바로 화면에 나와야 정상이다. 이 부분을 간과하고 조회하는 프로그램 작성한다고 하면 조회 결과를 어떠한 DataSet에 넣고 그 결과가 완료될 때 비로서 Loop을 사용하여 화면에 표현하기 때문에 row 개수에 영향을 받는다. 물론, 페이징 처리를 한다고 하지만 쿼리문 자체에 ‘order by’, ‘group by’ 등을 사용하면 Sort가 발생하고 이 모든 정렬이 끝나야 비로소 그 결과를 출력하기 시작한다. 즉, ‘부분범위처리’와 ‘전체범위처리’의 차이다. 부분범위처리’라 함은 데이터베이스가 해당 내용을 다 읽지 않고도 바로 하나의 row를 바로 출력할 수 있는 상태를 말한다. 옵티마이저가 봤을 때 끝까지 읽어서 분석할 필요가 없다고 판단하기 때문이다. 그래서 정렬하되 전체범위처리(끝까지 다 읽고나서야 결과를 뽑아낼수 있는 상태)가 되지 않도록 쿼리문을 잘 작성해야 한다. 여기에는 인덱스 전략도 포함되며 특히, ‘where’절에 column을 가공하면(예, where left(xxxx) = '1234') 작성한 ‘left(xxxx)’라는 column은 존재하지 않기 때문에 옵티마이저는 전체를 다 읽고 해당 column을 ‘left(xxxx)’로 모두 2차 가공한 다음에 조회하고 그 결과를 출력한다. 즉, 전체범위처리를 하는 것이다. 요지는 ‘어떻게 부분범위처리가 되도록 유도하느냐’이다. 부분범위처리가 되었다고 가정하고 조회 결과를 클라이언트의 프로그램에서 조회한 결과를 DataSet에 모두 넣고 그 다음에 화면에 Loop를 사용하여 표현하는 방법으로 처리하는 것이 아닌 비동기적으로 스트리밍하여 그 결과가 들어오는 즉시 화면에 출력하도록 IAsyncEnumerable, yield return을 사용하는 예제를 작성해 보았다. SQL Server의 특정 테이블에 50만 개 정도의 row가 저장된 테스트 데이터를 사용한다. nuget 패키지 추가 1 2 3 4 5 6 7 8 9 10 11 12 &lt;Project Sdk=\"Microsoft.NET.Sdk\"&gt; &lt;PropertyGroup&gt; &lt;OutputType&gt;Exe&lt;/OutputType&gt; &lt;TargetFramework&gt;net8.0&lt;/TargetFramework&gt; &lt;Nullable&gt;enable&lt;/Nullable&gt; &lt;/PropertyGroup&gt; &lt;ItemGroup&gt; &lt;PackageReference Include=\"Dapper\" Version=\"2.1.35\"/&gt; &lt;PackageReference Include=\"Microsoft.Data.SqlClient\" Version=\"6.0.0-preview3.24332.3\"/&gt; &lt;PackageReference Include=\"System.Linq.Async\" Version=\"6.0.1\" /&gt; &lt;/ItemGroup&gt; &lt;/Project&gt; 데이터 모델 정의 1 2 3 4 5 6 // 데이터베이스 table 중에서 특정 컬럼만 조회 public class MyData { public string ZipCode { get; init; } = string.Empty; public string SiDo { get; init; } = string.Empty; } 데이터베이스 처리 로직 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 using Dapper; using Microsoft.Data.SqlClient; using System.Collections.Generic; using System.Data; using System.Linq; namespace Hello; public class DatabaseContext { public const string M_CON = \"Server = 192.168.1.2; Uid = sa; Pwd = xxxxx; database = TestDatabase; TrustServerCertificate = True\"; public static async IAsyncEnumerable&lt;MyData&gt; GetDataAsync() { const string sql = \"select s.ZIP_CODE as ZipCode, s.SIDO as SiDo from COMMON.ZIP_CODE_SEOUL s\"; await using SqlConnection connection = new(M_CON); // IAsyncEnumerable&lt;IEnumerable&lt;MyData&gt;&gt; result = connection.QueryAsync&lt;MyData&gt;(sql, commandType: CommandType.Text).ToAsyncEnumerable(); // // await foreach (IEnumerable&lt;MyData&gt; items in result.AsAsyncEnumerable()) // { // foreach (MyData item in items) // { // yield return item; // } // } // IEnumerable&lt;MyData&gt; results = await connection.QueryAsync&lt;MyData&gt;(sql, commandType: CommandType.Text); // // await foreach (MyData data in results.ToAsyncEnumerable()) // { // yield return data; // } IEnumerable&lt;MyData&gt; results = await connection.QueryAsync&lt;MyData&gt;(sql, commandType: CommandType.Text); foreach (MyData result in results) { yield return result; } } } 테스트 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 using System; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; namespace Hello; internal class Program { private static async Task Main() { Stopwatch sw = Stopwatch.StartNew(); sw.Start(); await foreach ((MyData data, int i) in DatabaseContext.GetDataAsync().Select((v, i) =&gt; (v, i))) { Console.WriteLine($\"{i + 1} : {data.ZipCode}, {data.SiDo}\"); } sw.Stop(); Console.WriteLine($\"{sw.Elapsed.Seconds} 초\"); } } 위와 같이 작성하고 실행하면 실행하자마자 화면에 출력하는 내용을 볼 수 있다. CancellationToken 사용 예제 CancellationTokenSource를 사용하여 비동기 작업을 취소하여 데이터 읽기 작업을 중간에 중단한다. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public static async IAsyncEnumerable&lt;MyData&gt; GetDataAsync([EnumeratorCancellation] CancellationToken cancellationToken) { const string sql = \"select s.ZIP_CODE as ZipCode, s.SIDO as SiDo from COMMON.ZIP_CODE_SEOUL s\"; await using SqlConnection connection = new(M_CON); IEnumerable&lt;MyData&gt; results = await connection.QueryAsync&lt;MyData&gt;(sql, commandType: CommandType.Text); foreach (MyData result in results) { cancellationToken.ThrowIfCancellationRequested(); yield return result; } } private static async Task Main() { Stopwatch sw = Stopwatch.StartNew(); sw.Start(); using CancellationTokenSource cts = new(); // 해당 작업은 5초 후에 종료한다 cts.CancelAfter(TimeSpan.FromSeconds(5)); try { await foreach ((MyData data, int i) in DatabaseContext.GetDataAsync(cts.Token).Select((v, i) =&gt; (v, i)).WithCancellation(cts.Token)) { Console.WriteLine($\"{i + 1} : {data.ZipCode}, {data.SiDo}\"); } } catch (Exception ex) { Console.WriteLine(ex.Message); } sw.Stop(); Console.WriteLine($\"{sw.Elapsed.Seconds} 초\"); } 실제 스트리밍 부분범위처리 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // connection timout = 3초, 예외처리 생략 using CancellationTokenSource cts = new(TimeSpan.FromSeconds(3)); await using SqlConnection connection = new(M_CON); await connection.OpenAsync(cts.Token).ConfigureAwait(false); CommandDefinition cmd = new(sql, commandType: CommandType.Text, cancellationToken: cancellationToken); await using DbDataReader reader = await connection.ExecuteReaderAsync(cmd); Func&lt;DbDataReader, TestModel&gt; parser = reader.GetRowParser&lt;TestModel&gt;(); while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) { yield return parser(reader); } Row Versioning 글을 작성하고 보니 부분범위처리1와 더불어 데이터베이스별로 Row Versioning하는 방법이 달라서 대표적으로 Oracle과 비교해서 SQL Server에서 lock에 의한 경합을 다루는 트랜잭션 격리 수준2의 하나인 SNAPSHOT에 대해 소개해 볼까 한다. 오라클(MariaDB, PostgreSQL)은 기본적으로 ‘select’는 서로 경합이 없으며 for update row versioning을 할 수 있다. dirty read가 발생할 수는 있어도 일반적인 lock은 발생하지 않는다. 그러나 SQL Server는 반대 개념이라고 볼 수 있는데 기본적으로 SQL Server는 읽기 작업 시 공유 잠금을 얻어야 한다. 이 때문에 경합을 피하려면 tablename with(nolock) 힌트를 사용해야 dirty read가 가능해진다. 그래서 이 반대 상황을 이해하지 못하고 사용하면 SQL Server는 lock이 많다고 오해할 수 있는데 snpshot 격리 수준을 사용하여 트랜잭션 수준에서 경합을 처리하는 예제를 소개한다. 참고로 아래의 예제는 ‘Gemini 2.0’, ‘ChatGPT’ 그리고 ‘Github Copilot’에서 조회하고 테스트 하였다. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -- SNAPSHOT ISOLATION` 활성화 ALTER DATABASE YourDatabaseName SET ALLOW_SNAPSHOT_ISOLATION ON; -- 트랜잭션 내에서 SNAPSHOT ISOLATION 사용 SET TRANSACTION ISOLATION LEVEL SNAPSHOT; BEGIN TRANSACTION; -- 데이터 읽기 SELECT * FROM employees WHERE employee_id = 1; -- 데이터 수정 UPDATE employees SET salary = salary + 1000 WHERE employee_id = 1; COMMIT; 이렇게 하면 다른 세션의 사용자는 경합 없이 commit전 데이터를 읽을 수 있다. 이때 SQL Server tempdb를 사용한다. snapshot을 사용하려면 충분한 메모리 확보와 더불어 tempdb의 성능도 중요하다. 일반적으로 cpu 코어의 수만큼 tempdb를 만들고 생성할 때 동일한 크기로 설정하고 될 수 있으면 빠른 스토리지를 사용한다. SQL Server에서 자동으로 작업을 분산하여 tempdb를 활용한다. 오라클은 이런 경우 Undo 세그먼트를 이용한다. nolock 활용 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 BEGIN TRANSACTION; -- 임시 테이블이 존재하면 삭제 IF OBJECT_ID('tempdb..#TempEmployees') IS NOT NULL BEGIN DROP TABLE #TempEmployees; END; -- NOLOCK 힌트를 사용하여 데이터를 읽음 (잠금 없이 읽기) SELECT e.employee_id, e.salary INTO #TempEmployees FROM employees e WITH (NOLOCK) WHERE e.department_id = 10; -- 임시 테이블의 데이터를 기반으로 업데이트 수행 UPDATE e SET e.salary = e.salary + t.salary * 0.1 FROM employees e INNER JOIN #TempEmployees t ON e.employee_id = t.employee_id; -- 트랜잭션 커밋 COMMIT; -- 임시 테이블 삭제 (필요시) DROP TABLE #TempEmployees; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 BEGIN TRANSACTION; -- 테이블 변수 선언 DECLARE @TempEmployees TABLE ( employee_id INT, salary DECIMAL(10, 2) ); -- NOLOCK 힌트를 사용하여 데이터를 읽음 (잠금 없이 읽기) INSERT INTO @TempEmployees (employee_id, salary) SELECT e.employee_id, e.salary FROM employees e WITH (NOLOCK) WHERE e.department_id = 10; -- 테이블 변수의 데이터를 기반으로 업데이트 수행 UPDATE e SET e.salary = e.salary + t.salary * 0.1 FROM employees e INNER JOIN @TempEmployees t ON e.employee_id = t.employee_id; -- 트랜잭션 커밋 COMMIT; DECLARE @TempEmployees TABLE () 형태로 테이블 변수를 사용하면 작은 데이터 집합의 경우 테이블 변수가 더 빠를 수 있지만, 큰 데이터 집합에서는 임시 테이블이 더 나은 성능을 제공할 수 있다. 또한 테이블 변수를 사용하면 통계 처리가 되지 않는다. ‘SNAPSHOT ISOLATION’을 활성화한 후에 각 쿼리에서 트랜잭션을 처리 할 때마다 SET TRANSACTION ISOLATION LEVEL SNAPSHOT;을 사용해야 하는 약간의 불편함이 발생할 수 있다. 이 외에도 read committed with snapshot을 사용할 수 있다.3 read committed with snapshot(RCSI) SQL Server에서 동시성(Concurrency)을 높이고 블로킹(Blocking)을 최소화하기 위한 설정이다. 성능 최적화를 위해 TempDB 관리가 중요한데 일반적으로 8개 정도로 코어수에 맞추어 만들어 준다. (보통 8코어 이하면 코어 수만큼, 그 이상이면 8개로 시작 후 조정, 속도 및 공간확보 필수) 1 2 ALTER DATABASE [DBName] SET READ_COMMITTED_SNAPSHOT ON; ALTER TABLE [TableName] SET (LOCK_ESCALATION = DISABLE); 데이터를 읽을 때 공유 잠금(S Lock)을 걸지 않고, TempDB에 저장된 버전(Snapshot)을 읽는다. 따라서 WITH (NOLOCK)을 쿼리마다 일일이 붙일 필요가 없으며, Dirty Read(커밋되지 않은 데이터 읽기) 위험 없이 일관된 데이터를 읽을 수 있다. 주의: WITH (UPDLOCK, ROWLOCK)으로 다른 데이터베이스에서 지원하는 for update를 사용한다. 사용할 때 반드시 where절에 인덱스가 걸려 있어야 한다. 그리고 짧고 빠르게 처리하는게 좋음. LOCK_ESCALATION 설정은 특정 테이블에 대량의 데이터 조작(DML)이 발생할 때, 행 단위 잠금이 테이블 단위 잠금으로 바뀌는 것을 막아준다. Reference devsight, “SQL 튜닝 원리와 해법”에서 소개한 ‘오라클 성능 고화화 원리와 해법’, ‘SQL Server 튜닝 원리와 해법’은 필수적으로 읽어봐야 할 서적이다. 정재우, SQL Server 튜닝 원리와 해법, 비투엔컨설팅, 2010, p431 microsoft, “SET TRANSACTION ISOLATION LEVEL (Transact-SQL)”" } , { "title": "PostgreSQL 설치 및 환경설정", "url": "/20250103-1/", "date": "2025-01-03", "categories": "【 DatabaseㆍModeling 】, Database", "tags": "database", "content": "Database 시장에서 점유율의 순위는 크게 변화하지는 않았지만, 주목할 만한 것은 PostgreSQL의 상승세이다.1 특히 MySQL의 점유율은 하락 추세인데 이건 MariaDB 사용자를 포함하지 않아서일 수도 있다.2 오라클의 장점을 가지면서도 오픈소스로써 무료 데이터베이스인 PostgreSQL의 설치와 설정 과정, 튜닝포인트를 알아보고 MVCC(multiversion concurrency control)의 차이점 또한 간략하게 정리하였다. 테스트를 사용한 환경은 Windows WSL2에 Ubuntu 24.04 LTS 버전을 설치하여 진행했다.3 ?&gt;는 Shell 프롬프트이다. 기본적인 설치 1 2 3 4 5 6 ?&gt; sudo apt install postgresql postgresql-contrib ?&gt; sudo systemctl status postgresql ?&gt; sudo systemctl enable/disable postgresql ?&gt; sudo systemctl start postgresql ?&gt; sudo -i -u postgres psql \\password postgres #패스워드 외부 접속 설정 1 2 3 4 ?&gt; sudo vi /etc/postgresql/14/main/postgresql.conf isten_addresses = '*' ?&gt; sudo vi /etc/postgresql/14/main/pg_hba.conf host all all 0.0.0.0/0 md5 #추가 또는 수정 템블릿(DB) 삭제 방지(옵션) 1 2 3 # postgres로 로그인한 상황에서 update pg_database set datistemplate=true where datname='template0'; update pg_database set datistemplate=true where datname='template1'; 신규 DB, 사용자 생성 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #사용자 생성 create user mypg password 'password' superuser; #테이블스페이스 생성 ?&gt; mkdir -p /pg_tablespace ?&gt; sudo chown postgres:postgres /pg_tablespace # postgres로 로그인한 상황에서 create tablespace mytablespace owner mypg location '/pg_tablespace/'; create database mypgdb owner mypg tablespace mytablespce; # 시작 및 종료 ?&gt; sudo systemctl status postgresql # 상태확인 ?&gt; sudo systemctl start postgresql # 시작 ?&gt; sudo systemctl stop postgresql # 종료 ?&gt; ps aux | grep postgres # 동작확인 설정(튜닝) 포인트 PostgreSQL은 동시성 제어를 위해 MVCC를 사용한다. 동시성 제어를 높이기 위해서  ’읽기 작업은 쓰기 작업을 블로킹하지 않고, 쓰기 작업은 읽기 작업을 블로킹하지 않아야 한다.’ 이때 필요한 게 MVCC 이다.4 PostgreSQL은 트랜잭션을 식별하기 위해 4바이트 정수인 트랜잭션 ID(XID)를 사용한다. 이 값은 시간이 지남에 따라 증가하며, 특정 포인트에서 ‘래핑’ 또는 오버플로가 발생할 수 있다. AutoVacuum으로 이 문제를 방지하기 위해 오래된 트랜잭션 정보를 정리한다.5 AutoVacuum 자동 설정 1 2 3 4 5 6 7 8 9 10 11 # postgresql.conf autovacuum = on autovacuum_naptime = 60s autovacuum_max_workers = 4 #16G 4, 32, 64 4~6 autovacuum_vacuum_threshold = 10000 autovacuum_vacuum_scale_factor = 0.02 autovacuum_analyze_threshold = 5000 autovacuum_analyze_scale_factor = 0.01 autovacuum_vacuum_cost_limit = 1000 autovacuum_vacuum_cost_delay = 10ms log_autovacuum_min_duration = 5000 데이터베이스 설정 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # 8코어, 32G 메모리 max_connections = 200 shared_buffers = 8GB work_mem = 128MB maintenance_work_mem = 2GB effective_cache_size = 24GB effective_io_concurrency = 8 # SSD 사용 시 8, HDD 사용 시 2 random_page_cost = 1.1 # SSD 사용 시 1.1, HDD 사용 시 4 seq_page_cost = 1.0 wal_buffers = 128MB default_statistics_target = 100 #huge_pages = on # 가능하면 on 부팅 안되면 try, off min_wal_size = 2GB max_wal_size = 8GB max_worker_processes = 8 # CPU 코어 수와 동일하게 설정 max_parallel_workers_per_gather = 4 max_parallel_workers = 8 # max_worker_processes와 동일하게 설정 max_parallel_maintenance_workers = 4 checkpoint_completion_target = 0.9 메모리 16G, 64G 기본 설정 예 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 # 공통으로 CPU 코어은 8개로 가정 # 16G max_connections = 100 shared_buffers = 4GB work_mem = 64MB maintenance_work_mem = 1GB effective_cache_size = 12GB effective_io_concurrency = 4 random_page_cost = 1.1 seq_page_cost = 1.0 wal_buffers = 64MB default_statistics_target = 100 #huge_pages = on min_wal_size = 1GB max_wal_size = 4GB max_worker_processes = 8 max_parallel_workers_per_gather = 2 max_parallel_workers = 8 max_parallel_maintenance_workers = 2 checkpoint_completion_target = 0.9 # 64G max_connections = 400 shared_buffers = 16GB work_mem = 256MB maintenance_work_mem = 4GB effective_cache_size = 48GB effective_io_concurrency = 8 random_page_cost = 1.1 seq_page_cost = 1.0 wal_buffers = 256MB default_statistics_target = 100 #huge_pages = on min_wal_size = 4GB max_wal_size = 16GB max_worker_processes = 8 max_parallel_workers_per_gather = 4 max_parallel_workers = 8 max_parallel_maintenance_workers = 4 checkpoint_completion_target = 0.9 참고, 전체 설정(8코어, 32G) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #postgresql.conf data_directory = '/var/lib/postgresql/16/main' hba_file = '/etc/postgresql/16/main/pg_hba.conf' ident_file = '/etc/postgresql/16/main/pg_ident.conf' external_pid_file = '/var/run/postgresql/16-main.pid' listen_addresses = '*' port = 5432 unix_socket_directories = '/var/run/postgresql' ssl = on ssl_cert_file = '/etc/ssl/certs/ssl-cert-snakeoil.pem' ssl_key_file = '/etc/ssl/private/ssl-cert-snakeoil.key' dynamic_shared_memory_type = posix max_connections = 200 shared_buffers = 8GB work_mem = 128MB maintenance_work_mem = 2GB effective_cache_size = 24GB effective_io_concurrency = 8 random_page_cost = 1.1 seq_page_cost = 1.0 wal_buffers = 128MB default_statistics_target = 100 # huge_pages = on min_wal_size = 2GB max_wal_size = 8GB max_worker_processes = 8 max_parallel_workers_per_gather = 4 max_parallel_workers = 8 max_parallel_maintenance_workers = 4 checkpoint_completion_target = 0.9 autovacuum = on autovacuum_naptime = 60s autovacuum_max_workers = 4 #16G 4, 32, 64 4~6 autovacuum_vacuum_threshold = 10000 autovacuum_vacuum_scale_factor = 0.02 autovacuum_analyze_threshold = 5000 autovacuum_analyze_scale_factor = 0.01 autovacuum_vacuum_cost_limit = 1000 autovacuum_vacuum_cost_delay = 10ms log_autovacuum_min_duration = 5000 log_line_prefix = '%m [%p] %q%u@%d ' log_timezone = 'Asia/Seoul' log_destination = 'stderr' logging_collector = on log_directory = '/pg_log' log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' log_rotation_age = 1d log_rotation_size = 100MB log_autovacuum_min_duration = 5000 cluster_name = '16/main' datestyle = 'iso, mdy' timezone = 'Asia/Seoul' lc_messages = 'C.UTF-8' lc_monetary = 'C.UTF-8' lc_numeric = 'C.UTF-8' lc_time = 'C.UTF-8' default_text_search_config = 'pg_catalog.english' include_dir = 'conf.d' 참고로 MariaDB 11 버전에 대한 설치도 간략하게 정리해 보았다. MariaDB 11 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ?&gt; curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup | sudo bash -s -- --mariadb-server-version=\"mariadb-11.4\" # 기본적인 설정(root패스워드 초기설정) ?&gt; mariadb-secure-installation # 데이터메이스 TestDB를 만든 후 root로 로그인 후 ?&gt; grant all privileges on TestDB.* to '사용자'@'%' identified by '패스워드'; # 외부접속 허용 ?&gt; sudo vi /etc/mysql/mariadb.conf.d/50-server.cnf # bind-address=127.0.0.1 # 주석처리 # 기본설정 ?&gt; sudo vi /etc/mysql/my.cnf skip-name-resolve # 접속부하 줄이기(IP기반 빠른 접속) innodb_buffer_pool_size = 12G innodb_buffer_pool_instances = 10 innodb_thread_concurrency = 8 MariaDB 설정(참고) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 # 공통 skip-name-resolve [client] default-character-set = utf8mb4 [mysql] default-character-set = utf8mb4 [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci # 8코어, 32G innodb_buffer_pool_size = 24G innodb_buffer_pool_instances = 8 innodb_log_file_size = 1G innodb_log_buffer_size = 256M innodb_flush_log_at_trx_commit = 2 innodb_flush_method = O_DIRECT innodb_file_per_table = 1 max_connections = 500 thread_cache_size = 100 table_open_cache = 2000 query_cache_size = 0 query_cache_type = 0 tmp_table_size = 256M max_heap_table_size = 256M key_buffer_size = 512M sort_buffer_size = 4M read_buffer_size = 4M read_rnd_buffer_size = 16M join_buffer_size = 8M # 8코어, 16G innodb_buffer_pool_size = 12G # 메모리의 약 75% 할당 innodb_buffer_pool_instances = 4 # 코어 수에 맞게 설정 innodb_log_file_size = 512M innodb_log_buffer_size = 128M innodb_flush_log_at_trx_commit = 2 innodb_flush_method = O_DIRECT innodb_file_per_table = 1 max_connections = 300 thread_cache_size = 50 table_open_cache = 1000 query_cache_size = 0 query_cache_type = 0 tmp_table_size = 128M max_heap_table_size = 128M key_buffer_size = 256M sort_buffer_size = 2M read_buffer_size = 2M read_rnd_buffer_size = 8M join_buffer_size = 4M # 8코어, 64G innodb_buffer_pool_size = 48G innodb_buffer_pool_instances = 16 innodb_log_file_size = 2G innodb_log_buffer_size = 512M innodb_flush_log_at_trx_commit = 2 innodb_flush_method = O_DIRECT innodb_file_per_table = 1 max_connections = 1000 thread_cache_size = 200 table_open_cache = 4000 query_cache_size = 0 query_cache_type = 0 tmp_table_size = 512M max_heap_table_size = 512M key_buffer_size = 1G sort_buffer_size = 8M read_buffer_size = 8M read_rnd_buffer_size = 32M join_buffer_size = 16M rastalion.dev 웹사이트에 ‘MySQL 8.0 vs PostgreSQL 16: 심층 비교 분석’으로 MySQL과 PostgreSQL의 비교를 잘 정리한 글이 있어 일부를 소개한다. 측면 MySQL 8.0 PostgreSQL 16 성능 높은 읽기/쓰기 성능, 특히 단순 쿼리에서 우수 복잡한 쿼리와 대규모 데이터 처리에서 우수한 성능 확장성 제한적, 특히 대규모 동시 연결 처리에서 한계 우수한 확장성, 대규모 동시 연결 처리에 강점 기능 기본적인 RDBMS 기능에 충실 고급 기능 다수 제공 (예: JSON 지원, 테이블 상속) 데이터 무결성 기본적인 제약 조건 지원 강력한 데이터 무결성 기능 (예: CHECK 제약조건) 설정 및 관리 간편한 설정과 관리 초기 설정과 최적화가 상대적으로 복잡 에코시스템 광범위한 도구와 리소스 지원 성장 중인 에코시스템, 일부 도구 호환성 제한 클라우드 지원 우수한 클라우드 서비스 통합 (예: AWS RDS) 클라우드 지원 개선 중, 일부 제한적 MVCC 구현 언두 로그 사용, 효율적인 구현 테이블 내 다중 버전 저장, 데드 튜플 관리 필요 조인 성능 Nested Loop 조인 중심, 작은 테이블에 효과적 다양한 조인 알고리즘, 대규모 조인에 효과적 인덱싱 클러스터드 인덱스 사용, 효율적인 검색 다양한 인덱스 유형 지원, 복잡한 쿼리에 유리 참고로 lime.log에 올라온 ‘PostgreSQL 이야기1 - 우버의 PostgreSQL에서 MySQL로 전환사례’는 읽어볼 필요가 있다. 위의 이슈에 대하여 다음과 같은 해결 방법을 생각해 볼 수 있다. Write Amplification은 데이터베이스가 실제 데이터보다 더 많은 데이터를 디스크에 쓰는 현상을 의미한다. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # Autovacuum 설정을 조정하여 불필요한 쓰기 작업을 줄인다. SET autovacuum_vacuum_scale_factor = 0.2; SET autovacuum_analyze_scale_factor = 0.1; # WAL (Write-Ahead Logging) 설정을 튜닝하여 쓰기 증폭을 줄인다. SET wal_level = 'replica'; SET max_wal_size = '1GB'; SET min_wal_size = '80MB'; # 테이블 파티셔닝을 하여 쓰기 작업을 분산시킨다. CREATE TABLE tableName ( logdate DATE NOT NULL, ... 컬럼들... ) PARTITION BY RANGE (logdate); Replication 마스터-슬레이브 구조로 구성하는 복제를 사용 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # Streaming Replication 설정 : 마스터 서버 설정 wal_level = replica max_wal_senders = 10 archive_mode = on archive_command = 'cp %p /path/to/archive/%f' # 슬레이브 서버 설정 primary_conninfo = 'host=master_host port=5432 user=replication password=your_password' restore_command = 'cp /path/to/archive/%f %p' recovery_target_timeline = 'latest' # Logical Replication : 동기화 실행 # 마스터 퍼블리케이션 CREATE PUBLICATION my_pub FOR TABLE my_table; # 슬레이브 구독 CREATE SUBSCRIPTION my_sub CONNECTION 'host=master_host dbname=mydb user=replication password=your_password' PUBLICATION my_pub; Replica MVCC를 효율적으로 처리하기 위한 해결책 1 2 3 4 # Hot Standby 설정 : 복제본에서 읽기 작업 허용 hot_standby = on # 슬레이브 서버에서 쿼리 튜닝 SET max_parallel_workers_per_gather = 4; Reference 뜨는 포스트그레SQL, 지는 MySQL…DB 시장 지형 변화, 왜?, 디지털데일리, 2024-01-17, https://m.ddaily.co.kr/page/view/2024011705473798270, 2025-01-03 지난 12개월 동안 어떤 데이터베이스를 사용하셨나요?, Jetbrains 개발자 에코시스템, 2023, https://www.jetbrains.com/ko-kr/lp/devecosystem-2023/databases/, 2025-01-03 learn.microsoft.com, “WSL을 사용하여 Windows에 Linux를 설치하는 방법” todayscoding.tistory.com, “# Oracle 과 PostgreSQL의 차이점” youngjun0627.log, “Postgresql - AutoVacuum 에 대하여”" } , { "title": "Rust, Custom Error Handling", "url": "/20241221-1/", "date": "2024-12-21", "categories": "【 RustㆍZigㆍGo 】, Rust", "tags": "rust", "content": "프로그램 언어들은 예외 핸들링(exception handling) 또는 반환 값(return value) 이라는 두 가지 에러 핸들링 접근 방식 중 한 가지를 사용한다. Rust는 후자를 사용한다.1 이전 글 Rust 예외 및 에러 처리에서 복구 가능한 에러를 위한 Result&lt;T, E&gt; 사용법을 살펴봤다. Rust의 Result는 한가지 에러 타입만 처리가 기본적으로 가능하다. 두 가지 이상의 다른 에러 타입은 처리가 불가능할 때 사용할 수 있는 Custom Error Handling 방법을 이번 글에서 살펴본다. 에러처리 간소화를 위한 thiserror, anyhow 크레이트 예제를 소개하였다. 에러처리 실패 사례 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 use std::fs::File; use std::io::Write; use std::num::ParseIntError; fn main() { println!(\"{:?}\", square(\"2\")); println!(\"{:?}\", square(\"invalid\")); } fn square(val: &amp;str) -&gt; Result&lt;i32, ParseIntError&gt; { let num = val.parse::&lt;i32&gt;()?; let mut f = File::open(\"file.txt\")?; let string_to_write = format!(\"Square of {} is {}\", num, i32::pow(num, 2)); f.write(string_to_write.as_bytes())?; Ok(i32::pow(num, 2)) } /* the trait `From&lt;std::io::Error&gt;` is not implemented for `ParseIntError` ParseIntError, Error : 에러 타입이 두 가지지만 하나의 에러 타입만 지정 */ 실패 사례 개선 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 use std::error::Error; use std::fmt::{Display, Formatter, Result as FmtResult}; use std::fs::File; use std::io::Write; #[derive(Debug)] enum MyError { ParseError, IOError, } impl Display for MyError { fn fmt(&amp;self, f: &amp;mut Formatter&lt;'_&gt;) -&gt; FmtResult { match self { MyError::ParseError =&gt; write!(f, \"Parse Error\"), MyError::IOError =&gt; write!(f, \"IO Error\"), } } } impl Error for MyError {} fn main() { let result1 = square(\"2\"); let result2 = square(\"invalid\"); match result1 { Ok(res) =&gt; println!(\"Result1 is {:?}\", res), Err(e) =&gt; println!(\"Error1 is {:?}\", e), } match result2 { Ok(res) =&gt; println!(\"Result2 is {:?}\", res), Err(e) =&gt; println!(\"Error2 is {:?}\", e), } } fn square(val: &amp;str) -&gt; Result&lt;i32, MyError&gt; { let num = val.parse::&lt;i32&gt;().map_err(|_| MyError::ParseError)?; let mut f = File::open(\"file.txt\").map_err(|_| MyError::IOError)?; let string_to_write = format!(\"Square of {} is {}\", num, i32::pow(num, 2)); f.write(string_to_write.as_bytes()).map_err(|_| MyError::IOError)?; Ok(i32::pow(num, 2)) } /* Error1 is IOError Error2 is ParseError */ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 use std::fmt; use std::error::Error; #[derive(Debug)] struct MyError { message: String, } impl fmt::Display for MyError { fn fmt(&amp;self, f: &amp;mut fmt::Formatter&lt;'_&gt;) -&gt; fmt::Result { write!(f, \"MyError: {}\", self.message) } } impl Error for MyError {} fn fallible_function() -&gt; Result&lt;(), MyError&gt; { Err(MyError { message: \"Something went wrong\".to_string() }) } fn main() { match fallible_function() { Ok(_) =&gt; println!(\"Success!\"), Err(e) =&gt; println!(\"Error: {}\", e), } } //Error: MyError: Something went wrong thiserror 기본 사용법 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 use thiserror::Error; #[derive(Error, Debug)] pub enum MyError { #[error(\"Invalid argument: {0}\")] InvalidArgument(String), #[error(\"IO error: {0}\")] IoError(#[from] std::io::Error), #[error(\"Parse error: {0}\")] ParseError(#[from] std::num::ParseIntError), #[error(\"Custom error message\")] CustomError, } fn fallible_function() -&gt; Result&lt;(), MyError&gt; { Err(MyError::InvalidArgument(\"Invalid input\".to_string())) } fn main() { match fallible_function() { Ok(_) =&gt; println!(\"Success!\"), Err(e) =&gt; println!(\"Error: {}\", e), } } // Error: Invalid argument: Invalid input thiserror 기본 사용법 2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 use thiserror::Error; #[derive(Error, Debug)] pub enum MyError { #[error(\"Failed to open file: {0}\")] OpenFileError(String), #[error(\"Failed to read file: {0}\")] ReadFileError(String), #[error(\"Invalid number format: {0}\")] ParseIntError(#[from] std::num::ParseIntError), #[error(\"Division by zero\")] DivisionByZero, } fn read_number_from_file(path: &amp;str) -&gt; Result&lt;i32, MyError&gt; { let file_content = std::fs::read_to_string(path).map_err(|_| MyError::OpenFileError(path.to_string()))?; let number = file_content.trim().parse::&lt;i32&gt;().map_err(MyError::from)?; Ok(number) } fn divide(x: i32, y: i32) -&gt; Result&lt;i32, MyError&gt; { if y == 0 { return Err(MyError::DivisionByZero); } Ok(x / y) } fn main() { // 성공 케이스 match read_number_from_file(\"number.txt\") { Ok(number) =&gt; println!(\"File read successfully. Number: {}\", number), Err(e) =&gt; eprintln!(\"Error reading file: {}\", e), } match divide(10, 2) { Ok(result) =&gt; println!(\"Division successful. Result: {}\", result), Err(e) =&gt; eprintln!(\"Error during division: {}\", e), } match read_number_from_file(\"non_existent_file.txt\") { Ok(number) =&gt; println!(\"File read successfully. Number: {}\", number), Err(e) =&gt; eprintln!(\"Error reading file: {}\", e), } match read_number_from_file(\"invalid_number.txt\") { Ok(number) =&gt; println!(\"File read successfully. Number: {}\", number), Err(e) =&gt; eprintln!(\"Error reading file: {}\", e), } match divide(10, 0) { Ok(result) =&gt; println!(\"Division successful. Result: {}\", result), Err(e) =&gt; eprintln!(\"Error during division: {}\", e), } } /* Error reading file: Failed to open file: number.txt Division successful. Result: 5 Error reading file: Failed to open file: non_existent_file.txt Error reading file: Failed to open file: invalid_number.txt Error during division: Division by zero */ anyhow 기본 사용법 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 use anyhow::{anyhow, Context, Result as AnyResult}; use std::fs::File; use std::io::Read; fn divide(x: i32, y: i32) -&gt; AnyResult&lt;i32&gt; { if y == 0 { return Err(anyhow!(\"Division by zero\")); } Ok(x / y) } fn read_file(path: &amp;str) -&gt; AnyResult&lt;String&gt; { let mut file = File::open(path).with_context(|| format!(\"Failed to open file: {}\", path))?; let mut contents = String::new(); file.read_to_string(&amp;mut contents).with_context(|| format!(\"Failed to read file: {}\", path))?; Ok(contents) } fn parse_number(s: &amp;str) -&gt; AnyResult&lt;i32&gt; { s.parse::&lt;i32&gt;().map_err(|e| anyhow!(e)) } fn main() -&gt; AnyResult&lt;()&gt; { let result = divide(10, 2)?; println!(\"Result: {}\", result); if let Err(e) = divide(10, 0) { eprintln!(\"Divide Error: {}\", e); } match read_file(\"file.txt\") { Ok(contents) =&gt; println!(\"{}\", contents), Err(e) =&gt; eprintln!(\"Read File Error: {}\", e), } let num = parse_number(\"123\")?; println!(\"Number: {}\", num); if let Err(e) = parse_number(\"abc\") { eprintln!(\"Parse Number Error: {}\", e); } Ok(()) } /* Result: 5 Divide Error: Division by zero Read File Error: Failed to open file: file.txt Number: 123 Parse Number Error: invalid digit found in string */ thiserror, anyhow 요약 라이브러리를 개발하거나 세밀한 처리가 필요한 경우에는 thiserror를 사용하고 테스트를 위해 빠른 개발이 필요하거나 간단한 에러처리에는 anyhow를 사용하는 것을 권장한다. 특징 thiserror anyhow 에러 타입 명시적인 타입(enum, struct) anyhow::Error 로 추상화 에러 생성 다소 복잡 anyhow! Macros로 간편 에러 문맥 직접 구현해야 함 context() 메서드로 간편 디스패치 정적 동적 성능 약간 더 빠름 약간 느릴 수 있음(미미한 수준) 용도 라이브러리 개발, 세밀한 에러 처리,성능 중시 애플리케이션 개발, 빠른 개발,간단한 에러처리 Reference 프라부 에스왈라, 러스트서버 서비스 앱만들기, 김모세, 제이펍, 2024, p130" } , { "title": "C# .NET 8.0에서 LibraryImport 사용하기", "url": "/20241206-1/", "date": "2024-12-06", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "csharp", "content": "닷넷 7.0 이전 버전에서는 대부분 외부 DLL 파일을 Import 하거나 P/Invoke1를 활용하여 Native DLL(Unmanaged DLL)에 있는 함수를 호출하기 위해 DllImport를 사용해 왔다. 8.0버전부터는 LibraryImport의 사용을 권장한다. LibraryImport가 새로 생겨난 이유는 DllImport가 마샬링을 런타임에서 수행해서 IL 코드를 emit 한다고 하는데 NativeAOT 등 동적으로 IL 코드를 생성할 수 없는 환경에서 쓸 수 없으므로 LibraryImport의 소스 생성기 기능을 이용해 컴파일 시점에서 마샬링 코드를 삽입한다.2 기존에 작성했던 DllImport에서 LibraryImport 스타일로 변경할 때 몇 가지 변경 사항이 있는데 이 부분을 간단한 예제를 통하여 정리하였다. ~.csproj를 열어 AllowUnsafeBlocks를 true로 설정한다. WPF에서 윈도우 이동(DragMove) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 [LibraryImport(\"user32.DLL\", EntryPoint = \"ReleaseCapture\", StringMarshalling = StringMarshalling.Utf16)] [UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool ReleaseCapture(); [LibraryImport(\"user32.DLL\", EntryPoint = \"SendMessageW\", StringMarshalling = StringMarshalling.Utf16)] [UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])] [return: MarshalAs(UnmanagedType.SysInt)] private static partial IntPtr SendMessageW(IntPtr hWnd, uint wMsg, IntPtr wParam, IntPtr lParam); [LibraryImport(\"USER32.DLL\", EntryPoint = \"FindWindowW\", StringMarshalling = StringMarshalling.Utf16)] [UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])] [return: MarshalAs(UnmanagedType.SysInt)] private static partial IntPtr FindWindowW(string? lpClassName, string? lpWindowName); public static void DragMoveWindow(string windowTitle) { // MainWindow? mainWindow = Application.Current.MainWindow as MainWindow; // _ = ReleaseCapture(); // _ = SendMessageW(mainWindow == null ? Process.GetCurrentProcess().MainWindowHandle : FindWindowW(null, mainWindow.Title), // 0x112, 0xf012, 0); _ = ReleaseCapture(); _ = SendMessageW(FindWindowW(null, windowTitle), 0x112, 0xf012, 0); } INI Read/Write 예제 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 [LibraryImport(\"kernel32.dll\", EntryPoint = \"GetPrivateProfileStringW\", StringMarshalling = StringMarshalling.Utf16)] [UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])] private static partial void GetPrivateProfileStringW(string lpAppName, string lpKeyName, string lpDefault, [Out] char[] lpReturnedString, int nSize, string lpFileName); [LibraryImport(\"kernel32.dll\", EntryPoint = \"WritePrivateProfileStringW\", StringMarshalling = StringMarshalling.Utf16)] [UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool WritePrivateProfileStringW(string lpAppName, string lpKeyName, string lpString, string lpFileName); private const string INI_FILE = \"file_path\"; public static string Read(string key) { try { char[] _output = new char[1024]; GetPrivateProfileStringW(SECTION, key, \"\", _output, output.length, INI_FILE); output = output.Where(c =&gt; c != '\\0').ToArray(); return new string(_output); } catch (Exception _ex) { LogHelper.Logger.Debug($\"IniHelper Read ERROR : {_ex.Message}\"); return string.Empty; } } public static void Write(string key, string value) { try { _ = WritePrivateProfileStringW(SECTION, key, value, INI_FILE); } catch (Exception _ex) { LogHelper.Logger.Debug($\"IniHelper Write ERROR : {_ex.Message}\"); } } SendMessage IPC 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 private const uint WM_SETTEXT = 0x000C; [LibraryImport(\"user32.DLL\", EntryPoint = \"SendMessageW\", StringMarshalling = StringMarshalling.Utf16)] [UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])] [return: MarshalAs(UnmanagedType.SysInt)] private static partial IntPtr SendMessageW(IntPtr hWnd, uint wMsg, IntPtr wParam, IntPtr lParam); [LibraryImport(\"USER32.DLL\", EntryPoint = \"FindWindowW\", StringMarshalling = StringMarshalling.Utf16)] [UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])] [return: MarshalAs(UnmanagedType.SysInt)] private static partial IntPtr FindWindowW(string? lpClassName, string? lpWindowName); public static async Task SendMessage(string message) { await Task.Run(() =&gt; { try { IntPtr handle = FindWindowW(null, \"TITLE_XXX\"); _ = SendMessageW(handle, WM_SETTEXT, IntPtr.Zero, Marshal.StringToHGlobalAuto(message)); } catch (Exception ex) { LogHelper.Logger.Error($\"SendMessage ERROR : {ex.Message}\"); } }); } // 받는 부분 private static IntPtr WndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { try { switch (msg) { case 0x0011: // WM_QUERYENDSESSION _mainWindow.Close(); handled = true; break; case 0x000C: // WM_SETTEXT ReceiveMessage(Marshal.PtrToStringAuto(lParam) ?? string.Empty); handled = true; break; // case 0x0400: // WM_USER // ReceiveMessageUser(Marshal.PtrToStringAuto(lParam) ?? \"\"); // handled = true; // break; // case 0x004A: // WM_COPYDATA // ReceiveMessageCopyData(Marshal.PtrToStructure&lt;CopyDataStruct&gt;(lParam).LpData); // handled = true; // break; } return IntPtr.Zero; // 변경 } catch (Exception ex) { LogHelper.Logger.Error($\"WndProc Error : {ex.Message}\"); return IntPtr.Zero; } } SendMessage IPC 2 (IPC 1 수정본) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 [StructLayout(LayoutKind.Sequential)] struct COPYDATASTRUCT { public IntPtr dwData; // 식별자 (필요 시) public int cbData; // 데이터 크기 (바이트) public IntPtr lpData; // 실제 데이터 메모리 주소 } internal static partial class NativeMethods { private const string User32 = \"user32.dll\"; public const uint WM_COPYDATA = 0x004A; [LibraryImport(User32, EntryPoint = \"SendMessageW\")] [UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])] public static partial IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, ref COPYDATASTRUCT lParam); [LibraryImport(User32, EntryPoint = \"FindWindowW\", StringMarshalling = StringMarshalling.Utf16)] public static partial IntPtr FindWindowW(string? lpClassName, string? lpWindowName); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public static async Task SendCopyDataAsync(string message) { if (string.IsNullOrEmpty(message)) return; await Task.Run(() =&gt; { IntPtr hData = IntPtr.Zero; try { IntPtr hWnd = NativeMethods.FindWindowW(null, \"TITLE_XXX\"); if (hWnd == IntPtr.Zero) return; // 1. 문자열을 유니코드 바이트로 변환 byte[] buffer = Encoding.Unicode.GetBytes(message); int bufferLength = buffer.Length; // 2. 비관리 메모리 할당 및 복사 hData = Marshal.AllocHGlobal(bufferLength); Marshal.Copy(buffer, 0, hData, bufferLength); // 3. 구조체 설정 COPYDATASTRUCT cds = new() { dwData = IntPtr.Zero, cbData = bufferLength, lpData = hData }; // 4. 전송 (ref 키워드 사용) NativeMethods.SendMessage(hWnd, NativeMethods.WM_COPYDATA, IntPtr.Zero, ref cds); } catch (Exception ex) { LogHelper.Logger.Error($\"SendCopyData Error: {ex.Message}\"); } finally { // 5. 할당한 메모리 해제 if (hData != IntPtr.Zero) Marshal.FreeHGlobal(hData); } }); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 private static IntPtr WndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { if (msg == NativeMethods.WM_COPYDATA) { try { // 1. lParam을 구조체로 변환 var cds = Marshal.PtrToStructure&lt;COPYDATASTRUCT&gt;(lParam); // 2. lpData의 내용을 문자열로 복사 if (cds.lpData != IntPtr.Zero &amp;&amp; cds.cbData &gt; 0) { // 유니코드(2바이트)이므로 길이를 2로 나눔 string received = Marshal.PtrToStringUni(cds.lpData, cds.cbData / 2); ReceiveMessage(received); } handled = true; return new IntPtr(1); // 성공 반환 } catch (Exception ex) { LogHelper.Logger.Error($\"WndProc COPYDATA Error: {ex.Message}\"); } } return IntPtr.Zero; } AOT 활용 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // AOT(Native) : TestAot.dll [UnmanagedCallersOnly(EntryPoint = \"GetTest\")] public static IntPtr GetTest(IntPtr inputString) { string? tmpString = Marshal.PtrToStringAuto(inputString); return Marshal.StringToHGlobalAuto(\"문자열 : \" + tmpString); } // Managed code [LibraryImport(\"TestAot.dll\", EntryPoint = \"GetTest\", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] [UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])] [return: MarshalAs(UnmanagedType.SysInt)] private static partial IntPtr GetTest([MarshalAs(UnmanagedType.LPWStr)] string inputStr); private string? GetTestEx(string inputString) { try { return Marshal.PtrToStringAuto(GetTest(inputString)); } catch (Exception ex) { LogHelper.Logger.Error($\"GetTestEx ERROR : {ex.Message}\"); return (false, ex.Message); } } AOT 활용 2(utf-8) 1 2 3 4 5 6 7 8 9 10 11 [UnmanagedCallersOnly(EntryPoint = \"GetTest\")] public static IntPtr GetTest(IntPtr inputString) { // 비관리 UTF-8 포인터를 관리되는 string으로 변환 string? tmpString = Marshal.PtrToStringUTF8(inputString); string result = \"문자열 : \" + (tmpString ?? string.Empty); // 관리되는 string을 비관리 UTF-8 메모리(CoTaskMem)로 할당하여 반환 // CoTaskMem, Windows/Linux/macOS 공용 return Marshal.StringToCoTaskMemUTF8(result); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 [LibraryImport(\"TestAot.dll\", EntryPoint = \"GetTest\", StringMarshalling = StringMarshalling.Utf8)] private static partial IntPtr GetTest(string inputStr); private string? GetTestEx(string inputString) { IntPtr pResult = IntPtr.Zero; try { // DLL 호출 pResult = GetTest(inputString); if (pResult == IntPtr.Zero) return null; // 반환된 UTF-8 포인터를 string으로 읽기 return Marshal.PtrToStringUTF8(pResult); } catch (Exception ex) { LogHelper.Logger.Error($\"GetTestEx ERROR : {ex.Message}\"); return null; } finally { // StringToCoTaskMemUTF8로 할당된 메모리는 FreeCoTaskMem으로 해제 if (pResult != IntPtr.Zero) { Marshal.FreeCoTaskMem(pResult); } } } Reference Microsoft, “Platform Invoke (P/Invoke)” forum.dotnetdev.kr, “DllImport 대신 LibraryImport 사용”" } , { "title": "C#, JSON in Native AOT", "url": "/20241119-1/", "date": "2024-11-19", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "csharp, json, aot", "content": "C#(.NET)으로 만든 앱을 네이티브 AOT로 게시하면 자체 포함 배포처럼 네이티브 코드로 AOT(ahead-of-time) 컴파일된 앱이 생성된다. 즉 IL(Intermediate language)을 네이티브 코드로 컴파일한다.1 System.Text.Json에서 원본을 생성하는 방법은 몇 가지 추가 및 제약 사항이 있는데 아래의 예제는 이를 설명하는 소스이다.2 테스트를 위해 생성한 파일은 Program.cs, TestModel.cs, JsonHelper.cs이다. 테스트를 위하여 콘솔 프로젝트를 만들고 프로젝트 설정(~.csproj)을 아래와 같이 변경하고 터미널에서 dotnet publish -c Release -r win-x64 빌드한다. 정상적으로 빌드가 완료되면 하위 폴더 Native에 실행파일이 생성된다. 1 2 public record SubItem(int Id, string Name); public record MyModel(string Title, List&lt;SubItem&gt; Items); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 /* [JsonSourceGenerationOptions( WriteIndented = false, PropertyNamingPolicy = null, // null로 설정 가능 PropertyNameCaseInsensitive = true, GenerationMode = JsonSourceGenerationMode.Default // 직렬화/역직렬화 모두 생성 )] */ [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] //최상위 모델만 등록 (내부는 자동추적) [JsonSerializable(typeof(MyModel))] // partial 선언, 소스 생성기가 자동으로 Default static 프로퍼티 생성 internal partial class AppJsonContext : JsonSerializerContext { } // AppJsonContext.g.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static class JsonHelper { private static readonly JsonSerializerOptions _options = new() { // Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // 소스 생성기가 만든 map 등록 (AOT 설정) TypeInfoResolver = AppJsonContext.Default }; public static T? FromJson&lt;T&gt;(string json) =&gt; JsonSerializer.Deserialize&lt;T&gt;(json, _options); public static string ToJson&lt;T&gt;(T value) =&gt; JsonSerializer.Serialize(value, _options); } 위의 소스는 .NET 10에서 테스트하였고 아래는 .NET 8에서 작성한 코드이다. 프로젝트 설정(변경) 1 2 3 4 5 6 7 8 9 &lt;Project Sdk=\"Microsoft.NET.Sdk\"&gt; &lt;PropertyGroup&gt; &lt;OutputType&gt;Exe&lt;/OutputType&gt; &lt;TargetFramework&gt;net8.0&lt;/TargetFramework&gt; &lt;Nullable&gt;enable&lt;/Nullable&gt; &lt;PublishAot&gt;true&lt;/PublishAot&gt; &lt;IsAotCompatible&gt;true&lt;/IsAotCompatible&gt; &lt;/PropertyGroup&gt; &lt;/Project&gt; TestModel.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 using System.Text.Json.Serialization; namespace ConsoleTest; public class TestModel : IC { public string FirstName { get; set; } = string.Empty; public string LastName { get; set; } = string.Empty; public string FullName { get; set; } = string.Empty; // [JsonConverter(typeof(JsonStringEnumConverter&lt;ETest&gt;))] public ETest ETest { get; set; } = 0; public string CodeName { get; set; } = \"CodeName\"; } public interface IA { [JsonPropertyOrder(1)] string FirstName { get; set; } } public interface IB : IA { [JsonPropertyOrder(2)] string LastName { get; set; } } public interface IC : IB { [JsonPropertyOrder(3)] string FullName { get; set; } [JsonPropertyOrder(4)] ETest ETest { get; set; } [JsonIgnore] string CodeName { get; set; } } [JsonConverter(typeof(JsonStringEnumConverter&lt;ETest&gt;))] public enum ETest { Cpp, Python, Go } // [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization)] // [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(IA))] [JsonSerializable(typeof(IB))] [JsonSerializable(typeof(IC))] [JsonSerializable(typeof(TestModel))] //[JsonSerializable(typeof(bool))] //class 프로퍼티가 object type일 경우 데이터 타입 명시 //[JsonSerializable(typeof(int))] public partial class TestModelContext : JsonSerializerContext; public static class AotMessage { public const string AOT = \"Need runtime code generation for native AOT(may break when trimming)\"; } JsonHelper.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 using System; using System.Diagnostics.CodeAnalysis; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization.Metadata; namespace ConsoleTest; public static class JsonHelper { private static JsonSerializerOptions Options; static JsonHelper() { Options = new JsonSerializerOptions(); } [RequiresUnreferencedCode(AotMessage.AOT)] [RequiresDynamicCode(AotMessage.AOT)] public static string ModelToJson&lt;T&gt;(T modelClass, IJsonTypeInfoResolver resolver) { try { if (typeof(T).Name == \"String\") { return \"string.Empty1\"; } if (!(typeof(T).IsInterface || typeof(T).IsClass)) { return \"string.Empty2\"; } Options = new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, PropertyNameCaseInsensitive = true, PropertyNamingPolicy = null, TypeInfoResolver = resolver }; string _result = JsonSerializer.Serialize(modelClass, Options); return string.IsNullOrWhiteSpace(_result) ? string.Empty : _result; } catch (Exception _ex) { return _ex.Message; } } [RequiresUnreferencedCode(AotMessage.AOT)] [RequiresDynamicCode(AotMessage.AOT)] public static T JsonToModel&lt;T&gt;(string jsonString, IJsonTypeInfoResolver resolver) where T : class, new() { try { Options = new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, PropertyNameCaseInsensitive = true, PropertyNamingPolicy = null, TypeInfoResolver = resolver }; T? _result = JsonSerializer.Deserialize&lt;T&gt;(jsonString, Options); return _result ?? new T(); } catch { return new T(); } } } Program.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 using System; using System.Diagnostics.CodeAnalysis; namespace ConsoleTest; //Use concrete types when possible for improved performance #pragma warning disable CA1859 internal class Program { [RequiresUnreferencedCode(AotMessage.AOT)] [RequiresDynamicCode(AotMessage.AOT)] private static void Main() { IA _ia = new TestModel(); IB _ib = new TestModel(); IC _ic = new TestModel(); _ia.FirstName = \"a1가\"; _ib.FirstName = \"b1\"; _ib.LastName = \"b2\"; _ic.FirstName = \"c1\"; _ic.LastName = \"c2\"; _ic.FullName = \"c3\"; _ic.ETest = ETest.Python; TestModelContext _context = new(); string _result = JsonHelper.ModelToJson(_ia, _context); Console.WriteLine($\"{_result}\"); IA _a = JsonHelper.JsonToModel&lt;TestModel&gt;(_result, _context); Console.WriteLine($\"{_a.FirstName}\"); Console.WriteLine(\"--------------------------------------------------\"); _result = JsonHelper.ModelToJson(_ib, _context); Console.WriteLine($\"{_result}\"); IB _b = JsonHelper.JsonToModel&lt;TestModel&gt;(_result, _context); Console.WriteLine($\"{_b.FirstName} {_b.LastName}\"); Console.WriteLine(\"--------------------------------------------------\"); _result = JsonHelper.ModelToJson(_ic, _context); Console.WriteLine($\"{_result}\"); IC _c = JsonHelper.JsonToModel&lt;TestModel&gt;(_result, _context); Console.WriteLine($\"{_c.FirstName} {_c.LastName} {_c.FullName} {_c.ETest} {_c.CodeName}\"); } #pragma warning restore CA1859 /* {\"FirstName\":\"a1가\"} a1가 -------------------------------------------------- {\"FirstName\":\"b1\",\"LastName\":\"b2\"} b1 b2 -------------------------------------------------- {\"FirstName\":\"c1\",\"LastName\":\"c2\",\"FullName\":\"c3\",\"ETest\":\"Python\"} c1 c2 c3 Python CodeName */ } Reference Microsoft, “Native AOT deployment” Microsoft, “How to use source generation in System.Text.Json”" } , { "title": "Rust impl trait, C# interface", "url": "/20241023-1/", "date": "2024-10-23", "categories": "【 RustㆍZigㆍGo 】, Rust", "tags": "rust, csharp", "content": "C#과 같은 일반적인 프로그래밍 언어에서는 class, struct(구조체) 안에 property(속성)와 method(메서드)를 정의하여 활용할 수 있다. rust는 구조체를 지원하지만 해당 구조체 안에 속성만을 기술할 수 있지 메서드는 정의할 수 없고 impl 키워드를 사용하여 외부에 정의한다. impl 키워드와 더불어 사용할 수 있는 trait는 타입에 대해 공통된 동작을 표시한다. 약간의 차이는 있지만 다른 프로그래밍 언어(C#)에서 말하는 interface와 비슷한 개념이다. 아래의 소스는 C#에서의 interface 기능을 간략하게 살펴보고 이를 rust를 이용하여 구현하고 비교해 본 것이다. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 using System; namespace ConsoleTest; internal class Program { private static void Main() { Test1(); Console.WriteLine(\"------------------------------\"); Test2(); /* Truck can drive : 1111 : 1111 Truck can drive : 2222 : 1111 Truck can drive : 2222 Sedan can drive : 3333 Sedan can drive : 4444 : 1111 ------------------------------ Truck can drive : 1111 : 1111 Truck can drive : 2222 : 2222 Truck can drive : 2222 Sedan can drive : 3333 Sedan can drive : 4444 : 2222 */ } 콜백은 일반적으로 delegate를 통하여 구현하나 인터페이스를 사용하여 콜백을 구현할 수도 있다. 또한 모델 클래스를 만들 때 클래스에서 지원하지 않는 다중상속을 구현가능하게 해 준다. 아래는 전체 소스이다. Example Interface(C#) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 using System; namespace ConsoleTest; internal class Program { private static void Main() { Test1(); Console.WriteLine(\"------------------------------\"); Test2(); } private static void Test1() { Truck truck = new() { Name = \"1111\" }; truck.Do(truck); ICar iCar = new Truck(); // truck 비교 iCar.Name = \"2222\"; truck.Do(iCar); Console.WriteLine($\"{iCar.Drive()}\"); iCar = new Sedan(); iCar.Name = \"3333\"; Console.WriteLine($\"{iCar.Drive()}\"); Sedan sedan = new() { Name = \"4444\" }; truck.Do(sedan); } private static void Test2() { Truck truck = new() { Name = \"1111\" }; truck.Do(truck); ICar iCar = truck; // new Truck() 비교 iCar.Name = \"2222\"; truck.Do(iCar); Console.WriteLine($\"{iCar.Drive()}\"); iCar = new Sedan(); iCar.Name = \"3333\"; Console.WriteLine($\"{iCar.Drive()}\"); Sedan sedan = new() { Name = \"4444\" }; truck.Do(sedan); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public interface ICar { public string Name { get; set; } public string Drive(); } public class Truck : ICar { public string Name { get; set; } = string.Empty; public string Drive() { return $\"Truck can drive : {Name}\"; } public void Do(ICar iCar) { Console.WriteLine($\"{iCar.Drive()} : {Name} \"); } } public class Sedan : ICar { public string Name { get; set; } = string.Empty; public string Drive() { return $\"Sedan can drive : {Name}\"; } } Example Impl Trait(rust) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 use std::fmt::Debug; trait Car: Debug { fn drive(&amp;self, s: &amp;str); } #[derive(Debug)] struct Truck; impl Car for Truck { fn drive(&amp;self, s: &amp;str) { println!(\"{:?} can drive {}\", &amp;self, s); } } #[derive(Debug)] struct Sedan; impl Car for Sedan { fn drive(&amp;self, s: &amp;str) { println!(\"{:?} can drive {}\", &amp;self, s); } } // fn trait_bound&lt;T: Car + Debug&gt;(car: T) { // fn trait_bound&lt;T&gt;(car: T) where T: Car, T: Debug fn trait_bound&lt;T&gt;(car: T, s: &amp;str) where T: Car + Debug, { println!(\"T({:?}) can drive {}\", car, s); } fn drive_car(car: impl Car) { car.drive(\"1111\"); } fn get_car(is_sedan: bool) -&gt; Box&lt;dyn Car&gt; { if is_sedan { Box::new(Sedan) } else { Box::new(Truck) } } fn get_car2(car: impl Car + 'static) -&gt; Box&lt;dyn Car&gt; { car.drive(\"2222\"); Box::new(car) } fn main() { let truck = Truck {}; Car::drive(&amp;truck, \"3333\"); // truck.drive(); trait_bound(truck, \"4444\"); // let sedan = Sedan {}; // Car::drive(&amp;sedan); // sedan.drive(); Car::drive(&amp;Sedan, \"5555\"); trait_bound(Sedan, \"6666\"); drive_car(Truck); let car = get_car(false); car.drive(\"7777\"); let car2 = get_car2(Sedan); car2.drive(\"8888\"); println!(\"Car : {:?}\", car2); } /* Truck can drive 3333 T(Truck) can drive 4444 Sedan can drive 5555 T(Sedan) can drive 6666 Truck can drive 1111 Truck can drive 7777 Sedan can drive 2222 Sedan can drive 8888 Car : Sedan */ 객체지향 언어에서는 추상화와 코드 재사용을 위해 interface 또는 abstract class를 제공하는데 rust에서는 이와 유사한 trait를 제공하여 일부 이를 구현한다. 정적다형성(컴파일 타임 다형성)을 동적 디스패치로 구현하기 위해 Box&lt;dyn Trait&gt; 사용한다. 참고할 만한 강좌(링크) TaeGit, Rust 트레잇(Trait)과 트레잇 바운드(Trait Bound) TaeGit, impl Trait과 Box&lt;dyn Trait&gt; 예제로 배우는 Rust 프로그래밍, 구조체 impl 블럭" } , { "title": "C# delegate, C++ function pointer, Rust", "url": "/20240531-1/", "date": "2024-05-31", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "rust, csharp, cpp", "content": "C#은 안전한 함수 포인터 개체를 정의하는 delegate 형식을 제공한다. C++ 사용자정의함수에서 파라미터로 함수를 입력받기 위해 사용하는 개념과 동일하다. c#에서는 delegate를 선언하고 사용하는 과정을 간소화하여 Action, Func, Predicate를 제공하고 있다. 참고로 delegate는 이벤트에서 사용하므로 C#에서 이해는 필수이다. C++의 함수 포인터를 C#의 Func으로 표현해 보는 몇 가지 예제를 작성하였다. 특히 C++의 함수 포인터 예제는 유튜브 C/C++ 강좌로 유명한 두들낙서 94강. 함수 포인터를 참고하였다. Rust 예제포함. Example 1 (C++, C#) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include &lt;iostream&gt; #include &lt;string&gt; using namespace std; bool compare(int a, int b) { return a == b; } int main(void) { bool (*fp)(int, int) = compare; bool result = fp(2, 2); cout &lt;&lt; result &lt;&lt; endl; cout &lt;&lt; boolalpha &lt;&lt; result &lt;&lt; endl; cout &lt;&lt; string(result ? \"True\" : \"False\") &lt;&lt; endl; } 1 2 3 4 5 6 7 8 9 10 11 12 void Main() { Func&lt;int, int, bool&gt; func = compare; bool result = func(2, 2); (result ? 1 : 0).Dump(); result.Dump(); } bool compare(int a, int b) { return a == b; } Example 2 (C++, C#) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include &lt;iostream&gt; using namespace std; int square(int x) { return x * x; } int myFunc(int x) { return x * (x - 15) / 2; } int cube(int x) { return x * x * x; } int arrFnMin(const int arr[], int n, int (*f)(int)) { int min = f(arr[0]); for (int i = 1; i &lt; n; i++) { if (f(arr[i]) &lt; min) { min = f(arr[i]); } } return min; } int main(void) { int arr[7] = {3, 1, -4, 1, 5, 9, -2}; cout &lt;&lt; arrFnMin(arr, 7, square) &lt;&lt; endl; cout &lt;&lt; arrFnMin(arr, 7, myFunc) &lt;&lt; endl; cout &lt;&lt; arrFnMin(arr, 7, cube) &lt;&lt; endl; return 0; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 void Main() { int[] arr = { 3, 1, -4, 1, 5, 9, -2 }; ArrFnMin(arr, 7, Square).Dump(); ArrFnMin(arr, 7, MyFunc).Dump(); ArrFnMin(arr, 7, Cube).Dump(); \"----------\".Dump(); ArrFnMin(arr, Square).Dump(); ArrFnMin(arr, MyFunc).Dump(); ArrFnMin(arr, Cube).Dump(); } int Square(int x) { return x * x; } int MyFunc(int x) { return x * (x - 15) / 2; } int Cube(int x) { return x * x * x; } int ArrFnMin(int[] arr, int n, Func&lt;int, int&gt; func) { int min = func(arr[0]); for (int i = 1; i &lt; n; i++) { if (func(arr[i]) &lt; min) { min = func(arr[i]); } } return min; } int ArrFnMin(ReadOnlySpan&lt;int&gt; arr, Func&lt;int, int&gt; func) { int min = func(arr[0]); foreach (int item in arr[1..]) { if (func(item) &lt; min) { min = func(item); } } return min; } Rust Example 1 2 3 4 5 6 7 fn compare(a: i32, b: i32, check: fn(x: i32, y: i32) -&gt; bool) -&gt; bool { check(a, b) } fn main() { println!(\"{}\", compare(2, 2, |a, b| a == b)); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 use winsafe::OutputDebugString; fn square(x: i32) -&gt; i32 { x * x } fn my_func(x: i32) -&gt; i32 { x * (x - 15) / 2 } fn cube(x: i32) -&gt; i32 { x * x * x } fn arr_min1(arr: Box&lt;[i32]&gt;, arr_fn: fn(i32) -&gt; i32) -&gt; i32 { let mut min = arr_fn(arr[0]); for i in 1..arr.len() { if arr_fn(arr[i]) &lt; min { min = arr_fn(arr[i]); } } min } fn arr_min2(arr: &amp;Vec&lt;i32&gt;, arr_fn: fn(i32) -&gt; i32) -&gt; i32 { let mut min = arr_fn(arr[0]); for i in 1..arr.len() { if arr_fn(arr[i]) &lt; min { min = arr_fn(arr[i]); } } min } fn main() { let f = arr_min1; let arr = [3, 1, -4, 1, 5, 9, -2]; let debug = &amp;f(arr.into(), square).to_string()[..]; println!(\"{}\", debug); OutputDebugString(debug); println!(\"{}\", f(arr.into(), my_func)); println!(\"{}\", f(arr.into(), cube)); let f = arr_min2; let arr = vec![3, 1, -4, 1, 5, 9, -2]; println!(\"{}\", f(&amp;arr, square)); println!(\"{}\", f(&amp;arr, my_func)); println!(\"{}\", f(&amp;arr, cube)); } 참고할 만한 강좌(링크) 예제로 배우는 C# 프로그래밍, C# delegate의 개념 예제로 배우는 C# 프로그래밍, Action, Func, Predicate" } , { "title": "Rust 예외 및 에러 처리", "url": "/20240517-1/", "date": "2024-05-17", "categories": "【 RustㆍZigㆍGo 】, Rust", "tags": "rust", "content": "일반적인 프로그래밍 언어에서는 에러 처리를 위한 예외 처리가 기본이다. (원칙적으로 예외 처리는 내 소스코드 범위를 벗어난 어쩔 수 없는 에러(예, DB연결)가 발생할 때 처리하는 것을 말한다. 나머지는 assert 처리하여 버그를 수정해야 한다. ) 그렇지만 러스트(rust)는 예외 기능이 없다. 대신, 복구 가능한 에러를 위한 Result&lt;T, E&gt; 값과 복구 불가능한 에러가 발생했을 때 실행을 멈추는 panic! 매크로를 가지고 있다.1 또한 러스트는 다른 언어들이 가지는 Null이라는 기능이 없다. 러스트는 Null을 사용하지 않고, 존재하거나 존재하지 않음을 나타내는 개념을 나타내는 Option&lt;T&gt;을 이용한다.2 Result 및 Option 정의 1 2 3 4 enum Result&lt;T, E&gt; { Ok(T), Err(E) } 1 2 3 4 enum Option&lt;T&gt; { None, Some(T) } 여기에서 사용하는 대부분의 예제는 mithradates의 Easy Rust Korean를 참고하였다. Option 예제 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 fn take_fifth(value: Vec&lt;i32&gt;) -&gt; Option&lt;i32&gt; { if value.len() &lt; 5 { None } else { Some(value[4]) } } fn main() { let new_vec1 = vec![1, 2, 3, 4, 5]; let index1 = take_fifth(new_vec1); match index1 { Some(number) =&gt; println!(\"I got a number: {}\", number), None =&gt; println!(\"There was nothing inside\"), } let new_vec2 = vec![1, 2]; let index2 = take_fifth(new_vec2); if index2.is_some() { println!(\"I got a number: {}\", index2.unwrap()); } else { println!(\"There was nothing inside\"); } } Result 예제 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 fn check_error(input: i32) -&gt; Result&lt;(), ()&gt; { if input % 2 == 0 { Ok(()) } else { Err(()) } } fn main() { if check_error(5).is_ok() { println!(\"It's okay, guys!\"); } else { println!(\"It's an error, guys!\"); } match check_error(4) { Ok(..) =&gt; println!(\"It's okay, guys!\"), Err(..) =&gt; println!(\"It's an error, guys!\") } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 fn check_if_five(number: i32) -&gt; Result&lt;i32, String&gt; { match number { 5 =&gt; Ok(number), _ =&gt; Err(\"Sorry, the number wasn't five.\".to_string()), } } fn main() { let mut result_vec = Vec::new(); // Vec&lt;Result&lt;i32, String&gt;&gt; for number in 2..=7 { result_vec.push(check_if_five(number)); } println!(\"{:#?}\", result_vec); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 fn parse_number(number: &amp;str) -&gt; Result&lt;i32, std::num::ParseIntError&gt; { number.parse() } fn main() { let mut result_vec: Vec&lt;Result&lt;i32, std::num::ParseIntError&gt;&gt; = vec![]; result_vec.push(parse_number(\"8\")); result_vec.push(parse_number(\"one\")); result_vec.push(parse_number(\"7\")); for number in result_vec { if number.is_ok() { println!(\"Ok: {:?}\", number.unwrap()) } else { println!(\"Err: {:?}\", number.unwrap_err().kind()) } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 fn parse_number(number: &amp;str) -&gt; Result&lt;i32, std::num::ParseIntError&gt; { number.parse() } fn main() { let mut result_vec: Vec&lt;Result&lt;i32, std::num::ParseIntError&gt;&gt; = vec![]; result_vec.push(parse_number(\"8\")); result_vec.push(parse_number(\"one\")); result_vec.push(parse_number(\"7\")); for index in 0..result_vec.iter().count() { if let Some(number) = result_vec.get(index) { println!(\"{:?}\", number.as_ref().unwrap_or(&amp;0)); } } } 1 2 3 4 5 6 7 8 9 10 11 fn main() { let item_vec = vec![vec![\"홍길동\", \"홍길서\", \"홍길남\", \"홍길북\", \"10\"], vec![\"가나닭\", \"20\", \"30\"]]; for mut item in item_vec { while let Some(info) = item.pop() { if let Ok(number) = info.parse::&lt;i32&gt;() { println!(\"The number is: {}\", number); } } } } rust 예외처리 1 2 3 4 5 6 7 8 9 10 11 12 13 fn main() { let item_vec = vec![vec![\"홍길동\", \"10\"], vec![\"가나닭\", \"20\", \"30\"]]; for mut item in item_vec { while let Some(info) = item.pop() { if let Ok(number) = info.parse::&lt;i32&gt;() { println!(\"The number is: {}\", number); } else if let Err(e) = info.parse::&lt;i32&gt;() { println!(\"Error : {}\", e); } } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 fn main() { let item_vec = vec![vec![\"홍길동\", \"10\"], vec![\"가나닭\", \"20\", \"30\"]]; for mut item in item_vec { while let Some(info) = item.pop() { if let Ok(number) = info.parse::&lt;i32&gt;() { println!(\"The number is: {}\", number); } else { println!(\"Error : {:?}\", info.parse::&lt;i32&gt;().err().unwrap().kind()); } } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 use std::num::ParseIntError; fn parse_str(input: &amp;str) -&gt; Result&lt;i32, ParseIntError&gt; { let parsed_number = input.parse::&lt;i32&gt;()?; // return Error Ok(parsed_number) } fn main() { for item in vec![\"one\", \"2\", \"3\"] { let parsed = parse_str(item); println!(\"{:?}\", parsed); } println!(\"-----------------\"); let my_vec = vec![\"one\", \"2\", \"3\"]; let result = my_vec.iter().filter_map(|&amp;s| parse_str(s).ok()).collect::&lt;Vec&lt;_&gt;&gt;(); for item in result { println!(\"{:?}\", item); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 use std::num::ParseIntError; fn parse_str(input: &amp;str) -&gt; Result&lt;i32, ParseIntError&gt; { let parsed_number = input.parse::&lt;i32&gt;()?; Ok(parsed_number) } fn main() { let mut result_number: Vec&lt;i32&gt; = Vec::new(); let mut result_string: Vec&lt;String&gt; = Vec::new(); for item in vec![\"one\", \"2\", \"3\"] { let parsed = parse_str(item); if let Ok(number) = parsed { result_number.push(number); } else if let Err(e) = parsed { result_string.push(item.to_string() + \"#:$\" + &amp;e.to_string()); } } println!(\"{:?}\", result_number); println!(\"{:?}\", result_string); for n in result_number { println!(\"{}\", n); } for s in result_string { println!(\"{}\", s.split(\"#:$\").next().unwrap()); println!(\"{}\", s.split(\"#:$\").fold(\"\".to_string(), |_, b| b.to_string())); } } 예외처리 추가예제3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 use std::io::{Read, Write}; use std::net::{TcpListener, TcpStream}; fn handle_client(mut stream: TcpStream) { let mut buffer = [0; 1024]; if stream.read(&amp;mut buffer).is_ok() { println!(\"success to read from client!\"); } else { println!(\"Failed to read from client!\"); } let request = String::from_utf8_lossy(&amp;buffer[..]); println!(\"Received request: {}\", request); let response = \"Hello, Client!\".as_bytes(); let result = stream.write(response); match result { Ok(size) =&gt; println!(\"{}\", size), Err(_) =&gt; println!(\"Failed to write response!\"), } } fn main() { let listener = TcpListener::bind(\"127.0.0.1:8080\"); if let Ok(result) = listener { for stream in result.incoming() { match stream { Ok(stream) =&gt; { std::thread::spawn(|| handle_client(stream)); } Err(e) =&gt; { eprintln!(\"Failed to establish connection: {}\", e); } } } } else { println!(\"Failed to bind to address\"); } } Reference doc.rust-kr.org, “rust 에러처리” snowapril.github.io, “Rust가 Null을 도입하지 않은 이유” youtube.com/@BekBrace, “Network Programming in Rust - Building a TCP Server”" } , { "title": "C#, Native Safe Buffer 구현", "url": "/20230813-1/", "date": "2023-08-13", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "csharp, helper", "content": "Native Safe Buffer를 구현하기 위해 ref struct 와 NativeMemory 클래스를 사용한다. ref struct는 주로 실제 메모리 주소(포인터)를 들고 있는 Span&lt;T&gt;을 안전하게 만들기 위해 존재하고 그 메모리를 안전하고 빠르게 다루는 규칙이라고 할 수 있다. NativeMemory 클래스는 가비지 컬렉터(GC)의 관리를 받지 않는 네이티브 힙 메모리를 직접 할당하고 관리할 수 있게 해주는 도구인데, 기존의 Marshal.AllocHGlobal이나 stdole 등을 이용하던 방식보다 성능이 뛰어나고 C 언어의 malloc, free와 유사한 인터페이스를 제공한다. 다만 GC가 메모리를 치워주지 않으므로 Free호출이 필수이며, 잘못된 주소에 접근하면 프로그램이 즉시 Access Violation 될 수 있다. 메모리 할당 주체 배열로부터 가져올 때: Span&lt;byte&gt; span = new byte[1024]; : 이것은 결국 Managed 배열이고. GC가 관리하며, 대용량일 경우 GC 부하(LOH 등)가 발생한다. 스택으로부터 가져올 때: Span&lt;byte&gt; span = stackalloc byte[1024]; : GC 부하가 없고 빠르지만, 크기 제한이 엄격하며 보통 1MB가 넘어가면 StackOverflowException 발생 네이티브 메모리로부터 가져올 때: NativeMemory.Alloc(...count...) : 가장 빠르고 크기 제한도 없지만, 수동으로 해제(Free) 하지 않으면 메모리 누수(Memory Leak)가 발생 stackalloc으로 만든 Span은 그 함수가 끝나면 사라지지만 네이티브 메모리로 만든 NativeSafeBuffer는 (비록 ref struct라 제약은 있지만) Dispose를 호출하기 전까지는 메모리가 안정적으로 유지된다. 비교 항목 new byte[] (Span) stackalloc (Span) NativeSafeBuffer 관리 주체 가비지 컬렉터 (GC) 스택 (Stack) OS (Native) 해제 시점 GC가 한가할 때 함수 종료 시 즉시 Dispose 호출 시 즉시 크기 제한 힙 메모리만큼 매우 작음 (1MB) RAM 용량만큼 안전성 매우 안전함 빠르지만 위험함 수동 해제 필수 (using) NativeSafeBuffer.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 using System; using System.Runtime.InteropServices; namespace ConsoleProject; public unsafe ref struct NativeSafeBuffer&lt;T&gt; where T : unmanaged { private T* mPtr; public int Length { get; } public readonly int ByteCount =&gt; Length * sizeof(T); public NativeSafeBuffer(int count) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(count); Length = count; mPtr = (T*)NativeMemory.Alloc((nuint)(count * sizeof(T))); } public readonly ref T this[int index] { get { if (mPtr == null || (uint)index &gt;= (uint)Length) { throw new IndexOutOfRangeException(); } return ref mPtr[index]; } } public readonly Span&lt;T&gt; AsSpan() =&gt; mPtr == null ? Span&lt;T&gt;.Empty : new Span&lt;T&gt;(mPtr, Length); public void Dispose() { // Console.WriteLine(\"해제(Dispose)\"); if (mPtr == null) { return; } NativeMemory.Free(mPtr); mPtr = null; } } Program.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 using System; using System.Text; namespace ConsoleProject; internal class Program { private static void Main() { try { using NativeSafeBuffer&lt;byte&gt; byteBuffer = new(1024); const string message = \"Hello, Native Memory!\"; int written = Encoding.UTF8.GetBytes(message, byteBuffer.AsSpan()); byteBuffer[0] = (byte)'h'; string result = Encoding.UTF8.GetString(byteBuffer.AsSpan()[..written]); Console.WriteLine(result); // H -&gt; h Console.WriteLine($\"[byte 버퍼] 요소 개수: {byteBuffer.Length}, 할당된 총 바이트: {byteBuffer.ByteCount} bytes\"); Console.WriteLine(\"---------------------------\"); using NativeSafeBuffer&lt;int&gt; intBuffer = new(10); intBuffer[0] = 123; intBuffer[9] = 999; Console.WriteLine($\"Numbers: {intBuffer[0]}, {intBuffer[9]}\"); Console.WriteLine($\"[Integer 버퍼] 요소 개수: {intBuffer.Length}, 할당된 총 바이트: {intBuffer.ByteCount} bytes\"); Console.WriteLine(\"---------------------------\"); using NativeSafeBuffer&lt;char&gt; charBuffer = new(1024); const string message_char = \"안녕하세요 C# 네이티브!\"; if (!message_char.AsSpan().TryCopyTo(charBuffer.AsSpan())) { Console.WriteLine(\"버퍼가 부족하여 복사를 중단했습니다.\"); return; } charBuffer[message_char.Length - 1] = '?'; // ! -&gt; ? ReadOnlySpan&lt;char&gt; resultSpan = charBuffer.AsSpan()[..message_char.Length]; Console.WriteLine(resultSpan.ToString()); Console.WriteLine($\"Charsize: {resultSpan.Length}\"); for (int i = 0; i &lt; 10; i++) { charBuffer[i] = (char)('A' + i); } Console.WriteLine(charBuffer.AsSpan()[..10].ToString()); Console.WriteLine($\"[Char 버퍼] 요소 개수: {charBuffer.Length}, 할당된 총 바이트: {charBuffer.ByteCount} bytes\"); Console.WriteLine(\"---------------------------\"); } catch (Exception ex) { Console.WriteLine($\"오류 발생: {ex.Message}\"); } } }" } , { "title": "C# Concurrency with IProgress", "url": "/20230811-1/", "date": "2023-08-11", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "csharp", "content": "C#은 동시성(Concurrency) 구현을 쉽게 할 수 있는데 이번 글에서 여러 개의 비동기 함수를 병렬처리 형태로 구현해 보는 예제를 살펴볼 것이다. Concurrency 방식과 Parallel 방식의 비교1는 이전 글인 Concurrency in C# Example에서 살펴보았다. 아래의 예제처럼 각 1초, 2초, 3초를 소요하는 비동기 방식의 함수가 3개 있다고 가정하고 IProgress 또는 리턴값을 처리한다고 하면 총 6초가량이 걸리게 된다. 1 2 3 4 5 6 async Task Main() { string returnValue1 = await ProgressA(); //1초 string returnValue2 = await ProgressB(); //2초 string returnValue3 = await ProgressC(); //3초 : 총 6초 } 위의 예제를 아래처럼 Concurrency를 사용하면 총 3초가 소요되는 병렬처리가 가능하다.2 Concurrency 예제 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 async Task Main() { Stopwatch sw = new(); sw.Start(); Progress&lt;string&gt; progress = new(value =&gt; { $\"Start....................{value}\".Dump(); }); Progress&lt;int&gt; progressNumber = new(value =&gt; { $\"Start....................{value}\".Dump(); }); List&lt;Task&lt;string&gt;&gt; tasks = new() { ProgressA(progress), ProgressB(progressNumber), ProgressC(progress) }; string[] result = await Task.WhenAll(tasks); foreach (string str in result) { str.Dump(); } sw.Stop(); (\"Stop.....\" + sw.ElapsedMilliseconds / 1000.0 + \"초\").Dump(); } private async Task&lt;string&gt; ProgressA(IProgress&lt;string&gt; progress) { progress.Report(\"A\"); await Task.Delay(2000); return \"return A\"; } private async Task&lt;string&gt; ProgressB(IProgress&lt;int&gt; progressNumber) { progressNumber.Report(3); await Task.Delay(3000); return \"return B\"; } private async Task&lt;string&gt; ProgressC(IProgress&lt;string&gt; progress) { progress.Report(\"C\"); await Task.Delay(1000); return \"return C\"; } Reference devsight, “Concurrency in C# Example” gavilanch3, “Introduction to Asynchronous Programming in C#”" } , { "title": "Rust, async reqwest(request)", "url": "/20220216-1/", "date": "2022-02-16", "categories": "【 RustㆍZigㆍGo 】, Rust", "tags": "rust, api", "content": "Rust 프로그램에서 HTTP, Rest API(webapi)를 사용하기 위해 reqwest 패키지를 활용한 기본적인 아래의 예제는 blocking 방식과 async로 확장한 방법을 보여준다. 예제에 필요한 기본적인 패키지는 reqwest 외에 tokio, serde를 사용한다. 예제에 사용한 베이스 코드는 Proful Sadangi(Youtube)1를 참고2하였다. 환경구성(vscode) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #Cargo.toml [dependencies] reqwest = { version = \"0.11\", features = [\"blocking\",\"json\"] } tokio = { version = \"1.15.0\", features = [\"full\"] } serde = { version = \"1.0\", features = [\"derive\"] } serde_json = \"1.0.74\" #rustfmt.toml max_width = 200 fn_args_layout = \"Compressed\" use_small_heuristics = \"Max\" #.cargo/config : static compile option [target.x86_64-pc-windows-msvc] rustflags = [\"-C\", \"target-feature=+crt-static\"] 기본 Blocking 예제 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 fn main() { let posts_url = \"https://jsonplaceholder.typicode.com/posts\"; // [1] blocking reqwest GET call // let mut resp = reqwest::blocking::get(posts_url).unwrap(); // resp.copy_to(&amp;mut std::io::stdout()).unwrap(); // [2] blocking reqwest GET call let resp = reqwest::blocking::get(posts_url).unwrap(); let resp_url = resp.url().clone().to_string(); let resp_host = resp.url().host().clone().unwrap().to_string(); let resp_status = resp.status().clone().to_string(); let resp_header_result; let resp_header = resp.headers().get(\"content-type\").clone(); if resp_header.is_some() { resp_header_result = resp_header.clone().unwrap().to_str().unwrap(); } else { resp_header_result = \"Error\"; } println!(\"{}, {}, {}, {}\", resp_url, resp_host, resp_status, resp_header_result.to_string()); // [3] blocking reqwest GET call let post_res = reqwest::blocking::get(posts_url).unwrap().text().unwrap(); println!(\"{}\", post_res); } async 기본 예제 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 use reqwest::Error; use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize)] struct Post { #[serde(rename = \"userId\")] user_id: i32, id: i32, title: String, body: String, } #[tokio::main] async fn main() -&gt; Result&lt;(), Error&gt; { // [1] let posts_url = \"https://jsonplaceholder.typicode.com/posts\"; let resp = reqwest::get(posts_url).await.unwrap(); let posts: serde_json::Value = resp.json().await.unwrap(); println!(\"{}\", &amp;posts); println!(\"{}\", &amp;posts[10][\"body\"]); println!(\"{}\", &amp;posts[10][\"body\"].as_str().unwrap()); // [2] let posts_url = \"https://jsonplaceholder.typicode.com/posts\"; let resp = reqwest::get(posts_url).await.unwrap(); let posts: Vec&lt;Post&gt; = resp.json().await.unwrap(); for post in posts { println!(\"{:?} {:?} {:?} {:?}\", post.user_id, post.id, post.title, post.body); } // [3] let posts_url = \"https://jsonplaceholder.typicode.com/posts\"; let post_map = serde_json::json!({ \"userId\" : \"1000\", \"title\" : \"foo\", \"body\" : \"bar\", }); let client = reqwest::Client::new(); let resp = client.post(posts_url).json(&amp;post_map).send().await.unwrap(); let post_json: serde_json::Value = resp.json().await.unwrap(); println!(\"{:?}\", serde_json::to_string_pretty(&amp;post_json).unwrap()); // [4] let posts_url = \"https://jsonplaceholder.typicode.com/posts\"; let resp = reqwest::get(posts_url).await?.json::&lt;Vec&lt;Post&gt;&gt;().await; // println!(\"{:?}\", serde_json::to_string_pretty(&amp;resp)); if resp.is_ok() { println!(\"{:?}\", serde_json::to_string_pretty(&amp;resp.ok()).unwrap()); } else { println!(\"xxxxxxxxxxxxxx\"); } Ok(()) } 예제 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 use reqwest::Error; use serde::{Deserialize, Serialize}; use std::collections::HashMap; #[derive(Debug, PartialEq, Deserialize, Serialize)] struct IpInfo { ip: String, city: String, region: String, country: String, loc: String, org: String, postal: String, timezone: String, readme: String, } #[tokio::main] async fn main() -&gt; Result&lt;(), Error&gt; { let res_ipinfo1 = reqwest::get(\"http://ipinfo.io/json\").await; //.text().await?; if res_ipinfo1.is_ok() { let result = res_ipinfo1?.text().await?; if result.contains(\"error\") { println!(\"Error Message : {}\", result.replace(\"\\n\", \"\")); } else { println!(\"{}\\n\", result); } } else { if let Err(e) = res_ipinfo1 { println!(\"Error Message : {}\", e); } } let res_ipinfo2 = reqwest::get(\"http://ipinfo.io/json\").await; // ?.json::&lt;HashMap&lt;String, String&gt;&gt;().await?; if res_ipinfo2.is_ok() { let a = res_ipinfo2?.json::&lt;HashMap&lt;String, String&gt;&gt;().await; if a.is_ok() { let b = a?; let mut items: Vec&lt;_&gt; = b.iter().collect(); items.sort(); for (key, value) in items.iter() { println!(\"{} : {}\", key, value); } } else { println!(\"zzzzzzzzzzzz\"); } } else { println!(\"yyyyyyyyyyyyyyyy\"); } Ok(()) } 예제 2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 use reqwest::Error; use serde::Deserialize; #[derive(Deserialize, PartialEq, Debug)] struct HttpBin { slideshow: Show, } #[derive(Deserialize, PartialEq, Debug)] struct Show { author: String, date: String, slides: Vec&lt;Slide&gt;, title: String, } #[derive(Deserialize, PartialEq, Debug)] struct Slide { items: Option&lt;Vec&lt;String&gt;&gt;, title: String, r#type: String, } #[tokio::main] async fn main() -&gt; Result&lt;(), Error&gt; { let res = reqwest::get(\"https://httpbin.org/json\").await; if res.is_ok() { println!(\"{}\", res?.text().await?); } else if let Err(e) = res { println!(\"Error Message : {}\", e); } println!(\"-------------------------------------\"); let res_httpbin = reqwest::get(\"https://httpbin.org/json\").await?.json::&lt;HttpBin&gt;().await?; // println!(\"slideshow : {:?}\\n\", res_httpbin.slideshow); println!(\"author : {}\", res_httpbin.slideshow.author); println!(\"date : {}\", res_httpbin.slideshow.date); println!(\"title : {}\", res_httpbin.slideshow.title); println!(\"-------------------------------------\"); for (k1, v1) in res_httpbin.slideshow.slides.iter().enumerate() { for v2 in v1.items.iter() { for (k3, v3) in v2.iter().enumerate() { println!(\"slides[{}] : items[{}] : {}\", k1, k3, v3); } } println!(\"slides[{}] : title : {}\", k1, v1.title); println!(\"slides[{}] : type : {}\", k1, v1.r#type); } Ok(()) } // 결과 // { // \"slideshow\": { // \"author\": \"Yours Truly\", // \"date\": \"date of publication\", // \"slides\": [ // { // \"title\": \"Wake up to WonderWidgets!\", // \"type\": \"all\" // }, // { // \"items\": [ // \"Why &lt;em&gt;WonderWidgets&lt;/em&gt; are great\", // \"Who &lt;em&gt;buys&lt;/em&gt; WonderWidgets\" // ], // \"title\": \"Overview\", // \"type\": \"all\" // } // ], // \"title\": \"Sample Slide Show\" // } // } // // ------------------------------------- // author : Yours Truly // date : date of publication // title : Sample Slide Show // ------------------------------------- // slides[0] : title : Wake up to WonderWidgets! // slides[0] : type : all // slides[1] : items[0] : Why &lt;em&gt;WonderWidgets&lt;/em&gt; are great // slides[1] : items[1] : Who &lt;em&gt;buys&lt;/em&gt; WonderWidgets // slides[1] : title : Overview // slides[1] : type : all 추천 강좌 및 문서 pintuch, “Rust - Reqwest examples” dcode, “Rust Programming Tutorial #38 - HTTP Get Request (reqwest Crate)” robertohuertasm, “Rust, sesiones prácticas - API REST” Jeremy Chone, “Rust WebDev - TodoMVC - 1~3/3 - Data Access Layer” Jenifer Champagne, “Introduction to Rust syntax with a REST API built with Rocket” Reference Proful Sadangi, “reqwest Rust Tutorial” DebugJO, “rust_get_json.rs”" } , { "title": "REST API .NET 6.0, MongoDB", "url": "/20220123-1/", "date": "2022-01-23", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "csharp, database", "content": "Intro to MongoDB with C#에 이어 이번 주제는 MongoDB와 함께 ASP.NET Core 6 REST API1를 구현하는 예제(Sample)이다. 이전과 마찬가지로 Nuget에서 MongoDB.Driver 패키지를 추가한다. appsettings.json 1 2 3 4 5 6 7 8 9 10 11 12 13 14 { \"UserInfoDatabaseSetting\": { \"UserInfoCollectionName\": \"UserInfo\", \"ConnectionString\": \"mongodb://&lt;ID&gt;:&lt;Password&gt;@localhost:27017/?serverSelectionTimeoutMS=5000&amp;connectTimeoutMS=10000&amp;authSource=testdb&amp;authMechanism=SCRAM-SHA-256\", \"DatabaseName\": \"testdb\" }, \"Logging\": { \"LogLevel\": { \"Default\": \"Information\", \"Microsoft.AspNetCore\": \"Warning\" } }, \"AllowedHosts\": \"*\" } UserInfoDatabaseSetting.cs 1 2 3 4 5 6 7 8 9 10 // interface namespace UserInfoManagement.Models { public interface IUserInfoDatabaseSetting { string UserInfoCollectionName { get; set; } string ConnectionString { get; set; } string DatabaseName { get; set; } } } 1 2 3 4 5 6 7 8 9 10 // class model namespace UserInfoManagement.Models { public class UserInfoDatabaseSetting : IUserInfoDatabaseSetting { public string UserInfoCollectionName { get; set; } = string.Empty; public string ConnectionString { get; set; } = string.Empty; public string DatabaseName { get; set; } = string.Empty; } } MongoDB, testdb.UserInfo 1 2 3 4 5 6 7 8 9 10 11 12 { \"_id\" : ObjectId(\"61eaa77a9102f8d5ae5f01c4\"), \"UserId\" : \"1111\", \"UserPwd\" : \"1111\", \"UserName\" : \"홍길동\" } { \"_id\" : ObjectId(\"61eaa976318b8b74ca7fc524\"), \"UserId\" : \"2222\", \"UserPwd\" : \"2222\", \"UserName\" : \"가나닭\" } UserInfo.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; namespace UserInfoManagement.Models { public class UserInfo { [BsonId] [BsonRepresentation(BsonType.ObjectId)] public string Id { get; set; } = string.Empty; [BsonElement(\"UserId\")] public string UserId { get; set; } = string.Empty; [BsonElement(\"UserPwd\")] public string UserPwd { get; set; } = string.Empty; [BsonElement(\"UserName\")] public string UserName { get; set; } = string.Empty; } } UserInfoService.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 // interface using UserInfoManagement.Models; namespace UserInfoManagement.Services { public interface IUserInfoService { Task&lt;List&lt;UserInfo&gt;&gt; GetList(); UserInfo Get(string id); UserInfo Create(UserInfo userInfo); void Update(string id, UserInfo userInfo); void Remove(string id); } } // class(Service) using MongoDB.Driver; using UserInfoManagement.Models; namespace UserInfoManagement.Services { public class UserInfoService : IUserInfoService { private readonly IMongoCollection&lt;UserInfo&gt; _userInfo; public UserInfoService(IUserInfoDatabaseSetting setting, IMongoClient mongoClient) { var database = mongoClient.GetDatabase(setting.DatabaseName); _userInfo = database.GetCollection&lt;UserInfo&gt;(setting.UserInfoCollectionName); } public UserInfo Create(UserInfo userInfo) { _userInfo.InsertOne(userInfo); return userInfo; } public async Task&lt;List&lt;UserInfo&gt;&gt; GetList() { var cursor = await _userInfo.FindAsync(u =&gt; true); return cursor.ToList(); } public UserInfo Get(string userId) { return _userInfo.Find(u =&gt; u.UserId == userId).FirstOrDefault(); } public void Remove(string userId) { _userInfo.DeleteOne(u =&gt; u.UserId == userId); } public void Update(string userId, UserInfo userInfo) { _userInfo.ReplaceOne(u =&gt; u.UserId == userId, userInfo); } } } UserInfoController.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 using Microsoft.AspNetCore.Mvc; using UserInfoManagement.Models; using UserInfoManagement.Services; namespace UserInfoManagement.Controllers { [Route(\"api\")] [ApiController] public class UserInfoController : ControllerBase { private readonly IUserInfoService userInfoService; public UserInfoController(IUserInfoService userInfoService) { this.userInfoService = userInfoService; } [HttpPost(\"GetUserInfoList\")] public async Task&lt;ActionResult&lt;List&lt;UserInfo&gt;&gt;&gt; GetUserInfoList() { return await userInfoService.GetList(); } [HttpPost(\"GetUserInfo\")] public ActionResult&lt;UserInfo&gt; GetUserInfo([FromBody] UserInfo userInfo) { var _userInfo = userInfoService.Get(userInfo.UserId); if (_userInfo == null) { return new UserInfo { UserId = userInfo.UserId + \" : 사용자 없음\" }; } return _userInfo; } } } Program.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 using Microsoft.Extensions.Options; using MongoDB.Driver; using UserInfoManagement.Models; using UserInfoManagement.Services; var builder = WebApplication.CreateBuilder(args); builder.Services.Configure&lt;UserInfoDatabaseSetting&gt;(builder.Configuration.GetSection(nameof(UserInfoDatabaseSetting))); builder.Services.AddSingleton&lt;IUserInfoDatabaseSetting&gt;(s =&gt; s.GetRequiredService&lt;IOptions&lt;UserInfoDatabaseSetting&gt;&gt;().Value); builder.Services.AddSingleton&lt;IMongoClient&gt;(s =&gt; new MongoClient(builder.Configuration.GetValue&lt;string&gt;(\"UserInfoDatabaseSetting:ConnectionString\"))); builder.Services.AddScoped&lt;IUserInfoService, UserInfoService&gt;(); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run(); Reference kudvenkat, “ASP.NET Core 6 REST API Tutorial, MongoDB Database”" } , { "title": "Concurrency in C# Example", "url": "/20211208-1/", "date": "2021-12-08", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "csharp", "content": "C#, .NET 프로그램에 사용할 수 있는 라이브러리 및 언어 기능을 사용하면 동시성(Concurrency) 구현을 쉽게 할 수 있다. 동시성 개요1를 다시 정리해 보면 다음과 같다. Concurrency : Doing more than one thing at a time. (Concurrency Example) Multithreading : A form of concurrency that uses multiple threads of execution. Parallel Processing : Doing lots of work by dividing it up among multiple threads that run concurrently. Asynchronous Programming : A form of concurrency that uses futures or callbacks to avoid unnecessary threads. (Async-Await-Task Example) Reactive Programming : A declarative style of programming where the application reacts to events. 여기에 사용한 예제2는 gavilanch3(Youtube) - Introduction to Concurrency in C#, 강좌를 참고하였다. 일반 비동기 방식 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 using System.Diagnostics; var stopWatch = new Stopwatch(); var names = new List&lt;string&gt;() { \"홍길동\", \"홍길서\", \"홍길남\", \"홍길북\" }; Console.WriteLine(\"Default Start...(8초)\"); stopWatch.Start(); foreach (var name in names) { await Method1(name); await Method2(name); await Method3(name); await Method4(name); } stopWatch.Stop(); WriteTime(stopWatch.Elapsed.Seconds, ConsoleColor.Red); Concurrency 방식 1 2 3 4 5 6 Console.WriteLine(\"Concurrency Start...(2초)\"); stopWatch.Restart(); var validations = names.Select(name =&gt; MethodValidation(name)); await Task.WhenAll(validations); stopWatch.Stop(); WriteTime(stopWatch.Elapsed.Seconds, ConsoleColor.Red); Parallel 방식 1 2 3 4 5 6 7 8 9 Console.WriteLine(\"Parallel Start...(2초)\"); stopWatch.Restart(); var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = 4 }; await Parallel.ForEachAsync(names, parallelOptions, async (name, token) =&gt; { await MethodValidation(name); }); stopWatch.Stop(); WriteTime(stopWatch.Elapsed.Seconds, ConsoleColor.Red); 공통 소스 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 static async Task MethodValidation(string name) { await Method1(name); await Method2(name); await Method3(name); await Method4(name); } static async Task Method1(string name) { await Task.Delay(500); Console.WriteLine(\"Method1 : \" + name); } static async Task Method2(string name) { await Task.Delay(500); Console.WriteLine(\"Method2 : \" + name); } static async Task Method3(string name) { await Task.Delay(500); Console.WriteLine(\"Method3 : \" + name); } static async Task Method4(string name) { await Task.Delay(500); Console.WriteLine(\"Method4 : \" + name); } static void WriteTime(int seconds, ConsoleColor color) { Console.ForegroundColor = color; Console.WriteLine(\"소요시간 : \" + seconds.ToString() + \"\\r\\n\"); Console.ForegroundColor = ConsoleColor.White; } Reference oreilly.com, “Concurrency: An Overview” gavilanch3, “Using Task.WhenAll - Avoid Inefficient Code” / gavilanch3, “Parallel.ForEachAsync - Concurrent Tasks with a Limit”" } , { "title": "C# Delegate", "url": "/20210906-2/", "date": "2021-09-06", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "csharp, cpp", "content": "Delegate(델리게이트, 대리자)는 C#에서 매우 중요한 개념이다. Delegate는 CLI (Common Language Infrastructure)에서 사용하는 Type-safe function pointer의 한 형태1라고 설명할 수 있다. Method를 참조하여 호출하기에 콜백 및 이벤트 리스너2를 구현하는 데 사용한다. C++의 함수 포인터3와 같은 개념이라고 보면 된다. 단순하게 설명하면 int, string 타입처럼 함수를 변수처럼 선언하거나 함수의 파라미터로 활용4할 수 있게 해주는 것이다. 아래의 코드(소스)는 델리게이트, 함수포인터의 개념을 이해하고 이를 활용하는 예제이다. Delegate의 형태에는 Action, Func, Predicate5가 있다. LINQ, Lambda 1 2 3 // Lambda와 함께 LINQ를 사용할 수 있는 이유 : Delegate (Action, Func, Predicate) List&lt;int&gt; list = new() { 2, 3, 4, 5, 6, 7 }; list.Where(x =&gt; x &gt; 5).ToList().ForEach(Console.WriteLine); C++ 함수포인터 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include &lt;iostream&gt; using namespace std; bool compare(int a, int b) { return a == b; } int square(int x) { return x * x; } int myFunc(int x) { return x * (x - 15) / 2; } int arrayMin(const int arr[], int n, int (*f)(int)) { int min = f(arr[0]); for (int i = 1; i &lt; n; i++) { if (f(arr[i]) &lt; min) { min = f(arr[i]); } } return min; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int main() { // bool (*fp)(int, int); // fp = &amp;compare; // fp = compare; bool (*fp)(int, int) = compare; // bool res = compare(2, 3); // bool res = (*fp)(2, 3); bool res = fp(2, 3); cout &lt;&lt; res &lt;&lt; endl; int arr[7] = {3, 1, 4, 1, 5, 9, 2}; cout &lt;&lt; arrayMin(arr, 7, square) &lt;&lt; endl; cout &lt;&lt; arrayMin(arr, 7, myFunc) &lt;&lt; endl; return 0; } Delegate 이해 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 using System; namespace ConsoleApp1 { internal class Program { private static void Main() { _ = new TestClass(); } } public class TestClass { private const int _NUM = 10; private const string _STR = \"가나닭\"; // 위의 변수 선언처럼 아래와 같은 함수를 변수, 파라미터에 담고 싶다면??? // int function = FuncTest(): // 오류 // 델리게이트로 오류 해결 : C++에서 함수포인터 개념 public static void FuncTest() { Console.WriteLine(\"Hello World\"); } public static int Add(int a, int b) { return a + b; } private delegate void MyDelegate(); private delegate int AddDelegate(int a, int b); private readonly AddDelegate _addDelegate = Add; public TestClass() { // [1] Console.WriteLine($\"{_NUM}, {_STR}\"); // [2] MyDelegate myDelegate = FuncTest; myDelegate(); // [3] var res = MyFunc(_addDelegate); Console.WriteLine(res); } private static int MyFunc(AddDelegate add) { return add(1, 2); } } } Delegate 종류 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 using System; using System.Collections.Generic; using System.Linq; namespace ConsoleApp2 { internal class Program { private static void Main() { List&lt;int&gt; list = new() { 2, 3, 4, 5, 6, 7 }; list.Where(x =&gt; x &gt; 5).ToList().ForEach(Console.WriteLine); // Action delegate Action&lt;int&gt; myAction1 = Test1; myAction1(10); Action&lt;int&gt; myAction2 = x =&gt; Console.WriteLine(x); myAction2(20); // Func(tion) delegate Func&lt;int, int&gt; myFunc1 = Test2; Console.WriteLine(myFunc1(30)); Func&lt;int, int&gt; myFunc2 = x =&gt; x * 2; Console.WriteLine(myFunc2(30)); Func&lt;int, bool&gt; myFunc3 = Test3; Console.WriteLine(myFunc3(7)); Func&lt;int, bool&gt; myFunc4 = x =&gt; x == 7; Console.WriteLine(myFunc4(7)); Func&lt;int, int, int&gt; myFunc5 = (a, b) =&gt; a + b; Console.WriteLine(myFunc5(1, 2)); // Predicate delegate Predicate&lt;int&gt; myPredicate1 = x =&gt; x == 7; Console.WriteLine(myPredicate1(7)); Predicate&lt;string&gt; myPredicate2 = s =&gt; s.StartsWith(\"A\"); Console.WriteLine(myPredicate2(\"ABC\")); // delegate 사용 int[] arr = { -10, 20, -30, 4, -5 }; int[] pos = Array.FindAll(arr, IsPositive); foreach (int item in pos) { Console.WriteLine(item); } arr.Where(n =&gt; n &gt;= 0).ToList().ForEach(Console.WriteLine); } private static void Test1(int number) { Console.WriteLine(number); } private static int Test2(int number) { return number; } private static bool Test3(int number) { return number != 7; } private static bool IsPositive(int i) { return i &gt;= 0; } } } Events 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 using System; namespace ConsoleApp3 { internal class Program { private static void Main() { var tower = new ClockTower(); _ = new Person(\"Jone\", tower); tower.ChimeFivePm(); tower.ChimeSixAm(); } } public class Person { public Person(string name, ClockTower tower) { tower.Chime += (_, e) =&gt; { Console.WriteLine(\"{0} heard the clock chime.\", name); switch (e.Time) { case 6: Console.WriteLine(\"{0} is waking up.\", name); break; case 17: Console.WriteLine(\"{0} is going home.\", name); break; } }; } } public class ClockTowerEventArgs : EventArgs { public int Time { get; set; } } public delegate void ChimeEventHandler(object sender, ClockTowerEventArgs args); public class ClockTower { public event ChimeEventHandler Chime; public void ChimeFivePm() =&gt; Chime?.Invoke(this, new ClockTowerEventArgs { Time = 17 }); public void ChimeSixAm() =&gt; Chime?.Invoke(this, new ClockTowerEventArgs { Time = 6 }); } } Events 전체사용예제 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 using System; namespace ConsoleExam { public class Car { public delegate void CarEndRunEventHandler(int result); public event CarEndRunEventHandler EndRunEvent; public void Run(int time) { for (int i = 0; i &lt; time; i++) { Console.WriteLine(\"Running...\" + i); } EndRunEvent?.Invoke(time); } } internal class Program { private static void Main() { Car car = new(); car.EndRunEvent += CarEndRunEvent; car.Run(10); static void CarEndRunEvent(int result) { Console.WriteLine(\"Result : \" + result); } } } } “C#에서 사용자 정의 Event 만들기” 참조하기 Reference en.wikipedia.org, “Delegate (CLI)” Clint Eastwood, “Advanced C#: 08 Events” 두들낙서, “C/C++ 강좌, 94강. 함수 포인터” 마수리, “C#, 델리게이트의 모든 것(All about delegate)” High-Tech, “Lambda Expressions made easy”" } , { "title": "WPF Dependency Injection", "url": "/20210906-1/", "date": "2021-09-06", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "csharp, ui", "content": "기본적으로 ASP.NET Core는 클래스와 해당 종속성 간의 IoC(Inversion of Control)를 실현하는 기술인 DI(종속성 주입) 소프트웨어 디자인 패턴을 지원1한다. Desktop 애플리케이션 개발에 자주 사용하는 WPF는 개발자가 직접 작성하여 추가하거나 또는 MVVM 패턴을 지원하는 Caliburn.Micro2와 같은 프레임워크를 사용하여 DI(종속성 주입) 환경을 구축한다. WPF를 이용한 개발에 DI를 적용하는 기본적인 환경 구성을 Microsoft.Extensions.Hosting 패키지를 이용(IHost Interface3)하여 구축할 수 있다. App.xaml 1 2 3 4 5 6 7 &lt;Application x:Class=\"ExamHelloDI.App\" xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"&gt; &lt;!-- StartupUri=\"MainWindow.xaml\" : 기본값 제거--&gt; &lt;Application.Resources&gt; &lt;/Application.Resources&gt; &lt;/Application&gt; App.xaml.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 using ExamHelloDI.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System.Windows; namespace ExamHelloDI { public partial class App { private readonly IHost _host; public App() { _host = Host.CreateDefaultBuilder().ConfigureServices((context, services) =&gt; { context.HostingEnvironment.ApplicationName = \"ExamHelloDI\"; ConfigureServices(services); }).Build(); } private static void ConfigureServices(IServiceCollection services) { services.AddSingleton&lt;MainWindow&gt;(); services.AddTransient&lt;IDateTimeServices, DateTimeServices&gt;(); } protected override async void OnStartup(StartupEventArgs e) { await _host.StartAsync(); _host.Services.GetRequiredService&lt;MainWindow&gt;().Show(); base.OnStartup(e); } protected override async void OnExit(ExitEventArgs e) { using (_host) { await _host.StopAsync(); } base.OnExit(e); } } } MainWindow.xaml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 &lt;Window x:Class=\"ExamHelloDI.MainWindow\" xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\" mc:Ignorable=\"d\" xmlns:local=\"clr-namespace:ExamHelloDI\" d:DataContext=\"{d:DesignInstance local:MainWindow, IsDesignTimeCreatable=True}\" Title=\"MainWindow\" Height=\"300\" Width=\"500\"&gt; &lt;Grid&gt; &lt;TextBox Grid.Row=\"0\" Text=\"{Binding DateTime, Mode=OneWay}\" /&gt; &lt;/Grid&gt; &lt;/Window&gt; MainWindow.xaml.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 using ExamHelloDI.Services; namespace ExamHelloDI { public partial class MainWindow { private readonly IDateTimeServices _dateTimeServices; public MainWindow(IDateTimeServices dateTimeServices) { InitializeComponent(); _dateTimeServices = dateTimeServices; DataContext = this; } public string DateTime =&gt; _dateTimeServices.GetDateTimeString(); // set { } Mode=OneWay } } IDateTimeServices.cs 1 2 3 4 5 6 7 namespace ExamHelloDI.Services { public interface IDateTimeServices { string GetDateTimeString(); } } DateTimeServices.cs 1 2 3 4 5 6 7 8 9 10 11 12 using System; namespace ExamHelloDI.Services { public class DateTimeServices : IDateTimeServices { public string GetDateTimeString() { return DateTime.Now.ToString(\"yyyy-MM-dd hh:mm:ss\"); } } } 예제에 사용한 소스 코드는 Codewrinkles, “Adding Dependency Injection to WPF applications” 강좌를 참고하여 작성하였다. Reference docs.microsoft.com, “ASP.NET Core에서 종속성 주입” github.com, “Caliburn.Micro” docs.microsoft.com, “IHost Interface”" } , { "title": "Winform Singleton pattern example", "url": "/20210811-1/", "date": "2021-08-11", "categories": "【 csharpㆍdotnetㆍavalonia 】, dotnet", "tags": "csharp, dotnet", "content": "클래스의 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다. 이와 같은 디자인 유형을 싱글턴 패턴(Singleton pattern)이라고 한다.1 정적 클래스(Static Class)와 비교해 싱글턴은 인터페이스 구현, 비동기 지원2 등 OOP에 더 어울리는 장점3을 가지고 있다. Winform 응용프로그램에서 단일 인스턴스를 유지하고 이에 따라 하위 Winform이 프로그램 종료 시점까지 그 상태를 유지하기 원한다면 Winform을 싱글턴 패턴으로 만들어 사용하면 된다. 즉 하위 폼을 호출할 때 새로운 인스턴스(new)가 생성되는 것을 방지하고 처음에 생성한 폼을 지속해서 유지할 때 유용하다. 더욱 자세한 소스는 여기(github)에서 볼 수 있다. Program.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 using System; using System.Windows.Forms; namespace WinFormsApp { internal static class Program { [STAThread] private static void Main() { // https://docs.microsoft.com/dotnet/api/system.windows.forms.highdpimode // DpiUnaware, DpiUnawareGdiScaled, PerMonitor, PerMonitorV2, SystemAware Application.SetHighDpiMode(HighDpiMode.PerMonitorV2); Application.EnableVisualStyles(); // Theme Enable Application.SetCompatibleTextRenderingDefault(false); Application.Run(FormMain.Go); // Singleton Form } } } FormMain.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 // 메인폼, 메인패널(하위폼컨테이터), 버튼A(하위폼A호출), 버튼B(하위폼B호출) using System; using System.Drawing; using System.Windows.Forms; namespace WinFormsApp { public partial class FormMain : Form { private static readonly Lazy&lt;FormMain&gt; instance = new(() =&gt; new FormMain()); public static FormMain Go =&gt; instance.Value; private FormMain() { InitializeComponent(); SetScreenSize(ScreenSize.Normal); } public enum ScreenSize { Full, Max, Normal, MaxNoneBS } public Panel PanelFormMain { get =&gt; PanelMain; set =&gt; PanelMain = value; } public string LogMessage { get =&gt; TextBoxMain.Text; set =&gt; TextBoxMain.Text = $\"[{DateTime.Now:yyyy-MM-dd HH.mm.ss}] {value}\"; } private bool IsSystemShutdown = false; private void ButtonFormA_Click(object sender, System.EventArgs e) { FormA.Go.Show(); FormA.Go.BringToFront(); } private void ButtonFormB_Click(object sender, System.EventArgs e) { FormB.Go.Show(); FormB.Go.BringToFront(); } private void FormMain_FormClosed(object sender, FormClosedEventArgs e) { FormB.Go.Dispose(); FormA.Go.Dispose(); } // Desktop FullScreen or Maximized private void SetScreenSize(ScreenSize screenSize) { switch (screenSize) { case ScreenSize.Full: WindowState = FormWindowState.Normal; FormBorderStyle = FormBorderStyle.None; Bounds = Screen.PrimaryScreen.Bounds; //this.Bounds = Screen.GetBounds(this); break; case ScreenSize.Max: WindowState = FormWindowState.Maximized; FormBorderStyle = FormBorderStyle.Sizable; break; case ScreenSize.Normal: WindowState = FormWindowState.Normal; FormBorderStyle = FormBorderStyle.Sizable; break; case ScreenSize.MaxNoneBS: var rectangle = Screen.FromControl(this).Bounds; FormBorderStyle = FormBorderStyle.None; Size = new Size(rectangle.Width, rectangle.Height); Location = new Point(0, 0); Rectangle workingRectangle = Screen.PrimaryScreen.WorkingArea; Size = new Size(workingRectangle.Width, workingRectangle.Height); break; } } } } FormA.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 using System; using System.Windows.Forms; namespace WinFormsApp { public sealed partial class FormA : Form { private static readonly Lazy&lt;FormA&gt; instance = new(() =&gt; new FormA()); public static FormA Go =&gt; instance.Value; private FormA() { InitializeComponent(); TopLevel = false; FormBorderStyle = FormBorderStyle.None; Dock = DockStyle.Fill; FormMain.Go.PanelFormMain.Controls.Add(this); FormMain.Go.PanelFormMain.Tag = this; } private void ButtonA_Click(object sender, EventArgs e) { // (Application.OpenForms[nameof(FormMain)].Controls[\"TextBoxMain\"] as TextBox).Text = \"Form A LOG\"; FormMain.Go.LogMessage = \"Form A LOG\"; // Property Access Hide(); } } } FormB.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 using System; using System.Windows.Forms; namespace WinFormsApp { public sealed partial class FormB : Form { private static readonly object locker = new(); private static FormB instance; public static FormB Go { get { if (instance == null || instance.IsDisposed) { lock (locker) { if (instance == null || instance.IsDisposed) { instance = new FormB(); } } } return instance; } } private FormB() { InitializeComponent(); TopLevel = false; FormBorderStyle = FormBorderStyle.None; Dock = DockStyle.Fill; FormMain.Go.PanelFormMain.Controls.Add(this); FormMain.Go.PanelFormMain.Tag = this; } private void ButtonB_Click(object sender, EventArgs e) { FormMain.Go.LogMessage = \"Form B LOG\"; // Property Access Close(); } } } Reference 위키백과, “싱글턴 패턴” postpiglet, “싱글톤(Singletone) vs C#정적클래스(static class) 차이점 무엇?” os94, “Singleton vs Static Class 차이점”" } , { "title": "CQRS using C# and MediatR", "url": "/20210415-1/", "date": "2021-04-15", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "csharp", "content": "CQRS (Command and Query Responsibility Segregation, 명령과 쿼리의 역할 분리) 패턴은 데이터 저장소에 대한 읽기 및 업데이트 작업을 분리하여 구현하는 것으로 이렇게 하면 성능, 확장성 및 보안을 최대화할 수 있는 장점이 있다.1 CQRS는 정보를 업데이트할 때와 조회할 때 다른 모델을 사용하는 것이 핵심이다. 다만, 일부 경우에는 이점이 있지만, 대부분의 경우에는 CQRS를 적용하면 복잡성이 높아지는 위험성이 있다. CQRS는 시스템의 Bounded Context2에서만 사용돼야 하고, 시스템 전체에서 사용해서는 안 된다. 이러한 사고방식은 각 Bounded Context는 개별적으로 모델링을 해야 한다는 의미다.3 아래의 예제는 닷넷 API 프로젝트에 MediatR 패키지를 사용하여 CQRS를 구현한 간단한 예제이다. ‘Jonathan Williams’의 강좌4 를 참고하였으며 자세한 전체 예제는 GitHub(CQRSInDotnetCore)5에서 볼 수 있다. Domain, Todo.cs 1 2 3 4 5 6 7 8 9 namespace CQRSExam.Domain { public class Todo { public int Id { get; init; } public string Name { get; init; } public bool Completed { get; init; } } } Database, Repository.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 using CQRSExam.Domain; using System.Collections.Generic; namespace CQRSExam.Database { public class Repository { public List&lt;Todo&gt; Todos { get; } = new() { new Todo{Id = 1, Name = \"Todo List 1\", Completed = false }, new Todo{Id = 2, Name = \"Todo List 2\", Completed = true }, new Todo{Id = 3, Name = \"Todo List 3\", Completed = false }, new Todo{Id = 4, Name = \"Todo List 4\", Completed = true }, new Todo{Id = 5, Name = \"Todo List 5\", Completed = false }, }; } } Queries, GetTodoByID.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 using CQRSExam.Database; using MediatR; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace CQRSExam.Queries { public static class GetTodoByID { // Query, Command : Execute public record Query(int Id) : IRequest&lt;Response&gt;; // Handler : Logic public class Handler : IRequestHandler&lt;Query, Response&gt; { private readonly Repository _repository; public Handler(Repository repository) { _repository = repository; } public async Task&lt;Response&gt; Handle(Query request, CancellationToken cancellationToken) { var todo = _repository.Todos.FirstOrDefault(x =&gt; x.Id == request.Id); return await Task.FromResult(todo == null ? null : new Response(todo.Id, todo.Name, todo.Completed)); } } // Response : Return public record Response(int Id, string Name, bool Completed); } } Commands, AddTodo.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 using System.Threading; using System.Threading.Tasks; using CQRSExam.Database; using CQRSExam.Domain; using MediatR; namespace CQRSExam.Commands { public static class AddTodo { // Command public record Command(string Name) : IRequest&lt;int&gt;; // Handler public class Handler : IRequestHandler&lt;Command, int&gt; { private readonly Repository _repository; public Handler(Repository repository) { _repository = repository; } public async Task&lt;int&gt; Handle(Command request, CancellationToken cancellationToken) { _repository.Todos.Add(new Todo {Id = 10, Name = request.Name}); return await Task.FromResult(10); } } } } TodoController.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 using CQRSExam.Commands; using CQRSExam.Queries; using MediatR; using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; namespace CQRSExam.Controllers { [ApiController] public class TodoController : ControllerBase { private readonly IMediator _mediator; public TodoController(IMediator mediator) { _mediator = mediator; } [HttpGet(\"/{id:int}\")] public async Task&lt;IActionResult&gt; GetTodoById(int id) { var response = await _mediator.Send(new GetTodoByID.Query(id)); return response == null ? NotFound() : Ok(response); } [HttpPost(\"\")] public async Task&lt;IActionResult&gt; AddTodo(AddTodo.Command command) =&gt; Ok(await _mediator.Send(command)); } } Startup.cs 1 2 3 4 5 6 public static void ConfigureServices(IServiceCollection services) { // 추가 services.AddSingleton&lt;Repository&gt;(); services.AddMediatR(typeof(Startup).Assembly); } 추천강좌 Kilt and Code, “Using MediatR Request Handlers in ASP.NET Core to Decouple Code” Intro to MediatR - Implementing CQRS and Mediator Patterns asp.net core - MediatR (CQRS) Tutorial &amp; Tips Reference docs.microsoft.com, “Command and Query Responsibility Segregation (CQRS) pattern” Martin Fowler, “BoundedContext” Jooho Son, “(번역)마틴 파울러 CQRS 포스팅” Jonathan Williams, “CQRS using C# and MediatR” jonathanjameswilliams26, “CQRSInDotnetCore”" } , { "title": "Template MetaProgramming(TMP)", "url": "/20210326-1/", "date": "2021-03-26", "categories": "【 cppㆍqtㆍc3 】, cpp", "tags": "cpp, csharp", "content": "템플릿 메타프로그래밍(template metaprogramming)은 템플릿을 사용하는 프로그래밍 기법으로, 컴파일러에게 프로그램 코드를 생성하도록 하는 방식이다. 이러한 기법은 컴파일 시점에 많은 것을 결정하도록 하여, 실행 시점의 계산을 줄여준다. 이 기법은 C++ 프로그래밍 언어에서 주로 사용된다1. C#에서 비슷한 기능으로는 제네릭 특수화 (Generic Specialization), 소스 생성기 (Source Generators)를 이용 TMP처럼 “컴파일러가 코드를 생성하게 만든다” TMP는 버그를 찾기 쉽지도 않고 구현도 어렵지만, 사용하는 이유는 많은 C++ 라이브러리들이 TMP 를 이용해서 구현되었고, TMP 를 통해서 컴파일 타임에 여러 오류들을 잡아낼 수도 있으며 속도가 매우 중요한 프로그램의 경우 TMP 를 통해서 런타임 속도도 향상2 시킬 수 있기 때문이다. 아래의 예제는 콜라츠 추측(Collatz)을 C#과 C++ 언어로 각각 구현한 예제3를 보여주고 TMP를 사용한 C++의 소스가 컴파일 타임에서 C#과 어떠한 차이가 있는지 보여준다. 1 2 3 4 5 6 7 int factorial(int n) { if (n == 0) return 1; return n * factorial(n - 1); } // factorial(4) == (4 * 3 * 2 * 1) == 24 // factorial(0) == 0! == 1 1 2 3 4 5 6 7 8 9 10 11 template &lt;int N&gt; struct Factorial { enum { value = N * Factorial&lt;N - 1&gt;::value }; }; template &lt;&gt; struct Factorial&lt;0&gt; { enum { value = 1 }; }; // Factorial&lt;4&gt;::value == 24 // Factorial&lt;0&gt;::value == 1 CS 예제 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 using System.Linq; using System.Collections.Generic; namespace CollatzCS { class Program { public static IEnumerable&lt;(uint count, uint value)&gt; Collatz(uint seed) { uint count = 1; yield return (count, seed); while (seed &gt; 1) { if (seed % 2 == 0) seed /= 2; else seed = 3 * seed + 1; yield return (++count, seed); } } static void Main(string[] args) { // Collatz : 런타임에 실행 var col = Collatz(100); foreach (var val in col) { System.Console.WriteLine($\"{val.count}: {val.value}\"); } System.Console.WriteLine(\"------------------------\"); System.Console.WriteLine($\"Collatz of 100 = {col.Count()}\"); } } } CPP 예제 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #include &lt;iostream&gt; using namespace std; template &lt;unsigned n&gt; struct Factorial { static const unsigned value = n * Factorial&lt;n - 1&gt;::value; }; template &lt;&gt; struct Factorial&lt;1&gt; { static const unsigned value = 1; }; template &lt;unsigned depth, unsigned seed, bool odd&gt; struct CollatzBase { }; template &lt;unsigned depth, unsigned seed&gt; struct CollatzBase&lt;depth, seed, true&gt; : public CollatzBase&lt;depth + 1, seed * 3 + 1, (seed * 3 + 1) % 2&gt; { }; template &lt;unsigned depth, unsigned seed&gt; struct CollatzBase&lt;depth, seed, false&gt; : public CollatzBase&lt;depth + 1, seed / 2, (seed / 2) % 2&gt; { }; template &lt;unsigned depth&gt; struct CollatzBase&lt;depth, 1, true&gt; // false &lt;-&gt; true { static const unsigned count = depth; }; template &lt;unsigned seed&gt; struct Collatz : public CollatzBase&lt;1, seed, seed % 2&gt; { }; int main(void) { // Factorial 예제 constexpr unsigned fact5 = Factorial&lt;5&gt;::value; cout &lt;&lt; \"Factorial 5 = \" &lt;&lt; fact5 &lt;&lt; endl; cout &lt;&lt; \"Factorial 10 = \" &lt;&lt; Factorial&lt;10&gt;::value &lt;&lt; endl; cout &lt;&lt; \"-------------------------------\" &lt;&lt; endl; // Collatz : 컴파일 타임에 실행 cout &lt;&lt; \"Collatz of 100 = \" &lt;&lt; Collatz&lt;100&gt;::count &lt;&lt; endl; } Reference 위키백과, “템플릿 메타프로그래밍” 모두의 코드, “TMP를 왜 쓰는가?” Coding Tutorials, “C++ Template Metaprogramming”" } , { "title": "Rust, 한글 2byte HEX En/Decoding", "url": "/20210107-1/", "date": "2021-01-07", "categories": "【 RustㆍZigㆍGo 】, Rust", "tags": "rust", "content": "Rust는 기본적으로 UTF-8 중심으로 문자열을 처리한다.1 그러나 Windows는 euc-kr을 확장한 CP949 (MS949)와 Unicode로 한글을 처리하므로2 Windows에서는 영문, 숫자와 별개로 한글을 byte 단위로 처리할 때 2byte 또는 3byte의 문자가 혼재할 수 있다. 아래의 예제는 2byte (AnsiString) 한글만으로 Hex를 encode, decode 필요가 있을 때 참고할 만한 소스이다. 1 2 3 [dependencies] encoding = \"0.2.33\" hex = \"0.4.2\" main.rs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 use encoding::{label::encoding_from_whatwg_label, EncoderTrap, DecoderTrap}; use hex; fn main() { let kor_eng_num = String::from(\"가나다 ABC 123\"); println!(\"{}\", kor_eng_num); println!(\"---------------------\"); let mut vec = Vec::new(); let string_list = kor_eng_num.split_whitespace(); for s in string_list { println!(\"UTF8: {} / MS949: {}\", hex::encode(s).to_uppercase(), split_hex(s.into())); vec.push(split_hex(s.into())); // s.into() -&gt; &amp;s } println!(\"---------------------\"); for hex_str in vec.iter() { println!(\"{} : {}\", hex_str, hex2str(hex_str)); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 fn hex2str(s: &amp;str) -&gt; &amp;str { let euckr = encoding_from_whatwg_label(\"euc-kr\").unwrap(); let decode_string = euckr.decode(&amp;hex::decode(s).unwrap(), DecoderTrap::Replace).unwrap(); return string_to_static_str(decode_string); } fn split_hex(s: &amp;str) -&gt; &amp;str { //&amp;str -&gt; String let euckr = encoding_from_whatwg_label(\"euc-kr\").unwrap(); //println!(\"{} / {:?}\", euckr.name(), euckr.whatwg_name()); let encode_string = euckr.encode(&amp;s, EncoderTrap::Replace).unwrap(); let hex_encode_string = hex::encode(encode_string).to_uppercase(); return string_to_static_str(hex_encode_string); } // 메모리 누수가 발생하는 함수 fn string_to_static_str(s: String) -&gt; &amp;'static str { Box::leak(s.into_boxed_str()) } /* 출력결과 가나다 ABC 123 --------------------- UTF8: EAB080EB8298EB8BA4 / MS949: B0A1B3AAB4D9 UTF8: 414243 / MS949: 414243 UTF8: 313233 / MS949: 313233 --------------------- B0A1B3AAB4D9 : 가나다 414243 : ABC 313233 : 123 */ 메모리 누수 함수 수정 대안 1.소유권을 그대로 넘기기 1 2 3 fn string_to_owned(s: String) -&gt; String { s // 그대로 반환 (소유권 이동) } 2.라이프타임(Lifetime 'a) 사용하기 1 2 3 4 5 6 7 8 struct MyStruct&lt;'a&gt; { name: &amp;'a str, } fn process_str(s: &amp;str) { let item = MyStruct { name: s }; println!(\"{}\", item.name); } 3.공유 소유권(Arc 또는 Rc) 사용하기 1 2 3 4 5 6 7 8 9 10 use std::sync::Arc; fn string_to_shared(s: String) -&gt; Arc&lt;str&gt; { Arc::from(s.into_boxed_str()) } // 사용 let shared = string_to_shared(String::from(\"hello\")); // 참조 카운트 증가, 마지막 참조가 사라지면 자동 해제 let reference = Arc::clone(&amp;shared); 4.런타임 상수가 필요하다면 (once_cell 또는 lazy_static) 1 2 3 4 5 use once_cell::sync::Lazy; static GLOBAL_STR: Lazy&lt;String&gt; = Lazy::new(|| { String::from(\"런타임에 생성된 전역 문자열\") }); encoding.spec.whatwg.org github.com/lifthrasiir/rust-encoding/ Reference rust-lang.org, “Strings and UTF-8” 나무위키, “CP949”" } , { "title": "Rust, Concurrency and Channels", "url": "/20201202-1/", "date": "2020-12-02", "categories": "【 RustㆍZigㆍGo 】, Rust", "tags": "rust", "content": "Rust 프로그래밍에서 concurrency 모델의 핵심 메커니즘을 비교적 간단한 소스 코드를 통하여 단계별로 정리해보았다.1 Go(lang)에서 goroutine, channel을 사용하여 함수와 메소드의 동시성을 구현할 수 있게 해주는 것2처럼 Rust에서는 ‘Concurrency, Threads, Channels, Mutex and Arc’로 동시성을 구현할 수 있다. 기본 Thread 구현 및 한계 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 use std::sync::{mpsc, Arc, Mutex}; use std::thread; use std::time::Duration; fn main() { let v = vec![1, 2, 3]; let handle = thread::spawn(move || { println!(\"vector: {:?}\", v); }); // - error : value borrowed here after move // println!(\"{:?}\", v); // - channel을 이용하여 해결 handle.join().unwrap(); } 1 2 3 4 5 6 7 8 9 fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { tx.send(42).unwrap(); }); println!(\"get {}\", rx.recv().unwrap()); } Channels, Mutex 확장 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const NUM_TIMTERS: usize = 10; fn timer(d: usize, tx: mpsc::Sender&lt;usize&gt;) { thread::spawn(move || { println!(\"{}: setting timer...\", d); thread::sleep(Duration::from_secs(d as u64)); println!(\"{}: sent!\", d); tx.send(d).unwrap(); }); } fn main() { let (tx, rx) = mpsc::channel(); for i in 0..NUM_TIMTERS { timer(i, tx.clone()); } for v in rx.iter().take(NUM_TIMTERS) { println!(\"{}: received!\", v); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 fn main() { let c = Arc::new(Mutex::new(0)); let mut hs = vec![]; for _ in 0..10 { let c = Arc::clone(&amp;c); let h = thread::spawn(move || { let mut num = c.lock().unwrap(); *num += 1; println!(\"{}\", num); }); hs.push(h); } for h in hs { h.join().unwrap(); } println!(\"Result: {}\", *c.lock().unwrap()); } 동시성 사용 예제 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 fn is_prime(n: usize) -&gt; bool { return (2..n).all(|i| n % i != 0); } fn producer(tx: mpsc::SyncSender&lt;usize&gt;) -&gt; thread::JoinHandle&lt;()&gt; { return thread::spawn(move || { for i in 100_000_000.. { tx.send(i).unwrap(); } }); } fn worker(id: u64, shared_rx: Arc&lt;Mutex&lt;mpsc::Receiver&lt;usize&gt;&gt;&gt;) { thread::spawn(move || loop { { let mut n = 0; match shared_rx.lock() { Ok(rx) =&gt; match rx.try_recv() { Ok(_n) =&gt; { n = _n; } Err(_) =&gt; (), }, Err(_) =&gt; (), } if n != 0 { if is_prime(n) { println!(\"workder {} found a prime: {}\", id, n); } } } }); } fn main() { let (tx, rx) = mpsc::sync_channel(1024); let shared_rx = Arc::new(Mutex::new(rx)); for i in 1..13 { worker(i, shared_rx.clone()); } producer(tx).join().unwrap(); } Arc&lt;T&gt;(Atomic Reference Counting)는 Mutxt에서 동시적 상황을 안전하게 사용하게 해주는 Rc&lt;T&gt; 타입을 말한다3. 용어정리 동시성(Concurrency) : 어느 한 순간에 하나 이상의 것을 하는 것 멀티스레딩(Multithreading) : 여러 스레드를 사용하는 동시성의 한 형태 병렬 처리(Parallel Processing) : 동시에 작동하는 여러 스레드 사이에서 많은 작업들을 분할하여 처리하는 것 비동기 프로그래밍(Asynchronous Programming) : 불필요한 스레드 사용을 피하기 위해 future 혹은 콜백을 사용하는 동시성의 한 형태 반응성 프로그래밍(Reactive Programming) : 애플리케이션이 이벤트에 반응하는 선언적 스타일의 프로그래밍4 Go(lang)에서 Concurrency 구현 예제는 Go, REST API with Mux를 참고하자5. Reference Tensor Programming, “Rust-lang (Concurrency, Threads, Channels, Mutex and Arc)” 후니의 컴퓨터, “goroutine and channel” The Rust Programming Language, “공유 상태 동시성” 비블레리, “Concurrency in C# Cookbook” DebugJO, “Go, REST API with Mux”" } , { "title": "Go, REST API with Mux", "url": "/20201001-1/", "date": "2020-10-01", "categories": "【 RustㆍZigㆍGo 】, Go", "tags": "go, api", "content": "Go 언어의 REST API 심플 예제이다.1. 동영상 강좌로 TutorialEdge, Golang Development를 추천한다. go 설치 후 환경변수 설정 GOPATH는 Go 언어에서 프로젝트를 생성하고 개발할 때 기본 작업 디렉터리로 사용한다. 아래처럼 ‘GoProjects’ 폴더를 만들었다면 이 안에 ‘bin’, ‘pkg’, ‘src’ 폴더를 만들고 다시 src 폴더에 프로젝트 폴더(예, hello)를 만들고 이 안에 ‘main.go’ 파일을 만든다. 이제 ‘go run’을 실행할 경우 화면에 결과를 보여주며(temp폴더에 임시빌드), ‘go build’ 하면 프로젝트 폴더명(hello)으로 실행 파일(hello.exe)을 만들어 준다. 1 2 3 4 5 6 7 8 9 rem C:\\GoProjects\\bin rem C:\\GoProjects\\pkg rem C:\\GoProjects\\src\\hello\\main.go set Path=%PATH%;C:\\Go\\bin set GOCACHE=C:\\GoProjects\\go-build set GOENV=C:GoProjects\\env set GOPATH=C:\\GoProjects set GOTMPDIR=C:\\GoProjects\\temp Temp 디렉터리는 ‘go run’을 실행할 때 백신에 따라 ‘access denied’가 메시지가 나오는 데 백신에 예외처리를 해주어 해결한다. main.go example 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package main import ( \"encoding/json\" \"log\" \"math/rand\" \"net/http\" \"strconv\" \"github.com/gorilla/mux\" ) // Book Model type Book struct { ID string `json:\"id\"` Isbn string `json:\"isbn\"` Title string `json:\"title\"` Author *[]Author `json:\"author\"` } // Author Model type Author struct { Name string `json:\"name\"` Address string `json:\"address\"` } var books []Book func setBook(w http.ResponseWriter, r *http.Request) { w.Header().Set(\"Content-Type\", \"application/json\") var book Book _ = json.NewDecoder(r.Body).Decode(&amp;book) book.ID = strconv.Itoa(rand.Intn(100000000)) books = append(books, book) json.NewEncoder(w).Encode(book) } func main() { r := mux.NewRouter() r.HandleFunc(\"/books\", setBook).Methods(\"POST\") log.Fatal(http.ListenAndServe(\":8000\", r)) } go get -u github.com/gorilla/mux 명령어로 mux router를 설치한다. 아래는 Concurrency2. Concurrency 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package main import ( \"fmt\" \"log\" \"net/http\" \"os\" \"sync\" ) var wg sync.WaitGroup var mut sync.Mutex func sendRequest(url string) { defer wg.Done() res, err := http.Get(url) if err != nil { panic(err) } mut.Lock() defer mut.Unlock() fmt.Printf(\"[%d] %s\\n\", res.StatusCode, url) } func main() { if len(os.Args) &lt; 2 { log.Fatalln(\"usage: go run main.go &lt;url 1&gt;...&lt;url n&gt;\") } for _, url := range os.Args[1:] { go sendRequest(\"https://\" + url) wg.Add(1) } wg.Wait() } 참고자료(study) TutorialEdge, Golang Development Traversy Media, Go / Golang Crash Course freeCodeCamp.org, Learn Go Programming - Golang Tutorial for Beginners Reference Traversy Media, Golang REST API With Mux Rohit Awate, Concurrency in Golang" } , { "title": "Rust, Global static objects", "url": "/20200913-1/", "date": "2020-09-13", "categories": "【 RustㆍZigㆍGo 】, Rust", "tags": "rust", "content": "Rust는 ‘statically typed’ 언어로 런타임 비용 없이 컴파일 타임에 메모리 안전을 보장하여 런타임 버그를 방지한다. 그러나 때로는 동적 값을 사용해야 할 때가 있다.1 아래의 예제는 ‘전역 정적 객체’를 구현한 몇 가지 방법(트릭)을 보여준다.2 Declare lazily evaluated constant 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 use lazy_static::lazy_static; use std::collections::HashMap; lazy_static! { static ref PRIVILEGES: HashMap&lt;&amp;'static str, Vec&lt;&amp;'static str&gt;&gt; = { let mut map = HashMap::new(); map.insert(\"가나다\", vec![\"user\", \"admin\"]); map.insert(\"마바사\", vec![\"user\"]); map }; } fn show_access(name: &amp;str) { let access = PRIVILEGES.get(name); println!(\"{}: {:?}\", name, access); } fn main() { let access = PRIVILEGES.get(\"가나다\"); println!(\"가나다: {:?}\", access); show_access(\"마바사\"); } /* 실행결과 가나다: Some([\"user\", \"admin\"]) 마바사: Some([\"user\"]) */ Global static objects 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 /* [dependencies] lazy_static = \"1.4.0\" */ #[macro_use] extern crate lazy_static; use std::sync::Mutex; lazy_static! { static ref STATES: Mutex&lt;Vec&lt;i32&gt;&gt; = Mutex::new(vec![1, 2, 3]); } fn current_item() { println!(\"Current Value - {:?}\", STATES.lock().unwrap()); } fn total_count() -&gt; usize { STATES.lock().unwrap().iter().filter(|&amp;n| *n == *n).count() } fn print_item() { for (k, v) in STATES.lock().unwrap().iter().enumerate() { println!(\"Value : {} - {}\", k, v); } } fn add_item(item: i32) { STATES.lock().unwrap().push(item); } fn main() { println!(\"Start - STATES Count = {}\", total_count()); current_item(); for x in 100..102 { // 100, 101 or (in 100..=101) add_item(x) } print_item(); println!(\"End - STATES Count = {}\", total_count()); } /* 실행 결과 Start - STATES Count = 3 Current Value - [1, 2, 3] Value : 0 - 1 Value : 1 - 2 Value : 2 - 3 Value : 3 - 100 Value : 4 - 101 End - STATES Count = 5 */ 전역 정적 객체 구현의 주요 방식 lazy_static 또는 once_cell (외장 크레이트) std::sync::OnceLock (표준 라이브러리) const와 static의 조합 Mutex나 RwLock을 이용한 내부 가변성 참고로 ‘Rust’에 대한 추가(struct, webapi, json, …) 예제는 DebugJO/HelloWorldSample/Rust에서 볼 수 있다. Reference Dmitry Soshnikov, Rust notes: dynamic and global static objects rust-lang-nursery.github.io, global_static: Declare lazily evaluated constant" } , { "title": "C#에서 사용자 정의 Event 만들기", "url": "/20200906-1/", "date": "2020-09-06", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "csharp", "content": "C# .NET Framework에서 발생하는 시스템 이벤트와 별도로 사용자가 직접 이벤트를 정의하여 사용할 수 있다. 이벤트를 받을 때 파라미터로 데이터를 받으려면 EventArgs 클래스를 상속받아 여기에 항목을 추가하여 사용이 가능하다. WinForm이나 WPF에서 제공하는 이벤트와 별도로 개발자가 직접 이벤트 로직을 만들 수 있어야 한다. 전체 소스는 UserEventExam 에서 볼 수 있다. UserEvents.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 using System; namespace UserEventExam { public class UserEvents { public static event EventHandler&lt;UserArgs&gt; OnUserEvent; public static void ProcessEvent(UserArgs args) { OnUserEvent?.Invoke(OnUserEvent.Target, args); } } } UserArgs.cs 1 2 3 4 5 6 7 8 9 10 using System; namespace UserEventExam { public class UserArgs : EventArgs { public string Name { get; set; } public int Age { get; set; } } } Database.cs 1 2 3 4 5 6 7 8 9 10 11 12 using System; namespace UserEventExam { public class Database { public void SendData(object sender, UserArgs e) { Console.WriteLine($\"Data : User({e.Name}), Age({e.Age}), object({sender})\"); } } } Program.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 namespace UserEventExam { internal class Program { private static void Main() { var db = new Database(); UserEvents.OnUserEvent += db.SendData; const string name = \"홍길동\"; const int age = 30; UserEvents.ProcessEvent(new UserArgs() {Name = name, Age = age}); } } } 전체 사용 예제 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 using System; namespace ConsoleExam { public class Car { public delegate void CarEndRunEventHandler(int result); public event CarEndRunEventHandler EndRunEvent; public void Run(int time) { for (int i = 0; i &lt; time; i++) { Console.WriteLine(\"Running...\" + i); } EndRunEvent?.Invoke(time); } } internal class Program { private static void Main() { Car car = new(); car.EndRunEvent += CarEndRunEvent; car.Run(10); static void CarEndRunEvent(int result) { Console.WriteLine(\"Result : \" + result); } } } } 전체 사용 예제 2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 //#nullable enable void Main() { Car car = new(); car.EndRunEvent += CarEndRunEvent; car.Run(10); static void CarEndRunEvent(object? sender, int result) { Console.WriteLine(\"Result : \" + result); } } public class Car { public event EventHandler&lt;int&gt;? EndRunEvent; public void Run(int time) { for (int i = 0; i &lt; time; i++) { Console.WriteLine(\"Running...\" + i); } EndRunEvent?.Invoke(this, time); } } 전체 사용 예제 3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 //#nullable enable void Main() { Car car = new(); car.EndRunEvent += CarEndRunEvent; car.Run(10); static void CarEndRunEvent(object? sender, CarEventArgs args) { Console.WriteLine($\"Result : {sender?.GetType().Name} : {args.Time}\"); } } public class Car { public event EventHandler&lt;CarEventArgs&gt;? EndRunEvent; public void Run(int time) { for (int i = 0; i &lt; time; i++) { Console.WriteLine(\"Running...\" + i); } EndRunEvent?.Invoke(this, new CarEventArgs { Time = time }); } } public class CarEventArgs : EventArgs { public int Time { get; set; } } Reference CODELLIGENT, Events in C#.Net made easy! IAmTimCorey, C# Events - Creating and Consuming Events in Your Application" } , { "title": "Singleton with Magic Static in Qt/C++", "url": "/20200903-1/", "date": "2020-09-03", "categories": "【 cppㆍqtㆍc3 】, QT", "tags": "cpp, qt", "content": "컴파일러가 C++11을 완전히 지원하는 경우 싱글톤을 구현하는 가장 좋은 방법은 ‘Magic Static’을 사용하는 것이다1. C++11부터는 static 객체 생성이 스레드에 안전하게 보장하도록 표준에 추가되었다2. singleton.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #ifndef SINGLETON_H #define SINGLETON_H template &lt;typename T&gt; class Singleton final { public: static T &amp;GetInstance() { static T instance; return instance; } private: Singleton() = default; ~Singleton() = default; Singleton(const Singleton &amp;) = delete; Singleton &amp;operator=(const Singleton &amp;) = delete; Singleton(Singleton &amp;&amp;) = delete; Singleton &amp;operator=(Singleton &amp;&amp;) = delete; }; #endif // SINGLETON_H myclass.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #ifndef MYCLASS_H #define MYCLASS_H #include &lt;QObject&gt; #include &lt;QDebug&gt; class MyClass : public QObject { Q_OBJECT public: explicit MyClass(QObject *parent = nullptr); ~MyClass(); void display(const QString &amp;message); signals: }; #endif // MYCLASS_H myclass.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include \"myclass.h\" MyClass::MyClass(QObject *parent) : QObject(parent) { qDebug() &lt;&lt; this &lt;&lt; \"created\"; } MyClass::~MyClass() { qDebug() &lt;&lt; this &lt;&lt; \"destroyed\"; } void MyClass::display(const QString &amp;message) { qDebug() &lt;&lt; this &lt;&lt; message; } main.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include \"myclass.h\" #include \"singleton.h\" #include &lt;QCoreApplication&gt; void scopetest() { Singleton&lt;MyClass&gt;::GetInstance().setObjectName(\"Delphi\"); Singleton&lt;MyClass&gt;::GetInstance().display(\"from scope test\"); } void looptesst() { Singleton&lt;MyClass&gt;::GetInstance().display(\"starting loop\"); for (int i = 0; i &lt; 10; i++) { Singleton&lt;MyClass&gt;::GetInstance().display(QString::number(i)); } Singleton&lt;MyClass&gt;::GetInstance().display(\"finished loop\"); } int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); scopetest(); looptesst(); Singleton&lt;MyClass&gt;::GetInstance().display(\"hello from main\"); qDebug() &lt;&lt; Singleton&lt;MyClass&gt;::GetInstance().objectName(); a.exit(0); // return a.exec(); } 참고한 소스 : VoidRealms, Magic Statics - Singleton Replacement - Qt 5 Design Patterns Reference Marc Gregoire’s Blog, Implementing a Thread-Safe Singleton with C++11 Using Magic Static YoungJin Shin, C++ VS 2015의 magic statics 구현 세부 사항" } , { "title": "CRUD using BULK Operation in ASP.NET", "url": "/20200819-1/", "date": "2020-08-19", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "csharp, database, api", "content": "대용량의 데이터를 한 번에 입력, 수정, 삭제할 때 대부분의 개발자는 ‘for’와 같은 루프 문을 사용하여 이를 처리하는 데 처리할 ‘row’의 개수만큼 operation이 발생하므로 RDBMS가 SQL Server의 경우 Bulk Operation 기법을 사용하여 한 번에 처리하도록 해야 한다. 또는 Table 변수를 사용하여 한 번에 모든 데이틀 넘겨주고 데이터베이스에서는 이를 한 번의 로직으로 처리하도록 유도해야 한다. 소스 예제는 ‘Thumb IKR - Programming Examples’의 유튜브 강좌를 참고하였고 일부 소스는 수정하였다. VS2019에서 ASP.NET Core 웹 애플리케이션(MVC) 프로젝트를 생성하고 기본값을 그대로 사용하였으며 변경된 부분의 코드 위주로 아래에 소소를 나열하였다. 추가한 nuget package는 RepoDb.SqlServer와 RepoDb.SqlServer.BulkOperations이다(repodb.net). appsettings.json 1 2 3 4 5 { \"ConnectionStrings\": { \"testdb\": \"Server=아이피주소;Database=디비;User Id=아이디;Password=패스워드;\" } } Student.cs (모델) 1 2 3 4 5 6 7 8 9 namespace BulkCRUD.Models { public class Student { public int StudentID { get; set; } public string Name { get; set; } public string Roll { get; set; } } } Global.cs 1 2 3 4 5 6 7 8 namespace BulkCRUD.Common { public static class Global { public static string ConnectionString { get; set; } } } IStudentService 1 2 3 4 5 6 7 8 9 10 11 12 13 using BulkCRUD.Models; using System.Collections.Generic; namespace BulkCRUD.IService { public interface IStudentService { string BulkSave(List&lt;Student&gt; oStudents); string BulkUpdate(List&lt;Student&gt; oStudents); string BulkDelete(List&lt;Student&gt; oStudents); string BulkMerge(List&lt;Student&gt; oStudents); } } StudentService.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 using BulkCRUD.IService; using BulkCRUD.Models; using System.Collections.Generic; using BulkCRUD.Common; using Microsoft.Data.SqlClient; using RepoDb; namespace BulkCRUD.Service { public class StudentService : IStudentService { public string BulkSave(List&lt;Student&gt; oStudents) { using var connenction = new SqlConnection(Global.ConnectionString); var totalRows = connenction.BulkInsert(oStudents); return totalRows &gt; 0 ? \"Saved\" : \"Failed\"; } public string BulkUpdate(List&lt;Student&gt; oStudents) { using var connenction = new SqlConnection(Global.ConnectionString); var totalRows = connenction.BulkUpdate(oStudents); return totalRows &gt; 0 ? \"Updated\" : \"Failed\"; } public string BulkDelete(List&lt;Student&gt; oStudents) { using var connenction = new SqlConnection(Global.ConnectionString); var totalRows = connenction.BulkDelete(oStudents); return totalRows &gt; 0 ? \"Deleted\" : \"Failed\"; } public string BulkMerge(List&lt;Student&gt; oStudents) { using var connenction = new SqlConnection(Global.ConnectionString); var totalRows = connenction.BulkMerge(oStudents); return totalRows &gt; 0 ? \"Merged\" : \"Failed\"; } } } HomeController.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 using BulkCRUD.IService; using BulkCRUD.Models; using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; namespace BulkCRUD.Controllers { public class HomeController : Controller { private readonly IStudentService _studentService; public HomeController(IStudentService studentService) { _studentService = studentService; } public IActionResult Index() { var message = BulkSave(); //BulkUpdate(); //BulkMerge(); //BulkDelete(); ViewData[\"Message\"] = message; return View(); } private string BulkSave() { var oStudents = new List&lt;Student&gt;(); const int nTotalStudent = 2000; for (var i = 1; i &lt;= nTotalStudent; i++) { oStudents.Add(new Student() { StudentID = i, Name = \"학생-\" + i, Roll = \"Roll-\" + i }); } var message = _studentService.BulkSave(oStudents); return message; } //private string BulkUpdate() //{ // var oStudents = new List&lt;Student&gt; // { // new Student() {StudentID = 3, Name = \"홍길동\", Roll = \"11111\"}, // new Student() {StudentID = 4, Name = \"홍길서\", Roll = \"22222\"}, // new Student() {StudentID = 5, Name = \"홍길남\", Roll = \"33333\"}, // new Student() {StudentID = 6, Name = \"홍길북\", Roll = \"44444\"} // }; // var message = _studentService.BulkUpdate(oStudents); // return message; //} //private string BulkDelete() //{ // var oStudents = new List&lt;Student&gt; // { // new Student() {StudentID = 3}, // new Student() {StudentID = 4}, // new Student() {StudentID = 5}, // new Student() {StudentID = 2} // }; // var message = _studentService.BulkDelete(oStudents); // return message; //} //private string BulkMerge() //{ // var oStudents = new List&lt;Student&gt; // { // new Student() {StudentID = 3, Name = \"홍길동1\", Roll = \"A11111\"}, // new Student() {StudentID = 4, Name = \"홍길서2\", Roll = \"B22222\"}, // new Student() {StudentID = 5, Name = \"홍길남3\", Roll = \"C33333\"}, // new Student() {StudentID = 6, Name = \"홍길북4\", Roll = \"D44444\"} // }; // var message = _studentService.BulkMerge(oStudents); // return message; //} } } Startup.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 using BulkCRUD.Common; using BulkCRUD.IService; using BulkCRUD.Service; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace BulkCRUD { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); Global.ConnectionString = Configuration.GetConnectionString(\"testdb\"); services.AddScoped&lt;IStudentService, StudentService&gt;(); RepoDb.SqlServerBootstrap.Initialize(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints =&gt; { endpoints.MapControllerRoute( name: \"default\", pattern: \"{controller=Home}/{action=Index}/{id?}\"); }); } } } Reference Thumb IKR - Programming Examples, CRUD using BULK (Basic) Operation in ASP.NET Core Dapper - Insert and Update in Bulk" } , { "title": "Objects and behavior, Rust vs. C#", "url": "/20200708-1/", "date": "2020-07-08", "categories": "【 RustㆍZigㆍGo 】, Rust", "tags": "rust, csharp", "content": "프로그래밍 언어인 Rust와 C#을 OOP 관점에서 비교해 보았다. Rust는 OOP라기보다는 모듈/함수 지향형 프로그래밍 언어라고 보는 게 더 합당하다고 생각한다. What does “Rust &amp; OOP” mean to you? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 struct Door { is_open: bool, } trait Openable { fn new(is_open: bool) -&gt; Door; fn open(&amp;mut self); fn foo1(txt: &amp;str); fn foo2(&amp;mut self); } impl Openable for Door { fn new(is_open: bool) -&gt; Door { Door { is_open: is_open } } fn open(&amp;mut self) { self.is_open = true; } fn foo1(txt: &amp;str) { println!(\"{} {}\", \"Print foo ...\", txt); } fn foo2(&amp;mut self) { Self::foo1(\"2\"); } } fn main() { let mut door = Door::new(false); println!(\"{}\", if door.is_open { \"참\" } else { \"거짓\" }); door.open(); println!(\"{}\", if door.is_open { \"참\" } else { \"거짓\" }); Door::foo1(\"1\"); door.foo2(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 using System; namespace class_csharp { class Door { public bool is_open = false; public Door(bool is_open) { this.is_open = is_open; } public void Open() { this.is_open = true; } public static void Foo() { Console.WriteLine(\"Print Foo ...\"); } } class Program { static void Main(string[] args) { var door = new Door(false); Console.WriteLine(door.is_open? \"참\": \"거짓\"); door.Open(); Console.WriteLine(door.is_open? \"참\": \"거짓\"); Door.Foo(); // GC.Collect(); // GC.WaitForPendingFinalizers(); } } } 특징 Rust는 현대적인 시스템 프로그래밍 언어로, C/C++와 동등한 수준의 속도를 달성하면서 메모리 오류를 완전히 없애는 것을 목표로 한다. 또한 함수형 프로그래밍 언어로부터 발전된 타입 시스템을 도입하였으며, 클래스 대신 트레이트(Trait)를 기반으로 다형성을 달성한다. 매크로를 사용해 언어를 확장하는 것이 가능하며, 이 모든 것이 현대적인 모듈 시스템을 통해 쉽게 모듈화될 수 있다. 모듈들은 크레이트(Crate)라고 하는 단위로 묶여서 실행 파일이나 라이브러리로 배포될 수 있으며, Cargo라는 패키지 관리 프로그램을 통해 빌드 및 패키지 배포를 자동화하고 필요한 라이브러리를 Cargo를 통해 자동으로 다운로드받을 수 있다. - Rust, 나무위키 Reference C# to Rust Cheat Sheet, Carol" } , { "title": "Rust WebAPI using ACTIX", "url": "/20200621-1/", "date": "2020-06-21", "categories": "【 RustㆍZigㆍGo 】, Rust", "tags": "rust, api", "content": "Rust 언어에 기반한 web framework로는 Actix, Rocket, Gotham 등이 있다. 이 중에서 최근에 많이 사용하는 Actix를 이용하여 basic 인증을 포함한 간단한 WebAPI 예제를 구현해보았다. 추가로 데이터베이스와 연동은 하단의 ‘추천강좌’와 ‘Reference’를 참고하자. Cargo.toml 1 2 3 4 5 6 7 8 9 10 11 12 13 [package] name = \"hello_actix\" version = \"0.1.0\" authors = [\"DebugJO &lt;me@msjo.kr&gt;\"] edition = \"2018\" [dependencies] actix-rt = \"1.1.1\" actix-web = \"2.0.0\" actix-web-httpauth = \"0.4.1\" serde = {version = \"1.0.113\", features = [\"derive\"]} dotenv = \"0.15.0\" config = \"0.10.1\" 환경설정파일 .env root 폴더에 .env파일을 만들고 아래와 같이 설정파일을 작성한다. 1 2 SERVER.HOST=127.0.0.1 SERVER.PORT=80 config.rs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 /* src/config.rs */ use config::ConfigError; use serde::Deserialize; use std::result::Result; #[derive(Deserialize)] pub struct ServerConfig { pub host: String, pub port: i32, } #[derive(Deserialize)] pub struct Config { pub server: ServerConfig, } impl Config { pub fn from_env() -&gt; Result&lt;Self, ConfigError&gt; { let mut cfg = config::Config::new(); cfg.merge(config::Environment::new())?; cfg.try_into() } } models.rs 1 2 3 4 5 6 7 8 9 /* src/models.rs */ use serde::Serialize; #[derive(Serialize)] pub struct Student { pub id: String, pub name: String, pub email: String, } main.rs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 /* src/main.rs */ mod config; mod models; use crate::models::Student; use actix_web::{dev::ServiceRequest, web, App, Error, HttpServer, Responder}; use actix_web_httpauth::extractors::basic::{BasicAuth, Config}; use actix_web_httpauth::extractors::AuthenticationError; use actix_web_httpauth::middleware::HttpAuthentication; use dotenv::dotenv; use std::io; fn validate_credentials(user_id: &amp;str, user_password: &amp;str) -&gt; Result&lt;bool, std::io::Error&gt; { // Basic Auth (Username, Password) if user_id.eq(\"abc\") &amp;&amp; user_password.eq(\"123\") { return Ok(true); } return Err(std::io::Error::new(std::io::ErrorKind::Other, \"Authentication failed!\")); } async fn basic_auth_validator(req: ServiceRequest, credentials: BasicAuth) -&gt; Result&lt;ServiceRequest, Error&gt; { let config = req.app_data::&lt;Config&gt;().map(|data| data.get_ref().clone()).unwrap_or_else(Default::default); match validate_credentials(credentials.user_id(), credentials.password().unwrap().trim()) { Ok(res) =&gt; { if res == true { Ok(req) } else { Err(AuthenticationError::from(config).into()) } } Err(_) =&gt; Err(AuthenticationError::from(config).into()), } } async fn student() -&gt; impl Responder { web::HttpResponse::Ok().json(Student { id: \"1000\".to_string(), name: \"홍길동\".to_string(), email: \"1000@gmail.com\".to_string(), }) } #[actix_rt::main] async fn main() -&gt; io::Result&lt;()&gt; { dotenv().ok(); let config = crate::config::Config::from_env().unwrap(); println!(\"Staring server at http://{}:{}/\", config.server.host, config.server.port); HttpServer::new(|| { let auth = HttpAuthentication::basic(basic_auth_validator); App::new().wrap(auth).route(\"/\", web::get().to(student)) }) .bind(format!(\"{}:{}\", config.server.host, config.server.port))? .run() .await } 추천강좌 Live Coding with Rust and Actix, Tensor Programming Développement Web Rust &amp; Rocket, Thibaud Dauce A Basic Web Application with Rust and Actix-web, zupzup Reference Hello Rocket, Fredrik Christenson Web development with rust, Genus-v Programming Actix-Web Basic And Bearer Authentication Examples, Karl San Gabriel" } , { "title": "Blazor CRUD Material Design", "url": "/20200512-1/", "date": "2020-05-12", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "csharp, ui", "content": "Blazor 컴포넌트 중 인기 있는 MatBlazor를 설치하고 사용하는 법을 간단한 CRUD 예제를 통하여 살펴본다. 사용한 예제는 Thumb IKR - Programming Examples Youtube 강좌이다. Blazor 강좌뿐 아니라 .NET과 관련한 좋은 강좌가 많이 있으므로 전체를 살펴보는 것을 추천한다. MatBlazor는 Form Controls, Navigation, Layout, Button &amp; Indicators, Popups &amp; Modals, Data Table 등으로 전반적인 컨트롤을 제공한다. 참고로 델파이와 닷넷 컴포넌트로 유명한 DevExpress는 Blazor 컴포넌트를 무료로 제공하고 있다(Demo). 기본환경 설정 Nuget에서 MatBlazor 패키지 추가 _imports.razor 파일에 @using MatBlazor 선언 _Host.chtml 파일(상단css, 하단js)에 소스 추가 1 2 &lt;link href=\"_content/MatBlazor/dist/matBlazor.css\" rel=\"stylesheet\"/&gt; &lt;script src=\"_content/MatBlazor/dist/matBlazor.js\"&gt;&lt;/script&gt; Startup.cs 파일의 ConfigureServices 함수에 소스 추가 1 2 3 4 5 public void ConfigureServices(IServiceCollection services) { //... 아래에 추가 services.AddScoped&lt;HttpClient&gt;(); } Index.razor 파일 수정(작성) Index.razor 소스 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 @page \"/\" &lt;h1&gt;Basic CRUD MatBlazor Example&lt;/h1&gt; &lt;MatButton Outlined=\"true\" @onclick=\"@(e =&gt; OpenDialog(false))\"&gt;Add Employee&lt;/MatButton&gt; &lt;hr /&gt; &lt;MatTable Items=\"@employees\" class=\"mat-elevation-z5\" AllowSelection=\"true\" SelectionChanged=\"SelectionChangedEvent\" FilterByColumnName=\"Name\" ShowPaging=\"false\"&gt; &lt;MatTableHeader&gt; &lt;th&gt;ID&lt;/th&gt; &lt;th&gt;Name&lt;/th&gt; &lt;th&gt;Gender&lt;/th&gt; &lt;th&gt;Job&amp;nbsp;Exp.&lt;/th&gt; &lt;th&gt;Joining&amp;nbsp;Date&lt;/th&gt; &lt;th&gt;Action&lt;/th&gt; &lt;/MatTableHeader&gt; &lt;MatTableRow&gt; &lt;td&gt;@context.EmployeeID&lt;/td&gt; &lt;td&gt;@context.Name&lt;/td&gt; &lt;td&gt;@context.Gender&lt;/td&gt; &lt;td&gt;@context.YearOfExperience&lt;/td&gt; &lt;td&gt;@context.JoiningDate.ToString(\"yyyy-MM-dd\")&lt;/td&gt; &lt;td&gt; &lt;MatButton Raised=\"true\" @onclick=\"@(e =&gt; OpenDialog(true))\" Icon=\"edit\" title=\"Edit\"&gt;&lt;/MatButton&gt; &lt;MatButton Raised=\"true\" @onclick=\"DeleteEmployee\" Icon=\"restore_from_trash\" title=\"Delete\"&gt;&lt;/MatButton&gt; &lt;/td&gt; &lt;/MatTableRow&gt; &lt;/MatTable&gt; &lt;MatDialog @bind-IsOpen=\"@IsDialogIsOpen\"&gt; &lt;MatDialogTitle&gt;Add/Edit&lt;/MatDialogTitle&gt; &lt;MatDialogContent&gt; &lt;MatTextField Label=\"Name\" @bind-Value=\"@employee.Name\"&gt;&lt;/MatTextField&gt; &lt;MatRadioGroup @bind-Value=\"@employee.Gender\" TValue=\"string\"&gt; &lt;MatRadioButton Value=\"@(\"Male\")\" TValue=\"string\"&gt;Male&lt;/MatRadioButton&gt; &lt;MatRadioButton Value=\"@(\"Female\")\" TValue=\"string\"&gt;Female&lt;/MatRadioButton&gt; &lt;/MatRadioGroup&gt; &lt;MatDatePicker Label=\"Joining Date\" @bind-Value=\"@employee.JoiningDate\" lo&gt;&lt;/MatDatePicker&gt; &lt;MatNumericUpDownField Label=\"Job Exp.\" @bind-Value=\"@employee.YearOfExperience\" DecimalPlaces=0 Minimum=0 Maximum=100&gt;&lt;/MatNumericUpDownField&gt; &lt;/MatDialogContent&gt; &lt;MatDialogActions&gt; &lt;MatButton OnClick=\"OkClick\"&gt;Add&lt;/MatButton&gt; &lt;MatButton OnClick=\"CloseDialog\"&gt;Close&lt;/MatButton&gt; &lt;/MatDialogActions&gt; &lt;/MatDialog&gt; &lt;MatSnackbar @bind-IsOpen=\"@IsSnackBar\" Leading=\"true\"&gt; &lt;MatSnackbarContent&gt;Deleted&lt;/MatSnackbarContent&gt; &lt;MatSnackbarActions&gt; &lt;MatButton Raised=\"true\" @onclick=\"UndoDelete\"&gt;Undo&lt;/MatButton&gt; &lt;/MatSnackbarActions&gt; &lt;/MatSnackbar&gt; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 @code { Employee employee = new Employee(); Employee selectedEmp = null; Employee tempEmp = null; int Idx = -1; bool IsDelete = false; bool IsDialogIsOpen = false; bool IsEdit = false; bool IsSnackBar = false; List&lt;Employee&gt; employees = new List&lt;Employee&gt;() { new Employee(1, \"홍길동\", \"Male\", Convert.ToDateTime(\"2020-01-01\"), 4), new Employee(2, \"홍길서\", \"Female\", Convert.ToDateTime(\"2020-02-02\"), 2), new Employee(3, \"홍길남\", \"Male\", Convert.ToDateTime(\"2020-03-03\"), 3) }; public class Employee { public int EmployeeID { get; set; } public string Name { get; set; } public string Gender { get; set; } public DateTime JoiningDate { get; set; } public int YearOfExperience { get; set; } public Employee() { } public Employee(int employeeID, string name, string gender, DateTime joiningDate, int yearOfExperience) { EmployeeID = employeeID; Name = name; Gender = gender; JoiningDate = joiningDate; YearOfExperience = yearOfExperience; } } private void AddEmployee(Employee emp) { emp.EmployeeID = employees.Count + 1; employees.Add(emp); } private void EditEmployee(Employee emp) { if (emp != null &amp;&amp; emp.EmployeeID &gt; 0) { int index = employees.FindIndex(a =&gt; a.EmployeeID == emp.EmployeeID); employees.RemoveAll(x =&gt; x.EmployeeID == emp.EmployeeID); employees.Insert(index, emp); } } private void DeleteEmployee() { IsDelete = true; if (employee != null &amp;&amp; employee.EmployeeID &gt; 0) { Idx = employees.FindIndex(a =&gt; a.EmployeeID == employee.EmployeeID); tempEmp = new Employee(employee.EmployeeID, employee.Name, employee.Gender, employee.JoiningDate, employee.YearOfExperience); IsSnackBar = true; employees.Remove(employee); IsDelete = false; this.StateHasChanged(); } } private void UndoDelete() { if (tempEmp != null &amp;&amp; Idx &gt; 0) { employees.Insert(Idx, tempEmp); tempEmp = null; Idx = -1; } } private void OpenDialog(bool isEdit) { IsEdit = isEdit; if (!isEdit) employee = new Employee(); IsDialogIsOpen = true; } private void OkClick() { IsDialogIsOpen = false; if (!IsEdit) this.AddEmployee(employee); else this.EditEmployee(employee); } private void CloseDialog() { IsDialogIsOpen = false; if (selectedEmp != null) { employee = selectedEmp; this.EditEmployee(selectedEmp); } } private void SelectionChangedEvent(object emp) { var currentEmp = (Employee)emp; if (currentEmp != null) { employee = currentEmp; selectedEmp = new Employee(currentEmp.EmployeeID, currentEmp.Name, currentEmp.Gender, currentEmp.JoiningDate, currentEmp.YearOfExperience); } else { selectedEmp = new Employee(); } if (IsDelete) this.DeleteEmployee(); } } Reference Blazor (ASP.NET Core) Tutorials, Thumb IKR Microsoft Blazor Training, DevExpress Build your first Blazor app, Microsoft" } , { "title": "Qt/C++, Signals & Slots", "url": "/20200425-1/", "date": "2020-04-25", "categories": "【 cppㆍqtㆍc3 】, QT", "tags": "cpp, qt", "content": "QT의 핵심기능 중 하나인 Signal, Slot은 객체 간의 통신에 사용된다. 일반적인 개발언어의 객체 간 메시지 통신인 ‘메시지-메시지 핸들러’, ‘이벤트-이벤트 핸들러’와 유사한 개념이다. signal과 slot을 연결하기 위해 connect()를 사용한다. connect를 사용하는 방법은 함수 포인터를 사용하거나 람다에 연결한다. Singleton으로 확장한 예제는 여기(GitHub)에서 볼 수 있다. connect() 함수 형식 1 2 connect(sender, &amp;QObject::destroyed, this, &amp;MyObject::objectDestroyed); connect(sender, &amp;QObject::destroyed, this, [=](){this-&gt;m_objects.remove(sender);}); Console 예제 1 2 3 4 5 6 7 8 9 10 //main.cpp #include \"water_cooler.h\" #include &lt;QCoreApplication&gt; int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); WaterCooler Cooler; a.exit(0); } person.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #ifndef PERSON_H #define PERSON_H #include &lt;QObject&gt; #include &lt;QString&gt; #include &lt;QtDebug&gt; class Person : public QObject { Q_OBJECT public: explicit Person(QObject *parent = nullptr); QString Name; void Gossip(const QString &amp;words); signals: void Speak(const QString &amp;words); public slots: void Listen(const QString &amp;words); }; #endif // PERSON_H person.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include \"person.h\" Person::Person(QObject *parent) : QObject(parent){} void Person::Gossip(const QString &amp;words) { qDebug() &lt;&lt; Name &lt;&lt; \" says \" &lt;&lt; words; emit Speak(words); } void Person::Listen(const QString &amp;words) { qDebug() &lt;&lt; Name &lt;&lt; \" says someone told me... \" &lt;&lt; words; } water_cooler.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #ifndef WATER_COOLER_H #define WATER_COOLER_H #include &lt;QObject&gt; class WaterCooler : public QObject { Q_OBJECT public: explicit WaterCooler(QObject *parent = nullptr); ~WaterCooler(); }; #endif // WATER_COOLER_H water_cooler.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include \"water_cooler.h\" #include \"person.h\" WaterCooler::WaterCooler(QObject *parent) : QObject(parent) { qDebug() &lt;&lt; \"===== Init =====\"; Person Cathy; Person Bob; Person Sally; Cathy.Name = \"CCC\"; Bob.Name = \"BBB\"; Sally.Name = \"SSS\"; connect(&amp;Cathy, SIGNAL(Speak(QString)), &amp;Bob, SLOT(Listen(QString))); connect(&amp;Cathy, SIGNAL(Speak(QString)), &amp;Sally, SLOT(Listen(QString))); Cathy.Gossip(\"BALD\"); } WaterCooler::~WaterCooler() { qDebug() &lt;&lt; \"===== Exit =====\"; } /* 실행결과 ===== Init ===== \"CCC\" says \"BALD\" \"BBB\" says someone told me... \"BALD\" \"SSS\" says someone told me... \"BALD\" ===== Exit ===== */ Reference Viewer Feedback Signals and Slots in depth, VoidRealms signals &amp; slots, DuarteCorporation Tutoriales Qt Signal and slots, ProgrammingKnowledge" } , { "title": "SQL Server, XML and JSON", "url": "/20200418-1/", "date": "2020-04-18", "categories": "【 DatabaseㆍModeling 】, SQL", "tags": "sql, xml, json", "content": "JSON은 최신 웹 및 모바일 애플리케이션에서 데이터를 교환하는 데 사용되는 일반적인 텍스트 데이터 형식이다. 예전 JSON이 일반화되기 전에는 XML 형태로 문서를 주고받았고 지금도 XML 형식은 데이터의 검증이 필요한 곳에서 스키마를 사용하여 무결성을 검증할 뿐만 아니라 표준 ‘XML Web Services’의 기본 데이터 형식이기도 하다. 이번 글에서는 SQL Server(2019)에서 개발언어의 도움 없이 T-SQL 자체만으로 XML, JSON 형식을 다루는 몇 가지 예제를 소개한다. XML 일반적인 XML 리턴 1 2 3 4 5 6 7 8 9 10 11 12 13 select b.BoardNO, b.UserNO, b.Contents, b.HitCount, format(b.RegDate, 'yyyyMMdd') as RegDate, b.PublicIP, b.LocalIP, u.UserName, u.DeptNO, null as nullTest from TBoard b join TUser u on u.UserNO = b.UserNO for xml path('board'), root('boards'), elements xsinil xml explicit 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 select 1 as tag, null as parent, null as 'boards!1!', null as 'board!2!BoardNO', null as 'board!2!UserNO', null as 'board!2!Contents', null as 'board!2!HitCount', null as 'board!2!RegDate', null as 'board!2!PublicIP', null as 'board!2!LocalIP', null as 'board!2!UserName', null as 'board!2!DeptNO' union all select 2 as tag, 1 as parent, null, b.BoardNO, b.UserNO, b.Contents, b.HitCount, format(b.RegDate, 'yyyyMMdd') as RegDate, b.PublicIP, b.LocalIP, u.UserName, u.DeptNO from TBoard b join TUser u on u.UserNO = b.UserNO for xml explicit xml cdata 1 2 3 4 5 6 7 8 9 10 11 12 13 14 select 1 as tag, null as parent, b.BoardNO as 'board!1!BoardNO!element', b.UserNO as 'board!1!UserNO!element', b.Contents as 'board!1!Contents!cdata', -- cdata or xml b.HitCount as 'board!1!HitCount!element', format(b.RegDate, 'yyyyMMdd') as 'board!1!RegDate!element', b.PublicIP as 'board!1!PublicIP!element', b.LocalIP as 'board!1!LocalIP!element', u.UserName as 'board!1!UserName!element', u.DeptNO as 'board!1!DeptNO!element' from TBoard b join TUser u on u.UserNO = b.UserNO for xml explicit, root('boards') xml to table 1 2 3 4 5 6 7 8 9 10 11 12 13 declare @xml xml set @xml = '&lt;?xml version=\"1.0\" encoding=\"utf-8\"?&gt; &lt;boards&gt; &lt;board&gt; &lt;userid&gt;123&lt;/userid&gt; &lt;username&gt;abc&lt;/username&gt; &lt;/board&gt; &lt;/boards&gt;' select n.value('(./userid/text())[1]','int') as userid, n.value('(./username/text())[1]','varchar(50)') as username from @xml.nodes('/boards/board') as a(n) JSON 일반적인 JSON 리턴 1 2 3 4 5 6 7 8 9 10 11 12 select b.BoardNO, b.UserNO, b.Contents, b.HitCount, format(b.RegDate, 'yyyyMMdd') as RegDate, b.PublicIP, b.LocalIP, [User].UserName, [User].DeptNO from TBoard b join TUser [User] on [User].UserNO = b.UserNO for json auto, include_null_values json path 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 -- [example 1] declare @result nvarchar(max) set @result = ( select b.BoardNO, b.UserNO, b.Contents, b.HitCount, format(b.RegDate, 'yyyyMMdd') as RegDate, b.PublicIP, b.LocalIP, u.UserName as 'user.UserName', u.DeptNO as 'user.DepatNO' from TBoard b join TUser u on u.UserNO = b.UserNO for json path, root('boards'), include_null_values ) select @result as result -- [example 2] SELECT ent.Id AS 'Id', ent.Name AS 'Name', ent.Age AS 'Age', EMails = ( SELECT Emails.Id AS 'Id', Emails.Email AS 'Email' FROM EntitiesEmails Emails WHERE Emails.EntityId = ent.Id FOR JSON PATH ) FROM Entities ent FOR JSON PATH json to table 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 -- [example 1] declare @json nvarchar(max) set @json='{\"userid\":\"A123\",\"username\":\"가나닭\",\"age\":45,\"skills\":[\"sql\",\"c#\",\"mvc\"]}'; select * from openjson(@json) with( userid varchar(100) '$.userid' , username varchar(100) '$.username', age int '$.age', skills nvarchar(max) as json) -- [example 2] declare @json nvarchar(max) set @json='{\"userid\":\"A123\",\"username\":\"가나닭\",\"age\":45,\"skills\":[\"sql\",\"c#\",\"mvc\"]}'; select userid, username, age, s.value from openjson(@json) with( userid varchar(100) '$.userid' , username varchar(100) '$.username', age int '$.age') cross apply openjson(@json, '$.skills') as s -- [example 3] declare @json nvarchar(max) set @json='{\"userid\":\"A123\",\"username\":\"가나닭\",\"age\":45,\"items\":[{\"A\":\"A\", \"B\":\"B\"},{\"A\":\"C\", \"B\":\"D\"}]}'; select * from openjson(@json) with( userid varchar(100) '$.userid' , username varchar(100) '$.username', age int '$.age', items nvarchar(max) as json) cross apply openjson(items) with (A varchar(100), B varchar(100)) Reference SQL Server의 JSON 데이터 Transform JSON array into set of rows FOR XML(SQL Server)" } , { "title": "Rust Generics, Lifetimes", "url": "/20200414-1/", "date": "2020-04-14", "categories": "【 RustㆍZigㆍGo 】, Rust", "tags": "rust", "content": "Rust에서 제너릭 데이터 타입 사용법을 간단하게 살펴보고 또한 Rust에서 모든 참조는 Lifetime을 갖는 데 이 부분에 있어 다른 개발 언어와 약간 독특한 문법을 가지고 있어서 간단한 예제로 정리하였다. 어떠한 객체가 있고 이를 가리키는 참조가 있다면 객체가 삭제되었는데 참조가 여전히 존재하는 경우를 해결하는 방법이라고 생각하자. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 기본 참조 예제 fn main() { let mut var1 = 10; let mut var2 = 20; let mut var3 = 0; get_int_ref(&amp;mut var1, &amp;mut var2, &amp;mut var3); println!(\"{} + {} = {}\", var1, var2, var3); } fn get_int_ref(p1: &amp;mut i32, p2: &amp;mut i32, p3: &amp;mut i32) { *p1 = *p1 + 1; *p2 = *p2 - 1; *p3 = *p1 + *p2; } Lifetime 예제 1 2 3 4 5 6 7 8 9 10 11 12 13 14 fn main() { let mut var1 = 10; let mut var2 = 20; let result = get_int_ref(&amp;mut var1, &amp;mut var2); println!(\"{}\", result); } fn get_int_ref&lt;'a&gt;(p1: &amp;'a i32, p2: &amp;'a i32) -&gt; &amp;'a i32 { if p1 &gt; p2 { return p1; } else { return p2; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 fn main() { let mut var1 = 10; let mut var2 = 20; let mut var3 = 0; let result_ref = get_int_ref(&amp;mut var1, &amp;mut var2, &amp;mut var3); println!(\"{} + {} = {}\", result_ref.0, result_ref.1, result_ref.2); } fn get_int_ref&lt;'a&gt;(p1: &amp;'a mut i32, p2: &amp;'a mut i32, p3: &amp;'a mut i32) -&gt; (&amp;'a i32, &amp;'a i32, &amp;'a i32) { *p3 = *p1 + *p2; return (p1, p2, p3); } Generic 예제 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 struct SimpleStruct { something: i32, } trait SimpleTrait { fn simple_func(&amp;self, a: &amp;str, b: &amp;str) -&gt; String; } impl SimpleTrait for SimpleStruct { fn simple_func(&amp;self, a: &amp;str, b: &amp;str) -&gt; String { return self.something.to_string() + \" - \" + a + \" - \" + b; } } impl SimpleTrait for i32 { fn simple_func(&amp;self, a: &amp;str, b: &amp;str) -&gt; String { return self.to_string() + \" - \" + a + \" - \" + b; } } fn do_this&lt;T&gt;(some_var: &amp;T) -&gt; String where T: SimpleTrait, { return some_var.simple_func(\"가나닭\", \"마바삵\"); } fn main() { let test = SimpleStruct { something: 1000 }; let result = do_this(&amp;test); println!(\"{}\", result); let test32 = 1234; let result2 = do_this(&amp;test32); println!(\"{}\", result2); println!(\"{} + {} = {}\", 1.1, 2.2, func1(1.1, 2.2) as f32); } fn func1&lt;T&gt;(input_a: T, input_b: T) -&gt; T where T: std::ops::Add&lt;Output = T&gt; + std::fmt::Debug, { return input_a + input_b; } Reference Rust Tutorial, Doug Milford 라이프타임(lifetime) Rust 라이프타임 생략 규칙" } , { "title": "Intro to MongoDB with C#", "url": "/20200412-1/", "date": "2020-04-12", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "csharp, database", "content": "MongoDB를 C#에서 CRUD 형태로 조작해보는 간단한 샘플 예제이다. Visual Studio 2019에서 .NET Core 콘솔 프로젝트를 생성하고 Nuget에서 MongoDB.Driver 패키지를 추가한다. 대상 프레임워크는 3.1이며 Visual Studio 버전은 16.5.3이다. BoardModel.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; namespace ConsoleMongo { //[BsonIgnoreExtraElements] public class BoardModel { [BsonId] // [BsonIgnoreIfDefault] [BsonElement(\"_id\")] public ObjectId ID { get; set; } public string UserID { get; set; } public string UserName { get; set; } //[BsonElement(\"userkind\")] public string UserKind { get; set; } public AddressModel Address { get; set; } } public class AddressModel { public string StreetAddress { get; set; } public string ZipCode { get; set; } } } MongoCRUD.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 using MongoDB.Bson; using MongoDB.Driver; using System.Collections.Generic; namespace ConsoleMongo { public class MongoCRUD { private readonly IMongoDatabase db; public MongoCRUD() { var client = new MongoClient(@\"mongodb://아이디:패스워드@192.168.1.33:27017/testdb\"); db = client.GetDatabase(\"testdb\"); } // insert public void InsertRecord&lt;T&gt;(string table, T record) { var collection = db.GetCollection&lt;T&gt;(table); collection.InsertOne(record); } // select list public List&lt;T&gt; LoadRecord&lt;T&gt;(string table) { var collection = db.GetCollection&lt;T&gt;(table); return collection.Find(new BsonDocument()).ToList(); } // select one field public T LoadRecordById&lt;T&gt;(string table, string userID) { var collection = db.GetCollection&lt;T&gt;(table); var filter = Builders&lt;T&gt;.Filter.Eq(\"UserID\", userID); return collection.Find(filter).First(); } // insert, update public void UpsertRecord&lt;T&gt;(string table, ObjectId id, T record) { var collection = db.GetCollection&lt;T&gt;(table); collection.ReplaceOne(new BsonDocument(\"_id\", id), record, new ReplaceOptions { IsUpsert = true }); } // delete public void DeleteRecord&lt;T&gt;(string table, ObjectId id) { var collection = db.GetCollection&lt;T&gt;(table); var filter = Builders&lt;T&gt;.Filter.Eq(\"_id\", id); collection.DeleteOne(filter); } } } Program.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 using System; namespace ConsoleMongo { internal class Program { private static void Main() { MongoCRUD db = new MongoCRUD(); // Insert One db.InsertRecord(\"board\", new BoardModel { UserID = \"1004\", UserName = \"가나닭\", UserKind = \"C\" }); // Insert Embedded Documents BoardModel board = new BoardModel { UserID = \"1003\", UserName = \"홍길북\", UserKind = \"D\", Address = new AddressModel { StreetAddress = \"서울특별시\", ZipCode = \"12345\" } }; db.InsertRecord(\"board\", board); // Upsert var upsertRec = db.LoadRecordById&lt;BoardModel&gt;(\"board\", \"1003\"); upsertRec.UserKind = \"A\"; upsertRec.Address = new AddressModel { StreetAddress = \"서울특별시\", ZipCode = \"12345\" }; db.UpsertRecord(\"board\", upsertRec.ID, upsertRec); // Delete var deleteRec = db.LoadRecordById&lt;BoardModel&gt;(\"board\", \"1004\"); db.DeleteRecord&lt;BoardModel&gt;(\"board\", deleteRec.ID); // Select list var recs = db.LoadRecord&lt;BoardModel&gt;(\"board\"); foreach (var item in recs) { Console.WriteLine($\"{item.UserID}: {item.ID}, {item.UserName}, {item.UserKind}\"); if (item.Address != null) { Console.WriteLine($\"{item.Address.StreetAddress}, {item.Address.ZipCode}\"); } } // Select One Field (by UserID) var oneRec = db.LoadRecordById&lt;BoardModel&gt;(\"board\", \"1003\"); Console.WriteLine($\"{oneRec.UserID}: {oneRec.ID}, {oneRec.UserName}, {oneRec.UserKind}\"); if (oneRec.Address != null) { Console.WriteLine($\"{oneRec.Address.StreetAddress}, {oneRec.Address.ZipCode}\"); } } } } Sequence Field 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 db.counters.insert( { _id: \"userid\", seq: 0 } ) function getNextSequence(name) { var ret = db.counters.findAndModify( { query: { _id: name }, update: { $inc: { seq: 1 } }, new: true } ); return ret.seq; } db.users.insert( { _id: getNextSequence(\"userid\"), name: \"Sarah C.\" } ) // getDate new Date().toLocaleDateString('ko-KR').replace('x', 'y') // getDateString function GetDate() { return new Date(+new Date() + 3240 * 10000).toISOString().replace(\"T\", \"\").replace(\"Z\", \"\").replace(/-/g, \"\").replace(/:/g, \"\").replace(/\\./g, \"\") + Math.floor((Math.random() * (999 - 100) + 100)); } Reference Learn what NoSQL is, why it is different than SQL and how to use it in C# MongoDB - C# insert, update, delete Create an Auto-Incrementing Sequence Field ASP.NET Core 및 MongoDB를 사용하여 웹 API 만들기 Create a CRUD repository using MongoDB and C#" } , { "title": "Oracle 18c, AMM", "url": "/20200408-2/", "date": "2020-04-08", "categories": "【 DatabaseㆍModeling 】, Database", "tags": "database", "content": "AMM(Auto Memory Management)이란 오라클에서 인스턴스 메모리를 관리하는 가장 간단한 방법이다. 오라클에는 PGA(System Global Area), SGA(Program Global Area) 불리는 메모리 영역이 존재한다. 11g 이상에서 도입한 AMM 관리 기법은 SGA+PGA를 자동으로 관리하는 방식이다. 이때 전체 메모리를 설정하는 memory_max_target, memroy_target 파라미터를 설정해 주어야 하는 데 리눅스 운영체제의 /dev/shm 크기가 최대치가 된다. 리눅스는 기본적으로 전체 메모리의 50%를 ‘/dev/shm’에 할당하는 데 필요한 경우 이 영역을 변경하고 오라클 파라미터를 설정해야 오류가 나지 않는다. CentOS 8, Database 설치 글을 참고하여 필요한 만큼 설정(fstab, size)한다. AMM 기능을 사용하면 SGA_TARGET, PGA_AGGREGATE_TARGET의 값을 0으로 지정하는 것이 좋다. 0이 아닌 경우 그 값을 최솟값으로 인식하기 때문이다. 1 2 3 4 5 6 su - oracle sqlplus /nolog SQL&gt; conn / as sysdba SQL&gt; show parameter target # 현재 설정되어 있는 memory_max_target, memory_target, # pga_aggregate_target, sga_target 값을 확인한다. AMM 설정 1 2 3 4 5 6 7 8 9 10 SQL&gt; alter system set memory_max_target=6G scope=spfile; SQL&gt; alter system set memory_target=6G scope=spfile; SQL&gt; alter system set sga_target=0 scope=spfile; SQL&gt; alter system set pga_aggregate_target=0 scope=spfile; SQL&gt; shutdown immediate; SQL&gt; startup; SQL&gt; show parameter target SQL&gt; show parameter memory_max_target SQL&gt; show parameter memroy_target SQL&gt; exit CharacterSet PLS-553 1 2 3 4 5 6 7 SELECT DISTINCT(NLS_CHARSET_NAME(CHARSETID)) CHARACTERSET, DECODE(TYPE#, 1, DECODE(CHARSETFORM, 1, 'VARCHAR2', 2, 'NVARCHAR2', 'UNKOWN'), 9, DECODE(CHARSETFORM, 1, 'VARCHAR', 2, 'NCHAR VARYING', 'UNKOWN'), 96, DECODE(CHARSETFORM, 1, 'CHAR', 2, 'NCHAR', 'UNKOWN'), 112, DECODE(CHARSETFORM, 1, 'CLOB', 2, 'NCLOB', 'UNKOWN')) TYPES_USED_IN FROM SYS.COL$ WHERE CHARSETFORM IN (1,2) AND TYPE# IN (1, 9, 96, 112); Reference Enabling Automatic Memory Management 오라클 메모리(PGA, SGA)" } , { "title": "SQL Server, In-Memory OLTP", "url": "/20200408-1/", "date": "2020-04-08", "categories": "【 DatabaseㆍModeling 】, SQL", "tags": "database, sql", "content": "빅데이터, IoT, 인공지능과 같은 용어를 요즘은 주변에서 쉽게 볼 수 있다. 데이터베이스 분야도 전통적인 RDBMS뿐만 아니라 NoSQL, 메모리 데이터베이스, 시계열 데이터베이스 또한 관심 분야로 떠오르고 있다. 이제는 단순한 개념적 학습이 아닌 실무에서 방대한 데이터 처리를 실시간으로 처리해야 하는 상황에 직면한 것이다. 참고로 국산 시계열 데이터베이스인 마크베이스(MACHBASE)를 살펴볼 필요가 있는데 초당 104만 건 정도의 데이터를 처리할 수 있다고 한다. In-Memory OLTP는 SQL Server에서 트랜잭션 처리, 데이터 로드 및 일시적인 데이터 시나리오의 성능 최적화하기 위해 사용할 수 있는 뛰어난 기술이다 (In-Memory OLTP is the premier technology available in SQL Server and SQL Database for optimizing performance of transaction processing, data ingestion, data load, and transient data scenarios). 전통 방식인 디스크 테이블보다 실제로 5~30배의 성능 향상이 있다고 한다. High-throughput and low-latency transaction processing Data ingestion, including IoT (Internet-of-Things) Caching and session state Tempdb object replacement ETL (Extract Transform Load) 아래의 글은 Channel9에서 동영상 강좌로 소개한 SQL Server 2016 In-Memory OLTP를 정리한 것이다. 여기에 필요한 몇 가지 관련 내용을 추가하였다. In-Memory OLTP가 빠른 이유 Lock-Free, Latch-Free 구조 Native compilation 메모리 테이블 : 행이 인덱스로 연결된 구조(Hash index on Name) 기본 사용 예제 - 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 -- 1. 테스트 DB 생성 : 기존 형태로 생성 create database InMemoryDB ... -- 2. 파일그룹 만들기 alter database InMemoryDB add filegroup [InMemoryDB_fg] contains memory_optimized_data -- 3. 파일(폴더형태) 생성 alter database InMemoryDB add file(name = InMemoryDB_dir, filename='C:\\Data\\InMemoryDB_dir') to filegroup InMemoryDB_fg -- 4. 테스트 테이블 생성(with 구문 추가) use InMemoryDB go create table t1 ( col1 int not null primary key nonclustered hash with(bucket_count = 1000000) identity, col2 nvarchar(100) collate korean_wansung_bin2 not null, col3 nvarchar(100) null, col4 int null) with (memory_optimized = on, durability = schema_and_data) -- 참고: schema_only : 재시작 후 데이터는 사라짐, ldf에 로깅을 하지 않으므로 성능 up -- 5. 데이터 입력 insert into t1(col2, col3, col4) value('aaa', 'bbb', 1) 기본 사용 예제 - 2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 -- Create demo database CREATE DATABASE SQL2016_Demo ON PRIMARY ( NAME = N'SQL2016_Demo', FILENAME = N'C:\\Dump\\SQL2016_Demo.mdf', SIZE = 5120KB, FILEGROWTH = 1024KB ) LOG ON ( NAME = N'SQL2016_Demo_log', FILENAME = N'C:\\Dump\\SQL2016_Demo_log.ldf', SIZE = 1024KB, FILEGROWTH = 10% ) GO use SQL2016_Demo go -- Add Filegroup by MEMORY_OPTIMIZED_DATA type ALTER DATABASE SQL2016_Demo ADD FILEGROUP MemFG CONTAINS MEMORY_OPTIMIZED_DATA GO --Add a file to defined filegroup ALTER DATABASE SQL2016_Demo ADD FILE ( NAME = MemFG_File1, FILENAME = N'C:\\Dump\\MemFG_File1' -- your file path, check directory exist before executing this code ) TO FILEGROUP MemFG GO --Object Explorer -- check database created GO -- create memory optimized table 1 CREATE TABLE dbo.MemOptTable1 ( Column1 INT NOT NULL, Column2 NVARCHAR(4000) NULL, SpidFilter SMALLINT NOT NULL DEFAULT (@@spid), INDEX ix_SpidFiler NONCLUSTERED (SpidFilter), INDEX ix_SpidFilter HASH (SpidFilter) WITH (BUCKET_COUNT = 64), CONSTRAINT CHK_soSessionC_SpidFilter CHECK ( SpidFilter = @@spid ), ) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA); --or DURABILITY = SCHEMA_ONLY go -- create memory optimized table 2 CREATE TABLE MemOptTable2 ( ID INT NOT NULL PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 10000), FullName NVARCHAR(200) NOT NULL, DateAdded DATETIME NOT NULL ) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA) GO 인덱스 hash, range, columnstore index hash : point 쿼리에 효과적, bucket_count 이슈(range nonclustered사용) range : 범위 검색에 효과적 columnstore index 지원 : Overview 문서 여전히 지원 안 하는 구문(2016) select * : 컬럼명을 명시해야 함 case : 고유하게 컴파일된 저장 프로시저에서 CASE 식 구현 next value for : 시퀀스 미지원, 다른 채번 방법으로 사용 Creating Natively Compiled Stored Procedures 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 CREATE TABLE [dbo].[T2] ( [c1] [int] NOT NULL, [c2] [datetime] NOT NULL, [c3] nvarchar(5) NOT NULL, CONSTRAINT [PK_T1] PRIMARY KEY NONCLUSTERED ([c1]) ) WITH ( MEMORY_OPTIMIZED = ON , DURABILITY = SCHEMA_AND_DATA ) GO CREATE PROCEDURE [dbo].[usp_2] (@c1 int, @c3 nvarchar(5)) WITH NATIVE_COMPILATION, SCHEMABINDING AS BEGIN ATOMIC WITH ( TRANSACTION ISOLATION LEVEL = SNAPSHOT, LANGUAGE = N'us_english' ) DECLARE @c2 datetime = GETDATE(); INSERT INTO [dbo].[T2] (c1, c2, c3) values (@c1, @c2, @c3); END GO Reference In-Memory OLTP in SQL Server 2016 (동영상) In-Memory OLTP 개요 및 사용 시나리오 Microsoft SQL Server 메모리 최적화 테이블 만들기 메모리 내 OLTP에서 지원되지 않는 Transact-SQL 구문 고유하게 컴파일된 저장 프로시저에서 CASE 식 구현 고유하게 컴파일된 저장 프로시저 만들기 Columnstore 인덱스 - 새로운 기능 - 버전별 지원내용" } , { "title": "CentOS 8, Database 설치", "url": "/20200405-1/", "date": "2020-04-05", "categories": "【 DatabaseㆍModeling 】, Database", "tags": "database, linux", "content": "업무용 프로그램을 개발하기 위한(개인 프로젝트, 테스트 용도) 환경 구성 중 하나로 데이터베이스 구성으로 VirtualBox에 CentOS + Oracle, SQL Server, MySQL, MongoDB, PostgreSQL를 설치하는 데 이때에 필요한 설치과정을 필수적인 부분만 정리하여 보았다. CentOS 버전은 8.1이며 VirtualBox에 기본값으로 CentOS는 설치가 되었다고 가정한다. CentOS 기본설정 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 systemctl set-default multi-user.target #runlevel 3 vi /etc/sysconfig/selinux SELINUX=disabled #enforcing vi /etc/fstab tmpfs /dev/shm tmpfs size=6G 0 0 #추가(메모리8G) firewall-cmd --permanent --add-port=1433/tcp firewall-cmd --permanent --add-port=1521/tcp firewall-cmd --permanent --add-port=3306/tcp firewall-cmd --permanent --add-port=5432/tcp firewall-cmd --permanent --add-port=27017/tcp firewall-cmd --reload firewall-cmd --list-all reboot # 기타 firewall-cmd --permanent --add-rich-rule=\"rule family='ipv4' source address='xxx.xxx.xxx.xxx/24' reject\" firewall-cmd --permanent --remove-port=27017/tcp # 데몬설정확인 systemctl list-unit-files | grep -i enabled systemctl disable(enable) mssql-server.service netstat -tnlp 1 2 3 4 5 6 7 8 9 10 11 #추가 패키지 설치 yum install epel-release yum groupinstall \"Development Tools\" yum install java-11-openjdk yum install java-11-openjdk-devel yum install libnsl #오라클 rpm 설치에 필요 yum update #java default 설정 alternatives --list alternatives --config java Oracle 18c 설치 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 #Install an RPM package for Pre-Installation first. curl http://public-yum.oracle.com/public-yum-ol7.repo -o /etc/yum.repos.d/public-yum-ol7.repo sed -i -e \"s/enabled=1/enabled=0/g\" /etc/yum.repos.d/public-yum-ol7.repo rpm --import http://yum.oracle.com/RPM-GPG-KEY-oracle-ol7 yum --enablerepo=ol7_latest -y install oracle-database-preinstall-18c # 19c Pre-Installation curl -o oracle-database-preinstall-19c-1.0-2.el8.x86_64.rpm https://yum.oracle.com/repo/OracleLinux/OL8/appstream/x86_64/getPackage/oracle-database-preinstall-19c-1.0-2.el8.x86_64.rpm yum -y localinstall oracle-database-preinstall-19c-1.0-2.el8.x86_64.rpm #oracle 18c rpm버전 다운로드 설치 rpm -Uvh oracle-database-ee-18c-1.0-1.x86_64.rpm #오라클 rpm 설치 후 확인 변수 vi /etc/sysconfig/oracledb_ORCLCDB-18c.conf #오라클 초기 DB 설정 (oracledb_ORCLCDB-18c 편집가능) /etc/init.d/oracledb_ORCLCDB-18c configure #오라클 사용자 환경 설정 su - oracle vi .bash_profile umask 022 export ORACLE_SID=ORCLCDB export ORACLE_BASE=/opt/oracle/oradata export ORACLE_HOME=/opt/oracle/product/18c/dbhome_1 export PATH=$PATH:$ORACLE_HOME/bin export NLS_LANG=KOREAN_KOREA.AL32UTF8 source .bash_profile #시작, 종료 확인 sqlplus /nolog SQL&gt; conn / as sysdba SQL&gt; shutdown immediate #(종료) 또는 startup(시작) SQL&gt; exit lsnrctl stop #(리스너 종료) 또는 start(시작) exit su - oracle #샘플(HR) 스키마 설치 SQL&gt; alter session set \"_ORACLE_SCRIPT\"=true SQL&gt; @?/demo/schema/human_resources/hr_main.sql 1:패스워드 2:users #기본테이블스페이스 3:temp #temp테이블스페이스 4:$ORACLE_HOME/demo/schema/log #백업한 Data 불러오기(imp) SQL&gt; create tablespace pgspace datafile '/opt/oracle/oradata/ORCLCDB/pg.dbf' size 1G autoextend on next 100M; #create temporary tablespace pgtemp tempfile '/opt/oracle/oradata/ORCLCDB/pgtemp.dbf' size 1G autoextend on next 100M; SQL&gt; create user pg identified by \"*****\" default tablespace pgspace temporary tablespace temp; SQL&gt; grant connect, resource, dba to pg; #alter user 유저명 quota unlimited on 테이블스페이스이름; SQL&gt; exit imp userid=pg/***** file=./backup.dmp ignore=y fromuser=xxx touser=pg #참고(cpu 100% : alter system set \"_swrf_mmon_dbfus\"=false scope=memory;) #패스워드/기간 설정 select * from dba_profiles where profile = 'DEFAULT'; alter profile default limit password_life_time unlimited; alter user 계정명 account unlock; alter user 계정명 identified by 새비밀번호; SQL Server 2019 설치 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 sudo curl -o /etc/yum.repos.d/mssql-server.repo https://packages.microsoft.com/config/rhel/8/mssql-server-2019.repo sudo curl -o /etc/yum.repos.d/msprod.repo https://packages.microsoft.com/config/rhel/8/prod.repo yum update #닷넷코어가 필요한 경우 설치 yum install dotnet-sdk-3.1 #서버설치 및 설정(버전,비밀번호,한국어) yum install -y mssql-server /opt/mssql/bin/mssql-conf setup #도구설치 yum install -y mssql-tools unixODBC-devel #전체 텍스트 검색 설치 yum install -y mssql-server-fts #에이전트 설치(설정) /opt/mssql/bin/mssql-conf set sqlagent.enabled true #언어 및 기타 설정 /opt/mssql/bin/mssql-conf set-collation :Korean_Wansung_CI_AS /opt/mssql/bin/mssql-conf set language.lcid 1042 /opt/mssql/bin/mssql-conf set network.tcpport xxxxx #서버 시작(또는 restart) systemctl start mssql-server.service MySQL 8 설치 1 2 3 4 5 6 7 8 9 rpm -ivh https://dev.mysql.com/get/mysql80-community-release-el8-1.noarch.rpm yum install mysql-server systemctl start mysqld.service mysql_secure_installation mysql -u root -p #root 원격접속 허용도 아래와 같은 방법으로 한다. create database TestDB; create user'test'@'%' identified by '*****'; grant all privileges on TestDB.* to 'test'@'%'; MongoDB 4 설치 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 vi /etc/yum.repos.d/mongodb.repo [mongodb-org-4.4] name=MongoDB Repository baseurl=https://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/4.4/x86_64/ gpgcheck=1 enabled=1 gpgkey=https://www.mongodb.org/static/pgp/server-4.4.asc dnf install mongodb-org systemctl start mongod.service mongo &gt;use admin &gt;db.createUser( { user: \"admin\", pwd: \"*****\", roles: [ { role: \"userAdminAnyDatabase\", db: \"admin\" } ] } ) &gt;show users &gt;exit vi /lib/systemd/system/mongod.service #원격접속 또는 vi /etc/mongod.conf net: port: 27017 bindIp: 0.0.0.0 security: authorization: enabled systemctl restart mongod.service #몽고DB 권한 확인 및 DB추가 mongo &gt;db.version() &gt;use admin &gt;show users #권한에러남 &gt;db.auth('admin','*****') &gt;show users #정상출력 &gt;use testdb &gt;db.createUser({ user: \"test\", pwd: \"*****\", roles: [ { role: \"userAdmin\", db: \"testdb\" }] }) &gt;db.grantRolesToUser('test', ['readWrite']); MongoDB에 대한 기본 명령어는 여기를 참고한다. PostgreSQL 12 설치 1 2 3 4 5 6 7 sudo rpm -Uvh https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm sudo dnf install postgresql12-server --disablerepo=AppStream sudo /usr/pgsql-12/bin/postgresql-12-setup initdb sudo systemctl start postgresql-12.service su - postgres psql postgres=# \\password postgres PostgreSQL 추가 내용 How to Install PostgreSQL on CentOS 8 &amp; RHEL 8 How To Install PostgreSQL 12 on CentOS 7 / CentOS 8 How to install PostgreSQL 12 on CentOS 8 / RHEL 8 / Oracle Linux 8" } , { "title": "Rust Structs, Traits and Impl", "url": "/20200401-1/", "date": "2020-04-01", "categories": "【 RustㆍZigㆍGo 】, Rust", "tags": "rust", "content": "Rust는 OOP의 특성인 Class 키워드가 없다. 대신에 struct와 impl이 있으므로 이를 혼합하여 사용한다. 다음 예제는 Rust에서 구조적 프로그래밍을 위한 struct, trait, impl 키워드를 살펴볼 것이며 mod 키워드를 통해 다른 rust 파일을 불러와 재사용할 수 있는 모듈(module) 시스템 설명도 포함하였다. 참고로 실행 파일을 static build로 만드는 환경설정은 아래와 같다. random_info.rs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 pub struct RandomInfo { pub call_count: i64, pub some_bool: bool, pub some_int: i64, } pub trait SomeTrait { fn is_vaild(&amp;self) -&gt; bool; } impl SomeTrait for RandomInfo { fn is_vaild(&amp;self) -&gt; bool { self.some_bool } } impl RandomInfo { pub fn new(param_a: bool) -&gt; Self { Self { call_count: 0, some_bool: !param_a, some_int: 8, } } pub fn is_smaller(&amp;mut self, compare_to: i64) -&gt; bool { self.call_count += 1; self.some_int &lt; compare_to } } main.rs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 mod random_info; use random_info::*; struct DougsData { some_float: f64, random: RandomInfo, } fn main() { let mut dougs_var = DougsData { some_float: 10.3, random: RandomInfo::new(true), }; let is_this_smaller = dougs_var.random.is_smaller(9); let this_some_int = dougs_var.random.call_count; println!(\"{} {}\", is_this_smaller, this_some_int); let is_vaild = dougs_var.random.is_vaild(); println!(\"{}\", is_vaild); println!(\"{} {} {}\", dougs_var.some_float, dougs_var.random.some_bool, dougs_var.random.some_int); } cargo static build 프로젝트 폴더의 Root에 .cargo폴더를 만들고 이 안에 config파일을 만든다. 1 2 [target.x86_64-pc-windows-msvc] rustflags = [\"-C\", \"target-feature=+crt-static\"] Rust Naming conventions Item Convention Crates snake_case (but prefer single word) Modules snake_case Types CamelCase Traits CamelCase Enum variants CamelCase Functions snake_case Methods snake_case General constructors new or with_more_details Conversion constructors from_some_other_type Local variables snake_case Static variables SCREAMING_SNAKE_CASE Constant variables SCREAMING_SNAKE_CASE Type parameters concise CamelCase, usually single uppercase letter: T Lifetimes short, lowercase: ‘a Reference rust Cargo Configuration Classes in Rust rust, Inheritance with Traits" } , { "title": "Dependency Injection using Autofac", "url": "/20190923-1/", "date": "2019-09-23", "categories": "【 csharpㆍdotnetㆍavalonia 】, csharp", "tags": "csharp", "content": "Autofac is an IoC container for Microsoft .NET. It manages the dependencies between classes so that applications stay easy to change as they grow in size and complexity(GitHub-Autofac 2019). Autofac is an addictive Inversion of Control container for .NET Core, ASP.NET Core, .NET 4.5.1+, Universal Windows apps, and more(autofac.org 2019). 닷넷 프레임워크를 사용하다 보면 의존성 주입이란 단어가 자주 등장한다. ASP.NET Core MVC, WPF MVVM 그리고 Caliburn.Micro에서 Ioc.Get&lt;T&gt;() 에서 DI를 사용하는 예를 볼 수 있는 데 이번 글에서는 일반적인 개발환경에서 DI를 자동으로 그리고 쉽게 사용할 수 있는 Autofac 패키지의 사용법을 간단하게 작성해보았다. 참고 : 일반적인 DI, Interface 사용법 보기 메인 프로그램 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // Program.cs using Autofac; using System; namespace ConsoleUI; class Program { static void Main() { var container = ContainerConfig.Configure(); using(var scope = container.BeginLifetimeScope()) { var app = scope.Resolve&lt;IApplication&gt;(); app.Execute(); } Console.ReadLine(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // other Main namespace ConsoleUI; class Program { static void Main() { var container = BuildContainer(); using(var scope = container.BeginLifetimeScope()) { scope.Resolve&lt;IApplication&gt;().Run(); // Run()함수실행 } } private static IContainer BuildContainer() { var builder =- new ContainderBuilder(); builder.RegisterAssemblyTypes(Assembly.GetExcutingAssembly()).AsSelf().AsImplementedInterfaces(); return builder.Build(); } } Autofac 설정 파일 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // 인테페이스 연결 // ContainerConfig.cs using Autofac; using System.Linq; using System.Reflection; namespace ConsoleUI; public static class ContainerConfig { public static IContainer Configure() { var builder = new ContainerBuilder(); builder.RegisterType&lt;Application&gt;().As&lt;IApplication&gt;(); //builder.RegisterType&lt;DBHelper&gt;().As&lt;IDBHelper&gt;(); //DBHelper -&gt; DBHelperOther로 간단하게 로직 변경 builder.RegisterType&lt;DBHelperOther&gt;().As&lt;IDBHelper&gt;(); //Services 폴더의 인테페이스 파일 자동등록 builder.RegisterAssemblyTypes(Assembly.Load(nameof(ConsoleUI))) .Where(t =&gt; t.Namespace.Contains(\"Services\")) .As(t =&gt; t.GetInterfaces().FirstOrDefault(i =&gt; i.Name == \"I\" + t.Name)); return builder.Build(); } } Application 파일 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // 메인프로그램에서 사용한다. // Application.cs namespace ConsoleUI; public class Application : IApplication { private readonly IDBHelper _dbHelper; public Application(IDBHelper dbHelper) { _dbHelper = dbHelper; } public void Execute() { _dbHelper.ProcessDB(); } } // IApplication.cs namespace ConsoleUI; public interface IApplication { void Execute(); } DBHelper.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 using ConsoleUI.Services; using System; namespace ConsoleUI; public class DBHelper : IDBHelper { private readonly IDBConnection _dbConnection; private readonly ILogger _logger; public DBHelper(IDBConnection dbConnection, ILogger logger) { _logger = logger; _dbConnection = dbConnection; } public void ProcessDB() { _logger.Log(\"Starting DB...\"); Console.WriteLine(\"데이터 처리\"); _dbConnection.DBOpen(\"SQL Server\"); _dbConnection.DBClose(); _logger.Log(\"Finished DB...\"); } } public class DBHelperOther : IDBHelper { private readonly IDBConnection _dbConnection; private readonly ILogger _logger; public DBHelperOther(IDBConnection dbConnection, ILogger logger) { _logger = logger; _dbConnection = dbConnection; } public void ProcessDB() { _logger.Log(\"Starting OtherDB...\"); Console.WriteLine(\"Other 데이터 처리\"); _dbConnection.DBOpen(\"Other Database\"); _dbConnection.DBClose(); _logger.Log(\"Finished OtherDB...\"); } } // IDBHelper.cs namespace ConsoleUI; public interface IDBHelper { void ProcessDB(); } 기타 파일 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 // Services 폴더에 작성한다. // DBConnection.cs using System; namespace ConsoleUI.Services; class DBConnection : IDBConnection { public void DBOpen(string name) { Console.WriteLine($\"Open Database - {name}\"); } public void DBClose() { Console.WriteLine(\"Close Database\"); } } // IDBConnection.cs namespace ConsoleUI.Services; public interface IDBConnection { void DBOpen(string name); void DBClose(); } // Logger.cs using System; namespace ConsoleUI.Services; public class Logger : ILogger { public void Log(string message) { Console.WriteLine($\"Logging - {message}\"); } } // ILogger.cs namespace ConsoleUI.Services; public interface ILogger { void Log(string message); }" } , { "title": "Worker Services in .NET Core 3.0", "url": "/20190912-1/", "date": "2019-09-12", "categories": "【 csharpㆍdotnetㆍavalonia 】, dotnet", "tags": "csharp, dotnet, svc", "content": "닷넷 코어 3.0 버전에는 Worker Service라 불리는 새로운 template이 추가되었다. 윈도우 서비스 프로그램을 쉽게 작성하게 해준다. 아래의 예제는 닷넷 유튜브 강좌로 유명한 IAmTimCorey의 강의한 내용을 소스 코드 형식으로 정리한 것이다. 사용한 환경은 Visual Studio 2019 16.2.5, .NET Core 3.0 Preview 7이며 프로젝트 생성 시 ASP.NET Core 웹 응용 프로그램으로 프로젝트를 진행 후 작업자 서비스(Worker Service)를 선택한다. 패키지 추가 설치 Serilog.AspNetCore Serilog.Sinks.File Microsoft.Extensions.Hosting.WindowsServices 최종 작성된 실행 파일을 로그 텍스트 파일과 같은 위치에 놓고 윈도우 서비스를 다음과 같이 등록한다. 파워셸 관리자 모드 필요. 1 2 3 4 PS C:\\&gt; sc.exe create WebsiteStatus binpath=C:\\Test\\WebsiteStatus.exe start=auto [SC] CreateService 성공 PS C:\\&gt; sc.exe delete WebsiteStatus [SC] DeleteService 성공 아래는 전체 소스이다. Program.cs, Worker.cs 프로그램 소스 Worker.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Net.Http; using System.Threading; using System.Threading.Tasks; namespace WebsiteStatus; public class Worker : BackgroundService { private readonly ILogger&lt;Worker&gt; _logger; private HttpClient client; public Worker(ILogger&lt;Worker&gt; logger) { _logger = logger; } public override Task StartAsync(CancellationToken cancellationToken) { client = new HttpClient(); return base.StartAsync(cancellationToken); } public override Task StopAsync(CancellationToken cancellationToken) { client.Dispose(); return base.StopAsync(cancellationToken); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var result = await client.GetAsync(\"https://localhost\", stoppingToken); if (result.IsSuccessStatusCode) { _logger.LogInformation(\"The website is up. Status code {StatusCode}\", result.StatusCode); } else { _logger.LogError(\"The website is down. Status code {StatusCode}\", result.StatusCode); } await Task.Delay(5000, stoppingToken); } } } Program.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Serilog; using Serilog.Events; using System; namespace WebsiteStatus; public class Program { public static void Main(string[] args) { Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .MinimumLevel.Override(\"Microsoft\", LogEventLevel.Warning) .Enrich.FromLogContext() .WriteTo.File(@\"C:\\Test\\LogFile.txt\").CreateLogger(); try { Log.Information(\"Starting up the service\"); CreateHostBuilder(args).Build().Run(); } catch (Exception e) { Log.Fatal(e, \"There was a problem starting the service\"); } finally { Log.CloseAndFlush(); } } public static IHostBuilder CreateHostBuilder(string[] args) =&gt; Host.CreateDefaultBuilder(args) .UseWindowsService() .ConfigureServices((hostContext, services) =&gt; { services.AddHostedService&lt;Worker&gt;(); }).UseSerilog(); } Reference Worker Services in .NET Core 3.0 - The New Way to Create Services Worker Service Template in .NET Core 3.0" } , { "title": "우리에겐 진보보다 자주와 평화가 필요하다", "url": "/20181102-1/", "date": "2018-11-02", "categories": "【 Liberal Artsㆍφιλοσοφία 】, Insights", "tags": "진보", "content": "세상에는 정치적으로 ‘진보’와 ‘보수’만이 존재하는 것은 아니다. 여기에 어설프게 ‘중도’라는 탈출구를 만들어 무관심도 전체 프레임에 포함한다. 주체적 삶이란 주관적인 내 생각대로만 판단하거나 살아가는 것이 아닌 타인의 합리성을 그대로 인정하고 받아들이는 것을 말한다. 내가 판단하여 인정하는 것이기 때문에 주체적이라 할 수 있다. 세상은 논리적이고 합리적인 사람이 많아야 좋아지는 게 아니라 그것을 받아들이고 인정하는 사람이 많을 때 좋은 사회가 되는 것이다. ‘페미니즘’, ‘다문화주의’, ‘난민 문제’, ‘외국인노동자’ 같은 이슈는 보수∙진보의 정치적 문제가 아니라 내가 그것에 대해 주체적으로 얼마나 진실에 접근할 수 있느냐의 문제이다. 우리는 이분법적인 패러다임을 만들어내는 언론이나 정치인들과 같은 이들에 대해서 나 자신이 주체적으로 깨어 있어야 한다." } , { "title": "종교는 졸업하는 것이다", "url": "/20181029-1/", "date": "2018-10-29", "categories": "【 Liberal Artsㆍφιλοσοφία 】, Philo", "tags": "종교, philo", "content": "사람이 나이를 먹어가는 것은 그만큼 성숙해 가는 것이고 그 이전의 상태를 졸업하고 유아기 상태에서 어른이 된다는 것은 나 자신과 공동체에서의 책임과 책무가 따른 것을 말한다. 공자는 사람이 나이 먹음에 따라 지학 ∙ 이립 ∙ 불혹 ∙ 지천명 ∙ 이순 ∙ 종심으로 나누어 그 위치를 설명하였다. 종교적 인간이란 가장 원초적이면서 유아기 상태를 말함이다. 우리 사회에 종교가 있는 것은 아이에서 어른으로 영적 성숙이라는 과정에 일부분 역할을 하는 것이다. 그래서 학교처럼 과정이 끝나면 당연히 졸업하고 과거는 단순한 추억 정도로 남겨두어야 한다. 그런데도 무슨 조직을 만들고 유지하면서 세력을 모으는 것은 부처나 예수의 뜻이 아니라 장사치나 할 법한 일을 하는 것이다. 깨달으면 거기에 머물면 안 된다." } , { "title": "SQL 튜닝 원리와 해법", "url": "/20181021-1/", "date": "2018-10-21", "categories": "【 DatabaseㆍModeling 】, SQL", "tags": "sql, 서적", "content": "데이터베이스 쿼리의 결과는 ‘1분’과 ‘10분’의 차이는 있지만, 결과는 같다는 함정이 있다. 나의 기준이 ‘3초’ 안에서의 결과를 바란다면 ‘3초’ 이상은 에러라고 생각해야 한다. 1 2 3 4 5 6 7 8 9 10 11 12 13 개발자가 필수로 이해해야만 하는 Database 서적 # 기본 1. 조시형, 친절한 SQL 튜닝, DBian, 2018 2. 정재우, SQL Server 튜닝 원리와 해법, 비투엔컨설팅, 2010 3. 김상래, 프로젝트 성패를 결정짓는 데이터 모델링 이야기, 한빛미디어, 2015 4. 이춘식, 아는 만큼 보이는 데이터베이스 설계와 구축, 한빛미디어, 2008 5. 유동오, 핵심 데이터 모델링, DBian, 2020 # 고급 1. 조시형, 오라클 성능 고도화 원리와 해법 I, 비투엔컨설팅, 2009 2. 조시형, 오라클 성능 고도화 원리와 해법 II, 비투엔컨설팅, 2010 3. 이춘식, 데이터베이스 설계와 구축(개정판), 한빛미디어, 2011" } , { "title": "비트겐슈타인, 논리철학논고", "url": "/20181013-1/", "date": "2018-10-13", "categories": "【 Liberal Artsㆍφιλοσοφία 】, Philo", "tags": "philo", "content": "세계는 사례인 것 모두이다. 세계는 사실들의 총체이지, 사물들의 총체가 아니다. 세계는 사실들에 의해, 그리고 그것들이 모든 사실임에 의해 결정된다. 대상들은 이름 될 수 있을 뿐이다. 기호들이 그것들을 대표한다. 나는 대상들에 관하여 이야기할 수 있을 뿐, 대상들을 언표(言表)할 수는 없다. 명제는 사물이 어떠한가만을 말할 수 있을 뿐, 그것이 무언인가는 말할 수 없다.(논리철학논고 서문) 정신적 탐구에는 통틀어 두 가지 길만이 열려 있으니, 곧 미학 그리고 정치경제학이다.(말라르메, 방황/마법) 마음의 작용이 그치면 언어의 길도 끊어진다. 인연으로 생긴 것(연기), 나는 그것을 공이라 말한다. 그것은 또한 가명이며, 또한 중도의 의미이다.(용수,중론)" } , { "title": "경제학의 정치적 논쟁, 누가 이득을 보는가?", "url": "/20181010-1/", "date": "2018-10-10", "categories": "【 Liberal Artsㆍφιλοσοφία 】, Arts", "tags": "arts, 경제, 정치", "content": "누가 이득을 보는가? : 경제학은 정치적 논쟁이다 경제학은 정치적 논쟁이다. 과학이 아니고, 앞으로도 과학이 될 수 없다. 경제학에는 정치적, 도덕적 판단으로부터 자유로는 상태에서 확립될 수 있는 객관적 진실이 존재하지 않는다. 따라서 경제학적 논쟁을 대할 때 우리는 다음과 같은 오래된 질문을 던져야 한다. “Cui bono(누가 이득을 보는가?)” 로마의 정치인이자 유명한 웅변가였던 마르쿠스 툴리우스 키케로의 말이다.1 경제는 돈을 버는 게 아니라 민생을 온전히 경영하는 것 우리 사회를 리드하는 권력의 중핵이 아직도 너무 이념화되어 있다. 부국강병의 실용주의 노선이 없는 것이다. 경제를 살린다 하면서 그들의 경제논리는 기껏해야 패망해 가는 미국 경제에의 의존밖에는 구상해 놓은 것이 없다. 이념에 구애되지 않는 보다 자유로운 상상력을 발휘하지 않는 한 우리 경제는 점점 미궁으로 빠져들어 가기만 할 것이다.2 Reference 장하준,『장하준의 경제학 강의』, 부키(주), 2014, p.435. “도올고함(孤喊), 경제는 돈을 버는 게 아니라 민생을 온전히 경영하는 것”, &lt;중앙일보&gt;, 2008.10.22, https://www.joongang.co.kr/article/3347049, 2018.10.10." } , { "title": "공공(公共, Public)", "url": "/20181007-1/", "date": "2018-10-07", "categories": "【 Liberal Artsㆍφιλοσοφία 】, Insights", "tags": "公共, insights", "content": "행복은 개인의 몫이지만 불행은 공공이 해결해야 하는 몫이다." } ]
