[Java/자바] 인터페이스(interface)란 ?

인터페이스란 ?

인터페이스는 일종의 추상클래스로, 추상메서드를 갖지만 추상클래스보다 추상화 정도가 높아 추상클래스와 달리 몸통을 갖춘 일반 메서드 또는 멤버변수를 구성원으로 가질 수 없습니다.

오직 추상메서드와 상수만을 멤버로 가질 수 있으며, 그 외의 다른 어떠한 요소도 허용되지 않습니다.

 

추상 클래스를 부분적으로만 완성된 "미완성 설계도"라고 한다면, 인터페이스는 구현된 것은 아무것도 없는 그냥 스케치만 되어 있는 "기본 설계도"라 할 수 있습니다.

 

인터페이스의 특징
  • 다중 상속이 가능하다.
  • 추상 메서드와 상수만을 가진다.
  • 생성자 생성이 불가능하다.
  • 메서드 오버라이딩이 필수적이다.

 

인터페이스의 장점

1. 개발시간을 단축시킬 수 있다.

인터페이스를 통해 메서드를 호출하는 쪽에서는 메서드의 내용과 관계없이 선언부만 알면되기 때문에 효율적입니다.

그리고 동시에 다른 한 쪽에서는 인터페이스를 구현하는 클래스를 작성하게 하면, 인터페이스를 구현하는 클래스가 작성될 때 까지 기다리지 않고도 양쪽에서 동시에 개발을 진행할 수 있습니다.

 

2. 표준화가 가능하다.

프로젝트에 사용되는 기본 틀을 인터페이스로 작성한 다음, 개발자들에게 인터페이스를 구현하여 프로그램을 작성하도록 함으로써 보다 일관되고 정형화된 프로그램의 개발이 가능합니다.

 

3. 서로 관계없는 클래스들에게 관계를 맺어 줄 수 있다.

서로 상속관계에 있지도 않고, 같은 조상클래스를 가지고 있지 않은 서로 아무런 관계도 없는 클래스들에게  하나의 인터페이스를 공통적으로 구현하도록 함으로써 관계를 맺어 줄 수 있습니다.

 

4. 독립적인 프로그래밍이 가능하다.

인터페이스를 이용하면 클래스의 선언과 구현을 분리시킬 수 있기 때문에 실제구현에 독립적인 프로그램을 작성하는 것이 가능합니다.

클래스와 클래스간의 직접적인 관계를 인터페이스를 이용해서 간접적인 관계로 변경하면, 한 클래스의 변경이 관련된 다른 클래스에 영향을 미치지 않는 독립적인 프로그램이 가능합니다.


인터페이스 작성

 

인터페이스는 클래스를 작성하는 것과 같습니다.

차이점은 class 대신 interface를 사용하며 다음과 같이 작성합니다.

( interface도 클래스와 같이 접근제어자로 public 또는 default를 사용할 수 있습니다. )

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

 

인스턴스 클래스는 일반적인 클래스 멤버들과 달리 다음과 같은 제약사항을 가집니다.

  • 모든 멤버변수는 public static final 이여야 하고, 이를 생략할 수 있다.
  • 모든 메서드는 public abstract 이어야 하고, 이를 생략할 수 있다. (단, static 메서드와 default 메서드는 예외 - JDK 1.8부터 )

보통은 편의를 위해 생략하는 편이며, 생략 시 컴파일러가 자동적으로 추가해줍니다.

 


default메서드와 static 메서드

jdk 1.8이전까지는 상수, 추상메소드만 선언이 가능했지만, 이후부터 디폴트메소드, 정적메소드가 추가되었습니다.

 

public interface 인터페이스명 {

	//상수
    public static final 타입 상수이름 = 값;
    
	//추상 메소드
    public abstract 메서드이름(매개변수목록);

    	//디폴트 메소드
    default 타입 메소드이름(매개변수목록){
      //구현부
    }

    	//정적 메소드
    static 타입 메소드이름(매개변수목록) {
      //구현부
    }

}

 

  • 상수 : 인터페이스에서 값을 정해주니 함부로 바꾸지 말고 제공해주는 값만 참조 ( 절대적 )
  • 추상메서드 : 선언부만 줄테니 추상메서드를 오버라이딩해서 재구현 ( 강제적 )
  • default메서드 : 인터페이스에서 기본적으로 제공해주지만, 원하는 대로 각자 구현 ( 선택적 )
  • static메서드 : 인터페이스에서 제공해주는 것으로 무조건 사용 ( 절대적 )

 

예시 코드

https://limkydev.tistory.com/197의 글을 참고한 예시 코드입니다.

 

대한민국에서 은행 사업을 하려면, 금융결제원에서 정의한 어떠한 가이드를 따라야한다고 치고, Bank 라는 이름으로 인터페이스를 만듭니다.

이제 어느 은행이든 은행 시스템은 운영하려면 Bank라는 인터페이스 가이드에 맞게 구현해야합니다..

인출메소드, 입금메소드는 각 은행에서 오버라이딩 해서 재구현을 해야하며 

블록체인 인증 메소드는 무조건 금융결제원에서 제공해주는 메소드를 사용해야 합니다. ( 정적메소드로 구현하여 오버라이딩을 할 수 없음 )

 

public interface Bank {

	//상수 (최대 고객에게 인출해 줄 수 있는 금액 명시)
	public int MAX_INTEGER = 10000000;
	
	//추상메서드(인출 메서드)
	void withDraw(int price);
	
	//추상메서드(입금 메서드)
	void deposit(int price);
	
