본문 바로가기

 

1. 자바의 함수에 대해서
2. functional Interface는 언제 사용하는거고 왜 등장 했을까?
3. default method는 언제사용하고 왜 등장했을까?
4.추상클래스,인터페이스 비교

1. 자바의 함수

먼저 일급시민(first class citizen)이라는 용어에 대해서 이해하고 넘어가면 좋은데 사실 정의를 읽어도 무슨말인지 이해가 가지 않는다. 하지만 일급시민의 조건은 아래와 같다고 한다.

  • 변수에 담는것이 가능하다
  • 인자로 전달이 가능하다
  • 반환값으로 전달이 가능하다.


위 3가지 조건을 만족하는것이 자바에서는 객체이다. 하지만, 메서드 자체를 자바에서는 일급값으로 취급하지 않았기 때문에 변수로 담지못하고 인자로 전달하지 못하고 반환값으로 사용하지 못하는 문제가 있었다. 메서드가 일급시민이 아니면 인자로 넘겨주기 위해서 익명클래스를 구현하면서 코드의 길이가 불필요하게 길어지는 문제가 발생하는 것이다.

하지만, Java 8 부터는 메서드가 일급값이 되면서 메서드를 인자로 전달할 수 있고, 변수에 담을 수 있고 ,반환값으로 전달이 가능해졌다.


2. functional Interface

 

2.1 functional interface 정의


함수형 인터페이스는 정확히 `하나의 추상 메서드`를 지정하는 `인터페이스`이다. `@FunctionalInterface` 애노테이션을 태깅시켜서 함수형 인터페이스임을 명시할 수 있다. 또한, 함수형 인터페이스가 아니지만 해당 애노테이션을 태깅시키면 컴파일시에 에러가 발생할 수 있다.


2.2 functional interface를 언제 사용하는가?


람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있다. 즉, 람다로 표현한 내용을 함수형 인터페이스의 인스턴스로 취급할 수 있다. 즉 인터페이스 구현을 람다식을 이용해서 더 간결하게 표현할 수 있는 방법이다.

람다 표현식의 시그니처를 서술하는 메서드를 `함수 디스크립터`라고 부른다. 아래 예시코드를 보면 r1변수에는 람다식이 들어가 있다. Runnable인터페이스는 FunctionalInterface인것을 확인할 수 있는데 Runnable 인터페이스의 run 추상메서드는 `람다표현식의 함수 디스크립터 역할`을 하고 있는것을 알 수 있다.

@FunctionalInterface public interface Runnable { /** * When an object implementing interface <code>Runnable</code> is used * to create a thread, starting the thread causes the object's * <code>run</code> method to be called in that separately executing * thread. * <p> * The general contract of the method <code>run</code> is that it may * take any action whatsoever. * * @see java.lang.Thread#run() */ public abstract void run(); }
Runnable r1 = () -> System.out.println("Hello World1");
Runnable r2 = new Runnable() {
  public void run() { 
    Systeml.out.println("Hello World 2"); 
  } 
}



2.3 Functional Interface 사용예제

은행 거래내역을 조회하는 BankStatementProcessor라는 클래스에 findTransactions라는메서드가 있다.

public class BankStatementProcessor { 

     public List<BankTransaction> findTransactions(
       	 final BankTransactionFilter bankTransactionFilter
        ) { 
        
          final List<BankTransaction> result = new ArrayList<>();   
          for(final BankTransaction bankTransaction : bankTransactions) {
            if(bankTransactionFilter.test(bankTransaction) { 
              result.add(bankTransaction); 
             } 
          }
          
          return result; 
        } 
      }


FunctionalInterface는 람다식의 함수 디스크럽터 역할을 한다는 것을 확인했는데 이를 real world에서 적용해보면 아래와 같이 사용이 가능하다. 거래내역을 조회할 Filter를 FunctionalInterface를 통해서 시그니처를 정의하고, 이를 사용하는 클라이언트측의 코드에서 실제 구현을 람다식으로 표현하면 된다.

@FunctionalInterface public interface BankTransactionFilter {

    boolean test(BankTransaction bankTransaction);

}
final List<BankTransaction> transactions = bankStatmentProcessor.findTransactions(
		bankTransaction -> { 
    		bankTransaction.getDate().getMonth() == Month.FEBRUARY
        	&& bankTransaction.getAmount() >= 1000
    	});

 

3. Default Method

 

3.1 왜 등장했는가?

인터페이스에 새로운 메서드를 구현하는 경우를 생각해보자. 만약 인터페이스를 구현한 구현체가 수십개라면? 생각도 하기가 싫다. 인터페이스를 구현하는 모든 클래스에는 새로 추가된 메서드를 구현해야한다. 기존의 구현을 고치지 않고도 이미 공개된 인터페이스를 변경할 방법은 없을까? 라는 아이디어에서 default method가 등장하게 되었다.

즉, default method는 구현체에서 반드시 구현하지 않아도 되는 메서드를 인터페이스에서 구현해둘수가 있게 되었다. 구현체가 로드될때는 인터페이스에 default키워드가 붙어 있는 메서드가 로드되면서 컴파일시에도 아무 문제없이 실행이 가능하다.

즉, 기존의 코드를 최대한 수정하지 않으면서 설계된 인터페이스에 새로운 확장을 가능하게 만들기 위해서 등장한 기술이다.

Java8의 List인터페이스에는 sort메서드가 default method로 선언되어 있다. 즉, sort를 구현하지 않더라도 Collections의 sort를 이용해서 정렬을 진행하도록 선언되어 있는것이다.

default void sort(Comparator<? superE> c) { Collections.sort(this,c); }

 



4. 추상클래스와 인터페이스 비교

 

4.1 추상클래스

  • 추상클래스는 클래스 내에 `추상 메서드가` 포함되어 있고 `abstract으로 정의된 클래스`이다
  • 추상클래스를 상속받은 클래스는 `추상 메서드를 구현`해야한다 (일반메서드는 구현할 필요 없이 물려받은거 쓰면 된다)
  • 인터페이스를 구현한 추상클래스가 있다. 추상클래스에서는 a메서드만 추상메서드로 두고 b,c메서드는 구현을 했다. 이 추상클래스를 상속받은 A,B,C가 있는데 A,B,C에서는 a메서드내용만 구현하면된다.
  • A,B,C클래스는 부모클래스의 B,C메서드는 공통으로 사용하고 A메서드만 각 구현체별로 다르다면 이와 같은 형태로 설계하면 된다

 


4.2 인터페이스

  • 모든 메서드가 추상 메서드이다 (java8에는 default method가 추가되어 구현할 수 있음)
  • 인터페이스를 구현한 클래스는 추상메서드를 반드시 구현해야한다.
  • 위의 추상클래스를 설명하는 과정에서 default method를 사용하면 abstract class없이도 위의 형태를 설계할 수 있다.

 


abstract class, interface를 겉으로 비교해보면, abstract class는 상태(멤버변수) 가질수 있고 interface는 상태를 가지지 못한다. 그리고 abstract class는 다중상속이 불가능하지만, interface는 여러개를 구현할 수 있다.

겉으로 봤을때는 이런 차이가 있지만 java8의 default method가 등장하면서 사실 둘의 기능적 명세가 너무 비슷하다고 생각했다. (추상메서드를 제외한 메서드는 상위에서 구현하고 따로 오버라이딩 하지 않고 추상메서드만 오버라이딩 하면 되므로)

이펙티브 자바 아이템 20을 확인하면 이런 내용이 있다. `추상클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다`. 추상 클래스 방식은 새로운 타입을 정의하기 힘들게 한다. 반면, 인터페이스가 선언한 메서드를 모두 정의하고 규약을 잘 지킨 구현체라면 `다른 어떤 클래스를 상속했든 같은 타입`으로 취급된다.

인터페이스를 구현한 구현체 A,B,C는 다른 인터페이스를 구현할 수 있고, 다른 인터페이스를 구현해서 타입은 같고 기능은 추가되므로 확장에 열려 있다.

반면 추상클래스를 상속 받은 클래스 A,B,C는 다중상속이 불가능하므로 최초에 상속받은 추상클래스의 타입으로 타입이 제한된다. 즉, 기존에 상속받은 기능 + 새로운 기능을 위해서는 두개의 기능을 합친 또다른 부모 클래스를 만들고 이를 상속받아야 확장에 열려 있게 된다. (최초에 상속받은 추상클래스에 계속 메서드를 늘려가면서 확장 하면 되는거 아니냐고 생각할 수 있지만 God Object를 만드는 안티패턴이다. 즉, 이런 방법으로 기능을 확장하는 것은 단일 책임원칙을 위반하는 행위이다.)

참고



https://javabom.tistory.com/22

댓글