[์„œ๋ก ]

์•ˆ๋…•?


์˜ค๋Š˜์€ C#์—์„œ ์ดˆ๊ฐ„๋‹จ ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ ์ฝ”๋“œ๋ฅผ ๊ณต์œ ํ•ด๋ณผ๊นŒ ํ•ด.

Unity ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ C#์„ ์‚ฌ์šฉํ•˜๋Š” ๊ณณ์ด๋ฉด ์–ด๋””๋“  ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•จ.

์ด๊ฒƒ์„ ์ด์šฉํ•˜๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค ๊ท€์ฐฎ๊ฒŒ ๋”ฐ๋กœ UI์— ๊ฐ’์„ ์„ค์ •ํ•˜๊ฑฐ๋‚˜ ํ˜ธ์ถœํ•ด์ฃผ๋Š” ๋ฒˆ๊ฑฐ๋กœ์›€์„ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ์–ด.

์ฆ‰, ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ”๋€Œ๋ฉด ๊ทธ์™€ ๊ด€๋ จ๋œ ์ด๋ฒคํŠธ๋“ค์ด ์ž๋™์œผ๋กœ ์ˆ˜ํ–‰๋˜๋Š”๊ฑฐ์ง€.


์˜ˆ๋ฅผ ๋“ค์–ด์„œ ๊ฒŒ์ž„ ์ค‘์— ์ ์„ ํ•ด์ณ์„œ Score(์ ์ˆ˜)๊ฐ€ ์˜ฌ๋ผ๊ฐˆ ๋•Œ ๋ณดํ†ต์€


Score += 10;

ScoreLabel.text = $"์ ์ˆ˜: {Score}";


๋ญ ์ด๋Ÿฐ์‹์œผ๋กœ ํ•˜์ž–์•„?

์ง€๊ธˆ์€ ํฐ ๋ฌธ์ œ ์—†์–ด๋ณด์ด์ง€๋งŒ ๋‚˜์ค‘์— ์ฝ”๋“œ๊ฐ€ ๊ธธ์–ด์ง€๊ณ  ๋ณต์žกํ•ด์ง€๋‹ค ๋ณด๋ฉด ๊ฒฐ๊ตญ์€ ๋ฐ์ดํ„ฐ๋Š” ๋ฐ์ดํ„ฐ๋Œ€๋กœ, UI๋Š” UI๋Œ€๋กœ ๊ด€๋ฆฌํ•ด์•ผ ํ•  ํ•„์š”์„ฑ์ด ์ƒ๊ธธ๊ฑฐ์•ผ.


๊ทธ๋Ÿผ ๊ทธ ๋•Œ ์ € ๋‘˜์„ ๋‚˜๋ˆ„๋‹ค๋ณด๋ฉด ์—ฌ๋Ÿฌ ์—๋กœ์‚ฌํ•ญ์ด ์ƒ๊ฒจ. Score๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” A Class์™€ UI๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” B Class๊ฐ€ ์žˆ๋‹ค๋ฉด, ์ด A Class๊ฐ€ B Class๋ฅผ ์ฐธ์กฐํ•ด์„œ


B b = somewhere.getB();

b.ScoreLabel.text = $"์ ์ˆ˜: {Score}";


๋ญ ์ด๋Ÿฐ์‹์œผ๋กœ ๋‚˜๋ˆ„๋Š” ์˜๋ฏธ๊ฐ€ ์—†์–ด์งˆ ์ˆ˜๋„ ์žˆ๋‹ค๋Š”๊ฑฐ์ง€. ๊ฒฐ๊ตญ ๋˜‘๊ฐ™์ด ๋ณต์žกํ•ด์ง€๋‹ˆ๊น.


