프로그래밍/자바

인터페이스에서의 static 메소드와 default 메소드(Static and Default Methods in Interfaces in Java)

말랑공룡 2021. 8. 2. 15:12

이 포스팅은 https://www.baeldung.com/java-static-default-methods 포스팅의 번역 포스팅입니다.


1. 개요

 

자바8은 여러가지로 새로워졌습니다. 람다(lambda expressions), 함수형 인터페이스(functional interfaces), 메소드 참조(method references), 스트림(streams), 옵셔널(optional) 그리고 인터페이스의 staticdefault 메소드가 그것들입니다.

 

이 포스팅에서는 인터페이스의 staticdefault 메소드들을 어떻게 사용하고 활용할 수 있는지 다뤄볼 것 입니다.

 

2. 인터페이스에 default 메소드는 왜 필요한가

 

인터페이스의 일반 메소드들처럼 default 메소드는 암시적으로 public입니다. 굳이 public이라고 명시할 필요가 없기 때문입니다. 하지만, 인터페이스의 일반 메소드들과 다르게 그것은 메소드 앞에 default라는 키워드가 선언됨으로써 그 역할을 수행합니다.

 

예시를 봅시다:

public interface MyInterface {
    
    // regular interface methods
    
    default void defaultMethod() {
        // default method implementation
    }
}

자바8에서 인터페이스에 왜 default 메소드가 추가되었는지 그 이유는 꽤 명확합니다.

추상에 기반한 디자인에 따르면 인터페이스는 일반적으로 하나 혹은 다수의 구현 클래스들을 가지고 있는데 만약 인터페이스에 새로운 메소드가 추가된다면 그 추가된 수 만큼 구현 클래스들은 강제적으로 구현을 해야합니다. 그렇지 않으면 이 디자인은 바로 무너져 내립니다.

인터페이스의 default 메소드는 이 이슈를 효율적으로 해결할 수 있습니다. 그것들은 인터페이스에 새로운 메소드가 추가되면 자동적으로 구현 클래스에서도 사용할 수 있게 됩니다. 즉, 구현 클래스들을 수정할 필요가 없어집니다.

이렇게 구현 클래스를 수정하지 않고 깔끔하게 하위 호환(backward compatibility)이 가능하게 합니다.

 

3. default 인터페이스 메소드의 사용

 

인터페이스의 default 메소드를 더 기능적으로 이해하기 위해, 간단한 예제를 봅시다.

간단하게 Vehicle 인터페이스를 만들고 하나의 구현 클래스를 생성했습니다.

 

public interface Vehicle {
    
    String getBrand();
    
    String speedUp();
    
    String slowDown();
    
    default String turnAlarmOn() {
        return "Turning the vehicle alarm on.";
    }
    
    default String turnAlarmOff() {
        return "Turning the vehicle alarm off.";
    }
}
public class Car implements Vehicle {

    private String brand;
    
    // constructors/getters
    
    @Override
    public String getBrand() {
        return brand;
    }
    
    @Override
    public String speedUp() {
        return "The car is speeding up.";
    }
    
    @Override
    public String slowDown() {
        return "The car is slowing down.";
    }
}

마지막으로 main 클래스를 정의하고 Car 객체를 만들어서 메소드를 호출합니다.

public static void main(String[] args) { 
    Vehicle car = new Car("BMW");
    System.out.println(car.getBrand());
    System.out.println(car.speedUp());
    System.out.println(car.slowDown());
    System.out.println(car.turnAlarmOn());
    System.out.println(car.turnAlarmOff());
}

default 메소드인 turnAlarmOn() turnAlarmOff()자동으로 Car 클래스에서 사용 가능하다는 점에 주목하세요.

만약 Vehicle 인터페이스에 몇개의 default 메소드를 추가하려한다면 이 기능은 여전히 작동할 것이고 다른 클래스에 그 새로운 메소드의 구현을 강제하지 않아도 될 것입니다.

default 메소드의 가장 일반적인 사용 목적은 구현 클래스를 파괴하지 않은채로 점진적으로 추가적인 기능을 제공한다는 것에 있습니다. 덧붙여, 기존의 추상 메소드에 기능을 덧붙이는 목적으로 사용할 수 있습니다. :

public interface Vehicle {
    
    // additional interface methods 
    
    double getSpeed();
    
    default double getSpeedInKMH(double speed) {
       // conversion      
    }
}

 

