본문 바로가기
자바

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

by 책 읽는 개발자_테드 2021. 1. 7.
반응형

자바의 인터페이스(interface)란?

 

이 글은 `자바 인터페이스란 무엇인가?`, `어떻게 정의하고 사용하는가?`에 대하여 다룹니다. 추가적으로 인터페이스와 관련된 지식으로 익명 구현 객체, 함수형 인터페이스, 다형성, java8에 추가된 디폴트(default) 메소드와 정적(static) 메소드, java9에 추가된 private 메소드에 대해서 설명합니다.

 

인터페이스(interface)란?

- 자바에서 객체의 사용 방법을 정의해둔 타입 

- 객체의 교환성을 높여, 다형성을 구현하는데 중요한 역할을 함

- 장점: 1. 설계 단계에 인터페이스를 만들어 두면 설계 단계의 산출물과 개발 단계의 산출물을 효율적으로 관리할 수 있고,

               개발할 때 메소드명, 매개 변수 명을 고민하지 안아도됨

           2. 개발자의 역량에 따른 메소드, 매개 변수 네이밍 차이를 줄일 수 있음

 

인터페이스는 개발 코드와 객체가 서로 통신하는 접점 역할을 한다. 개발 코드가 인터페이스의 메소드를 호출하면 인터페이스는 객체의 메소드를 호출시킨다. 때문에 개발 코드는 객체의 내부 구조를 알 필요가 없고 인터페이스의 메소드만 알고 있으면 된다.

 

 

개발 코드가 직접 객체의 메소드를 호출하지 않고, 인터페이스를 이용하는 이유는?

인터페이스는 하나의 객체가 아니라 여러 객체들과 사용이 가능하다. 때문에 개발 코드의 변경 없이 어떤 객체를 사용하느냐에 따라 실행 내용과 리턴값을 다양화 할 수 있는 장점이 있다.

인터페이스를 정의하는 방법

 

 interface 키워드를 사용

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

 

예시: TV 인터페이스

인터페이스의 구성 멤버: 상수, 메소드 (인터페이스는 객체로 생성할 수 없기 때문에 생성자를 가질 수 없다. )

<->

클래스의 구성 멤버: 필드, 생성자 , 메소드

 

자바 7 이전까지는 인터페이스의 메소드는 실행 블록이 없는 추상 메소드로만 선언이 가능했지만, 자바 8부터 메소드 실행 블록 작성이 가능디폴트 메소드정적 메소드 선언이 가능해졌다.

interface 인터페이스명{

//상수
타입 상수명 = 값;

//추상 메소드
타입 메소드명(매개변수, ...);

//디폴트 메소드
default 타입 메소드명(매개변수, ...){...}

//정적 메소드
static 타입 메소드명(매개변수, ...){...}

}

 

인터페이스의 상수 필드  

클래스에서 상수는 public static final로 선언하지만, interface에서는 이를 생략 가능하다.

생략하더라도 자동적으로 컴파일 과정에서 붙는다.

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

 

상수명은 대문자로 작성하되, 서로 다른 단어는 언더바(_)로 연결하는 것이 관례

public interface TV {

   public static final int MAX_CHANNEL = 100;
   int MIN_CHANNEL = 0; // public static final 생략 가능

}

 

인터페이스의 추상 메소드 

 

인터페이스를 통해 호출된 메소드는 최종적으로 객체에서 실행된다. 때문에 인터페이스의 메소드는 실행 블록이 필요 없는 추상 메소드로 선언한다. 

 

 

 

인터페이스에 선언된 추상 메소드는 모두 public abstract의 특성을 갖기 때문에 이를 생략해도 자동적으로 컴파일 과정에서 붙는다.

추상 메소드: 리턴 타입, 메소드명, 매개변수만 기술하고 중괄호 ’{}’를 붙이지 않는 메소드

public interface TV {

    //상수
    int MAX_CHANNEL = 1000;
    int MIN_CHANNEL = 0;



    //추상 메소드
   public abstract void turnOn();
   public abstract void turnOff();
   void setChannel(int channelNum); //public abstract 생략가능

}

 

