소프트웨어 사색

초보자를 위한 동시성과 Future

[하마] 이승현 (wowlsh93@gmail.com) 2017. 2. 3. 11:21


1. 그림으로 보는 동시성 

2. 동시성과 Future 이야기

3. 자바로 밑바닥부터 Future 구현

4. 언어별 Future 살펴보기


1. 그림으로 보는 동시성 


은행에 창구가 하나입니다.  사람들은 줄을 서서 일을 처리합니다.


은행은 일처리가 빠른 직원을 고용해서 더 빨리 사람들의 일을 처리해줍니다.

하지만 고객들을 감당하기 힘들어서 더 빠른 일처리를 하는 직원을 고용합니다.

한계에 봉착합니다..



그래서 창구직원을 3명으로 늘렸습니다.  근데 사람들은 습관적(코딩)으로 예전 직원에게만 찾아갑니다. 

창구직원이 늘어나 바짜 고객들의 줄은 줄어들지 않습니다.


간혹가다 다른 창구로 찾아가는 손님들이 있어서 조금 나아졌습니다.

즉 다른 창구(CPU 코어) 를 활용하는 고객(쓰레드) 들이 많아 질 수록 업무환경(서버의 처리능력)은 좋아 질 것입니다. 


모든 코어에 적절하게 일처리가 분담되었습니다. 쓰레드를 직접 만들어서 처리할 수도 있지만 그렇게 하기 위해 노력 할 시간에 서비스를 위해 만들어야하는 기능에 충실해야 합니다. 그러기 위해서 쓰레드를 내부로 감춰놓고 외부에서 편하게 쓸 수 있는 도구가 필요합니다. Future , Promise , Async, Observerble 등입니다.


그림 처럼 B 함수를 호출하고 결과를 기다린 시간하고 


B 라는 쓰레드에 비동기로 업무를 맡겨바짜,  B 라는 쓰레드에서 결과를 나오기만을 기다리고 있다면 큰 효율을 얻을 수는 없을 것 입니다.  (Java7 에서의 Future 방식이며 물론 상황에 따라서 효율의 정도는 달라집니다.)


 B 에서 일을 처리한 결과를 C 에 입력해서 처리하고 결과를 받아야 한다고 할때 (체이닝이 필요할때) 
메인쓰레드에서는 B 의 결과를 기다렸다가 C 에게 넣어주고 또 C 의 결과를 기다리는 것보다..
(비록 멀티쓰레드 프로그래밍을 하고는 있다지만 먼가 답답하다..)


메인쓰레드는 그냥 모든것을 잊어버리고 자기 주력의 일을 하고 , B 에게 던진일은 알아서 B -> C-> Somthing 이 되게 한다면 효율적일 것이다. 

웹어플리케이션으로 얘기하자면 클라이언트의 요청을 받는 놈은 계속 받는일에만 신경쓰고, (요청이 어떻게 처리되고 있는지 신경을 딱 끊어버리고..) 요청을 처리하는 녀석들이 높은 동시성 효율을 보이도록 잘 구성해서 성능을 높이자. 라는거죠.

결국 이러한 필요성에 의해 각종 도구들이 나타납니다. 밑바닥부터 직접 구성하려면 하면 어렵지만 이러한 것들을 편하게 해주는 것들이 많아지게 되었습니다. 예를들어 PlayFramwork 나 최신의 Spring 등은 이러한 Reactive 파라다임을 지원하고 있습니다.


* 멀티쓰레드 패턴에서 Future 패턴은 따로 존재하는데 그 패턴을 각 언어,라이브러리마다 확장해서 사용하고 있습니다. 예를들어  Java7의 Future 와 Scala 의 Future 는 다르죠.  



2. 동시성 (Concurrency)  와 Future 이야기

요즘 비동기 개발방법들이 다양한 언어,라이브러리등을 통해 회자되고 있습니다. 동시성(Concurrent) 에 대한 이야기야 10년전에도 20년전에도 나온 이야기겠지만, 바라보는 시각 차이 혹은 대중들에게 요구하는 수준이 달라졌다고 할 수 있습니다. 여러 동시성 도구들이 언어마다 채택되고 있는 관계로 동시성프로그래밍은 매우 대중화 되었습니다. (아직 우리나라는 좀 멀은거 같기도 하지만....)

예전에는 쓰레드를 활용한다 정도로 접근했다면 요즘에는 가장 효율적으로 활용한다가 일반화되고 있습니다.

 암달의 법칙

 멀티코어를 사용하는 프로그램의 속도는 프로그램 내부에 존재하는 순차적sequential 부분이 사용하는 시간에 의해서 제한된다.

즉 쓰레드 간의 블럭이 많을 수록 비효율적이며 , 그것을 감소시키는것이 동시성 프로그래밍의 화두인겁니다.

그러기 위해서는 쓰레드 관련 개발하는데 굉장히 힘을 소비해야하며, 현대처럼 복잡해진 사회(솔루션)에 언제 개인이 그런 효율적인 바퀴를 만들고 있을까요? 그래서 다양한 도구들이 나옵니다.


Future, Promise, Async,Observerble

일단 이런것들은 쓰레드를 대체 하는 신기술이 아닙니다. 쓰레드를 사용 안하는게 아니라 깊숙히 감춰 놓은 기술인 겁니다. 쓰레드를 직접 사용하는 것 보다 쉽게 효율적으로 동시성 프로그래밍을 할 수 있으니깐 적용하는 겁니다.  그래서 신기술을 적용한다고 생각하는 대신에 " 쓰레드로 하던 것을 저거로 하자" 이렇게 생각하는게 간단합니다.


3. 자바로 Future 를 밑바닥에서부터 구현하자.


먼저 Future 패턴의 클래스 다이어그램을 보자.  

( IFood / Future / ReadFood 의 관계는 Proxy 패턴이다. 매우 많은 패턴이 저런 모양을 가지고있다. 컴포지트,데코레이터,프록시, 어댑터, 인터프리터 등등  결국 모양(구조) 과 패턴은 전혀 상관이 없다. "의도" 가 중요한것이다.)

당신은 주말에 꿀 같은 휴식을 취하고있다. 동물농장을 보다보니 출출해서 피짜헛에서 피자를 하나 시켜먹어야겠다고 생각했다.  

1. 주문을 한다.

2. 문앞에서 하염없이 기다린다.

맞나?  

그렇지 않다. 우리는 보통 주문을 한 다음에 보던 TV 를 계속보던가 ,  똥싸러 가거나  다른 일을 하게된다. 이런걸 비동기적인 행동이라고 한다. 위에 하염없이 기다리는것을 동기적( 블로킹 되었다라고도 ) 이라고 한다.

다시 저 위의 다이어그램을 살펴보자. 클라이언트(당신)은 레스토랑( 피짜헛) 에 주문을 하고 기다리는 코드는 대략 아래와 같다. 

Restaurant rest = new Restaurant();

Pizza pz = rest->requestPizza();     // 요기서 피자 올때까지 계속 기다림!!

eat(pz); // 피자 오면 냠냠!!!! 

이런식으로 코드가 진행된다면  직관적이라  코드리딩이 쉽고 에러날 확률이 줄어들겠지만  피자만들고 배달오는 시간동안 기다려야하기때문에 굉장히 지루하면서, 시간낭비라 할수있다. 그래서 결국 피자만드는 시간이 빠르면 빠를수록 동기적으로코딩하는게 나을테고, 느리면 느릴수록 비동기적으로 코딩하는게 나을것이다. 

그럼 비동기적으로는 어떻게 될까?  위의 UML 에 나온 클래스들을 기반으로 대략 코드를 보자.

Restaurant rest = new Restaurant();
Future  ticket = rest->requestPizza(); //전화로 주문을 한후에, 전자티켓을 받는다.
boolean b = future->isComplete();     // 티켓을 통해서 완료됬는지 (배달이 집앞에 왔는지) 알수있다. 

if(b){

eat(future->getPizza());    // 집앞에 배달이 왔으면 냠냠 먹는다. 

} else{                                      // 배달 안왔으면 하던일을 한다.
      .... TV 를 보거나 설겆이을 한다 ....
       .... // 좀 지나서 

  while(!future->isComplete()){  // 시간이 지나서 배달 올 때쯤에 다시 확인해본다.   
          Sleep(100);                       // 아직 안왔으면 올때까지 기다린다.   
  }

  eat(future->getPizza());         // 피자를 냠냠!!! 

}

 이런식으로 Future 객체를 사용하는것이 Future 패턴이다.  보면 알겠지만  무엇인가 일을 시킨후에 바로 리턴받고 자기 할일을 한다. 리턴받은 객체(Future) 를 통해서 간간히 자기가 시킨 일의 결과를 확인할수있게된다. 

조금 코드가 복잡해졌지만, 시간 활용도가 매우 높아졌다. 기본적인 Future 패턴을 살펴보았는데, 위에 코드를 보면 isComplete 를 통해서 계속 완료됬는지 안됬는지 신경써야한다는 단점이 있다.

이제 개별 클래스들의 코드를 살펴보자.  (위의 예와 아주 약간 다르다)


 다이어그램에서 Client

public class Client {
    public static void main(String[] args) {
        System.out.println("main BEGIN");
        Host host = new Restaurant();        Data data1 = host .request(10, 'A');        Data data2 = host.request(20, 'B');
        Data data3 = host.request(30, 'C');

        System.out.println("main otherJob BEGIN");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        System.out.println("main otherJob END");

        System.out.println("data1 = " + data1.getContent());
        System.out.println("data2 = " + data2.getContent());
        System.out.println("data3 = " + data3.getContent());
        System.out.println("main END");
    }
}

개별 쓰레드들에 업무를 요청하고, 그 쓰레드들에서 작업이 완료되길 기다린다.
즉 getContent 가 리턴 될 때까지 기다린다. (내부에서 wait 하고 있을 것이다)


다이어그램에서 Restaurant

public class Restaurant{
    public Data request(final int count, final char c) {
        System.out.println("    request(" + count + ", " + c + ") BEGIN");

        final FutureData future = new FutureData();

        new Thread() {
            public void run() {
                RealData realdata = new RealData(count, c);
                future.setRealData(realdata);
            }
        }.start();

        System.out.println("    request(" + count + ", " + c + ") END");
        return future;
    }
}

쓰레드를 내부에 가지고 있으며 요청이 오면 Future 를 즉시 리턴해주고 진짜 결과를 만들기위해 쓰레드를 만들어서 일을 시작 한다.


 RealData (다이어그램에서 RealFood)

public class RealData implements Data {
    private final String content;
    public RealData(int count, char c) {
        System.out.println("        making RealData(" + count + ", " + c + ") BEGIN");
        char[] buffer = new char[count];
        for (int i = 0; i < count; i++) {
            buffer[i] = c;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
            }
        }
        System.out.println(" making RealData(" + count + ", " + c + ") END");
        this.content = new String(buffer);
    }
    public String getContent() {
        return content;
    }
}

실제 업무를 처리한다.


 FutureData (다이어그램에서 Future)

public class FutureData implements Data {
    private RealData realdata = null;
    private boolean ready = false;
    public synchronized void setRealData(RealData realdata) {
        if (ready) {
            return;     // balk
        }
        this.realdata = realdata;
        this.ready = true;
        notifyAll();
    }
    public synchronized String getContent() {
        while (!ready) {
            try {
                wait();
            } catch (InterruptedException e) {
            }
        }
        return realdata.getContent();
    }
}

메인쓰레드에서 바로 받아보는 가짜(미래) 결과물이다. 메인쓰레드에서는 직접 진짜 결과물을 직접 접근 할 수 없으며 Future 에게 완료되었는지 물어보고 기다려서 결과를 받아간다.


* 데이터 

public interface Data {

    public abstract String getContent();

}

FutureData 와 RealData 의 인터페이스