Java 인터페이스(Interfcae)

yesjuhee·2024년 11월 17일

Java 공부

목록 보기
16/17

남궁성님의 Java의 정석 3판 "Chapter 07 객체지향 프로그래밍 2"을 읽고 저의 방식대로 정리 한 글입니다.

인터페이스는 오직 추상메서드와 상수만을 멤버로 가질 수 있다. 인터페이스는 구현된 것은 아무 것도 없고 밑그림만 그려져 있는 기본 설계도라 할 수 있다.

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

모든 멤버변수의 제어자는 public static final 이어야 하며, 이를 생략할 수 있다. 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있다. (단 JDK1.8부터 static 메서드와 디폴트 메서드를 추가할 수 있다.) 인터페이스에 정의된 모든 멤버에 예외없이 제공되는 사항이기 때문에 제어자를 생략해도 무방하다.

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

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

인터페이스도 추상클래스처럼 그 자체로는 인터페이스를 생성할 수 없고, 자신에게 정의된 추상메서드의 몸통을 만들어주는 클래스를 작성해야 한다. 만일 구현하는 인터페이스의 메서드 중 일부만 구현한다면, 추상클래스로 선언해야 한다. 인터페이스는 구현한다는 의미의 키워드 implements를 사용한다.

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

다형성에 대해 학습할 때 자손클래스의 인스턴스를 조상타입이 참조변수로 참조하는 것이 가능하다는 것을 배웠다. 인터페이스 역시 이를 구현한 클래스의 조상이라 할 수 있으므로 해당 인터페이스 타입의 참조변수로 이를 구현한 클래스의 인스턴스를 참조할 수 있으며, 인터페이스 타입으로 형변환도 가능하다.

인터페이스는 메서드 매개변수의 타입으로 사용될 수 있다. 인터페이스 타입의 매개변수가 갖는 의미는 메서드 호출 시 해당 인터페이스를 구현한 클래스의 인스턴스를 매개변수로 제공해야한다는 것이다. 메서드의 리턴타입으로 인터페이스의 타입을 지정하는 것 역시 가능하다. 리턴타입이 인터페이스라는 것은 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미한다.

interface Parserable {
	public abstract void parse(String filename);
}

class ParserManager {
	// 리턴 타입으로 인터페이스 사용
	public static Pareserble getParser(String type) {
		if (type.equals("XML")) {
			return new XMLParser();
		} else {
			// 참조변수로 인터페이스 사용
			Parseable p = new HTMLParser();
			return p;
		}
	}
}

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");
	}
}

인터페이스의 장점

  1. 개발시간을 단축시킬 수 있다.

    일단 인터페이스가 작성되면, 이를 사용해서 프로그램을 작성하는 것이 가능하다. 메서드를 호출하는 쪽에서는 메서드의 내용에 관계없이 선언부만 알면 되기 때문이다. 그리고 동시에 다른 한 쪽에서는 인터페이스를 구현하는 클래스를 작성하게 하면, 인터페이스를 구현하는 클래스가 작성될 때까지 기다리지 않고도 양쪽에서 동시에 개발을 진행할 수 있다.

  2. 표준화가 가능하다.

    프로젝트에 사용되는 기본 틀을 인터페이스로 작성한 다음, 개발자들에게 인터페이스를 구현하여 프로그램을 작성하도록 함으로써 보다 일관되고 정형화된 프로그램의 개발이 가능하다.

  3. 서로 관계없는 클래스들에게 관계를 맺어 줄 수 있다.

    서로 상속관계에 있지도 않고, 같은 조상클래스를 가지고 있지 않은 서로 아무런 관계도 없는 클래스들에게 하나의 인터페이스를 공통적으로 구현하도록 함으로써 관계를 맺어줄 수 있다.

  4. 독립적인 프로그래밍이 가능하다.

    인터페이스를 이용하면 클래스의 선언과 구현을 분리시킬 수 있기 때문에 실제구현에 독립적인 프로그램을 작성하는 것이 가능하다. 클래스와 클래스간의 직접적인 관계를 인터페이스를 이용해서 간접적인 관계로 변경하면, 한 클래스의 변경이 관련된 다른 클래스에 영향을 미치지 않는 독립적인 프로그래밍이 가능하다.

인터페이스 활용 예시 1

