[rxJava] Flowable 과 Observable 의 차이

rxjava 가 메이저 버전 업(1->2)을 하면서 몇 가지 변경점이 생겼다.

변경점에 대한 자세한 내용은 아래 링크를 참조하기 바란다.

Flowable 이라는 base reactive class 가 추가 되었다. Observable 과의 차이는 backpressure buffer의 기본 탑재 유무이다.

backpressure?

우리말로 번역하면 ‘등 뒤에서 떠밀리는 압박’ 정도가 될 듯 하다.

이런 상황을 가정해보자. 콘서트장을 사람들이 가득 메웠다. 콘서트장에 들어오려는 사람들은 저글링 개떼처럼 밀려드는데 나가는 사람은 별로 없다. 콘서트장 출입구를 통제하는 요원이 없다면? 콘서트장이 터지던지 안에 있던 사람들이 짜부러지던지 아무튼 대형 사고가 발생할거다.

publish / subscribe 모델에서도 이런 비극적인 시나리오가 발생할 수 있다. 생산자는 미친듯이 element 를 생산해 내는데 소비자가 처리하는 속도가 이를 따라가지 못한다면

  1. busy waiting 또는
  2. out of memory exception 이 발생할 것이다.

‘등 뒤에서 떠밀리는 압박’ 에 대한 흐름제어를 위한 버퍼가 바로 backpressure buffer 다. 버퍼가 가득 차면 어차피 소비자는 element 를 처리할 여유가 없는 상태이므로 더 이상 publish 를 하지 않는다.

기존에 없던 개념이 새로 추가된 것은 아니다. 기존 rxJava 1.xx 의 경우 Observable 에 backpressure buffer 를 직접 생성해 주면 사용이 가능하다. 허나 rxJava 개발자는 초보자들이 미처 알아채지 못하는 영역에서 기대하지 않는 동작이 일어날 가능성이 있다며 Flowable 을 추가하였다.

다음 예제코드를 보자. 생산자의 생산 속도를 소비자가 따라가지 못하는 시나리오다.
Flowable 을 사용하면 default buffer size(128) 이상 backpressure buffer 에 element 가 쌓일 경우 흐름제어를 한다.

public class example01 {

    public static void main(String... args) throws InterruptedException {

        final String tmpStr = Arrays.stream(new String[10_000_000]).map(x->"*").collect(Collectors.joining());
        Flowable foo = Flowable.range(0, 1000_000_000)
                .map(x-> {
                    System.out.println("[very fast sender] i'm fast. very fast.");
                    System.out.println(String.format("sending id: %s %d%50.50s", Thread.currentThread().getName(), x, tmpStr));
                    return x+tmpStr;
                });

        foo.observeOn(Schedulers.computation()).subscribe(x->{
            Thread.sleep(1000);
            System.out.println("[very busy receiver] i'm busy. very busy.");
            System.out.println(String.format("receiving id: %s %50.50s", Thread.currentThread().getName(), x));
        });

        while (true) {
            Thread.sleep(1000);
        }
    }
}
[very fast sender] i'm fast. very fast.
sending id: main 0**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 1**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 2**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 3**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 4**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 5**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 6**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 7**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 8**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 9**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 10**************************************************

... 중략 ...

[very busy receiver] i'm busy. very busy.
receiving id: RxComputationThreadPool-1 0*************************************************
receiving id: RxComputationThreadPool-1 1*************************************************
receiving id: RxComputationThreadPool-1 2*************************************************
[very fast sender] i'm fast. very fast.
sending id: main 117**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 118**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 119**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 120**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 121**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 122**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 123**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 124**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 125**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 126**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 127**************************************************
[very busy receiver] i'm busy. very busy.
receiving id: RxComputationThreadPool-1 3*************************************************
[very busy receiver] i'm busy. very busy.
receiving id: RxComputationThreadPool-1 4*************************************************
[very busy receiver] i'm busy. very busy.
receiving id: RxComputationThreadPool-1 5*************************************************
[very busy receiver] i'm busy. very busy.
receiving id: RxComputationThreadPool-1 6*************************************************
[very busy receiver] i'm busy. very busy.
receiving id: RxComputationThreadPool-1 7*************************************************
[very busy receiver] i'm busy. very busy.
receiving id: RxComputationThreadPool-1 8*************************************************
[very busy receiver] i'm busy. very busy.
receiving id: RxComputationThreadPool-1 9*************************************************

반면, 같은 시나리오를 Observable 을 backpressure buffer 생성 없이 사용하면 OutOfMemoryException 이 발생한다.

public class example02 {

    public static void main(String... args) throws InterruptedException {

        final String tmpStr = Arrays.stream(new String[10_000_000]).map(x->"*").collect(Collectors.joining());
        Observable foo = Observable.range(0, 1000_000_000)
                .map(x-> {
                    System.out.println("[very fast sender] i'm fast. very fast.");
                    System.out.println(String.format("sending id: %s %d%50.50s", Thread.currentThread().getName(), x, tmpStr));
                    return x+tmpStr;
                });

        foo.observeOn(Schedulers.computation()).subscribe(x->{
            Thread.sleep(1000);
            System.out.println("[very busy receiver] i'm busy. very busy.");
            System.out.println(String.format("receiving id: %s %50.50s", Thread.currentThread().getName(), x));
        });

        while (true) {
            Thread.sleep(1000);
        }
    }
}
[very fast sender] i'm fast. very fast.
sending id: main 0**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 1**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 2**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 3**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 4**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 5**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 6**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 7**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 8**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 9**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 10**************************************************
[very fast sender] i'm fast. very fast.

...중략...

sending id: main 198**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 199**************************************************
[very fast sender] i'm fast. very fast.
sending id: main 200**************************************************
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOfRange(Arrays.java:3664)
	at java.lang.String.<init>(String.java:207)
	at java.lang.StringBuilder.toString(StringBuilder.java:407)
	at example02.lambda$main$1(example02.java:24)
	at example02$$Lambda$6/123961122.apply(Unknown Source)
	at io.reactivex.internal.operators.observable.ObservableMap$MapObserver.onNext(ObservableMap.java:59)
	at io.reactivex.internal.operators.observable.ObservableRange$RangeDisposable.run(ObservableRange.java:64)
	at io.reactivex.internal.operators.observable.ObservableRange.subscribeActual(ObservableRange.java:35)
	at io.reactivex.Observable.subscribe(Observable.java:10700)
	at io.reactivex.internal.operators.observable.ObservableMap.subscribeActual(ObservableMap.java:33)
	at io.reactivex.Observable.subscribe(Observable.java:10700)
	at io.reactivex.internal.operators.observable.ObservableObserveOn.subscribeActual(ObservableObserveOn.java:45)
	at io.reactivex.Observable.subscribe(Observable.java:10700)
	at io.reactivex.Observable.subscribe(Observable.java:10686)
	at io.reactivex.Observable.subscribe(Observable.java:10589)
	at example02.main(example02.java:27)
[very busy receiver] i'm busy. very busy.
receiving id: RxComputationThreadPool-1 5*************************************************

참고로, Flowable 은 FlowableCreate 라는 builder 에서 생성되며, 특별한 설정이 없을 경우 buffer size 는 최소 16, 기본 128 로 설정한다.

