Posts :book: 자바의 정석 #7.7: 인터페이스
Post
Cancel

:book: 자바의 정석 #7.7: 인터페이스

인터페이스

인터페이스란?

일종의 추상클래스지만 보다 추상화 정도가 높아 몸통을 갖춘 일반 메서드 또는 멤버변수를 구성원으로 가질 수 없다. 오직 추상메서드와 상수만을 멤버로 가질 수 있다.

인터페이스의 작성

인터페이스 멤버 제약사항

  • 모든 멤버변수는 public static final이어야 하며, 이를 생략할 수 있다.
  • 모든 메서드는 public abstract이어야 하며, 이를 생략할 수 있다.
    • 단, static 메서드와 default 메서드 예외 (JDK 1.8부터)

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

인터페이스의 상속

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

자손 인터페이스는 조상 인터페이스에 정의된 멤버를 모두 상속받는다.

인터페이스의 구현

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

인터페이스의 이름은 주로 ‘able’로 끝나는데, 이는 어떠한 기능 또는 행위를 하는데 필요한 메서드를 제공한다는 의미를 강조하기 위해서다. 그 인터페이스를 구현한 클래스는 ‘~를 할 수 있는’ 능력을 갖추었다는 의미이기도 하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 인터페이스를 구현했습니다.");
        }
        
    }
}

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

class Unit {
    int currentHP;
    int x;
    int y;
}

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

인터페이스는 상속 대신 구현이라는 용어를 사용하지만, 인터페이스로부터 상속받은 추상메서드를 구현하는 것이기 때문에 인터페이스도 조금 다른 의미의 조상이라고 할 수 있다.

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

자바도 인터페이스를 이용하면 다중상속이 가능하지만 자바에서 인터페이스로 다중상속을 구현하는 경우는 거의 없다.

다중상속의 단점은 두 조상으로부터 상속받는 멤버 중에서 멤버 변수의 이름이 같거나 메서드의 선언부가 일치하고 구현 내용이 다르다면 두 조상으로부터 상속받는 자손 클래스는 어느 조상의 것을 상속받게 되는 것인지 알 수 없다는 것이다.

그러나 인터페이스는 static 상수만 정의할 수 있으므로 조상클래스의 멤버변수와 충돌하는 경우는 거의 없고 충돌된다 하더라도 클래스 이름을 붙여서 구분이 가능하다. 또한 추상메서드는 구현내용이 전혀 없으므로 단점은 문제가 되지 않는다.

방법

두 조상클래스 중에서 비중이 높은 쪽을 선택하고 다른 한쪽은 클래스 내부에 멤버로 포함시키는 방식으로 처리하거나 어느 한 쪽의 필요한 부분을 뽑아서 인터페이스로 만든 다음 구현한다.

예를 들어, 다음과 같이 Tv클래스와 VCR 클래스로부터 상속을 받아 TVCR 클래스를 작성하고 싶다면, 한 쪽만 선택하여 상속받고 나머지 한 쪽은 클래스 내에 포함시켜서 내부적으로 인스턴스를 생성해서 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Tv {
    protected boolean power;
    protected int channel;
    protected int volumn;
    
    public void power() { power = !power; }
    public void channelUp() { channel++; }
    public void channelDown() { channel--; }
    public void volumnUp() { volumn++ }
    public void volumnDown() { volumn-- }
}

public class VCR {
    protected int counter;
    public void play(){}
    public void stop(){}
    public void reset(){
        counter = 0;
    }
    public int getCounter(){
        return counter;
    }
    public int setCounter(int c) {
        counter = c;
    }
}

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

1
2
3
4
5
6
7
public interface IVCR {
    public void play();
    pubilc void stop();
    public void reset();
    public int getCounter();
    public void setCounter(int c);
}

IVCR 인터페이스를 구현하고 Tv 클래스로부터 상속 받는 TVCR 클래스를 작성한다. 이때 VCR 클래스 타입의 참조변수를 멤버변수로 선언하여 IVCR인터페이스의 추상메서드를 구현하는데 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TVCR extends Tv implements IVCR {
    VCR vcr = new VCR();
    
    public void play() {
        vcr.play(); // 코드를 작성하는 대신 VCR 인스턴스의 메서드를 호출한다.
    }
    public void stop() {
        vcr.stop();
    }
    public void reset() {
        vcr.reset();
    }
    public int setCounter(int c) {
        return vcr.getCounter(c);
    }
}

이처럼 VCR 클래스의 인스턴스를 사용하면 손쉽게 다중상속을 구현할 수 있다. VCR 클래스의 내용이 변경되어도 변경된 내용이 TVCR 클래스에도 자동적으로 반영된다.

인터페이스를 새로 작성하지 않고도 VCR 클래스를 TVCR 클래스에 포함시키는 것만으로도 충분하지만, 인터페이스를 이용하면 다형적 특성을 이용할 수 있다는 장점이 있다.

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
interface Parseable {
    public abstract void parse(String fileName);
}