4. 인터페이스 다중 상속 규칙

 

인터페이스의 default 메소드는 정말 멋진 기능이지만 주의해야 할 사항이 있습니다.

자바가 인터페이스의 다중 상속을 허용하기 때문에 같은 default 메소드를 정의한 여러개의 인터페이스를 한 클래스가 다중 상속하면 어떤 문제가 발생하는지 알아야 합니다.

예를 들어, 전에 예시에 등장했던 Vehicle 인터페이스와 새로운 Alarm 인터페이스를 구현하는 Car 클래스를 살펴봅니다. :

public interface Alarm {

    default String turnAlarmOn() {
        return "Turning the alarm on.";
    }
    
    default String turnAlarmOff() {
        return "Turning the alarm off.";
    }
}
public class Car implements Vehicle, Alarm {
    // ...
}

이 경우, 코드는 컴파일 되지 않습니다. 인터페이스 다중 상속으로 인한 충돌이 발생했기 때문입니다. (이것을 Diamond Problem 이라고 합니다.) Car 클래스는 각 인터페이스에 있는 동일한 default 메소드들 중, 어떤 것을 호출해야할까요?

이 모호함을 해결하기 위해, 우리는 메소드에 명시적으로 구현을 해야합니다.

@Override
public String turnAlarmOn() {
    // custom implementation
}
    
@Override
public String turnAlarmOff() {
    // custom implementation
}

이렇게 구현을 하고, 각 인터페이스(Vehicle, Alarm)의 default 메소드 중에 어떤 것을 사용할 지 정의해야 합니다.

Vehicle을 사용하고 싶다면 아래와 같이 명시합니다. :

@Override
public String turnAlarmOn() {
    return Vehicle.super.turnAlarmOn();
}

@Override
public String turnAlarmOff() {
    return Vehicle.super.turnAlarmOff();
}

Alarm을 사용하고 싶다면 아래와 같이 명시합니다. :

@Override
public String turnAlarmOn() {
    return Alarm.super.turnAlarmOn();
}

@Override
public String turnAlarmOff() {
    return Alarm.super.turnAlarmOff();
}

더 나아가, default 메소드들을 함께 사용할 수도 있습니다.

 

5. Static Interface Methods

 

default 메소드 화제와 별개로, 자바8은 인터페이스에서 static 메소드의 정의와 구현을 허용합니다.

static 메소드는 특정 객체에 속하지 않기 때문에, 인터페이스를 구현하는 클래스의 API의 한 부분이 아닙니다. 그것은 메소드명 앞에 인터페이스 이름을 넣어서 호출해야 합니다.

인터페이스에서의 static 메소드 동작을 이해하기 위해, Vehicle 인터페이스에 static 메소드를 추가해봅시다. :

public interface Vehicle {
    
    // regular / default interface methods
    
    static int getHorsePower(int rpm, int torque) {
        return (rpm * torque) / 5252;
    }
}

인터페이스에 static 메소드를 정의하는 것은 한 클래스에 그것을 정의하는 것과 동일합니다. 게다가 static 메소드는 다른 staticdefault 메소드에서 적용될 수 있습니다.

위의 getHorsePower()를 호출해봅니다. :

Vehicle.getHorsePower(2500, 480));

인터페이스의 static 메소드는 객체 생성 없이 한 공간에서 연관된 메소드들의 결합도를 상승시키는 간단한 방법을 제공합니다. 추상 클래스와 매우 흡사하지만 가장 다른 점은 추상 클래스는 constructors, statem, 그리고 behavior가 있습니다.

더욱이, 인터페이스의 state 메소드는 단지 static 메소드들의 집합일 뿐인 응용 클래스의 생성 없이 연관된 응용 메소드들을 그룹화할 수 있습니다.

 

6. 결론

 

이 포스팅에서 우리는 자바8에서 인터페이스의 staticdefault 메소드들을 살펴봤습니다. 처음 훑어보면 순수 객체지향 관점으로 봤을 때 조금 엉성해 보입니다. 이상적인 인터페이스는 너무 복합적인 기능으로 압축되어 있으면 안되고 특정 타입의 API를 정의하는 데 사용되어야 하기 때문입니다.

하지만 이것들은 기존의 코드들과 하위 호환하고 유지보수 하는 데에는 유용한 기능을 제공함으로써 위의 부정적인 관점과 상충관계에 있다고 볼 수 있습니다.