이러한 질문을 누구로부터 듣는다면
여러분은 뭐라고 대답하실 건가요?
캡슐화, 다형성, C++, 오버라이딩, 오버로딩, 추상화, 상속, virtual 기타 등등....
수많은 후보가 있을 거라 생각합니다.
이들 중 제가 이야기 하고 싶은 후보가 하나 있습니다.
바로 "인터페이스" 입니다.
인터페이스는 객체지향의 개념중
가장 늦게 등장했지만, 그 등장은 필연적인것이 아니었나 생각합니다.
(C++, 스몰토크엔 없지만, 유사한 개념이 있고,
C# JAVA등 나중에 등장한 언어에서는 필수적인 요소입니다.)
하지만 딱 봐서는 도대체 이것이 왜 나왔고, 어디에 써야 하는지 알기 어렵습니다.
저도 도대체 인터페이스가 무엇을 위한 것인지, 이해하기 어려웠고 오랫동안 알고 싶었습니다.
그래서 여러 글과 책을 보고 나름 연구해본 결과를 아래 공유합니다. (스크롤 압박 주의)
1) 인터페이스는 무엇을 위한 것인가
인터페이스(interface, 문화어: 대면부, 결합부) 또는 접속기는 사물 간 또는 사물과 인간 간의 의사소통이 가능하도록 일시적 혹은 영속적인 접근을 목적으로 ...
사전적인 의미에서의 "인터페이스"는 위의 위키피디아 링크처럼 "의사소통"을 위한
"수단(method)"과 같은 개념입니다.
우리가 사용하는 "언어 = 한국어"는 한국사람,
또는 한국어를 사용하는 사람들과 "소통" 하기 위한 수단입니다.
우리는 우리의 생각과 의지를 한국어란 수단을 통해서 다른 사람에게 전달합니다.
만일 영어를 안다면, 쓸수 있다면 더 많은 사람들에게 전달할수 있을 것입니다.
(왜냐면 영어는 표준이기 때문이죠.)
OOP언어에서도 인터페이스의 의미는 "한국어" 처럼
객체를 사용하기 위해서, 객체와 객체가 서로 "상호작용" 하기 위해서 필요한 도구를 의미합니다.
그래서 UML 2.0의 인터페이스 스타일을 보면 아래와 같이 "손잡이" 처럼 생겼습니다.
그리고 이 손잡이를 "붙잡음"으로서 인터페이스를 사용하게 되는 것이지요.
![]() | ![]() |
그림의 예는 매우 일반적인 예입니다.
자동차라는 클래스가 Handle이라는 Interface를 가지고 있고 이를 Driver가 이용한다는 의미이지요.
쉽게 말해서 "드라이버가 핸들로 자동차를 운전한다."는 것을 표현하는 것입니다. 붕~붕~
여기까지는 이해가 쉽습니다. 하지만 코드로 가면 약간 성격이 달라지게 됩니다.
C#문법 기준으로 인터페이스 Handle의 구현은 다음과 같습니다.
Interface Handle
{
void Drive();
}
그리고 인터페이스를 사용하는 Car 클래스는 다음과 같이 구현되어야 합니다.
class Car : handle
{
public void Drive()
{
}
}
그리고 Driver는 다음과 같이 인터페이스를 사용합니다.
class Driver
{
void Main()
{
Handle car = new Car();
car.Drive();
}
}
이 코드를 UML로 그대로 표현하면 이렇습니다.

