인터페이스

goose_bumps·2024년 6월 2일

Java

목록 보기
10/21

1. 인터페이스란?

인터페이스는 일종의 추상클래스이다. 다만, 추상화 정도가 높아서 추상클래스는 추상메서드 외에 일반메서드나 멤버변수를 가질 수 있는 반면, 인터페이스는 추상메서드와 상수만을 가질 수 있다(JDK 1.8 이후부터는 default 메서드와 static 메서드 사용이 가능하다)

인터페이스 작성은 다음과 같이 해야 한다.

interface 인터페이스명{
	public static final 타입 상수명 = 값;
    public abstract 메서드명(매개변수 목록);
    }

여기서 public static final과 public abstract은 생략이 가능하며, 생략 시 컴파일러가 자동으로 추가해준다.
인터페이스는 완성되지 않은 설계도와 같기 때문에 그 자체로 사용되기보다는 다른 클래스를 작성하는데 도움을 주는 용도로 사용된다.

인터페이스의 이름은 통상 able로 끝나는 경우가 많은데, 어떠한 기능 또는 행위를 하는데 필요한 메서드를 제공한다는 의미를 강조하기 위해서다. 이름이 able로 끝나면 인터페이스로 추측이 가능하지만, 의무는 아니다.

2. 인터페이스의 상속과 구현

인터페이스는 인터페이스로부터만 상속이 가능하며 클래스로부터 상속은 불가능하다.(대신, 구현이 가능하다)
클래스와는 달리 여러 개의 인터페이스를 다중 상속할 수 있다.

interface movable{
	abstract void move(int x, int y);
}

interface fixable{
	abstract void fix();
}

interface operatable extends movable, fixable{
}

인터페이스도 추상클래스와 마찬가지로 그 자체로 인스턴스 생성이 불가하며, 추상메서드를 정의하였을 경우 클래스를 통하여 추상메서드를 구현해야 한다.
클래스는 인터페이스를 상속할 수 없기 때문에 implements 라는 키워드를 사용하여 "구현"한다. 클래스는 여러 개의 인터페이스를 동시에 구현이 가능하다.

class 클래스명 implements 인터페이스명{
	//인터페이스에 정의된 추상메서드를 구현해야 함
    }

클래스 -> 인터페이스 : 구현
클래스 -> 클래스 : 상속
인터페이스 -> 인터페이스 : 상속
인터페이스 -> 클래스 : 구현 불가, 상속 불가

만약, 인터페이스를 구현하는 클래스가 인터페이스의 여러 개의 추상메서드들 중 일부만 구현할 경우 abstract을 붙여서 추상클래스로 선언해야 컴파일 오류가 발생하지 않는다.

interface movable{
	abstract void move(int x, int y);
	abstract void stop();
}
abstract class a implements movable{
	public void move(int x, int y) {}
}

movable 인터페이스의 stop 메서드를 구현하지 않았기 때문에 abstract 붙여 추상클래스로 만들지 않았다면, 컴파일 오류가 발생할 것이다.

인터페이스를 구현할 때 한 가지 주의해야 할 점이 있다.

interface movable{
	abstract void moveTo(int x, int y);
	abstract void stop();
}

class move implements movable{
	public void moveTo(int x, int y) {
		//구현할 내용
	}
	void stop() {
		//구현할 내용
	}
}

move 클래스는 movable 인터페이스의 모든 추상메서드를 오버라이딩 했지만, 컴파일 오류가 발생한다.
앞에서 인터페이스 작성 시 추상메서드는 public abstract이 붙는다고 언급했었다. 오버라이딩 시 조상의 메서드보다 더 넓은 범위의 접근 제어자를 지정해야 한다는 것을 상속에서 다뤘었다.
따라서, 인터페이스를 구현하는 클래스는 추상메서드를 오버라이딩 시 접근 제어자인 public을 반드시 추가해주어야 한다.

  • 인터페이스의 추상메서드는 public abstract으로 정의됨
  • 이를 구현하는 클래스는 오버라이딩 시 public 이상의 접근 제어자를 지정해야 함
  • 따라서, 추상메서드가 포함된 인터페이스를 구현 시 오버라이딩 하는 메서드에는 반드시 public이 추가되어야 함

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

