PHpullh

C# · 심층 가이드

C# 패턴 완전 정리

전략과 옵저버, 데코레이터 같은 고전 패턴을 델리게이트와 이벤트, 의존성 주입이 기본으로 깔린 C# 환경에 맞춰 다시 쓴 스무 가지 구현을 코드로 비교합니다.

주제 20개 · 예제 코드 포함 · 최종 수정 2026-09-03 · 작성 pullh 편집팀

고전 디자인 패턴 책의 예제를 C#으로 그대로 옮기면 대개 과합니다. 전략 패턴은 인터페이스와 구현체 세 개 대신 Func 하나로 끝나는 경우가 많고, 옵저버는 이벤트가 언어 차원에서 지원됩니다. 반대로 이 생태계에서 훨씬 중요해진 축도 있습니다. 생성자 주입과 컨테이너가 사실상 표준이 되면서, 팩터리와 저장소 패턴은 객체를 만드는 방법보다 수명을 누가 관리하느냐의 문제로 옮겨 갔습니다. 그래서 패턴 이름을 외우기보다, 이 언어에서 그 의도가 어떤 문법으로 실현되는지를 보는 편이 실전에 가깝습니다.

설정 객체는 뒤에 나오는 대부분의 예제가 값을 주입받는 통로라 먼저 봐 두면 편합니다. 전략 패턴과 옵저버는 인터페이스판과 델리게이트판을 비교하며 읽고, 데코레이터에서 캐시 래퍼와 retry 유틸로 이어지는 흐름은 같은 감싸기 기법을 세 가지 목적에 쓴 변주입니다. 미들웨어 체인은 그 감싸기를 여러 겹 쌓은 형태이고, 의존성 주입 컨테이너와 저장소 패턴은 조립 지점을 한곳으로 모으는 이야기로 묶입니다.

컨테이너를 쓰기 시작하면 수명 불일치가 조용한 사고로 돌아옵니다. 싱글턴으로 등록한 객체가 요청 단위 수명을 가진 의존성을 생성자로 받으면, 그 짧은 수명의 인스턴스가 싱글턴에 붙잡혀 앱이 끝날 때까지 살아남습니다. 데이터베이스 컨텍스트에서 특히 잘 터지는 형태입니다. 옵저버 쪽에도 비슷한 함정이 있습니다. 이벤트에 구독만 하고 해제하지 않으면 발행자가 구독자를 계속 참조해 회수되지 않으니, 구독 시점과 해제 시점을 짝으로 설계해야 합니다.

01설정 객체

이름을 키로, 할인 계산 람다를 값으로 담은 사전을 만들고 percent 항목에 100을 넣어 90을 출력합니다. 규칙 이름이 설정 파일이나 DB에서 문자열로 들어올 때 이 표를 그대로 씁니다. (int) 캐스팅은 반올림이 아니라 소수부를 버리므로 금액 계산에는 decimal이 안전합니다.

C# code

// 설정 객체
using System;

var strategies = new Dictionary<string, Func<int, int>> { ["percent"] = value => (int)(value * 0.9), ["flat"] = value => value - 5 };
Console.WriteLine(strategies["percent"](100));
알아두면 좋은 점

설정 표를 IReadOnlyDictionary로 노출하면 사용하는 쪽이 항목을 덧붙이거나 지울 수 없어, 시작 시점에 확정된 구성이 그대로 유지됩니다.

자주 하는 실수

(int)(value * 0.9)는 부동소수 오차 때문에 값에 따라 기대보다 1 작은 결과가 나올 수 있습니다. 금액은 decimal로 계산하고 반올림 방식을 명시해야 합니다.

02전략 패턴

GetUser는 사전에 키가 없을 때만 값을 채우고 그 뒤로는 저장된 값을 돌려줍니다. 비용이 큰 조회 함수를 감쌀 때 이런 지역 함수 하나면 충분합니다. ContainsKey로 확인한 뒤 인덱서로 다시 접근하면 해시 계산이 여러 번 일어나므로 TryGetValue 쪽이 낫습니다.

C# code

// 전략 패턴
using System;

var cache = new Dictionary<int, string>();
string GetUser(int id) { if (!cache.ContainsKey(id)) cache[id] = "user"; return cache[id]; }
Console.WriteLine(GetUser(2));
알아두면 좋은 점