인터페이스의 디폴트 메소드(기본 메소드, Default Method)

- 인터페이스에 선언되지만 실제 실행하는 구현 코드를 작성하는 메소드

- 자바 8에 추가된 인터페이스의 새로운 멤버

 

디폴트 메서드가 만들어진 이유

인터페이스를 쉽게 바꾸어서 미래에 프로그램이 쉽게 변화할 수 있는 환경을 제공하기 위해

 

이를 이해하기 위해 사례를 한 가지 들어보자. 자바 8 이전에는 List<T>가 stream이나 parallelStream 메서드를 지원하지 않았다. 이것을 지원하기 위한 간단한 해결책은 List<T>가 구현하고 있는 Collection 인터페이스에 stream 메서드를 추가하고, ArrayList 클래스에서 메서드를 구현하는 것이었다.

 

하지만 이 방법은 사용자에게 너무 고통을 안겨준다. 이미 Collection 인터페이스를 구현하는 많은 컬렉션 프레임워크들이 존재하기 때문에 인터페이스에 새로운 메서드를 추가하면 인터페이스를 구현한 모든 클래스에서 새로 추가된 메서드를 구현해야 했다.

 

자바 개발자들을 구현을 고치지 않고도 이미 공개된 인터페이스를 변경할 수 있는 방법을 생각했다. 바로 구현 클래스에서 구현하지 않아도 되는 메서드를 인터페이스에 추가하는 것이었다. 이 기능의 메서드 본문은 클래스 구현이 아니라 인터페이스의 일부로 포함되기 때문에 디폴트 메서드라고 부르게되었다.

 

디폴트 메소드를 정의하는 방법: default 키워드를 return 타입 앞에 표시

public 특성을 갖기 때문에 이를 생략해도 자동적으로 컴파일 과정에 붙게 된다.

[public] default 리턴타입 메소드명(매개변수, ...) {...}
public interface TV {

    //상수
    int MAX_CHANNEL = 1000;
    int MIN_CHANNEL = 0;

    //추상 메소드
    void turnOn();
    void turnOff();
    void setChannel(int channelNum);
  
	//디폴트 메소드
	default void setMute(boolean mute){

		if(mute){
    	   System.out.println("무음 처리");
   	 	}else{
    	   System.out.println("무음 해제");
   		}
	}
}

 

인터페이스의 정적 메소드 (static)

- 디폴트 메서드와 같이 실제 실행되는 구현 코드를 작성하는 메서드이며, 디폴트 메서드와 달리 객체가 없어도 인터페이스만으로 호출 가능

- 자바 8 부터 추가

 

인터페이스의 정적 메소드를 정의하는 방법형태는 클래스의 정적 메소드와 동일 하지만 public 특성을 갖기 때문에 public을 생략하더라도 자동으로 컴파일 과정에 붙는다.

[public] static 리턴타입 메소드명(매개변수, ...) {...}

 

public interface TV {

    //상수
    int MAX_CHANNEL = 1000;
    int MIN_CHANNEL = 0;


    //추상 메소드
    void turnOn();
    void turnOff();
    void setChannel(int channelNum);



    //디폴트 메소드
    default void setMute(boolean mute){
        if(mute){
            System.out.println("무음 처리");
        }else{
            System.out.println("무음 해제");
        }
    }

	//정적 메소드
	static void printMaxChannel(){
  		System.out.println("채녈은 "+MAX_CHANNEL+"번까지 존재합니다.");
	}
}

 

Q. 인터페이스의 defulat, static 메소드의 등장으로 추상클래스는 필요 없어졌을까?

public interface Keyboard {
    void ledOn();
    void ledOff();
    void touch();
}
public abstract class Computer implements Keyboard{
    private String message;

    @Override
    public void touch(){
        System.out.println(message);
    }

    public void setMessage(String message){
        this.message = message;
    }
}

추상 클래스에서는 위 코드의 String message 과 같이 변수를 선언할 수 있다. 인터페이스는 상수만 선언이 가능하다. 즉, 작성하려는 코드의 상태를 변수로 관리해야한다면 추상 클래스를 사용해야한다.

 

 

