[JAVA] 인터페이스의 발전 과정

2022. 4. 16. 21:30[ 백엔드 개발 ]/[ Java,Kotlin ]

디폴트 메소드 도입

자바 8부터 인터페이스의 기능이 개선되었다. 그중 하나가 인터페이스가 구현체를 가지게 된 것인데 이것이 인터페이스의 디폴트 메소드이다. 인터페이스는 모든 메소드가 추상 메소드로만 이루어진 클래스를 의미한다. 하지만 자바 8부터 디폴트메소드가 인터페이스에 들어갈 수 있게 되면서 추상 클래스와의 차이가 거의 없어졌다.

 

예시

default void say() {
	// 디폴트 메소드의 구현 몸체부
    // 디폴트 메소드의 구현 몸체는 이를 구현하는 클래스가 아닌,
    // 인터페이스가 자체적으로 가지고 있음
}

 

 

 

 

디폴트 메소드를 사용하는 이유

- 어뎁터의 기능 사용

- 디폴트 메소드 등장으로 인해 static 메소드도 가질 수 있게 됨.

- default, static 메소드들은 implements를 하지 않아도 되기 때문에 인터페이스 추가만으로 기능을 확장할 수 있게 됨

 

cf. 어뎁터 : 인터페이스의 메소드들 중 원하는 것만 구현하고 싶을 때 사용하는 패턴

 

디폴트 메소드의 등장으로 인해 인터페이스를 함수 제공자로서 사용할 수 있게 되었고 이로써 functional interface가 등장했다.

 

 

함수형 인터페이스(Functional Interface)

- 디폴트 메소드 등장으로 인해 도출된 개념

- 함수 단 한개를 제공함으로써 '함수 제공자' 역할을 하는 걸 목표로 함

- default나 static 메소드는 존재 여부와 관련 없이 추상 메소드만 단 1개인 인터페이스를 의미

 

 

익명 클래스의 등장

함수형 인터페이스를 구현하기 위해서 하나의 메소드를 구현하는 class를 만들고 이를 new를 통해 객체화 후 메소드를 호출해야 한다. 이러한 번거로움을 줄이고자 인터페이스를 구현하는 클래스를 임시적으로 생성해주는 익명 클래스 개념이 등장한다. 즉, 함수형 인터페이스에 존재하는 메소드를 구현해 사용하고 싶은 시점에서 임시적으로 해당 인터페이스를 구현하는 클래스를 정의하고 인스턴스를 바로 생성하는 방식이다.

 

예시

		MyrunnableMethod mm = new MyrunnableMethod() {
		    @Override
		    public void run() {
		        System.out.print("익명 클래스의 메소드");
		    }
		}.run();

 

 

람다 표현식의 등장

functional interface를 구현하는 익명 클래스의 경우 당연히 함수 1개를 override 한다는 특징이 전제되어 있기 때문에 메소드 정의를 보다 간소화할 수 있다. 익명 클래스 부분에서 다음과 같은 사항을 생략하여 functional interface를 구현하는 익명 클래스를 정의할 수 있다.

 

- @Override

- functional interface를 구현하는 익명 클래스의 이름

- new 키워드

- functional interface가 가지고 있는 메소드 이름

- 메소드 parameter type

- 메소드 return 타입(함수형 인터페이스에 정의되어 있기 때문에 생략 가능)

 

 

위와 같이 뻔하게 사용될 부분을 생략하고 메소드 parameter 부분과 몸체 부분을 구분하는 -> 키워드를 추가함으로써 아래와 같이 메소드 정의를 보다 간소화할 수 있다. 

 

MyrunnableMethod mm = () -> System.out.print("익명 메소드");

 

이처럼 익명 클래스 중 함수형 인터페이스를 구현하는 익명 클래스의 정의 부분을 간소화해 표현한 것을 람다 표현식이라고 한다. 람다 표현식을 사용하여 함수형 인터페이스의 인스턴스를 간결하게 정의하고 생성할 수 있다.

 

 

 

메소드 레퍼런스

람다 표현식으로 정의한 메소드에서 인자가 1개이고 이를 '변경없이' 바로 메소드 몸체에서 사용한다면 :: 키워드를 통해 생략하고 사용할 수 있다.

 

// 메소드 레퍼런스 적용 전
MyrunnableMethod mm = (i) -> System.out.print(i);
		
// 메소드 레퍼런스 적용 후
MyrunnableMethod mm = System.out::print;

 

만약 아래와 같이 인자로 들어온 값을 변경하여 사용하는 경우에는 메소드 레퍼런스를 적용할 수 없다.

 

MyrunnableMethod mm = (i) -> System.out.print(i + 10);

 

 

메소드 레퍼런스를 사용하면 메소드 인자에 대한 값 변경을 강제로 막을 수 있다. 즉, 메소드 레퍼런스에는 입력 값을 변경하지 말라는 강제성이 부여된 의미가 포함되어 있다.

 


 

cf. 람다 표현식 응용

- Supplier : 입력 값을 주면 무언가를 반환해주는 주체

- Consumer : 입력 값이 주어지면 해당 입력 값으로 특정 로직을 수행한 뒤 반환하지 않는 주체

 

예시

MySupplier<String> s = () -> "sudo";

MyMapper<String, Integer> m1 = String::length;
MyMapper<Integer, Integer> m2 = i -> i * i;

MyConsumer<String> c = System.out::print;

// 수행
c.consume(m2.map(m1.map(s.supply())));

 

MyMapper<String, Integer> : String 값을 받고 이에 해당하는 Integer 반환

MyMapper<Integer, Integer> : Integer 값을 받고 이에 해당하는 Integer 반환

 

 

 

 

 

Reference

- Consumer Java11 Docs : https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/function/Consumer.html

 

- BiConsumer(입력이 2개) Java11 Docs : https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/function/BiConsumer.html

 

- Supplier Java11 Docs : https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/function/Supplier.html