인터페이스의 메소드

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


인터페이스를 사용하면서도 많은 문제가 있다. 그런 것들을 해결해주기 위해 Java8 이후에는 default, static, private 메소드가 나오게 되었다.

만약 인터페이스를 모르고 있다면 필자가 작성한 인터페이스 글을 보고 오는 것을 추천한다.


🧩 default 메소드

default 메소드는 어떻게 나오게 되었을까? 만약 A라는 인터페이스를 구현하는 클래스가 A1, A2, A3 총 3개가 있다고 가정해보자. 그런데 기능 추가를 하고 싶어 인터페이스에 methodA 라는 것을 추가해야한다. 추가하는 방법은 구현 클래스 A1, A2, A3에 오버라이드를 통해 methodA를 추가하면 된다.

하지만 A 인터페이스를 구현하는 클래스가 약 100개쯤 된다면, methodA를 추가하기 위해서 모든 구현 클래스에 직접 구현하는 것은 너무 비효율적일 것이다. 이런 문제를 해결하기 위해 Java 8에서 도입된 것이 바로 default 메소드이다.

이를 코드로 구현한다면 아래와 같다.

public interface A {

    public abstract void method1();
    
    // 추가한 메소드
    default void method2() {
        System.out.println("추가한 기능");
    }
}

public class A1 implements A {

    @Override
    public void method1() {
        System.out.println("메소드1 호출");
    }
}

public class A2 implements A {

    @Override
    public void method1() {
        System.out.println("메소드1 호출");
    }
}

public class Example {

    public static void main(String[] args) {
        A a1 = new A1();
        A a2 = new A2();

        a1.method1(); // 메소드1 호출
        a2.method1(); // 메소드1 호출

        a1.method2(); // 추가한 기능
        a2.method2(); // 추가한 기능
    }
}

위의 코드를 보면 인터페이스 A가 있고 이를 구현하는 클래스인 A1A2가 있다. 인터페이스에는 추상 메소드인 method1()이 있고 method2()라는 default 메소드가 있다. 그리고 구현 클래스에서 인터페이스의 method1()을 구현할 때에는 A1A2에 오버라이드를 통해 직접 구현을 해주었다.

하지만 method2()를 추가할 때에는 default 메소드를 통해 굳이 직접 오버라이드를 통해 구현 클래스에 구현해주지 않아도 알아서 추가 되는 것을 위의 코드를 통해서 확인할 수 있다.

default 메소드의 선언 방법은 추상 메소드의 public abstract 자리에 default 키워드를 사용하고 구현부를 작성해주면 된다.


⚔️ default 메소드의 다중 상속 문제

반면 default 메소드에서는 다중 상속 문제가 있는데 한 번 알아보자.


1. 다중 인터페이스들 간의 default 메소드 충돌

첫번째로 다중 인터페이스 간의 default 메소드 충돌이다. 이것은 하나의 구현 클래스에서 여러 개의 인터페이스를 구현할 수 있어서 발생하는 문제이다. 만약 인터페이스 AB에서 같은 형태의 default 메소드를 구현을 하게 된다면 AB의 default 메소드 중 어느 것을 실행해야할지 모호해진다. 그래서 이를 조치해주지 않으면 아래의 코드에서처럼 컴파일 자체가 되지 않는다.

public interface A {

    public abstract void method1();
    
    // 추가한 메소드
    default void method2() {
        System.out.println("A의 디폴트 메소드");
    }
}

public interface B {
	// 추가한 메소드
    default void method2() {
        System.out.println("B의 디폴트 메소드");
    }
}

public class A1 implements A, B {

    @Override
    public void method1() {
        System.out.println("A1 : 메소드1 호출");
    }
}

public class Example {

    public static void main(String[] args) {
        A a = new A1();
        a.method2(); // java: types interfaceMethod.A and interfaceMethod.B are incompatible
    }
}

그렇다면 이 문제에 대한 해결 방법은 무엇일까?

그것은 오버라이딩을 하면 된다. 인터페이스를 구현한 클래스에서 디폴트 메소드를 커스터마이징 하거나 super를 통해 직접적으로 메소드를 호출할 인터페이스를 명시하는 방법이 있다. 인터페이스의 super는 뒤에서 설명하도록 하겠다.

public class A1 implements A, B  {

    @Override
    public void method1() {
        System.out.println("A1 : 메소드1 호출");
    }

    @Override
    public void method2() {
    	// 커스터마이징
        System.out.println("커스터마이징");
        
       // super를 통해 원하는 인터페이스의 default 메서드를 명시적으로 호출
       A.super.method2();
    }
}

2. 인터페이스의 디폴트 메소드와 부모 클래스 메소드 간의 충돌

두번째로는 인터페이스를 구현하면서 추상 클래스를 상속받는 경우에 메소드 간의 충돌이 발생하는 문제이다.

우선 코드를 한 번 보자.

public interface A {

    public abstract void method1();
    
    // 추가한 메소드
    default void method2() {
        System.out.println("A의 디폴트 메소드");
    }
}

public abstract class C {
	