์—ฌํŠผ ์ดˆ๊ธฐ์— ์„ค์ •๋งŒ ์ž˜ํ•ด ๋‘๋ฉด ์ € ๋‘ ์ฝ”๋“œ ์˜์—ญ๋“ค์ด ์„œ๋กœ ๊ฐ„์„ญํ•  ํ•„์š”๊ฐ€ ์—†์–ด์ง€๋Š”๊ฒŒ ๋ฐ”๋กœ ๋ฐ”์ธ๋”ฉ์„ ํ•˜๋Š” ๊ฐ€์žฅ ํฐ ์ด์œ ๊ฐ€ ์•„๋‹๊นŒ ์‹ถ์–ด.(๋ญ ๋ฐ”์ธ๋”ฉ ๋ง๊ณ ๋„ ์—ฌ๋Ÿฌ ๊ธฐ๋ฒ•๋“ค์ด ์žˆ์„ ์ˆ˜ ์žˆ๊ฒ ์ง€๋งŒ... )



[๋ณธ๋ก ]

๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์—ฌ๋Ÿฌ๊ฐ€์ง€๊ฐ€ ์žˆ๊ณ , Github์—๋„ ๊ด€๋ จ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์ด ๋งŽ์ด ์žˆ์–ด. ๋‚˜๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ”๋€Œ๋ฉด ๊ด€๋ จ๋œ ์ด๋ฒคํŠธ๋“ค์ด ๋™์ž‘ํ•˜๋Š” ๊ฐ„๋‹จํ•œ ๊ธฐ๋Šฅ๋งŒ ์›ํ–ˆ๊ธฐ์— ์ง์ ‘ ๊ตฌํ˜„ํ•ด๋ดค์–ด.

C#์—์„œ๋Š” ์ด๋ฅผ ์œ„ํ•ด ๋ฌธ๋ฒ•์ ์œผ๋กœ delegate ์™€ event ํ‚ค์›Œ๋“œ๋ฅผ ์ œ๊ณตํ•˜๊ณ , ๋‚˜๋Š” ๊ฑฐ๊ธฐ์— ์ข€ ๋” ์•ˆ์ •์„ฑ์„ ์ถ”๊ตฌํ•ด์„œ WeakReference Class๋„ ์‚ฌ์šฉํ–ˆ์–ด.

WeakReference๋Š” ์•ฝํ•œ ์ฐธ์กฐ๋ฅผ ๊ฐ€์ง€๊ธฐ ๋•Œ๋ฌธ์— GC์— ์˜ํ•ด ์ฐธ์กฐํ•˜๊ณ  ์žˆ๋Š” ๊ฐ์ฒด๊ฐ€ ํ•ด์ œ๋˜๋Š” ํŠน์„ฑ์ด ์žˆ์–ด์„œ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๋กœ๋ถ€ํ„ฐ ์•ˆ์ „ํ•  ์ˆ˜ ์žˆ์–ด.


์—ฌํŠผ ์•„๋ž˜ ๊ตฌํ˜„ ์ฝ”๋“œ๊ฐ€ ์žˆ์œผ๋‹ˆ๊น CCCV ํ•ด์„œ ์‚ฌ์šฉํ•˜๋ฉด ๋ผ.


using System;

using System.Collections.Generic;

using System.Runtime.CompilerServices;

using System.Collections.Immutable;


