java interface 왜 사용하나요?

ITWeb/개발일반 2012. 3. 12. 11:18
[interface 는 왜 사용하나요?]
- 요기에 토론이 있내요. : http://www.okjsp.pe.kr/seq/161248 


[개인적인 의견]

interface 를 사용하는 건 개인적으로
- 개발한 코드의 종속성을 줄이고 유지보수성을 높이기 위함이 아닐까 싶습니다.
- 뭐 설계를 잘해야 하는건 당연한 말이겠지만 가져다 사용하는 사람 입장에서 뒤에 뭐가 바뀌었다고 앞에 뭐를 고쳐야 한다고 하면.. 이게 삽질이 아니고 뭐겠습니까...

암튼 전 어려운말 별로 안좋아 해서.. 그냥 유지보수의 편의성이라고 우겨 봅니다. ㅎㅎ 



[Interface 의 다중 type 이해]

직접 인터페이스를 선언해서 사용해 보자.

public interface Book{

    private String author;

    private String title;

    public void publish(){

        //출판

    }

}


public class Cartoon implements Book{

    //author와 tittle에 접근 할 수 있음

    public void publish(){

        // 꼭 구현해야함

    }

}


public class Novel implements Book{

    //author와 tittle에 접근 할 수 있음


    public void publish(){


        // 꼭 구현해야함


    }

}


이제 부터 진짜 중요한 부분이다. 이 부분을 이해해야 객체 지향의 진정한 의미를 알게 되는 것이다.


Cartoon c = new Cartoon();

Novel n = new Novel();


보통 일반적으로 저렇게 많이 선언해서 사용한다.

하지만 이렇게 한 번 해보자.

Book c = new Cartoon();

c.publish();//만화책 출판


Book n = new Novel();

n.publish();//소설책 출판


위에 의미를 이해 하겠는가? 클래스 형식이 인터페이스 형식인것이다.

Type이 인터페이스가 될 수 있다. 이렇게 됐을 때 인터페이스의 메소드를 호출하게 되면 실제는 생성된 클래스의 구상 메소드가 호출된다.

인터페이스라는 것을 통해서 클래스의 기본 틀을 얻을수 있으며 구상 클래스들에 접근할수 있다는 것을 보았다.


이렇게 하면 좋은 점이 무엇이냐 하면 구상 클래스에 의존하지 않은 인터페이스에 의존하는 프로그램을 작성할 수 있다는 것이다. 인터페이스에 의존한다는 것은 쉽게 말해서 고정된 틀에 얽매이지 않아도 되는 것이다. 물론 인터페이스의 틀에는 얽매이겠지만 적어도 공통된 틀이기 때문에 어느 클래스에도 접근할 수 있다는 것이다.



[Polymorphism]

0. 개요

지금껏 객체지향의 꽃은 '다형성'이라는 말을 많이 들어봤을 것이다. Java 내에서도 이 다형성이라는 놈을 구현하기 위해 여러 가지 장치를 마련해놓았다. 사실 다형성이란 단어를 한 마디로 압축해서 정의하기는 어렵지만, 필자가 지금껏 Java를 공부하면서 느낀 '다형성'은 '융통성', '확장성'과 일맥상통한다고 말하고 싶다. 이번 강좌는 '융통성(확장성)'에 촛점을 두고 다형성에 대한 이야기를 풀어나가고자 한다.

1. 사전적 의미
다형성에 대해 백과사전에서 찾아보면 동질이상, 즉 화학적인 조성은 같은데 결정구조에 따라 형태가 달라지는 현상을 언급하고 있다. 이를테면 CaCo₃과 같이 화학식은 같은데, 입체적인 구조에 따라 방해석과 아라고나이트로 형태가 달라지는 식이다. 이러한 내용을 정확히 프로그래밍에 대입하기는 어렵겠지만, 프로그래밍에서 넓은 의미의 다형성이라고 한다면 '한가지 표현 방식으로 여러 가지 기능을 수행할 수 있는 것'이라고 표현할 수 있다.

2. Interface
넓은 의미에서의 인터페이스란 외부와의 의사 소통을 위한 매개체이다. 의사 소통을 위해서는 외부와의 정해진 약속이 있어야 할 것이다. 이를 강조하는 의미에서 예전 강좌에서 인터페이스를 '표준 규격'이라고 설명한 적이 있다.

인터페이스를 정의하고 활용하는 이유는 물론 인터페이스를 구현하는 부품들이 중구난방이 되지 않기 위한 최소한의 규약을 정하기 위해서이지만, 이를 약간 넓게 생각해본다면 확장의 여지가 있는 부분을 클래스로 제작해 닫아버리기보다는 인터페이스로 선언하여 최소한의 선을 정해놓고 융통성을 발휘하고자 하는 의도도 포함되어 있다.

