여러 개의 JDK를 설치하고 선택해서 사용하기

하나의 개발 장비에 여러 배포판/버전의 JDK를 설치하고 선택해서 사용할 때 편하게 쓸 수 있는 도구들을 소개합니다.

정상혁정상혁

다양한 배포판과 버전의 JDK를 명령어 한 줄로 설치하고 OS의 쉘에서 사용할 JDK를 쉽게 지정할 수 있게 해주는 도구들을 소개합니다.

주요 변경이력
  • 2023.03.27

    • Jabba 설치를 안내하는 URL을 현재 관리되는 포크된 버전으로 변경

  • 2019.07.03

    • Homebrew에 대한 소개를 별도의 단락으로 분리

    • Chocolatey에 대한 설명 보강

1. 특별한 도구를 안 쓸 때의 JDK 설치 & 버전 선택

1.1. JDK 설치

JDK를 수동으로 설치하는 절차는 아래와 같습니다.

  1. 설치할 버전/배포판을 다운로드합니다.

  2. 다운로드한 파일의 압축을 풉니다.

  3. OS의 환경변수를 지정합니다.

    • JAVA_HOME

      • JDK의 압축을 푼 디렉토리를 지정합니다.

      • Maven이나 Tomcat 같은 솔류션에서 이 환경변수로 JDK의 위치를 참조합니다.

    • PATH

      • 쓰고 있던 PATH 변수에 $JAVA_HOME/bin 을 더합니다.

      • java , javac 등을 명령행에서 직접 실행할 수 있도록 하기 위해서 하는 작업입니다.

1.2. JDK 버전 선택

IDE에서는 프로젝트별로 사용할 JDK의 위치를 선택할 수 있습니다. IntelliJ에서는 File > Project Structure > Platform Settings (단축키 Ctrl + Alt + Shift + S ) > SDK 메뉴 에서 이를 지정합니다.

:intelli-j-jdk.jpg

OS의 명령행에서 Maven, Gradle로 직접 빌드를 하거나 java -jar 로 직접 프로그램을 실행시킬 때를 대비해서 JAVA_HOME, PATH 설정이 되어 있어야합니다. 프로젝트마다 사용하는 JDK 버전이 다르면 사용할 JDK를 지정하기가 번거롭습니다. 매번 이런 JAVA_HOME 같은 환경 변수를 바꾸거나 /usr/lib/jvm/java-13-openjdk-amd64/bin/java 와 같이 전체 경로로 실행할 도구를 지정한다면 더욱 그렇습니다.

반복적인 작업을 쉡스크립트나 배치파일로 할 수도 있습니다. 그런데 이미 이런 작업을 편리하게 해주는 도구들이 몇 가지 있습니다.

2. 다양한 JDK 설치와 사용을 편하게 하는 도구

JDK의 설치와 OS의 명령행에서 사용할 버전을 지정할 때는 아래 도구들을 사용할 수 있습니다.

Table 1. JDK 설치와 버전 지정에 사용할 수 있는 도구들
이름 기능 사용 가능한 OS

YUM/APT

범용 패키지 관리 도구

Linux

update-alternatives/alternatives

범용 패키지 버전 선택 도구

Linux

Homebrew

범용 패키지 관리 도구

macOS

Chocolatey

범용 패키지 관리 도구

Windows

SDKMAN

범용 패키지 관리 도구

Linux
macOS
Windows(Cygwin, Git Bash)

jabba

JDK 설치 특화 도구

Linux
macOS
Windows

jEnv

JDK 버전 선택 특화 도구

Linux
macOS

direnv

범용 디렉토리별 환경 변수 관리 도구

Linux
macOS
Windows

2.1. APT/YUM

Ubuntu, CentOS 등의 Linux 배포판에서는 해당 OS에 맞도록 빌드한 OpenJDK 배포판을 APT,YUM 등으로 간단히 설치할 수 있도록 제공합니다.

APT(Advanced Packaging Tool)는 Ubuntu 등 Debian 계열의 리눅스에서 사용할 수 있는 패키지 관리 프로그램입니다. Ubuntu에서는 APT로 아래와 같이 여러 버전의 JDK를 설치할 수 있습니다.

sudo apt install openjdk-8-jdk
sudo apt install openjdk-11-jdk
sudo apt install openjdk-12-jdk

YUM(Yellow dog Updater, Modified)은 Red Hat/CentOS 리눅스 배포판에서 사용할 수 있는 패키지 관리자입니다. 아래와 같이 사용할 수 있습니다.

sudo yum install java-1.8.0-openjdk-devel.x86_64

Adopt OpenJDK 배포판은 패키지 저장소를 추가해서 설치할 수 있습니다. Ubuntu에서는 아래와 같이 합니다.

sudo add-apt-repository ppa:rpardini/adoptopenjdk
sudo apt install adoptopenjdk-11-installer

설치된 JDK의 java , javac 도구는 /usr/bin/java , /usr/bin/javac 에서 심볼릭 링크로 연결되어 어느 디렉토리에서나 실행될 수 있게 됩니다. 이 심볼릭 링크는 이어서 소개할 update-alternatives / alternatives 도구로 관리할수 있습니다. JAVA_HOME 환경 변수는 직접 ~/.bashrc 와 같은 쉘별 설정 파일에 넣어줘야 합니다.

  • 장점

    • OS에서 기본 제공하는 도구이기에 도구를 위한 별도의 설치 과정이 필요 없습니다.

    • JDK 외에도 Maven, Gradle의 설치에도 활용할 수 있는 범용적인 패키지 관리 도구입니다.

  • 단점

    • SDKMAN/ Jabba에 비하면 다양한 JDK 배포판을 제공하지는 않습니다.

2.2. update-alternatives / alternatives

update-alternatives와 alternatives는 여러 버전의 패키지를 관리할 수 있는 Linux에서 제공되는 도구입니다. 여기서는 Ubuntu에서 쓰는 update-alternatives 를 기준으로 설명하겠습니다.

앞서 나온데로 apt 로 설치한 JDK는 /usr/bin/java 에서 심볼릭 링크로 연결됩니다. 이 심블릭 링크는 /etc/alternatives/java 를 중간에 거쳐서 실제 설치한 디렉토리로 연결된 다는 것을 아래와 같이 확인할 수 있습니다.

➜  ~ ll /usr/bin/java
lrwxrwxrwx 1 root root 22  6월  9 22:20 /usr/bin/java -> /etc/alternatives/java
➜  ~ ll /etc/alternatives/java
lrwxrwxrwx 1 root root 43  6월  9 22:20 /etc/alternatives/java -> /usr/lib/jvm/java-12-openjdk-amd64/bin/java

readlink -f /usr/bin/java 명령으로도 동일한 결과를 볼 수 있습니다.

이 링크는 update-alternatives 로 관리됩니다. 아래와 같은 명령으로 현재 설치된 버전들과 우선 순위를 확인할 수 있습니다.

sudo update-alternatives --display java

수동으로 다운로드 압축을 풀어서 설치하거나 SDKMAN, Jabba등으로 설치한 JDK가 있다면 아래 명령으로 update-alternatives 의 관리대상에 추가할 수 있습니다.

sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/jdk1.8.0_31/bin/java 1000

심볼릭 링크로 연결되는 버전을 바꾸고 싶다면 아래와 같이 입력합니다.

sudo update-alternatives --config java

설치된 버전을 확인하고 번호를 선택해서 심볼릭 링크를 바꿀 수 있습니다.

There are 4 choices for the alternative java (providing /usr/bin/java).

  Selection    Path                                            Priority   Status
