어느 경우에도 정적 메서드를 재정의할 수는 없습니다.

이유는 정말 간단합니다. "재정의(Override)"라는 것은 주로 어느 클래스를 상속하는 클래스에서 기능을 변경하거나 추가하기 위해서 주로 사용됩니다.


예로 System.Exception 클래스를 보도록 하겠습니다.

클래스 상속 예 (C#)
using System;
                    
public class Program {
    class Secret : Exception {
        public Secret(string message) : base(message) { }
        public override string Message {
            get {
                return "안알랴줌";
            }
        }
    }
    
    public static void Main() {
        Secret s = new Secret("예외 메세지");
        Console.WriteLine(s.Message);
    }
}
예제 코드 (.NET Fiddle)


Secret 이라는 클래스는 System.Exception 클래스를 상속하고 있습니다.

System.Exception(링크를 누르면 소스 코드를 보실 수 있습니다) 클래스의 경우 총 4개의 생성자가 있는 것을 보실 수 있습니다.

만약 위의 클래스에서 Message 속성을 오버라이딩하지 않았다면, s.Message 속성 값은 "예외 메세지"가 되어야 하며 출력 값 또한 동일해야 합니다.

하지만, Message 속성을 오버라이딩했기 때문에 s.Message 속성 값과 출력 값은 "안알랴줌" 이 나오게 됩니다.


여기서 아실 수 있는 것은 Secret.Message 속성은 Secret 클래스가 개체(Instance)화 되었을 때 사용할 수 있다는 것을 아실 수 있습니다.

즉, Secret.Message 속성은 Secret 클래스에 속해있는 것이라고 표현할 수 있습니다.


하지만, 정적 멤버나 정적 메서드의 경우는 그렇지 않습니다.

클래스가 개체화되던 개체화되지 않던 사용할 수 있기 때문이죠.

정적 클래스 예 (C#)
using System;
                    
public class Program {
    class StaticMessage {
        public StaticMessage() { }
        public static string Message() {
            return "정적 메서드";
        }
    }
    
    public static void Main() {
        StaticMessage sm = new StaticMessage();
        Console.WriteLine(sm.Message());
        Console.WriteLine(StaticMessage.Message());
    }
}
예제 코드 (.NET Fiddle)


정적 메서드나 필드의 경우 개체화된 클래스를 통해 접근/호출하는 것이 불가능합니다.

Console.WriteLine(sm.Message()); < 이 부분에서 오류가 나게 되죠.

그 다음 코드를 보시면 Console.WriteLine(StaticMessage.Message()); 처럼 형식_이름.정적_메서드() 형식으로 호출하고 있는 것을 보실 수 있습니다.

여기서는 개체화된 클래스를 통해 접근하는 것을 볼 수 없었습니다.


딱 한 마디로 문제를 해결한다면

Instance 필드는 개체화된 클래스에서만 사용이 가능하지만, Static 필드의 경우 개체화된/되지 않은 클래스에서 사용이 가능합니다.

인스턴스/정적 필드 예 (C#)
using System;

class Test {
    static int Age = 24;
    int InstanceAge = 0;

    public static void PrintAge() {
        Console.WriteLine(Age);
        Console.WriteLine(InstanceAge);        // ERROR, CANNOT ACCESS INSTANCE FIELD.
    }

    public void PrintAgeInstance() {
        Console.WriteLine(Age);
        Console.WriteLine(InstanceAge);        // WORKING FINE
    }
}

public class Program {
    public static void Main() {
        Test t = new Test();
        
        Test.PrintAge();
        t.PrintAgeInstance();
    }
}
예제 코드 (.NET Fiddle)


이런 결과가 나오는 이유는 딱 한가지입니다.

정적인 메서드, 필드 등은 개체화를 통하지 않아도(열린 문) 접근이 가능합니다.

인스턴스 메서드, 필드 등은 개체화를 통해야(닫힌 문, 개체화함으로 문을 염) 접근이 가능합니다.


왜 이런 이야기를 갑자기 하느냐..

제가 프로그래밍 도중 큰 문제에 직면했습니다.


파일 형식에서 사용되는 헤더가 있는데, 기능이나 필드를 추가한 상위 버전 헤더를 사용자가 작성할 수 있게 해야 했습니다.

그리고, 기초 모델이 되는 헤더 형식에는 정적 메서드인 FromStream, FromFile 이 정의되어 있습니다.


헤더는 개체화를 해야만 내부 멤버에 접근이 가능한데, FromStream 메서드는 정적이기 때문에 접근이 불가능합니다.

그런데 이 메서드를 재정의하지 않으면 필드가 추가된 상위 버전의 헤더 필드 값을 읽거나 쓸 수 없어집니다.


그럼 이 문제를 어떻게 해결할까요??..

답은 정말 간단했습니다. 이 문제때문에 몇 시간을 생각했었는데 해결하고 나니 정말 뿌듯합니다.


FromStream 메서드는 정적인 채로 놔두고, 메서드 내부에서 개체를 생성합니다.

그리고 기초 모델이 되는 헤더에 인스턴스 메서드(protected virtual void UserProcessRoutine)를 만들고, 상위 버전의 헤더에서 저 메서드를 재정의하도록 했습니다.

메서드 재정의 (C#)
using System;
using System.IO;
using System.Reflection;

class HighLevelHeader : BasicHeader {
    int g_age;
    
    public HighLevelHeader() : base(0) {
        g_age = 0;
    }
    
    protected override void UserProcessRoutine(BinaryReader br) {
        // 나이를 읽어온다.
        g_age = br.ReadInt32();
    }
    
    public int Age {
        get { return g_age; }
    }
}

class BasicHeader {
    int g_id;
    
    BasicHeader() { }
    protected BasicHeader(int id) {
        g_id = id;
    }
    
    protected virtual void UserProcessRoutine(BinaryReader br) { }
    
    // 제네릭 형식 T에는 BasicHeader 및 해당 클래스를 상속하는 형식만 사용할 수 있도록
    // where 조건을 이용합니다.
    public static T FromStream<T>(Stream s) where T : BasicHeader {
        
        // 헤더를 개체화합니다.
        // 원래는 Activator.CreateInstance<T>(); 를 사용했는데
        // 이 메서드를 사용하면 생성자가 public이 아닐 경우에는 예외가 발생하게 된다.
        // 그래서 Reflection을 이용하여 생성자를 호출한다.
        
        // 공용/비공용 생성자를 검색한다. 
        ConstructorInfo ctor = typeof(T).GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null);
        
        // 이건 발생하면 절대절대 안된다.
        if ( ctor == null ) return default(T);
        
        T header = (T) ctor.Invoke(null);
        
        // 스트림의 데이터를 읽어올 개체를 만들고
        BinaryReader br = new BinaryReader(s);
        
        // 값을 읽어옵니다.
        header.g_id = br.ReadInt32();
        
        // 여기는 상속 클래스에서 재정의한 메서드가 호출될 곳
        header.UserProcessRoutine(br);
        
        // 이제 헤더를 불러왔으니 반환한다.
        return header;
    }
    
    public int Id {
        get { return g_id; }
    }
}

public class Program {
    public static void Main() {
        MemoryStream ms = new MemoryStream();
        BinaryWriter bw = new BinaryWriter(ms);
        
        // id = 32
        bw.Write(32);
        
        // age = 24
        bw.Write(24);
        
        // 헤더 기본 모델을 읽어온다.
        ms.Position = 0;
        BasicHeader bh = BasicHeader.FromStream<BasicHeader>(ms);
        
        ms.Position = 0;
        
        // 상위 레벨 헤더를 읽어온다.
        HighLevelHeader hlh = BasicHeader.FromStream<HighLevelHeader>(ms);
        
        Console.WriteLine(bh.Id);
        Console.WriteLine(hlh.Id + "\t" + hlh.Age);
        
        Console.ReadKey(true);
    }
}
예제 코드 (.NET Fiddle)


.NET Fiddle에서는 시간 초과 오류가 나오네요.. VS, SharpDevelop으로는 결과가 잘 나옵니다.

32

32 24

이렇게요!


프로그래밍을 하다가 막히면 그 문제를 해결하기 위해 정말 많은 생각을 합니다. 새로운 관점으로 바라볼 때도 있죠!

그리고 문제가 해결된 순간! 정말 기쁩니다.


이 글이 비슷한 문제를 겪고 있는 분께 도움이 되었으면 좋겠습니다.










+ Recent posts