본문 바로가기
Language/Java

[Java] 인터페이스(interface)

by 계범 2022. 3. 4.

인터페이스란?

인터페이스는 일종의 추상클래스이다.

인터페이스는 추상클래스와 달리 몸통을 갖춘 일반 메서드 또는 멤버변수를 구성원으로 가질 수 없다.

오직 추상메서드와 상수만을 멤버로 가질 수 있다.

 

추상 클래스를 '미완성 설계도'라고 한다면,

인터페이스는 구현된 것은 아무것도 없고 밑그림만 있는 '기본 설계도'라고 할 수 있다.

 

인터페이스의 작성

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

 

인터페이스의 멤버들은 제약사항이 있다.

- 모든 멤버변수는 public static final 이어야 하며, 생략할 수 있다.
- 모든 메서드는 public abstract 이어야 하며, 생략할 수 있다.
(단, static 메서드와 default 메서드는 예외[JDK1.8부터])

생략된 제어자는 컴파일러가 자동적으로 추가해준다.

 

인터페이스의 모든 메서드는 추상메서드여야했는데, JDK1.8부터 static메서드와 default 메서드의 추가를 허용하는 방향으로 변경되었다.

 

인터페이스의 상속

 

인터페이스는 클래스와 인터페이스가 상속 받을 수 있고,

인터페이스는 인터페이스로부터만 상속받을 수 있으며, 클래스와 달리 다중상속이 가능하다.

 

인터페이스의 구현

인터페이스도 추상 클래스처럼 그자체로는 인스턴스 생성이 불가하다.

인터페이스에선 구현한다는 의미로 키워드를 'implements'를 사용한다.

 

인터페이스가 인터페이스로부터 상속받을 땐, extends

클래스가 인터페이스를 상속받을땐 implements

 

class FighterTest {
	public static void main(String[] args) {
		Fighter f = new Fighter();

		if (f instanceof Unit)	{		
			System.out.println("f는 Unit클래스의 자손입니다.");
		}
		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인터페이스를 구현했습니다.");
		}
		if (f instanceof Object) {		
			System.out.println("f는 Object클래스의 자손입니다.");
		}
	}
}

class Fighter extends Unit implements Fightable {
	public void move(int x, int y) { /* 내용 생략 */ }
	public void attack(Unit u) { /* 내용 생략 */ }
}

class Unit {
	int currentHP;	// 유닛의 체력
	int x;			// 유닛의 위치(x좌표)
	int y;			// 유닛의 위치(y좌표)
}

interface Fightable extends Movable, Attackable { }
interface Movable {	void move(int x, int y);	}
interface Attackable {	void attack(Unit u); }

instanceof의 결과로는 모두 true로 출력문이 다 출력된다.

 

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

자바에선 다중상속의 단점때문에, 다중상속을 구현하는 경우는 거의 없다.

(클래스에선 아예 막아둠. 다중상속과 단일상속 참조 아래)

2022.03.04 - [Language/Java] - [Java] 객체지향 프로그래밍 -상속과 포함관계,Object클래스

 

인터페이스는 static상수만 정의할 수 있으므로 조상 클래스의 멤버변수와 충돌하는 경우는 거의 없고 충돌된다 하더라도 클래스 이름을 붙여서 구분이 가능하다. 그리고 추상 메서드는 구현내용이 없으므로 조상클래스의 메서드와 선언부가 일치하는 경우에는 당연히 조상 클래스 쪽의 메서드를 상속받으면 되므로 문제되지 않는다.

 

그러나 이렇게하면 다중상속의 장점을 읽게 된다.

그래서 이럴땐 다중상속보단 비중있는 한쪽을 선택하여 다른 한쪽은 클래스 내부에 멤버를 포함시키는 방식으로 처리하거나 어느 한쪽의 필요한 부분을 뽑아서 인터페이스로 만든 다음 구현하도록 한다.

 

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

다형성에 대해 학습할 때 자손클래스의 인스턴스를 조상타입의 참조변수로 참조하는 것이 가능하다는 것을 배웠다.

인터페이스도 구현한 클래스의 조상이라고 할 수 있으므로, 해당 인터페이스의 타입으로 형변환 가능하다.

 