------------------------------------------------------------
* 0            /usr/lib/jvm/java-12-openjdk-amd64/bin/java      1211      auto mode
  1            /usr/lib/jvm/java-11-openjdk-amd64/bin/java      1111      manual mode
  2            /usr/lib/jvm/java-12-openjdk-amd64/bin/java      1211      manual mode
  3            /usr/lib/jvm/java-13-openjdk-amd64/bin/java      1211      manual mode
  4            /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java   1081      manual mode

Press <enter> to keep the current choice[*], or type selection number:

그런데 명령행에서 실행한 java 가 어느 곳으로 연결될지는 환경변수 PATH 에 영향을 받습니다. /usr/bin/java 보다 더 우선 순위가 높게 먼저 선언된 디렉토리에 java`가 있다면 `update-alternatives 에서 지정한 java가 실행되지 않을 수도 있습니다. SDKMAN, Jabba 등을 함께 사용한다면 이 점을 유의해야 합니다. 현재 쉘, 디렉토리에서 어느 java 를 실행하고 있는지는 which java 로 확인할 수 있습니다.

  • 장점

    • OS에서 기본적으로 제공하는 도구라서 별도의 설치 과정이 필요하지 않습니다.

    • YUM/APT 과 자연스럽게 함께 쓰이는 도구입니다.

  • 단점

    • 심블릭 링크로 쉘에서 사용할 디폴트 버전을 지정하는 기능만 있습니다.

2.3. Homebrew

macOS에서 많이 쓰는 범용 패키지 관리 프로그램입니다.

Homebrew로 AdoptOpen JDK배포판은 아래와 같이 설치할 수 있습니다.

brew tap AdoptOpenJDK/openjdk
brew cask install adoptopenjdk11

제가 macOS를 써본적이 없어서 Homebrew로 설치하는 방법에 대해서는 homebrew로 opendjk 설치하기 글을 참조했습니다.

2.4. Chocolatey

Chocolatey는 Windows OS를 위한 패키지 관리자입니다. Linux에는 APT/YUM, macOS에는 Homebrew가 있다면 Windows에는 Chocolatey가 대표적인 패키지 관리자입니다. https://chocolatey.org/install 을 참고해서 설치할수 있습니다.

Chocolatey로 설치가능한 JDK 패키지는 https://chocolatey.org/packages?q=jdk 으로 확인하실 수 있습니다.

:chocolatey-jdk.jpg

Oracle의 OpenJDK 빌드나 Adopt OpenJDK 배포판 등을 아래와 같이 설치할 수 있습니다.

Oracle의 OpenJDK 빌드 최신 버전 설치
choco install openjdk
AdoptOpenJDK 최신 버전 설치
choco install adoptopenjdk
Corretto 11 버전 설치
choco install corretto11jdk
zulu 최신 버전 설치
choco install zulu

--version 옵션을 붙이면

Oracle의 OpenJDK 빌드 11.0.2 버전 설치
choco install openjdk --version 11.0.2

위의 명령이 수행되고 나면 \Program Files\ 디렉토리 아래에 JDK 들이 위치하게 됩니다.

  • Oracle의 OpenJDK 빌드 : \Program Files\OpenJDK

  • Adopt OpenJDK : \Program Files\AdoptOpenJDK

  • Corretto : \Program Files\Coretto

  • Zulu : \Program Files\zulu

그런대 Chocolatey는 여러 JDK 버전을 동시에 쓰는 쓰임새가 우선적으로 고려되지는 않았습니다. JDK 12.0.1을 설치후에 11.0.2을 뒤에 설치하려고하면, 다운그레이드가 된다는 경고 메시지가 나옵니다. 이럴 때에는 '-sidebyside' 혹은 --force 등의 옵션을 붙여줘야합니다. JAVA_HOME 도 마지막으로 설치한 JDK의 위치로 지정됩니다. echo %java_home% 명령으로 이를 확인해 볼 수 있습니다. 여러 배포판을 설치할 경우 PATH 환경 변수의 값도 새로 설치한 배포판의 %JAVA_HOME%\bin 디렉토리가 뒤 쪽에 계속 추가만 됩니다.

  • 장점

    • JDK 외에도 Maven, Gradle의 설치에도 활용할 수 있는 범용적인 패키지 관리 도구입니다.

  • 단점

    • SDKMAN/ Jabba에 비하면 다양한 JDK 배포판을 제공하지는 않습니다.

    • 여러 버전을 동시에 설치할 수 있는 동작이 디폴트가 아닙니다.

      • OS 명령행에서 여러 JDK 버전을 함께 사용하려면 direnv등 별도의 프로그램과 함께 쓰는 것이 좋습니다.

2.5. direnv

direnv 는 특정 디렉토리와 그 하위 디렉토리에서만 사용할 환경 변수를 지정할 수 있는 도구입니다. Linux와 macOS에서 사용할 수 있습니다. 설치 방법은 https://direnv.net/ 을 참조합니다.

direnv에서 참조하는 .envrc 라는 파일에 PATH, JAVA_HOME 을 아래와 같이 지정할 수 있습니다.

export JAVA_HOME=/home/benelog/.sdkman/candidates/java/12.0.1.hs-adpt
export PATH=$JAVA_HOME/bin:$PATH

파일을 처음 생성하거나 변경했을 때에는 direnv allow . 명령을 한번 내려줘야합니다. 이 파일이 의도하지 않게 생성/수정 되었을 때 보안을 위한 장치입니다.

이후로 이 파일이 있는 디렉토리에 들어가면 이 환경변수가 활성화됩니다. cd 명령으로 디렉토리에 들어가면 아래와 같은 메시지가 콘솔에 보입니다.

direnv: loading .envrc
direnv: export ~JAVA_HOME ~PATH

보편적으로 사용할 수 있는 도구이기에 JAVA_HOME 외의 다른 환경 변수도 관리할 수 있습니다. 같은 프로젝트를 하더라도 개발자의 PC마다 달라지는 값이나 테스트를 위한 변수도 .envrc 에 넣어둘만합니다. 그럴 경우에는 .envrc.gitignore 에 추가해서 Git 저장소에는 들어가지 않도록 해야 하겠습니다.

  • 장점

    • JAVA_HOME 이나 PATH 외의 환경 변수도 관리할 수 있습니다.

  • 단점

    • 특정 디렉토리 내에서의 환경 변수 기능만 제공합니다.

2.6. jEnv

jEnv 는 JDK 버전관리만을 위한 전용 도구입니다.

아래와 같이 add 명령으로 관리할 버전을 추가합니다.

jenv add /usr/lib/jvm/java-11-openjdk-amd64/

add 로 지정한 디렉토리에서 JDK의 버전을 인식하여 아래와 같은 메시지가 나옵니다.

jenv add 명령의 결과
openjdk64-11.0.3 added
11.0.3 added
11.0 added

설치된 버전은 jenv versions 명령으로 확인할 수 있습니다.

jenv versions 명령의 결과
  system
  1.8
  1.8.0.212
* 11.0 (set by JENV_VERSION environment variable)
  11.0.3
  openjdk64-1.8.0.212
  openjdk64-11.0.3

디폴트로 사용할 버전은 global 명령으로 지정합니다.

jenv global 11.0

해당 쉘에서 임시로 사용할 버전은 shell 명령으로 지정합니다.

jenv shell 11.0

현재 디렉토리에서 사용할 버전은 local 명령으로 지정합니다.

jenv local 11.0

위와 같이 디렉토리에 지정된 버전은 .java-version 이라는 파일에 저장됩니다. 다음 번에 같은 디렉토리에서 java를 실행하면 이 파일에 지정된 해당 버전이 선택됩니다.

JAVA_HOME 환경 변수가 제대로 지정되기 위해서는 jENV의 export plugin을 아래 명령으로 활성화해줘야 합니다.

jenv enable-plugin export