게임에 나오는 유닛을 클래스로 표현하고 이들의 관계를 상속계층도로 표현하면 그림과 같다. 이때 SCV 에게 TankDropship과 같은 기계화 유닛을 수리할 수 있는 기능을 제공하기 위해 repair 메서드를 정의하고 싶다고 해보자. 현재의 상속관계에서 이들만 따로 묶을 수 있는 공통점은 없지만, 인터페이스를 이용하면 기존의 상속체계를 유지하면서 이들 기계화 유닛에 공통점을 부여할 수 있다.

class RepairableTest {
	public static void main(String[] args) {
		Tank tank = new Tank();
		Dropship dropship = new Dropship();
		
		Marine marine = new Marine();
		SCV scv = new SCV(); // 정비공
		
		scv.repair(tank);
		scv.repair(dropship);
		scv.repair(marine); // Error: repair(Repairable) in SCV cannot be applied to (Marine)
	}
}

interface Repairable {} // 인스턴스의 타입체크에만 사용되고 정의도니 것은 아무것도 없는 인터페이스

class Unit { // 게임 유닛
	int hitPoint;
	final int MAX_HP;
	Unit(int hp) {
		MAX_HP = hp;
	}
	// ...
}
class GroundUnit extends Unit {
	GroundUnit(int hp) {
		super(hp)
	}
}
class AirUnit extends Unit {
	AirUnit(int hp) {
		super(hp);
	}
}
class Tank extends GroundUnit implements Repairable {
	Tank() {
		super(150);
		hitPoint = MAX_HP;
	}
	public String toString() {
		return "Tank";
	}
}
class Dropship extends AirUnit implements Repairable
	Dropship() {
		super(125);
		hitPoint = MAX_HP;
	}
	public String toString() {
		return "Dropship";
	}
}
class Marine extends GroundUnit {
	Marine() {
		super(40);
		hitPoint = MAX_HEAP;
	}
}

class SCV extends GroundUnit implements Repairable {
	SCV() {
		super(60);
		hitPoint = MAX_HP;
	}
	
	void repair(Repairable r) { // Repairable을 구현하는 클래스의 인스턴스
		if (r instanceof Unit) {
			Unit u = (Unit)r; // Unit으로 타입캐스팅 후 멤버변수와 클래스 사용
			while(u.hitPoint != u.MAX_HP) {
				u.hitPoint++;
			}
			System.out.println(u.toString() + "의 수리가 끝났습니다.");
		}
	}
}

인터페이스 활용 예시 2

게임에 나오는 건물들의 클래스로 표현하고 이들의 관계를 상속계층도로 표현하면 그림과 같다. 이 때 Barrack 클래스와 Factory 클래스에만 건물을 이동시키는 새로운 메서드를 추가하고자 한다면 어떻게 해야 할까? 이는 인터페이스를 이용해 해결할 수 있다.

  1. 우선 새로 추가하고자 하는 메서드를 정의하는 Liftable 인터페이스를 작성한다.
  2. Liftable 인터페이스를 구현하는 LiftableImpl 클래스를 작성한다.
  3. Barrack 클래스가 Liftable 인터페이스를 구현하도록 하고, 인터페이스를 구현한 LiftableImpl 클래스를 Barrack 클래스에 포함시켜서 내부적으로 호출해 사용한다.

이렇게 함으로써 같은 내용의 코드를 Barrack 클래스와 Factory 클래스에서 각각 작성하지 않고 LiftableImpl 클래스 한 곳에서 관리할 수 있다.

interface Liftable {
	void liftOff();
	void move(int x, int y);
	void stop();
	void land();
}

class LiftableImpl implements Liftable {
	public void liftOff() { ... }
	public void move(int x, int y) { ... }
	public void stop() { ... }
	public void land() { ... }
}
class Barrack extends Building implements Liftable {
	LiftableImpl l = new LiftableImpl();
	public void liftOff() { l.liftOff(); }
	public void move(int x, int y) { l.move(); }
	public void stop() { l.stop(); }
	public void land() { l.land(); }
	void trainMarine() { ... }
}
class Factory extends Building implements Liftable {
	LiftableImpl l = new LiftableImpl();
	public void liftOff() { l.liftOff(); }
	public void move(int x, int y) { l.move(); }
	public void stop() { l.stop(); }
	public void land() { l.land(); }
	void makeTank() { ... }
}

