brunch

You can make anything
by writing

C.S.Lewis

by Chansuk Yang Apr 08. 2016

안드로이드 N에서 자바 8 사용하기

lamda expression, method reference, etc.

TL; DR: 안드로이드 N 개발자 프리뷰 버전부터 일부 자바 8 기능을 활용할 수 있습니다. 자바 8 환경을 설정하는 방법과, 어떤 기능들을 활용할 수 있는지 살펴봅니다.


뭘 사용할 수 있을까?

안드로이드 N 개발자 프리뷰 버전에서 추가된 여러 기능 중, 개인적으로 가장 반가운 기능은 바로 향상된 자바 8 언어 지원에 관한 내용입니다. 자바 8 자체는 이미 2년 전에 공개되었고 , 덕분에 주요 기능에 관한 다양한 자료를 쉽게 찾아볼 수 있습니다. 다만, 안드로이드 상에서 지원되는 기능에는 몇몇 제한이 있는데요, 안드로이드 개발자 블로그의 내용을 참고하고 직접 테스트해본 결과 현재 개발자 프리뷰 버전 1 단계에서 지원되는 자바 8 기능은 다음과 같이 정리할 수 있습니다.


안드로이드 2.3 진저브레드 이상 (minSdk >= 9)

- 람다 표현식, Method Reference

안드로이드 N 개발자 프리뷰 버전 이상 (minSdk > 23)

- Default Method, 함수형 인터페이스 (Functional interfaces)


다시 말해, 람다와 Method Reference를 제외하면 이전 버전에 대한 호환성을 보장하지 않으며, 아쉽지만 앱 개발시 적극적으로 자바 8 기능을 사용하는 데는 어려움이 있을 것으로 보입니다. 또한, 개발자 블로그 문서에는 스트림(Stream) API 지원에 관해 언급되어있지만, 아직 개발자 프리뷰 버전에서는 해당 API를 사용할 수는 없습니다. 스트림 기능을 지원하기 위해 필요한 여러 요소들이 이미 많이 추가된 만큼, 아마도 차후에 지원될 것으로 기대합니다.


환경 설정하기

개발자 문서에 잘 설명되어 있습니다. 먼저, 개발자 프리뷰 버전에 포함된 안드로이드 스튜디오 2.1을 설치합니다. 다음 카나리아 버전 다운로드 페이지를 통해 설치할 수 있습니다. 다만, 카나리아 버전은 어디까지나 카나리아 버전인 법. 기존에 사용하고 계신 버전은 그대로 두고 따로 설치하는 것을 추천드립니다. 


다음으로, 안드로이드의 새로운 툴체인 JACK(Java Android Compiler Kit)을 사용하도록 빌드 파일을 조금 수정해야 합니다. JACK은 2015년 구글 I/O에서 처음 소개된 도구로 class 파일을 거치지 않고 소스코드를 dex 파일 형태로 직접 컴파일할 수 있습니다. 오픈소스 프로젝트인 만큼 이후 새로운 자바 언어기능을 보다 빠르게 지원하는데 큰 역할을 할 것으로 기대하고 있습니다 : )


다만, 아직까지 JACK 툴 체인은 정식버전이 아니며, app 모듈의 build.gradle 파일을 수정해 기능을 활성화시켜야 합니다. 다음과 같이 build.gradle 파일을 수정합니다.

android {
  ...
  defaultConfig {
    ...
    jackOptions {
      enabled true
    }
  }
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

혹시 기존 프로젝트에서 자바 8 기능을 사용하기 위해 build.gradle 파일을 수정한 경우에는 Gradle 버전이 맞지 않아 빌드가 실패할 수 있습니다. 이 때는 당황하지 말고 프로젝트 build.gradle 파일을 다음과 같이 수정해 올바른 Gradle 버전을 사용하도록 변경합니다. 

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.0-alpha5'
        ...
    }
}


안드로이드 자바 8 맛보기

람다 표현식

이제 제한적이나마 자바 8 기능을 활용할 수 있는 준비가 되었습니다. 그럼, 다음으로 어떤 기능을 활용할 수 있는지 알아볼까요? 가장 먼저 람다 표현식을 살펴봅니다. 아래는 익명 클래스를 이용해 버튼 클릭 이벤트에 반응하는 콜백 함수를 등록하는 코드 조각입니다. 

view.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Log.d(TAG, "Button is clicked");
    }
});

자바 8의 람다 표현식을 활용해 손가락 힘을 덜 들이고 동일한 기능을 구현할 수 있습니다.

view.setOnClickListener( v -> Log.d(TAG, "Button is Clicked"));

