인터페이스 (Interface)

김헌규·2025년 6월 23일
post-thumbnail


🧐 인터페이스란?

인터페이스란, 일종의 추상적인 설계도로, 추상 클래스와 유사하지만 더 엄격하고 높은 수준의 추상화를 제공한다. 추상 클래스처럼 추상 메서드를 포함할 수 있지만, 일반 메서드(단, default, static, private 메서드는 가능)멤버 변수(상수 제외)는 포함할 수 없다.

좀 더 쉽게 설명하자면 우리는 실생활에서 인터페이스를 사용한 적이 있을 것이다. 대표적으로 규격화 되어 있는 리모컨이 있다. 리모컨은 TV 제조사가 다르더라도 리모컨이 동일한 버튼과 기능을 제공한다면, 그 리모컨은 여러 브랜드의 TV를 똑같은 방식으로 조작할 수 있다.

이처럼 인터페이스도 추상화를 통해 구현해야할 부분을 규격화하고 구현은 각 클래스가 담당하는 원리로 이루어져 있다.



📘 인터페이스 선언

인터페이스의 선언은 클래스의 선언 자리에 class 대신에 interface 키워드를 사용하면 된다.

public interface Button {
    ...
}

인터페이스 이름은 관례적으로 클래스 이름 작성 방법과 동일하다. 영어 대소문자로 구분하며, 첫 글자를 대문자로 하고 나머지는 소문자로 작성하는 것이 관례이다. 이름이 길어 여러 단어로 구성될 경우, 각 단어의 첫 글자를 대문자로 작성하는 파스칼 표기법(PascalCase)을 사용한다.

클래스는 멤버 변수와 메소드를 가지고 있는 반면에 인터페이스는 상수 필드와 추상 메소드를 구성 멤버로 가진다. 또한 인터페이스는 객체를 생성할 수 없기 때문에 생성자를 가지지 않는다.

// 버튼 인터페이스
public interface Button {
	
    // 상수 선언
    public static final int DEFAULT_WIDTH = 100;
    public static final int DEFAULT_HEIGHT = 50;
	
    // 추상 메소드 선언
    public abstract void onClick();
}

인터페이스는 객체의 사용 방법(기능)을 정의하는 역할을 하므로 인스턴스 필드나 정적 필드를 선언할 수 없다. 하지만 상수 필드는 선언이 가능하다. 단, 상수는 고정된 값이기에 실행 시 데이터를 바꿀 수 없다.

선언 방법은 일반적인 상수 선언 방법과 같다. public static final을 변수 앞에 붙여서 선언할 수 있다. 그리고 인터페이스에서는 이를 생략하더라도 컴파일 과정에서 자동으로 붙여준다.

추상메소드의 경우는 일반적인 추상 클래스에서와 똑같이 선언하면된다. 즉, public abstract 키워드를 메소드에 붙여주면 되는데 상수에서의 경우와 마찬가지로 이를 생략하여도 컴파일 과정에서 자동으로 붙게 된다.



👨‍💻 인터페이스 구현

처음에 인터페이스를 간단하게 설명하면서 인터페이스로 규격화된 기능을 설정하고 이를 클래스를 통해서 인터페이스를 구현해야한다고 하였다. 그렇다면 어떻게 구현할 수 있을까?

우선 간단한 예시를 보자.

// 버튼 인터페이스
public interface Button {
	
    // 상수 선언
    public static final int DEFAULT_WIDTH = 100;
    public static final int DEFAULT_HEIGHT = 50;
	
    // 추상 메소드 선언
    public abstract void onClick();
}

// 로그인 버튼 - 구현 클래스
public class LoginButton implements Button {

    @Override
    public void onClick() {
        System.out.println("로그인 화면으로 이동합니다.");
    }
}

// 종료 버튼 - 구현 클래스
public class ExitButton implements Button {

    @Override
    public void onClick() {
        System.out.println("프로그램을 종료합니다.");
    }
}

위의 코드처럼 일반적인 클래스에 인터페이스를 구현할 때는 implements라는 키워드를 클래스 이름 뒤에 선언한 후 implements 뒤에 구현할 인터페이스를 명시해주면 된다. 그리고 클래스 안에 구현부를 작성해주면 끝이다. 또한 구현 클래스라고 해서 다른 것이 아닌 일반 클래스처럼 내부에서 멤버 변수 및 멤버 메소드를 작성할 수 있다.

그리고 인터페이스에 정의된 추상 메소드는 반드시 오버라이딩(재정의)하여 구현해야 한다.
이는 추상 클래스에서 추상 메소드를 구현할 때와 동일한 방식이다.

