[Java] 인터페이스 (interface)

Jeini·2022년 10월 29일
0

☕️  Java

목록 보기
18/59
post-thumbnail

💡 인터페이스


인터페이스는 abstract, final와 함께 대표적인 규제이다.
인터페이스의 역할은 이렇다. 어떤 클래스가 있고 그 클래스가 특정한 인터페이스를 사용한다면 그 객체는 반드시 인터페이스의 메서드들을 구현해야 한다.

만약 인터페이스에서 강제하고 있는 메소드를 구현하지 않으면 컴파일 조차 되지 않는다.

예시를 보자.

interface I {
    public void z(); // 중괄호 없음 비어있는 메서드 클래스에 구현함.
}
 
class A implements I { // class A 구현
    public void z() { // 이 부분이 없으면 Error
    }
}

클래스 A는 인터페이스 I를 '구현' 한다.

클래스 A 뒤의 implements I는 이 클래스가 인터페이스 I를 구현하고 있다는 의미다.
그것은 interface I의 맴버인 public void z() 메서드를 클래스 A가 반드시 포함하고 있어야 한다는 뜻이다. 따라서 위의 코드는 문제가 없다.

인터페이스와 상속은 다르다.

✔️ 인터페이스: 하위 클래스에 특정한 메서드가 반드시 존재하도록 강제(implements)
✔️ 상속: 상위 클래스의 기능을 하위 클래스가 물려 받는 것
(extends)

인터페이스의 이러한 규제가 왜 필요한지는 뒤에서 알아보자.

인터페이스는 사용하는 키워드도 다르다.
클래스를 선언 할 때는 class를 사용하지만 인터페이스는 interface를 사용한다.

💡 실질적인 쓰임


계산기 예제에 인터페이스를 도입해보자.

계산기 기능이 필요한 프로젝트를 진행하는데 시간이 촉박하다.
그래서 계산기 클래스는 개발자 A가 만들고, 개발자 B는 그 클래스를 사용하는 로직을 만들다고 해보자. 이런 경우 개발자 B는 개발자 A가 계산기를 잘 만들어서 나중에 제출할 것이라고 기대하고 개발을 진행할 것이다. 그리고 아래와 같이 가짜 로직을 만들어서 코드를 작성했다.

// 개발자 B의 사용 코드

class CalculatorDummy {
	// 일단 가짜 클래스 만듬
    public void setOprands(int first, int second, int third) {
    }
    
    public int sum() {
        return 60;
    }
    
    public int avg() {
        return 20;
    }
}
public class CalculatorConsumer {
    public static void main(String[] args) {
        
        CalculatorDummy c = new CalculatorDummy();
        c.setOprands(10, 20, 30);
        System.out.println(c.sum() + c.avg());
    }
}

개발자 A가 Calculator를 만드는데 3개월이 필요하다고 한다면 그 시간을 단축하기 위해서 위와 같은 코드를 작성하는 이유가 공감 할 수 있을 것이다. 3개월이 지나고 개발자 A가 Calculator 클래스를 완성해서 인계해줬다. 아래는 그 코드다.

// 개발자 A의 계산기 구현 코드

class Calculator {
    int left; 
    int right;
    
    public void setOprands(int left, int right) {
        this.left = left;
        this.right = right;
    }
    
    public void sum() {
        System.out.println(this.left + this.right);
    }
    
    public void avg() {
        System.out.println((this.left + this.right) / 2);
    }
}

아뿔싸. 개발자 A는 setOprands의 매개변수를 2개 받고 있지만 개발자 B는 이 메소드가 변수 3개를 받을 것이라고 생각한 것이다.

이건 마치 해저터널의 공사가 중간에서 만나지 않은 것과 같은 상황이다.
이때부터 신경전이 시작된다. 경우에 따라서는 치열하게 다투게 되고 프로젝트는 파국으로 치닫는다.

이러한 문제를 해결하기 위한 가장 좋은 방법은 무엇일까?
협업자 상호간에 구체적인 약속을 하면 된다. 특히 그 약속을 코드 안에서 할 수 있다면 참 좋을 것이다. 그렇다. 인터페이스가 필요한 순간이다.

클래스 Calculator를 사용할 개발자가 이 클래스가 가지고 있어야 할 메서드를 인터페이스로 만들어서 제공하는 것이다. 반대의 경우도 가능하다. 만드는 쪽에서 인터페이스를 제공하면 된다. 양쪽의 개발자는 이 인터페이스를 구현한 클래스 Calculator와 CalculatorDummy를 각각 구현하면 된다.

이렇게 해서 만들어진 코드를 보자. 아래는 약속을 정의하고 있는 인터페이스이다.

✏️ 인터페이스를 활용한 예제

// 3개의 매개변수를 받고 정수형 sum과 avg를 받도록 약속
public interface Calculatable {
    
    public void setOprands(int first, int second, int third) ;
    public int sum(); 
    public int avg();
}

다음은 인터페이스를 구현한 가짜 클래스를 임시로 사용해서 만든 애플리케이션이다.

class CalculatorDummy implements Calculatable{
    
    public void setOprands(int first, int second, int third) {
    }
    
    public int sum() {
        return 60;
    }
    
