프로그래밍 언어를 깊이 있게 이해하는데 꼭 필요한 개념 중 하나가 컴파일 타임(compile runtime)과 런타임(runtime)의 구분입니다. 우리는 보통 문법 오류 검사와 타입 검사는 컴파일 타임에 하고 기타 오류 처리는 런타임에 하는 것으로 이해하고 있지만, 프로그래밍 언어에 따라 같은 기능이라도 컴파일 타임에 수행되기도 하고 런타임에서 수행되기도 합니다.
이 글에서는 잘못 이해하기 쉬운 두 가지 예를 통해 컴파일 타임과 런타임에 대해 설명하겠습니다. 설명에 사용할 언어는 C#입니다.
오버라이드(override)와 오버로드(overload)
Java나 C#과 같은 객체지향 언어는 메소드를 오버로드할 수 있도 있고, 오버라이드할 수도 있습니다. 개념도 비슷한데 단어마저 비슷해서 많은 분들이 헷갈려 하시지만, 둘은 완전히 다른 개념입니다.
- 오버라이드: 상위 클래스에 정의된 같은 시그너처의 메소드를 재정의. 메소드 호출은 런타임에 결정됨.
- 오버로드: 메소드 이름은 같지만 시그너터(인자 타입)이 다름. 메소드 호출은 컴파일 타임에 결정됨.
오버라이드의 예는 다음과 같습니다. b1
과 b2
모두 Base
클래스로 선언되었지만, b2
는 실제로는 Sub
클래스의 인스턴스이므로 b2
는 Base.DoSomething
메소드가 아닌 Sub.DoSomething
메소드를 호출합니다. 그리고 이런 결정은 컴파일 타임이 아닌 런타임에 이루어집니다. 컴파일러는 메소드 단위로 컴파일을 수행하는데, MainClass.CallDoSomething
메소드의 인자 b
가 실제로 어떤 클래스의 인스턴스인지 컴파일 타임에는 알 수가 없기 때문입니다.
using System;
namespace OverloadVsOverrride
{
class Base
{
public virtual void DoSomething()
{
Console.WriteLine("Base's doSomething");
}
}
class Sub : Base
{
public override void DoSomething()
{
Console.WriteLine("Sub's doSomething");
}
}
class MainClass
{
public static void CallDoSomething(Base b)
{
b.DoSomething();
}
public static void Main(string[] args)
{
Base b1 = new Base();
Base b2 = new Sub();
CallDoSomething(b1); // Call Base's DoSomething
CallDoSomething(b2); // Call Sub's DoSomething
}
}
}
오버로딩은 이와는 달리 인자의 타입만으로 어떤 메소드를 호출할지 결정합니다. Base
클래스에 정의된DoSomething(int)
와 DoSomething(string)
메소드는 메소드 이름만 같지 컴파일러 입장에서는 완전히 다른 메소드입니다. int
타입과 string
타입은 컴파일 타임에 구분이 가능하므로, 오버라이드와 달리 컴파일 타임에 어떤 메소드를 호출할지 결정합니다.
using System;
namespace OverloadVsOverrride
{
class Base
{
public void DoSomething(int i)
{
Console.WriteLine("int: " + i);
}
public void DoSomething(string s)
{
Console.WriteLine("string: " + s);
}
}
class MainClass
{
public static void Main(string[] args)
{
Base b = new Base();
b.DoSomething(1); // Call DoSomething(int)
b.DoSomething("Hello World"); // Call DoSomething(string)
}
}
}
업 캐스팅(up casting)과 다운 캐스팅(down casting)
컴파일 타임과 런타임 구분이 중요한 또 다른 예로 캐스팅이 있습니다.
캐스팅은 하나의 타입을 다른 타입으로 바꾸는 걸 의미하는데, 아래 CallDoSomething
메소드는 Base
타입의 인자 b
를 받아 Sub
타입으로 다운 캐스팅하여 DoSomething
메소드를 호출하고 있습니다. 반대로, b2
변수의 선언을 보면 Sub
타입의 인스턴스를 생성해 Base
타입으로 선언된 b2
에 넣어주고 있는데, 이런 캐스팅은 업 캐스팅이라고 부릅니다.
using System;
namespace Casting
{
class Base
{
public virtual void DoSomething()
{
Console.WriteLine("Base's doSomething");
}
}
class Sub : Base
{
public override void DoSomething()
{
Console.WriteLine("Sub's doSomething");
}
}
class MainClass
{
public static void CallDoSomething(Base b)
{
Sub s = (Sub)b;
s.DoSomething();
}
public static void Main(string[] args)
{
Base b1 = new Base();
Base b2 = new Sub();
CallDoSomething(b1); // Throw an InvalidCastException.
CallDoSomething(b2);
}
}
}
얼핏 보기에는 업 캐스팅과 다운 캐스팅은 단순히 방향만 반대인 것처럼 보이지만, 시점을 생각해 보면 이야기가 달라집니다. 업 캐스팅은 항상 성공하기 때문에 컴파일 타임에만 처리하면 런타임에 더 이상 신경을 쓸 필요가 없습니다. 반대로 다운 캐스팅은 런타임 타입에 따라 성공할 수도 실패할 수도 있기 때문에, 반드시 런타임에 수행되어야 합니다. 정리하면 다음과 같습니다.
- Sub → Base: 업캐스팅. 런타임에 수행할 명령이 없음.
- Base → Sub: 다운캐스팅. 런타임에 타입을 확인하고 캐스팅이 불가능할 경우
InvalidCastException
을 발생시켜야함.
Pingback: 컴파일 타임과 런타임 | 서광열의 C# 스쿨