class ParserManager {
    public static Parseable getParser(String type) {
        if (type.equals("XML"))
            return new XMLParser();
        else
            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 = ParseManager.getParser("XML");
        parser.parse("document.xml");
        parser = ParserManager.getParser("HTML");
        parser.parse("document2.html");
    }
}

나중에 새로운 종류의 XML 구문분석기 NewXMLParser클래스가 나와도 ParserTest 클래스는 변경할 필요 없이 ParserManager클래스의 getParser 메서드에서 return new XMLParser(); 대신 return new NewXMLParser();로 변경하기만 하면 된다!

이런 장점은 특히 분산환경 프로그래밍에서 그 위력을 발휘한다. 사용자 컴퓨터에 설치된 프로그램을 변경하지 않고 서버측의 변경만으로도 사용자가 새로 개정된 프로그램을 사용하는 것이 가능하다.

인터페이스의 장점

  • 개발 시간을 단축시킬 수 있다.
    • 한쪽에서는 인터페이스를 구현하는 클래스를 작성하고, 한쪽에서는 인터페이스를 구현하는 클래스가 작성될 때까지 기다리지 않고 선언부만으로 메서드를 호출하여 개발을 진행한다.
  • 표준화가 가능하다.
    • 기본 틀을 인터페이스로 작성한 후 개발자들에게 인터페이스를 구현하여 작성하도록 한다.
  • 서로 관계없는 클래스들에게 관계를 맺어 줄 수 있다.
    • 상속관계도, 같은 조상 클래스를 가진 것도 아닌 클래스들에게 하나의 인터페이스를 공통적으로 구현하도록 함으로써 관계를 맺어 줄 수 있다.
  • 독립적인 프로그래밍이 가능하다.
    • 인터페이스를 이용하면 클래스의 선언과 구현을 분리시킬 수 있어 실제구현에 독립적인 프로그램을 작성하는 것이 가능하다.
    • 클래스와 클래스 간의 직접적인 관계를 인터페이스를 이용해서 간접적인 관계로 변경하면 한 클래스의 변경이 다른 클래스에 영향을 미치지 않는다.

예를 들어 데이터베이스 관련 인터페이스를 정의하고 이를 이용해서 프로그램을 작성하면, 데이터베이스의 종류가 변경되더라도 프로그램을 변경하지 않도록 할 수 있다. 단 데이터 베이스 회사에서 제공하는 클래스도 인터페이스를 구현하도록 요구해야 한다. 데이터베이스를 이용한 응용프로그램을 작성하는 쪽에서는 인터페이스를 이용해서 프로그램을 작성하고, 데이터베이스 회사에서는 인터페이스를 구현한 클래스를 작성해서 제공해야 한다.

인터페이스를 이용하면 기존의 상속체계를 유지하면서 유닛들에 공통점을 부여할 수 있다. Repairable 이라는 인터페이스를 정의하고 수리가 가능한 기계화 유닛에게 인터페이스를 구현하도록 한다. 이 인터페이스는 오로지 인스턴스의 타입체크에만 사용된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class RepairableTest {
	interface Repairable {}
	
	class GroundUnit extends Unit {
		GroundUnit(int hp) {
			super(hp);
		}
	}
	
	class AirUnit extends Unit {
		AirUnit(int hp) {
			super(hp);
		}
	}
	
	class Unit {
		int hitPoint;
		final int MAX_HP;
		Unit (int hp) {
			MAX_HP = 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_HP;
		}
		//...
	}
	
	class SCV extends GroundUnit implements Repairable {
		SCV() {
			super(60);
			hitPoint = MAX_HP;
		}
	}
	
	void repair(Repairable r) {
		if (r instanceof Unit) {
			Unit u = (Unit) r;
			while(u.hitPoint != u.MAX_HP) {
				u.hitPoint++;
			}
			System.out.println(u.toString() + "의 수리가 끝났습니다.");
		}
	}
	//..
}

repair()의 매개변수 r은 Repairable 타입이기 때문에 인터페이스 Repairable에 정의된 멤버만 상용할 수 있다. 그러나 Repairable에는 정의된 멤버가 없으므로 이 타입의 참조변수로는 할 수 있는 일이 아무 것도 없다. 그래서 instanceof 연산자로 타입을 체크한 뒤 캐스티어하여 Unit클래스에 정의된 hitPointMAX_HP를 사용할 수 있게 하였다. 한편, Marine은 Repairable 인터페이스를 구현하지 않았으므로 SCV 클래스의 repair 메서드의 매개변수로 Marine을 사용하면 컴파일 시 에러가 발생한다.

