메서드 오버로딩과 오버라이딩은 이름이 비슷하지만 의도와 동작 시점이 다릅니다. 오버로딩은 같은 이름의 메서드를 매개변수 시그니처로 구분해 컴파일 타임에 선택하며, 오버라이딩은 상속 구조에서 가상 멤버를 재정의해 런타임에 다형적으로 호출됩니다.
1. 핵심 개념 요약
오버로딩: 같은 이름, 다른 시그니처(매개변수 개수/타입/순서)입니다. 반환형만 다른 것은 오버로딩이 아닙니다. 호출 대상은 컴파일러가 결정합니다.
오버라이딩: base에 virtual/abstract 멤버가 있고, 파생 클래스에서 override로 재정의합니다. 호출 대상은 런타임 타입이 결정합니다. sealed override로 더 이상의 재정의를 막을 수 있습니다.
2. 오버로딩 예제와 규칙
using System;
using System.Linq;
public static class Calculator
{
public static int Add(int a, int b) => a + b;
public static double Add(double a, double b) => a + b;
public static int Add(params int[] values) => values.Sum();
}
public class Demo
{
public static void Main()
{
Console.WriteLine(Calculator.Add(1, 2)); // Add(int, int)
Console.WriteLine(Calculator.Add(1.2, 3.4)); // Add(double, double)
Console.WriteLine(Calculator.Add(1, 2, 3, 4)); // Add(params int[])
}
}
시그니처는 "메서드 이름 + 매개변수"로 정의됩니다. 반환형만 다르거나 매개변수 이름만 다르면 오버로딩이 아닙니다. params와 기본 매개변수(optional) 혼용은 모호성을 만들기 쉬우므로 최소화합니다.
3. 오버라이딩 예제와 규칙
using System;
public abstract class Shape
{
public abstract double Area();
public virtual void Draw()
{
Console.WriteLine("Shape");
}
}
public class Circle : Shape
{
public double Radius { get; }
public Circle(double radius) => Radius = radius;
public override double Area() => Math.PI * Radius * Radius;
public sealed override void Draw()
{
Console.WriteLine("Circle");
}
}
public class Demo
{
public static void Main()
{
Shape s = new Circle(2);
Console.WriteLine(s.Area()); // Circle.Area()
s.Draw(); // Circle.Draw() - 런타임 다형성
}
}
override는 base의 시그니처와 호환되어야 합니다. C# 9+에서는 반환형 공변성(더 구체적인 반환형)도 허용됩니다. sealed override는 더 이상의 재정의를 막아 성능/안정성을 높일 수 있습니다.
4. new 키워드로 숨김과 오버라이딩의 차이
using System;
public class Logger
{
public virtual void Log(string message)
{
Console.WriteLine($"Base: {message}");
}
}
public class FileLogger : Logger
{
// 오버라이딩이 아닌 숨김: 경고를 피하려면 new 사용
public new void Log(string message)
{
Console.WriteLine($"File: {message}");
}
}
public class Demo
{
public static void Main()
{
Logger a = new FileLogger();
a.Log("hi"); // Base.Log 호출 (런타임 타입이 FileLogger여도 숨김이라 base 버전)
FileLogger b = new FileLogger();
b.Log("hi"); // FileLogger.Log 호출
}
}
new는 멤버 숨김이며 다형성을 제공하지 않습니다. 다형적 동작이 필요하면 항상 override를 사용합니다.
5. 모호성과 함정 피하기
기본 매개변수와 오버로딩을 섞으면 호출이 예기치 않게 선택될 수 있습니다. 예: 매개변수 없는 호출이 기본값이 있는 오버로드로 흘러가는 경우가 있습니다. 라이브러리 설계 시에는 새 기능 추가 시 기존 메서드에 optional을 추가하기보다 새로운 시그니처의 오버로드를 추가하고, 오래된 것을 [Obsolete]로 안내하는 것이 안전합니다.
params와 유사한 시그니처를 함께 두면 모호해질 수 있습니다. 가령 params int[]와 IEnumerable<int>를 함께 두는 대신 하나만 노출하고 호출 측 확장 메서드로 보완하는 전략이 단순합니다.
dynamic 인수는 오버로드 선택을 런타임으로 미룹니다. 컴파일 타임 보장을 약화하므로 API 경계에서만 제한적으로 사용합니다.
6. 실전 모범 사례
오버로딩은 사용 시나리오를 분명히 구분할 때만 추가합니다. 과도한 오버로드는 API를 불분명하게 만듭니다. 가장 자주 쓰는 시그니처를 기본으로 두고, 나머지는 명확히 구분되는 타입/개수로 설계합니다.
반환형만 다른 오버로드를 시도하지 마십시오. 컴파일 오류이며 호출자 혼란을 유발합니다. 필요한 경우 Try 패턴(예: TryParse)처럼 out 매개변수로 의도를 드러냅니다.
오버라이딩은 확장 지점에만 virtual을 노출합니다. 내부적으로만 쓰는 메서드는 비가상으로 두어 인라이닝 여지를 남깁니다. 파생 클래스에서 동작을 고정하고 싶다면 sealed override를 사용합니다.
검증/공통 로직은 한 오버로드(핵심 구현)에 집중시키고 나머지 오버로드는 그리로 위임합니다. 코드 중복을 줄이고 버그 표면을 축소합니다.
7. 유지보수와 성능 팁
가상 호출은 약간의 오버헤드가 있지만 대부분의 비즈니스 코드에서 무시할 수준입니다. 성능이 중요한 핫패스에서는 sealed 클래스/메서드, 비가상 메서드, AggressiveInlining 힌트 등으로 최적화 여지를 확보합니다.
공개 API에서 시그니처 변경은 이진 호환성에 영향을 줍니다. 새 오버로드를 추가하는 형태로 진화시키고, 기존 메서드는 Obsolete 메시지로 마이그레이션 경로를 안내합니다.
8. 요약
오버로딩은 컴파일 타임 선택, 오버라이딩은 런타임 다형성입니다. 시그니처는 매개변수로만 구분되며, new는 숨김일 뿐 다형성이 아닙니다. 명확한 사용 시나리오, 최소한의 오버로드, 핵심 구현 위임, 신중한 virtual 공개가 모범 사례입니다.
'C#' 카테고리의 다른 글
| C# Reflection으로 메타데이터 수정하기 (0) | 2026.06.12 |
|---|---|
| C# Operator Overloading으로 사용자 정의 연산 구현 (0) | 2026.06.11 |
| C# 인터페이스의 명시적 구현 활용과 주의사항 (0) | 2026.06.05 |
| C# 암호화된 설정값을 안전하게 로드하기 (0) | 2026.06.05 |
| C# 메서드 체이닝(Method Chaining) 패턴 구현 (0) | 2026.06.04 |