인터페이스(Interface)
인터페이스란?
일종의 추상 클래스
추상 클래스처럼 추상 메서드를 갖지만 추상화 정도가 높아서 몸통을 갖춘 일반 메서드 또는 멤버변수를 구성원으로 가질 수 없다.
- 오직 추상메서드와 상수만을 멤버변수로 가질 수 있다.
추상 클래스를 '미완성 설계도'라고 하면, 인터페이스는 아무 것도 없고 밑그림만 그려져 있는 '기본 설계도' 라고 할 수 있다.
인터페이스는 다른 클래스를 작성하는데 도움 줄 목적으로 작성된다.
인터페이스의 작성
class 대신 interface
interface에도 클래스와 같이 접근 제어자로 public 또는 default를 사용할 수 있다.
interface 인터페이스이름 {
public static final 타입 상수이름 = 값;
public abstract 타입 메서드이름(매개변수 목록);
}
인터페이스의 멤버 제약 사항
- 모든 멤버변수는 public static final 이어야 하며, 이를 생략할 수 있다.
- 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있다.
(단, static 메서드와 default 메서드는 예외 // JDK 1.8부터 적용)
예외 없이 적용되기 때문에 제어자를 생략할 수 있다. (생략하면 컴파일러가 알아서 추가해줌)
JDK 1.8부터 인터페이스에 static 메서드와 디폴트 메서드의 추가를 허용하는 방향으로 변경되었다.
인터페이스의 상속
- 인터페이스는 인터페이스로부터만 상속받을 수 있으며, 클래스와 달리 다중 상속, 즉 여러 개의 인터페이스로부터 상속을 받는 것이 가능하다.
interface Movable {
void move(int x, int y);
}
interface Attackable {
void attack(Unit u);
}
interface Fightable extends Movable, Attackable { };
인터페이스의 구현
인터페이스도 추상클래스처럼 그 자체로는 인스턴스를 생성할 수 없다.
추상클래스에서 상속받는 클래스를 정의하는 것처럼 인터페이스도 자신에 정의된 추상 메서드의 body를 만들어주는 클래스를 작성해야 한다.
- 클래스에서는 extends를 사용하지만, 인터페이스에서는 implements(구현한다)를 사용한다.
만약 구현하는 인터페이스의 메서드 중 일부만 구현한다면 abstract를 붙혀서 추상클래스로 선언해야 한다.
상속과 구현(extends, implement)를 동시에 할 수도 있다.
class Fighter extends Unit implements Fightable {
public void move(int x, int y) { /* */ } // Unit의 move
public void attack(Unit u) { /* */ } // Fightable의 attack
}
- 참고
인터페이스의 이름에는 ~able로 끝나는 것들이 많다. 어떠한 기능 또는 행위를 하는데 필요한 메서드를 제공한다는 의미를 강조하고, ~할 능력을 갖추었다는 의미이기도 하기 때문이다.
Example 7.24
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); }
예전에 다뤘던 내용인데, 오버라이딩 할 때는 조상의 메서드보다 넓은 범위의 접근 제어자를 지정해야 한다.
Movable 인터페이스에 'void move()'로 정의되어 있지만, 사실 public abstract가 생략된 것이기 때문에 Fighter 클래스에서는 접근 제어자를 반드시 public으로 해야 한다
인터페이스를 이용한 다중상속
두 조상으로부터 상속받는 멤버 중에서 멤버 변수의 이름이 같거나 메서드의 선언부가 일치하고 구현 내용이 다르다면, 어느 조상의 것을 상속받게 되는 것인지 알 수 없다.
- 따라서 자바에서는 다중상속을 허용하지 않는다
물론 인터페이스를 이용하면 다중 상속을 구현할 수 있긴 하지만, 이런 경우는 거의 없다.
- 따라서 아래 나오는 다중상속 관련 내용은 가볍게 읽고 넘어가도 됩니다~
인터페이스는 static 상수만 정의할 수 있으므로 조상 클래스의 멤버 변수와 충돌하는 경우는 거의 없고, 충돌하더라도 클래스의 이름을 붙여서 구분이 가능하다.
추상 메서드는 구현내용이 전혀 없으므로 조상클래스의 메서드와 선언부가 일치하는 경우에는 당연히 조상 클래스 쪽의 메서드를 상속받으면 되므로 문제되지 않는다.
이렇게 하면 다중상속의 장점을 잃게 되므로, 비중이 높은 쪽을 선택하고 다른 한 쪽은 클래스 내부 멤버로 포함시키는 방식으로 처리할 수 있다.
또는 어느 한쪽의 필요한 부분을 뽑아서 인터페이스로 만든 다음 구현할 수 있다.
Example) TV, VCR 클래스로 부터 다중상속
TV와 VCR 클래스가 있을 때 TVCR 클래스를 작성하고 싶은 경우
두 클래스로부터 모두 상속받을 수 없다.
- 한 쪽만 선택하여 상속받고, 나머지 한 쪽은 클래스 내에 포함시켜 내부적으로 인스턴스를 생성해서 사용하자
public class TVCR extends TV implements IVCR {
VCR vcr = new VCR();
public void play() {
vcr.play(); // 코드를 작성하는 대신 vcr 인스턴스의 메서드를 호출
}
/* 중략 */
public int getCounter() {
return vcr.getCounter();
}
public void setCounter(int c) {
return vcr.setCounter(c);
}
}
VCR 클래스의 내용이 변경되어도 변경된 내용이 TVCR 클래스에도 자동적으로 반영되는 효과도 얻을 수 있다.
- 사실 인터페이스를 새로 작성하지 않고도 VCR 클래스를 TVCR 클래스에 포함시키는 것만으로도 충분하지만, 인터페이스를 이용하면 다형적 특성을 이용할 수 있다는 장점이 있다.
인터페이스를 이용한 다형성
- 인터페이스 타입의 참조변수로 이를 구현한 클래스의 인스턴스를 참조할 수 있으며, 인터페이스 타입으로의 형변환도 가능하다.
Fightable f = (Fightable)new Fighter();
// or
Fightable f = new Fighter();
- 인터페이스는 메서드의 매개변수 타입으로 사용될 수 있다.
void attack(Fightable f) {
//...
}
인터페이스 타입의 매개변수가 갖는 의미 : 메서드 호출 시 해당 인터페이스를 구현한 클래스의 인스턴스를 매개변수로 제공해야 한다는 것
- 공격을 하려면 싸울 수 있는 유닛을 넘겨줘야 한다.
아래와 같이 메서드의 리턴 타입으로 인터페이스의 타입을 지정하는 것 역시 가능하다.
Fightable method() {
Fighter f = new Figther();
return f;
// or
return new Figher();
}
- 리턴 타입이 인터페이스라는 것 : 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미한다.
Example 7.25
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");
}
}
만일 나중에 새로운 종류의 XML 구문 분석기 NewXMLParser 클래스가 나와도 ParserManager 클래스의 getParser 메서드에서 'return new XMLParser();' 대신 'return new NewXMLParser();'로 변경해주기만 하면 된다
- 이러한 장점은 분산환경 프로그래밍에서 위력을 발휘한다.
인터페이스의 장점
- 개발시간을 단축시킬 수 있다.
메서드를 호출하는 쪽에서는 메서드의 내용에 관계없이 선언부만 알면 되기 때문이다.
동시에 다른 한쪽에서는 인터페이스를 구현하는 클래스를 작성하도록 하여, 인터페이스를 구현하는 클래스가 작성될 때 까지 기다리지 않고도 양쪽에서 동시에 개발을 진행할 수 있다.
- 표준화가 가능하다.
- 프로젝트에 사용되는 기본 틀을 인터페이스로 작성한 다음 구현하므로써 일관되고 정형화된 프로그램 개발이 가능하다
서로 관계없는 클래스들에게 관계를 맺어줄 수 있다.
독립적인 프로그래밍이 가능하다.
인터페이스를 이용하면 선언과 구현을 분리시킬 수 있어서 실제구현에 독립적인 프로그램을 작성하는 것이 가능하다.
클래스와 클래스간의 직접적인 관계를 인터페이스를 이용해서 간접적인 관계로 변경하면, 한 클래스의 변경이 관련된 다른 클래스에 영향을 미치지 않는 독립적인 프로그래밍이 가능하다.
- ex) DBMS를 사용하는 프로그램에서 어떤 DB 구현체를 사용할 지 인터페이스를 통해 선택할 수 있다.
서로 관계없는 클래스들에게 관계를 맺는 예시 1)
스타크래프의 GroundUnit에 Marine, SCV, Tank 등이 있고, AirUnit에는 Dropship 등이 있다.
이 때 SCV, Tank, Dropship에 repair가 가능하게 하고 싶다.
void repair()의 매개변수에 각 타입의 인스턴스를 모두 오버로딩 하는 것은 매우 비효율적이다
그렇다고 GroundUnit, AirUnit을 통채로 repair의 인스턴스로 넣을 수는 없다.
- 이 때 인터페이스를 이용하면 Tank, Dropship, SCV에 공통점을 부여할 수 있다.
아래와 같이 Repairable이라는 인터페이스를 정의하고 수리가 가능한 기계화 유닛에게 이 인터페이스를 구현하도록 하면 된다
interface Repairable {}
class SCV extends GroundUnit implements Repairable {
// ...
}
class Tank extends GroundUnit implements Repairable {
// ...
}
class Dropship extends AirUnit implements Repairable {
// ...
}
3개의 클래스에 같은 인터페이스를 구현했다는 공통점이 생겼다.
- 인터페이스는 단지 인스턴스의 타입체크에만 사용될 뿐이다.
void repair(Repairable r) {
// 매개변수로 넘겨받은 유닛을 수리한다.
}
Example 7.26
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가 Tank 수리
scv.repair(dropship);
// scv.repair(marine); // 컴파일 에러
}
}
interface Repairable {}
class Unit {
int hitPoint;
final int MAX_HP;
Unit(int hp) {
MAX_HP = hp;
hitPoint = MAX_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); // Tank의 HP는 150
// hitPoint = MAX_HP;
}
public String toString() {
return "Tank";
}
// ...
}
class Dropship extends AirUnit implements Repairable {
Dropship() {
super(125); // Dropship의 HP는 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) {
/* Unit의 HP를 증가시킴 */
u.hitPoint++;
}
System.out.println(u.toString() + "의 수리가 끝났습니다.");
}
}
// ...
}
repair 메서드의 매개변수 r은 Repairable 타입이기 때문에 인터페이스 Repairable에 정의된 멤버만 사용할 수 있다.
- Repairable 인터페이스에는 정의된 멤버가 없으므로 이 타입의 참조된 변수로는 할 수 있는 일이 아무 것도 없다.
그래서 instanceof연산자로 타입을 체크한 뒤 캐스팅(down casting)을 하여 Unit클래스에 정의된 hitPoint와 MAX_HP를 사용할 수 있도록 할 수 있다.
서로 관계없는 클래스들에게 관계를 맺는 예시 2)
Barrack와 Factory에 건물을 이동시킬 수 있는 기능을 추가하려고 한다
- 예시 1처럼 lift는 하나의 메서드로 끝나는게 아니라, land -> move -> stop -> liftoff 과정을 거쳐야 한다.
liftable 인터페이스를 만들고, 이 인터페이스의 구현체를 만든다음, 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() { /* 내용 생략 */ }
}
인터페이스의 이해
인터페이스의 본질적인 의미에 대해 살펴보자.
다음 두 가지를 염두해야 한다.
클래스를 사용하는 쪽(user)과 클래스를 제공하는 쪽(Provider)이 있다.
메서드를 사용(호출)하는 쪽(user)에서는 사용하려는 메서드(Provider)의 선언부만 알면 된다.(내용은 몰라도 됨)
Example 7.27
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인 B의 methodB()의 선언부가 변경되면 이를 사용하는 클래스 A도 변경되어야 한다.
클래스 A가 클래스 B를 직접 호출하지 않고 인터페이스를 매개체로 해서 클래스 A가 인터페이스를 통해서 클래스 B의 메서드에 접근하도록 하면, 클래스 B에 변경사항이 생기거나 클래스 B와 같은 기능의 다른 클래스로 대체 되어도 클래스 A는 전혀 영향 받지 않도록 하는 것이 가능하다
interface I {
public abstract void methodB();
}
class B implements I {
public void methodB() {
// ..
}
}
class A {
public void methodA(I i) {
i.methodB();
}
}
'A-B'의 직접적인 관계에서 'A-I-B'의 간접적인 관계로 바뀐 것이다.
- 클래스 A는 오직 직접직인 관계에 있는 인터페이스 I의 영향만 받는다.
Example 7.28
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("paly 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) 호출
}
}
매개변수를 통해서 인터페이스 I를 구현한 클래스의 인스턴스를 동적으로 제공받아야 한다.
- ex) Thread 생성자
매개변수를 통해 동적으로 제공받을 수도 있지만, 아래와 같이 제 3의 클래스를 통해서 제공받을 수도 있다.
- ex) JDBC의 DriverManager 클래스
class InterfaceTest3 {
public static void main(String[] args) {
A a = new A();
a.methodA();
}
}
class A {
void methodA() {
I i = InstanceManager.getInstance(); // 제3의 클래스의 메서드를 통해서 인터페이스 I를 구현한 클래스의 인스턴스를 얻어옴
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();
}
}
이렇게 하면 나중에 다른 클래스의 인스턴스로 변경되어도 A클래스의 변경 없이 getInstance()만 변경하면 된다는 장점이 생긴다.
그리고 인터페이스 I 타입의 참조변수 i로도 Object 클래스에 정의된 메서드들을 호출할 수 있다.
디폴트 메서드와 static 메서드
static 메서드
static 메서드는 인스턴스와 관계가 없는 독립적인 메서드이기 때문에 JDK 1.8부터 추가할 수 있게 되었다.
- 항상 접근 제어자는 public이다.
default 메서드
인터페이스에 메서드를 새로 추가하면, 이 인터페이스를 구현한 기존의 모든 클래스들이 새로 추가된 메서드를 구현해야 한다.
- 아무리 설계를 잘해도 언젠가는 인터페이스를 변경할 일이 생김
JDK 설계자들이 default method라는 것을 고안해 냄
디폴트 메서드는 추상메서드의 기본적인 구현을 제공하는 메서드로, 추상 메서드가 아니기 때문에 디폴트 메서드가 새로 추가되어도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 된다.
메서드 앞에 키워드 default를 붙인다.
접근 제어자는 public이다.
반드시 몸통이 있어야 한다.
새로 추가된 디폴트 메서드가 기존의 메서드와 이름이 중복되어 충돌한다면 다음 규칙을 따른다
- 여러 인터페이스의 디폴트 메서드 간의 충돌
- 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩해야 한다
- 디폴트 메서드와 조상 클래스의 메서드 간의 충돌
- 조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.
Example 7.30
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("method() 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의 정석 3rd edition(남궁성, 도우출판)
'Programming Language > Java' 카테고리의 다른 글
[Java] 12. 예외 처리(Exception handling)(1) - 예외 처리란? (0) | 2024.07.09 |
---|---|
[Java] 11. OOP - 내부 클래스(Inner Class), 익명 클래스 (0) | 2024.07.07 |
[Java] 09. OOP - 추상 클래스(abstract class) (0) | 2024.06.24 |
[Java] 08. OOP - 다형성(polymorphism) (0) | 2024.06.24 |
[Java] 07. OOP - 상속과 접근제어자 (0) | 2024.03.12 |