Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
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
Archives
Today
Total
관리 메뉴

codingfarm

인터페이스(interface) 본문

Programming Language/JAVA

인터페이스(interface)

scarecrow1992 2021. 3. 11. 23:43

인터페이스란?

  • 추상클래스보다 추상화의 정도가 높은 클래스이다.
  • 일반 메서드나 필드를 구성원으로 가질 수 없다.
    • 오직 추상 메서드와 상수만을 멤버로 가진다.
추상 클래스 인터페이스
부분적으로만 완성된 '미완성 설계도' 구현된것이 아무것도 없는 '기본 설계도'

 


작성법

  • 키워드로 class 대신 interface를 사용한다.
  • 나머지는 일반 클래스와 작성법이 같다.
  • 접근제어자로 public 또는 default를 쓸 수 있다.
1
2
3
4
interface 인터페이스이름 {
    public static final 타입 상수이름 = 값;
    public abstract 반환타입 메서드이름(매개변수목록);
}
cs
  • 인스턴스의 멤버들은 다음과 같은 제약사항을 가진다.
    • 모든 멤버변수는 public static final 이어야 하며, 이를 생략할 수 있다.
    • 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
interface PlayingCard {
    // 모든 필드는 제어자를 어떻게 작성하든, public static final이 적용된다.
    public static final int SPADE = 4;
    final int DIAMOND = 3;
    static int HEART = 2;
    int CLOVER = 1;
    
    // 모든 메서드는 제어자를 어떻게 작성하든, public abstract로 고정된다.
    public abstract String getCardNumber();
    String getCardKind();
}
cs

 


인터페이스의 상속

  • 인터페이스는 인터페이스로부터만 상속받을 수 있다.
  • 다중상속이 가능하다.
  • 클래스와 달리 Object클래스와 같은 최고 조상이 없다.
1
2
3
4
5
6
7
8
9
interface Movable {
    void move(int x, int y);
}
 
interface Attackable {
    void attack(Unit u);
}
 
interface Fightable extends Movable, Attackable {  }
cs

 


인터페이스의 구현

  • 그 자체로는 인스턴스를 생성할 수 없다.
  • 상속을 통해 자식클래스가 완성해야한다.
  • 클래스가 인터페이스를 상속시 extends 키워드 대신 implements를 사용해야한다.
1
2
3
class 클래스이름 implements 인터페이스이름 {
    /* 인터페이스에 선언된 추상 메서드를 구현해야한다. */
}
cs

 

  • 인터페이스가 인터페이스를 상속하면 그대로 extends를 쓴다.
1
2
interface A{}
interface B extends A{}
cs

 

  • 만일 구현하는 인터페이스의 메서드 중 일부만 구현한다면, 추상클래스로 선언되어야 한다.
1
2
3
abstract class 클래스이름 implements 인터페이스이름 {
    /* 인터페이스에 선언된 추상 메서드를 구현해야한다. */
}
cs

 

  • 상속과 구현을 동시에 할 수도 있다.
1
class 클래스명1 extends 클래스명2 implements 인터페이스명 { }
cs

 

예제 코드를 확인해보자.

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
package project_1;
 
class Unit {
    int currentHP;
    int x;
    int y;
}
 
interface Attackable { void attack(Unit u); }
interface Movable { void move(int x, int y); }
interface Fightable extends Movable, Attackable{}
 
 
class Fighter extends Unit implements Fightable {
    public void move(int x, int y) {}
    public void attack(Unit u) {}
}
 
public class Hello {
    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클래스의 자손입니다.");
        }
    }
}
cs

 

위 예제에 사용된 클래스와 인터페이스간의 관계를 그려보면 아래와 같다.

Movable과 Attackable 인터페이스내에 있는 메서드 move와 attack를 Unit 클래스 내에서는 public으로 선언했음을 주의깊게 봐야한다.

오버라이딩 할 때는 조상의 메서드보다 넓은 범위의 접근 제어자를 지정해야함을 기억하라.