//FlowableCreate.java line:44

    @Override
    public void subscribeActual(Subscriber<? super T> t) {
        BaseEmitter<T> emitter;

        switch (backpressure) {
        case MISSING: {
            emitter = new MissingEmitter<T>(t);
            break;
        }
        case ERROR: {
            emitter = new ErrorAsyncEmitter<T>(t);
            break;
        }
        case DROP: {
            emitter = new DropAsyncEmitter<T>(t);
            break;
        }
        case LATEST: {
            emitter = new LatestAsyncEmitter<T>(t);
            break;
        }
        default: {
            emitter = new BufferAsyncEmitter<T>(t, bufferSize());
            break;
        }
        }

        t.onSubscribe(emitter);
        try {
            source.subscribe(emitter);
        } catch (Throwable ex) {
            Exceptions.throwIfFatal(ex);
            emitter.onError(ex);
        }
    }
//Flowable.java line:61
    static final int BUFFER_SIZE;
    static {
        BUFFER_SIZE = Math.max(16, Integer.getInteger("rx2.buffer-size", 128));
    }

java synchronization internal

java의 동기화는 크게 두 가지로 분류

  • 암묵적인 동기화(synchronized keyword)
  • 명시적인 동기화(concurrent.locks.Lock)

이 두가지는 구현 방식이 다르다.

  • synchronized는 jvm의 monitorenter / monitorexit 인스트럭션을 호출
  • Lock 은 hotspot에서 바로 native intrinsic function 을 호출

이 글에선 synchronized 의 구현을 살펴본다.

jvm-hotspot 소스코드는 openjdk8 을 참조하였으므로 다른 구현체(oracle, android 등)에서는 상황이 다를 수도 있지만 실제로 그럴 것 같지는 않다.

synchronized keyword

알다시피 synchronized keyword는 두 가지 경우에 쓰인다.

  • synchronized method
  • synchronized block

두 경우 모두 lock의 획득/반환은 jvm bytecode 중 monitorenter/monitorexit 인스트럭션으로
수행한다.

글에 착오가 있어 수정한다.

  1. synchronized block은 컴파일러에 의해 monitorenter / monitorexit 인스트럭션으로 변환이 되는 반면,
  2. synchronized method 는 bytecodeInterpreter가 method를 수행하는 시점에 동기화 여부를 판별하지 monitorenter/monitorexit 인스트럭션으로 컴파일되지는 않는다.

두 경우 모두 InterpreterRuntime::monitorenter() InterpreterRuntime::monitorexit() 메쏘드를 호출하기 때문에 최종적으로 lock을 획득하는 방식은 같지만 메커니즘은 다르다. hotspot 소스코드를 주의깊게 보지 않아 이런 혼동이 왔다. 이 글을 통해 잘못된 정보가 전달될 수 있으므로 앞으로는 조심하겠다.

jvm spec

synchronized method

synchronized method 에서 주의깊게 살펴 볼 내용은 biased lock 이라는 성능 향상을 위한 기법이다. document에 의하면 un-contented thread(=경쟁상태가 아닌 thread)의 성능 향상 효과가 있다고 하며 실제로 그렇다.

biased lock

biased lock이란 대부분의 java 쓰레드는 동기화를 위한 object가 1개인 경우가 많은데에서 착안,

object header 에 thread ID 와 biased 여부를 확인하는 flag 를 두어

동일 thread 가 연속적으로 critical section에 접근하는 경우 (= resource 경쟁 상태가 아닌경우)

atomic operation 을 수행하지 않는 lock 을 제공하여 수행 속도를 향상시키는 기법이다.

요약하면, biased lock = atomic operation을 하지 않는 lock = 흉내만 내는 lock 이다.

Dice-Moir-Scherer QR lock 최초 논문

hotspot에서의 biased lock 을 설명한 오라클 블로그

openJDK wiki 중 synchronization

implementation

openJDK8에서 bytecode interpreter가 synchronized method를 해석하는 구현. 모든 invoke 된 method는 이 구문에 진입하는데, synchronized keyword가 있으면 다음의 순서로 단계적으로 lock 획득을 시도한다.

  1. lock 최초 획득 – 이때 오브젝트 헤더에 thread id 와 bias flag 를 설정한다.
  2. biased lock 획득 시도 – 오브젝트 헤더와 동일한 thread 접근 시
  3. reboke bias – biased lock 획득을 실패
    1. thin lock 시도 – aging count 증가시킨다
    2. inflate lock 시도
  4. re-bias

hotspot/share/vm/interpreter/bytecodeInterpreter.cpp line:683

