08 인터페이스

winA·2025년 7월 1일

BE/Java

목록 보기
10/16
post-thumbnail

상속은 코드 재사용, 다형성을 구현하는 장점이 있다. 하지만 상속에는 다중 상속을 허용하지 않는다는 특징이 있다. 이점을 보완하기 위한 개념으로 인터페이스를 사용하고 있다.

💛 인터페이스 역할

두 객체를 연결하는 역할

인터페이스의 역할

객체 A는 인터페이스에 정의된 method만 사용하기 때문에, 실제로 어떤 구현 객체(B, C 등)가 대입되더라도 코드를 변경할 필요 없이 그대로 사용할 수 있다. 객체 A는 구현체가 무엇인지 알 필요가 없고, 인터페이스만 알고 있으면 된다.

자바에서 다형성을 구현할 때 상속도 사용할 수 있지만, 실제 개발에서는 인터페이스를 통한 다형성 구현이 더 일반적이다. 자바는 다중 상속이 불가능하지만 인터페이스는 다중 구현이 가능하므로, 설계의 유연성 측면에서도 인터페이스가 훨씬 유리하다. 또 코드 변경 범위의 최소화를 통해 오류 발생의 가능성을 낮춘다.

💛 인터페이스와 구현 클래스 선언

인터페이스 선언

class 키워드 대신 interface 키워드를 사용한다.

interface 인터페이스명{}
public interface 인터페이스명{}

인터페이스가 가질 수 있는 멤버

  • public 상수 필드
  • public abstract method
  • public default method
  • public static method
  • private method
  • private static method

구현 클래스 선언

  1. 객체 A가 인터페이스의 abstract method를 호출한다.
  2. 인터페이스는 객체B의 method를 실행한다.
  3. 객체B는 인터페이스에 선언된 abstract method와 동일한 선언부를 가진 method를 가지고 있어야 한다.
public class B implements 인터페이스명{}

객체B를 인터페이스를 구현한 객체라고 한다. 구현 객체는 인터페이스를 구현하고 있음을 선언부에 명시해야 한다.

implements키워드는 해당 클래스가 인터페이스를 통해 사용할 수 있다는 표시이며, 인터페이스의 abstract method를 재정의한 method가 있다는 뜻이다.

package ch08.sec02_contructor;

public class Audio implements RemoteControl {
	
	@Override
	public void turnOn() {
		System.out.println("Audio를 켭니다");
	}
}

변수 선언과 구현 객체 대입

인터페이스도 하나의 참조 타입이다. 그래서 변수의 타입으로 사용할 수 있다.

RemoteControl rc = new Televison;

인터페이스를 통해 구현 객체를 사용하려면, 인터페이스 변수에 구현 객체의 번지를 대입해야 한다. implements키워드와 선언된 클래스만 인터페이스의 abstract method를 호출할 수 있다. 구현 객체는 인터페이스에서 정의된 method만 사용할 수 있다.

💛 상수 필드

인터페이스에 선언된 필드는 모두 public static final 특성을 가지는 불변의 상수 필드를 멤버로 가진다. public static final 이 기본값이기 때문에 생략해도 컴파일 과정에서 위와 같은 역할을 한다.

[public static final] 타입 상수명 =;

상수는 구현 객체와 관련 없는 인터페이스 소속 멤버이므로 객체 생성 없이 인터페이스로 바로 접근해서 상수값을 읽을 수 있다.

package ch08.sec03_staticfinal;

public class RemoteControlExample {
	
	public static void main(String[] args) {
		System.out.println("리모콘 최대 볼륨: " + RemoteControl.MAX_VOLUME);
		System.out.println("리모콘 최저 볼륨: " + RemoteControl.MIN_VOLUME);
	}
}

💛 abstract method

인터페이스에 선언된 method는 모두 public abstract 특성을 가지는 abstract method를 멤버로 가진다. public abstract가 기본값이기 때문에 생략해도 컴파일 과정에서 위와 같은 역할을 한다.

[public abstract] 리턴타입 method명(매개변수, ...);

abstract method는 객체 A가 인터페이스를 통해 어떻게 method를 호출할 수 있는지 방법을 알려주는 역할을 한다. 그렇기에 구현 객체B는 abstract method의 실행부를 갖는 재정의된 method가 반드시 있어야 한다.