이전 그림에서 Car와 Handle 의 관계는 "Association"인데
지금은 "상속"으로 바뀌어 있는것을 볼수 있습니다. 코드에서도 실제로 상속이구요.
여기서 저의 경우엔 약간의 혼동이 있었습니다.
다른 클래스가 이 클래스를 사용하기 만들기 위해서 왜 인터페이스를 "상속"해야 하는가?
에 대해서 의문을 가지고 있었습니다. 왜 굳이 상속으로 표현되어야 하는 것일까요?
(인터페이스가 아무런 구현도 없다는 사실을 알면 더욱 의문스럽습니다. 상속해줄 구현이 없는데
상속관계라니... 이게 무슨 의미가 있는건가요?)
왜 이렇게 되냐면,
객체지향에서 "public 상속"의 의미는 "is-a" 관계이기 때문입니다.
혹시 잘 기억이 안나는 분들을 위해서, Scott Myers의 명저 : Effective C++에 나온
이야기를 정리해 인용하겠습니다.
B가 D를 public 상속 하면 이는 is-a 관계로서 B에서 되는 것은 모두 D에서도 되어야 한다.
여기서 B는 D의 부모 클래스를 의미합니다.
결국, is-a 상속을 통해서 부모 클래스에서 되는것, 부모 클래스의 특성을 자식 클래스 또한 그대로 가지고 있어야 한다는 것입니다. 인터페이스의 모든 요소는 비록 형체만 있고 구현이 없는 것일지라도 이를 상속받는 놈들에서는 반드시 구현되어야 합니다. 왜나면 자식들은 부모인 인터페이스와 상속을 통해서 is-a 관계를 유지하기 때문이죠. 인터페이스가 가진 것들은 자식에서도 다 되어야 합니다.
만일 인터페이스의 메소드 하나라도 구현하지 않는다면 컴파일러는 이를 꼭 구현해야 한다고 컴파일 에러를 뿜습니다. 컴파일 단계에서부터 날 상속해 쓰려면 꼭 구현하라고 강요하는 것이지요.
그럼 왜 이러한 "강요"를 시키는 것일까요? 내부에 구현도 없는 존재가 말입니다.
이는 "인터페이스"의 근본적인 특성과 관계가 있습니다.
다시 "한국어"로 돌아가 봅시다.
우리가 이야기를 하기 위해서 사용하는 "한국어"는 정형화된 언어입니다.
우리가 마음대로 만들어내고 자기 혼자 사용한다고 해서 그것이 한국어가 될수는 없습니다.
한국어를 사용하는 모두가 동의할수 있고 익숙한, 그리고 정해진 어법에 맞는 언어요소만이 한국어가
될수 있습니다. 여기서 이 "동의"와 "어법"이 중요합니다.
우리가 한국사람들과 소통하려면 우리가 사용하는 언어가 "한국어" 임을 보장해 줄수 있어야 합니다.
이를 위해서 우리는 "어법"이라는 것을 만들고 이를 모든 한국어를 사용하는 사람들이 배우고 지키게 해 왔습니다. 그리고 모두가 암묵적으로 "동의" 함으로서 사용하는 언어가 모두에게 사용될수 있는 것입니다. (
유행어가 오랫동안 유지되기 힘든데엔 이러한 이유가 있습니다. 한국어를 쓰는 일부 구성원들만이 동의하고 사용하며. 한국어 어법에 맞지 않기 때문에 오래가지 않아 도태되는 것입니다.)
예시가 길었는데, 결국 인터페이스가 상속으로 "구현"을 강제하는 이유는 클래스간에 소통의 편의를 위한 일관된 "규칙"으로서 존재하기 위함입니다. 이 "인터페이스"라는 규칙을 통해서 여러 클래스=컴포넌트들은 서로 "일관되게" 소통할수 있는 것입니다. 이 "일관된 규칙화된 소통"을 바꾸는 행위가 발생했을때 컴파일러는 이를 잡아내어 "인터페이스"를 사용하는 모든 클래스들에 대해서 새로운 행위를 지키도록 강제합니다. 이것이 지켜지지 않는다면, 마치 유행어가 어르신들에게 이해되지 않는 것처럼 클래스들은 서로 잘못 사용하게 되고 결국 프로그램은 망가지게 됩니다. 그러므로, 객체지향 개념과 언어의 설계자들은 이런것을 시스템적으로 컴파일러 레벨부터 지키도록 만들었습니다. (정말 멋지지 않습니까?)
결국 이러한 일관된 규칙을 통해서 클래스들은 서로 믿고 소통할수 있게 되었고,
이 규약만 지키면 되므로 꼭 자신이 소통하려는 클래스 전체를 알지 못해도 소통이 가능하게 되었습니다. 마치 우리가 상대방을 몰라도, 한국어만 알면 이야기를 할수 있는 것처럼 말입니다.
이를 통해서 우리가 객체지향과 인터페이스의 장점이라고 이야기하는
"객체끼리의 의존성 감소"가 실현되는 것입니다. 그리고 이것이 객체지향이 추구하는 바입니다.
(그렇기 때문에 제가 객체지향의 꽃이라 감히 부르는 것입니다.)
이해를 돕기 위해서,
지금까지의 이야기를 하나의 과정으로
정리하자면 다음과 같습니다.
1) 클래스 간의 소통이 필요
2) 소통을 위해서 정해진 규칙이 필요
3) 정해진 규칙을 여러 클래스들에게 강제할수 있는 방법이 필요
4) is-a 관계의 "public 상속"은 "부모에서 되는건 자식에서도 되어야 해" 이므로 3)의 조건 만족
5) 4)의 is-a 관계를 통해서 인터페이스를 3) 으로 강제 구현할수 있도록 함으로서 1)과 2)의 조건
모두 만족
6) 결과적으로, 1)과 2)가 실현되고 이것이 5)를 통해서 컴파일러에서 지켜지는 것이 보장됨으로서
지금의 인터페이스 개념이 필요해지고 완성됨.
좀 이해에 도움이 되셨는지요?
2) 인터페이스는 언제 어떻게 써야 할까
그렇다면 인터페이스는 도대체 언제 써야 하는걸까요?
제가 처음 인터페이스에 대해서 너무 궁금해서 여기저기 찾아 보았지만 명확한게 없어
제 코드 멘토였던 분에게 질문한적이 있었습니다. 그분은 다른말은 안하고 코딩으로 직접 알려 주셨던 기억이 납니다. 아래는 그 내용을 제가 기억을 살려 재구성한 것입니다.
옛날옛날에, 웹에서 휴대폰 문자 메세지를 보내는 서비스를 하던 회사가 있었는데 이 회사의 메세징 라이브러리는 다음과 같이 구성되어 있었다.