    public void method2() {
        System.out.println("C의 인스턴스 메소드");
    }
}

public class A1 extends C implements A  {

    @Override
    public void method1() {
        System.out.println("A1 : 메소드1 호출");
    }
}

public class Example {

    public static void main(String[] args) {
        A a = new A1();
        C c = new A1();
        A1 a1 = new A1();
        
        a.method2(); // C의 인스턴스 메소드
        c.method2(); // C의 인스턴스 메소드
        a1.method2(); // C의 인스턴스 메소드
    }
}

코드에서처럼 A1 클래스에서 A 인터페이스를 구현하고 C 추상 클래스를 상속 받았을 때 AC로부터 같은 이름의 메소드를 상속 받을때, 인터페이스 타입, 추상 클래스 타입, 구현 클래스 타입으로 중복되는 메소드를 실행한다면 추상 클래스의 메소드가 실행이 된다는 것을 알 수 있다.

만약 인터페이스 쪽의 디폴트 메소드를 사용하고 싶다면 구현 클래스에서 오버라이딩 하여 사용하면 된다.

public class A1 extends C implements A  {

    @Override
    public void method1() {
        System.out.println("A1 : 메소드1 호출");
    }

    @Override
    public void method2() {
        System.out.println("A1 인터페이스의 디폴트 메소드");
    }
}

따라서 인터페이스 간 default 메서드 충돌은 반드시 구현 클래스에서 오버라이딩을 통해 명시적으로 해결해야 하며 필요 시 super를 활용해 원하는 인터페이스의 동작을 선택할 수 있다.


🧭 default 메소드의 super

앞서 여러 인터페이스에서 같은 default 메소드가 있을 때 메소드 충돌에 대한 해결책으로 super가 있다고 하였다. super는 원래 클래스 상속에서 부모 클래스의 멤버를 참조할 때 사용하는 키워드인데, 인터페이스의 default 메소드에서도 유사한 방식으로 활용할 수 있다.

이와 같이 인터페이스에서도 default 메소드를 구현한 클래스에서 오버라이딩 했을때, super 키워드를 통해 특정 인터페이스의 디폴트 메소드 호출이 가능해진다.

다만 클래스의 super 호출과는 약간의 차이가 있다. 인터페이스명.super.디폴트메소드의 방식으로 호출하면 된다.

즉, 클래스가 여러 인터페이스를 구현하는 경우에는 super 앞에 인터페이스명을 반드시 명시해서 어떤 default 메소드를 호출할지 컴파일러가 판단할 수 있어서 default 메소드 충돌을 방지할 수 있다는 것이다.

public interface A {

    public abstract void method1();
    
    // 추가한 메소드
    default void method2() {
        System.out.println("A의 디폴트 메소드");
    }
}

public interface B {
	// 추가한 메소드
    default void method2() {
        System.out.println("B의 디폴트 메소드");
    }
}

public class A1 implements A, B  {

    @Override
    public void method1() {
        System.out.println("A1 : 메소드1 호출");
    }

    @Override
    public void method2() {
        A.super.method2();
        B.super.method2();
    }
}

public class Example {

    public static void main(String[] args) {
        A a = new A1();
        a.method2();
    }
}
실행 결과

A의 디폴트 메소드
B의 디폴트 메소드

이와 같은 코드를 작성하면서 만약 하나의 인터페이스만 구현하는 클래스에서 인터페이스의 default 메소드를 호출할 때 super.디폴트메소드로 호출이 가능한지 테스트 해보았다.

그런데 이와 같은 방식으로는 작동이 안되었고 IDE에서도 앞에 인터페이스명을 추가하라고 문구가 나오는 것을 확인했다. 그래서 아무리 구현하는 인터페이스가 하나라고 해도 맨앞에 인터페이스명을 붙여줘야 한다.



⚙️ static 메소드

Java8부터는 인터페이스에서 static 메소드를 선언할 수 있게 되었다. 이전에는 static 메소드를 인터페이스에 허용하지 않았는데, 이는 인터페이스의 역할을 모호하게 만들 수 있기 때문이었다.

인터페이스는 객체가 가져야 할 행위(기능)를 정의하는 설계도와 같은 역할을 한다. 하지만 static 메소드는 인스턴스 없이 호출되며, 이미 완전한 구현체를 가진 정적 메소드이기 때문에 이러한 메소드가 인터페이스에 포함되면 인터페이스의 단일 책임 원칙에 어긋날 수 있다는 우려가 있었다.

하지만 그럼에도 Java8에서는 static 메소드가 도입이 되었다. static 메소드는 default 메소드와 마찬가지로, 기존 인터페이스에 새로운 메소드를 추가할 때 모든 구현 클래스에서 이를 구현하지 않아도 되도록 해준다. 또한 static 메소드는 구현 클래스와 무관하게, 인터페이스 자체에 대한 공통 동작을 정의할 수 있다.

사용 방법으로는 인터페이스 명으로 접근해 사용할 수 있다.

public interface A {

    public abstract void method1();
    
