본문 바로가기

JAVA/혼공자

[혼공자] Chapter 08. 인터페이스

인터페이스 역할 : 강제적인 설계도 + USB 포트
인터페이스 타입에 구현 객체가 대입되었을 때(자동 형변환),
인터페이스의 메소드를 호출함으로써
구현 객체의 메소드를 모른 상태에서도 구현 객체의 메소드를 실행할 수 있음

 

▶ 인터페이스의 특징과 장점

① 다중 상속 가능
여러 인터페이스를 상속하면 여러 부모의 필드와 메소드를 이용할 수 있다. 즉, 다양한 동작을 수행할 수 있다.
→ 추상 클래스와의 가장 큰 차이점!

② 클래스 설계도
추상 클래스처럼, 인터페이스는 추상 메소드를 통해 클래스 설계를 강제할 수 있다. 
하지만, 인스턴스 메소드를 구현할 수 있는 추상 메소드와 달리 
인터페이스의 목적은 오로지 '설계'이므로 메소드를 구현할 순 없으며, 
오로지 추상 메소드만 선언만 가능하다. (default, static 제외)

③ ⭐ 클래스와 클래스를 연결하는 매개체 ⭐
개발코드가 인터페이스의 메소드를 호출하면, 인터페이스는 객체의 메소드를 호출하므로 
개발코드는 객체 내부 구조를 알 필요 없이 인터페이스의 메소드만 알고있으면 된다.
핵심 클래스를 PC라고 하고, 연결해야 하는 클래스를 스마트폰, 카메라, 하드디스크라고 하면,
이 둘을 연결하는 USB포트를 인터페이스라고 비유할 수 있다. 
USB 포트만 맞으면 어떤 기기든 연결할 수 있고, 컴퓨터는 어떤 기기가 연결되는지 신경쓸 필요가 없다.

출처 https://interconnection.tistory.com/129

 

정리 : 인터페이스의 역할
1. 다중 상속
2. 설계도
3. 객체와 객체 매개체 - 개발 코드는 객체의 내부 몰라도 인터페이스의 메소드만 호출해도 됨

 

▶ 인터페이스의 선언
인터페이스도 Class, Enum처럼 ~.java 파일로 생성되고 컴파일러에 의해 ~.class 파일로 컴파일된다.

선언 문법 : public interface 인터페이스이름 { ... }
구현 문법 : public class 클래스이름 implements 인터페이스이름 { ... }

▶ 인터페이스 멤버
인터페이스는 객체 사용 방법을 정의한 것이므로
상수 필드, 추상 메소드, 디폴트 메소드, 정적 메소드만을 구성 멤버로 가진다.
또 인터페이스는 애초에 공개를 목적으로 하기 때문에 멤버들은 모두 public 이여야 한다.

  상수 필드   고정된 값
  필드명을 대문자로 작성
  선언과 동시에 값을 초기화해야 함
 (public static final)
  추상 메소드   내부를 구현하지 않아 오버라이딩이 강제되는 하는 함수   (public abstract)
  default 메소드   리턴타입 앞에 default 키워드를 작성하고 내부를 구현함
  구현 객체에서 인스턴스 메소드처럼 사용됨
  오버라이딩은 선택 사항
  기능 추가에 많이 사용됨
  (public) default
  static 메소드   클래스의 static 메소드처럼 선언하고, 사용도 동일한 방법으로 하면 됨
  오버라이딩 불가
  (public) static

*인터페이스의 default는 접근 제어자 default랑 완전 다른 것이다.
인터페이스의 default 메소드는 구현을 강요하지 않는 다는 것이고,
접근 제어자 default는 같은 패키지 내부에서는 접근이 가능하다는 뜻

cf. 디폴트 메소드 추가 설명
클래스에 기능을 추가해야 하는 경우, 추상 메서드를 추가한다면
추상 메소드는 모든 자식이 구현해야 하는 함수이므로 이를 구현하지 않으면 에러가 발생한다.
따라서 이전에 만든 자식들 모두 에러가 발생하여 유지 보수에 좋지 않다.
이때 default 메소드를 추가한다면 
재정의가 선택이므로 나중에 추가해도 에러가 발생하지 않고, 미리 선언했어도 선택적으로 오버라이딩하면 되므로 유지보수에 효과적이다.