    public int avg() {
        return 20;
    }
}
public class CalculatorConsumer {
    public static void main(String[] args) {
        
        CalculatorDummy c = new CalculatorDummy();
        c.setOprands(10, 20, 30);
        System.out.println(c.sum()+c.avg());
    }
}

다음 코드는 인터페이스에 따라서 구현된 클래스이다.

class Calculator implements Calculatable {
    int first;
    int second;
    int third;
    
    public void setOprands(int first, int second, int third) {
        this.first = first;
        this.second = second;
        this.third = third;
    }
    
    public int sum() {
        return this.first + this.second + this.third;
    }
    
    public int avg() {
        return (this.first + this.second + this.third) / 3;
    }
}

이제 해야 할 일은 가짜 클래스인 CalculatorDummy를 실제 로직으로 교체하면 된다.

public class CalculatorConsumer {
    public static void main(String[] args) {
       
       Calculator c = new Calculator();
        c.setOprands(10, 20, 30);
        System.out.println(c.sum()+c.avg());
    }
}

이렇게해서 인터페이스를 이용한 협업에 대해서 알아봤다.

인터페이스를 이용해서 서로가 동일한 메서드를 만들도록 규약을 만들어서 공유한 결과 각자가 상대의 일정이나 구현하는 방식에 덜 영향을 받으면서 애플리케이션을 구축 할 수 있었다.

인터페이스는 어떠한 클래스가 어떠한 메서드를 가지고 있는가, 또한 어떤 멤버변수를 가지고 있느냐에 대한 명세서 같은 느낌이다.
그러면 저 계산기를 구현하고 있는 클래스들은 그 명세서에 적혀있는 대로 명세서에 정의되어 있는 대로 클래스의 멤버를 구체적으로 구현해야지만 컴파일이 되게 되는 것이다.

이렇게 함으로써, 서로간의 커뮤니케이션 미스를 방지할 수 있고 빠른 시간 내에 코드를 완성할 수 있다.

💡 인터페이스의 규칙


✔️ 하나의 클래스가 여러개의 인터페이스를 구현 할 수 있다.

클래스 A는 메소드 x나 z 중 하나라도 구현하지 않으면 오류가 발생한다.

interface I1 {
    public void x();
}
 
interface I2 {
    public void z();
}
 
class A implements I1, I2 {
    public void x() {}
    public void z() {}   
}

✔️ 인터페이스도 상속이 된다.

interface I3 {
    public void x();
}
 
interface I4 extends I3 {
    public void z();
}
 
class B implements I4 {
    public void x() {}
    public void z() {}   
}

✔️ 인터페이스의 맴버는 반드시 public이다.

interface I5 {
    private void x(); // Error
}

위 코드는 오류를 발생한다.
인터페이스는 그 인터페이스를 구현한 클래스를 어떻게 조작할 것인가를 규정한다.

그렇기 때문에 외부에서 제어 할 수 있는 가장 개방적인 접근 제어자인 public만을 허용한다.

public을 생략하면 접근 제어자 default가 되는 것이 아니라 public이 된다. 왜냐하면 인터페이스의 맴버는 무조건 public이기 때문이다.

💡 interface vs abstract


인터페이스와 추상 클래스는 서로 비슷한 듯 다른 기능이다.

✔️ 인터페이스
: 클래스가 아닌 인터페이스라는 고유한 형태를 지님
: 구체적인 로직이나 상태를 가지고 있을 수 없음
(아예 그냥 추상 메서드로!)
: 멤버 변수를 두지 않고 메서드로만 남기고 싶을 때 많이 사용
: 다중 상속의 효과를 가질 수 있음

✔️ 추상 클래스
: 일반적인 클래스
: 구체적인 로직이나 상태를 가짐
(일부는 구현된 메서드를 가질 수 있고 일부는 추상 메서드를 가질 수 있음.)

💡 default 메서드와 static 메서드


interface Myinterface {
	void method();
    default void newMethod() {}
}
  • 인터페이스에 디폴트 메서드, static메서드 추가 가능. (JDK1.8부터)
  • 인터페이스에 새로운 메서드(추상 메서드)를 추가하기 어려움.

    해결책 ➡️ 디폴트 메서드(default method)

  • 디폴트 메서드는 인스턴스 메서드(인터페이스 원칙 위반)

❗️ 디폴트 메서드가 기존의 메서드와 충돌할 때의 해결책

✔️ 여러 인터페이스의 디폴트 메서드 간의 충돌

  • 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩해야 한다.

✔️ 디폴트 메서드와 조상 클래스의 메서드 간의 충돌

  • 조상 클래스의 메서드가 상속되고 (우선권), 디폴트 메서드는 무시된다.

📌 마지막 정리


  • 추상 클래스중에, 기본적으로 추상메서드만 가지고 있는 것을 인터페이스로서 특별 취급한다.
  • 인터페이스에 선언된 메서드는 자동적으로 public abstract가 되고, 필드는 public static final이 된다.
  • 복수의 인터페이스를 부모로 두는 다중상속이 가능하다.
  • 인터페이스를 부모로 두는 자식 클래스 정의에 implements를 사용한다.

출처: https://opentutorials.org/module/516/6063

profile
Fill in my own colorful colors🎨

0개의 댓글