(1) 표준 규격

간단한 예로 컴퓨터에 키보드나 마우스를 연결한다고 생각해보자. 우리는 단지 키보드와 컴퓨터를 연결하는 단자가 PS/2 형식인지, 혹은 USB 형식인지를 확인하면 된다. 여기서 말하는 PS/2나 USB 포트는 일종의 규격이며, 형식에 맞게 제작된 키보드나 마우스는 해당 단자에, 구멍에 맞게 끼우기만 하면 어느 회사에서 만들었든간에 아무런 문제 없이 사용이 가능하다. 예전에 beginning 강좌에서 들었던 220V 콘센트의 예도 마찬가지일 것이다. Java 프로그래밍에서의 인터페이스의 기본 개념도 이러한 예들과 흡사하다.

인터페이스의 기본적인 의미는 인터페이스를 구현한 클래스들을 그룹화하고, 해당 그룹의 필수 메서드들을 선언함으로써 그룹을 형성하는 클래스들에게 외부와의 대화 방침을 대내외적으로 알리고자 하는 것이다. 인터페이스는 개발자에게는 인터페이스의 내용을 충실히 구현할 의무를 부여하고, 사용자에게는 인터페이스의 내용만 알면 해당 그룹의 클래스를 세부 명세 없이도 인터페이스 명세에 따라 사용할 수 있다는 메리트를 제공한다. 더 깊이 생각해보면 인터페이스는 본질적으로 encapsulation과 이어져 있다. 즉, 외부와의 대화를 인터페이스를 통해서만 할 수 있도록 하는 것이다.

(2) 확장성

사실 인터페이스는 공동 작업을 위해 존재한다. 혼자서 프로그램의 모든 부분을 코딩하고 수정하는 수준의 작은 규모의 작업이라면 인터페이스는 그다지 필요가 없다. 그래서 표준 규격이라는 비유가 적절한 것인지도 모르겠다. 실제로 Java에서 제공하는 기본 API를 살펴보면 이들 사이 인터페이스의 체계적인 모습에 감탄하게 될 것이다. 그러나 인터페이스는 단순히 규격을 정하는 데에서 그 의미를 다하지 않는다. 잘 선언된 인터페이스는 항상 확장성을 염두에 두고 있다.

예를 들자면 Review 강좌의 두번째 글의 예와 같을 것이다. logging에 대한 출력을 콘솔, 파일, DB, Mail 등 여러 가지 방식으로 할 수 있는데, 여기에 신속한 장애 대처를 위해 ERROR 레벨 로그를 관리자에게 SMS로 보내는 방법을 추가하고자 하는 상황이 있을 수 있다. 이때 logging 출력 라이브러리들을 인터페이스로 그룹화해서, 이들이 공통으로 구현해야 하는 로그 찍기 메서드를 선언만 해두었다면, SMS 출력용 클래스를 logging 인터페이스에 맞게 구현만 하면 간단하게 확장이 될 것이다.

(3) 인터페이스와 형변환, 그리고 다형성

흔히 상속, 인터페이스와 관련해서 다형성을 설명할 때 '상위 클래스 타입의 객체 참조 변수에 하위 클래스 인스턴스를 연결할 수 있다'고 말한다. 이제 위에서 설명한 내용을 생각해보면서 이 문장을 구체적으로 살펴보도록 하자.

// 인터페이스 Eproduct : 가전제품을 총칭하는 인터페이스
interface Eproduct { void disp(); }

// 가전제품 인터페이스를 구현하는 클래스들
TV implements Eproduct {

   int channel = 11;
   public void disp() { ... 저장된 채널값을 바탕으로 해당 채널을 TV 화면에 보여주는 내용 ... }
   public int setChannel(int ch) { this.channel = ch; }
   ... 다른 구현부

}

CDplayer Implements Eproduct {

   int cdNumber = 1;
   public void disp() { ... 오디오 CD의 현재 곡 번호를 액정에 표시하는 내용 ... }
   public int setCdNumber(int cdNum) { this.cdNumber = cdNum; }
   ... 다른 구현부

}

TV와 CD플레이어 클래스는 모두 가전제품 인터페이스를 구현하고 있다. 이들 클래스에서 인스턴스를 뽑아내는 방법은 다음의 두가지이다.

(a) TV tv = new TV(); CDplayer cdp = new CDplayer();
(b) Eproduct tv = new TV(); Eporduct cdp = new CDPlayer();

disp() 메서드를 활용하는 리모콘을 구현하는 메서드를 다음과 같다고 하자.

   remocon(Eproduct thing) { thing.disp(7); }