internal class TGDataBinder<A> : IDisposable, IComparable<TGDataBinder<A>>, IEquatable<TGDataBinder<A>> where A : IComparable<A> {

private sealed class WeakEventManager<T> {

// ๋“ฑ๋กํ•œ ์ฝœ๋ฐฑํ•จ์ˆ˜๊ฐ€ ์ €์žฅ๋˜๋Š” ๋ณ€์ˆ˜

private ImmutableList<WeakReference<Action<T>>> handlers = ImmutableList<WeakReference<Action<T>>>.Empty;


// lambda ํ•จ์ˆ˜๊ฐ€ ๋ฐ”๋กœ GC ๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ์žฅ์น˜

private readonly ConditionalWeakTable<object, List<object>> delegateKeepAlive = new();


public void AddHandler(Action<T> handler) {

if (handler == null)

return;


var weakReference = new WeakReference<Action<T>>(handler);

handlers = handlers.Add(weakReference);

if (handler.Target != null) {

delegateKeepAlive.GetOrCreateValue(handler.Target).Add(handler);

}

}


public void RemoveHandler(Action<T> handler) {

if (handler == null)

return;

handlers = handlers.RemoveAll(wr => !wr.TryGetTarget(out var target) || handler.Equals(target));

if (handler.Target != null && delegateKeepAlive.TryGetValue(handler.Target, out var weakReference)) {

weakReference.Remove(handler);

}

}


public void Raise(T data) {

foreach (var maybeHandler in handlers) {

if (maybeHandler.TryGetTarget(out var handler)) {

handler(data);

} else {

handlers = handlers.Remove(maybeHandler);

}

}

}


public void ClearHandlers() {

handlers.Clear();

delegateKeepAlive.Clear();

}

}


private A data;

private readonly WeakEventManager<A> weakEventManager;


// ์ด event ํ”„๋กœํผํ‹ฐ๋ฅผ ํ†ตํ•ด์„œ๋„ ์ฝœ๋ฐฑํ•จ์ˆ˜ ์ถ”๊ฐ€ or ์‚ญ์ œ ๊ฐ€๋Šฅ

public event Action<A> OnDataChanged {

add => weakEventManager.AddHandler(value);

remove => weakEventManager.RemoveHandler(value);

}


// ์‹ค์ œ ๊ด€๋ฆฌํ•  ๋ฐ์ดํ„ฐ ํ”„๋กœํผํ‹ฐ

public A Data {

get => data;

set {

data = value;

weakEventManager.Raise(data); // <- ์ด๋ถ€๋ถ„์ด ์‹ค์ œ ๋ฐ”์ธ๋”ฉ ๋˜๋Š” ์ฝ”๋“œ

}

}


public TGDataBinder() { weakEventManager = new(); }


// ์›ํ•˜๋Š” ์ฝœ๋ฐฑ ํ•จ์ˆ˜๋ฅผ ๋“ฑ๋ก

public void AddHandler(Action<A> handler) {

OnDataChanged += handler;

}


// ์ˆ˜๋™์œผ๋กœ ํ•ด์ œํ•  ๋•Œ ํ˜ธ์ถœ

public void Dispose() {

weakEventManager.ClearHandlers();

}


public int CompareTo(TGDataBinder<A> obj) => Data.CompareTo(obj.Data);

public bool Equals(TGDataBinder<A> other) => Data.Equals(other.Data);


public override int GetHashCode() => Data.GetHashCode();


public override bool Equals(object obj) {

if (!(obj is TGDataBinder<A>)) {

return false;

} else {

return Equals((TGDataBinder<A>)obj);

}

}


public static bool operator >(TGDataBinder<A> n1, TGDataBinder<A> n2) => n1.CompareTo(n2) > 0;

public static bool operator <(TGDataBinder<A> n1, TGDataBinder<A> n2) => n1.CompareTo(n2) < 0;

public static bool operator ==(TGDataBinder<A> n1, TGDataBinder<A> n2) => n1.Equals(n2);

public static bool operator !=(TGDataBinder<A> n1, TGDataBinder<A> n2) => !(n1 == n2);

}

๋นจ๊ฐ„์ƒ‰ ๊ธ€์ž๋Š” ์•„๋งˆ ๊ธฐ๋ณธ ํŒจํ‚ค์ง€์—์„œ ์ง€์› ์•ˆํ•  ์ˆ˜ ์žˆ์œผ๋‹ˆ ๊ธฐ๋ณธ List ์ž๋ฃŒ๊ตฌ์กฐ๋กœ ๋ฐ”๊พธ๊ฑฐ๋‚˜ ์•„๋‹ˆ๋ฉด Nuget ์„ ํ†ตํ•ด์„œ ์„ค์น˜ํ•˜๋ฉด๋ผ.