//bytecode interpreter 가 method 진입
case method_entry: {
... 중략 ...
// synchronized method 이면 Lock
if (METHOD->is_synchronized()) {
... 중략 ...
// 가장 먼저 biased lock 이 가능한지 확인한다.
// revoke 가능하면 revoke
// 아니면 re-bias
// 아니면 anonymously bias
if (mark->has_bias_pattern()) {
uintptr_t thread_ident;
uintptr_t anticipated_bias_locking_value;
thread_ident = (uintptr_t)istate->thread();
anticipated_bias_locking_value =
(((uintptr_t)rcvr->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) &
~((uintptr_t) markOopDesc::age_mask_in_place);

if (anticipated_bias_locking_value == 0) {
// Already biased towards this thread, nothing to do.
if (PrintBiasedLockingStatistics) {
(* BiasedLocking::biased_lock_entry_count_addr())++;
}
success = true;
} else if ((anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0) {
// Try to revoke bias.
markOop header = rcvr->klass()->prototype_header();
if (hash != markOopDesc::no_hash) {
header = header->copy_set_hash(hash);
}
if (Atomic::cmpxchg_ptr(header, rcvr->mark_addr(), mark) == mark) {
if (PrintBiasedLockingStatistics)
(*BiasedLocking::revoked_lock_entry_count_addr())++;
}
} else if ((anticipated_bias_locking_value & epoch_mask_in_place) != 0) {
// Try to rebias.
markOop new_header = (markOop) ( (intptr_t) rcvr->klass()->prototype_header() | thread_ident);
if (hash != markOopDesc::no_hash) {
new_header = new_header->copy_set_hash(hash);
}
if (Atomic::cmpxchg_ptr((void*)new_header, rcvr->mark_addr(), mark) == mark) {
if (PrintBiasedLockingStatistics) {
(* BiasedLocking::rebiased_lock_entry_count_addr())++;
}
} else {
CALL_VM(InterpreterRuntime::monitorenter(THREAD, mon), handle_exception);
}
success = true;
} else {
// Try to bias towards thread in case object is anonymously biased.
markOop header = (markOop) ((uintptr_t) mark &
((uintptr_t)markOopDesc::biased_lock_mask_in_place |
(uintptr_t)markOopDesc::age_mask_in_place | epoch_mask_in_place));
if (hash != markOopDesc::no_hash) {
header = header->copy_set_hash(hash);
}
markOop new_header = (markOop) ((uintptr_t) header | thread_ident);
// Debugging hint.
DEBUG_ONLY(mon->lock()->set_displaced_header((markOop) (uintptr_t) 0xdeaddead);)
if (Atomic::cmpxchg_ptr((void*)new_header, rcvr->mark_addr(), header) == header) {
if (PrintBiasedLockingStatistics) {
(* BiasedLocking::anonymously_biased_lock_entry_count_addr())++;
}
} else {
CALL_VM(InterpreterRuntime::monitorenter(THREAD, mon), handle_exception);
}
success = true;
}
}

// biased lock 이 실패하면 기존 방식대로 lock을 획득한다.
// Traditional lightweight locking.
if (!success) {
markOop displaced = rcvr->mark()->set_unlocked();
mon->lock()->set_displaced_header(displaced);
bool call_vm = UseHeavyMonitors;
if (call_vm || Atomic::cmpxchg_ptr(mon, rcvr->mark_addr(), displaced) != displaced) {
// Is it simple recursive case?
if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
mon->lock()->set_displaced_header(NULL);
} else {
CALL_VM(InterpreterRuntime::monitorenter(THREAD, mon), handle_exception);
}
}
}
}
THREAD->clr_do_not_unlock();

아래 코드는 monitorenter / monitorexit 구현이다. 궁극적으로 synchronized method나 synchronized block 모두 InterpreterRuntime::monitorenter() 메소드를 호출한다.
hotspot/share/vm/interpreter/interpreterRuntime.cpp line:606

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
if (PrintBiasedLockingStatistics) {
Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
}
Handle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
"must be NULL or an object");
if (UseBiasedLocking) {
// Retry fast entry if bias is revoked to avoid unnecessary inflation
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
"must be NULL or an object");
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END

//%note monitor_1
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorexit(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
Handle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
"must be NULL or an object");
if (elem == NULL || h_obj()->is_unlocked()) {
THROW(vmSymbols::java_lang_IllegalMonitorStateException());
}
ObjectSynchronizer::slow_exit(h_obj(), elem->lock(), thread);
// Free entry. This must be done here, since a pending exception might be installed on
// exit. If it is not cleared, the exception handling code will try to unlock the monitor again.
elem->set_obj(NULL);
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END

synchronized block

synchronized block은 해당하는 block의 시작과 끝을 java compiler가 monitorenter / monitorexit 로 변환한다.

아래 예제코드는 synchronized block 과 synchronized method 를 bytecode 로 변환하면 어떻게 되는지 보여준다.

public class SynchronizedExample {
public static void main(String[] args) {
Foo foo = new Foo();
synchronized (foo) {
++foo.foo;
}

methodFoo(foo);
}

static synchronized void methodFoo(Foo foo) {
++foo.foo;
}

static class Foo {
public int foo;
}
}

main 메쏘드. synchronized block 은 monitorenter 로 진입, monitorexit 로 끝나지만 synchronized method는 메소드를 invoke할 뿐이다.

0 new #2 <SynchronizedExample$Foo>
3 dup
4 invokespecial #3 <SynchronizedExample$Foo.<init>>
7 astore_1
8 aload_1
9 dup
10 astore_2
11 monitorenter //synchronized block 시작
12 aload_1
13 dup
14 getfield #4 <SynchronizedExample$Foo.foo>
17 iconst_1
18 iadd
19 putfield #4 <SynchronizedExample$Foo.foo>
22 aload_2
23 monitorexit  // synchronized block 끝
24 goto 32 (+8)
27 astore_3
28 aload_2
29 monitorexit
30 aload_3
31 athrow
32 aload_1
33 invokestatic #5 <SynchronizedExample.methodFoo> // method invoke
36 return

synchronized 메소드

0 aload_0
1 dup
2 getfield #5 <SynchronizedExample$Foo.foo>
5 iconst_1
6 iadd
7 putfield #5 <SynchronizedExample$Foo.foo>
10 return

결론

Lock object 를 명시적으로 사용하는 것 보다 synchronized keyword 를 사용하였을 때 유리한 경우는 다음과 같다.

  • 리소스가 공유될 필요는 있으나, 빈도가 적다.(=경쟁적이지 않다.)
  • 공유 리소스에 한 번 접근하였던 thread가 재진입하는 경우가 빈번하다.

concurrent programming #1

왜 concurrent programming 인가?

clockspeed

2000년대 초반까지 cpu의 처리속도는 2년 마다 2배씩 증가해 왔습니다. 복잡한 연산을 하고 싶으면 돈을 더 내고 비싼 cpu를 사면 해결되는 시대였죠. 그런데 어느 순간부터 cpu의 처리속도는 더 이상 빨라지지 않고 있습니다. 회로의 집적도와 전자의 이동속도에 한계가 있기 때문에 양자컴퓨터라도 나오지 않는 이상 현재로선 cpu의 수직적인 처리속도를 높일 수 있는 마땅한 방법은 없는 듯 합니다.

Cores.png

2004년 부터는 cpu의 처리속도를 높이는 대신 병렬적으로 코어의 갯수를 늘리는 시도를 하였습니다. 그리고 10여 년이 지난 현재, 손바닥만한 핸드폰에도 예외없이 cpu는 2개 이상 들어가는 것이 당연한 시대가 되었습니다. 멀티 코어 환경이 당연한 시대에 과연 그 안에서 동작하는 프로그램은 멀티 코어 환경에 적합하도록 만들어 졌을까요?

 

thread vs process

잘 아시겠지만 다수의 컴퓨팅 유닛을 사용하는 방법은

  1. multi processing
  2. multi threading

이 두 가지 방법 뿐입니다.

그렇다면 쓰레드랑 프로세스의 차이점은 무엇일까요? 찾아보면 많이들 나오니 자세한 설명은 패스하고 주요 차이점만 서술하면

쓰레드는 프로세스에 비해

  1. 생성 비용이 낮고
  2. 리소스 공유가 쉽습니다.

runtimearea2

위 그림은 jvm의 run-time data area 인데요. java 프로세스를 하나 생성하기 위해선 위 6가지 영역(정확히는 5가지, run-time constant pool은 method area에 포함) 이 모두 새로 생성되어야 합니다. 허나 쓰레드는 초록색 박스 영역만 새로 생성하고, 빨간색 박스 영역은 부모 프로세스와 공유합니다. java 를 예로 들었지만 다른 언어나 os도 상황은 대동소이합니다.

프로세스가 서로 리소스를 공유하는 수단은 1) named pipe 2) socket 3) shared memory 4) message 등이 있습니다. 이러한 프로세스 간 통신 수단은 어플리케이션 외부 커널에서 관리되기 때문에 비용이 높고 규약 외 처리(=exception)를 잘 정의하여야 합니다.

반면 쓰레드는 프로세스에 비해 리소스를 공유하는 방법이 쉽습니다.

  1. heap 에 리소스를 할당하거나
  2. 전역 변수를 설정하거나
  3. static 변수를 설정하면

해당 리소스는 쓰레드 간 공유가 가능합니다.

java의 경우, 전역변수가 없고 static keyword 는 멤버 변수에만 할당이 가능하므로 리소스가 공유되는 경우는 다음 두 가지 뿐입니다.

  • heap에 할당된 리소스 및 레퍼런스
  • static 멤버 변수 및 레퍼런스

 

thread 문제들

멀티쓰레드 프로그램은 분명 cpu 처리속도의 한계를 극복하기 위한 좋은 대안입니다만, 개발자에게 다양한 문제를 안겨주기도 합니다. 아마도 개발 초년생에게 가장 어려운 문제가 thread 문제일겁니다. thread 문제가 어려운 이유는 무엇일까요?

  1. 재현이 어렵고
  2. 현상이 그때그때 다르고
  3. 인간의 뇌가 동시에 두 가지 사고를 할 수 없기 때문

이 아닐까 생각합니다. 허나 얼핏 복잡다단해 보이는 thread 문제에도 유형이 있으니 문제의 유형들을 분류해 보도록 합시다.

thread 문제의 양상은 크게 두 가지로 구분합니다.

  1. thread safety 문제
  2. 기대하지 않는 동작

thread safety 문제는 프로그래머가 리소스 공유에 대한 이해가 부족하기 때문에 발생합니다. 공유되는 리소스는 반드시라고 해도 좋을 정도로 대부분 동기화 문제가 발생합니다. 리소스가 공유되더라도 동기화 문제가 발생하지 않는 경우는 다음 세 가지 뿐입니다. 이 세 가지는 반드시 기억해 두길 바랍니다. 여러 가지 다른 thread 문제들은 쉽게 예측이 어렵고 해결방안이 뚜렷하지 않을 수도 있으나 thread safety 문제는 명약관화하고 해결방안이 분명합니다.

  1. 공유 리소스가 immutable 하거나
  2. critical section에 대한 동기화 보장을 하였거나
  3. instruction 이 atomic 하거나