람다 표현식은 (인자) -> { 함수 구문 } 형태로 이루어지며, 기본적으로 추상 메서드가 하나뿐인 이른바 함수형 인터페이스(Functional Interface)를 구현한 익명 클래스를 대체하여 사용할 수 있습니다. 또한, 컴파일러단에서 대상 타입과 매개변수 타입 추론이 이루어지기 때문에, 불필요한 코드를 대폭 줄일 수 있습니다. 위의 예의 경우, 타입 추론 덕분에 생략할 수 있는 코드를 모두 포함하면 다음과 같습니다.

view.setOnClickListener(
        (View.OnClickListener) (View v) -> { Log.d(TAG, "Button is Clicked"); });

람다 표현식을 이용해 안드로이드 UI를 구현할 때 악명 높은 익명 클래스 지옥에서 벗어나 좀 더 읽기 쉽고 깔끔한 코드를 작성할 수 있습니다. 다만, 기존 익명 클래스를 사용하고 있던 부분 외에 다른 곳에도 적극적으로 활용할 때는 성능 상의 손해가 없는지 주의가 필요합니다. 기존 자바 8 컴파일러의 경우 람다 표현식을 조금 더 가벼운 형태로 컴파일되며, 성능 상의 이점을 가져가는 반면, 안드로이드에서의 람다는 하위 호환을 위하여 내부적으로도 온전한 익명 클래스로 구현됩니다.


Method Reference

람다 표현식을 적용하면 코드를 깔끔하게 정리할 수 있습니다. 하지만 Method Reference를 사용하면 여기서 한 발 더 나아갈 수 있습니다. Method Reference는 이미 클래스에 정의되어 있는 '이름을 갖고 있는 메소드'를 람다 표현식을 대신 직접 활용할 수 있게 해줍니다. 예를 들어, 위 예시 코드에서 사용한 액티비티에 이미 버튼 클릭 이벤트를 처리하기 위한 다음과 같은 메소드가 존재한다고 생각해보겠습니다.

public class MainActivity extends Activity {
    private void handleButtonClick(View v) {
        Log.d(TAG, "Button is Clicked");
    }
    ...
}

이 경우, 버튼 클릭 이벤트를 처리하기 위해 아래와 같이 람다 표현식 내에서 해당 함수를 호출하도록 선언할 수 있습니다.

view.setOnClickListener( v -> handleButtonClick(v) );

그리고, 이런 경우 다음과 같이 람다 표현식 대신 Method Reference를 사용해 직접 해당 메서드를 참조할 수 있습니다.

view.setOnClickListener(this::handleButtonClick);

코드가 한층 간결하고 읽기 쉬어졌습니다. Method Reference로 대체할 수 있는 람다 표현식의 경우에는 기본적으로 Method Reference를 활용하는 것이 권장됩니다. 실제로 별다른 어려움 없이 둘을 교체할 수 있는 경우에는 안드로이드 스튜디오의 똑똑한 Lint 툴이 알아서 Method Reference로 해당 코드를 변경할 것을 권유해줍니다.


참고 삼아, Method Reference는 위의 예처럼 private 한 멤버 메서드뿐만 아니라 public / static 메서드를 가리지 않고 모두 적용이 가능합니다. 또한 클래스 생성자의 경우 예약된 new 키워드를 이용해 Foo::new와 같은 형태로 생성자를 참조할 수 있습니다.


Default Method

기존 자바에서는 인터페이스 내부에 메서드를 직접 구현할 수는 없었습니다. 자바 8부터는 인터페이스 내부에도 Default Method와 Static Method를 정의하고 구현할 수 있습니다. 

public interface DefaultMethod {
    default void printLog() {
        Log.d("DefaultMethod", "printLog");
    }
    static String parseLog(String log) {
        return log;
    }
}

인터페이스 상에 메서드 구현을 추가할 수 있게 됨으로 기존의 빡빡한 상속 모델에 숨통이 트이는 기분입니다. 무엇보다도, 하위 호환성을 헤치지 않고도 이전에 정의한 인터페이스에 새로운 메서드를 추가하거나 변경할 수 있습니다. 특정 인터페이스와 연관된 유틸 함수들은 별도의 유틸 클래스를 구현하지 않고 바로 인터페이스 상에 추가해 둘 수 있습니다. 메서드를 내부에서 구현할 수 있다 하더라도 인터페이스는 여전히 인터페이스입니다. 추상 클래스(Abstract Class)와는 달리 멤버 변수를 가질 수는 없지만, 클래스는 Default Method를 포함한 인터페이스를 포함하여 하나 이상의 인터페이스를 구현할 수 있습니다. 