전략을 사전 대신 switch 식으로 고르면 새 항목을 빠뜨렸을 때 컴파일러가 짚어 줄 수 있지만, 실행 중에 항목을 추가할 수는 없게 됩니다.

자주 하는 실수

조회에 실패했을 때도 값을 채워 넣는 구조라면 실패한 결과까지 캐시에 남습니다. 성공 여부를 구분해 실패는 저장하지 않도록 해야 합니다.

03옵저버

사전에 담긴 두 개의 Func<int, int> 중 하나를 키로 꺼내 즉시 호출합니다. 사건이 생겼을 때 등록해 둔 여러 반응을 모두 돌려야 한다면 값 타입을 목록이나 event로 바꿉니다. C#의 event는 구독자가 없으면 델리게이트가 null이라 ?.Invoke로 감싸 불러야 합니다.

C# code

// 옵저버
using System;

var strategies = new Dictionary<string, Func<int, int>> { ["percent"] = value => (int)(value * 0.9), ["flat"] = value => value - 5 };
Console.WriteLine(strategies["percent"](100));
알아두면 좋은 점

이벤트 필드를 지역 변수에 복사한 뒤 호출하던 옛 관용구 대신, Handler?.Invoke(...) 한 줄로 같은 안전성을 얻을 수 있습니다.

자주 하는 실수

이벤트에 등록한 핸들러를 해제하지 않으면 발행자가 구독자를 계속 참조해, 구독자 객체가 회수되지 않는 누수가 생깁니다.

04데코레이터

원래 조회 앞에 사전 확인 단계를 덧대어 같은 id에는 다시 계산하지 않게 만듭니다. 로깅이나 권한 검사처럼 본체를 건드리지 않고 앞뒤에 동작을 끼울 때 같은 구조를 씁니다. 여러 겹을 쌓으면 감싼 순서가 곧 호출 순서라, 캐시를 로깅보다 바깥에 두면 캐시가 맞은 요청은 로그에 남지 않습니다.

C# code

// 데코레이터
using System;

var cache = new Dictionary<int, string>();
string GetUser(int id) { if (!cache.ContainsKey(id)) cache[id] = "user"; return cache[id]; }
Console.WriteLine(GetUser(4));
알아두면 좋은 점

컨테이너에서 데코레이터를 구성할 때는 안쪽 구현을 먼저 등록하고 바깥 데코레이터가 그것을 생성자로 받게 해야, 감싸는 순서가 코드에 분명히 드러납니다.

자주 하는 실수

데코레이터가 안쪽 구현의 예외를 잡아 기본값으로 바꾸면 바깥에서는 실패를 알 수 없게 됩니다. 감싸는 계층은 동작을 더할 뿐 의미를 바꾸지 않아야 합니다.

05어댑터

호출부는 strategies[key](100) 한 줄로 통일되고, 실제 계산 방식은 사전 안 람다가 결정합니다. 시그니처가 제각각인 함수를 하나의 모양으로 맞춰 끼울 때 델리게이트로 감싸면 구현 클래스를 새로 만들지 않아도 됩니다. 없는 키를 넣으면 KeyNotFoundException이 나므로 기본 항목을 하나 정해 둡니다.

C# code

// 어댑터
using System;

var strategies = new Dictionary<string, Func<int, int>> { ["percent"] = value => (int)(value * 0.9), ["flat"] = value => value - 5 };
Console.WriteLine(strategies["percent"](100));
알아두면 좋은 점

어댑터를 델리게이트로 두면 테스트에서 람다 한 줄로 갈아 끼울 수 있어, 인터페이스와 목 객체를 따로 만들지 않아도 됩니다.

자주 하는 실수

없는 키를 확인 없이 인덱서로 바로 읽으면 사용자가 넣은 문자열 하나에 처리 전체가 예외로 멈춥니다. 기본 전략이나 TryGetValue가 필요합니다.

06커맨드

GetUser라는 이름 하나에 상태인 사전과 실행 절차가 함께 묶여 있습니다. 실행할 일을 객체로 만들어 큐에 넣거나 나중에 되돌려야 한다면 이 묶음을 클래스로 승격시킵니다. 지역 함수가 바깥 변수 cache를 잡으면 컴파일러가 클로저 객체를 힙에 만들어, 그 사전은 함수가 살아 있는 동안 함께 남습니다.

C# code

// 커맨드
using System;