ํŒŒ๋ž€์ƒ‰ ๊ธ€์ž๋Š” ์„ ํƒ์‚ฌํ•ญ์ด๋ผ ์›ํ•˜์ง€ ์•Š์œผ๋ฉด ์‚ญ์ œํ•ด๋„ ๋ฌด๋ฐฉํ•ด.

์ง„ํ•œ ๊ธ€์ž๋Š” ์‹ค์ œ ์‚ฌ์šฉ๋˜๋Š” ์ƒ์„ฑ์ž, ํ”„๋กœํผํ‹ฐ, ๋ฉ”์†Œ๋“œ์•ผ. ํด๋ž˜์Šค ์ด๋ฆ„๋„ ์›ํ•˜๋Š” ๊ฑธ๋กœ ๋ฐ”๊ฟ”์„œ ์‚ฌ์šฉํ•˜๋ฉด๋ผ.



์ด์ œ ์‚ฌ์šฉ์˜ˆ๋ฅผ ๋ณด์ž.


24b0d121e09c28a8699fe8b115ef0469943df0b9

์ด๋ ‡๊ฒŒ PlayerRepositoryํด๋ž˜์Šค์— ๋ฉค๋ฒ„๋ณ€์ˆ˜๋กœ ์„ ์–ธ ๋ฐ ์ƒ์„ฑํ•ด์ฃผ๊ณ ,


24b0d121e09c28a8699fe8b115ef046f584b9f9e

UIManagerํด๋ž˜์Šค์—์„œ๋Š” UI Text ๋ณ€๊ฒฝ์šฉ ๋ฉ”์†Œ๋“œ๋ฅผ ์ž‘์„ฑํ•ด์ฃผ๊ณ .


24b0d121e09c28a8699fe8b115ef046f5d499a9a

GameManagerํด๋ž˜์Šค์—์„œ PlayerRepository์™€ UIManager๋ฅผ ์ฐธ์กฐํ•˜๊ณ  PlayerRepository์˜ ๊ฐ ๋ณ€์ˆ˜ ๋‚ด๋ถ€์˜ ๋ฐ์ดํ„ฐ๋“ค์ด ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค ํ˜ธ์ถœํ•ด์ค„ ์ฝœ๋ฐฑํ•จ์ˆ˜๋“ค์„ UIManager๋กœ๋ถ€ํ„ฐ ๊ฐ€์ ธ์™€์„œ ์ง€์ •ํ•ด์ฃผ์—ˆ์–ด.


24b0d121e09c28a8699fe8b115ef0464d08ce5

PlayerRepository๋ฅผ ์ฐธ์กฐํ•˜๊ณ ์žˆ๋Š” GameManagerํด๋ž˜์Šค๋‚˜ ํ˜น์€ ๋‹ค๋ฅธ ํด๋ž˜์Šค์—์„œ AddScore๊ฐ™์€ ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด, ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•จ๊ณผ ๋™์‹œ์— ๋‚ด๋ถ€์ ์œผ๋กœ ๋“ฑ๋ก๋œ ์ฝœ๋ฐฑํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ด์„œ UI๋ฅผ ๋ณ€๊ฒฝํ• ๊ฑฐ์•ผ. ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ–ˆ๋‹ค๊ณ  ํ•ด์„œ UIManager์˜ SetScore() ๋ฉ”์†Œ๋“œ๋ฅผ ๋”ฐ๋กœ ํ˜ธ์ถœํ•ด์ค„ ํ•„์š”๊ฐ€ ์—†๋‹จ ์†Œ๋ฆฌ์ง€.


์ฆ‰, PlayerRepository์™€ UIManager๋Š” ์„œ๋กœ์˜ ๊ด€๊ณ„๋กœ๋ถ€ํ„ฐ ๋ถ„๋ฆฌ๋˜์—ˆ๋‹ค๊ณ  ๋ณผ ์ˆ˜ ์žˆ์ง€.