Fightable f = (fightable) new Fighter();
Fightable f = new Fighter();

 

따라서 인터페이스는 메서드의 매개변수의 타입으로 사용될 수도 있다.

 

class Fighter extends Unit implements Fightable{
	public void move(int x, int y) { //... }
    public void attack(Fightable f) { //... }
}

인터페이스 타입의 매개변수가 갖는 의미는 메서드 호출 시 해당 인터페이스를 구현한 클래스의 인스턴스를 매개변수로 전달해줘야한다는 뜻이다.

attack(new Fighter())

또한 리턴타입이 인터페이스라는 것은 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미한다.

 

예제

interface Parseable {
	// 구문 분석작업을 수행한다.
	public abstract void parse(String fileName);
}

class ParserManager {
	// 리턴타입이 Parseable인터페이스이다.
	public static Parseable getParser(String type) {
		if(type.equals("XML")) {
			return new XMLParser();
		} else {
			Parseable p = new HTMLParser();
			return p;
			// return new HTMLParser();
		}
	}
}

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 ParserTest {
	public static void main(String args[]) {
		Parseable parser = ParserManager.getParser("XML");
		parser.parse("document.xml");
		parser = ParserManager.getParser("HTML");
		parser.parse("document2.html");
	}
}

Parseable 인터페이스는 구문분석을 수행하는 기능의 틀을 만들어 둔것.

추상메서드 'parse(String fileName)'이 구문 분석작업을 수행하도록 정의.

 

인터페이스를 상속받아 구현한 것이 XMLParser 와 HTMLParser.

 

ParserManager 클래스는 구문분석을 수행하는 것을 도와줄 클래스.

이안의 getParser("타입")메서드를 통해 타입에 알맞은 실제 수행할 객체를 반환해준다.

 

즉, 실ParserTest에서 ParserManager.getParser("타입"); 을 실행하면

"XML"일땐 구현 클래스 XMLParser를, 아니면 구현 클래스 HTMLParser를 반환한다.

 

이렇게되면 다른 종류의 구문분석기가 나와도 인터페이스를 상속받아 구현해둔 뒤,

ParserManager에서 해당 타입일때 해당 객체를 반환하게 해두면 된다.

 

이러한 장점은 분산환경 프로그래밍에서 좋다.

사용자 컴퓨터에 설치된 프로그램을 변경하지 않고 서버측의 변경만으로도 사용자가 새로 개정된 프로그램을 사용하는 것이 가능해진다.

 

인터페이스의 장점

  1. 개발시간을 단축시킬 수 있다.
    • 선언부를 알기에 인터페이스의 양식대로 여러명이서도 실제 구현 클래스를 개발할 수 있다.
  2. 표준화가 가능하다
    • 프로젝트에 사용되는 기본 틀을 인터페이스로 작성해둠으로써, 일관되고 정형화된 프로그램 개발이 가능하다.
  3. 서로 관계없는 클래스들에게 관계를 맺어 줄 수 있다.
    • 하나의 인터페이스를 공통적으로 구현함으로써 관계를 맺어 줄 수 있다.
  4. 독립적인 프로그래밍이 가능하다.
    • 클래스와 클래스 간의 직접적인 관계를 인터페이스를 이용하여 간접적인 관계로 변경하면, 한 클래스의 변경이 관련된 다른 클래스에 영향을 미치지 않게 되어 독립적인 프로그래밍이 가능하다.

 

인터페이스의 이해

 

위의 독립적인 프로그래밍이 가능하단 부분에서의 얘기를 풀어보자.

 

a클래스가 b클래스의 인스턴스를 생성하고 b 인스턴스의 메서드를 호출한다는 상황이면,

a와 b는 직접적인 관계로 되어 있다.

 

이럴 경우, b 클래스가 아닌 다른 클래스로 변경되면 a 클래스도 영향을 받는다.

 

하지만 인터페이스를 쓰면 이러한 변경사항에 영향을 받지 않는 간접관계로 놓이게 된다.

 

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

// 인터페이스로 간접관계 변경
class A{
	public void methodA(I i){
    	i.methodB();
    }
}

 

'A-B'의 직접적인 관계에서 'A-I-B'의 간접적인 관계로 바뀐 것이다.

이렇게 되면 B가 아닌 C로 변경되어도, A에는 영향이 없다. 매개변수로만 B인스턴스에서 C 인스턴스를 주면 된다.

 

즉, A클래스는 껍데기인 인터페이스만 알고, 실제로 사용하는 클래스는 몰라도 된다.

 

예제

 class A {
    void autoPlay(I i) {
          i.play();
     }
 }

 interface I {
      public abstract void play();
 }

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

 class C implements I {
     public void play() {
          System.out.println("play in C class");
     }
 }

class InterfaceTest2 {
	public static void main(String[] args) {
		A a = new A();
		a.autoPlay(new B()); // void autoPlay(I i)호출
		a.autoPlay(new C()); // void autoPlay(I i)호출
	}
}

 

클래스 Thread의 생성자인 Thread(Runnable target)이 이런방식으로 되어 있다. Runnable은 인터페이스.

 

이처럼 매개변수를 통해 동적으로 제공받을 수도 있지만,

아래와 같이 제3의 클래스를 통해 제공 받을 수도 있다.

class InterfaceTest3 {
	public static void main(String[] args) {
		A a = new A();
		a.methodA();
	}
}

 class A {
    void methodA() {
          I i = InstanceManager.getInstance();
		  i.methodB();
		  System.out.println(i.toString()); // i로 Object클래스의 메서드 호출가능
     }
 }

 interface I {
      public abstract void methodB();
 }

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

	  public String toString() { return "class B";}
 }