기본값이 public이기 때문에 구현클래스에서 public보다 낮은 접근 제한으로 재정의할 수 없다. 그래서 구현 클래스의 모든 method는 public 접근 제한자를 가진다.

💛 default method

인터페이스에서는 완전한 실행 코드를 가진 default method를 선언할 수 있다.

public default 리턴타입 method명(매개변수,...);

default method의 실행부에는 상수 필드를 읽거나 abstract method를 호출하는 코드를 작성할 수 있다.

구현 클래스는 default method를 재정의해서 자신에게 맞게 수정할 수도 있다. 재정의시 반드시 public 접근제한자를 붙여줘야 한다.

💛  static method

인터페이스에는 static method도 선언이 가능하다.

[public|private] static 리턴타입 method명(매개변수, ...){};

static method는 구현 객체가 없어도 인터페이스만으로 호출할 수 있다. public을 생략하더라도 자동으로 컴파일 과정에서 붙는 것이 차이점이다.

public interface RemoteControl {
	//	static method
	static void changeBattery() {
		System.out.println("리모콘 건전지를 교환합니다");
	}
}
public class RemoteControlExample {
	public static void main(String[] args) {
//		static method 호출
		RemoteControl.changeBattery();
	}
}

인터페이스에서 static method에서 호출한 것을 구현객체 없이 바로 인터페이스의 static method를 호출할 수 있다.

인터페이스의 static method의 실행부에는 상수 필드를 제외한 abstract method, default method, private method 등은 호출할 수 없다.static method 내부에는 똑같이 구현객체가 필요하지 않은 것만 올 수 있다.

인터페이스의 static method는 구현 객체를 호출할 수 없다. 반드시 인터페이스 이름으로만 호출해야 한다.

💛 prviate method

인터페이스 내의 멤버는 모두 기본값은 public이다. 그래서 public을 생략하더라도 컴파일 과정에서 public 접근 제한자가 붙어 항상 외부에서 접근이 가능하다. 물론 private 접근 제한자를 붙여 외부에서 접근하지 못하도록 제한할 수도 있다.

구분설명
private method구현 객체가 필요한 method
private static method구현 객체가 필요 없는 method

private method는 default method 안에서만 호출이 가능하다. 하지만 private static method는 default method뿐만 아니라 static method 안에서도 호출이 가능하다. private method는 default, static method들의 중복 코드를 줄이기 위해 사용된다.

💛 다중 인터페이스 구현

Java에서 다중 상속을 허용하지 않는 이유(다이아몬드 문제)

자바는 클래스 간의 다중 상속을 허용하지 않는다.

    A
   / \
  B   C
   \ /
    D

D는 B와 C 둘다 상속받지만, method()라는 method가 B와 C에 각각 다르게 정의돼있을 때, D는 어떤 method()를 상속받아야 할지 혼란이 생긴다. 이를 다이아몬드 문제라고 한다.

다중 인터페이스

인터페이스에는 구현 내용이 없다. 인터페이스에는 method명만 선언하고, 실행문은 존재하지 않는다. 그렇기 때문에 B와 C가 같은 이름의 method()을 가지고 있어도 충돌될 내용이 없기 때문에 문제될 것이 없다.

하나의 클래스는 두개 이상의 인터페이스를 implements를 할 수 있다. 이를 통해 인터페이스로서 타입을 여러개 가질 수 있도록 한다.

public class 구현클래스명 implements 인터페이스A, 인터페이스B{
	//모든 abstract method 재정의
}
인터페이스A 변수 = new 구현클래스명();
인터페이스B 변수 = new 구현클래스명();

implements 한 모든 인터페이스의 abstract method를 정의해줘야 한다.

참조할 수 있는 타입

public class SmartTelevision implements RemoteControl, Searchable {
	
	//turnOn() abstract method 오버라이딩
	@Override
	public void turnOn() {
		System.out.println("TV를 켭니다.");
	}
	
	//turnoff() abstract method 오버라이딩
	@Override
	public void turnOff() {
		System.out.println("TV를 끕니다.");
	}
	