이로써 각 클래스는 인터페이스가 정의한 기능의 규약을 따르게 된다.


🎛️ 다중 인터페이스 구현 클래스

또한 자바에서는 클래스는 하나만 상속할 수 있지만 인터페이스는 여러 개를 동시에 구현할 수 있다.


public interface Button {
    public abstract void onClick();
    
    // 공통된 메소드
    public abstract void delete();
}

public interface Toggleable {
    public abstract void toggle();
    
    // 공통된 메소드
    public abstract void delete();
}

// 다중 인터페이스 구현 클래스
public class ToggleButton implements Button, Toggleable {

    boolean isToggle = false;

    @Override
    public void onClick() {
        isToggle = !isToggle;
        toggle();
    }

    @Override
    public void toggle() {
        if (isToggle) {
            System.out.println("토글에 의해 화면이 띄워짐");
        } else {
            System.out.println("토글에 의해 화면이 없어짐");
        }
    }
    
    @Override
    public void delete() {
    	System.out.println("삭제!");
    }
}


public class Example {
    public static void main(String[] args) {
        ToggleButton toggleButton = new ToggleButton();

        toggleButton.onClick(); // 토글에 의해 화면이 띄워짐
        toggleButton.onClick(); // 토글에 의해 화면이 없어짐
        toggleButton.toggle(); // 토글에 의해 화면이 없어짐

        Button button = toggleButton;

        button.onClick(); // 토글에 의해 화면이 띄워짐
        button.onClick(); // 토글에 의해 화면이 없어짐

        Toggleable toggleable = toggleButton;
        toggleable.toggle(); // 토글에 의해 화면이 없어짐
        
        toggleButton.delete(); // 삭제!
    }
}

위의 코드의 ToggleButton 클래스처럼 implements 키워드 뒤에 기능을 구현하고자 하는 인터페이스들을 작성해주면 다중 인터페이스를 구현할 수 있게 된다. 여러 개의 인터페이스를 구현하는 경우, 각 인터페이스에 선언된 모든 추상 메서드를 구현해야 한다.

실행 코드를 보면 먼저 일반 클래스인 ToggleButton 타입변수로 받은 다음 실행해보았다. ToggleButton 클래스에는 onClick()toggle() 메소드가 둘 다 구현이 되어 있어 실행이 가능하다.

반면 다음 코드에서는 Button 또는 Toggleable 인터페이스 타입으로 객체를 참조할 경우, 참조한 타입에 선언된 메서드만 호출할 수 있다.

이는 다형성(polymorphism)의 개념을 기반으로 하며, 구현 객체는 모든 기능을 가지고 있어도 참조 타입에 따라 접근 가능한 범위가 달라진다.

하지만 코드를 작성하면서 onClick()을 실행하면서 toggle()을 내부에서 간접적으로 실행해 놓았기 때문에 toggle()로 실행하여 결과가 달라지지 않는 것과는 달리 Button 타입으로 onClick()을 실행하면 토글 기능이 잘 작동한다는 것을 알 수 있다.

즉, 이 코드에서는 다중 인터페이스를 통해 버튼에 토글 기능을 추가하여 구현한 것이기에 여러 기능을 독립적으로 분리하고 조합할 수 있다는 장점이 있다.

이러한 구조는 SRP(단일 책임 원칙)을 지키면서도, 유연하고 확장 가능한 설계를 가능하게 해준다.


또한 인터페이스는 다중 상속이 되기 때문에 같은 시그니처 형태의 메소드를 여러 인터페이스에서 공통으로 상속해주고 이를 구현하는 것도 가능하다.

코드에서 보면 Button 인터페이스와 Toggleable 인터페이스에 같은 시그니처 형태로 delete() 메소드를 정의하였고 이를 ToggleButton 구현 클래스에 재정의를 한 후 실행했더니 잘 작동되었다.

이는 Java에서 클래스의 경우 여러 상위 클래스에서 같은 시그니처 형태의 메소드를 상속 받아 실행하려고 할 때 어떤 상위 클래스의 메소드를 사용할지 모호성이 발생한다. 이를 다이아몬드 문제라고 한다.

하지만 인터페이스의 경우 메소드는 정의만 하고 실제 구현은 구현 클래스에서 담당하기 때문에 메소드의 형태가 겹쳐도 상관이 없다.

이처럼 자바에서는 클래스는 하나만 상속받을 수 있지만 인터페이스는 여러 개를 동시에 구현할 수 있다. 이를 통해 기능을 모듈화하고 재사용성 높은 코드 구조를 만들 수 있으며 중복된 시그니처의 메서드도 충돌 없이 구현할 수 있다.