Movable 인터페이스에 void move(int x, int y)와 같이 정의되어 있지만 사실 public abstract가 생략된 것이기 때문에 실제로 public abstract void move(int x, int y) 이다.

따라서 이를 구현하는 Fighter클래스에서는 move 메서드의 접근 제어자를 반드시 public으로 해야한다.

 


다중상속

  • JAVA는 클래스의 다중상속을 지원하지 않는다.
  • 인터페이스는 다중상속을 지원하지만 신중하게 써야한다.
  • 두개의 클래스로부터 상속을 받아야할 경우 2가지 방법중 하나를 사용하는 것을 추천
    1. 비중이 높은쪽에 다른 클래스를 멤버로 포함시킨다.
    2. 한쪽의 필요한 부분을 뽑아서 인터페이스로 만든 후 구현한다.

가령 TV클래스와 VCR 클래스가 있을 때, 두 클래스의 특성을 모두다 상속받고 싶다면, 한쪽만 선택하여 상속받고, 나머지 한쪽은 클래스내에 포함시켜 내부적으로 인스턴스를 생성해서 사용하도록 한다.

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
class Tv{
    protected boolean power;
    protected int channel;
    protected int volume;
    
    public void power()         { power = !power; }
    public void channelUp()     { channel++; }
    public void channelDown()     { channel--; }
    public void volumeUp()        { volume++; }
    public void volumeDown()    { volume--; }
}
 
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; }
}
 
interface IVCR{
    public void play();
    public void stop();
    public void reset();
    public int getCounter();
    public void setCounter(int c);
}
 
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); }
}
cs

텔레비전을 정의하는 필드와 메서드를 포함하는 클래스 TV가 존재한다.

그리고 비디오 레코더를 정의하는 필드와 메서드를 포함하는 클래스 VCR이 존재한다.

이 TV와 VCR 일체형의 클래스를 만들고 싶다.

하지만 2개의 클래스를 다중상속을 받을 순 없다.

그러므로 비디오 레코더의 함수를 똑같이 담고있은 인터페이스 IVCR을 정의한다.

이런식으로 클래스 TV와 인터페이스 IVCR을 상속받고 VCR을 포함하는 클래스 TVCR을 만들 수 있다.

TV 기능을 다루고 싶다면 상속받은 클래스 Tv의 필드와 메서드를 다루면 되지만.

VCR의 기능을 다루고 싶다면 TVCR내에서 VCR의 함수를 호출하는 IVCR로부터 상속받은 함수를 재정의하여 래핑 함수를 만든다.

 

IVCR인터페이스를 구현하기 위해 새로 메서드를 작성해야하는 부담이 있지만, 이렇게 VCR 클래스의 인스턴스를 사용하면 손쉽게 다중상속을 구현할 수 있다.

 

여기서 IVCR은 VCR의 함수를 호출하기 위한 래핑함수의 정의를 강제하게 만든다.

또한 인터페이스를 이용하여 다형적 특성을 이용할 수 있다는 장점이 있다.

 


Interface 충돌

1.interface와 조상클래스의 변수가 겹칠경우

  • interface는 static 변수만 선언할 수 있으므로 필드명 앞에 인터페이스명을 붙여 구분이 가능하다.

 

2. interface와 조상클래스의 메서드가 겹칠경우

  • 인터페이스의 추상메서드는 구현내용이 없으므로 조상클래스쪽의 메서드를 상속받으면 된다.
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
package project_1;
 
interface myInterface{
    public static final int value = 5;
    public abstract void func1();
    public abstract void func2();
}
 
class myClass{
    public int value = 10;
    
    public void func2() { System.out.println(value); }
    
}
 
class childClass extends myClass implements myInterface{
    int value = 15;
    public void func1(){ System.out.println(myInterface.value); }
    
    public void func2() { super.func2(); }
    
    public void func3() { System.out.println(this.value); }    
}
 
public class Hello {
    public static void main(String[] args) {
        childClass a = new childClass();
        a.func1();
        a.func2();
        a.func3();
    }
}
cs

 

 


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

  • 다형성의 기본 원리 : 조상타입의 참조변수로 자손클래스의 인스턴스를 참조하는 것이 가능하다.
    • 인터페이스의 참조변수로 이를 구현한 클래스의 인스턴스를 참조할 수 있다.
    • 클래스의 인스턴스를 인터페이스 타입으로 형변환도 가능하다.

 

가령 인터페이스 Fightable을 클래스 Fighter가 구현했을 때, 아래처럼 Fighter 인스턴스를 Fightable 타입의 참조변수로 참조하는 것이 가능하다. (이 경우 Fightable에 정의된 멤버들만 참조변수를 통해 호출 가능하다.)

1
2
3
4
5
Fightable f = (Fightable) new Fighter();
 
// 또는
 
Fightable f = new Fighter();
cs

 

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

1
2
3
void attack(Fightable f) {
    // ...
}
cs

 

  • 인터페이스 타입의 매개변수가 갖는 의미
    • 메서드 호출 시 해당 인터페이스를 구현한 클래스의 인스턴스를 매개변수로 제공해야한다는 것
    • 그래서 attack 메서드를 호출할 때는 매개변수로 Fightable인터페이스를 구현한 클래스의 인스턴스를 넘겨주어야 한다.

 

다음과 같이 메서드의 리턴타입으로 인터페이스의 타입을 지정하는 것 역시 가능하다.

1
2
3
4
Fightable mothod() {
    // ...
    return new Fighter();
}
cs

 

참조변수가 인터페이스 타입이라는 것은 해당 인터페이스를 구현한 클래스의 인스턴스를 참조해야함을 의미한다.
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
package project_1;
 
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;
            // return new HTMLParser();
        }
    }
}
 
public class Hello {
    public static void main(String[] args) {
        Parseable parser = ParserManager.getParser("XML");
        parser.parse("document.xml");
        parser = ParserManager.getParser("HTML");
        parser.parse("document2.html");
    }
}
cs

위와 같은 방법을 통해 개발자는 문서의 종류(XML, HTML)가 다르더라도 paring 방법을 하나로 통일할 수 있으며(parser.parse), 새로운 종류의 문서가 나오거나, 기존 문서에 대해 수정할 사항이 생기더라도, Hello 클래스는 수정할 필요 없이, Parseable을 인터페이스를 구현하는 클래스를 정의한 후, parse 메서드를 오버라이딩 하면 된다.

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

 


인터페이스의 장점

  1. 개발시간을 단축시킬 수 있다.
    • 메서드를 호출하는 쪽에서는 메서드의 내용에 관계 없이 선언부만 알면 함수의 정의가 가능하다.
    • 인터페이스를 구현하는 쪽에서는, 클래스가 작성될 때까지 기다리지 않고도 개발이 가능하다.
      • 동시에 양쪽에서 개발을 진행할 수 있다.
  2. 표준화가 가능하다.
    • 프로젝트에 사용되는 기본틀을 인터페이스로 작성한 후, 개발자들에게 인터페이스를 구현하여 프로그램을 작성하도록 하여, 보다 일관되고 정형화된 프로그램의 개발이 가능하다.
  3. 서로 관계없는 클래스들에게 관계를 맺어줄 수 있다.
    • 상속관계도 없고, 공통 조상을 가지지 않는 클래스에게 하나의 인터페이스를 공통적으로 구현하도록 하여 관계를 맺어 줄 수 있다.
  4. 독립적인 프로그래밍이 가능하다.
    • 인터페이스를 사용하면 클래스의 선언과 구현을 분리시킬 수 있다.
      • 실제 구현에 독립적인 프로그램의 작성이 가능하다.
    • 클래스간의 직접적인 관계를 인터페이스를 이용해서 간접적인 관계로 변경하면, 한 클래스의 변경이 관련된 다른 클래스에 영향을 미치지 않는 독립적인 프로그래밍이 가능하다.

 

가령 특정 DB를 사용하는데 필요한 클래스가 존재한다. 그리고 이 클래스를 사용해서 프로그램을 작성했다. 하지만 이후에 이 프로그램은 다른 종류의 DB를 사용하기 위해서 전체 프로그램 중 DB에 관련된 부분은 모두 변경해야 할 것이다.

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
package project_1;
 
