[Java] 7. 객체지향 프로그래밍 2 #7 인터페이스(interface)

2022. 5. 11. 17:45JAVA/Language

1. 인터페이스란 무엇인가?

인터페이스도 추상 클래스와 마찬가지로 일종의 추상 클래스입니다. 다만 추상 클래스와는 다른점은 일반 메서드 또는 멤버 변수를 구성원으로 가질 수 없습니다. 오직 추상 메서드와 상수만을 멤버로 가질 수 있습니다.

 

2. 인터페이스의 작성

interface 인터페이스이름{
    public static final 타입 상수이름 = 값;
    
    public abstract 리턴타입 메서드이름(매개변수 목록);
}

 

인터페이스 멤버들의 제약

  1. 모든 멤버 변수는 public static final이어야 하며, 이를 생략할 수 있음
  2. 모든 메서드는 public abstract이어야 하며, 이를 생략할 수 있음. 단, static 메서드와 디폴트 메서드는 예외(JDK 1.8~)
interface PlayingCard{
    public static final int SPADE = 4;    // public static final int SPADE = 4;
    final int DIAMOND = 3;                // public static final int DIAMOND = 3;
    static int HEART = 2;                 // public static final int HEART = 2;
    int CLOVER = 1;                       // public static final int CLOVER = 1;
    
    public abstract String getCardNumber();
    String getCardKind();                       // public abstract String getCardKind();
}

 

3. 인터페이스의 상속

  • 인터페이스는 인터페이스로부터만 상속받을 수 있음
  • 인터페이스는 클래스와는 달리 여러개의 인터페이스로부터 상속을 받을 수 있음 (다중 상속)
public interface Movable {
	void move(int x, int y);
}

public interface Attackable {
	void attack(Unit u);
}

public interface Fightable extends Movable, Attackable{
	
}

 

4. 인터페이스의 구현

class 클래스이름 implements 인터페이스이름{
	// 인터페이스에 정의된 추상 메서드를 구현 (오버라이딩)
}

class Fighter implments Fightable{
	public void move(int x, int y){
    	//...
    }
    
    public void attack(Unit u){
    	//...
    }
	
}

 

5. 인터페이스를 이용한 다중 상속

왜 클래스는 다중상속을 금지하는 이유는 무엇인가?

한 자손 클래스가 2개의 조상 클래스 A, B로부터 상속을 받는다고 가정할때 A클래스에 name이라는 필드 멤버가 존재하고 B클래스에도 name이라는 필드멤버가 존재하면 A, B 조상 클래스를 상속받는 자손 클래스는 어느 조상 클래스의 name 필드멤버를 참조해야할지 판단할 수 없기 때문입니다.

 

왜 인터페이스의 다중상속은 충돌이 발생하지 않는가?

첫번째 이유는 인터페이스의 필드 멤버는 기본적으로 상수일 수 밖에 없기 때문에 인터페이스간의 동일한 이름의 필드멤버(상수)가 존재하더라도 인터페이스명을 통해서 구분할 수 있습니다.

interface A{
	public static final PI = 3.14;	// 클래스 멤버, 상수, A.PI를 통해서 구분이 가능함
}

두번째 이유는 조상 클래스와 인터페이스간의 추상 메서드 선언부가 동일한 경우 컴파일러가 조상 클래스쪽의 추상 메서드를 상속받기 때문에 문제가 되지 않습니다. 

 

하지만 이렇게 하면 멤버(상수, 추상 메서드)의 충돌은 피할 수는 있지만 다중상속의 장점을 읽게 됩니다. 만일 두 개의 클래스로부터 상속을 받아야 할 상황이라면, 두 조상 클래스 중에서 비중이 높은 쪽을 선택하고, 다른 한쪽은 클래스 내부에 멤버로 포함시키는 방식으로 처리하거나, 어느 한쪽의 필요한 부분을 추출하여 인터페이스로 만든 다음 구현하도록 하는 방법이 존재합니다.

 