그리고 이를 사용하는 메인코드는 다음과 같이 구성되어 있었다.
[code csharp]
public void Main(string ISPName, int phoneNo, string message)
{
if (ISPName == "SKT")
{
SKTSMSManager manager = new SKTSMSManager();
manager.InitSKT();
manager.SendSKT(phoneNo, message);
manager.CloseSKT();
}
else if(ISPName == "KTF")
{
KTFSMSManager manager = new KTFSMSManager();
manager.InitKTF();
manager.SendKTF(phoneNo, message);
manager.CloseKTF();
}
else if(ISPName == "LGT")
{
LGTSMSManager manager = new LGTSMSManager();
manager.InitLGT();
manager.SendLGT(phoneNo, message);
manager.CloseLGT();
}
}
[/code]
처음에는 이러한 방식이 별 문제가 없었다. 하지만 시간이 지남에 따라 SMS 뿐만 아니라, MMS 또한 지원해야만 하게되었다. 통신사는 3개지만, 구현해야 하는 클래스는 6개로 늘어나게 되었다.

그리고 위 메인코드의 분기문또한 2배로 늘어나게 되었다. 이후 통신사 서비스, 요금제마다 또 방식이 바뀌게 되어 결국 이에 맞추어 새로운 클래스와 분기문 코드를 만들어야만 했다. 분기문이 길어지다보니 코드들이 서로 꼬이는 부분이 늘어나 버그가 급증하고 읽기도 어렵게 되었다. 사장은 코드중복을 줄이거나 버그를 열심히 잡으려 노력했지만 늘어나는 클래스와 분기문을 주체하기 힘들었다. 게다가 클래스들이 동작하는 방식들이 각 통신사들이 서비스마다 메세징을 지원하는 방식에 따라 미묘하게 달라 이들을 추상화하거나 통합할 엄두를 내지 못하고 있었다. (이런일은 현업에서 매우 비일비재합니다.)
그때 혜성처럼 한 개발자가 나타났다. 그는 재야의 숨은 고수로 불리던 이XX 였다.
그는 3개월 이라는 시간만에 이 복잡한 클래스와 분기문들을 싹다 정리하고 수천만원을 챙기고 다시 재야로 사라졌다. 그가 사용한 방법은 무엇이었을까?
그는 인테페이스를 사용했던 것이었다.
각 클래스마다 공통으로 수행해야 하는 시퀸스를 담은 메소드들 Init(), Send(), Close()를 인터페이스로 추상화 하고 모든 메세징 클래스들이 이를 상속받게 한다. 이렇게 하면 새 메세징 클래스가 만들어 지더라도 IMessenger라는 인터페이스에서 요구하는 메소드들을 구현해야 되므로, 모든 클래스가 동일한 방식으로 접근하고 동작할수 있도록 강제할수 있다.