이제 remocon() 메서드에 tv와 cdp가 매개변수로 들어가는 경우를 나누어서 생각해보자.

(a) 자손형 객체변수를 인터페이스형 파라미터 공간에 대입하게 되면, 임시 upcasting이 일어난다. 즉, 매개변수 thing은 Eproduct형이지만, 결론적으로는 자손형 인스턴스를 참조할 수 있다.
(b) (a)에서 임시로 형변환이 이루어지는 과정을 아예 인스턴스 생성 부분에서 표현하고 있다. 즉, Eproduct 타입인 tv 객체 참조 변수로 TV 타입 인스턴스를 참조하고 있는 것이다. 이 경우 tv 변수로 TV 인스턴스의 setter 메서드인 setChannel()에 접근할 수 없다. (a)와 같은 경우에도 remocon 메서드 안에서 thing 지역 변수는 절대로 setChannel()이나 setCdNumber()에 접근할 수 없다.

(c) 그렇다면 다음과 같은 예는 어떠한가?

Eproduct tv = new TV();
TV realTV = (TV)tv;
realTV.setChannel(9);

결론만 이야기하자면 realTV 변수는 tv가 가리키는 인스턴스의 모든 것을 다시금 접근할 수 있게 된다. tv는 단지 Eproduct형 객체 참조 변수이기 때문에 TV 인스턴스의 고유 메서드에 접근할 수 없을 뿐이지, 인스턴스 자체에 손실이 있는 것은 아니다. 따라서 TV형 참조 변수를 이 인스턴스에 연결하는 것도 합법이며, RealTV는 아무런 손실 없이 tv가 가리키고 있던 TV형 인스턴스의 고유 메서드를 모두 사용할 수 있게 된다. 위와 같은 예를 downcasting이라고 하는데, (b)와는 달리 강제로 형변환하지 않으면 대입 자체가 불가능해진다.

(d) 확장성과 연관지어 생각해보자면, 위의 리모콘 메서드는 가전제품 인터페이스를 구현하는 두 종류의 인스턴스를 매개변수로 받고 있다. 가전제품 인터페이스를 구현하는 다른 클래스, 이를테면 MicowaveOven이라던가 AirConditioner와 같은 새로운 클래스를 제작하게 되더라도 리모콘 메서드에 변화를 주지 않고 Eproduct형으로 인스턴스를 받을 수 있을 것이다. 가전제품 인터페이스를 활용하는 개발자 입장에서는 전자렌지나 에어컨이 구현하는 인터페이스의 내용 disp()의 세부 구현 사항을 알 필요가 없다. 그저 이들의 disp()를 호출했을 때 '액정이나 화면에 핵심 정보를 출력하는 기능'을 수행한다는 인터페이스 명세서를 가지고 있으면 된다.

3. 상속, 인터페이스에서의 유의점 몇 가지
복습하는 김에 몇 가지 정리해보고 가자.

(1) 상속 (extends)

□ 기능 확장, 코드 재사용의 개념이다. 필드와 메서드를 상속받는다. 생성자는 상속되지 않는다.
□ 하위 클래스의 인스턴스가 생성될 때 자동으로 상위 클래스의 default 생성자가 호출된다. (super();)
□ 상위 클래스에 default 생성자를 기술하지 않았다면 다른 매개변수를 갖는 상위 생성자를 반드시 호출해야 한다.
□ 하위 클래스의 생성자에서 상위 클래스의 인수를 갖는 생성자를 호출하기 위해서 super(arg0, ..., argn);를 기술한다.
□ 메모리에는 상위 클래스의 메서드와 하위 클래스의 메서드가 모두 저장되어 있다.
□ 메서드 오버라이딩 시에 하위 클래스의 메서드가 우선권을 가진다. 상위 클래스의 메서드에 접근할 때에는 super를 써야 한다.
□ C++에서는 메서드 오버라이딩 시 상위 함수에 virtual 키워드를 적용해야 Java와 같은 방식의 메서드 동적 호출이 가능해진다.
□ 오버라이딩 시 하위 클래스 메서드의 접근 지정자는 부모 클래스 메서드의 접근 지정자 범위보다 크거나 같아야 한다.
□ 위 범위 공식은 메서드가 throws로 예외를 던질 때 예외 값의 범위에도 그대로 적용된다.

(2) 추상클래스 (abstract)

□ 추상메서드를 포함한다. (구현x) => 인스턴스 생성 불가, 상속받은 클래스를 통해 인스턴스를 생성한다.
□ 상속된 하위 클래스의 인스턴스 생성 시에 다형성 구현이 가능하다. 추상클래스형 변수 = new 하위클래스형 인스턴스;
□ 상속된 하위 클래스에서 추상 메서드를 반드시 재정의한다.