thread safety를 보장하였는데도 어플리케이션이 기대하지 않는 엉뚱한 동작을 한다면 이는 어플리케이션 동작 환경을 고려하지 않은 설계/정책의 문제입니다. 쓰레드는 태생적으로 제어가 어렵습니다. 실행중인 쓰레드가 언제 종료될지, sleep 상태의 쓰레드가 언제 다시 작업을 재개할 지 실행중에는 알 수가 없기 때문입니다. 또한 모든 thread 는 태생적으로 다른 thread들과 리소스를 점유하기 위한 경쟁관계에 놓여있기 때문에 경쟁의 과열로 인한 문제가 발생할 가능성이 항상 존재합니다.

  1. starvation
  2. dead lock / live lock 문제
  3. aba 문제

이러한 문제들은 설계와 정책으로 해결하여야 합니다. 어플리케이션의 동작환경에 따라, 사용자의 사용 패턴에 따라 해당 문제는 발생할 수도 있고 안할 수도 있습니다. 또한 어떤 문제들은 소프트웨어 설계 시점에 원천적으로 문제의 원인을 봉쇄할 수도 있습니다. 모든 분야가 마찬가지겠지만 소프트웨어 설계와 정책은 외부 환경에 대한 고려가 필수적입니다.

 

동기화 방식과 성능 병목

monitor

critical section

mutex

semaphore

spinlock

 

reentrantlock

synchronized

 

volatile

memory transaction (hardware / software)

lock-free / wait-free

concurrent programming 사전 학습

금번 스터디 주제는 concurrent programming 입니다.

멀티코어 환경에서 코어 갯수가 2배로 늘어나면 성능도 2배로 늘어나기를 기대하지만, 실제로 thread가 어떤 구조로 되어있고, 어떤 자원들이 공유되며, 해당 공유 자원을 코어가 어떻게 처리하는지에 대한 정확한 이해가 없으면 이러한 성능향상은 있을 수 없습니다. 때문에 멀티코어 환경에서 프로그램의 성능을 높이려면 concurrent 한 데이터 처리 기법에 대한 이해가 반드시 필요합니다.

스터디 시작 전 미리 알아두어야 할 사전 지식에 대하여

  • 자료 링크
  • (설명이 필요한 내용은)간단한 설명

를 이 페이지에 정리해 두었으니 스터디 시작 전에 미리 공부를 해두었으면 합니다.

사전 학습 내용은 모두 java 및 jvm 내용이지만, 다른 언어/os 도 비슷비슷합니다. 하나만 제대로 알고 있으면 나머지는 쉽게 이해가능합니다.

jvm memory 구조

jvm run-time data area

dependency hell과 빌드지옥 탈출

jvm의 메모리 구조를 잘 이해하려면 java process가 어떻게 동작하는지 알아야 합니다. 근데 아이러니하게도 java process가 어떻게 동작하는지 알려면 jvm의 메모리 구조를 이해하고 있어야 합니다. 이러한 모순적 상황에서 이해의 수준을 높이려면 메모리 구조와 process 의 동작 원리를 반복적으로 여러번 공부하는 방법 밖에는 없습니다.

runtimeArea.PNG

jvm 내에서 프로세스의 흐름을 기술하기 위한 저장 영역을 run-time data area 라고 합니다. 어떤 process가 실행되어 종료될 때 까지 사용되는 모든 데이터는 이 영역에 기록됩니다.

  • program counter register : 프로그램 카운터(pc)는 현재 동작중인 프로세스/또는 쓰레드가 sleep 상태에 진입하여 CPU 점유권을 잃어버렸다가 다시 되찾을 때 현재 이 쓰레드가 어디까지 진행되었는지에 대한 기록을 남겨놓기 위한 저장공간입니다. pc register에는 현재 진행중인 instruction의 주소값이 저장됩니다.
  • jvm stack : 프로그램 내에서 method가 호출되면 그 method 를 수행하기 위해 필요한 저장공간을 제공합니다. 또한 method의 호출은 계층적으로 호출순서가 발생하므로 이 순서를 보장하여 처리하기 위해 stack 을 사용합니다. stack에 저장되는 element의 단위는 frame 이라 합니다.
  • native method stack : native method stack은 조금 특별한 영역으로, jvm의 깍두기와도 같은 예외 케이스입니다. 외부에서 실행되는 native process와의 인터페이스(jni) 를 호출하기 위한 공간입니다.
  • method area : method area는 프로그램을 실행하기 위한 메타정보(class구조, method, constructor 등등)을 저장하는 영역입니다. 특히 static 으로 선언된 변수가 이 영역에 저장된다는 사실을 기억해 두기 바랍니다.
  • run time constant pool : run time constant pool 은 method area 내부에 존재하는 영역으로, constant,  class 및 interface의 symbolic reference table 정보 등을 저장하고 있습니다.
  • heap : run time 에 동적으로 할당되는 데이터가 저장되는 영역입니다. heap에 할당된 데이터는 gc의 대상입니다.

지금부터 설명하는 내용이 중요합니다. run-time data area 내에서 어떤 영역은 프로세스 내의 모든 thread 들이 공유하는 반면, 어떤 영역은 각 thread 마다 독립적으로 생성됩니다. 여기서 thread 간에 공유되는 리소스 영역이 중요합니다. 극단적으로 말하면 모든 쓰레드 문제는 동기화 문제이며, 모든 동기화 문제는 공유 리소스로부터 발생합니다.

run-time data area 에서 다음 영역은 process 내 모든 thread가 공유합니다.

  • method area
  • run-time constant pool
  • heap

다음 영역은 thread 1개 마다 독립적으로 1개씩 생성되며, 데이터는 공유되지 않습니다.

  • program counter register
  • stack
  • native method stack

runtimeArea2.PNG

java process

process 를 간결하고 정확하게 정의하자면, “program in execution”(=실행중인 프로그램)입니다.

process 가 실행되는 순서는 다음과 같습니다.

  1. jvm startup
  2. load class
  3. link class
  4. init class
  5. create class

java process의 동작 원리를 자세하게 설명하는 것은 본 문서의 목적을 넘어서므로 무얼 보고 공부해야 하는지 링크만 제공합니다.

java program execution

 

spring boot

참고 문서

 

spring-boot

spring boot life-cycle

Cosysto Gimbh 라는 사람이 life-cycle에 대한 flow 를 아주 잘 그려놓았다.

 


 

실제 구현체 이름은 매우 길고 복잡하다. (e.g. ConfigurationWarningsApplicationContextInitializer)

spring application 내부에서 실제로 어떤 일이 일어나는지에 집중하도록 긴 이름은 짧게 축약하였다.

 

ApplicationContext 생성(=SpringApplication.run())

  • Listener 생성 및 시작
  • Environment 준비
  • Context 생성
  • Context 준비
    • Environment 세팅
    • 후처리(post process)
    • Initializer 초기화(apply initializer)
  • Context 로드
    • Source 로드
    • BeanDefinitionLoader 에 Source , Context 탑재
    • BeanDefinitionLoader 로드
  • 종료
    • Context Refresh

 

ApplicationContext 는 SpringApplication의 몸통에 해당하는 실제 instance의 추상화이다.

SpringApplication

SpringApplication 의 Entry point 는 반드시 특정 package를 지정하고, 그 하위에 위치시켜야 한다. (=Default package path에 entry point 시작 금지)