따라서, 메인에서 분기문을 없애고 직접 해당 메세징 클래스 객체를 넘기도록 다음과 같이 수정할수있다. 메세징 클래스에 접근하는 방법을 하나로 통일한 것이다. 결국 아래 메인 코드는 메세징 클래스가 늘어나거나 줄어들거나 상관없이 일관된 동작을 수행한다.
[code csharp]
public void Main(IMessenger messenger, int phoneNo, string message)
{
IMessenger manager = messenger;
manager.Init();
manager.Send(phoneNo, message);
manager.Close();
}
[/code]
여기에 약간의 추상화를 더하여 추상 베이스 클래스를 만들수 있다. 하지만 이는 세부적인 구현과 관계된 문제 : 서비스 방식이나, 통신사 규약이냐는 차이점에 따라 달라질수 있기 때문에 이번 화에서 이야기하려는 범위를 벗어남으로 "추상화와 추상 클래스 이야기"에서 다루도록 합시다...
어쨌든, 인터페이스를 통해서 서로다른 것들에 일관된 접근 방법을 제시함으로서 우리는 아래의 목적을 달성할수 있습니다.
1) 여러 클래스 간의 일관된 구현/확장 강제
2) 여러 클래스를 일관되게 Access 하고 싶을때
제가 처음 인터페이스에 대해서 너무 궁금해서 여기저기 찾아 보았지만 명확한게 없어
제 코드 멘토였던 분에게 질문한적이 있었습니다. 그분은 다른말은 안하고 코딩으로 직접 알려 주셨던 기억이 납니다. 아래는 그 내용을 제가 기억을 살려 재구성한 것입니다.
옛날옛날에, 웹에서 휴대폰 문자 메세지를 보내는 서비스를 하던 회사가 있었는데 이 회사의 메세징 라이브러리는 다음과 같이 구성되어 있었다.

그리고 이를 사용하는 메인코드는 다음과 같이 구성되어 있었다.
[code csharp]
public void Main(string ISPName, int phoneNo, string message)
{
if (ISPName == "SKT")
{
SKTSMSManager manager = new SKTSMSManager();
manager.InitSKT();
manager.SendSKT(phoneNo, message);
manager.CloseSKT();
}
else if(ISPName == "KTF")
{
KTFSMSManager manager = new KTFSMSManager();
manager.InitKTF();
manager.SendKTF(phoneNo, message);
manager.CloseKTF();
}
else if(ISPName == "LGT")
{
LGTSMSManager manager = new LGTSMSManager();
manager.InitLGT();
manager.SendLGT(phoneNo, message);
manager.CloseLGT();
}
}
[/code]
처음에는 이러한 방식이 별 문제가 없었다. 하지만 시간이 지남에 따라 SMS 뿐만 아니라, MMS 또한 지원해야만 하게되었다. 통신사는 3개지만, 구현해야 하는 클래스는 6개로 늘어나게 되었다.