🚀 인터페이스 사용

그렇다면 이렇게 인터페이스를 정의하고 구현 클래스를 구현했다면 실행코드에서는 어떤식으로 실행 되는 것일까?

예제로 확인해보자.

// 인터페이스
public interface Button {

    public static final int DEFAULT_WIDTH = 100;
    public static final int DEFAULT_HEIGHT = 50;

    public abstract void onClick();
}

// 로그인 버튼 - 구현 클래스
public class LoginButton implements Button {

    private int loginAttemptCount = 0;

    @Override
    public void onClick() {
        memberMethod1(); // 간접 사용
        this.loginAttemptCount++;
        System.out.println("로그인 화면으로 이동합니다. (시도 횟수: " + loginAttemptCount + ")");
    }

    public void memberMethod1() {
        System.out.println("멤버 메소드 간접적으로 사용");
    }

}

// 종료 버튼 - 구현 클래스
public class ExitButton implements Button {

    private boolean isRealExit = false;

    @Override
    public void onClick() {
    
        if (!isRealExit) {
            isRealExit = true;
            System.out.println("정말 종료하시겠습니까? 한 번 더 눌러주세요.");
        } else {
            isRealExit = false;
            System.out.println("프로그램을 종료합니다.");
        }
        
    }
}

// 실행 코드
public class Example {
    public static void main(String[] args) {
        Button loginButton = new LoginButton();
        Button exitButton = new ExitButton();

        // 로그인 버튼 클릭 2번
        loginButton.onClick(); // 시도 횟수 : 1
        loginButton.onClick(); // 시도 횟수 : 2


        // 종료 버튼 클릭 2번
        exitButton.onClick(); // 정말 종료하시겠습니까? 한 번 더 눌러주세요.
        exitButton.onClick(); // 프로그램을 종료합니다.

        // 상수 출력
        System.out.println(Button.DEFAULT_WIDTH); // 100
        System.out.println(Button.DEFAULT_HEIGHT); // 50
    }
}
실행 결과

멤버 메소드 간접적으로 사용
로그인 화면으로 이동합니다. (시도 횟수: 1)
멤버 메소드 간접적으로 사용
로그인 화면으로 이동합니다. (시도 횟수: 2)
정말 종료하시겠습니까? 한 번 더 눌러주세요.
프로그램을 종료합니다.
100
50

사용방법으로 우선 인터페이스와 구현 클래스를 작성한 후, 실행 코드에서는 인터페이스 타입의 변수에 구현 클래스의 인스턴스를 할당하여 사용할 수 있다. 이렇게 하면 인터페이스 타입으로 구현 클래스의 객체를 다룰 수 있다. 그리고 이렇게 객체를 할당받은 인터페이스 변수를 통해서 메소드를 실행할 수 있다.

또한 앞서 인터페이스에 정의된 상수는 public static final로 선언된다고 하였다. 즉, 클래스의 공용 상수처럼 작동하기 때문에 상수에 접근하기 위해서는 객체가 아닌 인터페이스 이름으로 접근해야 한다. 코드의 Button.DEFAULT_WIDTH처럼 사용하는 것이 대표적인 예다. 반면 클래스의 static 변수나 상수처럼 인스턴스 상수를 인스턴스를 통해 접근하려 하면 경고가 발생할 수 있고 Java에서는 권장하지 않는 방식이다.

다만, 여기서 주의할 점이 있다. 예제를 작성하면서 구현 클래스에 멤버 변수와 멤버 메소드를 직접적으로 사용할 수 있는지 궁금해서 작성해 보았지만 사용할 수 없었다. 그 이유는 인터페이스에서는 정의되지 않은 구현 클래스의 멤버(변수, 메서드)에 접근하려고 했기 때문이다.

그래서 코드를 보면 LoginButton에서 멤버 메소드를 사용하기 위해서 인터페이스의 추상 메소드를 구현한 메소드에서 간접적으로 호출하여 사용하였다. 또 다른 방법으로는 인터페이스 타입을 구현 클래스 타입으로 캐스팅하는 방법을 사용할 수 있지만, 구현 클래스에 의존하게 되므로 다형성을 제대로 활용하지 못하게 된다.




참조
https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4Interface%EC%9D%98-%EC%A0%95%EC%84%9D-%ED%83%84%ED%83%84%ED%95%98%EA%B2%8C-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC
https://dkswnkk.tistory.com/459
혼자 공부하는 자바 - 신용권(한빛 미디어)

profile
꾸준하게 가자

0개의 댓글