인터페이스의 private 메소드

 

자바 8에서 인터페이스 메소드는 특정 기능을 처리하는 내부 메소드도 외부에 공개되는 public method로 만들어야 하는 단점이 있었다.

 

하지만 인터페이스를 구현하는 다른 인터페이스 또는 클래스가 해당 메소드에 액세스하거나 상속 하는 것을 원하지 않는 경우가 있다. 이를 위해 자바 9에서는 인터페이스에 메소드에 private을 사용할 수 있도록 했다.

 

public interface TV {

	/*   ~ 생략 ~   */


	//private 메소드
    
	private int getMaxChannel(){
  	 return MAX_CHANNEL;
	}

	private static int getMinChannel(){
 	  return  MIN_CHANNEL;
	}

}

 

인터페이스를 구현하는 방법

 

개발 코드가 인터페이스 메소드를 호출하면 인터페이스는 객체의 메소드를 호출한다. 객체는 인터페이스에서 정의된 추상 메소드와 동일한 메소드 이름, 매개 타입, 리턴 타입을 가진 실체 메소드를 가지고 있어야 한다. 

 

이러한 객체를 인터페이스의 구현(implement) 객체라고 하고, 구현 객체를 생성하는 클래스를 구현 클래스라고 한다. 

 

구현 클래스는 인터페이스 타입으로 사용할 수 있음을 알려주기 위해 클래스 선언부에 implements 키워드를 추가하고 인터페이스명을 명시해야 한다. 그리고 인터페이스에 선언된 추상 실체 메소드를 선언한다.

 

public class 구현클래스명 implements 인터페이스명{
//인터페이스에 선언된 추상 메소드의 실체 메소드 선언
}

 

이때 다음과 같은 주의 사항을 명심하자. 

 

 🖍 실체 메소드를 작성할 때 인터페이스의 모든 메소드는 기본적으로 public 접근 제한을 갖기 때문에 public보다 낮은 접근 제한으로 작성할 수 없다.

🖍 실체 메소드 위에 @Override 어노테이션을 붙인다. 이렇게 하면 추상 메소드에 대한 정확한 실체 메소드인지 컴파일러가 체크한다.

 

public class SamsungTV implements TV{

   int currentChannel = 0;

   @Override
   public void turnOn() {
       System.out.println("TV On");
   }

   @Override
   public void turnOff() {
       System.out.println("TV Off");
   }

   @Override
   public void setChannel(int channelNum) {
       currentChannel = channelNum;
   }

}

 

만약 인터페이스에 선언된 추상 메소드에 대응하는 실체 메소드를 구현 클래스가 작성하지 않고, 클래스 선언부에 abstract 키워드를 추가하면 구현 클래스는 자동으로 추상 클래스다 된다. 

 

public abstract class SamsungTV implements TV{

}

 

익명 구현 객체

 

구현 클래스를 만들면 클래스를 재사용할 수 있기 때문에 편리하지만, 일회성의 구현 객체를 만들기 위해 소스 파일을 만들고 클래스를 선언하는 것은 비효율적이다. 자바는 소스 파일을 만들지 않고도 구현 객체를 만들 수 있는 방법을 제공하는데, 그것이 익명 구현 객체이다. 

 

익명 구현 객체는 아래와 같이 중괄호 ‘{}’ 내부에 인터페이스에 선언된 모든 추상 메소드들의 실체 메소드를 작성하고, 중괄호 ‘{}’ 뒤에 세미콜론';’을 붙여 생성한다. 추가적으로 필드와 메소드를 선언할 수 있지만, 익명 객체 안에서만 사용할 수 있고 인터페이스 변수로 접근할 수 없다.

 

인터페이스 변수 = new 인터페이스(){

//인터페이스에 선언된 추상 메소드의 실제 메소드 선언

};

public class Main {

