[자바의 정석] Chapter 7 객체 지향 프로그래밍II (8) - 인터페이스(interface)
인터페이스(interface)
인터페이스란?
o 인터페이스는 추상클래스의 일종으로 추상클래스와 비슷하나 약간의 다른 점이 있다. 구현부를 갖춘 메서드 또는 멤버 변수를 멤버로 가질 수 없으며 오직 추상 메서드와 상수만을 멤버로 가질 수 있다.
o 추상 클래스가 미완성 설계도였으면 인터페이스는 구현된 것은 아무것도 없이 밑그림만 그려진 기본 설계도라 할 수 있다.
o 인터페이스도 자체만으로 사용되기 보다 다른 클래스를 작성하는데 도움 줄 목적으로 작성된다.
인터페이스 작성
o 인터페이스 작성은 class키워드 대신 interface를 사용하며 접근 제어자 또한 클래스와 같이 public 또는 default를 사용할 수 있다. 그러나 클래스 멤버와 달리 인터페이스 멤버들은 다음과 같은 제약사항이 있다.
- 모든 멤버변수는 public static final 이어야 하며, 이를 생략할 수 있다. - 모든 메서드는 public abstract이어야 하며, 이를 생략할 수 있다. [단, static메서드와 디폴트 메서드는 예외(JDK1.8부터)] |
o 인터페이스에 정의된 모든 멤버는 예외 없이 적용되는 사항이기에 제어자를 생략할 수 있는 것이며, 편의상 생략하는 경우가 많다. 생략된 제어자는 컴파일러가 자동으로 추가해준다.
o JDK1.8 이전 버전은 인터페이스의 모든 메서드가 추상 메서드였으나 JDK1.8 이후부터는 인터페이스에 static메서드와 디폴트 메서드의 추가를 허용하기에 알아두어야 한다.
|
|
|
인터페이스의 상속
o 인터페이스는 인터페이스만 상속받을 수 있으며 클래스와 달리 다중 상속, 즉 여러 인터페이스의 상속을 받는 것이 가능하다.
o 인터페이스는 클래스와 달리 Object클래스 같은 최고 조상이 없다.
o 클래스와 마찬가지로 자손 인터페이스는 조상 인터페이스의 모든 멤버를 상속받는다.
interface Movable {
void move (int x, int y);
}
interface Attackable {
void attack(Unit u);
}
interface Fightable extends Movable, Attackable {/*상속받은 멤버 메소드(move,attack) 사용가능*/}
|
인터페이스 구현
o 인터페이스도 추상클래스처럼 이 자체로는 인터페이스를 생성할 수 없으며, 상속을 통해 몸통을 만들어주는 클래스를 작성해야 한다. 클래스의 extends키워드 대신에 구현한다는 의미인 ‘implements’키워드를 사용하면 된다.
o 인터페이스를 구현하는 클래스는 추상화 클래스와 마찬가지로 인터페이스의 모든 메서드를 구현해야 하며 일부만 구현할 경우 abstract키워드를 붙여 추상 클래스로 선언해야 한다.
// ‘Fighter클래스는 Fightable인터페이스를 구현한다.’라고 한다.
class Fighter implements Fightable {
public void move(int x, int y) { }
public void attack(Unit u) { }
}
// 구현하려는 인터페이스의 모든 메서드가 구현되지 않은 경우 abstract를 붙인다.
abstract class Fighter implements Fightable {
// public void move(int x, int y) { }
public void attack(Unit u) { }
}
상속과 구현을 동시에 할 수도 있다.
class Fighter extends Unit implements Fightable {
public void move(int x, int y) { }
public void attack(Unit u) { }
}
|
※인터페이스 이름에는 주로 ~able(~할 수 있는)로 끝나는 것들이 많다. 어떠한 기능 또는 행위를 하는데 필요한 메서드를 제공한다는 의미를 강조하기 위해서다. 또한 그 인터페이스를 구현한 클래스는 ‘~를 할 수 있는’능력을 갖추었다는 의미이기도 한다.
package JavaProject;
public class Test {
public static void main(String[] args) {
Fighter f = new Fighter();
if(f instanceof Unit)
System.out.println("f는 Unit클래스의 자손입니다.");
if(f instanceof Object)
System.out.println("f는 Object클래스의 자손입니다.");
if(f instanceof Fightable)
System.out.println("f는 Fightable인터페이스를 구현했습니다.");
if(f instanceof Movable)
System.out.println("f는 Movable인터페이스를 구현했습니다.");
if(f instanceof Attackable)
System.out.println("f는 Attackable인터페이스를 구현했습니다.");
}
}
class Fighter extends Unit implements Fightable {
public void move(int x, int y) {/*내용 생략*/}
public void attack(Unit u) {/*내용 생략*/}
}
abstract class Unit {
int currntHP;
int x;
int y;
}
interface Fightable extends Movable, Attackable {/*상속받은 멤버 메소드(move,attack) 사용가능*/}
interface Movable { void move (int x, int y); }
interface Attackable { void attack(Unit u); }
|
- Fighter 클래스는 Unit클래스를 상속받고 Fightable인터페이스를 구현했지만 그 위에 조상도 있기에 이 모든 클래스와 인터페이스의 자손이라고 할 수 있다.
- 여기서 Movable인터페이스에 정의된 ‘void move(int x, int y)’를 Fighter클래스에서 구현할 때 접근 제어자를 public으로 했다는 것을 주의 깊게 봐야한다.
- 오버라이딩 시에서는 조상 메서드보다 넓은 범위의 접근 제어자를 지정해야 한다. Movable인터페이스에 정의된 ‘void move(int x, int y)’는 public abstract가 생략된 것이기에 실제는 ‘public abstract void move(int x, int y)’이다. 그래서 Fighter에서 구현 시 접근제어자를 public으로 해주어야 한다.
인터페이스를 이용한 다중상속
o 두 조상으로부터 상속을 받을 경우 멤버 중 멤버변수가 같거나 메서드 선언부는 일치하는데 내용이 다르면 어느 조상의 것을 상속받는지 알 수 없기에 한쪽 상속을 포기하던가 조상 클래스를 변경해야 한다. 다중 상속은 이러한 단점으로 인해 자바에서는 허용하지 않으며 이에 대한 대응으로 인터페이스를 이용한 다중 상속이 있다. 그러나 자바에서 인터페이스를 이용해 다중 상속을 하는 경우는 거의 없다.
o 인터페이스는 static상수만 정의할 수 있으므로 조상클래스 멤버 변수와 충돌하는 경우는 거의 없으며 충돌된다 하더라도 클래스 이름을 붙여 구분이 가능하다. 추상메서드 또한 구현 내용이 없으므로 선언부가 일치하는 경우에는 상속받으면 된다.
o 그렇지만 이럴 경우 다중 상속의 장점을 읽게 되기에 두 개의 클래스를 상속받아야 하는 상황이면 비중이 높은 클래스를 상속받고 나머지 다른 클래스는 클래스 내부에 멤버로 포함시키거나 어느 한쪽의 필요한 부분을 뽑아서 인터페이스로 만든 다음 구현하도록 한다.
|
|
|||
|
||||
|
- 인터페이스를 구현하기 위해 새로 메서드를 작성해야 하는 부담이 있지만 이처럼 클래스의 인스턴스를 사용하면 다중상속을 구현할 수 있다.
- VCR클래스가 변경되어도 TVCR클래스에도 자동으로 반영되는 효과도 있다.
인터페이스를 이용한 다형성
o 클래스 다형성에서 조상클래스 타입의 참조변수로 자손클래스 타입의 인스턴스를 참조할 수 있었다. 인터페이스 또한 이를 구현한 클래스의 조상이라 할 수 있기에 구현한 클래스의 인스턴스를 참조할 수 있으며 형변환도 가능하다.
Fightable f = (Fightable)new Fighter();
또는
Fightable f = new Fighter();
(Fightable정의된 멤버만 호출 가능)
|
o 인터페이스는 메서드의 매개변수 타입으로도 사용이 될 수 있으며 메서드 호출 시 해당 인터페이스를 구현한 클래스의 인스턴스를 매개변수로 제공해야 한다.
o 메서드의 리턴 타입으로 인터페이스를 지정하는 것도 가능하며 이 또한 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 의미이다.
package JavaProject;
public class Test {
public static void main(String[] args) {
// 인터페이스 참조변수를 통해 클래스의 인스턴스를 받는다.
Parseable parser = ParserManager.getParser("XML");
// Parseable parser = new XMLParser();와 같다.
parser.parse("document.xml");
parser = ParserManager.getParser("HTML");
parser.parse("document2.html");
}
}
interface Parseable {
public abstract void parse(String fileName);
}
class XMLParser implements Parseable {
public void parse(String fileName) {
System.out.println(fileName+"- XML parsing completed.");
}
}
class HTMLParser implements Parseable {
public void parse(String fileName) {
System.out.println(fileName+"- HTML parsing completed.");
}
}
class ParserManager {
// 리턴 타입이 Parseable 인터페이스인 메서드
public static Parseable getParser(String type ) {
if(type.equals("XML")) {
return new XMLParser();
} else {
Parseable p =new HTMLParser();
return p;
}
}
}
|
- XMLParser, HTMLParser 클래스는 Parseable인터페이스를 구현하는 클래스이다. ParserManager클래스의 getParser메서드에서 넘겨 받는 매개 변수 값에 따라 다른 인스턴스를 반환한다.
- parser 변수는 넘겨 받은 인스턴스 값에 따라 참조하게 된다.
인터페이스의 장점
o 인터페이스를 사용하는 장점은 다음과 같다.
1. 개발시간을 단축시킬 수 있다.
인터페이스가 작성되면 이를 사용해서 프로그램을 작성하는 것이 가능하다. 메서드를 호출하는 쪽에서는 메서드 내용에 관계없이 선언부만 알면 되고 다른 한 쪽에서는 인터페이스를 구현하는 클래스를 작성한다. 이러면 인터페이스를 구현하는 클래스가 작성될 때까지 기다리지 않고도 양쪽에서 개발을 동시에 진행할 수 있다.
2. 표준화가 가능하다.
프로젝트에 사용되는 기본 들을 인터페이스로 작성한 다음, 개발자들에게 인터페이스를 구현하여 프로그램을 작성하도록 함으로써 보다 일관되고 정형화된 프로그램의 개발이 가능하다.
3. 서로 관계없는 클래스들에게 관계를 맺어줄 수 있다.
서로 상속관계에 있지도 않고, 같은 조상클래스를 가지고 있지 않은 서로 아무런 관계도 없는 클래스들에게 하나의 인터페이스를 공통적으로 구현하도록 함으로써 관계를 맺어줄 수 있다.
4. 독립적인 프로그래밍이 가능하다.
인터페이스를 이용하면 클래스의 선언과 구현을 분리시킬 수 있기에 실제 구현에 독립적인 프로그램을 작성하는 것이 가능하다. 클래스와 클래스 간의 직접적인 관계를 인터페이스를 이용해서 간접적인 관계로 변경하면, 한 클래스의 변경이 관련된 다른 클래스에 영향을 미치지 않는 독립적인 프로그래밍이 가능하다.
인터페이스의 이해
o 인터페이스를 본질적인 측면을 이해하기 위해서는 다음 두 가지 사항을 반드시 염두에 두어야 한다.
- 클래스를 사용하는 쪽(User)과 클래스를 제공하는 쪽(Provider)이 있다. - 메서드를 사용(호출)하는 쪽(User)에서는 사용하려는 메서드(Provider)의 선언부만 알면 된다. (내용은 몰라도 된다.) |
o 두 클래스 간의 관계가 직접적인 관계일 경우 한쪽에서 변경 시 다른 한쪽에도 영향이 있지만 간접적인 관계로 만들면 한쪽에서 변경이 되어도 다른 한쪽에는 영향이 없다. 인터페이스를 이용해서 클래스의 선언과 구현을 분리하여 간접 두 클래스 간의 관계를 간접적으로 변경할 수 있다.
o 클래스를 사용하는 쪽(User)에서는 인터페이스를 통해 실제로 사용하는 클래스의 이름을 몰라도 되고 심지어 실제로 구현된 클래스가 존재하지 않아도 문제되지 않는다.오직 직접적인 관계에 있는 인터페이스의 영향만 받는다.
인터페이스 사용 전 (직접 관계) |
인터페이스 사용 후 (간접 관계) |
||
|
|
||
![]() |
![]() |
||
- 인터페이스를 사용하기 전에는 클래스 A와 B가 직접적인 관계에 있다. B클래스의 선언부가 변경되면 A클래스에서도 변경해 주어야한다. - 이와 같이 직접적인 관계에서는 한쪽이 변경되면 다른 한 쪽도 변경되어야 하는 단점이 있다. |
- 클래스 A와 B의 관계가 ‘A-B’ 직접적인 관계에서 ‘A-I-B’간접적인 관계로 바뀌었다. - 클래스 A는 인터페이스 I를 호출하기에 클래스 B에서 변경사항이 발생하여도 영향을 받지 않는다. |
o 간접 적인 관계를 이용하여 인스턴스를 제공받는 방식은 아래와 같다.
매개변수를 이용한 인스턴스 할당 |
제3의 클래스를 이용한 인스턴스 할당 |
||
|
|
||
- 클래스A에서 매개변수를 통해 인터페이스 I를 구현한 클래스의 인스턴스를 동적으로 제공받는다. |
- 인스턴스를 직접 생성하지 않고, getInstance( )메서드를 통해 제공받는다. 이러면 나중에 다른 클래스의 인스턴스로 변경되어도 A클래스 변경없이 getInstance( )만 변경하면 된다는 장점이 있다. |
디폴트 메서드와 static 메서드
o 원래 인터페이스에는 추상 메서드만 선언할 수 있었으나 JDK1.8부터 디폴트 메서드와 static메서드도 추가할 수 있게 되었다. static은 인스턴스와 관계없는 독립적인 메서드이기 때문에 추가해도 무방하다.
디폴트 메서드(Default Method)
o 인터페이스에서 메서드를 추가한다는 것은 추상 메서드를 추가한다는 것인데 이럴 경우 이를 구현한 모든 클래스에서 추가해 주어야 하기에 큰 일이 될 수 있다.
o 그래서 디폴트 메서드(default method)가 고안되었다. 디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드로 추상 메서드가 아니기에 디폴트 메서드가 추가되어도 구현한 클래스를 변경하지 않아도 된다.
o 메서드 앞에 ‘default’키워드를 붙이며 추상 메서드와 달리 몸통{ }이 있어야 한다. 접근 제어자는 디폴트 메서드 역시 public이며 생략 가능하다.
|
|
- 추상 메서드를 추가하는 대신 오른쪽과 같이 디폴트 메서드를 추가하면 기존의 인터페이스를 구현한 클래스를 변경하지 않아도 된다.
- 조상클래스에 새로운 메서드를 추가한 것과 동일해지는 것이다.
o 새로 추가된 디폴트 메서드가 기존의 메서드와 이름이 중복되어 충돌하는 경우가 발생하는데 이 충돌을 해결하는 규칙은 다음과 같다.
1. 여러 인터페이스의 디폴트 메서드 간의 충돌 2. 디폴트 메서드와 조상 클래스의 메서드 간의 충돌 - 조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다. |
package JavaProject;
public class Test {
public static void main(String[] args) {
Child c= new Child();
c.method1(); // MyInterface, MyInterface2 메서드 중복 -> 오버 라이딩
c.method2(); // Parent, MyInterface 메서드 중복 -> 조상 클래스(Parent) 상속
MyInterface.staticMethod();
MyInterface2.staticMethod();
}
}
class Child extends Parent implements MyInterface, MyInterface2 {
@Override
public void method1() {
System.out.println("method() in Child");
}
}
class Parent {
public void method2() {
System.out.println("method2() in Parent");
}
}
interface MyInterface {
default void method1() {
System.out.println("method1() in MyInterface");
}
default void method2() {
System.out.println("mehtod2() in MyInterface");
}
static void staticMethod() {
System.out.println("staticMethod() in MyInterface");
}
}
interface MyInterface2 {
default void method1() {
System.out.println("method1() in MyInterface2");
}
static void staticMethod() {
System.out.println("staticMethod() in MyInterface2");
}
}
|
- method1의 경우 MyInterface, MyInterface2에서 디폴트 메서드가 중복되었으나 오버라이딩하여 출력했다.
- mehtod2의 경우 Parent 클래스와 MyInterface가 서로 중복이 되었다. 조상 클래스 메서드가 상속되고 디폴트 메서드는 무시됨에 따라 조상 클래스의 출력문이 출력되었다.
교재 외) 추상클래스와 인터페이스 차이