	//defualt 메소드(고객의 휴면계좌 찾아주는 메서드 : 필수구현은 선택사항)
	default String findDormancyAccount(String custId){
		System.out.println("**금융개정법안 00이후 고객의 휴면계좌 찾아주기 운동**");
		System.out.println("**금융결제원에서 제공하는 로직**");
		return "00은행 000-000-0000-00";
	}
	
	//정적 메서드(블록체인 인증을 요청하는 메서드 : 오버라이딩 불가능)
	static void BCAuth(String bankName){
		System.out.println(bankName+" 에서 블록체인 인증을 요청합니다.");
		System.out.println("전 금융사 공통 블록체인 로직 수행");
	}
		
	
}

 

이 예시를 보고 추상메서드와 정적메서드에 대해서는 이해가 가지만 디폴트메서드에 대해서는 확실한 이해가 가지 않을 수 있습니다.

 

디폴트메서드 예시 코드

조상 클래스에 새로운 메서드를 추가하는 것은 별 일이 아니지만,

인터페이스의 경우에는 메서드를 추가하게 되면 이 인터페이스를 구현한 기존의 모든 클래스들이 이 새로 추가된 추상 메서드를 구현해야한다는 불편함이있습니다.

 

디폴트메서드는 이 불편함을 해결하기 위해 새로 추가되었으며 디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드로,

추상 메서드가 아니기 때문에 디폴트 메서드가 새로 추가되더라도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 됩니다.

class DefaultMethodTest {
	public static void main(String[] args) {
		Child2 c = new Child2();
		c.method1();
		c.method2();
		MyInterface.staticMethod(); 
		MyInterface2.staticMethod();
	}
}

class Child2 extends Parent2 implements MyInterface, MyInterface2 {
	public void method1() {	
		System.out.println("method1() in Child");
	}			
}

class Parent2 {
	public void method2() {	
		System.out.println("method2() 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 메서드와 static 메서드는 정의 가능하다.
	default void method1() { 
		System.out.println("method1() in MyInterface2");
	}

	static  void staticMethod() { 
		System.out.println("staticMethod() in MyInterface2");
	}
}
출력 결과

위 예시를 보면 MyInterface와 MyInterface2를 구현하는 Child2 클래스는 MyInterface와 MyInterface2의 default 메서드를 재구현하지 않아도 오류가 발생하지 않습니다.

다만, 새로 추가된 default 메서드가 기존의 메서드와 이름이 중복되는 경우 충돌이 발생하여 위처럼 MyInterface 의 메서드가 무시되고 "method1() in Child"가 출력됐습니다.

충돌을 해결하는 규칙은 다음과 같으며 위 코드에는 규칙 2번이 적용됩니다.

1. 여러 인터페이스의 디폴트 메서드 간의 충돌 : 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩 한다.
2. 디폴트 메서드와 조상 클래스의 메서든 간의 충돌 : 조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.

 

default 메서드에 대해 쉽게 이해하자면, 결국 이미 운영되고 있는 시스템에서 추가 요건으로 인해 불가피하게 반영을 해야할 때 디폴트메소드를 쓰면 효과적입니다.

 


인터페이스 상속과 다중상속

인터페이스는 인터페이스로부터만 상속받을 수 있으며, 클래스와 달리 다중 상속이 가능합니다.

클래스의 상속과 마찬가지로 자손 인터페이스는 조상 인터페이스에 정의된 멤버를 모두 상속 받습니다.

 

상속시 클래스의 "extends"와 달리 구현한다는 의미인 "implements"를 사용합니다.

그리고 다음과 같이 상속과 구현을 동시에 할 수도 있습니다.

class 클래스이름 extends 조상클래스 implements 인터페이스이름 { /* 내용 생략 */ }

 

예시 코드
interface Animal { public abstract void cry(); }
interface Pet { public abstract void play(); }

class Cat implements Animal, Pet {

    public void cry() {
        System.out.println("Cat : 냐옹!");
    }

    public void play() {
        System.out.println("Cat : 쥐 잡기 놀이하자");
    }
}

class Dog implements Animal, Pet {
    public void cry() {
        System.out.println("Dog : 멍멍!");
    }

    public void play() {
        System.out.println("Dog : 산책갈까?");
    }
}

public class Test1 {
    public static void main(String[] args) {
        Cat c = new Cat();
        Dog d = new Dog();

        c.cry();
        c.play();
        d.cry();
        d.play();
    }
}

출력 결과

 

 


인터페이스 구현

 

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클래스의 자손입니다.");
		}
	}
}

// Unit 클래스를 상속받고, Fightable 인터페이스를 구현한다.
class Fighter extends Unit implements Fightable {
	// 조상의 메서드보다 넓은 범위의 접근 제어자를 사용해야하기 때문에 public을 붙여야 한다.
	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 {	public abstract void move(int x, int y);	}
interface Attackable {	public abstract void attack(Unit u); }

출력 결과

 

위 코드의 출력 결과를 그림으로 나타내면 다음과 같습니다.

 

여기서 중요한 점은 Movable 인터페이스에 정의된 "void move(int x, int y)"를 Fighter클래스에서 구현할 때 접근 제어자를 public으로 한 점입니다.

그 이유는 Movable 인터페이스에 정의된 "void move(int x, int y)"에 사실 public abstract가 생략된 것이기 때문에, 실제로는 "public abstract void move(int x, int y)"와 같습니다.

그렇기 때문에 이를 구현하는 Fighter 클래스에서는 조상의 메서드보다 넓은 범위의 접근 제어자를 지정해야하기 때문에 접근 제어자를 반드시 "public"으로 해야 합니다.

 


참고자료
자바의 정석3
https://limkydev.tistory.com/197
http://www.tcpschool.com/java/java_polymorphism_interface