다만 Default Method를 갖는 여러 개의 인터페이스를 구현하는 클래스는 같은 이름을 갖는 메서드를 중복하여 가질 수 있습니다. 이 경우에는, 해당 메서드를 오버라이드 하여 직접 구현하거나, 어떤 인터페이스에 속한 메서드를 호출하는지 명시적으로 지정할 필요가 있습니다. 


더 유연하게 API를 설계할 수 있도록 도와주는, 여러 가지로 유용한 기능이지만 Default Method에는 치명적인 단점이 있습니다. 아직까지 이 기능은 API 레벨이 N, 다시 말해 안드로이드 N 이상 버전에서만 지원됩니다.


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

함수형 인터페이스는 하나의 추상 메서드를 갖고 있는 인터페이스입니다. 자바에서 모든 것들이 객체 상태로 존재합니다. 람다 표현식의 경우도 마찬가지입니다. 람다 표현식은 바로 이 함수형 인터페이스를 구현한 익명 객체의 형태로 존재하게 됩니다. 사실, 자바 8 이전부터 자바에서는 함수형 인터페이스가 존재했습니다. 예를 들어 Comparator 인터페이스는 두 개의 값을 인자로 받고 int 값을 반환하는 추상 메서드를 정의하고 있으며(eqauls는 Object 메서드들 상속한 것이라 메서드 카운트에서 제외됩니다), 따라서, 다음과 같이 손쉽게 람다 표현식으로 교체될 수 있습니다.

Collections.sort(strings, new Comparator <String>() {
    @Override
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
});

//위와 동일한 기능을 수행합니다.
Collections.sort(strings, (a, b) -> a.length() - b.length());

자바 8 에서는 'java.util.function' 패키지안에 범용적으로 사용될 수 있는 다양한 함수형 인터페이스를 추가해두었습니다. 그 수가 꽤 많지만, 자바 8에서 추가된 대표적인 함수형 인터페이스를 꼽아보자면 다음과 같습니다.

Consumer <T>: 하나의 인자를 받고 리턴 값은 없는 추상 메서드를 정의하고 있습니다.
Predicate <T>: 하나의 인자를 받고 True/False 값을 리턴 하는 추상 메서드를 정의하고 있습니다.
Function <T, R>: 하나의 인자를 받고 하나의 값을 리턴 합니다.

함수형 인터페이스와 람다 표현식을 이용하면 훨씬 범용적으로 그리고 간결하게 사용할 수 있는 API들을 제공할 수 있습니다. 예를 들어, 자바 8에서는 모든 Collection 객체들이 구현하고 있는 Iterable 인터페이스에 forEach 메서드가 Default Method로 추가되었습니다. 그리고 forEach 메서드는 하나의 Consumer 함수형 인터페이스를 인자로 넘겨받습니다. 메서드를 호출하면, 이름이 의미하는 그대로 Collection 안에 모든 객체 인스턴스들에 대하여 Consumer 인터페이스에 정의된 accept 메서드를 호출합니다. 람다 표현식과 이를 조합하여 활용하면, 다음과 같이 특정 Collection 내에 포함된 객체 정보를 로그로 찍는 dump 함수를 간단히 구현할 수 있습니다.

ArrayList <String> strings = new ArrayList <>();
...
strings.forEach(s -> {
    Log.d(TAG, "Content: " + s);
    Log.d(TAG, "Length: " + s.length());
    Log.d(TAG, "Hashcode: " + s.hashCode());
});

많은 분들이 기대하고 계신 Stream API 역시 함수형 인터페이스와 람다 표현식을 조합해, 반복문을 활용하여 복잡하게 처리하던 비즈니스 로직을 훨씬 깔끔하고 직관적인 방법으로 표현할 수 있도록 지원해줍니다. 다만 새롭게 추가된 여러 함수형 인터페이스들 역시 API 레벨이 N 이상이기 때문에, 아쉽게도 안드로이드 N 이상 버전에서만 지원됩니다.


지금까지 안드로이드 N 개발자 첫 번째 프리뷰 버전에서 자바 8 기능을 활성화시키는 방법과 안드로이드 앱 개발 시 활용할 수 있는 주요 기능들을 살펴보았습니다. 자바 8 기능 지원은 이제 시작인만큼 앞으로 더 다양한 기능을 더 다양한 안드로이드 버전에서 사용할 수 있기를 기대합니다. 그럼 그때까지 손가락 건강에 유의하시기 바랍니다 : p

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari