static int Run()
{
try { return 7; }
finally { Console.Write("F"); }
}
Console.Write(Run());
return은 값을 호출한 쪽에 돌려주는 명령이고, Console.Write()는 콘솔에 문자를 출력하는 명령이야. 둘은 완전히 다른 동작이야.
1. 실제 코드 실행 순서
static int Run()
{
try { return 7; }
finally { Console.Write("F"); }
}
Console.Write(Run());
이 코드를 실행 순서대로 따라가 보자.
-
Console.Write(Run())을 만난다.
Console.Write()를 실행하려면 인자로 전달할 Run()의 반환값을 먼저 알아야 하므로 Run()부터 실행한다.
-
try { return 7; }을 실행한다.
반환할 값 7을 결정한다. 하지만 아직 Run() 함수가 완전히 종료된 것은 아니다.
-
finally 블록을 실행한다.
Console.Write("F");따라서 콘솔에 F가 먼저 출력된다.
-
Run()이 종료되면서 7을 반환한다.
이제 바깥의 Console.Write(Run())은 7이라는 값을 전달받는다.
-
바깥쪽 Console.Write(7)을 실행한다.
기존에 출력된 F 뒤에 7이 이어서 출력된다.
최종 출력
F7
3. 그러면 언제 7F가 출력될까?
다음과 같이 try 내부에서 직접 출력한다면 달라져.
static void Run()
{
try
{
Console.Write("7");
return;
}
finally
{
Console.Write("F");
}
}
Run();
이 경우에는 try 안에서 실제로 7을 출력했기 때문에 결과가 다음과 같아.
7F
차이를 비교하면:
| return 7; | 호출한 함수에 7을 돌려준다. 콘솔 출력은 하지 않는다. |
| Console.Write(7); | 콘솔에 7을 출력한다. |
| finally | try에서 return을 만나더라도 함수가 정상적으로 반환되기 전에 실행된다. |
6번. C# 값 복사
객관식 · 4점
AI 사용 불가 · 단일 정답
다음 코드를 실행했을 때 출력되는 값은?
var a = new Counter { Value = 1 };
var b = a;
b.Value = 5;
Console.WriteLine($"{a.Value},{b.Value}");
public struct Counter
{
public int Value;
}
A. 1,1
B. 1,5
C. 5,5
1,5 -> B
7번. 게임 시뮬레이션 개념
주관식 · 6점
AI 사용 불가
같은 초기 상태와 같은 순서의 입력을 주었을 때 항상 같은 최종 결과가 나오는 성질을 무엇이라 하는가?
한글 세 글자로 쓰고, 그 성질을 만족하는 프로그램의 결과가 반드시 게임 규칙상 올바르지는 않은 이유를 한 문장으로 설명하시오.
개념: 결정성
설명: 결정적인 프로그램도 매번 동일한 잘못된 결과를 낼 수 있다는 점이 핵심.
리스폰 후 새 몬스터가 과거의 예약 피해를 받는 문제가 결정성(Determinism)과 정확성(Correctness)의 차이를 보여주는 좋은 사례야.
1. 결정성이란 정확히 무엇인가?
결정적인 프로그램은 동일한 초기 상태와 동일한 순서의 입력이 주어지면 항상 같은 결과를 만들어야 해.
예를 들어 전투 시뮬레이터에 다음 조건을 입력했다고 해보자.
초기 상태
Player HP = 100
Monster HP = 100
입력
T1: Player가 Monster에게 30 damage
T2: Monster가 Player에게 20 damage
이걸 100번 실행했을 때 결과가 매번 다음과 같다면:
Player HP = 80
Monster HP = 70
이 프로그램은 해당 조건에서 결정적인 동작을 보이는 거야.
즉, 결정성은 다음 관계를 의미해.
-
\(S\): 초기 상태
-
\(I\): 동일한 순서의 입력
-
\(O\): 실행 결과
동일한 \(S\)와 \(I\)를 사용하면 항상 동일한 \(O\)가 나와야 한다는 뜻이지.
하지만 여기에는 게임 규칙에 맞는 결과여야 한다는 조건이 없어.
바로 이게 핵심이야.
2. 결정적인데 잘못된 프로그램이란?
우리가 풀었던 Goblin 리스폰 문제를 생각해 보자.
정상적인 게임 규칙은 다음과 같았어.
제거된 엔티티에 예약된 공격은 해당 엔티티가 제거된 이후 새로 생성된 엔티티에 영향을 주면 안 된다.
이제 기존 버그가 남아 있는 프로그램을 실행해 보자.
동일한 시나리오를 세 번 실행
입력 조건
T0: Goblin-A 생성 (E0, HP 100)
T0: A에게 3틱 뒤 35 피해 예약
T1: Goblin-A 제거
T1: Goblin-B 생성 (E0 재사용)
T2: Tick
T3: Tick
기존 버그가 있는 프로그램의 실행 결과
| 1회차 | Goblin-B HP = 65 |
| 2회차 | Goblin-B HP = 65 |
| 3회차 | Goblin-B HP = 65 |
결정성 만족: 매번 결과가 같음
정확성 위반: 정상적인 HP는 100이어야 함
여기서 주목할 점은 프로그램이 무작위로 잘못 작동한 게 아니라는 거야.
매번 똑같은 규칙을 따라서 일관되게 잘못 작동하고 있었어.
왜냐하면 내부 코드가 매번 다음과 같이 실행됐으니까.
과거의 예약 피해 대상 = E0
↓
Tick에서 Registry.Find(E0)
↓
현재 E0를 사용하는 Goblin-B 반환
↓
35 damage 적용
↓
HP 65
이 실행 흐름 자체는 매번 동일해.
즉, 프로그램은 결정적이지만 엔티티 생명주기 불변식을 위반한 거지.
우리가 Generation을 도입한 것은 단순히 결과를 일정하게 만들려던 게 아니라 잘못된 게임 규칙 적용을 수정하기 위해서였어.
3. 결정성과 정확성은 별도로 검증해야 한다
두 개를 표로 비교하면 더 명확해.
| 결정성 | 정확성 | |
| 핵심 질문 | 결과가 항상 같은가? | 결과가 요구사항에 맞는가? |
| 판단 기준 | 동일 조건에서 결과의 일관성 | 명세와 불변식 충족 |
| 대표 테스트 | 동일 입력 반복 실행 후 결과 비교 | 실제 결과와 기대 결과 비교 |
| 버그 예시 | 동일 입력인데 HP가 65 또는 100으로 달라짐 | 매번 HP가 65지만 정답은 100임 |
둘을 조합하면 다음 네 가지 상황도 가능해.
| 정확함 | 잘못됨 | |
| 결정적 | 매번 올바른 결과 | 매번 똑같이 잘못된 결과 |
| 비결정적 | 허용된 범위에서 매번 달라도 규칙을 충족하는 결과 | 실행할 때마다 달라지고 게임 규칙도 위반하는 결과 |
참고로 게임의 모든 시스템이 반드시 결정적이어야 하는 건 아니야. 예를 들어 시각 효과의 무작위 파티클 위치처럼 결과가 조금씩 달라도 허용되는 시스템이 있거든.
반대로 서버 전투 시뮬레이션이나 Replay 시스템에서는 결정성이 매우 중요한 설계 요구사항이 될 수 있어.
문제: 결정성이란 무엇이며, 결정성을 만족해도 결과가 올바르다고 보장할 수 없는 이유는?
개념: 결정성
결정성은 동일한 초기 상태와 동일한 순서의 입력에 대해 항상 동일한 결과를 생성하는 성질이다. 하지만 결정성은 결과의 일관성만 보장하며, 잘못된 로직으로 매번 동일한 오답이 생성될 수 있으므로 게임 규칙상 정확성을 보장하지는 않는다.
결정성은 결과의 재현 가능성이고, 정확성은 결과의 올바름이다. 재현 가능한 버그도 엄연히 버그다.
8번. C# 콜백과 Null 조건부 연산자
객관식 · 4점
AI 사용 불가 · 단일 정답
다음 코드를 실행했을 때 콘솔에 출력되는 결과는?
Action<string>? sink = null;
sink?.Invoke("A");
sink = text => Console.Write(text);
sink?.Invoke("B");
A. AB
B. A
C. B
D. nullB
1. 코드를 한 줄씩 해석해 보자
Action<string>? sink = null;
뜻은 다음과 같아.
-
Action<string>: 문자열 하나를 매개변수로 받고 반환값이 없는 함수를 담을 수 있음.
-
?: null일 수도 있음.
- sink = null: 현재는 연결된 함수가 없음.따라서 아래 코드는 실행할 함수가 없어서 아무 일도 일어나지 않아.여기서 ?.는 sink가 null이 아닐 때만 Invoke()를 실행한다는 뜻이야.2. 네가 궁금해한 핵심 부분이것은 람다식으로 만든 함수를 sink에 할당하는 코드야. 풀어쓰면 대략 이런 형태와 같아.즉, 아래 람다에서:text는 아직 어떤 특정 문자열이 들어 있는 상태가 아니야.현재 상태는 이렇게 변해.3. 그다음 Invoke를 만나면?이제 연결되어 있는 함수를 실제로 호출하는 거야.
따라서 전체 코드는 다음 순서로 실행되는 거지.이번 문법에서 기억할 세 가지네가 말한 표현을 조금만 다듬으면:참고로 이런 형태는 게임 프로그래밍에서도 로그 출력, UI 알림, 이벤트 발생 시 외부에 결과를 전달하는 콜백 등에 자주 활용돼.
실제 실행 흐름
sink?.Invoke("B") │ │ "B" 전달 ▼ text => Console.Write(text) │ │ text = "B" ▼ Console.Write("B") │ ▼ B 출력 -
sink에는 문자열을 받아 출력하는 함수가 등록되고, Invoke("B")를 호출하는 순간 B가 text에 전달되어 Console.Write(text)가 실행된다.
-
Action<string>? sink = null; // 1. 함수를 담을 변수 선언 sink = text => Console.Write(text); // 2. 실행할 함수 연결 sink?.Invoke("B"); // 3. 연결된 함수에 "B"를 전달해 실행 - 실행 코드실제 동작콘솔 출력
sink = null 함수가 연결되지 않은 상태 없음 sink?.Invoke("A") sink가 null이므로 호출하지 않음 없음 sink = text => Console.Write(text) sink에 함수를 연결함 없음 sink?.Invoke("B") 연결된 함수에 B를 전달해 호출함 B - Invoke("B")의 "B"가 람다식의 text 매개변수로 전달돼.
-
sink?.Invoke("B"); - 나중에 함수가 호출되면 전달받을 매개변수의 이름이야.
-
text => Console.Write(text) -
void PrintText(string text) { Console.Write(text); } sink = PrintText; - 이것은 람다식으로 만든 함수를 sink에 할당하는 코드야.
- 풀어쓰면 대략 이런 형태와 같아.
-
sink = text => Console.Write(text); -
sink?.Invoke("A");