예를 들어 TV 클래스와 VCR 클래스가 존재할때 이 두 클래스를 상속받는 TVCR 클래스를 정의하기를 원한다고 가정합니다. 하지만 두 클래스를 다중상속 받는 것은 안되기 때문에 TV 클래스만 상속을 받고 VCR 클래스는 필드 멤버로 포함하여 호출합니다.

public class TV{
    protected boolean power;
    protected int channel;
    rpotected int volume;
    
    public void power(){ power = !power;}
    public void channelUp(){ channel++;}
    public void channelDown(){ channel--;}
    public void volumeUp(){ volume++;}
    public void volumeDown(){ volume--;}
    
}
public class VCR{
	protected int counter;	// VCR의 카운터
    
    public void play() { // 재생 }
    public void stop() { // 재생을 멈춤 }
    public void reset() { counter = 0}
    public int getCounter() { return counter; }
    public void setCounter(int c) { counter = c; }
}

 

위 VCR 클래스에 정의된 메서드와 일치하는 추상메서드를 갖는 인터페이스를 정의

interface IVCR{
    public void play();
    public void stop();
    public void reset();
    public int getCounter();
    public void setCounter(int c);
}

 

TV 클래스는 상속을 받고, VCR 클래스는 필드멤버로 포함하고 IVCR 인터페이스를 다음과 같이 구현합니다.

public class TVCR extends TV implements IVCR{
    VCR vcr = new VCR();
    
    public void play() {
    	vcr.play();
    }
    
    public void stop() {
    	vcr.stop();
    }
    
    public void reset() {
    	vcr.reset();
    }
    
    public int getCounter() {
    	return vcr.getCounter();
    }
    
    public void setCounter(int c) {
    	vcr.setCounter(c);
    }
}

 

 

6. 인터페이스를 이용한 다형성

자손클래스의 인스터스를 조상타입의 참조변수로 참조하는 것이 가능합니다. 인터페이스 역시 인터페이스 타입의 참조변수가 인터페이스를 구현한 클래스의 인스턴스를 가리킬 수 있습니다.

 

Fightable f = new Fighter();	// (Fightable) new Figher();

 

인터페이스의 다형성을 이용하여 메서드의 매개변수로 정의하여 인터페이스를 구현한 클래스의 인스턴스를 인자로써 받을 수 있습니다. 이러한 법칙은 리턴 타입에도 적용됩니다.

void attack(Fightable f){
	//...
}

attack(new Fighter());

 

7. 인터페이스의 장점

  • 개발시간 단축
  • 표준화 가능
    • 인터페이스의 추상 메서드를 선언함으로써 일관되고 정형화된 프로그램 개발이 가능함
  • 서로 관계없는 클래스들에게 관게를 맺어 줄 수 있음
    • 서로 상속 관계에 있지도 않고, 같은 조상 클래스를 가지고 있지 않은 클래스들에게 하나의 인터페이스를 공통적으로 구현하도록 함으로써 관계를 맺어 줄 수 있음
  • 독립적인 프로그래밍이 가능함
    • 인터페이스를 이용하여 클래스의 선언과 구현을 분리시킴
    • 한 클래스의 변경이 관련된 다른 클래스에 영향을 미치지 않음

 

8. 인터페이스의 이해

두 클래스간의 관계 (한 클래스가 다른 한쪽의 클래스를 참조한다고 가정)

  • 클래스를 사용하는 쪽(User)과 클래스를 제공하는 쪽(Provider)이 존재합니다.
  • 메서드를 호출하는 쪽(User)에서는 호출하려는 메서드(Provider)의 선언부만 알면 됩니다. (내용은 몰라도 됨)

위 관계를 예제로 표현하면 다음과 같습니다.

public class A {
	public void methodA(B b) {
		b.methodB();
	}
}

public class B {
	public void methodB() {
		System.out.println("methodB()");
	}
}

public class InterfaceTest {

	public static void main(String[] args) {
		A a = new A();
		a.methodA(new B());	// Expected Output : methodB()
	}

}

위와 같이 클래스 A는 클래스 B의 인스턴스를 인자로 받아서 메서드(methodB)를 호출합니다. 이 두 클래스는 서로 직접적인 관게에 있습니다. 이를 그림으로 표현하면 다음과 같습니다.

이 경우 클래스 A를 구현하기 위해서는 클래스 B가 먼저 구현되어 있어야 합니다. 그리고 클래스 B의 method()의 선언부가 변경되면, 이를 사용하는 클래스 A도 변경되어야 합니다. 이 문제를 정의하면 다음과 같습니다.

직접적인 관계의 두 클래스는 한쪽(Provider, B)가 변경되면 
다른 한쪽(User, A)도 변경되어야 한다는 단점이 존재

 

인터페이스를 활용한 직접적인 관계를 해소

클래스 A가 클래스 B를 직접 호출하지 않고 인터페이스를 매개체로 해서 클래스 A가 인터페이스를 통해서 클래스 B의 메서드에 접근하도록 한다면, 클래스 B의 변경사항이 생기거나 클래스 B와 같은 기능의 다른 클래스로 대체 되어도 클래스 A는 영향을 받지 않도록 됩니다.

interface I{
    public void methodB();
}

class B implements I{
    public void methodB(){
        System.out.println("method B in B class");
    }
}

class A{
    public void methodA(I i){
    	i.methodB();
    }
}

위와 같이 클래스 A는 클래스 B를 몰라도 사용하는데 문제가 없습니다. 클래스 Thread의 생성자인 Thread(Runnable target) 메서드도 이러한 방식으로 정의되어 있습니다.

 

9. 디폴트 메서드와 static 메서드

  • JDK1.8부터 인터페이스에 디폴트 메서드와 static 메서드도 추가할 수 있게됨

디폴트 메서드

  • 추상 메서드의 기본적인 구현을 제공하는 메서드
  • 추상 메서드가 아니기 때문에 디폴트 메서드가 추가되어도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 됨
  • 키워드는 "default"
interface MyInterface{
    void method();
    default void newMethod(){
    	//...
    }
}

 

만약 디폴트 메서드가 기존의 메서드와 이름이 중복되어 충돌이 발생하는 경우 다음과 같은 규칙을 따릅니다.

  1. 여러 인터페이스의 디폴트 메서드간의 충돌하는 경우
    • 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩해야함
  2. 디폴트 메서드와 조상 클래스의 메서드간의 충돌
    • 조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시됨

디폴트 메서드 예제

public interface MyInterface {
	default void method1() {
		System.out.println("method1() in MyInterface");
	}
	
	default void method2() {
		System.out.println("method2() in MyInterface");
	}
	
	static void staticMethod() {
		System.out.println("staticMethod() in MyInterface");
	}
}

public interface MyInterface2 {
	default void method1() {
		System.out.println("method1() in MyInterface2");
	}
		
	static void staticMethod() {
		System.out.println("staticMethod() in MyInterface2");
	}
}

public class Parent {
	public void method2() {
		System.out.println("method2() in Parent class");
	}
}

public class Child extends Parent implements MyInterface, MyInterface2{
	
	public void method1() {
		System.out.println("method1() in Child");	// 오버라이딩
	}
}

public class DefaultMethodTest {

	public static void main(String[] args) {
		Child c = new Child();
		c.method1();	// 여러 인터페이스의 디폴트 메서드 간의 충돌 : 
						// 인터페이스를 구현한 클래스(Child)에서 디폴트 메서드를 오버라이딩해야함
		
		c.method2();	// 디폴트 메서드와 조상클래스 간의 메서드 충돌 : 
						//조상 클래스의 메서드가 상속됨
		MyInterface.staticMethod();
		MyInterface2.staticMethod();
	}

}
method1() in Child
method2() in Parent class
staticMethod() in MyInterface
staticMethod() in MyInterface2

 

References

source code : https://github.com/yonghwankim-dev/java_study
Java의 정석, 남궁 성 지음