그렇지 않으면 ** WARNING ** : Your ApplicationContext is unlikely to start due to a @ComponentScan of the default package.  이 발생한다.

 

@SpringBootApplication 을 사용하면 다음 Annotation 을 생략 가능하다.

Source Code


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Configuration
@EnableAutoConfiguration
@ComponentScan
public @interface SpringBootApplication {
/**
* Exclude specific auto-configuration classes such that they will never be applied.
* @return the classes to exclude
*/
Class<?>[] exclude() default {};
}

 

SpringApplicationRunListener

ConfigurableEnvironment

ConfigurableApplicationContext

 

실제로 호출되는 implementation 은 AnnotationConfigEmbddedWebApplicationContext 이다.

java wildcard

참고 문서

개요

작년에 wildcard에 대해 스터디한 내용이 있었다는 사실에  한 번 개깜놀

까맣게 잊고 있었다는 사실에 두 번 개깜놀

wildcard 와 type parameter 간의 가장 중요한 차이점은 code statement 내에 선언된 param이 재참조되는지 여부이다.

재참조 되면 type parameter 써야 하고, 재참조 안되고 귀찮으면 그냥 ? 쓴다.

Wildcard ?

java syntax에는 ? 라는 것이 있다. generic type parameter로 쓰인다. (e.g. ArrayList<?>)

Long story short…

generic type parameter 를 명확히 결정하지 않은 채로 두기 위한(=unknown type) syntax

Generic 의 type parameter 는 polymorphism 을 지원하지 않는다.

generic에서는 type prarmeter 에 대하여 polymorphism 을 지원하지 않는다.  다음 예제를 보자.

Generic

Collection<String> foo = new ArrayList<String>;
Collection<Object> refFoo = foo; //compile error!!

refFoo.add(new Object());
String bar = foo.get(0); //만일 line2 가 error가 아니었다면, bar 에 Object 를 할당하는 참사가 발생하게 된다.

 

얼핏 생각하면 ArrayList<Dog> 을 List<Animal> 으로 받을 수 있을 것 같지만, 상기 예제와 같은 문제를 막기 위해 Generic 의 type parameter 는 문법적으로 subtype 변환을 허용하지 않는다.

혼란을 막기 위한 제한이지만, type 체크가 엄격하면 유연한 사용이 힘들다. Wildcard 는 이런 상황을 해결하기 위해 고안된 문법이다.

 

case Wildcard<?>
private void foo(Collection<?> c) {
  for (Object iter : c)
    System.out.println(iter);
}

List<String> bar = new ArrayList<String>;
bar.add("hello");

foo(bar); //okay!!

 

case Obect

private void foo(Collection<Object> c) {
  for (Object iter : c)
    System.out.println(iter);
}

List<String> bar = new ArrayList<String>;
bar.add("hello");

foo(bar); //compile error!!

 

Wildcard 사용시 주의할 점

 

bounded wildcard

 

spring에서 용례

spring-context 의 SimpleApplicationEventMulticaster.multicastEvent()

정확한 type을 알 수 없는 ApplicationListner<T>를 이터레이션 하기 위하여 wildcard가 사용되었다.

SimpleApplicationEventMulticaster.java

@Override
public void multicastEvent(final ApplicationEvent event) {
  for (final ApplicationListener<?> listener : getApplicationListeners(event)) {
    Executor executor = getTaskExecutor();
    if (executor != null) {
      executor.execute(new Runnable() {
        @Override
        public void run() {
          invokeListener(listener, event);
        }
      });
    }
    else {
      invokeListener(listener, event);
    }
  }
}

java8 consumer / supplier 를 이용한 builder pattern 구현

개요

java8 feature에 대하여 실무에서 활용 가능한 수준으로 best practice 제공

시작하는 말

디자인패턴에 대한 흔한 오해 중 대표적인 한 가지가 “디자인패턴은 프로그램 언어 독립적이다.” 입니다.

허나 실상은 그렇지 않으며, 매우 언어에 종속적이어서 특정 언어에서 제공하는 기능을 면밀히 살피지 않으면

의도한 대로 코드가 동작하지 않을 수도 있고, 해당 언어만의 장점을 살리지 못하는 반쪽짜리 코드가 될 수도 있습니다.

java8 에 추가된 functional interface 중 consumer / supplier 를 이용하면 보다 직관적인 builder pattern을 구현할 수 있습니다.

먼저, 종래에 java에서 builder pattern을 어떻게 구현하는지 살펴보고, 여기에 java8 feature 를 추가하여 어떻게 더 나아지는지 살펴보겠습니다.

builder pattern?

왜 쓰나

디자인패턴을 공부할 때 항상 염두에 두어야 할 것이 있습니다.

이 패턴을 왜 쓰나?

무엇이 좋아지나?

무엇이 안좋아지나?

 

모든 문제를 한방에 해결하는 만능의 해법이란 존재하지 않습니다.

어떤 문제를 해결하기 위한 선택을 하면 반드시 trade-off 가 발생합니다.

 

스타크래프트에서, 미네랄을 먼저 채취하여 후반에 많은 병력을 생산하기 위한 12드론 2해처리 빌드는 초반 러시에 취약하다는 단점이 존재합니다.

반대로 초반에 병력을 상대보다 먼저 뽑는 4드론 빌드는 초반러시에 강력하나 후반으로 갈수록 취약합니다.

초반에 병력도 많이 뽑고, 미네랄도 많이 캐고, 후반 병력도 많이 뽑는 그런 마법과도 같은 해법이 존재할까요?

아니면 그때 그때 상황에 맞는 적절한 젼략을 사용해야 할까요?

 

예시가 즈질스러우니 국민 패턴 싱글톤 패턴을 예로 들어봅시다.

싱글톤으로 생성된 객체는 어플리케이션 실행~종료 시까지 어디에서나 접근 가능합니다.

어플리케이션 내부에서 공유하여야 할 리소스 정보를 쉽게 참조 가능하다는 장점이 존재하지만,

싱글톤 객체는 객체지향 언어 버전의 전역변수나 마찬가지여서

1) 싱글톤 객체 – 다른 객체간 의존성이 높아짐,

2) 인스턴스 생성의 책임이 분리되지 않기 때문에 SRP 위반,

3) unittest객체가 싱글톤 객체에 종속성이 발생하기 때문에 단위 테스트 어려움

등의 단점이 있습니다.

객체지향 5원칙 (aka. S.O.L.I.D)에 만족하는 싱글톤 패턴은 존재하지 않습니다. 싱글톤 자체가 태생적으로 SRP를 위반하기 때문이죠.

허나 파일경로, ip주소 등 1) 쉽게 변경되지 않고, 2) 어플리케이션 영역 전체에 공유가 필요한 리소스는 싱글톤으로 구현하는 편이 유리합니다.

 

GoF 의 디자인패턴에 소개된 모든 패턴은 저마다 장/단점이 분명합니다.

각 패턴의 장/단점이 무엇인지 분명히 알면 장점은 부각시키고 단점은 최소화할 수 있습니다.

장점은 살리고, 단점은 감춘다. 이것이 좋은 소프트웨어 아키텍트가 갖추어야 할 설계 역량입니다.

 

이 글에서 소개할 디자인 패턴은 builder 패턴입니다. 빌더 패턴을 쓰는 이유는 크게 2가지입니다.

1) 귀찮아서

2) 보기 좋으라고

 

다음과 같은 class foo 를 가정해 봅시다.

