1. 소개
실생활에서 많은 행동들은 마치 다기능 스위스 나이프 같아: 똑같은 명령이지만 여러 도구 세트로 동작할 수 있지. 예를 들어, ATM을 생각해봐: 카드를 넣으면 ATM이 비밀번호를 물어보고, 전화번호를 입력하면 SMS로 온 인증코드를 기다려. 행동은 하나 — "사용자 확인", 하지만 방식은 다르지.
프로그래밍에서도 비슷한 상황을 자주 만나: 뭔가 한 가지 작업을 해야 하는데, 데이터 타입이나 파라미터 개수가 다를 수 있어. 예를 들어, 우리 메서드는 사람이나 동물 모두에게 인사를 해야 하거나, 두 개, 세 개, 아니면 열 개의 정수를 더해야 할 수도 있지.
물론, 메서드 이름을 다르게 지을 수도 있어: SumTwo, SumThree, SumArray 이런 식으로. 근데 개발자들은 귀찮은 걸 싫어해 (괜히 "귀찮음이 진보의 원동력"이라는 말이 있는 게 아니지). 게다가 이렇게 하면 코드가 덜 읽기 쉬워져.
메서드 오버로딩
메서드 오버로딩은 같은 이름의 메서드가 서로 다른 파라미터 세트로 동작하게 하는 방법이야. 메서드 이름은 그대로 두고 파라미터만 다르게 하는 거지. 이건 다형성(polymorphism)의 한 형태지만, 상속이랑은 관련 없어.
오버로딩이란 한 클래스(혹은 구조체) 안에서 같은 이름의 여러 메서드를 만들 수 있게 해주는 거야. 단, 파라미터의 타입, 개수, 순서가 달라야 해.
메서드 시그니처
시그니처란 C#에서 메서드 이름 + 파라미터 타입(들) + 순서를 말해. 메서드의 반환 타입은 시그니처에 포함되지 않아! 이거 때문에 종종 예상치 못한 에러가 나기도 해 (조금 뒤에 설명할게).
2. 오버로딩 실전: 간단한 예제
이제 Greeter라는 클래스를 만들어서, 이름만으로, 이름과 나이로, 혹은 아예 파라미터 없이 유저를 다르게 인사해보자.
public class Greeter
{
// 파라미터 없는 인사
public void Greet()
{
Console.WriteLine("안녕, 세상!");
}
// 이름으로 인사
public void Greet(string name)
{
Console.WriteLine($"안녕, {name}!");
}
// 이름과 나이로 인사
public void Greet(string name, int age)
{
Console.WriteLine($"안녕, {name}! 벌써 {age}살이야? 멋지다!");
}
}
이제 이 메서드들 중 아무거나 호출하면, C# 컴파일러가 파라미터 개수와 타입을 보고 알아서 맞는 버전을 골라줄 거야.
var greeter = new Greeter();
greeter.Greet(); // 안녕, 세상!
greeter.Greet("아냐"); // 안녕, 아냐!
greeter.Greet("표트르", 23); // 안녕, 표트르! 벌써 23살이야? 멋지다!
3. 파라미터 타입과 개수로 구분하기
오버로딩은 메서드가 다음과 같이 다를 때 동작해:
- 파라미터 개수,
- 파라미터 중 하나 이상의 타입,
- 파라미터 타입의 순서 (이건 좀 조심해야 해).
이번엔 나이만 받는 오버로딩을 추가해보자:
public void Greet(int age)
{
Console.WriteLine($"이 나이, 멋지다! ({age}살)");
}
이제 호출해보면:
greeter.Greet(10); // 이 나이, 멋지다! (10살)
중요: 메서드가 반환 타입만 다르고 파라미터가 같으면 오버로딩 불가야. 예를 들어, 이런 코드는 에러가 나:
// 컴파일 에러!
public int Foo(string s) { ... }
public double Foo(string s) { ... }
컴파일러가 이렇게 말할 거야: "이미 Foo(string) 메서드가 있는데, 좀 더 복잡하게 해봐!"
4. 오버로딩과 C# 표준 라이브러리
오버로딩은 우리가 만든 Greet만 있는 게 아니야. .NET의 Console.WriteLine 문서를 열어보자:
| 시그니처 | 설명 |
|---|---|
|
빈 줄 출력 |
|
문자열 출력 |
|
정수 출력 |
|
실수 출력 |
|
문자열을 하나의 인자로 포맷 |
|
여러 인자로 포맷 |
이게 다 같은 WriteLine 메서드의 오버로딩이야. 그래서 항상 이렇게 쓸 수 있는 거지:
Console.WriteLine("그냥 문자열");
Console.WriteLine(123);
Console.WriteLine(2.5);
Console.WriteLine("합계: {0}", 42);
그리고 컴파일러가 항상 네 호출을 제대로 해석해!
5. 컴파일러는 어떤 오버로딩을 호출할까?
여기선 엄격해: 실제로 넘긴 인자의 타입과 개수를 본다. 표로 보면:
| 호출 | 어떤 버전이 실행될까? |
|---|---|
|
|
|
|
애매할 땐 어떻게 될까?
가끔 상황이 꼬일 때가 있어. 예를 들어, 컴파일러가 어떤 버전을 골라야 할지 모르는 애매한 오버로딩:
public void Print(int a, double b) { ... }
public void Print(double a, int b) { ... }
printer.Print(5, 10);
// 에러: 애매함 — 어떤 Print를 호출해야 할까? (둘 다 될 것 같음)
컴파일러가 애매하다고 에러를 낼 거야. 이런 경우엔 파라미터 개수나 타입이 비슷한 오버로딩은 피하는 게 좋아, 컴파일러가 헷갈릴 수 있으니까.
6. params — 파라미터 개수 가변
숫자를 몇 개가 될지 모르게 받고 싶을 때가 있지? 그럴 땐 params 키워드를 쓰면 돼.
public void SumAll(params int[] numbers)
{
int sum = 0;
foreach (int n in numbers)
sum += n;
Console.WriteLine($"합계: {sum}");
}
이제 이렇게 호출할 수 있어:
SumAll(1, 2, 3); // 합계: 6
SumAll(10, 20); // 합계: 30
SumAll(); // 합계: 0
params가 붙은 메서드는 오버로딩이랑 같이 쓸 수 있는데, 컴파일러가 어떤 버전을 골라야 할지 헷갈릴 만한 오버로딩은 피하는 게 좋아.
7. 오버로딩과 파라미터 한정자 (ref, out, in)
C#은 파라미터 한정자에 따라 메서드를 구분해 (즉, void Foo(int a)와 void Foo(ref int a)는 시그니처가 달라서 둘 다 같은 클래스에 있을 수 있어):
public void SetValue(int a)
{
a = 42;
}
public void SetValue(ref int a)
{
a = 100;
}
ref 없이 호출하면 첫 번째 버전, ref로 호출하면 두 번째 버전이 실행돼:
int n = 5;
SetValue(n); // n은 5 그대로 (값이 복사됨)
SetValue(ref n); // n은 100이 됨
8. 도식: 오버로딩이란?
+----------+
| MyClass |
+----------+
|
| (메서드 일부)
+-----------------------+
| void Foo() |
| void Foo(int a) |
| void Foo(string s) |
| void Foo(int a, int b) |
+-----------------------+
코드로 표현하면:
// 오버로딩된 Foo() 메서드 호출:
var mc = new MyClass();
mc.Foo(); // void Foo()
mc.Foo(5); // void Foo(int)
mc.Foo("Hello"); // void Foo(string)
mc.Foo(2, 3); // void Foo(int, int)
9. 예제: 우리 앱에 메서드 오버로딩 추가
우리 학습용 앱을 더 발전시켜서, 동물 계층에 메서드 오버로딩을 추가해보자.
public class Animal
{
public string Name { get; set; }
// 소리 내기 메서드
public virtual void MakeSound()
{
Console.WriteLine("이상한 소리...");
}
// 오버로딩: 볼륨 지정해서 소리 내기
public void MakeSound(int volume)
{
Console.WriteLine($"동물이 {volume} dB로 소리 낸다.");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("멍!");
}
// 오버로딩: 볼륨 지정해서 멍멍
public void MakeSound(int volume)
{
Console.WriteLine($"멍! (볼륨: {volume} dB)");
}
}
이렇게 호출해봐:
Dog rex = new Dog();
rex.MakeSound(); // 멍!
rex.MakeSound(75); // 멍! (볼륨: 75 dB)
주의: 자식 클래스(Dog)에서 MakeSound(int volume) 메서드를 오버로딩했으니, 이제 파라미터 있는 버전과 없는 버전 둘 다 쓸 수 있어.
10. 메서드 오버로딩에서 흔한 실수
실수 1: 반환 타입만 다르게 오버로딩하려고 함.
이건 불가능해 — 반환 타입은 시그니처에 포함되지 않아. 오버로딩은 입력 파라미터의 개수나 타입이 달라야 하고, void나 int 같은 반환 타입으론 구분 못 해.
실수 2: 컴파일러를 헷갈리게 하는 애매한 오버로딩.
파라미터 개수는 같고 타입만 비슷한 오버로딩(int와 double 등)은 컴파일러를 혼란스럽게 할 수 있어. 예: Print(int a, double b)와 Print(double a, int b) — Print(1, 1) 호출 시 애매하다고 에러가 남.
실수 3: params와 다른 오버로딩이 충돌.
params가 붙은 메서드는 다른 오버로딩을 가로챌 수 있어. 타입이 같으면 컴파일러가 네가 기대한 게 아닌 버전을 고를 수도 있음.
실수 4: ref와 out이 시그니처에 포함된다는 걸 잊음.
Do(ref int x)와 Do(out int x)는 서로 다른 오버로딩이야. 이걸 신경 안 쓰면 잘못된 버전을 호출할 수 있어.
GO TO FULL VERSION