▶ 추상 클래스와 인터페이스의 차이점
0. 다중 상속 가능 여부
추상 클래스 - 다중 상속 불가 / 인터페이스 - 다중 상속 가능
추상 클래스를 다중 상속 하게되면, 컴파일러는 이름이 같은 함수의 출처를 구분하지 못하게 된다. (메소드의 모호성 문제)
반면 인터페이스는 추상 메소드 밖에 없으므로 메소드를 객체에서 구현해야 하기 때문에
어떤 메소드인지를 명시할 수 있으므로 메소드의 모허성 문제 없이 다중 상속이 가능하다.
예를들어 같은 이름, 같은 매개변수의 추상 메서드를 갖는 인터페이스 두개를 상속받는다면,
그 추상 메서드를 클래스 안에서 하나로 구현할 수 있으므로 메서드의 모호성 문제가 발생하지 않는다.
(http://www.tcpschool.com/java/java_polymorphism_interface)

1. 목적이 다르다.
추상 클래스 : 상속하여 같은 종류의 하위 클래스를 만들기 위함
인터페이스 : 함수 구현을 강제해서 사용 방법이 같은 클래스를 만들기 위함

2. 내부가 다르다
추상 클래스 : 필드, 생성자, 구현 메소드, 추상 메소드
인터페이스 : 상수 필드, 추상 메소드, default 메소드, static 메소드

3. 용도가 다르다.
추상 클래스는 하나만 상속 가능하므로 소속 or 종류를 나타내기 위해 사용된다.
인터페이스는 다중 상속이 가능하므로 할 수 있는 기능을 나타내기 위해 사용된다.

⭐ 한문장 정리 => ❗다중 상속 여부가 다르고❗ 내부 구성 요소가 다르다.
하지만 추상 메소드를 이용해서 오버로딩을 강제한다는 것과
자동 형변환과 추상 메소드 호출로 다형성이라는 이익을 취한다는 것은 공통점이다.

 

⭐ 인터페이스의 사용 ⭐
그렇다면, 인터페이스의 주 역할인 '매개체'역할은 어떻게 가능한 것일까? 
인터페이스가 매개체의 역할을 하기 위해선, 아래의 2step이 필요하다.

1. 인터페이스 변수에 구현 객체를 대입 (자동 타입 변환)
2. 개발 코드가 인터페이스의 메소드 호출
=> 인터페이스의 메소드를 호출함으로서 객체의 메소드를 호출하고, 리턴값을 넘겨받을 수 있게 됨

즉, 인터페이스로 구현 객체에 접근하기 위해 자동 형변환의 특징을 사용하는 것이다.
: 부모 변수에 자식 객체를 대입하면, 부모의 메소드를 호출할 때 오버로딩 된 자식의 메소드를 호출하게 되는 것

따라서 위의 1st step을 위해서 개발 코드에서는 
클래스의 필드 / 생성자의 매개변수 / 메소드의 매개변수 / 메소드의 로컬변수 등을 인터페이스 타입으로 선언하고, 
여기에 구현 객체를 대입해준다.
이후, 2nd step에서는 인터페이스 타입으로 선언된 변수의 메소드를 호출한다.

▶ 인터페이스로 객체를 연결하는 예시
1. 메인 객체의 메서드 매개변수를 인터페이스 타입으로 줌 // class Zookeeper { void feed (Predator predator) {...} }
2. 인터페이스 매개변수에 인터페이스가 구현된 객체가 전달됨 // class Tiger implements Predator { ... } // feed(tiger)
3. 개발코드에서 해당 인스턴스의 메소드 호출 // void feed (Predator predator) { predator.getfood() }
4. 인스턴스는 객체의 메소드 호출 // class Tiger implements Predator { public String getFood() {return "apple";} }
5. 개발코드에 객체의 리턴값 전달됨 >> "apple"

이렇게 두 객체가 인터페이스의 메소드로 연결되면, 핵심 객체(개발코드)는 
구현 객체 내부 구조를 모른채 인터페이스 메소드만 호출해도 구현 객체에 접근할 수 있게 된다.

⭐ 인터페이스를 사용해서 메소드를 호출하면 좋은 점 ⭐

인터페이스의 사용 방법까지 공부한 지금, 인터페이스의 강력한 장점인
'코드 변경 없이 구현 객체만을 교체하여 실행 내용과 리턴값을 다양화할 수 있다'를 이해할 수 있다.

예를들어, A 객체에 문제가 생겨 B 객체로 바꾸려 하는 경우, 아래 코드의 모든 라인을 바꿔야 한다.
A a = new A( ); ➡️ B b = new B( );
a.method1( );   ➡️ b.method1( );  
a.method2( );   ➡️ b.method2( ); 
하지만 인터페이스를 사용한경우, 구현 객체를 인터페이스에 대입하는 부분만 바꿔도 된다.
Inter i = new A( ); ➡️ Inter i = new B( );
i.method1( );   
i.method2( );  
인터페이스는 주로 메소드의 매개변수 타입으로 많이 선언되므로, 메소드에 대입하는 객체만 바꿔줌으로써 다양한 결과를 만들 수 있다.

▶ 강제 타입 변환
인터페이스와 구현 객체에 '자동 타입 변환'이 사용되었다는 것에 대해 이런 의문을 갖을 수 있다.
'자동으로 타입변환 되면, 오버라이딩 된 추상메소드 이외에, 구현 객체의 다른 멤버에는 접근할 수 없는거 아닌가?🤔'
하지만, 여느 클래스의 상속에서와 마찬가지로 인터페이스 역시 강제 타입 변환을 지원한다. 
문법 역시 (바꿀 타입) 변수 로 동일하다.

또 강제 타입변환하기 전, 해당 인터페이스가 어떤 구현 객체를 받았는지 검사하기 위해 instanceof 로 조사를 해야 하는 것도 동일하다.
차이점 : 일반적인 상속에서 instanceof는 보통 instanceof Child로 '자동 형변환이 일어난 부모 객체인가?'를 조사했지만,
인터페이스에서는 instanceof Class로 '특정 구현객체가 형변환된 것인가?'를 조사한다.

// 인터페이스
interface Vehicle {
	public abstract void run();
}

// 구현객체1
class Bus implements Vehicle{
	@Override
	public void run() {
		System.out.println("버스가 달립니다.");
	}
	// 인스턴스 메소드
	public void checkFare() {
		System.out.println("승차 요금을 체크합니다.");
	}
}

//구현객체2
class Taxi implements Vehicle{
	@Override
	public void run() {
		System.out.println("택시가 달립니다.");
	}
}

class Driver {
	public void drive(Vehicle vehicle) {
		vehicle.run();
		// instanceof를 이용한 강제 형변환
		if(vehicle instanceof Bus) {
			((Bus)vehicle).checkFare();
		}
	}
}

public class DriverExample{
	public static void main(String args[]) {
		Driver driver = new Driver();
		Vehicle vihicle;
		
		vihicle = new Bus(); // 이터페이스 타입에 구현객체 대입. 이 부분만 Taxi();로 바꾸면 결과 달라짐
		
		driver.drive(vihicle); // 출력 : 버스가 달립니다. 승차 요금을 체크합니다.
	}
}


▶ 상속 정리
클래스가 클래스 상속받음 : extends (다중 상속 불가)
인터페이스가 클래스 상속받음 : 불가능
클래스가 인터페이스 상속받음 : implements (다중 상속 가능)
인터페이스가 인터페이스 상속받음 : extends (다중 상속 가능)

▶ 인터페이스 상속
하위 인터페이스를 구현하는 클래스는 상위 인터페이스의 추상 메소드까지 구현해야 한다.
이렇게 구현된 구현 객체는 하위&상위 인터페이스 변수 어디에든 대입될 수 있다.
(여러 인터페이스를 상속했다 생각하면 쉬움)
이때 하위 인터페이스에 대입되면, 상하위 인터페이스의 메소드를 모두 사용할 수 있지만,
상위 인터페이스에 대입되면 상위 인터페이스의 메소드만 사용가능하다.

cf. 매개체의 역할 : 내부의 구현을 몰라도 인터페이스의 메소드를 통해 구현 객체의 메소드를 실행할 수 있다.
다형성 : 인터페이스 타입에 구현 객체를 대입하면, 어떤 구현 객체가 오든 같은 사용법을 사용할 수 있으며,
이때 실행 결과는 어떤 구현 객체가 오는지에 따라 다르게 나올 수 있다.
따라서 이후의 코드를 수정하지 않고 구현 객체만 바꿈으로써 실행 결과를 달라지게 할 수 있다.