class DataBaseA{
    void Connect() {
        System.out.println("Connect to DB_A");
    }
}
 
class DataBaseB{
    void Connect() {
        System.out.println("Connect to DB_B");
    }
}
 
/*
class DataBaseC{
    void Connect() {
        System.out.println("Connect to DB_C");
    }
}
*/
 
class DBManager{
    void Connect(String key) {
        if(key.equals("A")) {
            DataBaseA db = new DataBaseA();
            db.Connect();
        }
        
        if(key.equals("B")) {
            DataBaseB db = new DataBaseB();
            db.Connect();
        }
        /*
        if(key.equals("C")) {
            DataBaseB db = new DataBaseB();
            db.Connect();
        }
        */
    }
}
 
public class Hello {
    public static void main(String[] args) {
        DBManager dbman = new DBManager();
        dbman.Connect("A");
        dbman.Connect("B");
        //dbman.Connect("C");        
    }
}
cs

예를들어 회사 A사와 B사의 db에 접속할 수 있도록 클래스 DataBaseA, DataBaseB를 작성했다.

개발자는 DB와의 연결을 제어하기 위한 DBManager 클래스를 만들었으며 해당 클래스 내에서 2개의 DB에 접속하기 위한 코드를 작성한다. 하지만 만약 C사의 db에도 추가적으로 접속할 일이 생긴다면? 그러면 위 코드에서 주석으로 된 부부분을 모두다 해제하면 된다. 여기서 우리는 DBManager의 내부 코드를 수정해야한다. 이는 생산성 측면에서 봤을때 적절치 못하다.

 

 

그러나 DB별로 인터페이스를 정의하고 이를 이용해서 프로그램을 작성하면, DB의 종류가 변경되더라도 프로그램을 변경하지 않도록 할 수 있다.

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
package project_1;
 
interface DB{
    public abstract void Connect();
}
 
class DataBaseA implements DB{
    public void Connect() {
        System.out.println("Connect to DB_A");
    }
}
 
class DataBaseB implements DB{
    public void Connect() {
        System.out.println("Connect to DB_B");
    }
}
 
/*
class DataBaseC implements DB{
    public void Connect() {
        System.out.println("Connect to DB_C");
    }
}
*/
 
class DBManager{
    void Connect(DB db) {
        db.Connect();
    }
}
 
public class Hello {
    public static void main(String[] args) {
        DBManager dbman = new DBManager();
        dbman.Connect(new DataBaseA());
        dbman.Connect(new DataBaseB());
        //dbman.Connect(new DataBaseC());        
    }
}
cs

제대로 구현한것인지 모르겠지만, 의도는 어느정도 통했다고 생각한다.

위와 같이 인터페이스를 활용할 경우 C사의 DB에 접속할 추가적인 업데이트가 필요할지라도 DBManager에서는 수정이 없어도 된다.

내가 구현한 예제 코드가 의도에 맞는것인지는 잘 모르겠지만 인터페이스를 활용한 다형성의 이점에 대해서는 어느정도 감이 잡힌다.

 

 

예1) 게임 스타크래프트

스타크래프트에 나오는 유닛을 클래스로 표현했을 때, 이들의 상속계층도는 아래와 같다.

SCV에게는 Tank나 Dropship같은 기계화 유닛을 수리할 수 있는 repair 기능이 있다. 각 유닛을 치료하기 위해 repair 메서드를 구현해보면 아래와 같을것이다.

1
2
3
4
5
6
7
void repair(Tank t) {
    // Tank를 수리한다.
}
 
void repair(Dropship d) {
    // Dropship을 수리한다.
}
cs

이런식으로 함수를 작성하면 앞에서 설명했던것처럼 몇가지 단점이 있다.

  1. 모든 유닛에 대해 별개의 함수를 만들어야 한다.
  2. repair의 내부 로직이 바뀐다면 모든 함수를 수정해야한다.
  3. 유닛이 추가될 때마다 repair 함수도 추가되어야 하므로 강한 결합도(coupling)를 이룬다.