다형성으로 인해 자손 클래스의 인스턴스를 조상타입의 참조변수로 참조하는 것이 가능하다.

class Parent{}
class Child extends Parent{}

class polymorphism{
	public static void main(String args[]){
    	Parent p = new Child(); //조상타입의 참조변수로 자손 클래스의 인스턴스 참조
        }
    }

인터페이스도 클래스와 마찬가지로 인터페이스 타입의 참조변수로 이를 구현한 클래스 인스턴스를 참조할 수 있으며, 인터페이스 타입으로 형변환도 가능하다.

interface changable{}
class change implements changable{}

class polymorphism{
	public static void main(String args[]){
    	Changable c = new Change();
        }
    }

1) 리턴 타입으로 사용

이러한 다형적인 특성을 이용하면 한 가지 인터페이스 타입으로 서로 다른 클래스의 인스턴스 반환이 가능하다.
예제를 통해 확인을 해보자.

interface Liftable{
	public abstract void BenchPress();
	public abstract void Squat();
	public abstract void DeadLift();
}

class A implements Liftable{
	public void BenchPress() {
		System.out.println("100kg");
	}
	public void Squat() {
		System.out.println("150kg");
	}
	public void DeadLift() {
		System.out.println("170kg");
	}
}

class B implements Liftable{
	public void BenchPress() {
		System.out.println("120kg");
	}
	public void Squat() {
		System.out.println("180kg");
	}
	public void DeadLift() {
		System.out.println("200kg");
	}
}

class C implements Liftable{
	public void BenchPress() {
		System.out.println("135kg");
	}
	public void Squat() {
		System.out.println("200kg");
	}
	public void DeadLift() {
		System.out.println("240kg");
	}
}

class PersonManger{
	public static Liftable getPerson(int year) {
		if(year < 5) {
			return new A();
		}
		else if(year >= 5 && year < 7) {
			return new B();
		}
		else {
			return new C();
		}
	}
}


public class Polymorphism {
	public static void main(String[] args) {
		Liftable L1 = PersonManger.getPerson(3);
		Liftable L2 = PersonManger.getPerson(6);
		Liftable L3 = PersonManger.getPerson(12);
		L1.Squat();
		L2.Squat();
		L3.Squat();
		
	}
}

A,B,C 클래스는 Liftable 인터페이스를 구현하는 서로 다른 클래스이다. PersonManger 클래스에 getPerson 메서드를 리턴 타입은 Liftable로 선언하고 year에 따라 각각 다른 클래스의 인스턴스가 반환되도록 하였다.

이처럼 Liftable이라는 한 가지 타입으로 리턴 타입을 설정하여 서로 다른 클래스의 인스턴스를 반환할 수 있는 것은 바로 같은 인터페이스를 구현하고 있다는 공통점 덕분에 가능한 것이다.

만약, A 클래스가 NewA로 변경되어도 getPerson 메서드의 return new A(); 만 return new NewA();로 바꿔주면 된다. 이러한 특징은 분산환경 프로그래밍에서 큰 장점이 된다.
사용자에 설치된 프로그램을 변경하지 않고 서버측의 변경만으로도 사용자가 개정된 프로그램 사용이 가능하기 때문이다.

2) 매개변수로 사용

매개변수로 인터페이스 타입의 변수를 사용할 수 있다.
예시를 하나 들어보자. Marine, Tank, SCV는 GroundUnit을 상속하고 Dropship, Wraith는 AirUnit을 상속한다. 모든 유닛은 Unit 클래스라는 공통 조상을 가진다.
SCV에는 수리 기능이 있는데 기계 유닛에만 적용된다.
기계 유닛은 Tank, DropShip, Wraith인데 사용할 수 있는 공통 조상은 Unit뿐이다. 하지만, Unit 타입으로 하여 적용을 하면 Marine도 대상이 된다.
클래스 간의 상속 관계에 영향을 미치지 않으면서 공통 관계를 형성할 수 있는 방법이 없을까?
공통의 인터페이스를 구현하면 해결된다.

Repairable이라는 인터페이스를 생성하여 이를 구현한 클래스만 수리할 수 있게 하면 된다.
코드를 짜서 이해해보자.

class Unit{}

class GroundUnit extends Unit{}

class AirUnit extends Unit{}