	//search() abstract method 오버라이딩
	@Override
	public void search(String url) {
		System.out.println(url + "을 검색합니다.");
	}
}

SmartTelevision은 RemoteControl과 Searchable 두 인터페이스를 모두 구현했기 때문에, SmartTelevision 객체는 다음 세가지 타입으로 참조될 수 있다.

  1. smartTelevision: 모든 method(turnOn, turnOff, search) 접근 가능하다.
  2. remoteControl: turnOn, turnOff method에 접근 가능하다.
  3. Searchable: search method만 접근 가능하다.

어떤 타입으로 참조하느냐에 따라 사용 가능한 method의 범위가 달라진다.

명명 관례

접미어로 ~able을 붙인다. 인터페이스 내에 method가 하나인 경우에 많이 이렇게 명명한다.

ISP

필요한 기능만 나눠서 인터페이스를 설계하자

인터페이스A와 인터페이스B 모두 method()라는 동일한 시그니처의 method를 가지고 있다면, 구현 클래스는 method()를 한번만 구현하면 된다.

💛 인터페이스 상속

인터페이스는 클래스와 달리 다중 상속을 허용한다.

public interface 자식인터페이스 extends 부모인터페이스1, 부모인터페이스2{}

자식 인터페이스의 구현 클래스는 자식 인터페이스의 method 뿐만 아니라, 부모 인터페이스의 모든 abstract method를 재정의해야 한다.

예시

package ch08.sec09_extends;

public class ExtendsExample {
	
	public static void main(String[] args) {
		InterfaceCImpl impl = new InterfaceCImpl();
		
		//부모 인터페이스 변수
		InterfaceA ia = impl;
		ia.methodA();
//ia.methodB();
		System.out.println();
		
		InterfaceB ib = impl;
//ib.methodA();
		ib.methodB();
		System.out.println();
		
		//자식 인터페이스 변수
		InterfaceC ic = impl;
		ic.methodA();
		ic.methodB();
		ic.methodC();
	}
}

구현 객체가 자식 인터페이스 변수에 대입되면 자식 및 부모 인터페이스의 abstract method를 모두 호출할 수 있으나, 부모 인터페이스 변수에 대입되면 부모 인터페이스에 선언된 abstract method만 호출 가능하다.

💛 타입 변환

인터페이스와 구현 클래스 간에 발생

구현 클래스 명명 관례

인터페이스명Impl로 인터페이스의 구현클래스명을 정해주는 것이 관례이다.

  • 자동 타입 변환: 인터페이스 변수에 구현 객체를 대입하면 구현객체→인터페이스 타입으로 자동 타입 변환된다.
    인터페이스 변수 = 구현객체;
  • 강제 타입 변환: 인터페이스→구현 클래스 타입으로 변화하고자 한다면 캐스팅 연산자를 사용해서 강제 타입 변환해야 한다.
    구현클래스 변수 = (구현클래스) 인터페이스변수;
    package ch08.sec10_type.exam02;
    
    public class CastingExample {
    	
    	public static void main(String[] args) {
    //인터페이스 변수 선언과 구현 객체 대입
    		Vehicle vehicle = new Bus();
    
    //인터페이스를 통해서 호출
    		vehicle.run();
    //vehicle.checkFare(); (x)
    
    //강제 타입 변환후 호출
    		**Bus bus = (Bus) vehicle;**
    		bus.run();
    		bus.checkFare();
    	}
    }
    구현 객체가 인터페이스 타입으로 자동 변환되면 , 인터페이스에 선언된 method만 사용이 가능하다. 만약 구현 객체에서만 사용된 method를 사용하고 싶다면 캐스팅 연산자를 이용해서 구현클래스 타입으로 강제 타입 변환을 해준 뒤에 method를 호출해야 한다.

Object

모든 클래스의 최상위 부모 클래스

자바에서는 모든 클래스가 명시적으로 extends를 적지 않아도 자동으로 extends Object가 붙은 것처럼 처리된다.

모든 클래스는 Object를 부모로 가지고 있기 때문에, 어떤 클래스 타입이든 Object 타입으로 자동 타입 변환이 가능한 것이다.

메모리 할당