jEnv를 다른 도구와 잘 어우러지게 사용하기 위해서는 동작 원리를 알아두는 것이 좋습니다. jEnv로 JDK 버전을 지정한 후 which java 로 어느 디렉토리에 있는 java 와 연결되는지 확인을 해보면 ~/.jenv/shims/java 가 나옵니다. 이 파일의 내용을 보면 실제 설치한 JDK의 java 가 아닌 쉘 스크립트라는 것을 알수 있습니다.

cat ~/.jenv/shims/java 명령의 결과
#!/usr/bin/env bash
set -e
[ -n "$JENV_DEBUG" ] && set -x

program="${0##*/}"
if [ "$program" = "java" ]; then
  for arg; do
    case "$arg" in
    -e* | -- ) break ;;
    */* )
      if [ -f "$arg" ]; then
        export JENV_DIR="${arg%/*}"
        break
      fi
      ;;
    esac
  done
fi

export JENV_ROOT="/root/.jenv"
exec "/root/.jenv/libexec/jenv" exec "$program" "$@"

따라서 다른 도구와 병행해서 사용할 경우, 환경변수 $PATH`에 `~/.jenv/shims/java`가 다른 도구에서 넣어준 JDK와 연결된 경로들보다 앞에 있어야 jEnv에서 설정한 버전대로 `java 가 실행됩니다.

$JAVA_HOME`도 어떻게 지정되어 있는지 `echo $JAVA_HOME 로 확인을 해보면 ~/.jenv/versions/11.0 와 같이 지정되어 있습니다. `~/.jenv/versions/ 디렉토리에 각 버전별로 실제로 JDK가 설처되어있는 디렉토리로의 심볼릭 링크가 들어가 있습니다.

~/.jenv/versions 디렉토리 안의 심볼릭 링크
lrwxrwxrwx  1 benelog benelog   33 Jun 30 17:05 1.8 -> /usr/lib/jvm/java-8-openjdk-amd64/
lrwxrwxrwx  1 benelog benelog   33 Jun 30 17:05 1.8.0.212 -> /usr/lib/jvm/java-8-openjdk-amd64/
lrwxrwxrwx  1 benelog benelog   34 Jun 30 17:08 11.0 -> /usr/lib/jvm/java-11-openjdk-amd64/
lrwxrwxrwx  1 benelog benelog   34 Jun 30 17:08 11.0.3 -> /usr/lib/jvm/java-11-openjdk-amd64/
lrwxrwxrwx  1 benelog benelog   33 Jun 30 17:05 openjdk64-1.8.0.212 -> /usr/lib/jvm/java-8-openjdk-amd64/
lrwxrwxrwx  1 benelog benelog   34 Jun 30 17:08 openjdk64-11.0.3 -> /usr/lib/jvm/java-11-openjdk-amd64/

그런데 jEnv는 여러 배포판을 동시에 설치할 때는 충돌을 일으킬수 있습니다. 예를 들어 Ubuntu 패키지 저장소의 OpenJDK 11을 이미 'jenv add' 로 넣은 다음, AdoptOpenJDK 11을 추가하면 아래와 같이 이미 존재하는 버전이라는 메시지가 나옵니다.

`jenv add /usr/lib/jvm/adoptopenjdk-11-jdk-hotspot 실행결과
 openjdk64-11.0.3 already present, skip installation
 11.0.3 already present, skip installation
 11.0 already present, skip installation

jEnv는 동일한 JDK 배포판의 여러 버전을 관리하는데 적합합니다.

  • 장점

    • 다양한 범위(디폴트(global), 디렉토리별, 쉘 범위)의 버전 방식을 지원합니다.

  • 단점

    • 다양한 배포판의 동일한 JDK 버전(예: 11.0.3)을 관리할 수 없습니다.

2.7. SDKMAN

SDKMAN(The Software Development Kit Manager)은 여러 개발도구를 설치할 수 있는 도구입니다. JDK 뿐만 아니라 Maven, Gradle, Ant, AsciidoctorJ 등 JVM 세계의 다양한 도구들을 설치할 수 있습니다.

OS별로 SDKMAN을 설치하는 방법은 https://sdkman.io/install 을 참조합니다.

SDKMAN으로 설치할 수 있는 JDK 배포판/버전은 sdk list java 명령으로 확인할 수 있습니다. 아래와 같이 사용할 수 있는 배포판들과 설치된 버전 등을 표시해 줍니다.

================================================================================
Available Java Versions
================================================================================
 Vendor        | Use | Version      | Dist    | Status     | Identifier
--------------------------------------------------------------------------------
 AdoptOpenJDK  |     | 12.0.1.j9    | adpt    |            | 12.0.1.j9-adpt
               |     | 12.0.1.hs    | adpt    | installed  | 12.0.1.hs-adpt
               |     | 11.0.3.j9    | adpt    |            | 11.0.3.j9-adpt
               |     | 11.0.3.hs    | adpt    |            | 11.0.3.hs-adpt
               |     | 8.0.212.j9   | adpt    |            | 8.0.212.j9-adpt
               | >>> | 8.0.212.hs   | adpt    | installed  | 8.0.212.hs-adpt
 Amazon        |     | 11.0.3       | amzn    |            | 11.0.3-amzn
               |     | 8.0.212      | amzn    |            | 8.0.212-amzn
 Azul Zulu     |     | 12.0.1       | zulu    |            | 12.0.1-zulu
               |     | 11.0.3       | zulu    |            | 11.0.3-zulu
               |     | 10.0.2       | zulu    |            | 10.0.2-zulu
               |     | 9.0.7        | zulu    |            | 9.0.7-zulu
               |     | 8.0.212      | zulu    |            | 8.0.212-zulu
               |     | 7.0.222      | zulu    |            | 7.0.222-zulu
               |     | 6.0.119      | zulu    |            | 6.0.119-zulu
 Azul ZuluFX   |     | 11.0.2       | zulufx  |            | 11.0.2-zulufx
               |     | 8.0.202      | zulufx  |            | 8.0.202-zulufx
 BellSoft      |     | 12.0.1       | librca  |            | 12.0.1-librca
               |     | 11.0.3       | librca  |            | 11.0.3-librca
               |     | 8.0.212      | librca  |            | 8.0.212-librca
 GraalVM       |     | 19.0.2       | grl     |            | 19.0.2-grl
               |     | 19.0.0       | grl     |            | 19.0.0-grl
               |     | 1.0.0        | grl     | installed  | 1.0.0-rc-16-grl
 SAP           |     | 12.0.1       | sapmchn |            | 12.0.1-sapmchn
               |     | 11.0.3       | sapmchn |            | 11.0.3-sapmchn
 java.net      |     | 14.ea.1      | open    |            | 14.ea.1-open
               |     | 13.ea.25     | open    |            | 13.ea.25-open
               |     | 12.0.1       | open    |            | 12.0.1-open
               |     | 11.0.2       | open    |            | 11.0.2-open
               |     | 10.0.2       | open    |            | 10.0.2-open
               |     | 9.0.4        | open    |            | 9.0.4-open
================================================================================
Use the Identifier for installation:

    $ sdk install java 11.0.3.hs-adpt
================================================================================

AdoptOpenJDK HotSpot 배포판 12.0.1 버전을 설치하고 싶다면 아래와 같은 명령을 내립니다.

sdk install java 12.0.1.hs-adpt

PATH , JAVA_HOME 환경변수도 알아서 잘 잡아줍니다.

명령행에서 디폴트로 사용할 JDK 버전은 ~/.sdkman/candidates/java/current 에서 심볼릭 링크로 관리됩니다. 이 링크가 환경변수 $PATH`와 `$JAVA_HOME 에 추가 됩니다.

이 심볼릭 링크는 아래 명령으로 바꿀 수 있습니다.

sdk default java 8.0.212.hs-adpt

현재 쉘에서 사용할 버전만 임시로 바꾸고 싶다면 default 대신 use 명령을 씁니다.

sdk use java 8.0.212.hs-adpt
  • 장점

    • 다양한 JDK 배포판을 설치할 수 있습니다.

    • JDK 설치와 버전 지정을 하나의 도구로 관리할 수 있습니다.

  • 단점

    • 특정 디렉토리에 들어갔을 때 사용할 버전을 자동을 지정하는 기능이 없습니다.

    • sdk use 명령이 jabba의 동일한 기능에 비해 실행 속도가 느립니다.

2.8. jabba

jabba는 JDK의 설치/버전 관리만을 위한 도구입니다.

각 OS별 jabba의 설치 방법은 https://github.com/Jabba-Team/jabba#installation 을 참조합니다.

설치할 수 있는 JDK의 배포판은 jabba ls-remote 명령으로 확인할 수 있습니다. 이중 Amazon에서 제공하는 Corretto 배포판 JDK 11을 설치한다면 아래와 같은 명령을 내립니다.

jabba install amazon-corretto@1.11.0-3.7.1

설치된 버전들은 jabba ls 명령으로 확인할 수 있습니다. 현재 쉘에서 사용할 버전은 아래와 같이 지정할 수 있습니다.

jabba use adopt-openj9@1.12.33-0

jabba use 를 실행하면 PATHJAVA_HOME 환경변수를 지정한 JDK 버전을 참조할수 있도록 바꾸어줍니다. echo $PATH 로 PATH 값을 확인해보면, 가장 앞에 설치한 JDK의 bin 디렉토리를 지정할 것을 확인할 수 있습니다.

같은 디렉토리에 `.jabbarc`라는 파일이 있다면, 그 파일에 지정된 버전을 참조할 수 있습니다. 즉 아래와 같이 실행해도 특정 버전을 지정할 수 있습니다.

echo "adopt-openj9@1.12.33-0" > .jabbarc
jabba use

다음 번에 같은 디렉토리에 들어왔을 떄에는 jabba use 만 간단하게 실행해서 같은 효과를 낼 수 있습니다. direnv나 jEnv를 쓸 때처럼 디렉토리에 들어가면 자동으로 환경변수를 바꾸어주는 기능은 없습니다.

현재 쉘범위의 JDK 버전만 지정한다는 점이 jabba의 장점이나 단점입니다.

  • 장점

    • 다른 도구와 충돌없이 쓰기에 좋습니다.

    • jabba use 명령이 SDKMAN의 sdk use 에 비해 실행 속도가 빠릅니다.

  • 단점

    • 디폴트 버전 지정이 없습니다.

    • 디렉토리별 버전 비전 기능이 완전 자동이 아닙니다. 해당 디렉토리에서 jabba use 를 한번 입력해야 합니다.

3. 무엇을 어떻게 사용할 것인가?

위의 다양한 도구 중 어떤 것을 골라 쓸지는 개발장비의 OS와 필요한 범위에 따라서 결정해야할 것입니다.

우선 다양한 배포판의 JDK를 쓰는 것까지 필요가 없다면 아래 정도의 조합을 고려할만합니다.

  • Windows : Chocolatey + direnv

  • Linux : APT/YUM + update-alternatives + jEnv (또는 direnv)

  • macOS : Homebrew + jEnv(또는 direnv)

    • [Mac에 Java 여러 버전 설치] 글에서는 Homebrew로 Oracle JDK를 설치하고 jEnv와 함께 사용하는 사례가 정리되어 있습니다.

Amazon Corretto, GraalVM 등 다양한 배포판의 여러버전을 설치해보고 싶다면 SDKMAN이나 jabba를 함꼐 쓰는 것을 추천합니다. 각 도구들이 지원하는 배포판은 아래와 같습니다. (2019년 7월1일 기준)

Table 2. JDK 설치 도구들이 지원하는 배포판
이름 지원하는 JDK 배포판

YUM/APT

OS 배포판별 OpenJDK (*1)
AdoptOpen JDK

Homebrew

Oracle JDK
Adopt OpenJDK

Chocolatey

Oracle JDK
Oracle의 OpenJDK 빌드 (*2)
Adopt OpenJDK
Amazon Corretto
Zulu OpenJDK

SDKMAN

Oracle의 OpenJDK 빌드 (*2)
Adopt OpenJDK
Amazon Corretto GraalVM CE
Zulu OpenJDK
Zulu OpenJDK + OpenJFX
SapMachine
Liberica JDK

jabba

Oracle JDK
Oracle의 OpenJDK 빌드 (*2)
Adopt OpenJDK
Amazon Corretto
GraalVM CE
Zulu OpenJDK
IBM SDK
OpenJDK 참조 구현체
OpenJDK + Shenandoah GC
Liberica JDK

  • (*1) : 해당 OS 배포판을 위해 빌드된 OpenJDK 배포판입니다. OS의 배포판을 관리하는 업체/커뮤니티에서 관리합니다.

  • (*2) : https://jdk.java.net/ 에서 다운로드 받을 수 있는 OpenJDK 배포판입니다. 출시 후 6개월까지만 최신 버전이 업데이트됩니다.

위에 정리한 것처럼 SDKMAN과 jabba가 많은 JDK 배포판을 지원합니다. 둘다 Adopt OpenJDK, Amazon Corretto, GraalVM CE, Zulu 등 주목받는 주요 배포판은 모두 포함하고 있습니다.

SDKMAN에서는 제공하는 반면 jabba에는 없는 배포판은 아래와 같습니다.

  • Zulu OpenJDK + OpenJFX

  • SapMachine

jabba에서는 제공하는 반면 SDKMAN에는 없는 배포판은 아래와 같습니다.

SDKMAN과 jabba는 JDK 설치와 버전 지정 기능을 동시에 제공합니다. 그런데 jenv등 다른 도구에서 제공하는 버전 지정 기능을 완정히 제공하지는 않습니다.

Table 3. JDK 버전 지정 기능
도구 디폴트 디렉토리별 쉘 범위

update-alternatives/ alternatives

O

X

X

SDKMAN

O

X

O

jabba

X

(*3)

O

jEnv

O

O

O

direnv

X

O

X

  • (*3) : jEnv나 direnv처럼 디렉토리에 들어가면 자동으로 특정 JDK 버전이 선택되는 방식은 아니기 때문에 △로 표기했습니다.

따라서 SDKMAN이나 jabba는 다른 도구와 조합해서 사용하면 더욱 편리하게 쓸 수 있습니다. 그런데 앞서 언급했듯이 jEnv는 SDKMAN이나 jabba와 함께 쓰기에는 적합하지 않습니다. $PATH 환경 변수에 지정된 경로의 순서에 따라서 여러 도구의 버전 지정 결과가 의도하지 않게 덮어 써질수 있습니다. 즉 SDKMAN에 지정한 경로가 앞에 있으면 jEnv에서 지정한 JDK 버전이 인식되지 않는 것처럼 보일수도 있습니다. 그리고 jabba로는 여러 배포판의 JDK 11.0.3 을 설치할 수 있지만 jEnv에서는 'jenv add' 로 같은 버전(11.0.3)의 다른 배포판을 추가할 수 없습니다.

따라서 다양한 배포판을 설치하고자 할때는 SDKMAN(또는 jabba) + direnv 조합을 추천합니다.

제가 이 도구들을 쓰는 환경은 아래와 같습니다.

  • 각각 다른 JDK 버전을 쓰는 여러 프로젝트의 소스를 고칩니다.

  • 업무 혹은 취미로 JDK의 여러 배포판/ 버전을 설치해서 차이가 있는지 확인하고 있습니다.

    • (예: 포함된 ca-cert 목록 비교, GraalVM으로 네이티브 이미지 만들기 시도)

  • 회사의 업무용 노트북과 집에 있는 PC에서 Ubuntu 19.04를 씁니다.

이에 따라 저는 아래와 같이 도구를 조합해서 쓰고 있습니다.

  • JDK 설치에는 APT, SDKMAN, jabba를 다 사용해 보고 있습니다.

  • 사용할 버전을 선택할 때는

    • 디폴트 버전은 SDKMAN으로 지정합니다.

      • SDKMAN을 설치하면 SDK에서 관리하는 패키지들이 /usr/bin 보다 앞에 오기 때문입니다. 디폴트 버전은 자주 바꾸진 않기 때문에 굳이 이를 조정하진 않았습니다.

    • 특정 디렉토리에서 사용한 버전을 지정할 때는 direnv를 씁니다.

    • 쉘에서 일시적으로 사용할 버전을 지정할 때는 SDKMAN, jabba를 씁니다.

정적 사이트로 블로그 마이그레이션

이 블로그를 마이그레이션 했던 과정에서 얻은 경험을 정리했습니다. Github와 AsiiDoc, JBake, Gradle, Netflify 등의 기술을 이용했습니다.

정상혁정상혁

1. 블로그를 이전한 동기

저는 이 블로그를 Egloos에서 커스텀 URL(blog.benelog.net)을 설정하여 쓰고 있었습니다. Egloos는 무난한 블로깅 플랫폼입니다. 그러나 개발과 관련된 글을 쓰면서 코드를 편집하기에는 저한테는 Egloos에서 제공하는 에디터가 잘 맞지가 않았습니다. 점점 글을 안 쓰게 되었고, 이 블로그는 갱신되지 않는 옛날 글만 가득하게 되었습니다. 공유할 글이 생길 때는 https://gist.github.com/ 를 이용하기도 했습니다.

이전처럼 가끔씩이라도 블로그에 글을 올리려면 블로그 플랫폼을 바꿔야겠다는 생각이 들었습니다.

2. 요구사항

블로그 플랫폼을 선택하는데 염두에 둔 조건은 아래와 같았습니다.

  • 기존의 포스트의 주소가 동일하게 유지되어야 한다.

    • 외부에서 걸린 링크가 깨어지지 않아야한다.

  • 기존 블로그에 달린 댓글을 그대로 옮겨올 수 있어야 한다.

  • 다른 서비스/도구로 다시 옮겨가기가 쉬워야한다.

  • AsciiDoc으로 포스트를 작성할 수 있어야한다.

3. 선택한 기술

앞의 요구사항을 충족시키는 다음과 같은 서비스/도구들을 선택했습니다.

새로운 블로그는 정적 사이트 생성기(Static Site Generator)로 Git 저장소에 있는 AsciiDoc 형식의 컨텐츠를 HTML 파일로 만들어서 배포하는 구조입니다. 덕분에 Egloos로부터의 마이그레이션 작업이 더 수월했고 더 좋은 서비스가 있을 때 쉽게 이전을 할 수 있습니다.

컨텐츠 저장소는 GitHub외에도 Gitlab, Bitbucket 등의 대안이 있고, 컨텐츠 빌드 배포 플랫폼도 다양합니다. 필요하다면 더 좋은 기능이 있는 서비스를 유료로 사용할 수도 있습니다. 컨텐츠 편집기도 여러가지를 사용할 수 있습니다.

사용할 기술을 선택하면서 했던 고민들은 아래에 정리했습니다.

3.1. 정적 사이트 생성기

정적 사이트 생성기로 JBake를 선택한 이유는 다음과 같습니다.

  • 필수요구사항이였던 AsciiDoc 지원

  • JVM 기반

    • 일할 때도 쓰는 환경이라서 문제를 만났을 때 해결하는 시간이 짧고, 직접 고쳐서 쓸 수 있는 가능성도 높습니다.

    • Gradle 빌드 구성 등 이미 알고 있는 지식과 결합시키기에도 편했습니다.

  • JBake로 만들어진 템플릿인 jbake-future-imperfect-template의 편의성

    • Disqus와 Google Analytics를 설정파일을 1줄씩을 고쳐서 간단히 적용할 수 있습니다.

비슷한 고민을 하실 분들이 참고할만한 링크들을 정리해봅니다.

미래에 JBake를 다른 도구로 교체한다면 아래 2개의 도구를 가장 우선적으로 고려할 예정입니다.

3.2. Git 저장소, 빌드/배포

Git 저장소는 많이 쓰이는 GitHub를 무난하게 선택했습니다. AsciiDoc으로 작성된 포스트의 원본은 github/benelog/blog/src/content 에서 이력을 관리합니다.

처음에는 Travis CI와 Githb Page를 활용해서 빌드/배포를 구성하고 커스텀 도메인과 HTTPS를 적용하려고 했었습니다. 그래서 이 블로그는 https://benelog.github.io/blog 로도 접근이 가능합니다.

빌드 스크립트는 build.gradle에서 확인이 가능합니다. Travis CI 설정인 .travis.yml은 아래와 같이 단순합니다.

language: java
script: ./gradlew gitPublishPush

이 작업을 실행하기 위해서는 https://github.com/settings/tokens 에서 발급받은 Personal Access Token을 Tracis CI의 저장소별 설정에서 GRGIT_USER 라는 환경변수로 지정을 해주어야 합니다.

travis-ci-settings

최종적으로는 지원하는 기능이 더 풍부한 Netflify을 이 도메인에는 사용했습니다.

Gradle로 만든 빌드 스크립트를 지정하고 Domain을 지정하고 HTTPS를 적용하는 작업까지 편하게 할 수 있었습니다. Netflify에는 Name 서버 기능까지 있어서 사용하던 Name 서버도 이 서비스로 이전을 했습니다.

netflify

Netflify에서는 ./gradlew bake 로 빌드를 합니다.

netflify

이 블로그의 JBake 설정으로는 https://blog.benelog.net/904735.html 와 같이 HTML 확장자를 붙인 주소를 씁니다. Egloos의 이전 포스트들은 https://blog.benelog.net/904735 와 같이 HTML 확정자가 없게 되어 있습니다. Netflify와 Github pages에서는 디폴트로 아무런 설정이 없어도 HTML 확장자가 없는 주소를 HTML 확장자가 붙은 주소와 동일하게 취급을 해주어서 URL의 하위 호환성을 쉽게 유지할 수 있었습니다. JBake의 설정으로 HTML 확장자를 제외하는 것도 시도해보았지만, 태그 링크 같은 블로그의 다른 요소의 링크들과 함께 잘 어우러지는 것이 쉽지 않았습니다. 호스팅 서비스 단에서 연결해 해주는 방법이 더 간편하고 부작용이 없어서 최종적으로 그에 의지하도록 했습니다. 즉, 기존 주소 연결을 위해서 아무런 설정도 안 했다는 이야기입니다.

정적 페이지를 호스팅할수 있는 다른 서비스를 찾는다면 아래의 링크들을 참조하실 수 있습니다.

3.3. 편집기

긴 글을 편집할 때는 IntelliJ Community Edition을 사용했습니다. 평소 개발할 때의 편집방식, 단축키와 Git 저장소 연동 방식을 그대로 활용할 수 있습니다. Find in Path (Ctrl + Shift + F) , Replace in Path (Ctrl + Shift + R) 으로 여러 파일에 걸쳐 문자열을 검색하고 치환하는 기능은 마이그리에션 된 컨텐츠를 정리할 때 많은 도움이 되었습니다.

AsciiDoc plugin을 설치하면 HTML으로 렌더링될 모습을 동시에 보면서 편집을 할 수 있고 이미지 파일을 참조할 때는 자동완성이 되기도합니다.

IntelliJ로 AsciiDoc 편집

간단한 글을 쓰거나 수정할 때는 Github에서 바로 온라인으로 편집을 할 생각입니다.

참고로 정적 사이트 생성기를 기반으로 온라인 에디터를 제공하는 서비스들은 아래와 같은 것들이 있습니다.

3.4. 마이그리에션 프로그램

마이그리에션 프로그램의 소스와 사용법은 https://github.com/benelog/egloos-migration/ 을 참고하시기 바랍니다. 이것도 제가 알고 있는 기술들을 활용해서 Kotlin + Spring 으로 작성했습니다.

컨텐츠를 담은 파일을 생성할 때 Kotlin의 문자열 문법이 유용할 것으로 예상해서 언어는 Kotlin을 선택했습니다. 아래와 같은 코드입니다.

    private fun convert(post: EgloosPost): String {
        val asciiDoc = converter.convert(post.content)
        return """= ${post.title}
${post.nick}
${post.createdAt.format(formatter)}
:jbake-type: post
:jbake-status: published
:jbake-tags: ${post.tags}
:idprefix:

$asciiDoc
"""
        // String을 inputStream으로 읽어서쓰니 trimIndent가 안 먹음.
    }
}

그리고 프로그램을 부분적으로 실행하고 검증해보기 위해서 Spring Batch를 썼습니다. 원래 Spring Batch는 대용량 데이터를 메모리에 다 올리지 않고 처리할 때 적합한 프레임워크입니다. 제 블로그의 포스트는 100개 정도로 작은 데이터인데 여기에 Spring Batch를 쓰는 것은 어울리지 않을 수도 있습니다.

그런데, Spring Batch의 ItemReader, ItemProcessor, ItemWriter 구조에 맞춰서 프로그램을 만들고 이 기능 단위로 테스트를 해보면 것이 마이그레이션 프로그램을 만드는 효율적인 방법이라고 판단했습니다. Spring Batch를 쓰지 않았어도 비슷한 기능 단위로 프로그램을 쪼갯을 것 같았기에 이미 익숙한 프레임워크의 인터페이스를 활용했습니다.

부분적인 기능의 테스트들은 https://github.com/benelog/egloos-migration/tree/master/src/test/kotlin/net/benelog/blog/migration/etl 을 참조하실 수 있습니다.

4. 마치며

오랜 숙원 사업이였던 블로그 이전을 마쳐서 뿌듯합니다. 다른 개발자들에게 도움이 될만한 글을 가끔씩이라도 써보겠습니다. 이전에 썼던 글들도 틈틈히 최신화하려고 합니다. 할 작업들은 https://github.com/benelog/blog/issues 에 기록해두고 있습니다.

Robolectric을 활용한 안드로이드 쾌속 테스팅

Android 테스팅 프레임워크 Robolectric에 대한 소개

정상혁정상혁
  • 2014년 8월1일 제9회 오픈 세미나 in 대구 행사에서 한 발표입니다.

  • 예제 코드들은 당시의 Rolbectric의 최신 버전을 기준으로 한 것이라 현재의 최신 버전에서는 그대로 실행되지 않을 수 있습니다.

오늘 발표에서는 테스트 프레임워크인 Robolectric을 사용하면서 실무에서 얻었던 경험을 공유하고자합니다.

먼저 테스트코드란 무엇인지와 안드로이드에서 테스트 작성을 어렵게 하는 난관등을 말씀드리고 Robolectric을 활용하는 방법을 소개하겠습니다.

테스트 코드란?

참석하신 분 중에서 JUnit(제이유닛)에 대해서 한번이라도 들어보신 분은 손을 들어보시겠습니까? 이중에 Junit을 실제로 써보신분은 얼마나 되시나요? Android에서 JUnit으로 테스트를 시도해보신 분은 계신가요? 경험을 하신 정도가 다양하기 때문에 우선 오늘 다룰 테스트 코드란 무엇인지를 한번 정리하고 시작을 하겠습니다.

단순히 말해서 테스트 코드는 '검증’을 위한 코드입니다. 다음은 이미지로딩 라이브러리인 Universal ImageLoader의 소스 중 테스트 코드입니다. github에서 전체소스를 확인하실 수 있습니다.

@Test
public void testSchemeFile() throws Exception {
    String uri = "file://path/on/the/device/1.png";
    Scheme result = Scheme.ofUri(uri);
    Scheme expected = Scheme.FILE;
    assertThat(result).isEqualTo(expected);
}

@Test
public void testSchemeUnknown() throws Exception {
    String uri = "other://image.com/1.png";
    Scheme result = Scheme.ofUri(uri);
    Scheme expected = Scheme.UNKNOWN;
    assertThat(result).isEqualTo(expected);
}

( Android Universal Image Loader의 BaseImageDownloaderTest )

골뱅이 Test가 붙은 애노테이션으로 실행될 코드를 지정하고 assert 구문으로 기대하는 결과를 명시합니다. 이 코드는 uri를 파싱해서 Scheme (스킴)이라는 Enum(이늄)클래스를 만든 결과가 기대대로 FILE이나 UNKNOWN인지를 확인하고 있습니다. 이 코드는 오늘 이야기할 Robolectric으로 만들어졌습니다.

테스트를 도와주는 프레임워크도 굉장히 많습니다. 앞에서도 언급드렷듯이, 테스트를 실행하는데는 JUnit이 가장 많이 쓰입니다. 안드로이드 SDK에서도 이를 바탕으로 한 테스트 프레임워크를 제공하고 있습니다. 그리고 테스트에 쓰이는 가짜 객체를 흔히 목(Mock)이라고 부르는데, Mockito, JMock, PowerMOck과 같은 라이브러리들이 있습니다. 안드로이드를 위한 테스트 프레임워크에도 오늘 다룰 Robolectric을 비롯해 Robotium, Spoon, Robospock 등이 존재합니다.

오늘 설명을 이어가는데 혼동을 줄이기 위해 유의해야할 개념을 몇가지 말씀드리겠습니다.

첫째, JUnit으로 하는 테스트라고 전부 유닛테스트는 아니라는 것입니다. JUnit에 유닛(Unit)이라는 이름이 들어가서 생기는 혼동입니다. 테스트 Functional 테스트 (혹은 시스템 테스트)도 JUnit으로 작성하는 경우도 많습니다. 오늘 발표에서는 이를 엄밀히 구분하지는 않고 폭넓게 '테스트 코드’라는 말로만 칭하겠습니다.

둘째, 테스트 코드를 작성하는 작업을 'TDD를 한다’라고 한마디로 말하기는 어렵다는 것입니다. TDD는 테스트를 작성하는 하나의 방식입니다. TDD 기법은 테스트를 먼저 작성하고, 테스트를 통과시키는 코드를 구현한 후 리팩토링을 하는 절차를 거칩니다. 오늘 발표에서는 TDD 같은 절차와 상관없이 테스트를 작성하는 라이브러리 활용법에 대해서 주로 말씀드리겠습니다.

왜 이런 테스트를 만드는지에 대한 의문을 가지실분도 있을 듯합니다. 간단히 설명드리면, 그 이유는 다음과 같습니다.

첫째, 디버깅 편의성을 위해서입니다. 테스트 코드 작성에 능숙해지면 실제 어플리케이션을 실행하고 수동으로 반복 테스트하는것보다 훨씬 빠르고 정교하게 내가 짠 코드의 동작을 확인하고 오류를 수정할 수 있습니다.

둘째, 설계를 개선하기 위해서입니다. 테스트 하기에 쉬운 구조의 코드는 역할과 책임이 잘 나누어진 코드입니다. 그런 코드는 재활용하고 기능을 추가하거나 버그를 발견하기에도 편합니다. 테스트를 의식하면서 개발을 하면 그런 구조의 코드를 작성하는데 도움이 됩니다.

셋째, 테스트 자체가 동작하는 예제이자 명세가 됩니다. 다른 사람이 어플리케이션이나 라이브러리의 전체를 실행시키지 않아도 코드가 실행된 결과를 이해할 수 있습니다.

넷째, 반복적으로 수행할 회귀테스트를 자동화합니다. 앞으로 기능을 추가하거나 코드를 개선할 때 든든한 버팀목이 되고 시간을 아껴줍니다.

다섯째, 개발 작업에 더 집중하게 해줍니다. 테스트를 통과한다는 명확한 목표가 있고, 이를 빠른 시점에 명확하고 신호로 알려주고 작업의 난이도와 간격은 스스로 적당하게 조절할수 있습니다. 심리학에서 말하는 몰입경험의 조건과 일치합니다.

안드로이드 테스트의 장벽

그렇다면 이런 테스트의 장점을 안드로이드에서도 행복하게 누릴수가 있을까요? 불행히도 많은 장벽이 있다는 것을 경험했습니다. 그 이유를 몇가지 나누어서 말씀드리겠습니다.

첫째, Mock을 쓰기 어려운 기본 프레임워크 구조입니다. 안드로이드에서 굉장히 많이쓰이 getViewById, getSystemService 같은 코드는 상위클래스에 있는 메서드를 호출하는 구조입니다. 이런 형태는 가짜 클래스인 Mock을 주입하기가 어렵습니다.

둘째, 빈약한 기본 Mock클래스입니다. android.test.mock 패캐지 아래에는 MockContext, MockApplication, MockResource 등의 많은 클래스들이 있지만, 이들은 UnsupportedOperationException을 던지는 껍데기일 뿐입니다. 필요한 동작은 다음과 같이 직접 override해서 구현해야 합니다.

static public class MockServiceContext extends MockContext {
    @Overrride
    public getSystemService(String name){
        ……
    }
}

셋째, 기본적으로 제공되는 Instrumentation Test를 쓰는것도 배우기가 쉽지않습니다. 예를 들면 Activity를 테스트할때 ActivityTestCase, ActivityUnitTestCase, ActivityInstrumentationTestCase2의 세 가지 클래스 중 어느것을 써야할까를 하려면 많은 것을 알고 있어야합니다. 그리고 ActivityUnitTestCase에서 Dialog생성 등에 Event가 전달되면 BadToken Exception이 발생한다거나, ActivityInstrumentationTestCase2에서 Dialog 객체를 생성 후 dismiss() 메서드를 호출하지 않으면 leak window Exception이 발생하는등 부딛히는 예외상황도 많습니다.

넷째, UI 테스트 본연의 어려움이 있습니다. 안드로이드 코드는 역할상 UI 생성과 이벤트를 다루는 코드의 비중이 높습니다. 이는 웹어플리케이션 등 다른 플랫폼에서도 테스트하기 어려운 분야입니다. UI 객체의 속성은 자주 바뀌고 익명 클래스 등을 통해서 처리되는 이벤트는 Mock 객체로 바꾸고 추적하기가 어렵습니다.

다섯째, 느린 테스트 실행 속도입니다. 단 한줄을 고쳐도 패키징 → 설치 → 실행 싸이클을 거칩니다. 이 부분이 테스트의 장점을 다 말아먹는 가장 치명적인 단점입니다. 요즘 PC나 단말이 많이 빨라졌고 Genymotion같은 빠른 에뮬레이터도 활용할 수 있어서 많이 나아졌지만, 그래도 실행싸이클의 특성상 개선에 한계가 있습니다.

Robolectric 활용법

저도 Android의 기본 Instrutation테스트를 시도해보다 앞에서 말씀드린 이유로 많은 좌절을 느꼈습니다. 그래서 이를 개선하는 기술로 Robolecric을 시도해봤고, 어느정도 노하우를 쌓았습니다.

Robolectric은 배포, 설치가 필요없도록 PC의 JVM에서 안드로이드 코드를 실행해줍니다. 아마 한두번쯤을 만나셨을 메시지일듯한데, 이클랩스 같은 IDE안에서 안드로이드 코드를 바로 돌리면 'java.lang.RuntimeException: Stub!?' 에러가 발생합니다. Robolectric은 Android SDK가 제공하는 클래스에를 가로채어서 서 JVM에서 ANdroid 코드를 실행해도 저런 에러가 나지 않는 가짜코드를 시랭합니다.

이 프로젝트는 Github에서 활발하게 개발되고 있습니다. ActionbarSherlock으로 유명한 JakeWharton도 주요 커미터입니다. 174명의 기여자가 참여했고, 저도 그 174명 중의 한명이기도 합니다.

릴리즈 노트를 보시면 아시겠지만, 이 프레임워크는 꾸준히 발전하고 있습니다. 최근에는 KitKat에 추가된 API를 지원하는 작업도 진행되고 있습니다.

그리고 Android를 만든 구글에서도 Robolectric의 1점대의 버전을 자체 포크해서 쓴 흔적이 Android 코드저장소에 남아있습니다. 이렇게 포크로 그치지 않고 구글에서도 같이 Robolectric 2점대 버전의 개발에 참여했으면 더 좋지 않을까 하는 아쉬움이 남습니다.

물론 Dalvik이나 Art같은 Android 본연의 환경이 아닌 JVM에서 실행되다는 점 때문에 이 라이브러리의 한계는 있습니다. 그리고 Android SDK의 모든 영역을 SDK 출시 즉시에 제공하지도 못합니다. 그렇지만 Robolectric의 한계를 잘 인식하고 효율적으로 테스트할수 있는 부분에 집중을 한다면 앱이나 라이브러리를 개발하는데 많은 도움이 됩니다.

몇가지 대표적인 사용사례를 들어보겠습니다.

로그를 System.out으로 출력하기

우선 LogCat으로 출력되는 로그를 Log를 System.out으로 출력하려면 아래와 같이 구현을 하면 됩니다.

@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class SystemUtilsTest {
    @Before
    public void setUp() {
        ShadowLog.stream = System.out;
    }

android.util.Log를 이용한 클래스를 JVM에서 바로 실행가능합니다. java.lang, java.util등 기본 JDK에도 동일한 이름으로 존재하는 클래스를 주로 쓰는 유틸리티 클래스를 만덜어도 Log를 찍는 코드가 중간에 들어가있으면 이를 Dalvik에서만 실행해야했습니다. Robolect은 그런 코드도 JVM에서 실행되도록 하며 위와 같이 ShawdowLog클래스에 stream속성을 System.out으로 지정하면 System.out.println으로 찍는것과 유사하게 PC의 표준출력에서 로그메시지를 확인할수 있습니다.

단말기의 정보 변경

종종 Build.VERSION.SDK_INT 변수의 값을 참조해서 SDK의 버전별로 분기처리를 해야하는 코드가 있습니다. Robolectric에서는 이런 상수값도 아래와 같이 조작을 할 수 있습니다.

	Robolectric.Reflection.setFinalStaticField(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.JELLY_BEAN);

이런 기법은 Http호출을 하는 클라이언트에서 userAgent에 단말의 정보를 조합해서 넣어하는 경우를 테스트하는 경우에 유용하게 썼습니다.

Activity 클래스는 ActivityController라는 클래스를 통해 생성할 수 있습니다. 아래 코드는 스크린밝기를 지정하는 유틸리티는 테스트하는 코드입니다. 이 소스코드는 github에서 전체를 확인해보실수 있습니다.

@Test
public void shouldChangeScreenBrightness() {
    TestActivity activity = createActivity(TestActivity.class);
    float brightness = 0.5f;
    ScreenUtils.setScreenBrightness(activity, brightness);

    LayoutParams lp = activity.getWindow().getAttributes();

    assertThat(lp.screenBrightness, is(brightness));
}

private <T extends Activity> T createActivity(Class<T> activityClass) {
        ActivityController<T> controller = Robolectric.buildActivity(activityClass);
        controller.create();
        return controller.get();
}

DisplayMetricsDensity 속성은 직접 org.robolectric.Robolectric의 set메서드로 지정할 수 있습니다. 아래는 DP와 Pixel을 전환하는 코드를 예제로 들어봤습니다.

@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class PixelUtilsTest {
    private Context context;

    @Before
    public void setUp() {
        ShadowLog.stream = System.out;
        this.context = Robolectric.application;
    }

    @Test
    public void shouldGetDpFromPixel(){
        Robolectric.setDisplayMetricsDensity(1.5f);
        int dp = PixelUtils.getDpFromPixel(context, 50);
        assertThat(dp, is(33));
}

이 클래스의 Setter 메소드를 살펴보면 그밖에도 다양하게 테스트를 위한 가짜 객체를 설정하는 기능을 찾으실 수 있습니다.

단말의 SDK 정보를 원하는 값으로

System 서비스의 결과를 원하는 값으로


몇가지 예를 들었는데, Robolectric을 결국 어떻게 활용할 것이 좋을까요? JVM에서 테스트해도 동일한 결과를 보장하는 문자열, 날짜 처리, 프로토콜 파싱 영역에서 이득이 많습니다. 주로java.lang, java.util , java.io 패키지가 다루는 영역에 우선 집중하기는 것이 좋습니다. 처음부터 Activity, Fragment같은 UI영역까지 포함한 통합 테스트에 너무 많은 기대를 걸면 오히려 어려울 수 있습니다. Utility 클래스부터 우선 적용해보면서 점점 영역을 넓혀가시기를 권장드립니다.

Robolectric의 버전 2.3부터는 실제 Sqlite 구현체를 이용하기 시작했습니다. 이 버전부터는 DB관련 테스트도 JVM에서 시도해볼만합니다.

당연히 Robolectric으로 테스트를 포기할 영역도 많습니다. 노하우가 쌓이면 이를 의식해서 테스트의 이득이 높은 영역을 분리해서 설계할 수 있습니다. 이는 재활용/기능 추가/버그 발견에도 좋은 구조가 될것입니다.

코드 기여

계속 발전하고 있는 프레임워크이기 때문에 Robolectric에는 미비한 기능도 많습니다. 테스트 대상인 ANdroid 자체가 계속 변화하고 있어서 더욱 그렇기도 합니다. Robolectric은 Github에 올라간 오픈소스 프로젝트이기 때문에 누구나 코드 기여를 할 수 있습니다. 저도 3번 정도 Pull request를 날렸는데 그 경험을 공유해보겠습니다.

처음에는 Javadoc의 오타부터 수정해봤습니다. Pull request 번호 804번에서는 ShadowCookieManager의 javadoc에서 TelephonyManager로 작힌 단어를 CookieManager 로 수정했습니다. 주석을 한번이라도 본 사람이면 할 수 있는 아주 단순한 수정이였습니다.

한번 해보고나니 조금 더 어려운 기여를 해보고 싶어였습니다. 프로젝트를 진행하다가 Robolectric의 ShawdowCookieManager가 실제 android의 CookieManager의 동작과는 많이 다르다는 것을 발견했습니다. Robolectric 2.2까지는 단순히 HashMap에 key,value를 저장하는 수준이였습니다. Expires같은 속성이 들어가면 실제 SDK와 다르게 동작함. 아래 코드는 테스트가 실패합니다.

	cookieManager.setCookie(httpUrl, "name=value; Expires=Wed, 11 Jul 2035 08:12:26 GMT");
	assertThat(cookieManager.getCookie(httpUrl)).isEqualTo("name=value");

Pull request 번호 853번에서는 이를 실제와 비슷하게 재구현했습니다.

이 과정이 흥미로웠기 때문에 잠시 설명드리면, 먼저 실제 단말에서의 동작을 AndroidTestCase로 확인했습니다. ( https://gist.github.com/benelog/7655764 )

예를 들면 아래와 같이 removeExpireCookie를 호출했을 때 Expires값이 지나간 쿠키값은 삭제하는 동작을 확인해봤습니다.

CookieManager cookieManager;

public void setUp() {
    Context context = getContext();
    CookieSyncManager.createInstance(context);
    cookieManager = CookieManager.getInstance();
    cookieManager.removeAllCookie();
}

public void testRemoveExpiredCookie() {
    cookieManager.setCookie(url, "name=value; Expires=Wed, 11 Jul 2035 10:18:14 GMT");
    cookieManager.setCookie(url, "name2=value2; Expires=Wed, 13 Jul 2011 10:18:14 GMT");
    cookieManager.removeExpiredCookie();
    assertEquals("name=value", cookieManager.getCookie(url));
}

그리고 유사한 테스트 케이스를 Robolectric으로 작성했습니다.

CookieManager cookieManager = Robolectric.newInstanceOf(CookieManager.class);

@Test
public void shouldRemoveExpiredCookie() {
    cookieManager.setCookie(url, "name=value; Expires=Wed, 11 Jul 2035 10:18:14 GMT");
    cookieManager.setCookie(url, "name2=value2; Expires=Wed, 13 Jul 2011 10:18:14 GMT");
    cookieManager.removeExpiredCookie();
    assertThat(cookieManager.getCookie(url)).isEqualTo("name=value");
}

위의 테스트를 통과시키는 ShadowCookieManager를 구현하여 Pull request를 날렸습니다. Robolectric에 들어갈 코드를 Robolecric으로 검증한 셈입니다.

마지막으로 ShawdowProcess 구현한 코드입니다. 이 클래스로 android.os.Process.myPid()에서 나오는 값을 가짜로 지정할 수 있습니다.

@Test
public void shouldBeTrueWhenThisContextIsForeground(){
    int pid = 3;
    ShadowProcess.setPid(pid);
    createRunningAppProcessInfo(pid);
    boolean foreground = ActivityUtils.isContextForeground(context);
    assertThat(foreground, is(true));
}

구글의 Android 소스 저장소의 Robolectric fork판에도 유사한 클래스가 있습니다.

이 클래스는 Pull request 861번 으로 던져서 반영되었습니다. 중간에 이 클래스가 없으면 어떻게 되냐고 물어보길래 자세히 설명하려고 노력했던 과정이 재미있었습니다.

코드 기여에 유의할 점도 있습니다. Merge를 받아줄 주요 커미터들이 작업하기 편하게 Pull request를 하는 브랜치는 계속 master의 최신 commit으로 맞춰서 rebase를 해줘야합니다. 제가 한 요청들도 다른 요청에 밀려서 merge가 안 되고 있었는데, 계속적으로 rebase를 하고 있으니 그 정성을 봐서도 merge를 해준것 같기도합니다.

그외에는 컨티리뷰션 가이드를 참조하시면 됩니다. 대표적인 내용을 소개드리면, Indent에는 탭대신 공백 2칸을 쓰는등 컨벤션을 맞춰야하고, 적절한 테스트 코드를 같이 commit을 해야합니다. 앞에서 나온 CookieManager 사례를 참고하셔도 좋습니다.

정리

정리하자면 다음과 같습니다. Android 테스트는 난관이 많습니다. 특히 느린 실행속도가 치명적입니다. 여기서 Robolectric이 도움이 됩니다. 우선은 문자열, API 파싱. 유틸리티등 테스트하기 쉬운 영역부터 시도해볼만하고, 궁극적으로는 설계개선을 고민하는 것이 좋습니다. 코드 기여도 어렵지 않은, 기여자에게 관대한 프로젝트입니다.

오늘 발표와 관련해서 helloworld 블로그에 게시된 Android에서 @Inject, @Test글도 참고하실만합니다.