오랜만에 UI 코드에 손 대다가 RecyclerView에 무한 스크롤링 기능을 넣어야했습니다. CodePath를 보니 RecyclerView.OnScrollListener를 상속하여 복잡한 계산을 한  EndlessRecyclerViewScrollListener.java가 소개되어 있습니다.[CodePath Endless scrolling page 링크


좀 더 심플한 방법이 있을것 같아 RecyclerView JAVADOC 문서를 좀 보고 다른 블로그도 보았지만. 전 아래와 같은 해결책을 CodePath에 추가했습니다.[CodePath에 추가한 글 링크] RecyclerView에서 main thread 점유는 최소화 하는게 좋아서 최대한 스크롤링에 대한 계산을 별도로 안하는 걸 고민 하다가 RecyclerView.Adapter.onBindViewHolder()를 이용했습니다. 한번 써보시고 이상하시면 답글 달아주세요. 테스트 해보니 딱히 문제는 없지만, fine tuning 하시려면 if(position == getItemCount() - 1) 에서 1을 변경해야 할 것 같습니다. RecyclerView에 보여지는 각 뷰의 크기에 따라 1을 3으로 변경하던지 다른 값으로 변경해서 fine tunig 하세요. (전 아래와 같이 글을 올렸는데 CodePath 담당자가 문장을 좀 바꾸었군요ㅋ)


Instead of using EndlessRecyclerViewScrollListener.java introduced in the following section. There's more simple and less computational way to implement endless scrolling. Make use of the following code snippet for endless scrolling.

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
  EndlessScrollListener endlessScrollListener
  
  ...
  public void setEndlessScrollListener(EndlessScrollListener endlessScrollListener) {
      this.endlessScrollListener = endlessScrollListener;
  }
  
  @Override
  public void onBindViewHolder(MyAdapter.ViewHolder holder, int position) {
      final Data data = dataset.get(position);

      // you can cache getItemCount() in a member variable for more performance tuning
      if(position == getItemCount() - 1) {  
          if(endlessScrollListener != null) {
              endlessScrollListener.onLoadMore(position);
          }
      }

      ...
  }
  
  @Override
  public int getItemCount() {
      if(dataset == null)
          return 0;
      else
          return dataset.size();
  }
  ...
  
  public interface EndlessScrollListener {
      /**
       * Loads more data.
       * @param position
       * @return true loads data actually, false otherwise.
       */
      boolean onLoadMore(int position);
  }
}


'android' 카테고리의 다른 글

Firebase  (0) 2016.06.11
Android N Preview 대응  (0) 2016.06.04
ButterKnife를 왜 쓰냐구요?  (0) 2016.02.29
뭐? 이벤트? 버스 태워 보내! #2  (0) 2016.02.29
뭐? 이벤트? 버스 태워 보내! #1  (0) 2016.02.29
Posted by 제이제이랩
,

소프트웨어 엔지니어로서 자신이 만든 로직 또는 알고리즘이나 3rd party 라이브러리의 성능을 측정하고 비교 분석하고 싶을 때가 있습니다. 무심코 작성한 코드가 성능에 영향을 미칠 수 있어서 특정 동작을 하는 로직을 다른 방식으로 여러개 만들어 벤치마킹 할 수도 있고 그냥 지적 호기심에 특정 라이브러리의 성능을 다른 경쟁 라이브러리의 성능과 비교 분석하고자 할 수도 있습니다. JAVA로 구현된 코드를 측정할 때 가장 공신력있는 벤치마킹 결과를 얻고자 한다면 JMH(Java Microbenchmark Harness)를 이용하는걸 추천합니다.

  • 왜 JMH로 벤치마킹 해야하는가?

JVM을 만든 사람들이 JMH를 만들었습니다. 벤치마킹 환경 구성이나 실제 성능 측정에 영향을 미칠만한 부가적인 요소를 최소화 했겠죠? 메소드가 JVM에서 실행될 때 JIT 컴파일러는 최적화를 합니다. 어느 부분에 최적화 할곳이 있는지, inline method로 만들부분이 있는지, 중복 코드가 있는지, dead code가 있는지, 코드 reordering 할 부분이 있는지 등을 분석해 최적화를 하기 때문에 제대로된 벤치마킹을 하기가 어렵습니다. JMH를 이용하면 컴파일러의 최적화 작업을 어느정도 조정할 수 있고 코드 웜업을 한 상태에서 메소드들을 측정하기 때문에 측정 오류를 최소화 할 수 있습니다. 그리고 공식적으로 JAVA 9에 벤치마킹 툴로 배포된다고 합니다.

  • JMH 사용법