var cache = new Dictionary<int, string>();
string GetUser(int id) { if (!cache.ContainsKey(id)) cache[id] = "user"; return cache[id]; }
Console.WriteLine(GetUser(6));
알아두면 좋은 점

실행할 일을 객체로 만들면 되돌리기 메서드를 함께 둘 수 있고, 커맨드를 직렬화해 두면 재시작 후에도 대기 중이던 작업을 이어서 처리할 수 있습니다.

자주 하는 실수

커맨드가 실행 시점의 상태에 의존하면 큐에서 꺼낼 때 그 상태가 이미 달라져 있을 수 있습니다. 필요한 값은 커맨드를 만들 때 함께 담아 두어야 합니다.

07상태 패턴

키 문자열이 현재 모드이고 값 람다가 그 모드에서의 동작입니다. 상태가 늘어날 때 switch를 계속 붙이는 대신 표에 항목을 추가하는 식으로 넓힙니다. 문자열 키는 기본적으로 대소문자를 구분하므로, 입력을 그대로 받는다면 사전을 만들 때 StringComparer.OrdinalIgnoreCase를 넘깁니다.

C# code

// 상태 패턴
using System;

var strategies = new Dictionary<string, Func<int, int>> { ["percent"] = value => (int)(value * 0.9), ["flat"] = value => value - 5 };
Console.WriteLine(strategies["percent"](100));
알아두면 좋은 점

상태 전이를 표로 두면 어떤 상태에서 어디로 갈 수 있는지 한눈에 드러나, 조건문에 흩어져 있을 때보다 빠진 전이를 발견하기 훨씬 쉽습니다.

자주 하는 실수

상태를 문자열로 두면 오타가 컴파일 시점에 걸리지 않고 실행 중 키 없음 예외로만 드러납니다. 종류가 고정돼 있다면 enum을 키로 쓰는 편이 안전합니다.

08팩터리 메서드

GetUser(8) 호출 한 번에 값이 없으면 만들고 있으면 재사용하는 흐름이 모두 들어 있습니다. 생성 규칙이 한 곳에만 있어야 할 때 이렇게 통로를 함수 하나로 좁힙니다. 이 사전은 스레드 안전하지 않아 여러 스레드가 동시에 넣으면 내부 구조가 깨질 수 있고, 그럴 때는 ConcurrentDictionary.GetOrAdd를 씁니다.

C# code

// 팩터리 메서드
using System;

var cache = new Dictionary<int, string>();
string GetUser(int id) { if (!cache.ContainsKey(id)) cache[id] = "user"; return cache[id]; }
Console.WriteLine(GetUser(8));
알아두면 좋은 점

ConcurrentDictionary.GetOrAdd는 값 생성 함수를 여러 번 부를 수 있으므로, 생성 비용이 크거나 부작용이 있다면 Lazy<T>를 값으로 담아야 한 번만 실행됩니다.

자주 하는 실수

값 생성 함수 안에서 같은 사전에 다시 접근하면 재진입으로 교착이나 예외가 납니다. 생성 로직은 사전과 독립적으로 두어야 합니다.

09빌더 패턴

컬렉션 초기화 구문으로 사전을 만들면서 키와 람다를 한 번에 채워 넣습니다. 조립 과정이 길어지면 이런 초기화 블록이 단계별 빌더 역할을 대신합니다. 대괄호 형태는 내부적으로 인덱서 대입이라 같은 키를 두 번 적어도 예외 없이 뒤에 적은 값이 남습니다.

C# code

// 빌더 패턴
using System;

var strategies = new Dictionary<string, Func<int, int>> { ["percent"] = value => (int)(value * 0.9), ["flat"] = value => value - 5 };
Console.WriteLine(strategies["percent"](100));
알아두면 좋은 점

초기화 구문에서 대괄호 형태는 인덱서 대입이고 중괄호 쌍 형태는 Add 호출이라, 같은 키가 두 번 나올 때의 동작이 서로 다릅니다.

자주 하는 실수

각 단계가 자기 자신을 돌려주는 체이닝에서 중간에 새 인스턴스를 만들어 돌려주면, 앞 단계에서 설정한 값이 사라진 채 마지막 것만 남습니다.

10캐시 래퍼

사전을 조회 결과 보관소로 두고 값이 없을 때만 채운 뒤 돌려줍니다. 같은 입력이 반복되는 계산이나 원격 호출 앞에 두면 호출 횟수가 줄어듭니다. 여기에는 만료도 크기 제한도 없어 키 종류가 계속 늘면 메모리를 그대로 붙잡으므로, 실제 서비스에서는 정리 정책이 있는 MemoryCache를 씁니다.