귀찮은 class 생성
public class foo {
    private String a;
    private String b;
    private String c;
    private String d;
    private String e;
    private String f;
    private String g;
    private String h;
    private String i;
    private String j;
    private String k;
    private String l;
    public foo(String a, String b, String c, String d, String e, String f,
               String g, String h, String i, String j, String k, String l) {
        this.a = a;
        this.b = b;
        this.c = c;
        this.d = d;
        this.e = e;
        this.f = f;
        this.g = g;
        this.h = h;
        this.i = i;
        this.j = j;
        this.k = k;
        this.l = l;
    }
}

new foo(a,b,c,d,e,f…,z) 하기 귀찮습니다. 또 생성자의 parameter 가 길어지면 실수나 오타를 발견하기도 어렵습니다.

 

빌더 패턴은 new foo(a,b,c,d,e,f…,z) 를 new builder().a(a).b(b)…build() 로 보기 좋게 바꾸기 위하여 사용합니다.

‘보기 좋게 바꾼다’고 표현했지만 실제로는 부수적으로 따라오는 몇 가지 장점이 더 있습니다.

1) 실수, 오타의 여지를 줄여줌

2) 생성 parameter에 제약조건을 부여할 수 있습니다. (e.g. 타입, 필수 여부 등등)

 

java8 feature 를 사용하지 않은 builder pattern, Ver. 1

빌더 패턴의 가장 기본적인 아이디어는 객체 생성의 책임을 분리하는 데에 있습니다.

내가 만들려 하는 객체 생성의 역할을 다른 객체에 위임하는 것이죠.

 

builder pattern의 가장 기본적인 구현은 다음과 같습니다.

foo builder
public class fooBuilder {
    private String a;
    private String b;
    ...
    
    public fooBuilder setA(String a) {
        this.a = a;
        return this;
    }
    public fooBuilder setB(String b) {
        this.b = b;
        return this;
    }
    ...
    public foo build() {
        return new foo(this.a, this.b, ...);
}

 

위에서 작성한 fooBuilder class 를 사용하는 방법은 다음과 같습니다.

foo builder
//기존
foo myFoo = new foo(a,b,c,d,...);
 
//builder 사용
fooBuilder myFooBuilder = new fooBuilder();
foo myFoo = myFooBuilder.setA(a).setB(b)....build();

 

위에서 작성한 fooBuilder 클래스는 드닥 마음에 들지 않습니다. foo 객체를 생성하기 위해 불필요하게 fooBuilder 객체도 생성하여야 하니까요.

그러면 fooBuilder 의 메소드를 static으로 만들면 어떨까요? fooBuilder 객체를 생성하지 않아도 foo 객체를 생성할 수 있지 않을까요?

 

java8 feature 를 사용하지 않은 builder pattern, Ver. 2

fooBuilder class를 이렇게 바꿔봅시다.

foo builder
public class fooBuilder2 {
    private String a;
    private String b;
    ...
    
    public static fooBuilder setA(String a) {
        this.a = a;
        return this;
    }
    public static fooBuilder setB(String b) {
        this.b = b;
        return this;
    }
    ...
    public static foo build() {
        return new foo(this.a, this.b, ...);
}
foo builder
//기존
foo myFoo = new foo(a,b,c,d,...);
 
//builder 사용
fooBuilder myFooBuilder = new fooBuilder();
foo myFoo = myFooBuilder.setA(a).setB(b)....build();
 
//builder2 사용
foo myFoo = fooBuilder2.setA(a).setB(b)...build();

불필요한 builder 객체 생성을 없앴습니다. 어떤가요? 잘 작동할까요?

절대 이딴 식으로 프로그래밍 하시면 안됩니다. 오밤중에 서버실 끌려가 밤새도록 디버깅하는 수가 있어요.

fooBuilder2 는 thread safe 하지가 않습니다. 따라서 concurrent 환경에서 foo 객체가 지멋대로 만들어 지는 수가 있어요.

concurrent 와 thread safety 얘기는 길어지니 다음에 따로 다루기로 하고, 암튼 fooBuilder2 는 실패실패 대실패입니다. 절때 이딴식으로 개발하면 안되요.

 

java8 feature 를 사용하지 않은 builder pattern, Ver. 3

서두에 언급하였듯, 디자인패턴은 언어 종속적입니다.

java 언어에서만 지원하는 특별한 feature(inner class) 를 사용하면 builder pattern을 좀 더 아름답게 구현할 수 있어요.

fooBuilder를 foo class 내에 static inner class 로 구현하는 겁니다.

foo builder
public class foo {
    private String a;
    private String b;
    ...
    public foo(String a, String b, String c, String d, String e, String f,
               String g, String h, String i, String j, String k, String l) {
        this.a = a;
        this.b = b;
        ...
    }
    public static class fooBuilder {
        private String a;
        private String b;
        ...
        fooBuilder() {}
        public fooBuilder setA(String a) {
            this.a = a;
            return this;
        }
        public fooBuilder setB(String b) {
            this.b = b;
            return this;
        }
        ...
        public foo build() {
            return new foo(a, b,...);
        }
    }
}
foo builder
//기존
foo myFoo = new foo(a,b,c,d,...);
 
//builder 사용
fooBuilder myFooBuilder = new fooBuilder();
foo myFoo = myFooBuilder.setA(a).setB(b)....build();
 
//builder2 사용
foo myFoo = fooBuilder2.setA(a).setB(b)...build();
 
//static inner class builder 사용
foo myFoo = new foo.fooBuilder.setA(a).setB(b)...build();

static inner class 로 객체 생성 코드를 보기 좋게 바꾸었지만 여전히 단점은 존재합니다.

1개의 foo 객체 생성을 위하여 반드시 1개의 fooBuilder 객체를 생성해야 하는 문제는 남아 있습니다.

메모리를 효율적으로 사용해야 하는 환경에서는 적합하지 않습니다.

또한 builder class가 inner class 로 포함되면서 클래스 재사용성에 대한 고민을 다시 해야 합니다.

 

java8 feature 를 사용한 builder pattern

지금까지

1) GoF가 처음 제안한, 고전적 형태의 Builder pattern

2) java의 언어적 특징, static inner class 를 사용한 Builder pattern

을 살펴 보았습니다.

이번에는 java8에 추가된 functional interface 를 사용하여 builder pattern을 구현하는 방법을 소개할건데요.

시작하기에 앞서, java8의 Consumer / Supplier 가 무엇인지 알아보도록 합시다.

 

functional interface?

functional interface에 대하여 잘 모르는 분들이 많이 계시리라 생각되어 간단히 소개합니다.

함수형 언어의 가장 큰 특징 중 하나가 function(=method) 을 변수처럼 사용하는겁니다.

 

예를 들면,

var foo = someFunction(); //함수 definition에 대한 reference
 
var foo = (x) -> x*x; //definition이 없는 함수(=익명함수)에 대한 reference

 

예제에서의 var foo 처럼 function을 저장하는 방법이 언어적으로 제공되어야 하는데요.

java8 이전의 java에서 L-value 에 위치할 수 있는 type 들(Object나 primitive type 등)은 function reference 에 대한 저장을 고려하지 않았습니다.

때문에 함수형 언어로의 확장을 위하여 java8은

1) var foo 와 같은 역할을 제공하는 새로운 type을 정의하던가

2) 아니면 기존에 제공하던 언어 기반을 크게 변경하지 않으면서 꼼수를 쓰던가

둘 중 하나를 선택하여야 했습니다.

 

java8 은 2) 를 선택하는 꼼수를 두었는데요. 그 꼼수가 functional interface 입니다.

java compiler는 @FunctionalInterface 라는 Annotation 이 포함된 다음 조건을 충족하는 interface를 위 예제의 var foo 처럼 취급하기로 약속하였습니다.