아래 참고에 있는 링크에서 학습하시면 됩니다. 그 중 Jakob Jenkov가 작성한 tutorial이 가장 읽기 편합니다.  http://tutorials.jenkov.com/java-performance/jmh.html 여길 참고하세요.

  • 간단한 벤치마킹 코드를 짜보자.

(Github[https://github.com/jayjaykim/jmh-sample]에 에 소스 올려 놓았으니 앞으로 설명할 샘플 코드를 다운 받아 참고 하시면 됩니다.)

JMH를 이용하여 벤치마킹 코드를 구현하려면 maven build system을 사용해야 합니다. https://maven.apache.org/ 에서 maven을 다운 받고 실행 파일 경로 설정을 한 후 아래와 같이 콘솔에 입력합니다. 하기 명령어의 각 옵션 사이가 라인피드로 되어 있는데요 가독성을 위해 편의상 그렇게 한것이고 실제로는 그냥 스페이스 처리 하시면 됩니다. 

$ mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jmh -DarchetypeArtifactId=jmh-java-benchmark-archetype -DgroupId=com.jayjaylab.sample -DartifactId=jmh-sample -Dversion=1

이렇게 하면 JAVA maven project가 하나 만들어 지는데, 바로 아래와 같이 명령어 입력해 주시면 벤치마킹이 수행이 됩니다. 자세한 사항은 참고[3](http://tutorials.jenkov.com/java-performance/jmh.html)를 따라하시면 쉽게 됩니다.^^

 $ mvn clean install
 $ java -jar target/benchmarks.jar

이렇게 하면 jmh를 이용해 벤치마킹을 실행할 수 있습니다. 벤치마킹 모드를 Mode.Throughput으로 하면 한 메소드 당 기본 10번 세팅(한 세트에 20 warmup iteration, 20 실제 iteration)을 돌립니다.


재미로 그럼 벤치마킹 코드를 작성해 보겠습니다. 간단히 1부터 10,000까지 합을 구하는 로직을 for loop을 이용해 구하는 법과 JAVA 8의 IntStream을 이용해 구하는 법을 벤치마킹합니다. 아래 코드의 testSummingByForLoop(), testSummingByStream() 메소드는 각각 for loop과 IntStream으로 합계를 구하는 메소드입니다.

public class StreamBenchmark {
    @org.openjdk.jmh.annotations.State(Scope.Thread)
    public static class State {
        int startNumber;
        int endNumber;

        @Setup(Level.Trial)
        public void setUp() {
            startNumber = 0;
            endNumber = 10000;
        }

        @TearDown(Level.Trial)
        public void tearDown() {
        }
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    @OutputTimeUnit(TimeUnit.SECONDS)
    public void testSummingByForLoop(State state, Blackhole blackhole) {
        long sum = 0l;
        for(int i = state.startNumber; i < state.endNumber; i++) {
            sum += i;
        }
        blackhole.consume(sum);
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    @OutputTimeUnit(TimeUnit.SECONDS)
    public void testSummingByStream(State state, Blackhole blackhole) {
        blackhole.consume(
                IntStream.range(state.startNumber, state.endNumber).sum());
    }
}

위 코드를 빌드하고 벤치마킹을 실행하면 하기와 같은 결과를 얻습니다. for-loop 확실히 약 7배 빠르군요. (그런데 정확히 7배라고 말하기 어렵습니다. 원래 벤치마킹할 때는 벤치마킹만 돌리고 다른 프로그램을 실행하면 안되는데 저는 이것저것 하면서 돌린것이라서;;). 참고로 본 벤치마킹을 실행한 환경은 MAC OS X yosemite version 10.10.5(14F27), 2.7GHz Intel core i5, 8GB 1867MHz DD3 입니다. Throughput으로도 충분히 해석할 수 있지만 @BenchmarkMode를 Mode.AverageTime으로 변경하여 각 메소드가 얼마나 걸리는지 알수 있습니다. 아래 결과를 좀더 보시면 min, max, mean을 구하고 표준편차와 분포를 normal distribution을 가정했을 때 confidence interval도 계산되어 나옵니다(confidence level : 99.9%, margin of error : (41011.042, 42081.252]). 이 부분에 대한 해석은 통계에 대한 지식이 있어야겠지요? 통계도 아셔야합니다. 저는 대학교, 대학원때 공부했는데 아직도 가물가물해서 다시 들여다 보고 있네요^^;;

Result "testSummingByForLoop":
  280454.648 ±(99.9%) 2181.860 ops/s [Average]
  (min, avg, max) = (227831.822, 280454.648, 289601.359), stdev = 9238.133
  CI (99.9%): [278272.788, 282636.509] (assumes normal distribution)

Result "testSummingByStream":
  41546.147 ±(99.9%) 535.105 ops/s [Average]
  (min, avg, max) = (25794.615, 41546.147, 43398.632), stdev = 2265.668
  CI (99.9%): [41011.042, 42081.252] (assumes normal distribution)


# Run complete. Total time: 00:13:28

Benchmark                              Mode  Cnt       Score      Error  Units
StreamBenchmark.testSummingByForLoop  thrpt  200  280454.648 ± 2181.860  ops/s
StreamBenchmark.testSummingByStream   thrpt  200   41546.147 ±  535.105  ops/s


어떤가요? 벤치마킹이 참 편해졌습니다. 자 그럼 JMH를 써보시고, 저는 나중 나중에 좀더 재밌는 벤치마킹을 들고 #2를 작성해 올리겠습니다.


참고

[1] http://psy-lob-saw.blogspot.kr/2013/04/writing-java-micro-benchmarks-with-jmh.html

[2] http://java-performance.info/jmh/

[3] http://tutorials.jenkov.com/java-performance/jmh.html

Posted by 제이제이랩
,

2016/03/02 - [java] - 추상화(Abstraction) 이해의 중요성 #1


자동차는 영어로 Car이고, 자동차를 이루는 구성요소로는 엔진(Engine), 바퀴(Wheel), 제조사(Manufacturer), 모델(Model) 등이 있습니다. 중고매매에서 가장 중요한 주행거리, 연식, 자동차 색상도 놓칠순 없겠군요. 단순한 자동차(Car) 추상화는 java.lang.Object만을 상속하는 Car 클래스를 만드는 것입니다. 다른 말로 POJO(Plain Old Java Object)를 만드는 것입니다.

자동차 추상(Abstraction)은 com.jayjaylab.app.usedcar.Car 클래스 아래와 같이 만들었습니다.

package com.jayjaylab.app.usedcar

public class Car {
    public int drivingDistance;    // in km(killo meters)
    public String dateMadeAt;
    public String color;
    public String engine;
    public String wheel;
    public String manufacturer;
    public String model;
    public String trunk;
    public String roof;
    // and so on...
} 

자동차는 제조사에서 한번 만들어지면 제조사, 모델이 변경되지 않습니다. 위처럼 자동차를 단순하게 추상화했는데, 자동차에서 자주 변경이 되는 부분, 변할 수 있는 부분이 무엇이 있을까 생각해 봅시다.

  • 엔진이 바뀔 수 있다.
  • 휠이 바뀔 수 있다.
  • 주행 거리가 바뀔 수 있다. 자동차 중고매매 사업장에서 이런 요구사항이 있다면 주행 거리 조작! 불법이지요;;
  • 루프가 바뀔 수 있다.

상기 정도로 정리해 볼 수 있습니다. 위 Car 클래스가 이런 변경 사항에 충분히 유연한지 한번 점검해 보겠습니다.

package com.jayjaylab.app.usedcar

public class Car {
    public int drivingDistance;    // in km(killo meters)
    public String dateMadeAt;
    public String color;
    public String engine;
    public String wheel;
    public String manufacturer;
    public String model;
    public String trunk;
    public String roof;
    // and so on...

    public static void main(String[] args) {
        Car car = new Car();
        // 엔진 변경
        car.engine = "yamato";
        car.engine = "superengine";
        // 휠 변경
        car.wheel = "kumho_wheel";
        car.wheel = "fantastic_wheel";
        // 루프 변경
        car.roof = "default_roof";
        car.roof = "sun_roof";
        // 주행 거리 변경. 불법 이죠잉!
        car.drivingDistance = 100;  // 100 km
        car.drivingDistance = 200; // 200 km
    }
} 

위 소스 코드를 보니, 런타임 동안 자동차 엔진, 휠, 루프, 주행거리 변경을 손 쉽게 할 수 있습니다. 그런데 엔진, 휠, 루프, 주행거리를 변경하는데 Car 클래스의 멤버변수를 직접 접근해서 변경해야 합니다. Car 클래스를 사용하는 입장에서 Car 클래스의 멤버 변수가 무엇인지 알 필요가 없습니다. 엔진을 변경하기 위해 Car 클래스에서는 Car.engine이라는 멤버 변수를 외부에 노출시키는게 아닌 엔진을 변경할 수 있는 메소드를 제공해야할 것입니다. Car 클래스 사용자들(개발자들)에 Car 클래스의 속성을 변경 시키려면 메소드를 통하여 변경 시키도록 하고 해당 멤버 변수는 개발자에게 노출시키지 않도록 합니다. 위의 Car 클래스는 캡슐화(Encapsulation)이 잘못된 예의 하나입니다. 아래와 같이 Accessor를 public에서 protected로 변경하도록 하고 멤버 변수에 대한 직접 접근을 막습니다. 사실 멤버 변수를 직접 참조하는것이 메소드를 호출하는 것보다 몇 CPU 싸이클 빠를것 같지만 난독화 때문에 Proguard 사용하면 신경 안써도 됩니다. getter-setter inlining을 proguard가 해준다고 하네요. [참고 http://developer.android.com/training/articles/perf-tips.html#GettersSetters]

package com.jayjaylab.app.usedcar

public class Car {
    int drivingDistance;    // in km(killo meters)
    String dateMadeAt;
    String color;
    String engine;
    String wheel;
    String manufacturer;
    String model;
    String trunk;
    String roof;
    // and so on...

    public void setEngine(String engine) {
        this.engine = engine;
    }

    public void setWheel(String wheel) {
        this.wheel = wheel;
    }

    public void setRoof(String Roof) {
        this.roof = roof;
    }
    
    public void setDrivingDistance(int drivingDistance) {
        this.drivingDistance = drivingDistance;
    }

    ...

    public static void main(String[] args) {
        Car car = new Car();
        // 엔진 변경
        car.setEngine("yamato");
        car.setEngine("superengine");
        // 휠 변경
        car.setWheel("kumho_wheel");
        car.setWheel("fantastic_wheel");
        // 루프 변경
        car.setRoof("default_roof");
        car.setRoof("sun_roof");
        // 주행 거리 변경. 불법 이죠잉!
        car.setDrivingDistance(100);  // 100 km
        car.setDrivingDistance(200); // 200 km
    }
} 

Car 클래스 사용자가 엔진, 휠 등을 변경하기 위해서 위 처럼 set method를 사용하도록 강제하면 Car의 멤버 변수를 잘못된 값으로 변경하는 것을 막을 수 있습니다. 예를 들면, Car.setEngine(), Car.setWheel()의 메소드에 매개변수가 유효한지 체크하는 로직을 넣거나 현재 변경 가능한 상태에만 변경을 하도록 로직을 추가하여 각 변경을 안전하게 수행하도록 합니다. 또한 메소드를 통하여 변경을 할 경우 멀티 쓰레딩 환경에서 thread safety 하도록 로직을 추가할 수도 있습니다. 응용 프로그램을 개발할 때 절대 네버 멤버 변수를 public로 선언하여 직접 접근하는 방식을 피하셔야 합니다. getter-setter로 Encapsulation 하시는 것은 기본입니다. 예외인 경우는 자료 구조 만들 때인데요. 성능(메소드 사용으로 인한 스택 접근 시간)을 위해 cpu cycle 사용을 최소화하기 위한 노력을 자료 구조에서는 해줘야하는데, 자료 구조 내부에서 getter-setter를 이용하지 않고 멤버 변수에 직접 접근을 하는 경우가 있습니다. 안드로이드의 경우는 ViewHolder 패턴을 사용할 때 멤버 변수를 직접 접근합니다. ViewHolder 클래스 내부 멤버변수를 접근할 때 getter로 접근하지 않고 멤버 변수를 직접 접근해서 UI thread 사용 시간을 최소화 해줘야 합니다.


잡설이 많았는데, 그럼 다음 글에서 더욱 제대로된 Car abstraction을 만들어 봅시다.

Posted by 제이제이랩
,