C# code

// 캐시 래퍼
using System;

var cache = new Dictionary<int, string>();
string GetUser(int id) { if (!cache.ContainsKey(id)) cache[id] = "user"; return cache[id]; }
Console.WriteLine(GetUser(10));
알아두면 좋은 점

MemoryCache는 만료 시간과 크기 제한을 함께 지정할 수 있고, 항목이 제거될 때 콜백을 받아 정리 작업을 붙일 수도 있습니다.

자주 하는 실수

인기 있는 키의 캐시가 동시에 만료되면 그 순간 모든 요청이 원본으로 몰립니다. 만료 시각에 약간의 무작위 편차를 주면 이 몰림을 완화할 수 있습니다.

11retry 유틸

실패 대응 정책도 이 코드처럼 이름과 함수의 짝으로 표현할 수 있습니다. 재시도라면 값을 변환하는 대신 실행할 동작을 인자로 받아 예외를 잡고 다시 부르는 형태가 됩니다. 실패 즉시 그대로 다시 부르면 장애가 난 쪽에 부하가 몰리므로 시도 간격을 점점 늘리는 방식이 기본입니다.

C# code

// retry 유틸
using System;

var strategies = new Dictionary<string, Func<int, int>> { ["percent"] = value => (int)(value * 0.9), ["flat"] = value => value - 5 };
Console.WriteLine(strategies["percent"](100));
알아두면 좋은 점

재시도 간격을 두 배씩 늘리면서 약간의 무작위 값을 더하면, 동시에 실패한 클라이언트들이 같은 시각에 몰려 다시 요청하는 상황을 피할 수 있습니다.

자주 하는 실수

결제나 주문처럼 두 번 보내면 안 되는 작업을 재시도하면 중복 처리가 일어납니다. 요청에 멱등성 키를 붙이거나 재시도 대상에서 빼야 합니다.

12미들웨어 체인

조회 앞에 캐시 확인을 끼워 넣은 한 겹짜리 체인입니다. 요청 하나가 인증, 로깅, 캐시를 차례로 지나야 한다면 각 단계가 다음 단계를 인자로 받는 형태로 이어 붙입니다. ASP.NET Core에서는 어떤 단계가 다음을 부르지 않으면 파이프라인이 거기서 끊겨 뒤쪽은 실행되지 않습니다.

C# code

// 미들웨어 체인
using System;

var cache = new Dictionary<int, string>();
string GetUser(int id) { if (!cache.ContainsKey(id)) cache[id] = "user"; return cache[id]; }
Console.WriteLine(GetUser(12));
알아두면 좋은 점

각 단계는 다음 단계를 부르기 전과 후에 모두 코드를 넣을 수 있어, 요청과 응답 양쪽을 한 곳에서 함께 다룰 수 있습니다.

자주 하는 실수

응답이 이미 클라이언트로 나가기 시작한 뒤에 헤더를 고치려 하면 예외가 납니다. 응답을 손보는 단계는 본문 쓰기보다 앞에 있어야 합니다.

13검증기 조합

값 하나를 받아 값을 돌려주는 함수를 표에 모아 둔 구조라, 검증 규칙 여러 개도 같은 방식으로 담을 수 있습니다. 규칙마다 통과 여부와 메시지를 돌려주게 바꾸고 전체를 순회하면 오류를 모아 보고할 수 있습니다. 첫 실패에서 멈추는 방식과 달리 모아 보고하면 사용자가 폼을 여러 번 다시 제출하지 않아도 됩니다.

C# code

// 검증기 조합
using System;

var strategies = new Dictionary<string, Func<int, int>> { ["percent"] = value => (int)(value * 0.9), ["flat"] = value => value - 5 };
Console.WriteLine(strategies["percent"](100));
알아두면 좋은 점

검증 규칙을 Func<T, string?> 목록으로 두면 null은 통과, 문자열은 오류 메시지라는 약속만으로 조합과 순회가 아주 단순해집니다.

자주 하는 실수

규칙을 순서 없이 모두 실행하면 null 검사보다 먼저 속성을 읽는 규칙이 예외를 던질 수 있습니다. 선행 조건이 있는 규칙은 앞 검사가 통과했을 때만 돌려야 합니다.