   public static void main(String[] args){

       TV tv = new TV(){

           int currentChannel = 0;

           @Override
           public void turnOn() {
               System.out.println("TV On");
           }

           @Override
           public void turnOff() {
               System.out.println("TV Off");
           }
           
           @Override
           public void setChannel(int channelNum) {
               currentChannel = channelNum;
           }
       };
   }
}

 

함수형 인터페이스

 

자바 8에서 지원하는 람다식이 이러한 인터페이스의 익명 구현 객체를 만든다. 자바 8에서 인터페이스의 중요성은 더욱 커졌다. 자바 8의 람다식은 함수형 인터페이스의 구현 객체를 생성하기 때문이다.  함수형 인터페이스는 1개의 추상 메소드를 갖고 있는 인터페이스다.

 

public interface FunctionalInterface {
    public abstract void method(String message);
}

 

람다식은 이러한 함수현 인터페이스로만 접근이 가능하다. 아래 코드에서 변수 func는 람다식으로 생성한 객체를 가리킨다.

public class Main {
    public static void main(String[] args){
        FunctionalInterface func = message -> {
            System.out.println(message);
        };

        func.method("메서드 실행");
   }
}

 

다중 인터페이스 구현 객체

 

객체는 다수의 인터페이스 타입으로 사용할 수 있다.

 

인터페이스 A와 인터페이스 B가 객체의 메소드를 호출할 수 있으려면, 객체는 이 두 인터페이스를 모두 구현하여 다음과 같이 작성된다.

 

public class 구현클래스명 implements 인터페이스A, 인터페이스B{
//인터페이스 A에 선언된 추상 메소드의 실체 메소드 선언
//인터페이스 B에 선언된 추상 메소드의 실체 메소드 선언
}

 

다중 인터페이스를 구현할 때, 구현 클래스는 모든 인터페이스의 추상 메소드에 대해 실체 메소드를 작성해야 한다. 만약 하나라도 없다면 추상 클래스로 선언해야 한다.

 

public interface Searchable {
   void search(String url);
}

public class SamsungTV implements TV, Searchable{

   int currentChannel = 0;

   @Override
   public void turnOn() {
       System.out.println("TV On");
   }

   @Override
   public void turnOff() {
       System.out.println("TV Off");
   }


   @Override
   public void setChannel(int channelNum) {
       currentChannel = channelNum;
   }

   @Override
   public void search(String url) {
       System.out.println(url+ "검색 시작");
   }

}

 

Q. 다중 인터페이스를 구현할 때 메소드 시그니쳐가 같은 경우 어떻게 할까?

 

이런 경우 인터페이스의 메소드를 재정의 하여 사용할 수 있다. 메소드를 재정의 하지 않으면 에러가 발생한다. 만약 인터페이스의 디폴트 메소드를 사용하고 싶다면 '인터페이스.super.메소드' 형태로 참조할 수 있다.

 

public interface InterfaceA {
    default public void method(){
        System.out.println("InterfaceA - method() 실행");
    }
}

public interface InterfaceB {

    default public void method(){
        System.out.println("InterfaceB - method() 실행");
    }
}

public class Implementation implements InterfaceA, InterfaceB{

    @Override
    public void method() {
        InterfaceA.super.method();
        InterfaceB.super.method();
    }
}

 

인터페이스 레퍼런스를 통해 구현체를 사용하는 방법

 

구현 클래스가 작성되면 new 연산자로 객체를 생성할 수 있다. 이때 인터페이스 레퍼런스로 구현 객체를 사용하려면 다음과 같이 인터페이스 변수를 선언하고 구현 객체를 대입해야한다. 인터페이스 변수는 참조 타입이기 때문에 구현 객체가 대입될 경우 구현 객체의 번지를 저장한다.

 

인터페이스 변수 = 구현객체;

 

public class Main {

   public static void main(String[] args){
       TV tv = new SamsungTV();
   }

}

 

추상 메소드 사용

 

구현 객체가 인터페이스 타입에 대입되면 인터페이스에 선언된 추상 메소드를 개발 코드에서 호출할 수 있다. 

public class Main {