1) non-static method

2) non-default method

3) only 1 method in interface

@FunctionalInterface
interface var {
    void execute(String arg);
}
 
var foo = (x) -> System.out.println(x);
 
foo.execute("Hello");

참 쉽죠?

 

java8에선 기본적인 형태의 functional interface 를 미리 정의해 두었습니다.

Consumer / Supplier / Function / Predicate 4가지인데요.

각각 용도가 다릅니다만 자세한 것은 다음 기회에 다루도록 하겠습니다.

 

Consumer 를 사용한 builder pattern

기본적인 아이디어는 이렇습니다.

1) 생성하고자 하는 객체의 setter method 를 변수로 저장

2) 객체 생성 시 저장해 두었던 setter method를 실행하여 객체 생성

 

구현체는 다음과 같습니다.

foo builder
public class foo {
    private String a;
    private String b;
    ...
    public foo(String a, String b, String c, String d, String e, String f,
               String g, String h, String i, String j, String k, String l) {
        this.a = a;
        this.b = b;
        ...
    }
    public static class fooBuilder {
        private foo _myFoo;
        private List<Consumer<foo>> _setters = new ArrayList<>();
        public <T> fooBuilder with(BiConsumer<foo, T> setter, T val) {
          if (_myFoo == null)
               _myFoo = new foo();
         Consumer<foo> c = _myFoo -> setter.accept(_myFoo, val);
         _setters.add(c);
         return this;
       }
       public foo build() {
           foo val = _myFoo;
           _setters.forEach(_setters -> _setters.accept(val));
           _setters.clear();
           return _myFoo;
       }
    }
}
foo builder
//function interface 로 구현한 builder class 사용
foo myFoo = new foo.fooBuilder().with(foo::setA, "A").with(foo::setB, "B")...build();

 

위 패턴의 장단점을 살펴 볼까요?

장점은

1) 코드 재사용 : 생성할 Class 에 존재하는 setter 를 Builder Class에 중복으로 생성하지 않고 변수에 담아 재사용

 

단점은

1) 대동소이한 Builder를 매번 재구현하여야 함

 

Supplier / Consumer 를 사용한 builder pattern

위의 단점을 개선하여 봅시다.

Builder 코드를 재사용하여 생산성을 높이는 데에는 다음 두 가지 방법이 있습니다.

1) 상속을 통한 재사용

2) Generic을 통한 재사용

 

여기선 2) 방법으로 builder pattern을 고쳐 보겠습니다.

 

구현체는 다음과 같습니다.

generic builder
public class GenericBuilder<T> {
    private final Supplier<T> _instance;
    private List<Consumer<T>> _setters = new ArrayList<>();
    private GenericBuilder(Supplier<T> instance) {
        this._instance = instance;
    }
    public static <T> GenericBuilder<T> of(Supplier<T> instance) {
        return new GenericBuilder<T>(instance);
    }
    public <U> GenericBuilder<T> with(BiConsumer<T, U> consumer, U value) {
        Consumer<T> c = setter -> consumer.accept(setter, value);
        _setters.add(c);
        return this;
    }
    public T build() {
        T value = _instance.get();
        _setters.forEach(iter -> iter.accept(value));
        _setters.clear();
        return value;
    }
}
foo builder
//Generic builder class 사용
foo myFoo4 = GenericBuilder.of(foo::new)
        .with(foo::setA, "A")
        .with(foo::setB, "B")
        .with(foo::setC, "C")
        ...
        .build();

 

위 패턴의 장단점은 다음과 같습니다.

장점은

1) 코드 재사용 : GenericBuilder 클래스를 범용적으로 재사용 가능

 

단점은

1) inner class 를 사용할 때 보다 직관적이지 않다.

 

결론

여기서는 builder 패턴만 다루었지만 다른 패턴 공부도 위와 비슷하게 진행하시면 많은 도움이 될 겁니다.

이 패턴을 왜 쓰는지

내가 사용하는 언어에 특화된 구현 방법은 없는지

특히 java8 feature 를 사용할 여지가 있는지

java6 / java7을 쓰면 안되는 이유

개요

kth 내부 서비스 또는 kt 용역 서비스 중 상당수는 호환성이라는 명목 하에 java6 / java7 로 개발/유지보수하는 프로젝트들이 많이 있습니다.

물론 java6 / java7 로 개발한다고 잘 돌던 서비스가 당장 멈추거나 하는 일은 없을겁니다만

서비스 품질은 다음과 같은 측면에서 경쟁사에 뒤쳐진다고 보시면 됩니다.

1) 보안

2) 가용성

3) 호환성

4) 기술 역량

 

보안

oracle java6 의 End of Public Update 는 2013년 2월, java7은 2015년 4월에 종료되었습니다.

무슨 말인고 하면, 누군가가 java의 새로운 보안 결함을 발견한다면 java6 / java7 로 개발된 서비스는 더이상 보안 업데이트가 없다는 뜻입니다.

다음 사이트(oracle jre 링크)를 참고하시면, 지금까지 알려진 보안 결함 리스트를 보실 수 있는데요.

java6 은 대략 200여 개, java7은 대략 100여 개 정도의 보안 결함 업데이트가 없었습니다.

물론 네트워크 방화벽이라던지 정기 OS 패치 등으로 이중 삼중 보안 장치를 마련해 두고 있지만

때때로 이러한 모든 보안 장치를 무력화할 만한 대형 결함이 발견되기도 하는데, 그럴 때 동작하는 서비스가 java6 / java7 기반이라면 대책이 없습니다.

 

가용성

Interned String 문제

java6을 쓰면서 서비스에 특별한 장애 요소도 없는데 이상하게 보름에서 한달 정도 지나면 서비스가 내려가는 경험이 있으셨다면 십중팔구는 Interned String 문제입니다.

java6 과 java7/8 은 String pool 저장소가 근본적으로 다릅니다.

java6 이하 버전의 경우 신규 생성된 String은 PermGen 에 위치하며, 동일한 String의 경우 pooling 합니다.

 

PermGen 사이즈는 고정이므로(java8은 좀 다릅니다만),

1) 100bytes 사이즈의 서로 다른 값을 가진 String(e.g. 임시 발급 key, UUID 등등등)이

2) 100Mbytes 사이즈 PermGen

에 채울 수 있는 최대값은 100 * 1000 * 1000 = 100M 이므로

최대 100만 개의 String 이상 String이 신규로 생성되면 PermGen 영역이 가득차게 되고, 아래와 같은 익셉션과 함께 프로그램이 제대로 동작하지 않을겁니다.

exception

java.lang.OutOfMemoryError: PermGen space

 

java7부터는 String.intern() 메소드를 명시적으로 호출하지 않는 한, PermGen 영역에 String을 Poolling 하지 않습니다.

단지 jvm 버전을 올리는 것 만으로도 가용성 및 장애 요소를 한 가지 줄일 수 있습니다.

 

Fixed PermGen size 문제

허나, java7 이하 버전에서 여전히 PermGen size 는 고정이라는 문제가 존재합니다.

대개의 경우 String이 PermGen space를 잡아먹는 주된 요인이겠지만,

본래 PermGen의 주된 목적은 어플리케이션에서 사용되는(=load한) class 정보의 저장입니다.

java7에서 PermGen 사이즈를 적당히 크게 잡아줄 경우, PermGen이 고갈되는 경우는 왠만해선 발생하지 않겠습니다만

다음과 같은 시나리오에서는 PermGen이 고갈됩니다.

1) classloader 가 동적으로 신규 class를 지속적으로 load