14포매터 파이프

GetUser는 id를 받아 문자열을 돌려주는 한 단계짜리 변환입니다. 출력 가공이 여러 단계라면 각 단계를 Func<string, string>으로 만들어 차례로 넘깁니다. 단계마다 새 문자열이 생기므로 단계가 많고 문자열이 길다면 StringBuilder 하나에 이어 쓰는 편이 할당이 적습니다.

C# code

// 포매터 파이프
using System;

var cache = new Dictionary<int, string>();
string GetUser(int id) { if (!cache.ContainsKey(id)) cache[id] = "user"; return cache[id]; }
Console.WriteLine(GetUser(14));
알아두면 좋은 점

여러 단계의 문자열 가공은 각 단계를 Func<string, string>으로 두고 차례로 접으면, 단계를 늘리는 일이 목록에 항목 하나를 넣는 일로 줄어듭니다.

자주 하는 실수

치환 단계에서 Regex 객체를 호출마다 새로 만들면 패턴 해석이 매번 반복됩니다. 패턴은 static readonly로 한 번만 만들어 두어야 합니다.

15페이지네이션

사전에서 항목 하나만 꺼내 쓰는 코드지만, 목록을 잘라 내보내는 일도 같은 방식으로 함수화할 수 있습니다. 페이지 번호와 크기를 받아 Skip과 Take를 적용하는 함수를 표에 담아 정책별로 나눕니다. 데이터베이스 질의에서는 정렬 기준 없이 건너뛰면 페이지마다 순서가 달라져 같은 행이 두 번 나올 수 있습니다.

C# code

// 페이지네이션
using System;

var strategies = new Dictionary<string, Func<int, int>> { ["percent"] = value => (int)(value * 0.9), ["flat"] = value => value - 5 };
Console.WriteLine(strategies["percent"](100));
알아두면 좋은 점

총 개수를 함께 내려 주려면 목록 조회와 개수 세기를 각각 질의해야 하는데, 개수 질의에는 정렬이 필요 없으므로 정렬 절을 빼면 부담이 줄어듭니다.

자주 하는 실수

건너뛴 행도 데이터베이스가 훑어야 하므로 뒤쪽 페이지일수록 느려집니다. 목록이 크다면 마지막으로 본 키를 기준으로 이어 읽는 방식이 낫습니다.

16데이터 매퍼

id를 키로 받아 대응하는 값을 돌려주는, 식별자와 객체를 잇는 최소 형태입니다. 실제 매퍼는 이 자리에서 결과 행을 객체로 옮겨 담습니다. 지금은 항상 값을 채우지만 실제로는 찾지 못하는 경우가 생기므로, nullable 참조 형식을 켜 두면 그 경로를 컴파일러가 경고로 짚어 줍니다.

C# code

// 데이터 매퍼
using System;

var cache = new Dictionary<int, string>();
string GetUser(int id) { if (!cache.ContainsKey(id)) cache[id] = "user"; return cache[id]; }
Console.WriteLine(GetUser(16));
알아두면 좋은 점

결과가 없을 수 있다면 반환 타입을 nullable로 선언해야, 호출부에서 null 검사를 빠뜨렸을 때 컴파일러가 경고로 짚어 줍니다.

자주 하는 실수

데이터베이스의 NULL을 값 타입 속성으로 그대로 매핑하면 변환에서 예외가 납니다. 컬럼이 null일 수 있다면 nullable 타입으로 받아야 합니다.

17저장소 패턴

동작을 키로 골라 부르는 이 구조는 저장소가 여러 조회 방법을 노출하는 모양과 닮았습니다. 호출부가 저장 매체를 모르게 하려면 이런 진입점 뒤로 질의를 감춥니다. 저장소 인터페이스에 IQueryable을 그대로 노출하면 호출부가 아무 조건이나 붙일 수 있어, 감추려던 세부가 다시 새어 나옵니다.

C# code

// 저장소 패턴
using System;

var strategies = new Dictionary<string, Func<int, int>> { ["percent"] = value => (int)(value * 0.9), ["flat"] = value => value - 5 };
Console.WriteLine(strategies["percent"](100));
알아두면 좋은 점

저장소 인터페이스에는 도메인이 실제로 필요로 하는 질의만 노출하는 편이 좋습니다. 그래야 나중에 구현을 바꿔도 호출부가 영향을 받지 않습니다.