PlayerRepository ์™€ ๊ทธ ๋‚ด๋ถ€์— ์žˆ๋Š” TGDataBinder ๋ชจ๋‘ UIManager๊ฐ€ ๋ฌด์Šจ์ผ์„ ํ•˜๋Š”์ง€ ์•Œ ํ•„์š”๊ฐ€ ์—†๋‹ค๋Š” ์†Œ๋ฆฌ์•ผ. ๊ทธ์ € ์ž์‹ ํ•œํ…Œ ๋“ฑ๋ก๋œ ์ฝœ๋ฐฑํ•จ์ˆ˜๋งŒ ํ˜ธ์ถœํ•ด์ฃผ๋ฉด๋˜๋‹ˆ๊น. ๋˜ํ•œ UIManager๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ๋“ค์ด ์–ด๋–ค์‹์œผ๋กœ ๊ด€๋ฆฌ๋˜๋Š”์ง€ ์•Œ ํ•„์š” ์—†๊ณ .


๊ทธ๋ฆฌ๊ณ  ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๊ฒ€์‚ฌ๋ฅผ ์œ„ํ•ด ํ”„๋กœํŒŒ์ผ๋Ÿฌ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด๋ดค์–ด.


24b0d121e09c28a8699fe8b115ef046c66f1294f

์œ„ ์Šค์ƒท์€ ์œ ๋‹ˆํ‹ฐ Memory Profiler๋กœ TGDataBinder์— ๊ฐ•์ œ ๋ถ€ํ•˜๋ฅผ ์ฃผ๊ณ  Dispose ์‹œ์ผฐ์„ ๋•Œ ๋‚ด๋ถ€์ ์—์„œ ์ œ๋Œ€๋กœ ๋ฉ”๋ชจ๋ฆฌ ํ•ด์ œ๊ฐ€ ๋˜๋Š”์ง€ ๊ฐ๊ฐ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ์บก์ณ ํ•œ ํ›„ ๋น„๊ต ํ…Œ์ŠคํŠธ ํ•ด๋ณธ ๊ฒฐ๊ณผ์•ผ.(A๋Š” 100001๊ฐœ์˜€์„ ๋•Œ ์บก์ณํ•œ ๋ฉ”๋ชจ๋ฆฌ๊ณ , B๋Š” Dispose ํ›„ ์บก์ณํ•œ ๋ฉ”๋ชจ๋ฆฌ์•ผ)

์ƒ์„ฑ๋œ ์ฝœ๋ฐฑํ•จ์ˆ˜๊ฐ€ ์ด 100001์ธ๋ฐ ์ด๊ฑด TGDataBinder ์ƒ์„ฑํ•˜๋ฉด์„œ ์ตœ์ดˆ ์ฝœ๋ฐฑํ•จ์ˆ˜ 1๊ฐœ ์ถ”๊ฐ€ํ•˜๊ณ , ์ดํ›„ 10๋งŒ๊ฐœ๋ฅผ (ํ…Œ์ŠคํŠธํ•˜๊ณ ์ž) ์ผ๋ถ€๋Ÿฌ ๋” ์ถ”๊ฐ€ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ ‡๊ฒŒ ํ‘œ์‹œ๋œ๊ฑฐ์•ผ.

์ฆ‰, ์ œ๋Œ€๋กœ ๋™์ž‘ํ•œ๋‹ค๋Š” ๊ฑธ ์•Œ ์ˆ˜ ์žˆ์ง€. DI์—์„œ ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ๋ฅผ ํ•˜๋ฉด Release๋‚˜ Dispose ๋˜๋Š” ์‹œ์ ์— ๊ฐ™์ด Dispose ๋ฉ”์†Œ๋“œ ํ˜ธ์ถœํ•ด์ฃผ๋ฉด ๋˜๊ณ , ๊ตณ์ด ๊ด€๋ฆฌ ์•ˆํ•˜๋”๋ผ๋„ GC๊ฐ€ ํ˜ธ์ถœํ•˜๋Š” ์‹œ์ ์— ํ•ด๋‹น DataBinder๋„ ๊ฐ™์ด ์ •๋ฆฌ๋ ๊ฑฐ์•ผ.