 class InstanceManager {
	public static I getInstance() {
		return new B();
	}
 }

인스턴스를 직접 생성하지 않고, getInstance()라는 메소드를 통해 제공받는다.

이 방법으로하면, 다른 클래스의 인스턴스로 변경해도 A클래스는 변경하지않고 getInstance()만 변경하면 된다는 장점이 있다.

 

디폴드 메서드와 static 메서드

인터페이스는 원래는 추상 메서드만 선언할 수 있는데, JDK1.8부터 디폴트 메서드와 static 메서드도 추가할 수 있게 되었다.

 

과거에 안되었던 이유 중 static메서드는 보다 인터페이스를 쉽게 배울 수 있게 인터페이스 메서드는 추상 메서드이어야한다는 규칙으로 못 썼다.

 

디폴트 메서드는 인터페이스가 변경될 때를 생각해서 고안되었다.

과거엔 인터페이스에 메서드가 추가 된다는 것은, 추상 메서드를 추가되는 것이고,

추상 메서드가 추가되면 그 아래 구현된 모든 클래스에 추가된 클래스를 구현했어야 했다.

 

그래서 디폴트 메서드라는 것을 만들고, 디폴트 메서드는 새로 추가되어도 구현된 클래스를 변경하지 않아도 되는 것으로 했다.

 

디폴트 메서드는 키워드 default를 앞에 붙이고, 추상 메서드와 달리 일반 메서드처럼 몸통{}이 있어야 한다.

디폴트 메서드와 static메서드는 접근 제어자가 public이며, 생략가능하다.

 

디폴트 메서드를 추가하면 조상 클래스에 새로운 메서드를 추가한 것과 동일해 진다.

하지만 2가지 문제점이 있다.

1. 여러 인터페이스의 디폴트 메서드 간의 충돌
-> 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩해야한다.
2. 디폴트 메서드와 조상 클래스의 메서드 간의 충돌
-> 조상 클래스이 메서드가 상속되고, 디폴트 메서드는 무시된다.
더보기

예시

class DefaultMethodTest {
	public static void main(String[] args) {
		Child c = new Child();
		c.method1();
		c.method2();
		MyInterface.staticMethod(); 
		MyInterface2.staticMethod();
	}
}

class Child extends Parent implements MyInterface, MyInterface2 {
	public void method1() {	
		System.out.println("method1() 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("method2() 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() in Child
method2() in Parent
staticMethod() in MyInterface
staticMethod() in MyInterface2

 

참조

'Java의 정석' 책

댓글