    // 추가한 메소드
    default void method2() {
        System.out.println("A의 디폴트 메소드");
    }

    // static 메소드
    public static void method3() {
        System.out.println("A의 스태틱 메소드");
    }
}

public class A1 implements A, B  {

    @Override
    public void method1() {
        System.out.println("A1 : 메소드1 호출");
    }

    @Override
    public void method2() {
        A.super.method2();
    }
}

public class Example {

    public static void main(String[] args) {
        A a = new A1();
        a.method1();
        a.method2();
        // 스태틱 메소드
        A.method3(); 
    }
}
실행 결과

A1 : 메소드1 호출
A의 디폴트 메소드
A의 스태틱 메소드

위 코드에서 method1()method2()는 인터페이스 타입의 변수로 메소드를 호출했지만, static 메소드인 method3()은 인터페이스명으로 직접 호출한 것을 볼 수 있다.


이러한 static 메소드는 유틸리티 메소드 제공 측면에서 인터페이스와 관련된 공통 작업 수행에 활용할 수 있다. 예를 들어, Java 코드를 작성하면서 수학 관련 기능을 활용하고자 할 때 Math 클래스와 관련된 유틸리티 메소드를 사용한 적이 있을 것이다. 유틸리티 메소드란, 객체의 상태나 동작과 직접적으로 관련되진 않지만 자주 사용하는 보조적인 기능을 수행하는 메소드를 말한다.

물론 이 예시는 Math 클래스를 기반으로 하고 있지만, static 메소드라는 점에서는 인터페이스에서도 동일하게 적용될 수 있으므로 적절한 예시가 될 수 있다.

Math의 내부 코드를 한 번 보자.

public final class Math {

    private Math() {}

    public static final double E = 2.718281828459045;
    public static final double PI = 3.141592653589793;

   
    @IntrinsicCandidate
    public static int max(int a, int b) {
        return (a >= b) ? a : b;
    }
    
    @IntrinsicCandidate
    public static int min(int a, int b) {
        return (a <= b) ? a : b;
    }
}

내부에서는 이런식으로 여러 개의 static 메소드가 있어서 우리가 수학 관련 메소드를 사용할 때 Math.max(), Math.min()과 같이 유틸리티 메소드를 사용할 수 있는 것이다.

이처럼 Math 클래스는 다양한 static 메소드를 통해 수학적 기능을 제공하며, 인터페이스의 static 메소드도 유사한 방식으로 사용될 수 있다.



🔒 private 메소드

private 메소드는 Java9 버전에 추가된 메소드이다. 인터페이스에 default, static 메소드가 생긴 후 이러한 메소드들의 로직을 공통화하고 재사용하기 위해 생긴 메소드이다. 이러한 private 메소드도 구현부를 가져야 한다.

로직을 공통화하고 재사용 한다는 것은 예를 들어, 기존에는 default 메소드 내에 동일한 로직을 여러번 작성을 해야했다면 이제는 private 메소드를 통해 재사용성을 높일 수 있다는 것이다.

접근제어자를 알고 있다면 private 메소드에 대해 이해하기 쉬울 것이다. 접근제어자로 private을 클래스의 멤버에 활용한 적이 있다면 외부에서 접근을 할 수 없고 내부에서만 접근할 수 있는 접근제어자라는 것을 알고 있을 것이다.

이는 인터페이스에서도 같다. 인터페이스의 외부에서 private 메소드를 호출할 수 없기 때문에 private 메소드를 외부에서 호출하고자 한다면 default 메소드의 내부에서 간접적으로 호출하여 외부에서 default 메소드를 호출하면 된다.

선언 방법으로는 메소드를 선언할 때 제일 처음으로 private를 선언하고 메소드 내부를 구현하면 된다.

또한 private static 키워드를 붙인 메소드는 static 메소드에서만 호출이 가능하다.

코드를 통해 한 번 알아보자.

public interface A {
    // default 메소드
    default void method1() {
        method2();
    }

    // private 메소드
    private void method2() {
        System.out.println("A의 private 메소드");
    }

    // static 메소드
    static void method3() {
        method4();
    }

    // private static 메소드
    private static void method4() {
        System.out.println("A의 private static 메소드");
    }
}

public class Example {

    public static void main(String[] args) {
        A a = new A1();
        a.method1(); // A의 private 메소드
        A.method3(); // A의 private static 메소드
    }
}

위의 코드를 보면 private 메소드를 실행하기 위해 default 메소드 내부에서 간접적으로 private 메소드를 실행하는 코드를 작성하였고 private static 메소드도 역시 간접적으로 실행하기 위해서 static 메소드 내부에서 간접적으로 실행하였다.

즉, private 메소드는 이러한 간접적인 방식으로 실행하면 된다.




참조
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://velog.io/@heoseungyeon/%EB%94%94%ED%8F%B4%ED%8A%B8-%EB%A9%94%EC%84%9C%EB%93%9CDefault-Method
https://lifework-archive-reservoir.tistory.com/282
https://swlin23.tistory.com/32

profile
꾸준하게 가자

0개의 댓글