우리는 repair 함수가 어떤 유닛을 치료하는지 신경 안쓰게 하고 싶다.

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Repairable {}
 
class SCV extends GroundUnit implements Repairable {
    // ...
}
 
class Tank extends GroundUnit implements Repairable {
    // ...
}
 
class Dropship extends GroundUnit implements Repairable {
    // ...
}
cs

위 처럼 Repairable이라는 인터페이스를 정의하고, 수리가 가능한 기계화 유닛에게 이 인터페이스를 구현하도록 하면 된다.

그리고 repair 메서드의 매개변수의 타입을 Repairable로 선언하면, 이 메서드의 매개변수로 Repairable 인터페이스를 구현한 클래스의 인스턴스만 받아들여질 것이다.

1
2
3
void repair(Repairable r) {
    // 매개변수로 넘겨받은 유닛을 수리한다.
}
cs

 

본격적으로 구현해보면 아래와 같다.

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package project_1;
 
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_HP;
    }
    
    public String toString() {
        return "Dropship";
    }
    // ...
}
 
class SCV extends GroundUnit implements Repairable {
    SCV() {
        super(60);
        hitPoint = MAX_HP;
    }
    
    public String toString() {
        return "Dropship";
    }
    
    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() + "의 수리가 끝났습니다.");
        }
    }
    // ...
}
 
public class Hello {
    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);        // 에러
    }
}
cs

여러가지로 아쉬운점이 많은 코드이다.

  1. marine에 대한 수리를 시도조차 할 수 없음
  2. SCV로 수리명령을 내릴때 앞서 했던 부대지정의 예제 처럼 상위 클래스의 참조변수로 SCV 인스턴스를 참조하는게 아닌, SCV의 참조변수를 직접적으로 이요한 점

 

 

 

예2) 게임 스타크래프트

테란의 건물을 공중에 띄우는 기능을 추가해보자

테란의 건물을 상속계층도로 표현하면 아래와 같다.

테란 건물이라 해서 모두다 공중에 띄울 수 있는것은 아니다. 공중에 띄울 수 있는 건물은 한정되어 있으며, 위의 4개 건물중에서는 Barrack과 Factory가 이에 속한다.

건물의 이륙, 이동, 정지, 착륙역할을 하는 함수는 liftOff, move, stop, land가 있으며 이 함수들을 띄울 수 있는 건물 클래스의 메서드로 포함시키면 된다.

1
2
3
4
void liftOff()                { /* 내용 생략 */ }
void move(int x, int y);    { /* 내용 생략 */ }
void stop();                { /* 내용 생략 */ }
void land();                { /* 내용 생략 */ }
cs

 

하지만 이런 방법이 비실용적이라는 사실은 잘 알것이다.

그러므로 이륙에 관련된 메서드들을 포함하는 인터페이스를 정의한후 클래스에서 이를 구현하도록 한다.

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
interface Liftable {
    // 건물을 이륙시킨다.
    void liftOff();
    // 건물을 이동한다.
    void move(int x, int y);
    // 건물을 정지시킨다.
    void stop();
    // 건물을 착륙시킨다.
    void land();    
}
 
class Building{}
 
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)    { /* 내용 생략 */ }
    public void stop() {l.stop();}
    public void land() {l.land();}
    public void trainMarine() {/* 내용 생략 */}
    public void trainMedic()  {/* 내용 생략 */}
    // ...
}
cs

 


인터페이스의 이해

지금까지 인터페이스에 대한 일반적인 사항들을 살펴봤지만 "인터페이스란 도대체 무엇인가?" 라는 의문은 여전히 남이있을것이다.

이번 절에서는 인터페이스의 규칙이나 활용이 아닌 본질적인 측면에 대해 살펴보자.

인터페이스를 이해하기 위해서는 다음의 두가지 사항을 반드시 염두해야한다.

클래스를 사용하는 쪽(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()");
    }
}
 
public class Hello {
    public static void main(String[] args) {
        A a = new A();
        a.methodA(new B());
    }
}
cs

