1. 소개
프로그래머라면 누구나 알지: 함수(또는 메서드)는 사실상 모든 앱의 "일꾼"이야. 코드 구조화, 로직 재사용, 그리고 끝없는 복붙에서 벗어나게 도와주지. 지금까지 네가 써온 함수들은 전부 클래스나 구조체 본문에서 "이웃"처럼 있었을 거야.
근데 만약에, 네가 큰 함수 안에 그렇게 크지 않은 로직이 있는데, 그걸 살짝 분리하고 싶고, 프로젝트 전체에 공개하고 싶지 않다면? 예를 들어, 메서드 안에서만 쓰는 보조 계산 같은 거 말이야.
이럴 때 로컬 함수가 딱이야.
로컬 함수는 다른 함수(메서드, 생성자, 프로퍼티, 심지어 또 다른 로컬 함수) 안에 선언된 함수야. 이런 로컬 함수는 "부모"한테만 보이고, 그 외에는 아무도 못 봐.
로컬 함수 문법
void MainMethod()
{
Console.WriteLine("메인 메서드 시작."); // 메인 메서드 시작.
int result = InnerSum(5, 7); // 로컬 함수 InnerSum 호출 (내부 합계)
Console.WriteLine($"로컬 함수 결과: {result}"); // 로컬 함수 결과: {result}
// 로컬 함수는 MainMethod 안에서 바로 선언돼
int InnerSum(int a, int b) // 두 숫자 더하는 로컬 함수 { return a + b; // a와 b의 합을 반환 }
}
이 예제에서 InnerSum 함수는 바로 MainMethod 안에 선언돼 있어. 메인 메서드 밖에서는 절대 못 써! 비밀 지령서처럼, 단둘이 있을 때만 공유할 수 있는 거지.
2. 로컬 함수 선언과 사용 위치
기본 규칙
- 로컬 함수는 메서드, 생성자, 프로퍼티, 연산자, 심지어 또 다른 로컬 함수 안에서도 선언할 수 있어!
- 로컬 함수는 자기 영역(scope)에서만 보여.
- 로컬 함수는 부모 함수에서 선언 전이든 후든 호출할 수 있어(C#이 미리 다 분석해줌).
예시: 평균 점수 계산
// 평균 점수 출력하는 메인 함수
void PrintAverageScore(int[] scores)
{
if (scores.Length == 0)
{
Console.WriteLine("계산할 점수가 없어."); // 계산할 점수가 없어.
return;
}
double average = CalculateAverage(scores);
Console.WriteLine($"평균 점수: {average:0.00}"); // 평균 점수
// 평균 계산하는 로컬 함수
double CalculateAverage(int[] arr) { int sum = 0; foreach (var score in arr) sum += score; return (double)sum / arr.Length; // 평균 반환 }
}
// 함수 호출:
int[] myScores = { 5, 4, 3, 4, 5 };
PrintAverageScore(myScores); // 출력: 평균 점수: 4.20
편하지? PrintAverageScore 밖에서는 내부 로직, 즉 로컬 함수 CalculateAverage를 쓸 수 없어. 이런 방식 덕분에 코드를 깔끔하게 정리할 수 있고, 보조 메서드로 글로벌 네임스페이스를 어지럽히지 않아도 돼.
3. 로컬 함수와 영역(scope)
로컬 함수의 핵심은 "부모" 메서드 안에서만 보인다는 거야. 이런 동작 덕분에 클래스가 한 번만 쓰는 작은 메서드로 어질러지는 걸 막을 수 있어.
+-----------------+
| 클래스 메서드 |
| void Foo() |
| |
| +-----------+ |
| | 로컬 함수 | |
| +-----------+ |
+-----------------+
로컬 함수는 오직 선언된 메서드 안에서만 존재해. 밖에서 호출하려고 하면, 비밀 전화선이 없는데 전화하려는 거랑 똑같아.
만약 실수로 부모 메서드 밖에서 로컬 함수를 호출하면, 컴파일 에러가 나: '...' 이름이 현재 컨텍스트에 존재하지 않아.
로컬 함수는 부모의 변수를 볼 수 있어
로컬 함수는 부모 메서드의 모든 변수에 접근할 수 있어 — 이게 바로 로컬 함수의 슈퍼파워지!
void ShowStudent(string name, int age)
{
string message = BuildMessage();
Console.WriteLine(message);
// 로컬 함수가 "name"과 "age" 변수를 사용함
string BuildMessage() { return $"이름: {
name}, 나이: {
age}"; }
}
이렇게 하면 코드가 더 짧아지고, "시끄럽지"도 않아. 매번 메서드에 파라미터 열 개씩 넘길 필요 없어 — 로컬 함수는 부모에 있는 모든 걸 "볼 수" 있으니까.
4. 반복문과 조건문 안의 로컬 함수
로컬 함수는 반복문이나 조건문 안, 어디서든 원하는 만큼 중첩해서 선언할 수 있어:
void ProcessNumbers(int[] numbers)
{
foreach (int number in numbers)
{
if (number % 2 == 0)
{
Console.WriteLine($"{number} — 짝수");
PrintEvenMessage();
}
else
{
Console.WriteLine($"{number} — 홀수");
}
void PrintEvenMessage() { Console.WriteLine("이건 짝수야, 축하해!"); }
}
}
근데 이런 식으로 너무 많이 쓰면 코드가 금방 읽기 힘들어질 수 있어. 로컬 함수가 너무 많거나 길어지면 특히 그렇지. 남용은 금물! 그래도 이런 기능이 있다는 건 알아두면 좋아.
로컬 함수 vs 일반 private 메서드
| 기준 | 로컬 함수 | 클래스의 private 메서드 |
|---|---|---|
| 영역(scope) | 부모 함수만 | 클래스 전체 |
| 부모 변수 접근 | 가능 | 불가(파라미터로만) |
| 가독성 향상 | 높음 | 보통 |
| 재사용성 | 낮음 | 높음 |
| 테스트 용이성 | 테스트 어렵다 | 테스트 쉽다 |
5. 흔한 실수와 구현 특징
실수 1: 로컬 함수 이름 오타 또는 선언 누락.
로컬 함수를 잘못된 이름으로 호출하거나 선언을 빼먹으면, 컴파일러가 CS0103 또는 CS0128 에러를 내고, 코드는 컴파일되지 않아.
실수 2: 캡처된 변수(closure) 변경.
로컬 함수가 외부 메서드의 변수를 바꾸면, 예상과 전혀 다른 결과가 나올 수 있어 — 캡처된 변수는 복사본이거나 참조일 수 있고, 바뀌면 로직에 영향이 가.
실수 3: IDE의 private 메서드 로컬 함수 변환 제안 무시.
클래스에 쓸데없는 private 메서드를 남겨두면, IDE(예: Rider)가 로컬 함수로 바꾸라고 해도 무시하면 코드에 "쓰레기"가 쌓이고 가독성이 떨어져.
실수 4: 로컬 함수 중첩이 너무 깊음.
함수가 너무 많이 중첩되면 이해와 디버깅이 힘들어져. 간결함이 유지보수의 핵심이야.
GO TO FULL VERSION