인터페이스는 객체의 틀만 정의할 뿐, 구현 내용이 없는 구조이다. 인터페이스는 메모리의 heap 영역에 인스턴스로 할당되지 않는다. 그렇기 때문에 new Interface처럼 인터페이스 자체를 인스턴스화 할 수 없다.

new 연산자로 선언된 것만 메모리의 heap영역에 할당 될 수 있다.

인터페이스는 컴파일 되면 클래스처럼 .class 파일로 저장되고, 실행시 JVM의 method 영역에 클래스 정보로만 저장이 된다.

💛 다형성

사용 방법은 동일하지만 다양한 결과가 나오는 성질

문법 자체는 상속과 다른 점이 거의 없지만, 실무에서는 인터페이스를 사용한 다형성을 더 많이 사용한다! 인터페이스를 사용하면 객체 간 결합도를 낮출 수 있고, 구현체를 쉽게 교체할 수 있는 유연한 설계가 가능해진다.

다형성의 구현 요소

  • method의 재정의(Override)
  • 자동 타입 변환(Upcasting)
package ch08.sec11_poly.exam01;

public class Car {
	
	//필드
	Tire tire1 = new HankookTire();
	Tire tire2 = new HankookTire();
	
	//method
	void run() {
		tire1.roll();
		tire2.roll();
	}
}
package ch08.sec11_poly.exam01;

public class CarExample {
	
	public static void main(String[] args) {
//자동차 객체 생성
		Car myCar = new Car();

//run() method 실행
		myCar.run();

//타이어 객체 교체
		myCar.tire1 = new KumhoTire();
		myCar.tire2 = new KumhoTire();

//run() method 실행(다형성: 실행 결과가 다름)
		myCar.run();
	}
}

Car 클래스에서 초기에 tire1과 tire2를 모두 HankookTire 객체로 초기화된다. 그렇기 때문에 run() method를 실행하면

한국 타이어
한국 타이어

가 출력이 된다. 그런 다음 myCar의 tire1, tire2를 모두 KumhoTire로 객체를 교체해준다. 그 후에 run()을 실행하면

금호 타이어
금호 타이어

를 출력한다.

Car는 Tire 인터페이스 타입만 바라보고 있고, 실제로는 어떤 타이어가 들어오는지는 신경 쓰지 않는다. 그래서 구현 객체를 교체해도 Car 코드 수정 없이 동작 결과를 변경할 수 있다.

매개변수의 다형성

매개변수 타입을 인터페이스로 선언하면 method 호출 시 다양한 구현 객체를 대입할 수 있다.

public interface Vehicle{
	void run();
}
public class Bus implements Vehicle {
    @Override
    public void run() {
        System.out.println("버스가 달립니다.");
    }
}
public class Driver{
	void drive(Vehicle vehicle){
		vehicle.run();
	}
}
Driver driver = new Driver();
Bus bus = new Bus();
driver.drive(bus);

drive() method를 호출할 때 인터페이스 Vehicle을 구현하는 어떠한 객체라도 자동변환되기 때문에 매개값으로 줄 수 있다. 어떤 객체를 주느냐에 따라 run() method의 실행결과는 다르게 나온다. 구현 객체에서 재정의된 run() method의 실행 내용이 다르기 때문이다.

💛 객체 타입 확인

인터페이스 타입으로 객체를 참조하고 있을 때, 구현 객체의 실제 타입을 확인하기 위해 instanceof 연산자를 사용한다. 이를 통해 특정 구현 객체인 경우에만 강제 타입 변환을 수행하고, 구현 클래스에만 존재하는 고유 method를 호출할 수 있다.

💛 봉인된 인터페이스

Java15부터 sealed 인터페이스가 도입됐다. 이는 부모 인터페이스에서 만들 수 있는 자식 인터페이스를 지정함으로써 이 외에는 자식 인터페이스로 생성되지 못하도록 제한한다.

public sealed interface Person permits Employee, Manager{}

상속 가능 지정받은 자식 인터페이스는 선언할 때 다음 키워드 중 하나를 반드시 선언해줘야 한다.

  • final: 더 이상 상속할 수 없다.
    public final interface Employee extends Person{}
  • non-sealed: 봉인을 해제한다.
    public non-sealed interface Manager extends Person{}

0개의 댓글