위와 같이 클래스 A,B가 있다. 각 클래스 A는 클래스 B의 인스턴스를 생성하고 메서드를 호출한다. 이 두 클래스는 서로 직접적인 관계에 있으며 이를 간단히 A-B라 표현한다.

이경우를 위해선 여러가지 필요 사항들이 있다.

  • 클래스 A의 작성을 위해선 클래스 B가 이미 작성되어 있어야 한다.
  • 클래스 B의 methodB()의 선언부가 변경되면, 이를 사용하는 클래스 A도 변경되어야 한다.

직접적인 관계의 두 클래스는 한쪽(Provider)이 변경되면 다른 한쪽(User)도 변경되어야 한다는 단점이 있다.

 

그러나 클래스 A가 클래스 B를 직접 호출하지 않고 인터페이스를 매개체로 해서 클래스 A가 인터페이스를 통해서 클래스 B의 메서드에 접근하도록 하면,

  1. 클래스 B에 변경사항이 생길 경우
  2. 클래스 B와 같은 기능의 다른 클래스로 대체 될 경우

위와 같은 상황에서도 클래스 A는 전혀 영향을 받지 않도록 하는 것이 가능하다.

두 클래스간의 관계를 간접적으로 변경하기 위해서는 먼저 인터페이스를 이용해서 클래스 B(Provider)의 선언과 구현을 분리해야 한다.

먼저 다음과 같이 클래스 B에 정의된 메서드를 추상 메서드로 정의하는 인터페이스 I를 정의한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface I {
    public abstract void methodB();
}
 
class B implements I {
    public void methodB() {
        System.out.println("methodB in B class");
    }
}
 
class A {
    // methodA가 호출되기 위해선 인터페이스 I를 구현한 클래스의 인스턴스(클래스 B의 인스턴스)를 제공받아야 한다.
    public void methodA(I i) {
        i.methodB();
    }
}
cs

클래스 A의 작성에 있어서 클래스 B가 사용되지 않았음에 주목하라.

이제 클래스 A와 클래스 B는 'A-B'의 직접적인 관계에서 'A-I-B'의 간접적인 관계로 바뀌었다.

결국 클래스 A는 클래스 B의 변경에 영향을 받지 않게되며, 오직 인터페이스 I의 영향만 받는다.

클래스 B는 인터페이스 I라는 껍데기에 둘러싸인 형태이며 클래스 A는 오직 껍데기인 인터페이스 I만 알면된다. 그 안에 무엇이 있는지는 몰라도 된다.

 

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
package project_1;
 
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 A {
    // methodA가 호출되기 위해선 인터페이스 I를 구현한 클래스의 인스턴스(클래스 B의 인스턴스)를 제공받아야 한다.
    public void autoPlay(I i) {
        i.play();
    }
}
 
 
public class Hello {
    public static void main(String[] args) {
        A a = new A();
        a.autoPlay(new B());
        a.autoPlay(new C());
    }
}
cs

A와 B가 잘 분리된것 같아 보이지만 인터페이스의 참조변수를 통해 인스턴스를 직접 전달받아야 한다는 점에서 아직 불안전하다. 그러므로 제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
package project_1;
 
class InstanceManager {
    public static I getIndtance() {
        return new B();
    }
}
 
interface I {
    public abstract void methodB();
}
 
class B implements I {
    public void methodB() {
        System.out.println("methodB in B class");    
    }
}
 
class A {
    void methodA() {
        I i =InstanceManager.getIndtance();
        i.methodB();
    }
    
}
 
public class Hello {
    public static void main(String[] args) {
        A a = new A();
        a.methodA();
    }
}
cs

 

 

 

 

 

 

'Programming Language > JAVA' 카테고리의 다른 글

예외 되던지기(exception re-throwing  (0) 2021.04.05
예외 처리(exception handling)  (0) 2021.03.22
추상클래스(abstract class)  (0) 2021.03.11
다형성(polymorphism)  (0) 2021.03.10
제어자(modifier)  (0) 2021.03.07
Comments