Search

인터페이스의 변화

1. 인터페이스 기본 메소드와 스태틱 메소드

지금까지 사용하던 자바의 인터페이스 기능은 추상메소드와는 달리 메소드 선언만 할 뿐 그 내부에 로직이 들어가는 일이 없었습니다. 하지만 Java 8 부터는 인터페이스에도 메소드 선언이 아니라 구현이 가능해졌는데 이 방법이 default methodstatic method 입니다.

기본 메소드(Default Methods)

public interface Foo { void printName(); }
JavaScript
public class DefaultFoo implements Foo{ @Override public void printName() { System.out.println("DefaultFoo"); } }
Java
기본적인 Foo 인터페이스와 DefaultFoo 구현체입니다. 동작도 제대로 하고있습니다.
여기서 DefaultFoo뿐아니라 DefaultFoo2, ... , DefaultFooN 까지 많은 클래스들이 Foo를 구현한다고 할 때 나중에 처음 기획과는 다르게 대문자로 이름을 출력하는 기능이 추가되야한다고 하면 어떨까요? Foo 인터페이스에 void printNameUpperCase() 라는 메소드를 선언해준다면 구현체 모두 컴파일 에러가 발생합니다. 따로 구현을 해주지 않았기 때문이죠. 기본 메소드(Default Method)는 이런 상황때문에 생겼습니다.
public interface Foo { void printName(); //기본 메소드(Default Method) default void printNameUpperCase(){ System.out.println("FOO"); } }
Java
public static void main(String[] args){ Foo foo = new DefaultFoo(); foo.printName(); foo.printNameUpperCase(); }
Java
이처럼 기본 메소드를 사용하면 구현체에서 따로 해당 메소드를 구현하지 않아도 사용이 가능하다.
하지만, 기본 메소드는 구현체가 모르게 추가된 기능이기 때문에 구현체에서는 알 방도가 없습니다.
그렇기에 구현체에서는 기본메소드의 기능, 반환값, 요구값 등등을 모르기 때문에 문제가 생길수 있습니다.
public interface Foo { void printName(); default void printNameUpperCase(){ System.out.println(getName().toUpperCase()); } String getName(); }
Java
예를들어 위 코드에서 기본메소드인 printNameUpperCase() 에서는 구현체가 구현한 getName()메소드를 호출해 toUpperCase()를 호출하지만, 실제로 구현체의 getName()에서 무조건 문자열이 반환될지에 대해서는 확실하지 않습니다. 만약 null값이 반환된다면 에러가 발생하게 됩니다.
그렇기에 최소한으로 반드시 문서화를 해야합니다. (Java 8에 추가된 @ImplSpec)
/** * @ImplSpec * 이 구현체는 getName()으로 가져온 문자열을 대문자로 변환 후 출력한다. */ default void printNameUpperCase(){ System.out.println(getName().toUpperCase()); }
Java
만일, 이래도 문제가 된다면 구현체에서 재정의하는것 역시 가능합니다. 일반적으로 선언된 메소드와 동일하게 오버라이드 하여 기능을 정의하면 됩니다.
추가로 여기서 기억해야할 사항은 기본메소드는 Object가 제공하는 기능(equals, hasCode)은 기본 메소드로 제공할 수 없으며 재정의는 구현체에서 해야 합니다. 또한 기본 메소드는 Foo 클래스처럼 사용자가 직접 정의한 인터페이스에서만 사용이 가능하며 외부라이브러리등에 추가하여 사용할 순 없습니다.
인터페이스를 상속하는 인터페이스에서는 기본 메소드가 필요 없다면?
기본메소드로 인삿말을 출력하는 기능이 있는 hello() 라는 기본 메소드가 있다고 할 때 이 인터페이스를 상속하는 다른 인터페이스가 있을 수 있습니다.
그런데 이 인터페이스에서는 구현체들이 각각 자신의 나라말로 인삿말을 하는 hello() 메소드가 필요하기에 이 메소드를 기본 메소드로 두고싶지 않습니다. 그렇다면 해당 인터페이스는 이 메소드를 다시 추상 메소드로 변경할 수 있습니다.
public interface Foo { void printName(); /** * @ImplSpec * 이 구현체는 한국어 인삿말을 출력합니다. */ default void hello(){ System.out.println("안녕하세요"); } }
Java
public interface Bar extends Foo{ void hello(); }
Java
구현체가 상속받는 두 인터페이스 모두 동일한 기본메소드가 있는 경우
Foo라는 클래스에 hello()라는 기본메소드가 있는데 Bar라는 인터페이스에도 hello()라는 인터페이스가 있습니다. 근데 DefaultFoo라는 클래스에서 이 두 인터페이스를 다중상속하는경우 동일한이름의 기본 메소드는 어떻게 될까요?
public interface Bar{ default void hello(){ System.out.println("Bar"); } }
Java
public interface Foo { void printName(); default void hello(){ System.out.println("Foo"); } }
Java
public class DefaultFoo implements Foo, Bar{ private String name; @Override public void printName() { System.out.println("DefaultFoo"); } }
Java
Error: DefaultFoo inherits unrelated defaults for hello() from types Foo and Bar
에러가 발생하며 Foo와 Bar 둘 중 어느 기본메소드인지 모호하다는 것입니다. 이는 따로 규칙을 정하기에도 모호하기 때문에 이 경우 직접 오버라이딩을 해서 hello() 메소드를 재정의 해줘야 합니다.
public class DefaultFoo implements Foo, Bar{ private String name; @Override public void printName() { System.out.println("DefaultFoo"); } @Override public void hello() { System.out.println("DefaultHello"); } }
Java

static 메소드

: 해당 인터페이스를 구현한 모든 인스턴스나 해당 타입에 관련된 유틸리티나 헬퍼 메소드를 제공할 때 인터페이스에 static 메소드를 제공할 수 있습니다.
public interface Foo { void printName(); default void hello(){ System.out.println("Foo"); } static void helloAll(){ System.out.println("인삿말"); } }
Java
public class App { public static void main(String[] args) { Foo foo = new DefaultFoo(); foo.hello(); Foo.helloAll();//static 메소드 사용 } }
Java

2. 자바 8 API의 기본 메소드와 스태틱 메소드

자바 8 에서 추가된 api들을 살펴본다.

1. Collection : Iterable을 상속받는 인터페이스이기에 같이 기술한다.

forEach
: 조금 더 손쉽게 순회를 가능하게 해주는 메소드
public static void main(String[] args) { List<String> names = new ArrayList<>(); names.add("catsbi"); names.add("hansol"); names.add("toby"); names.add("mijeong"); names.forEach(System.out::println); /*for (String name : names) { System.out.println(name); }*/ }
Java
1.
names.forEach(System.out::println)
⇒ forEach는 내부 엘리먼트를 순회하며 각각의 요소들을 파라미터로 전달된 일급함수에 Functional Interface인 Consumer가 들어오게되는데 이를 받아서 처리할 수 있습니다. 단순 출력만 해 줄 것이기에 메소드 레퍼런스기능을 이용해 간결하게 작성해줍니다.
2.
for-of{...}
⇒ 기존에는 for-of문을 사용해 출력도 가능합니다. 하지만 조금이나마 더 간결한 forEach를 사용할 수 있습니다.
spliterator
:iterator와 비슷한 개념으로 Collection을 분할한다는 점에서 차이를 보인다.
public static void main(String[] args) { List<String> names = new ArrayList<>(); names.add("catsbi"); names.add("hansol"); names.add("toby"); names.add("mijeong"); Spliterator<String> spliterator = names.spliterator(); while(spliterator.tryAdvance(System.out::println)); }
Java
1.
Spliterator<String> spliterator = names.spliterator();
⇒ iterator와 비슷하지만 분할할 수 있는 기능을 가진 Iterator 를 만들어 반환합니다.
2.
spliterator.tryAdvance(System.out::println)
⇒ iterator의 hasNext()메소드와 유사합니다. 다만 내부 메소드에 파라미터로 forEach()와 동일하게 Functional Interface인 Consumer가 들어오며 더 이상 들어올게 없을 경우 false를 반환합니다. 이 예제에서는 들어온 값을 단순 출력하는 메소드 레퍼런스를 사용했습니다.
여기까지만 적용하고 출력하면 iterator와 차이가 없습니다. 그렇기에 trySplit을 사용합니다.
public static void main(String[] args) { List<String> names = new ArrayList<>(); names.add("catsbi"); names.add("hansol"); names.add("toby"); names.add("mijeong"); Spliterator<String> spliterator = names.spliterator(); Spliterator<String> trySplit = spliterator.trySplit(); while(spliterator.tryAdvance(System.out::println)); System.out.println("========================="); while(trySplit.tryAdvance(System.out::println)); } /* [실행 결과] toby mijeong ======= catsbi hansol */
Java
1.
Spliterator<String> trySplit = spliterator.trySplit();
⇒ spliterator에서 trySplit()메소드를 호출하게되면 해당 spliterator에서 앞에서부터 절반의 요소를 꺼내 새로운 spliterator를 만들어 반환합니다.
trySplit() 실행 결과
2.
while(spliterator.tryAdvance(System.out::println));
⇒ trySplit()을 통해 앞 두 요소(Catsbi, Hansol)이 분할되어 빠져나갔기 때문에 뒤의 두 요소만 출력합니다.
3.
while(trySplit.tryAdvance(System.out::println)); ⇒ spliterator에서 가져온 앞 두 요소(Catsbi, Hansol)을 출력합니다.
removeIf : Collection요소를 순회하며 인자로 넘겨주는 함수를 Functional Interface인 Predicate에 넘겨줘서 합당한(true를 반환하는) 값을 찾아 삭제합니다.
public static void main(String[] args) { List<String> names = new ArrayList<>(); names.add("catsbi"); names.add("hansol"); names.add("toby"); names.add("mijeong"); names.removeIf(s -> s.startsWith("c")); names.forEach(System.out::println); }
Java
1.
names.removeIf(s -> s.startsWith("c"));
⇒ names를 순회하며 각 요소들 중 단어가 'c'로 시작하는 단어를 찾아 삭제합니다.

2. Comparator

정렬에 사용되는 인터페이스
reversed
: 메소드명의 의미 그대로 역전한다는 것인데, 예를들어 내가 숫자를 오름차순으로 정렬한 뒤 reversed()메소드를 호출하면 반대로 내림차순 정렬이 됩니다.
public static void main(String[] args) { List<String> names = new ArrayList<>(); names.add("catsbi"); names.add("catsbi2"); names.add("hansol"); names.add("toby"); names.add("mijeong"); Comparator<String> compareToIgnoreCase = String::compareToIgnoreCase; names.sort(compareToIgnoreCase.reversed()); names.forEach(System.out::println); }
Java
1.
Comparator<String> compareToIgnoreCase = String::compareToIgnoreCase
⇒ 메소드 레퍼런스를 메소드 체이닝 방식으로 사용할수는 없기에 분리하여 Comparator 타입의 Functional InterfacecompareToIgnoreCase 를 만들어줍니다.
2.
names.sort(compareToIgnoreCase.reversed());
⇒ 미리 선언해놓은 String의 정렬 기준 메소드 레퍼런스에 reversed() 메소드를 호출해 정렬순서를 역순으로 바꿔줍니다.
만약, 여기서 다음 정렬조건을 사용하여 정렬을 이어가고싶다면 thenComparing() 메소드를 이용하여 추가적인 정렬을 할 수 있습니다.

3. 정리

java 8 이후 추가된 기본메소드와 static메소드 그리고 추가된 api들에 대해 간략하게나마 알아봤습니다. 이쯤되면 드는 의문이 있습니다. 그렇다면 인터페이스가 추상클래스와 다른점은 무엇인가냐에 대한 궁금증입니다. 그림을 통해서 간단하게 알아봅시다.
: 위와같이 Foo라는 인터페이스와 이를 구현하는 A,B,C라는 세개의 클래스가 있습니다.
이 경우 각각의 클래슨느 Foo 인터페이스가 선언한 메소드를 모두 구현해야하는 문제가 있습니다. 실제로는 특정 몇 부분의 메소드만 오버라이딩하길 원해도 말이죠.
그렇기 때문에 Java 8 이전에는 추상클래스를 이용하여 이를 해결하였습니다.
: 이처럼 중간에 추상클래스 FooAdapter를 만들어 하위 클래스인 A,B,C는 FooAdapter 추상클래스를 상속받아 필요한 부분만 오버라이딩을 해서 사용을 했습니다. 하지만, 이것도 문제점이 있습니다.
매번 adapter 추상클래스를 만들어야하는 번거로움도 있고 무엇보다 다중상속의 불가능입니다.
implements를 이용한 다중상속과는 다르게 extends를 이용한 상속은 다중 상속이 불가능 하기 때문에 제약이 생겨버립니다.
그런데, Java 8 이후 생겨난 기본메소드(default methods)와 static 메소드덕분에 이제 이런식으로 adapter 추상클래스를 구현할 필요가 사라졌습니다.