그리고 위 메인코드의 분기문또한 2배로 늘어나게 되었다. 이후 통신사 서비스, 요금제마다 또 방식이 바뀌게 되어 결국 이에 맞추어 새로운 클래스와 분기문 코드를 만들어야만 했다. 분기문이 길어지다보니 코드들이 서로 꼬이는 부분이 늘어나 버그가 급증하고 읽기도 어렵게 되었다. 사장은 코드중복을 줄이거나 버그를 열심히 잡으려 노력했지만 늘어나는 클래스와 분기문을 주체하기 힘들었다. 게다가 클래스들이 동작하는 방식들이 각 통신사들이 서비스마다 메세징을 지원하는 방식에 따라 미묘하게 달라 이들을 추상화하거나 통합할 엄두를 내지 못하고 있었다. (이런일은 현업에서 매우 비일비재합니다.)
그때 혜성처럼 한 개발자가 나타났다. 그는 재야의 숨은 고수로 불리던 이XX 였다.
그는 3개월 이라는 시간만에 이 복잡한 클래스와 분기문들을 싹다 정리하고 수천만원을 챙기고 다시 재야로 사라졌다. 그가 사용한 방법은 무엇이었을까?
그는 인테페이스를 사용했던 것이었다.
각 클래스마다 공통으로 수행해야 하는 시퀸스를 담은 메소드들 Init(), Send(), Close()를 인터페이스로 추상화 하고 모든 메세징 클래스들이 이를 상속받게 한다. 이렇게 하면 새 메세징 클래스가 만들어 지더라도 IMessenger라는 인터페이스에서 요구하는 메소드들을 구현해야 되므로, 모든 클래스가 동일한 방식으로 접근하고 동작할수 있도록 강제할수 있다.

따라서, 메인에서 분기문을 없애고 직접 해당 메세징 클래스 객체를 넘기도록 다음과 같이 수정할수있다. 메세징 클래스에 접근하는 방법을 하나로 통일한 것이다. 결국 아래 메인 코드는 메세징 클래스가 늘어나거나 줄어들거나 상관없이 일관된 동작을 수행한다.
[code csharp]
public void Main(IMessenger messenger, int phoneNo, string message)
{
IMessenger manager = messenger;
manager.Init();
manager.Send(phoneNo, message);
manager.Close();
}
[/code]
여기에 약간의 추상화를 더하여 추상 베이스 클래스를 만들수 있다. 하지만 이는 세부적인 구현과 관계된 문제 : 서비스 방식이나, 통신사 규약이냐는 차이점에 따라 달라질수 있기 때문에 이번 화에서 이야기하려는 범위를 벗어남으로 "추상화와 추상 클래스 이야기"에서 다루도록 합시다...
어쨌든, 인터페이스를 통해서 서로다른 것들에 일관된 접근 방법을 제시함으로서 우리는 아래의 목적을 달성할수 있습니다.
1) 여러 클래스 간의 일관된 구현/확장 강제
2) 여러 클래스를 일관되게 Access 하고 싶을때
이렇게 인터페이스는 가장 나중에 나온 객체지향 개념답게, 객체지향을 확실히 객체지향 답게 만들어주는 중요한 기능을 제공해주고 있습니다. 따라서 인터페이스에 친해진다면 좀더 객체지향다운 코드를 짜실수 있을 겁니다.
지금까지의 이야기는 제 경험과 연구 그리고 생각에 의한 이야기이기 때문에 실제 사실과는 조금 다를수 있습니다. 다만, 보시는 분들이 이글을 통해 "인터페이스"에 대해서 좀더 이해를 넓히는 실마리가 되었음 합니다.
OOP의 꽃, Interface, Abstract... 그중 Interface에 대한 이야기라.. 기대됩니다^^~ 두근두근!
답글삭제간단히 줄이면 "캡슐화의 확장"일 뿐이라 생각합니다.
답글삭제@류청파(koc/SALM) - 2010/04/15 22:48
답글삭제댓글 감사합니다.
열마디를 한마디로 잘 이야기 해주시는군요 :)