class Marine extends GroundUnit{}

class Tank extends GroundUnit implements Repairable{}

class SCV extends GroundUnit implements Repairable{
	void repair(Repairable r) {
		// 수리 기능
		}
}
	
class DropShip extends AirUnit implements Repairable{}

class Wraith extends AirUnit implements Repairable{}

interface Repairable{}

이렇게 인터페이스를 공통적으로 구현하면 공통관계를 형성하여 매개변수의 타입으로 사용이 가능하다.
만약, repair 메서드에 매개변수로 Marine의 인스턴스가 들어가면 컴파일 에러가 발생할 것이다.

4. 인터페이스 장점

  • 개발시간 단축이 가능
  • 표준화 가능
  • 서로 관계없는 클래스에게 관계를 맺어줄 수 있음
  • 독립적인 프로그래밍 가능

인터페이스를 사용하면 위와 같은 장점이 있다.
인터페이스가 작성되면 선언부만 알면 되기 때문에 동시에 프로그래밍이 가능하다. 무슨 말이냐면, 한 쪽에서는 인터페이스를 작성하면서 큰 틀을 짜고, 다른 한 쪽에서는 그 기능을 구현하는 작업을 할 수 있어 개발시간이 단축된다는 것이다.
인터페이스를 기본 틀로 하여 개발자들에게 인터페이스를 구현하도록 하여 정형화된 즉, 표준화가 가능하다.
앞에서 언급했지만, 같은 인터페이스를 구현함으로써 서로 다른 클래스에게 관계를 맺어주는 것도 가능하다.
또한, 클래스-클래스 간의 직접적인 관계를 클래스-인터페이스-클래스 같은 간접적인 관계로 변경하여 독립적인 프로그래밍이 가능하다.

5. 인터페이스를 이용한 간접 관계 형성

인터페이스의 본질은 직접적인 관계를 간접적인 관계로 만드는데에 있다.
클래스는 사용하는 쪽과 제공하는 쪽으로 나뉜다.
A,B 클래스가 있다고 가정하자. B클래스에는 methodB라는 메서드가 있고 A클래스는 methodB라는 메서드명만 알면 사용가능하다.
클래스A는 methodB를 사용하는 쪽인데 사용하기 전에 우선 클래스B가 만들어져야 한다. 또한, methodB의 선언부가 변경되면 클래스A도 변경되어야 한다. 이를 직접적인 관계인 A-B라고 하겠다.

class A{
	public void methodA(B b){
    	b.methodB();
        }
 }
 
 class B{
 	public void methodB(){
    	System.out.println("methodB()");
        }
 }

인터페이스를 사용하여 클래스의 선언과 구현을 분리하면 이 직접적인 관계를 간접적인 관계로 바꿀 수 있다.
클래스B가 인터페이스를 구현하고 클래스A는 매개변수로 인터페이스를 받으면 A-I-B라는 관계가 형성된다.

class A{
	public void methodA(I i){
    	i.methodB();
        }
 }
 
 class B implements I{
 	public void methodB(){
    	System.out.println("methodB()");
        }
 }
 
 interface I{
 	public abstract void methodB();
 }

이 관계에서는 클래스B가 변경되어도 클래스A는 직접적인 관계인 인터페이스I와 관계가 있기 때문에 클래스A는 클래스B의 영향을 받지 않는다.

이를 응용한 코드를 작성해보자.

class A{
	void methodA() {
		I i = classManager.getInstanceOf();
		i.methodB();
		System.out.println(i.toString());
	}
}
class B implements I{
	public void methodB() {
		System.out.println("methodB()");
	}
	public String toString() {
		return "class B";
	}
}
interface I{
	public abstract void methodB();
}
class InstanceProvider{
	public static B getInstanceOf() {
		return new B();
	}
}

public class Interfaceundirectrelation {
	public static void main(String[] args) {
		A a = new A();
		a.methodA();
	}
}

제3의 클래스인 InstanceProvider를 통해 인터페이스I를 구현한 클래스B의 인스턴스를 가져올 수 있다.
B의 인스턴스를 return하는 getInstanceOf 메서드는 클래스 메서드이기 때문에 인스턴스 생성 없이 호출이 가능하다.
인스턴스를 getInstanceOf가 제공하기 때문에 다른 클래스의 인스턴스로 변경되어도 클래스A의 변경없이 return new B()만 변경하면 된다.