[๊ฒฐ๋ก ]

์‚ฌ์‹ค ๋‚ด๊ฐ€ ๊ฐ„๋‹จํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๋ ค๊ณ  ๋งŒ๋“ ๊ฑฐ๋‹ค ๋ณด๋‹ˆ ํ•œ๊ณ„๋„ ์กด์žฌํ•ด.


1. ๊ฐ„๋‹จํ•œ ๋™์ž‘๊ณผ ๋ฉ”๋ชจ๋ฆฌ ํ…Œ์ŠคํŠธ๋งŒ ํ•ด๋ดค๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค๋ฅธ ๋ฒ„๊ทธ๊ฐ€ ๋ฐœ์ƒํ•  ์œ„ํ—˜์ด ์žˆ์Œ.

2. int, float, string ๊ฐ™์€ Primitive Type๋“ค ์œ„์ฃผ๋กœ ์‚ฌ์šฉํ•  ๋ชฉ์ ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋งŒ์•ฝ Data Type์ด Class๋‚˜ Struct ์ธ ๊ฒฝ์šฐ๋Š” ๊ณ ๋ คํ•ด๋ด์•ผ ํ•จ.

3. ์„œ๋ก ์—์„œ ๋งํ–ˆ๋‹ค์‹œํ”ผ Github๋‚˜ ์˜คํ”ˆ์†Œ์Šค๋กœ ๋” ์ข‹์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์ด ๋งŽ์ด ์กด์žฌํ•˜๋‹ˆ๊น ๋” ๋งŽ์€ ๊ธฐ๋Šฅ์ด๋‚˜ ์•ˆ์ •์„ฑ์„ ์›ํ•˜๋ฉด ์ฐพ์•„๋ณด๋Š” ๊ฒƒ๋„ ๋‚˜์˜์ง€ ์•Š์Œ.


๋‚˜๋Š” ์™ธ๋ถ€ ์ž๋ฃŒ๊ตฌ์กฐ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์›๋ž˜ ์ฝ”๋“œ๋Š” ์ข€ ๋งŽ์ด ๋‹ฌ๋ž๊ธฐ ๋•Œ๋ฌธ์— ์ตœ๋Œ€ํ•œ ์˜์กด์„ฑ์—†์ด ๋ฒ”์šฉ์„ฑ์žˆ๊ฒŒ ์ˆ˜์ •ํ•œ๋‹ค๊ณ  ์‹œ๊ฐ„์ด ๋งŽ์ด ๊ฑธ๋ ธ์–ด.

๊ทธ๋ฆฌ๊ณ  ๊ธ€ ์“ฐ๋ฉด์„œ ๊ดœํžˆ ๊ธ€์ผ๋‚˜? ์ƒ๊ฐ๋„ ๋“ค๊ณ ... ์•„๊นŒ ์“ฐ๋‹ค๊ฐ€ ํ•œ๋ฒˆ ๋‚ ๋ ค๋จน์–ด์„œ๋ฆฌ ใ… .ใ…  ์–ด์ฐจํ”ผ ๋” ์ข‹์€ lib ๋“ค๋„ ๋งŽ์€๋ฐ... ํ•˜๋ฉด์„œ.


์งˆ๋ฌธ์žˆ์œผ๋ฉด ๋Œ“๊ธ€์ฃผ๊ณ , ์–ด์จŒ๋“  ๋„์›€์ด ๋˜์—ˆ๊ธธ ๋ฐ”๋ผ!



๋!



source:

https://www.meziantou.net/weak-events-in-csharp.htm