   public static void main(String[] args){

       TV tv = new SamsungTV();
       tv.turnOn();
       tv.turnOff();
   }
}

🔔결과

 

디폴트 메소드 사용

 

디폴트 메소드는 인터페이스에 선언되지만, 인터페이스에서 바로 사용할 수 없다. 디폴트 메소드는 추상 메소드가 아닌 인스턴스 메소드이므로 구현 객체가 있어야 사용할 수 있다. 다음과 같은 TV 인터페이스가 있다.

 

public interface TV {

    //디폴트 메소드
    default void setMute(boolean mute){
        if(mute){
            System.out.println("무음 처리");
        }else{
            System.out.println("무음 해제");
        }
    }
}

그리고 이것을 상속하는 SamsungTV 클래스가 있다.

 

public class SamsungTV implements TV{
}

 

비록 setMute() 메소드가 SamsungTV에 선언되지는 않았지만, SamsungTV 객체가 없다면 setMute() 메소드도 호출할 수 없다.

 

public class Main {

   public static void main(String[] args){

       TV tv = new SamsungTV();
       tv.setMute(true);
   }
}

🔔결과

디폴트 메소드는 인터페이스의 모든 구현 객체가 가지고 있는 기본 메소드라고 생각하자. 물론 디폴트 메소드의 내용이 맞지 않으면, 구현 클래스를 작성할 때 디폴트 메소드를 재정의(오버라이딩)해서 자신에게 맞게 수정할 수 있다.

 

정적 메소드 사용

 

인터페이스의 정적 메소드는 인터페이스에서 바로 호출이 가능하다.

 

public class Main {

   public static void main(String[] args){
   
       TV.printMaxChannel();
   }
}

 

🔔결과

 

인터페이스 상속

 

인터페이스도 다른 인터페이스를 상속할 수 있다. 인터페이스는 클래스와 달리 다중 상속을 허용한다.

 

public interface 하위인터페이스 extends 상위인터페이스1, 상위인터페이스2{...}

 

하위 인터페이스를 구현하는 클래스는 하위 인터페이스의 메소드와 상위 인터페이스의 모든 추상 메소드에 대한 실체 메소드를 가지고 있어야 한다. 때문에 구현 클래스로부터 객체를 생성하면 다음과 같이 하위 및 상위 인터페이스 타입으로 변환이 가능하다.

 

하위인터페이스 변수 = new 구현클래스(...);
상위인터페이스1 변수 = new 구현클래스(...);
상위인터페이스2 변수  = new 구현클래스(...);

 

하위 인터페이스로 타입 변환이 되면 상,하위 인터페이스에 선언된 모든 메소드를 사용할 수 있다. 상위 인터페이스로 타입 변환되면 상위 인터페이스에 선언된 메소드만 사용 가능하다. 

 

public interface InterfaceA {
   public void methodA();
}

public interface InterfaceB {
   public void methodB();
}

public interface InterfaceC extends InterfaceA, InterfaceB {
   public void methodC()
}

public class Implementation implements InterfaceC{


   @Override
   public void methodC() {
       System.out.println("InterfaceC - methodA() 실행");
   }

   @Override
   public void methodB() {
       System.out.println("InterfaceB - methodA() 실행");
   }

   @Override
   public void methodA() {
       System.out.println("InterfaceA - methodA() 실행");
   }
}
public class Main {

   public static void main(String[] args){

       Implementation impl = new Implementation();
       InterfaceA ia = impl;
       ia.methodA();
       InterfaceB ib = impl;
       ib.methodB();

       InterfaceC ic = impl;
       ic.methodA();
       ic.methodB();
       ic.methodC();
   }
}

 

🔔결과

 

인터페이스와 다형성

 

다형성 하나의 타입에 대입되는 객체에 따라서 실행 결과가 다양한 형태로 나오는 성질을 말한다. 부모 타입에 어떤 자식 객체를 대입하느냐에 따라 실행 결과가 달라지듯이, 인터페이스 타입에 어떤 구현 객체를 대입하느냐에 따라 실행 결과가 달라진다.

 