인터페이스I 타입의 참조변수가 B의 인스턴스를 참조하고 methodB()를 호출하였다.
여기서 한 가지 알아야 할 점은 인터페이스 타입으로 toString을 호출하였다는 것이다. 즉, Object 클래스의 메서드를 호출할 수 있다.

인터페이스에는 toString()이 정의되어 있지 않지만, 모든 객체는 Object클래스에 정의된 메서드를 가지고 있을 것이기 때문에 허용하는 것이다.

6. 인터페이스의 default 메서드와 static 메서드

JDK 1.8 버전 이후부터는 인터페이스에 default 메서드와 static 메서드가 추가되었다.

1) 인터페이스의 static 메서드

클래스의 static 메서드와 마찬가지로 인터페이스명.메서드명으로 호출한다.. 접근 제어자는 항상 public이며 생략 시 자동으로 추가된다. 추상메서드와 달리 {}구현부가 존재한다.
JDK 1.8 이전까지는 자바를 쉽게 배울 수 있도록 "인터페이스의 모든 메서드는 추상 메서드"라는 규칙에 예외를 두지 않았다.
이 때문에 인터페이스와 관련된 static 메서드는 별도의 클래스를 두어야 했다.(Collections 클래스)

interface I{
	public static void methodI() {
		System.out.println("인터페이스의 static 메서드");
	}
}
public class InterfaceStaticMethod {
	public static void main(String[] args) {
		I.methodI(); //인터페이스명.메서드명으로 호출
	}
}

2) 인터페이스의 default 메서드

JDK 1.8 이전 기준으로 인터페이스에 메서드를 추가한다는 것은 곧 추상 메서드를 추가하는 것이었고, 추상 메서드가 추가되면 인터페이스를 구현한 모든 클래스에 메서드를 구현해야 했다.
이러한 이유로 default 메서드가 등장하였고 추상 메서드가 아니기 때문에 추가하여도 구현할 의무가 없다.

default메서드는 접근 제어자가 항상 public이며 {}구현부가 존재한다.

기존의 메서드와 새로 추가된 default 메서드가 이름이 중복되어 충돌하는 경우 해결하는 규칙이 있다.

  • default 메서드와 조상 클래스의 메서드간 충돌 -> 조상 클래스의 메서드가 상속되고 default 메서드 무시
  • 여러 인터페이스의 default 메서드 간 충돌 -> 인터페이스를 구현한 클래스에서 default 메서드 오버라이딩

글로만 봐서는 의미가 잘 와닿지 않으니 예제로 이해해보자.

class Child extends Parent implements MyInterface1, MyInterface2{
	public void method1() {
		System.out.println("여러 인터페이스의 default 메서드 간 충돌");
	}
}
class Parent{
	public void method2() {
		System.out.println("Parent method2"); // default 메서드와 조상클래스의 메서드 충돌 // 조상클래스의 메서드가 우선된다
	}
}
interface MyInterface1{
	public default void method1() {
		System.out.println("MyInterface1 method1"); //여러 인터페이스의 default 메서드 간 충돌 //오버라이딩 해야 함
	}
	public default void method2() {
		System.out.println("MyInterface1 method2"); // default 메서드와 조상클래스의 메서드 충돌 // 조상클래스의 메서드가 우선된다
	}
}
interface MyInterface2{
	public default void method1() {
		System.out.println("MyInterface2 method1"); //여러 인터페이스의 default 메서드 간 충돌 //오버라이딩 해야 함
	}
}
public class Interfaceundirectrelation {
	public static void main(String[] args) {
		Child c = new Child();
		c.method1();
		c.method2();
	}
}

//여러 인터페이스의 default 메서드 간 충돌
//Parent method2

method1은 MyInterface1, MyInterface2가 이름이 겹친다. 이 경우에는 인터페이스를 구현한 Child 클래스에서 method1을 구현해야만 한다.(그렇지 않으면 컴파일 에러가 발생한다)

method2는 MyInterface1과 Parent가 이름이 겹친다. 이 경우에는 조상 클래스인 Parent클래스의 메서드가 우선되어 호출된다.


0개의 댓글