Building의 자식클래스 Academy, Bunker, Barrack, Factory가 있는데, 이 중 Barrack과 Factory에만 새로운 메서드(liftOff(), move(), stop(), land()를 추가하려면 어떻게 해야 할까? 각 클래스마다 코드를 적는다면 코드가 중복된다. 그렇다고 Buidling에 해당 메서드를 추가할 수도 없다. 이럴 때는 새로 추가하고자 하는 메서드를 정의하는 인터페이스를 정의하고, 이를 구현하는 클래스를 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
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(){}
}

그리고 Barrack과 Factory클래스가 Liftable 인터페이스를 구현하도록하고, 인터페이스를 구현한 Liftable 클래스를 Barrack클래스에 포함시켜 내부적으로 호출해서 사용하도록 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Barrack extends Building implements Liftable {
    LiftableImpl l = new LiftableImpl();
    void liftOff() { l.liftOff(); }
    void move(int x, int y) { l.move(x, y); }
    void stop() { l.stop(); }
    void land() { l.land(); }
    void trainMarine() {}
    //..
}

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

인터페이스의 이해

인터페이스 이해를 위한 사전 지식

  • 클래스를 사용하는 쪽(User)과 클래스를 제공하는 쪽(Provider)가 있다.
  • 메서드를 사용(호출)하는 쪽(User)에서는 사용하려는 메서드(Provider)의 선언부만 알면 된다. (내용은 몰라도 된다.)

직접적인 관계

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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());
    }
}

Provider가 변경되면 User도 변경되어야 한다는 단점이 있다.

간접적인 관계: 인터페이스를 매개로

User가 Provider를 직접 호출하지 않고 인터페이스를 매개체로 해서 접근하도록 하면, Provider에 변경 사항이 생기거나 대체되어도 User는 전혀 영향을 받지 않도록 할 수 있다.

먼저 인터페이스를 이용해서 Provider의 선언과 구현을 분리한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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());
        a.autoPlay(new C());
    }
}

클래스A가 인터페이스 I를 사용해서 작성되긴 하였지만, 매개변수를 통해서 인터페이스 I를 구현한 클래스의 인스턴스를 동적으로 제공받아야 한다.

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

한편 매개변수를 통해 동적으로 제공받을 수도 있지만 다음과 같이 제3의 클래스를 통해서 제공받을 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class InterfaceTest3 {
    public static void main(String[] args) {
        A a = new A();
        a.methodA();
    }
}

class A {
    void methodA() {
        // 제3의 클래스의 메서드를 통해서 인터페이스 I를 구현한 클래스의 인스턴스를 얻어온다.
        I i = InstanceManager.getInstanc();
        i.methodB();
        // i로 Object의 메서드 호출 가능
        System.out.println(i.toString());
    }
}

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()만 변경하면 된다는 장점이 생긴다.

default 메서드와 static 메서드

static 메서드

static 메서드는 관계 없는 독립적인 메서드이기 때문에 예전부터 인터페이스에 추가하지 못할 이유가 없었다. 그러나 초반에는 규칙을 단순화하기 위해 인터페이스의 모든 메서드는 추상 메서드여야 한다는 규칙에 예외를 두지 않았다. 덕분에 인터페이스와 관련된 static 메서드는 별도의 클래스에 따로 두어야 했다. 인터페이스의 static 메서드 역시 접근제어자가 항상 public이며, 생략할 수 있다.

java.util.Collection이 대표적인데, 1.8 이전에는 관련된 static 메서드들은 별도의 클래스, Collections라는 클래스에 들어가게 되었다. 만일 인터페이스에 static 메서드를 추가할 수 있었다면, Collections 클래스는 존재하지 않았을 것이다.

default 메서드

조상 클래스에 새로운 메서드를 추가하는 것은 별 일이 아니지만, 인터페이스의 경우는 큰일이다. 이는 추상 메서드를 추가한다는 것이고, 이 인터페이스를 구현한 기존의 모든 클래스들이 새로 추가된 메서드를 구현해야 하기 때문이다.

디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드이다. 추상 메서드가 아니기 때문에 디폴트 메서드가 새로 추가되어도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 된다. 즉 조상 클래스에 새로운 메서드를 추가한 것과 동일해지는 것이다. 디폴트 메서드는 default를 붙이며, body가 있어야 한다. 디폴트 메서드 역시 접근 제어자가 public이며, 생략할 수 있다.

새로 추가된 디폴트 메서드가 기존의 메서드와 이름이 중복되어 충돌하는 경우

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

외우기 어려우면 그냥 필요한 쪽의 메서드와 같은 내용으로 오버라이딩하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class DefaultMethodTest {
    public static void main(String[] args) {
        Child c = new Child();
        c.method1(); // method1() in Child
        c.method2(); // method2() in Parent
        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");
    }
}
This post is licensed under CC BY 4.0 by the author.

Do it! 자료구조와 함께 배우는 알고리즘 #1 (1) 알고리즘이란?

[Java] 백준 #2440, 2441, 2442, 2443: 별찍기 - 3, 4, 5, 6

Loading comments from Disqus ...