2) 프로그램 사이즈가 정말 커서 많은 class가 필요

PermGen 사이즈는 고정이라는 원죄가 있기 때문에 jvm 옵션으로 PermGen 사이즈를 조정하더라도 항상 문제가 발생할 여지가 있습니다.

java8 에서는 PermGen 이 사라진 대신 해당 역할을 metaspace가 대체합니다.

차이점은 다음과 같습니다.

1) metaspace의 메모리 영역은 native 입니다.

2) memory가 부족하면 동적으로 할당 가능합니다.

 

cf) PermGen?

2015년도 신입사원 교육자료 중 2일차, dependency hell 작성할 때 그려둔 native application 의 memory 모델입니다.

물론 java와 디테일에는 차이가 있지만, Heap / Stack / Data / Code 섹션에 대한 포괄적인 개념은

java나 native나 혹은 이후 여러분들이 접하게 될 어떤 언어라도 대동소이합니다. 하나만 제대로 알고 있으면 되겠죠?

위 메모리 모델은 내가 짠 프로그램이 실제로 어떻게 작동하는지 알고 싶은 개발자라면 반드시 대가리헤또에 필수 탑재되어 있어야 합니다.

메모리 모델

stack : 프로그램의 흐름(=function call), local 변수 등을 저장

heap : 프로그램 내에서 동적으로 할당한 데이더를 저장

data : 전역변수를 저장

code : 프로그램 메타정보(=function meta) 등을 저장

각 메모리 영역별로 역할이 주어져 있지만, 가장 중요한 핵심은

1) 어플리케이션 시작 전에 사이즈를 결정할 수 있는 영역과(code / data)

2) 어플리케이션 실행 도중에 사이즈가 변경되는 영역(stack / heap)

이 분리되어 있다는 점입니다.

 

디테일은 다르지만 자바 메모리모델도 어플리케이션 시작 전에 사이즈를 결정할 수 있는 영역과 그렇지 않은 영역으로 구분됩니다.

다만, PermGen 영역의 정체성이 모호한데요.

본래 의도대로라면 PermGen 영역은 런타임에 증가되어선 안되는 영역입니다만 몇 가지 경우에 예외를 두었고

그 예외 때문에 자바 어플리케이션은 지금까지 고통받아 왔습니다.

 

java memory model

아래 그림은 어떤 존잘님께서 그린 java 메모리 모델입니다. (출처 : http://javarevisited.blogspot.com/2011/04/garbage-collection-in-java.html)

1) java7 이하 메모리 모델

2) java8 메모리 모델

 

Compressed Ordinary Object Pointer default enabled

jvm에는 Ordinary Object Pointer, 줄여서 OOP(객체지향 프로그래밍과 혼동 금지!, 보통 hotspot oop 라고 부릅니다.)에 대한 압축을 enable / disable 하는 옵션이 있습니다.

hotspot oop 는 기회가 된다면 나중에 따로 다루겠지만, 간단히 요약하면

데이터의 주소 정보를 효율적으로 쓰기 위한 꼼수

라고 보시면 됩니다.

compressed oop 옵션은 java7부터 default enabled 입니다.

만일 64bit 플랫폼 환경에서 java6을 쓰신다면 어플리케이션 실행 시 이 옵션을 enable 하여야 메모리를 좀 더 효율적으로 쓸 수 있습니다.

 

호환성

신규 버전의 java 에는 보안 / 기능 / 성능 / 정책 위배 등 여러가지 사유로 과거 버전중 deprecated 되는 API들이 발생합니다.

java6 또는 java7에서 java8로 올리지 못하는 여러 가지 이유중에는 deprecated API에 대한 호환성 문제가 꽤 비중이 높은 편인데요.

당장 deprecated API를 수정할 여력 없이 운영만 하는 서비스는 현실적으로 java 버전을 높이기 어려운 것이 사실입니다.

허나 신규 프로젝트에서 deprecated API 를 사용하거나, 해당 API를 사용한 library 를 쓰는 일은 없어야 합니다.

이미 배포한 API를 없앤다는 건 정말 어려운 일입니다. 그래서 보통은 왠만하면 해당 API를 없애지 않고 구현을 변경하거나 다른 더 좋은 구현을 만들어내 해당 API 사용률을 낮추도록 유도합니다.

그럼에도 불구하고 API를 없애는 결정을 했다는건 정말 그 API를 쓰지 말아야 할 중대한 이유가 있기 때문에 없애는 겁니다.(쓰지 말라면 쓰지 마루요)

아래 링크는 oracle에서 제공하는 java7 / java8 의 deprecated 된 api 리스트 및 java8 호환성 guide 문서입니다.

 

기술역량

modern language 열풍이 분지 올해로 10년 쯤 지났습니다.

무수히 많은 modern language가 생겨나기도 하였지만, 기존 language들이 modern language 의 철학을 담는 경우도 종종 있었습니다.

java는 다른 언어에 비해 modern language의 주요 개념, 기능 등을 담아내는 시점이 매우 늦은 편입니다.

심지어 java보다 30년은 오래된 c++이 오히려 java보다 100만배는 modern 합니다.

 

현대의 소프트웨어 개발 환경은 몇 가지 특징이 있습니다.

1) 과거에 비해 어플리케이션의 규모 / 복잡도가 기하급수적으로 높아졌습니다.

2) 난립하는 하드웨어에 효율적으로 대응하기 위한 수단이 필요합니다.

3) 더이상 수직적인 하드웨어 성능은 높아지지 않습니다. 허나 처리해야 할 데이터의 양은 엄청나게 늘었습니다.

 

과거, 구조적 프로그래밍의 한계를 객체지향 프로그래밍으로 극복한 시절이 있었습니다.

현재는 객체지향 프로그래밍의 한계에 다다랐다고 보아도 과언이 아닙니다.

우리가 쓰는 대부분의 CPU 는 멀티코어 프로세서로 설계되었습니다.

허나 객체지향 언어로 멀티코어 성능을 온전히 내는 어플리케이션을 만들기는 매우 어렵습니다. 디버깅은 100만배 더 어렵습니다.

 

과거, 비동기 네트워킹은 시스템 프로그래머만의 전유물이던 시절이 있었습니다.

현재는 클라이언트 프로그래머도 비동기 처리의 동작 원리를 반드시 이해하여야 합니다.

비동기 처리는 반드시 OS의 지원 / 프로그램 언어의 지원이 필요합니다.

 

modern language 에는 다음과 같은 몇 가지 공통적인 특징이 있습니다.

1) 함수형 프로그래밍 패러다임 수용

2) 불필요한 코드 노가다 지양

3) 강력한 type 추론

4) lazy execution 을 구현하기 위한 효율적인 수단 제공

5) REPL(Read – Evelauation – Print – Loop) 또는 준하는 어플리케이션 확인 수단 제공

6) thread local stack 에 대한 디버깅 수단 제공

 

java8 도 많이 늦은 편인데 이러한 소프트웨어 개발 환경의 변화를 java6 / java7 로 대응한다? 쉽지 않습니다.

지금으로부터 약 40여 년 전, 프로그래밍 세계를 평정하던 cobol이라는 언어가 있었습니다.

당시 두터운 사용자/커뮤니티층을 형성하던 cobol 개발자들은 굳이 OOP 를 학습할 필요성을 느끼지 못하였습니다.

c++이 출시되고 채 10년이 못되어 cobol 어플리케이션은 씨가 말랐고, OOP는 대세가 되었습니다.

요즘의 java를 보면서 과거 cobol의 역사가 자꾸 오버랩되는 것은 우연의 일치일까요?

 

요약

java8 써라. 두번 써라.