자주 하는 실수

저장소마다 별도의 트랜잭션을 열면 여러 저장소를 함께 쓰는 작업이 부분적으로만 반영될 수 있습니다. 트랜잭션 경계는 저장소 바깥에서 잡아야 합니다.

18의존성 주입 컨테이너

키가 없으면 만들어 저장하고 이후에는 같은 것을 돌려주는 흐름은 컨테이너의 싱글턴 해석과 같습니다. 타입별 인스턴스를 한 번만 만들어 재사용해야 할 때 이 구조가 바탕이 됩니다. 요청마다 새로 만들어야 하는 대상을 이렇게 담아 두면, 스코프가 끝나 정리된 객체를 계속 붙잡는 문제가 생깁니다.

C# code

// 의존성 주입 컨테이너
using System;

var cache = new Dictionary<int, string>();
string GetUser(int id) { if (!cache.ContainsKey(id)) cache[id] = "user"; return cache[id]; }
Console.WriteLine(GetUser(18));
알아두면 좋은 점

등록은 구현 타입이 아니라 인터페이스로 요청하도록 두고 조립 코드는 시작 지점 한 곳에만 모아 두면, 의존 방향을 흐트러뜨리지 않고 유지하기 쉽습니다.

자주 하는 실수

필요할 때마다 컨테이너에서 직접 꺼내 쓰는 방식은 의존 관계를 코드 속에 숨겨, 무엇이 필요한 타입인지 생성자만 봐서는 알 수 없게 만듭니다.

19템플릿 메서드

전체 흐름은 사전에서 함수를 꺼내 호출하는 것으로 고정되어 있고, 달라지는 계산만 람다가 맡습니다. 절차는 같고 한두 단계만 다른 작업들이 모일 때 이 분리가 들어맞습니다. 추상 클래스로 같은 일을 하면 상속이 하나뿐이라 조합이 막히지만, 델리게이트나 인터페이스는 여러 개를 동시에 끼울 수 있습니다.

C# code

// 템플릿 메서드
using System;

var strategies = new Dictionary<string, Func<int, int>> { ["percent"] = value => (int)(value * 0.9), ["flat"] = value => value - 5 };
Console.WriteLine(strategies["percent"](100));
알아두면 좋은 점

달라지는 단계를 델리게이트로 받으면 상속 없이도 같은 절차를 재사용할 수 있고, 테스트에서 그 단계만 람다로 갈아 끼울 수 있습니다.

자주 하는 실수

추상 메서드로 뺀 단계를 기반 클래스 생성자에서 부르면 파생 클래스의 필드가 아직 초기화되기 전에 실행되어 null을 만나게 됩니다.

20파사드

호출부는 GetUser(20)만 알면 되고 사전 확인과 값 생성은 함수 안에 숨어 있습니다. 여러 하위 단계를 한 번의 호출로 감쌀 때 이렇게 좁은 진입점을 만듭니다. 진입점 하나에 계속 기능을 더하면 결국 내부 전부를 아는 타입이 되므로, 감출 범위를 처음에 정해 두는 편이 좋습니다.

C# code

// 파사드
using System;

var cache = new Dictionary<int, string>();
string GetUser(int id) { if (!cache.ContainsKey(id)) cache[id] = "user"; return cache[id]; }
Console.WriteLine(GetUser(20));
알아두면 좋은 점

파사드는 하위 타입을 감출 뿐 없애지는 않으므로, 특별한 사용이 필요한 호출부는 여전히 하위 API를 직접 쓸 수 있게 열어 두는 편이 좋습니다.

자주 하는 실수

파사드가 하위 단계의 예외를 그대로 흘려보내면 감춘 세부 구현이 예외 타입으로 드러납니다. 경계에서 의미 있는 타입으로 바꿔 던져야 합니다.

정리하며

  • 인터페이스와 구현체를 늘리기 전에 델리게이트 하나로 끝나는지 먼저 따져 봅니다
  • 감싸기 한 기법으로 캐시, 재시도, 로깅을 모두 만들 수 있으니 순서만 잘 정합니다
  • 싱글턴이 짧은 수명 의존성을 주입받으면 그 인스턴스가 앱 전체 수명 동안 붙잡힙니다
  • 이벤트는 구독 코드를 쓰는 자리에서 해제 시점을 함께 정해야 누수가 없습니다

더 깊이 들어가고 싶다면 C# 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.