프로그램을 개발할 때 인터페이스를 사용해서 메소드를 호출하도록 코딩을 했다면, 구현 객체를 교체하는 것은 손쉽고 빠르게 할 수 있다. 프로그램 소스 코드는 변함이 없는데, 구현 객체를 교체함으로써 프로그램의 실행 결과가 다양해진다. 이것이 인터페이스의 다형성이다.

 

예를 들어 다음과 같이 인터페이스를 이용해서 프로그램을 개발할 때, I 인터페이스를 구현한 클래스로 처음에는 A 클래스를 선택한다. 개발 완료 후 테스트를 해보니 A 클래스에 문제가 생겨 다른 클래스를 사용해야 한다. 이런 경우 I 인터페이스를 구현한 B 클래스를 만들고 단 한 줄만 수정해서 프로그램을 재실행할 수 있다.

 

public interface I {

   public void method1();
   public void method2();

}

public class A implements I {

   @Override
   public void method1() {
       System.out.println("method1 - A 로직 실행");
   }

   @Override
   public void method2() {
       System.out.println("method2 - A 로직 실행");
   }
}



public class Main {

   public static void main(String[] args){
       I i = new A();
       i.method1();
       i.method2();
   }
}

 

✂ 구현 클래스 교체(수정)

 

public class B implements I {

   @Override
   public void method1() {
       System.out.println("method1 - B 로직 실행");
   }

   @Override
   public void method2() {
       System.out.println("method2 - B 로직 실행");
   }
}


public class Main {
   public static void main(String[] args){
       I i = new B();
       i.method1();
       i.method2();
   }
}

 

 

🖍 인터페이스는 메소드의 매개 변수로 많이 등장한다. 인터페이스 타입으로 매개 변수를 선언하면 메소드 호출 시 매개값으로 여러 가지 종류의 구현 객체를 줄 수 있기 때문에 메소드 실행 결과가 다양하게 나온다.

 

public class Main {

   public static void main(String[] args){
   
       I i = new A();
       useMethods(i);
       i = new B();
       useMethods(i);
   }

   public static void useMethods(I i){
       i.method1();
       i.method2();
   }
}

 

🔔결과

 

강한 결합과 느슨한 결합

 

인터페이스를 이용해서 강한 결합을 느슨한 결합으로 변경할 수 있다. 

🌈위 그림의 강한 결합부터 살펴보자. 클래스 A는 클래스 B에 의존하고 있다.(클래스 B의 메소드를 사용한다)

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());
    }
}

 

이때, 클래스 A가 클래스 C를 사용하도록 수정하자. 아래 코드 처럼 클래스 B에 의존하고 있는 코드를 클래스 C에 의존하게 변경해야 한다. 이것은 강한 결합이라 한다.

 

class A {
    public void methodC(C c) { 
        c.methodB();
    }
}

class C {
    public void methodB() {
        System.out.println("methodB()");
    }
}

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

 

🌈이제 느슨한 결합을 알아보자. 클래스 A는 인터페이스 I에 의존하고 있고, 인터페이스 I를 구현한 클래스 B를 사용한다.

이때, 클래스 A가 클래스 C를 사용하도록 수정한다고 가정하자. A는 I에 의존하고 있기 때문에 인터페이스 I를 구현한 클래스 C를 사용한다면, A의 코드를 따로 변경할 필요가 없다. 이것을 느슨한 결합이라한다.

class A {
    public void methodA(I i) {
        i.methodB();
    }
}

interface I {
    public abstract void methodB();
}

class B implements I {
    public void methodB() {
        System.out.println("methodB()");
    }
}

class C implements I {
    public void methodB() {
        System.out.println("methodB() in C");
    }
}

class InterfaceTest {
    public static void main(String args[]) {
        I a = new A();
        a.methodB(new B());
    }
 }

 

 

 

출처

모던 자바 인 액션

이것이 자바다

https://devahea.github.io/2019/04/23/Java9/

codechacha.com/ko/java8-functional-interface/

www.notion.so/4b0cf3f6ff7549adb2951e27519fc0e6

반응형

댓글