인터페이스 활용 예시 3

  • Provider - User 관계 클래스 A는 B클래스를 사용한다. 이때 A는 User, B는 Provider이다. 아래 예제에서 User A와 Provider B는 직접적인 관계가 있다. 두 클래스의 관계를 간접적으로 변경하기 위해서는 인터페이스를 이용해 클래스 B(Provider)의 선언과 구현을 분리해야한다.
    class A {
    	public void methodA(B b) {
    		b.methodB();
    	}
    }
    class B {
    	public void methodB() {
    		System.out.println("methodB()");
    	}
    }
    class InterfaceTest {
    	public static void main(String args[]) {
    		A a = new A();
    		a.methodA(new B());
    	}
    }
  • A - I - B 의 간접 관계 아래처럼 인터페이스 I를 정의해서 사용하면 클래스 A를 작성하는데 B를 사용할 필요가 없다. 클래스 A는 B의 이름을 몰라도 되고, 심지어는 실제로 구현된 클래스가 존재하지 않아도 문제되지 않는다.
    interface I {
    	public abstract void mehtodB();
    }
    
    class A {
    	public void methodA(I i) {
    		i.methodB();
    	}
    }
    class B implements I{
    	public void methodB() {
    		System.out.println("methodB() in B class");
    	}
    }
  • 인터페이스 구현 클래스의 동적 제공 예시 클래스 A가 인터페이스 I를 사용할 때 매개변수를 통해 인터페이스 I를 구현한 클래스의 인스턴스를 동적으로 제공받아야 한다. 클래스 Thread의 생성자인 Thread(Runnable target)이 이런 방식으로 되어 있다.
    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()); // B를 동적으로 제공
    		a.autoPlay(new C()); // C를 동적으로 제공
    	}
    }
  • 제3의 클래스를 통해 구현 클래스를 제공 받는 예시 인터페이스의 구현 클래스를 매개변수를 통해 동적으로 제공받을 수도 있지만, 다음과 같이 제3의클래스를 통해서 제공받을 수도 있다. JDBC의 DriverManager 클래스가 이런 방식으로 되어있다. 이렇게 하면 변경 사항이 있을 때 A의 변경 없이 getInstance() 만 변경하면 된다는 장점이 생긴다.
    class InterfaceTest3 {
    	public static void main(String[] args) {
    		A a = new A();
    		a.methodA(); // 매개변수 사용x
    	}
    }
    
    class A {
    	void methodA() {
    		I i = InstanceManager.getInstance(); // 제3의 클래스를 통해 구현 클래스의 인스턴스를 가져온다
    		i.play();
    		System.out.println(i.toString());
    	}
    }
    
    interface I {
    	public abstract void play();
    }
    
    class B implements I {
    	public void play() {
    		System.out.println("play in B class");
    	}
    	public String toString() { return "class B" }
    }
    
    class InstanceManager {
    	public static I getInstance {
    		return new B();
    	}
    }
    

디폴트 메서드와 static 메서드

원래는 인터페이스에 추상 메서드만 선언할 수 있었지만, JDK1.8부터 디폴트 메서드와 static 메서드도 추가할 수 있게 되었다. static 메서드는 인스턴스와 관계가 없는 독립적인 메서드이기 때문에 예전부터 인터페이스에 추가하지 못할 이유가 없었다. 인터페이스의 static 메서드는 접근 제어자가 항상 public이며, 생략할 수 있다.

디폴트 메서드는 기본적인 구현을 제공하는 메서드로, 추상 메서드가 아니기 때문에 디폴트 메서드가 새로 추가되어도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 된다. 디폴트 메서드는 앞에 키워드 default를 붙이며, 추상 메서드와 달리 일반 메서드처럼 몸통이 있어야 한다. 디폴트 메서드 역시 접근 제어자가 항상 public이며 생략 가능하다.

새로 추가된 디폴트 메서드가 기존의 메서드와 이름이 중복되어 충돌하는 경우가 발생할 경우 아래 규칙에 따라 충돌을 해결한다.

  1. 여러 인터페이스의 디폴트 메서드 간의 충돌
    : 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩해야 한다.
  2. 디폴트 메서드와 조상 클래스의 메서드 간의 충돌
    : 조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.
profile
https://yesjuhee.tistory.com/

0개의 댓글