(3) 인터페이스 (interface)

□ 상수 필드 + 추상 메서드의 구조이다. 상수는 public static final, 추상 메서드는 public이며 생략해도 자동으로 추가된다.
□ 인터페이스를 상속받는 인터페이스 작성 가능 -> extends를 사용한다.
□ 인스턴스 생성 불가. 구현(상속) 클래스를 통해 인스턴스를 생성한다. 추상클래스와 마찬가지로 다형성 구현이 가능하다.
□ 추상으로 선언되어 구현되지 않은 모든 메서드는 인터페이스 제작자의 의도인 셈이다. 모든 메서드를 반드시 재정의할 것.
□ 인터페이스 선언 시에 생성자를 명시하지 않고 구현 클래스 또한 기본 생성자가 없는 경우 default 생성자가 자동 생성된다.
□ 그러나 구현 클래스에서 매개변수가 있는 생성자를 만들고 default 생성자를 구현하지 않으면 상속과 마찬가지로 error 발생.

4. Generics
Generics에 관한 내용을 첨가할까 말까 하다가 간단히만 설명하고 넘어가기로 결정했다. 자세한 내용은 추후에 다시 다뤄볼 생각이다. Generics는 C++의 템플릿과 같은 개념이다. (C#에서는 아예 Template이 Generics로 바뀌었으니 일치하는 내용이라고 봐도 무관할 것이다) Generics를 설명할 수 있는 가장 쉬운 예로는 Collection 계열 클래스에서 흔히 사용되는 List와 같은 컨테이너 인터페이스가 있다.

List myIntList = new LinkedList();
myIntList.add(new Integer(0));
Integer x = (Integer).myIntList.iterator().next();

myIntList는 List 인터페이스 타입으로 LinkedList 인스턴스를 생성하고 있다. Integer(0)은 정수 0을 Integer 클래스로 객체화되어 감싸진다.(wrapping) myIntList에 이 인스턴스를 넣고, 다시 Iterator로 뽑아내고 있다. 이 과정에서, List 인터페이스의 add는 Object형 매개변수를 받고 있으므로 Integer(0)은 upcasting이 되어 myIntList라는 컨테이너에 들어가고, Iterator에 의해 나올 때에도 Object형으로 리턴된다. 3행에서 리턴된 값을 (Integer)로 형변환하는 것을 주목해보자. 이렇게 downcasting해주지 않으면 Integer x 에 대입할 수가 없다.

문제는 바로 여기서 발생한다. 3행에서의 강제 형변환은 프로그램을 난잡하게 할 뿐만 아니라, 런타임 에러의 가능성을 발생시킨다. 위의 코드조각에서는 (Integer) 강제 형변환으로 해결하고 있지만, 사실 프로그래머의 의도대로 돌아가기 위해서는 컨테이너 차원에서 "이 컨테이너는 Integer형 인스턴스만을 저장할 수 있다"라고 명시하는게 논리적일 것이다. 애초에 리스트에 특정 타입만 들어갈 수 있도록 강제하는 것, 이것이 Generics의 핵심 개념이다. Generics를 이용해서 위의 코드를 바꾼다면 아래와 같을 것이다.

List<Integer> myIntList = new LinkedList<Integer>();
myIntList.add(new Integer(0));
Integer x = myIntList.iterator().next();

이제 강제 형변환 없이 논리적으로 리스트를 규정하고 활용하는 코드 조각이 완성되었다. 이러한 경우 List는 Integer를 받는 generic 인 터페이스라고 한다. 이 글에서 Generics에 관해 간략히 언급하고 넘어가는 이유는, 인터페이스와 관련된 형변환이 주로 컨테이너 클래스를 사용할 때 빈번히 일어나기 때문이다. 다형성과 연관지어 이런 내용이 있다는 정도만 알아두고, 나중에 generic 인터페이스를 작성하는 방법에 대해 구체적으로 살펴보도록 하자.

5. 광의의 다형성
우리가 보통 Java의 다형성을 이야기할 때 위의 내용을 주로 언급하지만, 넓은 의미로 살펴보면 보다 많은 다형성의 예를 살펴볼 수 있다. 이를테면 메서드의 매개변수의 다양성을 보장해주는 Overloading 또한 넓은 의미의 다형성 범주에 포함될 것이다. 예전 beginning 강좌 초반에 다형성의 예로 오버로딩을 든 것은 보다 쉽게 다형성의 의미를 심어주기 위함이었지만, 사실 Java에서의 다형성의 핵심은 상속과 인터페이스에서의 메서드 오버라이딩이라는 것을 이제는 알 수 있을 것이다.

: