매우 길고 복잡한 글을 썼으나 5년만에 지웠습니다.
다시 읽어보니 재밌긴 한데, 그 글을 읽는 분들에게 복잡함을 더 가중 할 수 있겠다 싶어서요..
그냥 핵심만 잘 정리된 아래 링크로 대신해서 새 글을 올립니다. 
문자열 포맷 및 자바/웹개발에 대한 역사가 깊어서 인지 너무너무 복잡하죠. ㅎㅎ


Guide to Character Encoding
www.baeldung.com/java-char-encoding

[Spring] Spring UTF8 한글 설정하기
gmlwjd9405.github.io/2019/01/01/spring-utf8.html  

한글 인코딩의 이해 2편: 유니코드와 Java를 이용한 한글 처리
http://d2.naver.com/helloworld/76650

인코딩(Encoding)에 대한 이해 - (3) JSP 에서의 인코딩 설정 및 한글 매개변수 가져오기에 대한 고찰.  
http://okky.kr/article/284291

인코딩(Encoding)에 대한 이해 - (1) ASCII 부터 UTF-8 까지의 변화.
http://okky.kr/article/283470

JAVA String의 Encoding 과 Decoding에 대한 개념 정리
http://nomore7.tistory.com/entry/JAVA-Encoding-%EA%B3%BC-Decoding%EC%97%90-%EB%8C%80%ED%95%9C-%EC%A0%95%EB%A6%AC    

 




자바 쓰레드풀의 상태관리

쓰레드풀의 상태를 나타내는 멤버변수인 ctl 에 대해  (코딩 스킬적으로) 살펴보자.
*  코딩 스킬적으로 
 하나의 int 변수 안에  길이가 다른 다양한 특성을 담아둘때 이런 기법을 이용하시면 된다. 

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); // RUNNING 상태와 쓰레드 개수1로 초기화
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1; // 00011111 11111111 11111111 11111111

// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS; // 11100000 00000000 00000000 00000000
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;

private static int ctlOf(int rs, int wc) { return rs | wc; } // 2가지 상태를 하나의 Int 로 합쳐주는 함수

이 ctl 는 하나의 변수 안에 2가지 내용을 패킹시켜 두었다. 

첫째. workerCount   - 
현재  워커(쓰레드) 의 개수  (앞의 3비트 이용)
둘째. runState - 쓰레드풀의 현재 상태 (running, shutting down 등등)  (뒤의 29비트 이용) 

위의 코드를 보면 ctlOf 메소드를 통해서 초기에는 (RUNNING,0) 으로 초기화 시켜 놓은 부분이 보일 것이다. 즉 초기 상태는 
workerCount 는 0 이고 runState 는 RUNNING 이라는 것이다. 앞쪽비트는 runState 로 사용하고, 뒤쪽 비트는 workerCount 로 사용 한다는 것인데, 맞는지 확인 해보자.

- CAPACITY ( -1 을 해주는 이유는 하위 모든 비트를 1로 만들기 위해서이다. 대략 5억으로 workCount 의 용량제한이다.)
(1<< 29) - 1 = 2^29 -1  =  536870911 = 0x1FFFFFFF = 00011111 11111111 11111111 11111111
 

- RUNNING 은 다음과 같다.
-1 << 29  =  -536870912 =   0xE0000000 = 11100000 00000000 00000000 00000000

위 쪽의 3비트와 나머지 비트로 나누어 진 것을 알 수 있다. 

따라서 ctl 의 초기 상태는 다음과 같다.
11100000 00000000 00000000 00000000


workerCount
하나의 Int 타입으로 2가지 내용을 패킹하기 위해서 먼저 workerCount 를 대략 5억개로 제한시켜두었고 

private static final int COUNT_BITS = Integer.SIZE - 3; // 32-3 = 29

private static final int CAPACITY = (1 << COUNT_BITS) - 1; // 00011111 11111111 11111111 11111111

private static int workerCountOf(int c) { return c & CAPACITY; } // 현재 워커 갯수는?

비트마스킹이 조금이라도 더 빠르니깐 이렇게 한것이며, workerCount 는 워커 쓰레드의 숫자로써, 살아있는 쓰레드들의 실질적인 숫자로써 계속 달라 질 것이다.(예를들어 ThreadFactory 가 쓰레드 생성에 실패 했을 때, 종료되는 와중에도 여전히 태스크 처리를 수행 될 때 등등), 내부적으로 사용되는 workerCount와는 다르게 사용자에게 알려지는 풀의 크기는 worker 를 모아둔 workers(HashSet) 의 크기가 된다.  

현재 ctl 의 초기 상태는 다음과 같다. 이것이 workerCountOf 의 매개변수인 c 로 들어가게 되는데 

11100000 00000000 00000000 00000000   (ctl)
 00011111 11111111 11111111 11111111 (capacity)

이 둘을 비트연산(&)하면 0 이다. 맞는지 확인 해보자.
int c = ctl.get();   
int ws = workerCountOf(c); // ws 는 0

runState
RUNNING: 새로운 태스크를 받고,  큐에 집어 넣는 일을 하라.
SHOUTDOWN: 새로운 태스크를 받지마라, 그러나 이미 큐에 있는 태스크는 처리하라
STOP: 새로운 태스크를 받지마라, 큐에 있는 태스크도 처리하지 않는다. 현재 진행중인 태스크에 인터럽트를 건다.
TIDYING: 모든 태스크는 소멸되었고, workerCount 는 0 이다. TIDYING 상태로 전이되는 쓰레드는 terminated() 훅 메소드를 실행 시킬 것이다. 
TERMINATED: terminated() 메소드가 완료 되었다. 

전이형태는 다음과 같다.
RUNNING -> SHUTDOWN    ( shoutdown() 메소드 호출시)
(RUNNING or SHUTDOWN) -> STOP ( shoutdownNow() 메소드 호출시)
SHUTDOWN -> TIDYING ( 풀과 큐가 비었을 때)
STOP -> TIDYING (풀이 비었을 때) 
TYDYING -> TERMINATED (terminated() 메소드가 완료 되었을 때)


소프트웨어 엔지니어링에서 풀의 종류는 다양한데요.

쓰레드풀,메모리풀,캐쉬풀,커넥션풀,객체풀 (자바에서 객체풀은 사용을 지양합니다. 메모리를 할당하는 작업이 C/C++보다 빠름) 등등이 있습니다. "풀"어서 말하면 미리 만들어두고 돌려막기로 사용하자 라고 볼 수 있는데요. 미리 만들어 두는 방식 / 쓰레드가 태스크를 처리하는 방식에 따라서 다양한 풀의 구현체들이 있을 수 있습니다.  이 글에서는 openJDK8 기준의 자바에서 구현된 newFixedThreadPool 를 해부해보도록 하겠습니다.  



쓰레드풀은 동일하고 서로 독립적인 다수의 작업을 실행 할 때 가장 효과적이다.실행 시간이 오래 걸리는 작업과 금방 끝나는 작업을 섞어서 실행하도록 하면 풀의 크기가 굉장히 크지 않은 한 작업 실행을 방해하는 것과 비슷한 상황이 발생한다. 또한 크기게 제한되어 있는 쓰레드 풀에 다른 작업의 내용에 의존성을 갖고 있는 작업을 등록하면 데드락이 발생할 가능성이 높다. 다행스컯게도 일반적인 네트웍 기반의 서버 어플리케이션 (웹서버,메일서버,파일서버등)은 작업이 서로 동일하면서 독립적이어야 한다는 조건을 대부분 만족한다.  -  Java concurrency in practice 책 발췌 


전설의 개발자들이 참여한 저 엄청난 책은 자신들이 참여한 java.util.concurrent 패키지를 기준으로 한다. 근데 이 책이 2006년에 JDK1.5 기준으로 쓰여졌기 때문에 "동일하고 서로 독립적인 다수의 작업" 이라고 한정지었는데, 후에 JDK1.7에서 FORK-JOIN 풀이 나오면서 기존 풀들에서는 할 수 없었던 분할 수행작업을 효율적으로 할 수 있는 보완재가 되었다.

                             


1.newFixedThreadPool 사용법 (자바8기준)


execute 메소드


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream; 
public class SimpleThreadPool {
  public static void main(String[] args) {   
  ExecutorService executor = Executors.newFixedThreadPool(10);   
  IntStream.range(0, 10).forEach( n -> executor.execute( () -> 
        {       
          try {         
                TimeUnit.MILLISECONDS.sleep(300);         
                String threadName = Thread.currentThread().getName();         
                System.out.println("Hello " + threadName);        
          } catch (InterruptedException e) {
             e.printStackTrace();        
          }     
      })   
  );  
}
}

- 쓰레드풀에 쓰레드를 10개를 뛰어놀게 한다. 
- 10번을 반복해서 쓰레드풀에 일을 시킨다. (쓰레드풀안의 쓰레드 하나가 선택되어 일처리를 할것이다)
- execute 메소드를 사용했다. 이 메소드는 void 를 리턴한다. 즉 일처리를 시키기만 하지 결과를 보고받지 않을것이다.
- 구현내용은 300밀리초를 기다렸다가 Hello 쓰레드이름을 출력하는 것이다. 

- 자바8부터는 for(int i = 0; i < 10; i++) 보다는 저렇게 사용하는게 좋다.


submit 메소드

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.*;

public class SimpleThreadPool {

  public static void main(String[] args) {

    ExecutorService executor = Executors.newFixedThreadPool(2);
    final List<Integer> integers = Arrays.asList(1,2,3,4,5);
    Future<Integer> future = executor.submit(() -> {
      TimeUnit.MILLISECONDS.sleep(5000);
      int result = integers.stream().mapToInt(i -> i.intValue()).sum();
      return result;
    });

    try {
      Integer result = future.get();
      System.out.print("result: " + result);
      executor.shutdownNow();
    } catch (InterruptedException e) {
      e.printStackTrace();
    } catch (ExecutionException e) {
      e.printStackTrace();
    }
  }
}

- 쓰레드풀에 쓰레드를 10개를 뛰어놀게 한다. 
- 쓰레드풀에 하나의 일을 시킨다. (쓰레드풀안의 쓰레드 하나가 선택되어 일처리를 할것이다)
- submit 메소드를 사용했다. 이 메소드는 future 를 리턴한다. 즉 일처리를 시키기고, 그에 따른 결과를 보고받을 것이다.
- 구현내용은 5000밀리초를 기다렸다가 리스트안의 숫자들의 합을 리턴한다.

- 리턴 받은 future 로 부터 값을 얻는다. 여기서 get()메소드는 블럭된다. (타임아웃을 매개변수로 넣을 수도 있다) 아마 일처리를 하는 쪽의 쓰레드에서 일을 다 끝내고 set() 같은 것을 해 줄 때가지 블럭될거 같다.


이제 사용법은 알았으니 과연 쓰레드풀을 자바에서 어떻게 구현하고 있는지 확인 해보자.

@어떻게 10개의 쓰레드를 가진 풀을 만드는지? (newFixedThreadPool(10))
@어떻게 풀에서 쓰레드 하나를 할당해 주는지? (execute)

@쓰레드풀에서 할당 될 워커(쓰레드)가 없을 때 태스크가 어떻게 되는지?

@어떻게 future 를 리턴해주는지. (submit



2.newFixedThreadPool 소스 분석(자바8기준)


* 참고로 지면이 한정된 관계로 예외처리 포함해서 여러 로직들이 생략되었다.





(발췌: http://codepumpkin.com/threadpool-using-executor-framework/)




2-1. 어떻게 10개의 쓰레드를 가진 풀을 만드는지? (newFixedThreadPool(10))

public class Executors {

java/util/concurrent/Executors.java 라는 파일에는 Executors 라는 클래스가 있으며, 정적메소드로써 newFiexedThreadPool 이라는 것을 제공한다. 매개변수는 쓰레드풀에 들어갈 쓰레드들의 개수를 넣어준다.


public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>());
}

퍼사드 역할 (Gof의 패턴중 하나로 내부 시스템의 복잡함을 감추고 사용자가 간편히 사용하도록 하는 의도)

을 하고있다. 그저 팩토리로써 내부의 ThreadPoolExecutor 객체를 대신 만들어주고 있는데, 매개변수로는 순서대로 살펴보자.


- corePoolSize : 풀 안에 유지되는 쓰레드 개수 (시작시) 

- maximumPoolSize : 풀에 유지되는 최대 쓰레드 개수

- keepAliveTime : corePoolSize 보다 쓰레드 개수가 많아 질 때, 새로운 테스크를 기다리기 위한 시간. 시간이 지나면 쓰레드를 없애서 corePoolSize 를 유지한다.

- unit :  keepAliveTime 시간단위

- workQueue : 실행 되기전에 홀드시켜 두는 태스크를 유지하는 큐. 쓰레드가 남지 않을 경우 여기 태스크를 넣는다.


시작개수가 1이고 최대개수가 1이면  newSingleThreadExecutor 이 되는것이고,

시작개수가 0이고 최대개수가 MAX_VALUE 가 되면, newCachedThreadPool 풀이라 한다.
우리는 최소,최대가 10개로 고정된 쓰레드풀을 만든 것이다.

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

java/util/concurrent/ThreadPoolExecutor.java 라는 파일에는 ThreadPoolExecutor라는 클래스가 있으며, 들어온 매개변수에 추가로 Executors.defaultThreadFactory()와 defaultHandler 를 추가 매개변수로 받아서 새 객체를 만든다.

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}


 Executors.defaultThreadFactory()
이게 먼가 쓰레드를 풀에 생성하는 것과 관련이 있는거 같아서 따라가 본다.
public static ThreadFactory defaultThreadFactory() {
    return new DefaultThreadFactory();
}
static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                              Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                      poolNumber.getAndIncrement() +
                     "-thread-";
    }

    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        if (t.isDaemon())
            t.setDaemon(false);
       
        return t;
    }
}

- 쓰레드 그룹이라는 것을 만들어 두고 있다. 자바는 하나의 객체안에 여러개의 쓰레드들을 그루핑하기 위한 편리한 방식을 제공하는데, 이 방식을 통해서suspend, resume, interrupt 같은 콜을 한방에 적용되게 할 수 있다.
- pool 의 첫번째 쓰레드라는 이름을 지정해주고 있다. "pool-1-thread-"
- newThread 라는 메소드를 제공하는거 봐서 역시 이 객체는 새로운 쓰레드를 생성하는 팩토리였던 것이다.
- 비데몬 쓰레드로 만든다. (메인쓰레드가 종료되도 종료되지 않고 살아있는)

결론적으로 위의 코드는 각종 세팅만 했지 실질적으로 쓰레드를 만들지는 않았다. 그럼 언제 만드는 것일까?

2-2. 어떻게 풀에서 쓰레드 하나를 할당해 주는지? (execute)

이번에는 executor.execute( 태스크 이 호출에 대해서 따라가 보자. 

public void execute(Runnable command) {

  int c = ctl.get(); 
  // 1 단계 
  if (workerCountOf(c) < corePoolSize) {
    if (addWorker(command, true))
      return;

  }

  // 2 단계
    .... 생략 ....
}
위 소스에서 1단계만 살펴보자. 만약 corePoolSize 보다 작은 개수의 쓰레드가 풀 안에 생성되있다면 새로운 쓰레드를 만들어서 새로 들어온 태스크(위에 소스에서 command) 를 처리하고 리턴해준다. addWorker(command, true) 를 쫒아가보자.

private boolean addWorker(Runnable firstTask, boolean core) {
  .. 상태체크 부분 생략 .. 

  Worker w = null;

  w = new Worker(firstTask); // (1)
  final Thread t = w.thread;
  if (t != null) {

    workers.add(w); // (1)
    t.start();
    workerStarted = true;

  }

  return workerStarted;
}
쓰레드 생성 및 태스크 할당에 촛점을 맞추어 천천히 살펴보자.

1. Worker 객체를 만들고 태스크를 할당한다. 
  w = new Worker(firstTask); // (1)
Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
}
Worker 생성자에서 반가운 손님을 보게되는데 newThread 를 통해서 태스크를 새로운 쓰레드에 할당하는 모습이다.
여기서 눈여겨 봐야 할 것이, 워커객체는 쓰레드를 참조하고 있으며, 역으로 쓰레드에도 this (워커객체)가 매개변수로 들어가는 것인데, 쓰레드는 워커객체 this 를 통해서 할당된 태스크에 대한 처리를 하리라는 것을 알 수 있다. 즉 쓰레드에 태스크가 직접적으로 할당되는 것이 아니라, 워커객체라는 대리자를 통해서 관리되는 것. 

2.쓰레드풀의  workers 컬렉션에 Worker 객체를 보관한다. 
workers.add(w);

즉 쓰레드풀은 쓰레드를 직접 관리하는게 아니라, 워커라는 대리자를 통해서 관리한다.

3. 쓰레드를 실행 시킨다. 
t.start();
워커를 실행시키는게 아니라, 쓰레드를 직접 실행시킨다. 이렇게 되면 쓰레드는 워커 참조를 내부적으로 가지고 있기 때문에, 워커 참조를 통해 워커가 가지고 있는 우리가 전달한 태스크를 실행 시킬 것이다. 참고로 좀 더 정밀하게 컨트롤하기 위한 상태체크 부분들은 모두 빠져있는데 나중에 여유가 생기면 각자 살펴보도록 하자.



2-3. 쓰레드풀에서 할당 될 워커(쓰레드)가 없을 때 태스크가 어떻게 되는지?

10개의 고정된 크기의 쓰레드풀을 만들었는데, 12개의 태스크를 넣어주면 어떻게 될까? 예상대로 10개의 워커(쓰레드) 만 생겨서 10개의 태스크만 처리하다가, 먼저 끝난 워커가 나머지 2개중 하나의 태스크를 처리하게 된다. 이 과정에 대해서 소스로 살펴보도록 하자.
public void execute(Runnable command) {
        // 1단계
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        // 2단계 
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }
1단계에서는 아직 워커(쓰레드)가 10개가 안되었을때, addWorker 를 통해 워커도 만들고 쓰레드도 만들어서 들어온 태스크를 처리하게 된다.
2단계에서는 이제 더 이상 처리할 워커(스레드)가 없을 경우에 대해서 처리하는데, workerQueue.offer(command) 즉  워커큐에 태스크를 넣어주고 리턴한다. 

여기서 예상 할 수 있는 것이 워커(쓰레드)가 자신이 담당한 태스크 처리를 끝내면 저 큐에서 기다리고 있는 태스크를 가져다가 실행 하리라는 것을 눈치 챌 수 있을 것이다. 맞는지 확인해보자.
 final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
  
        while (task != null || (task = getTask()) != null) {

            ... 생략 ...

            task.run();
                   
            ... 생략 ...
        }
           
    }
 예상대로 워커객체의  runWorker() 메소드를 보면 처음 워커가 시작하고 나서 while 문을 돌면서 들어오는 태스크를 꾸준히 처리해 주고 있다. 즉 쓰레드와 워커객체는 풀이 살아있는 동안 계속 살아 있는 것이다  (자동 소멸이 필요하면 그렇게 쓰레드풀을 구현 할 수도 있을 것이며, 이미 있다) . 가져온 태스크는 task.run()을 해주고 있다. 기억하라 우리의 task 는 Runnable 인터페이스를 상속받은 객체이고 run 을 구현했었다. getTask() 를 통해서 태스크를 가져오는 것이 보일 것이다. 그 안에서  workerQueue 에서 태스크를 가져올것이다. 확인해보자.

 private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?

        for (;;) {
                   .. 생략 ..

            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
              
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }
 workerQueue 에서 (이름을 TaskQueue 라고 해야지 않나 싶다) 태스크를 가져오고 있다.


2-4. 어떻게 future 를 리턴해주는지. (submit
execute 메소드와 비슷할 거라 예상되는 submit 메소드를 살펴보자.
public <T> Future<T> submit(Callable<T> task) {
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}
java/util/concurrent/AbstractExecutorService.java 는 ThreadPoolExecutor 의 부모클래스인데 submit는 여기에 존재한다.

1. 건네어진 태스크를 RunnableFuture 인터페이스를 상속받는 FutureTask 로 만든 후에
public FutureTask(Callable<V> callable) {
  this.callable = callable;
  this.state = NEW; // ensure visibility of callable
}
2. Runnable 인터페이스가 아닌 RunnableFuture 를 매개변수로 받는 execute 메소드로 보내진다.
   참고로 RunnableFuture 는 Runnable 인터페이스를 상속받았기 때문에 문제없다.


public void execute(Runnable command) {

  int c = ctl.get(); //1 단계
  if (workerCountOf(c) < corePoolSize) {
    if (addWorker(command, true))
      return;
  c = ctl.get();
  }

  //2 단계
    .... 생략 ....
}


위의 addWorker 안에서는 쓰레드를 실행시키는데,(addWorker 코드는 위쪽에 이미소개했다.)
public void run() {
  runWorker(this);
}
Worker 객체의 run 메소드안에는 runWorker 메소드를 실행시킨다. 살펴보자.
final void runWorker(Worker w) {
  Thread wt = Thread.currentThread();
  Runnable task = w.firstTask;
  w.firstTask = null;

  try {

    task.run();

  } finally {
    processWorkerExit(w, completedAbruptly);
  }
}
이게 핵심 중 하나인데 Worker 객체에서 태스크를 실행 시키고 널로 세팅한다

새로운 태스크가 할당될때마다 쓰레드안에 넣어주면 되는 것이며, 쓰레드를 소멸 시킬 필요없이 새로운 태스크를 받아서 계속 활용 하는 것이 쓰레드풀의 아이디어 이며, 이 글에서 생략된 나머지 상태관리 구현은 매우 복잡하긴 하지만 핵심은 아니다. 

  task.run();
에 주목하자. runnable 이나 runnableFuture 는 각각 run을 구현한다.

public void run() {

  try {
    Callable<V> c = callable;
    if (c != null && state == NEW) {
    V result;
    boolean ran;
    try {
      result = c.call();
      ran = true;
    } catch (Throwable ex) {
      result = null;
      ran = false;
      setException(ex);
    }
    if (ran)
      set(result);
    }
  }

....
}
- run 내부에서 태스크를 실행시키고 있다. c.call() 리턴받은 result 를 set 메소드로 넘겨준다.

마지막으로 Future get() 과 set() 매칭에 대해서 생각해보자.
글 서두에 활용 예제 코드에서 Integer result = future.get();  기억 나는가?
future 객체의 get()메소드를 통해 무엇인가 해결이 됬으면 결과를 리턴받고 있는데, get()이 완료되려면 바로 위의 코드에서 set(result)가 호출되어야 한다.
if (ran)
    set(result);
이거 말이다. set 과 get 메소드의 코드는 아래에 있는데 이 부분은 쓰레드 풀링과 직접적인 관계가 없는 주제이므로 이 글을 읽는 분에게 맡겨 놓겠다.

set 메소드
protected void set(V v) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = v;
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
        finishCompletion();
    }
}

get 메소드


public V get() throws InterruptedException, ExecutionException {
  int s = state;
  if (s <= COMPLETING)
    s = awaitDone(false, 0L); // awaitDone 에서 블럭되고 있음을 알수 있다. 하염없이 기다림 
  return report(s);
}



부록: 쓰레드풀의 상태관리)

쓰레드풀의 상태를 나타내는 멤버변수로 ctl 에 대해 살펴보자.

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); // RUNNING 상태와 쓰레드 개수1로 초기화
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1; // 00011111 11111111 11111111 11111111

// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS; // 11100000 00000000 00000000 00000000
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;

private static int ctlOf(int rs, int wc) { return rs | wc; } // 2가지 상태를 하나의 Int 로 합쳐주는 함수

이 ctl 는 하나의 변수 안에 2가지 내용을 패킹시켜 두었다.

첫째. workerCount   -
현재  워커(쓰레드) 의 개수
둘째. runState - 쓰레드풀의 현재 상태 (running, shutting down 등등)

위의 코드를 보면 ctlOf 메소드를 통해서 초기에는 (RUNNING,0) 으로 초기화 시켜 놓은 부분이 보일 것이다. 즉 초기 상태는 
workerCount 는 0 이고 runState 는 RUNNING 이라는 것이다. 앞쪽비트는 runState 로 사용하고, 뒤쪽 비트는 workerCount 로 사용 한다는 것인데, 맞는지 확인 해보자.

- CAPACITY ( -1 을 해주는 이유는 하위 모든 비트를 1로 만들기 위해서이다.)
(1<< 29) - 1 = 2^29 -1  =  536870911 = 0x1FFFFFFF = 00011111 11111111 11111111 11111111
 

- RUNNING 은 다음과 같다.
-1 << 29  =  -536870912 =   0xE0000000 = 11100000 00000000 00000000 00000000

위 쪽의 3비트와 나머지 비트로 나누어 가진 것을 알 수 있다. 

따라서 ctl 의 초기 상태는 다음과 같다.
11100000 00000000 00000000 00000000

workerCount
하나의 Int 타입으로 2가지 내용을 패킹하기 위해서는 먼저 workerCount 를 대략 5억개로 제한시켜두었고 

private static final int COUNT_BITS = Integer.SIZE - 3; // 32-3 = 29

private static final int CAPACITY = (1 << COUNT_BITS) - 1; // 00011111 11111111 11111111 11111111

private static int workerCountOf(int c) { return c & CAPACITY; } // 현재 워커 갯수는?

비트마스킹이 조금이라도 더 빠르니깐 이렇게 한것이며,위에서 workerCount 가 쓰레드 개수라고 했듯이, 그 숫자가 더 넘어서게도면 AtomicLong 으로 교체 해야 할 수도 있겠다.

workerCount 는 워커의 숫자로써, 살아있는 쓰레드들의 실질적인 숫자로써 계속 달라 질 것이다.(예를들어 ThreadFactory 가 쓰레드 생성에 실패 했을 때, 종료되는 와중에도 여전히 태스크 처리를 수행 될때 등등), 내부적으로 사용되는 workerCount와는 다르게 사용자에게 알려지는 풀의 크기는 worker 를 모아둔 workers(HashSet) 의 크기가 된다.  

현재 ctl 의 초기 상태는 다음과 같다. 이것이 workerCountOf 의 매개변수인 c 로 들어가게 되는데 

11100000 00000000 00000000 00000000   (ctl)
 00011111 11111111 11111111 11111111 (capacity)

이 둘을 비트연산(&)하면 0 이다. 맞는지 확인 해보자.
int c = ctl.get();   
int ws = workerCountOf(c); // ws 는 0

runState
RUNNING: 새로운 태스크를 받고,  큐에 집어 넣는 일을 하라.
SHOUTDOWN: 새로운 태스크를 받지마라, 그러나 이미 큐에 있는 태스크는 처리하라
STOP: 새로운 태스크를 받지마라, 큐에 있는 태스크도 처리하지 않는다. 현재 진행중인 태스크에 인터럽트를 건다.
TIDYING: 모든 태스크는 소멸되었고, workerCount 는 0 이다. TIDYING 상태로 전이되는 쓰레드는 terminated() 훅 메소드를 실행 시킬 것이다. 
TERMINATED: terminated() 메소드가 완료 되었다. 

전이형태는 다음과 같다.
RUNNING -> SHUTDOWN    ( shoutdown() 메소드 호출시)
(RUNNING or SHUTDOWN) -> STOP ( shoutdownNow() 메소드 호출시)
SHUTDOWN -> TIDYING ( 풀과 큐가 비었을 때)
STOP -> TIDYING (풀이 비었을 때) 
TYDYING -> TERMINATED (terminated() 메소드가 완료 되었을 때)



참고:

http://tutorials.jenkov.com/java-util-concurrent/executorservice.html 

http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/00cd9dc3c2b5/src/share/classes/java/util/concurrent/Executors.java 




잡설


개인적으로 리소스 해제와 관련해서 가장 먼저 떠오르는것은 C++의 포인터이다. 리소스를 해제하지 않아서 생기는 문제 또는 해제한 리소스를 사용하려고 해서 생기는 문제는 대규모 솔루션에서는 가장 골치 아픈 걱정거리가 되곤 하는데 스택트레이스에 잡히지 않는 머나먼 곳에서 해당 포인터를 조작한 것들이기에 전체 코드를 샅샅히 살피지 않으면 풀기 힘든 문제로 남기도 한다. (널포인터 문제와 레이스 컨디션 문제는 모든 솔루션에서 가장 대다수의 버그로 리포팅되며, 다른 폭탄을 야기하며, 해결하기 어려운 2가지 문제) C++은 이를 해결하고자 다음과 같은 스마트 포인터들을 제공하지만 스마트포인터 종류 분석  제대로 잘 활용하는 것도 문제이다.

자바(VM류)가 떠오르면서 리소스 관리로 부터 어느정도는 해방시켜 주었다. 하지만 외부리소스(파일,소켓,디비connection 등 의 리소스) 에 관해서는 자바도 사용자가 직접 리소스해제를 해야하는데, 리소스해제를 까먹는 경우가 생겨나서 말썽이 된다. 개발자 잘못이라고? 뭐 그렇게 볼수도 있지만 바야흐로 2018년. 개발자가 잘못하지 않게 만들어주는 덕목도 중요하리라~

지금부터 소개할 내용은 자바와 스칼라에서의 자동 리소스 해제 및 비슷한 상황으로 볼 수 있는 동기화객체 자동 해제 에 관련된 것들이며 소개차원의 범위를 가진다. 또한 정답도 없다.


* 참고로 다른 언어들도 관련된 것들이 있다. 예를들어 파이썬의 with 구문


- ARMAutomatic Resource Management 자동 리소스 관리를 지칭하는 일반적인 용어
- EAMEexcute Around Method Pattern 함수 실행시 규칙적으로 일어나는 일을 묶어 놓는 패턴 (리소스 할당/해제에만 국한된 개념이 아니다. 트랜잭션,락등에도 사용할 수 있다. AOP ,Proxy Pattern, Decorator Pattern 등도 비슷한 일을 의미/활용하긴 하는데 이것들의 의미는 좀 더 넓다)  


 리소스릭 막기



Java 


다음 코드를 보자.

private void incorrectWriting() throws IOException {
  DataOutputStream out = new DataOutputStream(new FileOutputStream("data"));
  out.writeInt(666);
  out.writeUTF("Hello");
  out.close();
}

파일 리소스를 얻어서 데이터를 쓰고(write), flush 로 밀어내고,리소스를 반납(close)를 하는 소스이다. 여기서의 문제는

1. close를 잊어버리는 개발자가 많이 있다. (개발자 수준이 낮아서 그렇다고? 쩌는 개발자들이 참여하는 훌륭한 오픈소스에서도 equals 와 == 실수 조차도 엄청 많이 발견된다. 실수는 누구나 할 수 있다.) 


2. close 를 하더라도 쓰는 과정에서 예외가 발생할 경우 리소스 누수가 생겨난다. 

따라서 리소스 누수 문제를 해결하기 위해서 아래와 같이 코딩한다.


private void correctWriting() throws IOException {
  DataOutputStream out = null;
  try {
    out = new DataOutputStream(new FileOutputStream("data"));
    out.writeInt(666);
    out.writeUTF("Hello");
  } finally {
    if (out != null) {
      out.close();
    }
  }
}

보시다사피 try ~ finally 블럭으로 감싸서, 무슨일이 있어도 finally 블록안의 내용을 실행하도록 하였다.
문제는 없다. 하지만 먼가 상투적인 코드가 남발되어 있다는 느낌적인 느낌? 강제가 아니라서 대충 안하고 넘겨 버릴거 같은 느낌? 

Java7에서는 이렇게 해결한다.


Java7 (try-with-resources 제공)


private void writingWithARM() throws IOException {
  try (DataOutputStream out = new DataOutputStream(new FileOutputStream("data"))) {
    out.writeInt(666);
    out.writeUTF("Hello");
  }
}

별거 없다. 그냥 코드를 좀 줄여주는 장치를 추가한거다. try 블럭의 모양새가 변했다. 
try 문안에 해제되야할 리소스 할당부분을 적어주고, 본문에 로직을 적어 주면 된다. (참고로 python 은 with 문으로 할당부분을 감싼다) 그러면 자동적으로 try 문을 벗어날때 자동적으로 close 를 호출해 준다.
이 특별한 형식을 try-with-resources 라고 부른다.

근데 저런 형식이 거저로 얻어지는 것은 아니다. 클래스 만드는 사람은 손이 더 가야한다.
try-with-resources 를 사용하려면 java.lang.AutoCloseable 인터페이스를 구현해야한다.


public class AutoClose implements AutoCloseable {

  @Override
  public void close() {
    System.out.println(">>> close()");  // DataOutputStream 경우는 내부에서 스트림에 대한 close()를 호출

  }

  public void work() throws MyException {
    System.out.println(">>> work()");
  }
}

이렇게 AutoCloseable 인터페이스를 통해 close 를 오버라이드 하면 된다.


public static void main(String[] args) {
  try (AutoClose autoClose = new AutoClose()) {
    autoClose.work();
  } 
}

이제 try-with-resources 를 사용할 수 있다.

* 참고로 JDK7 의 OuputStream 를 보면 public abstract class OutputStream implements Closeable, Flushable { 로 되있는데 Closeable 은 AutoCloseable 을 상속받은 인터페이스이며 해당i/O 부분에 특화되어있다. (IOException 을 발생시킴) 


자바7 ARM 실용예제 실습 

import java.io.FileWriter;
import java.io.IOException;
import java.lang.AutoCloseable;

public class FileWriterARM implements AutoCloseable {
    private final FileWriter writer;

    public FileWriterARM(final String fileName) throws IOException {
        writer = new FileWriter(fileName);
    }

    public void writeStuff(final String message) throws IOException {
        writer.write(message);
    }

    public void close() throws IOException {
        System.out.println("close called automatically...");
        writer.close();
    }
    //...
    public static void main(final String[] args) throws IOException {
        try(final FileWriterARM writerARM = new FileWriterARM("peekaboo.txt")) {
                writerARM.writeStuff("peek-a-boo");
                System.out.println("done with the resource...");
        }
    }
}

뭐 좋은거 같긴하다. 근데 문제가 있을까? 저런게 (try 구문및 AutoCloseable 인터페이스등) 있다는 것을 기억하고 사용을 하게 사용자에게 책임을 넘겨야 한다는 것이 문제라고 하면 문제이다. 저걸 할 사람들이 모여있는 곳이라면 그냥 try ~finally 감싸고 close 도 잘 해주겠지. (농담이다. 저런것을 팀 차원에서 강제한다는거 자체가 문제를 해결하는 방법일 수도 있다.) 암튼 더 일반화/강제화 할 수 있는 방법도 있을까? Java8에서의 람다를 이용한 방식을 살펴보자. 


Java8 (EAM) 

Java8에는 드디어 염원하던 람다식이 도입되므로써, 함수형 프로그래밍을 자바에서 할 수 있는 멋진 도구를 선물 해 주었다.이 람다식을 활용하면 리소스 할당/해제라든지, 트랜잭션,락 시작/종료와 같은 '틀'에 박힌 행동을 다른 방식으로 관리 할 수 있게 해주는데 어떤식으로 하는지 살펴보자.  (다른 방식이지 정답이거나 가장 좋은 방식이라고 말 할 순 없다.) 


import java.util.function.Consumer;
import static java.lang.System.out;

class JavaResource {
  private JavaResource() {out.println("created...");}
  public void operation1() {out.println("operation 1");}
  public void operation2() {out.println("operation 2");}
  private void close() { out.println("cleanup");}

  public static void use(Consumer<JavaResource> block) {
    JavaResource resource = new JavaResource();
    try {
      block.accept(resource);
    } finally {
      resource.close();
    }
  }
}

Interface Consumer<T>가 있다. 자바8에서는 자주 이용되는 함수형 인터페이스를 java.util.function 을 통해서 제공하는데, 함수형 인터페이스는 메서드가 하나만 정의 되있다고 약속하였다. Consumer 인터페이스는 accept 를 통해 하나의 인자를 매개변수로 받아서 어떤 행위(객체T를 받아서 소비,부수효과를 일으킴)를 하고 리턴은 안하는 것으로, 함수형 프로그래밍에서는 최대한 부수효과(side effect) 를 없애지만 어쩔 수 없이 필요할때는 이렇게 잘보여지도록 표현한다. 

위의 코드에서는 객체생성,정리 부분을 자기가 처리하는게 아니라 use 메소드를 통해서 위임하고 있다. (private를 보라)  use 는 어떻게 보면 팩토리패턴(생성) + 리소스정리(close)가 함께 존재하는 것으로써, 내부에서 객체생성을 하고 close()메소드도 호출해준다.

(역시 자바는 accept 라는것을 강제하는등 조금 불편한 구석이 있다. 신규 개발은 스칼라로 해보는것도 좋을 것이다) 


public class Main {
  public static void main(String... args) {

    JavaResource.use(resource -> {
      resource.operation1();
      resource.operation2();
    });

  }
}

이제 생성,해제를 함께 해주는 JavaResource.use 팩토리에게 맞기고, 관심사인 부수효과적인 행위에 대한 것만 Interface Consumer<T>를 타겟으로 하는 람다식을 구현하면 된다. 


(resource -> {
      resource.operation1();
      resource.operation2();
    }

그 람다식은 위와 같으며, 위의 resource 는 accept 의 매개변수로 들어가서 operation1() 과 operation2() 라는 부수효과를 발생시킬 것이다. 결국 이런 정형화된 패턴의 코드를 통해 위험한(?) 객체를 직접적으로 사용하는 것을 금지당하므로써, 보다 안정적인 솔루션이 만들어 질 것이다.

좀 더 실용적인 예를 살펴보자.


람다식 + 함수형 인터페이스 실습 

import java.sql.Connection;
import java.sql.SQLException;

public interface Transaction {
  public void execute(Connection connection) throws SQLException;
}

Transaction 이라는 인터페이스가 있다. 한번의 트랜잭션을 일으키기 원할 때 사용하는데, 이 인터페이스를 어떻게 사용하는지 아래 예제에서 확인하자.


import java.sql.Connection;
import java.sql.DriverManager;

public class TransactionHandler {

  public static void runInTransaction(Transaction transaction) throws Exception {

    Connection dbConnection = createDatabaseConnection();
    dbConnection.setAutoCommit(false);

    try {

      System.out.println("Starting transaction");
      transaction.execute(dbConnection);

      System.out.println("Committing transaction");
      dbConnection.commit();

    } catch (Exception e) {

      System.out.println(e.getMessage());
      System.out.println("Rolling back...");
      dbConnection.rollback();
    } finally {
      dbConnection.close();
    }
  }

  private static Connection createDatabaseConnection() throws Exception {
    Class.forName("com.mysql.jdbc.Driver");
    return DriverManager.getConnection("jdbc:mysql://localhost:3306/ticket_system", "user", "password");
  }
}

코드 자체로 모든 설명이 들어가 있기에 추가적으로 설명 할 필요는 없을 거 같다.


"트랜잭션 핸들러" 클래스가 runInTransaction 이라는 스태틱 메소드를 통해 생성/정리를 대행해주고 있다. 전형적인 데코레이터 패턴이라고 볼 수 있다. (*자바에서는 이런곳에 사용하라고 전문적으로 다이내믹 프록시라는 기술을 제공하며, 하둡코어에서는 일반 함수를 호출하는 듯하지만 내부적으론 RPC 를 하는데 그것이 사용되고 있다.)


  public static void runInTransaction(Transaction transaction) throws Exception {


이렇게 Transaction 인터페이스에 해당하는 로직을 넘겨 줘야하는데, 

TransactionHandler.runInTransaction(connection -> {

  int ticketId = findAvailableTicket(connection);

  reserveTicket(ticketId, connection);
  markAsBought(ticketId, connection);
});

람다식을 통해 Transaction 인터페이스를 타겟으로 갖는 비지니스 로직을 가시권에 들어오게 직접 만들었다. 함수형 인터페이스는 메소드가 하나만 정의되 있으므로, execute 라는 메소드는 자동으로 매칭해준다.


람다식 + Consumer 함수형 인터페이스 실습 (리소스 해제)

import java.io.FileWriter;
import java.io.IOException;
import java.lang.AutoCloseable;

public class FileWriterEAM  {
  private final FileWriter writer;

  private FileWriterEAM(final String fileName) throws IOException {
    writer = new FileWriter(fileName);
  }
  private void close() throws IOException {
    System.out.println("close called automatically...");
    writer.close();
  }
  public void writeStuff(final String message) throws IOException {
    writer.write(message);
  }
  //...
  public static void use(final String fileName, final UseInstance<FileWriterEAM, IOException> block) throws IOException {

    final FileWriterEAM writerEAM = new FileWriterEAM(fileName);
    try {
      block.accept(writerEAM);
    } finally {
      writerEAM.close();
    }
  }

  public static void main(final String[] args) throws IOException {
    FileWriterEAM.use("eam.txt", writerEAM -> {
      writerEAM.writeStuff("how");
      writerEAM.writeStuff("sweet");
    });
  }
}

 use 를 통해서 객체생성,실행,해제를 모두 처리 해 주고 있다. 딱 정해져 있기 때문에 클래스 사용자가 실수를 범할 가능성이 없어졌다.


람다식 + Runnable 함수형 인터페이스 실습 (동기화 객체 자동 해제)

import java.util.concurrent.locks.Lock;

public class Locker {
  public static void runLocked(Lock lock, Runnable block) {
    lock.lock();

    try {
      block.run();
    } finally {
      lock.unlock();
    }    
  }
}

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static fpij.Locker.runLocked;

public class Locking {
  Lock lock = new ReentrantLock(); //or mock
  protected void setLock(final Lock mock) {
    lock = mock;
  } 
  public void doOp1() {   // 행사코드 남발!!!!
    lock.lock();
    try {
      //...critical code...
    } finally {
      lock.unlock();
    }
  }
  
  // 깔끔해 졌다!!
  public void doOp2() {
    runLocked(lock, () -> {/*...critical code ... */});
  }
  public void doOp3() {
    runLocked(lock, () -> {/*...critical code ... */});
  }
  public void doOp4() {
    runLocked(lock, () -> {/*...critical code ... */});
  }
}


Scala (EAM) 

class ScalaResource private {
  println("created...")

  def operation1() = println("operation 1")
  def operation2() = println("operation 2")
  private def close() = println("cleaning up")
}
object ScalaResource {
  def use(closure : ScalaResource => Unit) = {
    val resource = new ScalaResource
    try {
      closure(resource)
    } finally {
      resource.close()
    }
  }
}
object MainEAM extends App {
  ScalaResource.use { resource =>
    resource.operation1()
    resource.operation2()
  }
}

-> 가 => 로 바뀌고 val, object같은 키워드가 있다는 것 정도의 기본 문법 차이를 제외하고 중요 포인트는 
- 스칼라는 굳이 Consumer 인터페이스라는것이 있을 필요가 없다는것~ 
- 자바에서 아주 군더더기 처럼 느껴졌던 accept 를 사용하지 않아도 된다.




참고:

http://www.oracle.com/technetwork/articles/java/trywithresources-401775.html 

https://www.geeksforgeeks.org/automatic-resource-management-java/ 

https://gist.github.com/dpsoft/9013481 

http://chrisoldwood.blogspot.kr/2011/07/execute-around-method-subsystem.html 

http://www.deadcoderising.com/transactions-using-execute-around-method-in-java-8/ 

https://pragprog.com/book/vsjava8/functional-programming-in-java 




인터페이스 vs 추상 클래스 

예전에 이 캐캐묵은 논쟁에 대한 글을 쓴적이 있었는데 , 그 중 일부를 발췌해보면 

" 먼저 왜 자바는 인터페이스를 만들게 됬나를 생각해야 하며 , 다중상속은 이 둘을 구분하는데 중요한 맥이 아닙니다중요한것은 인터페이스가 추구하는 바를 추상클래스로 이루려면  가능은 하지만 지켜야 할 제약이 생기고 위험/복잡성 또한 생기기 마련입니다. 또한 약속된 관례가 없기 때문에 커뮤니케이션에 문제가 생기며 의도가 불분명해 집니다.  그래서 행위의 틀 immutable 한 속성만으로 제한해서 유연성과 사용성을 극대화한것이 자바의 "interface" 입니다.  ... 후략 "

뭐 이런글을 썼었습니다만..

이런 저의 글과 여러 무수한 논쟁들이 자바8의 등장과 더불어 소각장으로 직행해야 할 가비지가 되었습니다.

자바의 인터페이스가 자바8에서 엄청난 변화를 가져 왔는데요. 즉 디폴트 메소드 구현, 정적메소드 구현 등 인터페이스 자체도 심플한 "틀" 뿐 만 아니라 "구현" 을 가질 수 있게 되었습니다. 장점은 강력해 졌다겠는데 단점은 복잡한게 좋은거냐? C++ 에 비해 자바 인터페이스의 간결했던 그 장점을 던저벼리냐? 뭐 이렇게 말 하곤 합니다. 

인터페이스가 어떻게 추가기능이 생기던간에 ..인터페이스는 인터페이스 입니다. 즉 is-a 를 위한 놈이 아니라는거죠. has-a 에 중점이 있습니다. 예전에는 "틀" 만 제공하는 has-a  였지만 이제 "구현" 도 어느정도 추가할 수 있는 "has-a" 가 되었습니다. is-a 는 추상메소드에게 맡겨 둡시다. 

오늘은 자바 월드에 기고된 자바 8 인터페이스 관련 글을 각색해서 소개 해 봅니다.

에피소드 

자바 전문가인 내 친구 제임스는 이렇게 말했어 " 내 생각에 자바 8의 가장 중요한 변경은 람다가 아니야. 인터페이스가 정적,디폴트 메소드를 가질 수 있게 되었다는 거지"  계속해서 말하길 " 인터페이스에 디폴트 메소드를 추가 할 수 있게 되었다는 것은 추상클래스를 어쩔 수 없이 써야했던 속박에서 나를 풀어 주었거든" 

이 말을 듣고 , 나도 굉장히 동감이 가더라구. 원래 자바 세계에서 추상클래스 VS 인터페이스에 대한 케케묵은 주제는 예전 부터 개발자들 사이에서 꽤 큰 논쟁거리였었는데 이 새로운 JDK 8 로 개발하는 새내기 개발자들에겐 이 두 개념을 구분하는 일이 더 어려워 질 거 같아. (역주 : 단순히 다중상속 되냐 안되냐가 아니란거죠) 

JavaWorld's Abstract classes vs. interfaces, StackOverflow's When do I have to use interfaces instead of abstract classes?Difference Between Interface and Abstract Class, and 10 Abstract Class and Interface Interview Questions Answers in Java.

위에 링크한 이런 것 들이 예전에는 꽤나 유용한 정보였지만 이제 유통기한이 지나버렸지. 자바8로 개발하기 시작하는 새내기들이 저런 글을 보면 더더욱 혼란에 빠질 것이 자명해보여.

자바8 에서 인터페이스와 추상클래스 사이에 존재하는 차이점에 관해 살펴보기 위해 먼저 해야할 것은 표준 문서를 살펴보는 것 인데 먼저 내용이 업데이트된 자바 튜토리얼 Abstract Methods and Classes 여기에 가보면 차이점과 유사점에 대해 설명은 해  놓았어.

차이점은 주로 데이타 멤버와 메소드들에 대한 접근성에 관한 것 인데 

추상클래스는 non-static 과 non-final 필드 및 public, private, protected 메소드를 사용 할 수 있다.
인터페이스는 public, static, final 을 상속할 수 있고 모든 상속 가능한 메소드는 public 이다. 

( 하마:  인터페이스를  "구현" 이 가능하게 바꾸었지만 아직 적절한(?) 한계를 두었다. 라는 건데, 일단 문법적 복잡도가 올라가 버렸습니다. 스칼라에는 인터페이스 키워드가 아예 없고, 자바8의 인터페이스와 비슷한 trait 이라는게 있는데  변화된 자바8의 인터페이스가 스칼라 영향을 받은게 아닌가 싶기도 합니다. 옛날부터 개발자들 사이에서 필요성이 언급되었던 부분에 대해 스칼라는 언어가 생겨났을때부터 적용을 한것이고 자바는 이제서야 적용을 한 것 이기도 하겠지요.)

문서에 보면 언제 추상클래스를 사용해야하고, 언제 인터페이스를 고려해야하나 에 대한 말이 있는데. 뭐 놀랍지도 않게도 위에 한 얘기를 뻔하게 되풀이 하고 있지. non-static 과 non-final 필드 및 public, private, protected 메소드가 필요하면 추상 클래스 쓰고 구체적인 구현이 없는 것 에 촛점을 맞추고 다중상속이 필요한 부분은 인터페이스쪽이 낫고... 뭐 이런..-.-;;  

자바는 알다시피 하나의 클래스에서 만 상속받을 수 있고, 여러개의 인터페이스로부터 구현 상속 될 수 있는데 인터페이스는 그래서 여러 타입(기능)들을 연관시켜서 구현 될 필요가 있는 상황에서 특별히 이득을 볼 수 있어. 고맙게도 자바 8 부터는 디폴트 메소드가 그런 일반적인 상황에서의 행동에 대한 지침을 미리 구현하여 제공 해 줄 수 있게 되었지.  ( 하마 : 예를들어 자동자인터페이스는 전진/후진/클락숀에 대한 틀만 제공하고 자동자 인터페이스를 구현상속한 우리는 클래스에 전진/후진/클락숀에 대한 실제 구현을 넣게 됩니다. 근데 세상은 빠르게 변화하고 자동차의 기본적인 틀 자체가 변화 하고 있습니다. 또한 상위에서 틀만 제공하면 하위에서 우리들은 구현을 책임져야 하는데요.  예를들어 네비게이션이 자동차 틀의 기본이 되었다고 합시다. 그럼 하위에서 우리의 자동차에 네비게이션을 구현 상속해야하는데 네비게이션은 우리가 더 잘 알까요? 네비게이션 회사가 더 잘 알까요? 즉 상위에서 여러가지 틀들이 기본적으로 구현이 되어 있으면 가져다 쓰기 편리한거죠. 이런 이유로 자바8에서 인터페이스는 기본 구현을 가지게 되었습니다)  

여기서 본능적인 질문하나 들어가는데....만약 두개의 인터페이스을 가지고 구현을 한 클래스가 동일한 시그니쳐를 가진 디폴트 메소드 두개를 상속 받았을 경우 어떻게 핸들링 할 수 있을까?  

NetBeans 8 에서는 에러를 내보내.

스냅샷을 보면 알 수 있듯이 컴파일러 에러가 나와. ( 클래스/디폴트 메소드/두개의 인터페이스 등 관련 정보와 함께) 이에 대해 내 친구 피터 버하스는 좀 더 상세한 글을 작성 했는데 다음 링크를 참고해봐 ( 자바 8 디폴트 메소드 : 무엇을 할 수 있고 무엇을 못하나?) 동일한 시그니처를 가진 디폴트 메소드로 구현된 여러 개의 인터페이스를 상속받는데 관련된 몇가지 케이스를 살펴 볼 수 있지.

결론 

자바8은 기존 인터페이스에 비해 많은 기능을 제공했던 추상클래스들의 장점을 인터페이스에게도 베풀었다. 
이것이 암시하는 바는 현재 쓰여져 있는 많은 수의 추상 클래스들을 쉽게 대체 할 수 있게 되었다는것과 앞으로 추상클래스를 사용하여 작업 했어야할 많은 곳에서  디폴트 메소드를 가진 인터페이스로 대체 될 것이란 것이다.

부록 : 공식 문서에서 발췌) 

추상클래스와 인터페이스 언제 사용하나?

  • 추상클래스 
    • 여러개의 가까운 클래스들 (is-a 관계가 형성될) 사이에 동일한 코드를 공유해서 사용하고 싶을때.
    • 추상클래스를 상속한 클래스들이 많은 공통 메소드들과 필드와 public 보다 다양한 접근 제어자에 의해 사용하고 싶을때.
    • non-static 과 non-final 필드를 선언하고 싶을때.  결과적으로 객체들의 상태를 메소드에서 접근하고 수정 할 수 있게 되겠지.
  • 인터페이스
    • 크게 상관없는(is-a 정도는 아닌 has-a 정도인) 클래스들이 너의 인터페이스를 구현( java8 부터는 구현된 것을 사용도 포함)해야 할 필요가 있을때. 예를들어 Comparable and Cloneable
    • 특정 데이터타입의 행위를 특별하게 구현하길 원할때 그러나 누가 그것의 행위를 구현 했는지에 대한 관심은 없을때 
    • 다중 구현상속의 이점을 누려야 할때 



http://docs.oracle.com/javase/tutorial/java/IandI/abstract.htmlhttp://www.javaworld.com/article/2139921/learn-java/abstract-class-versus-interface-in-the-jdk-8-era.html


자바 8에서 인터페이스가 대폭 변경 되었기 때문이 아래 내용은 이제 구시대 유물이 되었다.

자바8로 개발을 시작하는 분들이면 읽지 말길 권유함.


질문 :

안녕하세요~

인터페이스와 추상클래스가 도대체 어떻게 다른 건지 궁금한데요.

구글링을 해보면 죄다 인터페이스의 특징, 추상클래스의 특징을 나열하면서

비교하는 글밖에 없는데, 이건 결과론적인 얘기인 것 같고요.

사실 추상클래스로도 인터페이스처럼 사용할 수는 있잖아요? 기술적으로 보면요.

그리고 상속을 사용하면 강결합이 발생해서 좋지 않다는데

구현도 마찬가지로 관계를 심어주긴 하니까 별 차이가 없다고 생각하고요.

그래서 제가 생각해본 결론은,

'만약 인터페이스가 없더라도 추상클래스로 동일하게 구현이 가능하지만(다중상속 허용 가정)

추상클래스는 그 자체로도 완전한 클래스를 만들 수 있는 설계도로서의 역할을 위해,

인터페이스는 단순히 행위만을 나타내는 명세서로서의 역할을 위해 둘을 구분지었다.

즉, 추상클래스를 적절히 사용치 못하였을 경우에 발생하는 위험을 피하고자

필요한 제약을 몇가지 추가하여 인터페이스를 만든 것이다'

이렇습니다.

간략히 말씀드리자면, '개발자의 편의를 위해 선택지를 추가하였다' 정도가 되겠네요.

인터페이스라는 것이 왜 나왔는지, 추상클래스와는 어떻게 다르게 써야하는지 생각해보다가

이 둘이 기술적으로 배타적인 관계가 아닌 듯하여 위와 같은 결론을 내려봤습니다.

제가 맞게 나불대는 건지 의견 부탁드릴게요. ㅋ



하마 :

추상클래스 = (단독객체생성불가, 구현가능) ,  인터페이스 = (단독객체생성불가, 구현불가) 로 언어창시자가 이렇게 룰을 정해놨으면 그거에 맞게 사용하면 됩니다. 굳이 어렵게 생각할 필요가 없다고 보네요.. 1.어떤 고정적인 흐름을 가진 템플릿이 있다고 확신한다.밑에 구현될놈들은 그 흐름을 사용할것이고 변화될 가능성이 희박하다. <-- 이러면 추상클래스를 써도 팀원들로부터 비판당하지 않겠고 2.어떤 고정적인 템플릿이 없다. 연결고리만 제공해서 유연성 강화. <--- 인터페이스 (대부분의 디자인패턴에 적용) 3.어떤 고정적인 템플릿이 있는데 ..클래스 내부에 고정시켜 놓기는 꺼림직하다. has-a 이다. <-- 이러면 인터페이스 + 템플릿인젝션 이렇게 상황에 맞게 변화되겠지요..


A님: 

Java 와 C# 언어는 다중 상속이 불가능한 언어입니다. (오류수정: 기존의 언어 중에서 다중 상속을 구현한 언어로 C++ 이 있는데, Java 와 C# 언어는 C++ 과 동일한 방식의 다중 상속을 제공하는 것은 아니라는 말입니다.) C++ 과 동일한 방식의 다중 상속은 설계하기도 어렵고 복잡한 문제가 많았던 것 같습니다. 그래서 언어를 설계할 때 다중 상속 기능은 아예 빼버렸던 것 같습니다. (오류수정: C++ 과 동일한 방식의 다중 상속을 도입하지 않았다는 뜻입니다. Java 와 C# 언어는 C++ 과는 다른 방식으로 다중 상속을 구현합니다.) 그런데, Java 와 C# 언어도 사용하다 보면, 다중 상속이 꼭 필요한 경우가 자주 있습니다. 그래서, 인터페이스를 이용하여 다중 상속을 흉내 낼 수 있게 만들었습니다. 글쓴이는 질문 글에서 “사실 추상클래스로도 인터페이스처럼 사용할 수는 있잖아요? 기술적으로 보면요.” 라고 말씀하셨는데, 그 말씀은 오직 단일 상속의 경우에만 그렇게 생각할 수 있습니다. 단일 상속만을 사용하는 경우에는, 인터페이스와 추상 클래스의 차이가 별로 느껴지지 않을 것입니다. 하지만, 추상 클래스는 다중 상속이 불가능하지만, 인터페이스는 가능하다는 점에서, 기술적으로 매우 다르게 구별됩니다. 예를 들어, Java 및 C# 의 기본 라이브러리들 중에는 하나의 클래스가 여러 인터페이스를 상속, 구현하는 경우가 자주 있습니다. 사실 인터페이스는 구현 코드를 갖지 않기 때문에 실제로는 다중 상속이 아니라고 생각됩니다. 그럼에도 불구하고 다수의 인터페이스를 상속, 구현함으로써 다중 상속과 비슷한 효과를 내고 있습니다. <추신> 추상클래스가 구현 코드를 가질 수 있고, 인터페이스가 구현 코드를 가질 수 없다는 것은 사실이지만, 추상클래스와 인터페이스를 직접 비교하는 것은 한마디로 엉터리 비교라고 생각합니다. 왜냐하면, 추상클래스와 인터페이스는 근본적으로 다른 목적으로 만들어졌기 때문에, 서로 상관이 없고, 따라서, 비교 자체가 잘못된 비교입니다. 비유를 들자면, 수지의 팔뚝과 나은이의 종아리를 비교하면서, 어디가 더 예쁘냐고 비교하는 것은 한마디로 엉터리 비교입니다. 왜냐하면, 팔뚝과 종아리는 완전히 다른 부위라서, 비교 자체가 논리가 성립하지 않습니다. 클래스의 상속-구현 개념은, 다수의 클래스가 공통된 속성이나 중복된 코드를 가질 때, 공통된 부분을 모아 하나의 상위 클래스로 통합하여 관리하는 것을 목적으로 설계된 개념입니다. 추상클래스에 '추상'이라는 부가적인 의미가 붙은 이유는, 일부 코드를 상위 클래스에서 구현하지 않고, 하위 클래스가 직접 구현하도록 강제하는 방법을 쓰기 때문에 붙여진 이름일 뿐, 추상클래스는 '클래스의 상속-구현'의 범주에 포함되는 개념입니다. 추상클래스도 상위클래스이기 때문에 당연히 구현 코드를 가질 수 있습니다. 반면에, 인터페이스는 다중 상속을 흉내내기 위한 도구로서 개발된 개념입니다. (오류수정: 다중 상속을 흉내낼 때 기존의 interface 라는 용어를 가져다 썼습니다. 그럼으로써, interface 라는 용어는 새로운 의미로 확장되었습니다.) Java 및 C# 언어는 C++ 방식의 다중 상속이 불가능한 언어이기 때문에 인터페이스는 구현 코드를 가질 수 없습니다. 추상클래스와 인터페이스는 비슷해 보이기는 해도, 근본적으로 서로 다른 개념이기 때문에, 이 둘을 직접적으로 비교하는 것은 엉터리 비교라고 생각합니다.


하마:

A님 // "인터페이스는 다중 상속을 흉내내기 위한 도구로서 개발된 개념입니다."<-- 저라면 면접에서 이렇게 말하면 0점. 다투고 싶진 않지만... 너무나 큰 오류라 ..(현재 다중상속에 너무 꽂히셔서 그런듯 싶네요..) 인터페이스 개념은 자바언어가 생겨나기 훨씬 전부터 있어왔고 그 용도는 다중상속과는 상관이 없습니다. 디자인패턴에서 왜 대부분의 패턴이 인터페이스를 가지고 있는가를 생각해보시기 바랍니다..


B님:그럼 한두마디로 표현한다면 어떤 표현이 좋을까요? 저도 개발은단지님 처럼 다중상속 가능 유무 차이 밖에 없다고 생각했었는데..


하마 :  한두마디로 표현한다라... 저는 "행위 템플릿" " 변경가능속성" 의 존재유무라고 보네요. 저 위에 제 글은 "행위" 측면에서 쓰여졌는데 읽어보시고요.. 변경가능한 속성들의 유무가 또 하나의 축이지요..( immutable 하냐 안하냐) 다중상속은 곁가지라고 봅니다..


질문자:

'처음부터 그렇게 만들어진 것이다'라고들 말씀하시는데, 전 그렇게 생각하지 않습니다. 일단, 다중 상속에 문제가 있어 이를 금지하고 대신 인터페이스로 해결하였다는 것은 제가 봐도 오류인 듯 싶습니다. 정말 다중 상속이 큰 문제였다면, 문법적인 제약을 두어 두 개의 구현된 메서드를 동시에 상속 받지 못하도록 할 수도 있을 겁니다. 굳이 인터페이스를 만들 필요도 없었겠죠. Holub은 "실용주의 디자인 패턴"이란 책에서 '디자인 패턴을 매우 일상적으로 사용하게 되면 이는 패턴이 아니라 이디엄이다' 라며, 일례로 C 언어가 주류일 당시 상속은 하나의 디자인 패턴이었으나 C++에 상속 기능이 언어에 내장된 이후로 이는 패턴이 아닌 이디엄이 되었다고 말을 합니다. Holub의 표현을 빌리자면, 현재 우리가 쓰는 인터페이스의 기능 역시 추상클래스를 이용한 하나의 패턴이었으며, 이것이 언어의 기능으로 들어오면서 상속처럼 특별한 패턴이 아닌 이디엄이 되었다고 볼 수 있습니다. (물론 이는 인터페이스가 추상클래스 보다 나중에 나왔다는 추측에 기반한 것입니다. 두 개념이 처음 고안된 시기를 찾아보려 했으나 실패를.. 혹 아시는 분은 알려주세요) 그 까닭은, 언어마다 추상클래스의 용법은 조금씩 다르겠지만 개념상 이를 인터페이스처럼 사용하는 것이 불가능하지는 않기 때문입니다. 하지만 인터페이스가 추구하는 바를 추상클래스만으로 이루려면 지켜야 할 제약이 생기고 위험 또한 생기기 마련입니다. 상속 기능 없이 상속을 구현한다는 것은 - 물론 저로선 상상조차 안되지만 - 정말 복잡하고 번거로운 일이겠지요. 마지막으로 C++의 '__interface'에 대한 msdn 문서에 다음과 같은 말이 있습니다. 'A C++ class or struct could be implemented with these rules, but __interface enforces them.' (http://msdn.microsoft.com/en-us/library/50h7kwtb)


하마 :

질문자 // " 인터페이스가 추구하는 바를 추상클래스만으로 이루려면 지켜야 할 제약이 생기고 위험 또한 생기기 마련입니다." <-- 맞습니다. 그래서 행위의 틀 과 immutable 한 속성만으로 제한해서 유연성을 극대화한것이 아시다시피 자바의 "interface" 입니다. 고슬링이 저렇게 만들어놨으면 저 의도에 충실하게 사용하면 된다는 거지요. "의도" 가 중요한데 그 의도 즉 나뉘어 지는 키 포인트가 "행위 템플릿" " 변경가능속성" 라는 얘기입니다. p.s 왜 저걸 사용하냐? 에 포커싱을 맞추는게 생산적일거 같네요.. 스마트포인터은 패턴이 아니라 이디엄이다. 이런거보다는 리소스 해제의 타이밍을 사용자가 직접 개입하는것은 위험성이 높아지기때문에 자동화 이런 "의도" 가 중요하겠지요. 홀럽이 좋아하죠? "의도"


질문자:

하마// 좋은 말씀 감사합니다. ㅎ 사실 님께서 '룰'에 대해 말씀하신 것도 '왜' 그 룰이 정해졌는지가 궁금한 거였거든요. 추상클래스로도 할 수 있는 것을 말이죠. 그러다 문득 책에서 읽었던 내용이 떠올라 접목해서 끄적여봤습니다.


A님:

하마// 귀하께서 말씀하시는 "자바언어가 생겨나기 훨씬 전부터 있어왔"던 인터페이스와 제가 말하는 인터페이스는 의미가 다릅니다. 같은 용어라도, 다른 분야에서는 다른 의미로 쓰이거나, 같은 분야에서도 다중적인 의미로 해석되는 경우가 많습니다. 예를 들자면, 소프트웨어 개발자가 말하는 "플랫폼"은 철도공사 업체가 쓰는 "플랫폼"과 같은 단어이지만, 전혀 다른 의미로 사용되고 있습니다. "인터페이스"라는 말도 소프트웨어 역사 속에서 그 의미가 계속 확장,변화되어 왔습니다. 다중 상속 기능을 언어에 구현하면서, 다중 상속 전용의 새로운 용어를 만들기보다는, 기존의 interface 라는 용어를 그대로 가져다 썼습니다. 왜냐하면, 다중 상속과 기존의 interface 는 그 핵심적 의미가 같으니까요. 가져다 쓰는 순간, interface 라는 용어는 그 의미가 확장,변화됩니다. 기존의 의미와는 다른(새로운) 의미를 갖게 됩니다. 제가 말하는 인터페이스는 디자인 패턴의 인터페이스도 아니고, COM 인터페이스도 아닙니다. 제가 말하는 인터페이스는 Java 및 C# 에서 다중 상속에 사용되는 인터페이스에 한정해서 말씀드렸습니다. 저는 "다른 것"을 말하고 있는데, 귀하께서는 "다른 것은 틀린 것"이라고 말씀하시는군요? 인터페이스가 "유연성"을 극대화한다고 볼 수 있는 이유는 당연하게도 구현 코드를 갖지 않기 때문입니다. 그러나, 추상클래스가 구현 코드를 갖는다는 이유로, 하위 클래스에게 "고정적인 흐름"을 강제하거나 "변화될 가능성이 희박"하다고 생각하지 않습니다. 추상클래스를 상속 받는 클래스는 기존의 구현된 코드를 override 해서 새로운 코드로 대체할 수도 있고, 아예 새로운 코드를 작성해서 다른 기능을 사용할 수도 있습니다. 추상클래스는 이미 추상멤버변수나 추상멤버함수를 통해 인터페이스와 동등한 수준의 유연성을 제공하고 있습니다. 추상클래스 또한 인터페이스 못지 않게 변화의 가능성이 충분하다고 생각합니다. 게다가, 인터페이스가 선언하는 멤버변수들(또는 멤버함수들) 또한 하위 클래스에게 "이런 규칙을 따르라"고 제약하기는 마찬가지입니다. 이런 관점에서는 추상클래스와 인터페이스의 차이점이 보이지 않습니다. 추상클래스를 비롯해서, 단일상속에 사용되는 클래스들은 "기존의 코드를 재사용할 때 얻는 효율성" 관점에서 보는 것이 타당합니다. 추상클래스를 "고정적인 흐름"이나 "변화 가능성의 제약"을 위해 사용한다고 생각하지 않습니다. 샤프란// "다중 상속에 문제가 있어 이를 금지하고 대신 인터페이스로 해결하였다는 것은 제가 봐도 오류인 듯 싶습니다" ==> 저는 다중 상속이 문제가 있어 "금지한다"고 생각한 적 없습니다. 현대적인 객체지향 언어가 다중 상속을 사용하지 않는다는 것이 가능하겠습니까? C++ 의 다중 상속은, 상위 클래스들이 모두 독자적인 구현 코드를 가질 수 있고, 하위 클래스가 그 모든 코드를 재사용할 수 있다는 점에서 매우 강력합니다. 그러나, Java 및 C# 언어는 C++ 언어와는 컨셉이 다른 언어이기 때문에, C++ 방식의 다중 상속을 그대로 도입하지 않았고, 그 대신 다른 방법을 이용하여 다중 상속을 구현하고 있습니다. 그 대신, Java 및 C# 에서는 "구현 코드가 없는" 클래스(선언만 갖는,인터페이스)들로부터만 다중 상속이 가능하도록 제한되었습니다. 강력함을 일부 포기하는 대신, 보다 쉽게 사용할 수 있게 한 것입니다. C++/Java/C# 등의 언어는 모두 다중 상속을 사용합니다. 제 말은 각각의 언어가 다중 상속을 구현하는 방법에 차이가 있다는 얘기입니다. "정말 다중 상속이 큰 문제였다면, 문법적인 제약을 두어 두 개의 구현된 메서드를 동시에 상속 받지 못하도록 할 수도 있을 겁니다. 굳이 인터페이스를 만들 필요도 없었겠죠" ==> 할 수도 있을 게 아니라 그렇게 했습니다. 즉, Java 및 C# 에서는 (단일상속을 제외한 추가적인)다중 상속 시 반드시 "구현 코드가 없는" 클래스로부터 상속받도록 제약을 두었습니다. "추상클래스로도 할 수 있는 것을 말이죠" ==> 인터페이스의 기능 중 하나인 다중 상속 기능은, 추상클래스로 할 수 있는 것이 아닙니다. 애초 원문글쓴이께서 추상클래스와 인터페이스가 별로 차이가 없다는 뉘앙스로 말씀하신 부분이 있기 때문에, 본인은 그 둘은 분명한 차이가 있다는 취지로, 추상클래스의 특징 중 하나인 단일상속과, 인터페이스의 특징 중 하나인 다중상속을 대비시켜서 말씀드린 게 제 이야기의 본질입니다.


질문자:

A답변자// 글 잘 보았습니다. 그런데 너무 다중 상속에만 치중하시는 건 아니신지요? 물론 저도 어떠한 추측에 기반하여 글을 썼지만, 님 또한 정확한 근거는 없으신 듯 합니다. 하마님께서도 말씀하셨지만 대부분의 디자인 패턴이 인터페이스를 활용한다는 것은 우연이 아닐 겁니다. 디자인 패턴에서 다중 상속 개념을 이용하는 것이 그렇게 많은가요? 다중 상속을 해결하고자 인터페이스를 만들었다면, 그 부분에만 쓰면 될텐데 왜 추상클래스 보다는 인터페이스를 지향할까요? 청출어람인가요? 추상클래스로는 얻기 힘든 이점들을 인터페이스를 이용하면 쉽게 얻을 수 있습니다. 추상클래스는 단일 상속만을, 인터페이스는 다중 상속을 지원한다는 것이 이 둘의 결정적인 목표는 아니라 생각합니다. p.s 주제넘지만, 하마님께서 말씀하신 '인터페이스'가 다른 의미인가요? 전 같아 보여서..


하마:

A답변자// "추상클래스의 특징 중 하나인 단일상속과, 인터페이스의 특징 중 하나인 다중상속을 대비시켜서 말씀드린 게 제 이야기의 본질입니다." <-- 문법적으로는 맞습니다. 틀리지 않습니다. 그 "차이" 는 분명합니다. 하지만 인터페이스(개념)가 추구하는 바를 추상클래스만으로 이루려면 지켜야 할 제약이 생기고 위험 또한 생기기 마련이라서 규제수준을 높혀서 유연성을 극대화하기 위해서 만들어진게 interface (자바문법) 입니다. 의도가 이거란 말입니다. "의도"를 봐야지 "차이" 중 하나에 포커싱을 맞추는게 틀린건 아니지만 아쉽다는 얘기였습니다.. 

p.s 저런식의 규제 혹은 특정모형을 통해서 소프트웨어를 더 좋게 만드는경우가 매우 많지요. 일례하나만 들면 객체지향개발에서 쓰레드를 직접사용하면 다양하게 요리될수있으나 그 위험성이 크기에 규제를 주고 특정 모델을 만든게 Actor 모형이라고 합니다. akka 라는 라이브러리가 그 구현체이고.. 큰 틀에서보면 이런거지요..

이 글은 자바 8 Stream API 를 아는 사람이 주의해야 할 것에 대해 쓰여진   글이지만 , 몰라도 상관없습니다.

이 글 읽어보면 대충 이런거구나 알 수 있으니깐요. 

Java 8 Stream API 을 배워야하는 이유로  "가독성/간편성" 과 "성능/공짜점심" 으로 보통 꼽습니다.


가독성


코어 자바

1
2
3
4
5
6
7
8
9
10
11
private static int sumIterator(List<Integer> list) {
    Iterator<Integer> it = list.iterator();
    int sum = 0;
    while (it.hasNext()) {
        int num = it.next();
        if (num > 10) {
            sum += num;
        }
    }
    return sum;
}


Stream API

1
2
3
private static int sumStream(List<Integer> list) {
    return list.stream().filter(i -> i > 10).mapToInt(i -> i).sum();
}


여러줄의 코드가 한줄로 되어버렸습니다. 가독성이 좋아졌고, 실수할 여지를 줄여 놓았습니다. 

라고 광고합니다. 만은  저렇게 되기 위해서는 팀원들이  Stream API 에 익숙하다라는 조건이 충족되야겠지요. 또한 어떻게 보면 순수한 FOR 문을 돌리는게 더  실수할 여지를 없애는것이기도 합니다.  단순할 수록 실수도 단순하니깐요. 저렇게 추상층이 높은 코드는 내부적으로 어떻게 실수가 유발될지 상상하기 어렵습니다. 



* 성능

간단한 업무를 (하지만 양은 많은)  손쉽게 병렬로 돌려주는 고마운 존재입니다. 
개인적으로    기능 > UI/UX >> 보안 > 성능  으로 우선순위를 생각합니다. 성능이란 항상 최후에 적절한 핫 스팟을 찾아서 처리해주는것이죠. 그 때 고려하면 되는것이지, 애초에 성능때문에 이걸 배워서 써먹어야겠다라고는 말해드리지 못할거 같네요. 


이제부터 번역본입니다.



Streams API 를 사용할때 발생할수있는 미묘한 실수 10가지


1. 재사용 스트림 문제 

모든 사람들이 적어도 한번은 겪어 봤을 법 한 문제로 , 스트림은 오직 한번만 소비 할 수 있어서 다음 코드는 작동하지 않아요. 

1
2
3
4
5
IntStream stream = IntStream.of(1, 2);
stream.forEach(x -> System.out::println(x));
 
// 다시 사용~!!
stream.forEach(x -> System.out::println(x));

이런 에러를 만날것이고

java.lang.IllegalStateException: 
  stream has already been operated upon or closed

스트림을 소비할때는 조심해야합니다. 오직 한번만 완료될 수 있어요.

2.  "무한" 스트림 생성 문제 

무한 스트림을 특별한 주의를 기울이지 않아도 쉽게 생성 할 수 있습니다.  다음 예를 보시죠.

1
2
3
// 무한이 돌아감
IntStream.iterate(0, i -> i + 1)
         .forEach(System.out::println);

원래 그 역할을 하게 짠 거라면 어쩔수 없지만,  의도되지 않은것이라면 적절한 한계를 두는게 좋겠지요.

1
2
3
4
// 이게 더 좋을거 같습니다. (0~9)
IntStream.iterate(0, i -> i + 1)
         .limit(10)
         .forEach(System.out::println);


3. 의도치 않게  생성된 무한 스트림 

이것도 무한 스트림이 될 수 있습니다. limit(10) 을 사용했는데도 불구하고 말이지요. 

1
2
3
4
IntStream.iterate(0, i -> ( i + 1 ) % 2)
         .distinct()
         .limit(10)
         .forEach(System.out::println);

System.out.println("complete");

각 라인을  설명해보면 

  • 0 과 1 을 반복적으로 생성 할 것 입니다. 
  • 그리고 개별 값 하나를  유지할 것 입니다. 즉 단일 0 과 1 이겠지요.
  • 그리고 10개의 크기로 스트림에 제한을 걸 것입니다.
  • 그리고 그것을 (10개까지)  출력합니다.

여기서 문제는 distinct() 연산자는 iterate() 함수가 오직 0과 1이라는 값만 생성 할 것을 알지 못해요. 결국 스트림으로부터 무한히 값을 받아드려서 사용 할 것입니다.  limit(10)  에는 결코 다다르지 못하죠. 즉 "complete" 가 절대로 찍히지 않습니다.

4. 의도치 않게 생성된 병렬 무한 스트림 

만약 distinct() 연산자가 병렬로 수행될 수 있다고 믿는다면 다음과 같이 작성할 수 있을 것 입니다. 

1
2
3
4
5
IntStream.iterate(0, i -> ( i + 1 ) % 2)
         .parallel()
         .distinct()
         .limit(10)
         .forEach(System.out::println);

이게 영원히 돌것이라는것은 이미 위에서 확인했는데요. 그래도 이전에는 cpu 를 하나만 혹사시킨 반면 , 여기서는 더 많이  사용할 것입니다. 잠재적으로 당신 시스템의 모든 리소스를 점거 할 것이란 얘기죠. 

If I were a laptop, this is how I'd like to go.

If I were a laptop, this is how I’d like to go.

5. 연산자 순서 섞기  

limit() 와  distinct() 의 순서를 바꾸면 잘 작동 할 것입니다. 

1
2
3
4
IntStream.iterate(0, i -> ( i + 1 ) % 2)
         .limit(10)
         .distinct()
         .forEach(System.out::println); System.out.println("complete");

이렇게 출력 될 것입니다.

0 1 "complete"

먼저 10개의 값을 먼저 받은후에 (0 1 0 1 0 1 0 1 0 1),  distinct 를 통해 줄여서  (0 1) 만 남게 될 것입니다. 재밌는건 , 당신이 만약 SQL 개발자 출신이라면, 저런 차이를 기대하기 어려웠을겁니다.  SQL Server 2012에서는  다음 2개의 SQL문은 같기 때문입니다. ( 역주 : SQL Server 2012 가 없어서 테스트는 못해봄) 

1
2
3
4
5
6
7
8
9
10
11
-- Using TOP
SELECT DISTINCT TOP 10 *
FROM i
ORDER BY ..
 
-- Using FETCH (
SELECT *
FROM i
ORDER BY ..
OFFSET 0 ROWS
FETCH NEXT 10 ROWS ONLY


6. 연산자 순서 섞기 2

SQL 이야기가 나와서 하는 말인데, 만약 당신이  MySQL 나  PostgreSQL 개발자라면 , LIMIT .. OFFSET 절을 사용했을겁니다 . 

만약 MySQL / PostgreSQL’s 방언을  streams 으로 바꾼다면 이렇게 했을텐데요. 

1
2
3
4
IntStream.iterate(0, i -> i + 1)
         .limit(10) // LIMIT
         .skip(5)   // OFFSET
         .forEach(System.out::println);

아래와 같이 나옵니다.

5
6
7
8
9

네. 9 이후로 계속되지 않습니다. 먼저 10개로 제한한후에 5개를 건너뛰고 출력한 모습으로 당신이 의도하는대로 안됬을 겁니다.

LIMIT .. OFFSET vs. "OFFSET .. LIMIT" 의 차이를 살펴 보세요! 

(역주: postgresql 에서  select * from table order by id asc  (offset 5 limit 5  와   limit 5 offset 5)  순서를 바꾸어도 차이없음)

7. 필터와 함께 파일시스템  walking  사용하기


1
2
3
Files.walk(Paths.get("."))
     .filter(p -> !p.toFile().getName().startsWith("."))
     .forEach(System.out::println);

위의 스트림은 오직 숨겨지지 않은 디렉토리들을 순회하는것으로 보여집니다. 즉  닷(.) 으로 시작하지 않는 디렉토리 말이죠. 운이 없게도,  #5 와  #6 의 실수를 다시 복할 겁니다. walk() 는 이미 현재 디렉토리의 전체 서브디렉토리의 스트림이 생산되어졌습니다. 게으른방식으로 말이죠.  논리적으로 모든 서브패스들을 포함합니다. 지금 필터는 정확히 닷(.) 으로 시작하는 이름을 필터링할것이구요. 예를들어 .git or .idea 같은것은 결과 스트림에서 제외될 것 입니다. .\.git\refs or .\.idea\libraryies 이런 패스들 까지  당신이 의도하는건 아니죠. 


8. 스트림의 Backing 콜렉션을 수정하기 

List 를 순회하는 동안, 이터레이션 바디안의 동일한 리스트를 수정하지 말아야합니다. Java8 이전까지는 사실이였지만 Java 8 스트림에서는 좀 더 영리해졌습니다. 

1
2
3
4
5
// list 를 streams 을 이용해서 ArrayList 로 (0..9) :
List<Integer> list =
IntStream.range(0, 10)
         .boxed()
         .collect(toCollection(ArrayList::new));

각각의 요소들을 그것을 사용하고나서 제거한다고 가정해봅시다.

1
2
3
4
list.stream()
    // remove(Object), not remove(int)!
    .peek(list::remove)
    .forEach(System.out::println);

재밌게도, 이것은 요소들의 부분으로 작동을 합니다. 아웃풋은 다음과 같을수 있습니다.

0
2
4
6
8
null
null
null
null
null
java.util.ConcurrentModificationException

예외를 잡고나서 리스트를 확인해보면, 재밌게도 다음과 같습니다. 

[1, 3, 5, 7, 9]

전부 홀수네요. 버그 일까요??  아닙니다. 만약 jdk 코드안으로 파헤처들어가면 당신은 이런것을 발견할수가 있어요.  ArrayList.ArraListSpliterator:    (역주: http://okky.kr/article/279692 참고) 

/*
 * If ArrayLists were immutable, or structurally immutable (no
 * adds, removes, etc), we could implement their spliterators
 * with Arrays.spliterator. Instead we detect as much
 * interference during traversal as practical without
 * sacrificing much performance. We rely primarily on
 * modCounts. These are not guaranteed to detect concurrency
 * violations, and are sometimes overly conservative about
 * within-thread interference, but detect enough problems to
 * be worthwhile in practice. To carry this out, we (1) lazily
 * initialize fence and expectedModCount until the latest
 * point that we need to commit to the state we are checking
 * against; thus improving precision.  (This doesn't apply to
 * SubLists, that create spliterators with current non-lazy
 * values).  (2) We perform only a single
 * ConcurrentModificationException check at the end of forEach
 * (the most performance-sensitive method). When using forEach
 * (as opposed to iterators), we can normally only detect
 * interference after actions, not before. Further
 * CME-triggering checks apply to all other possible
 * violations of assumptions for example null or too-small
 * elementData array given its size(), that could only have
 * occurred due to interference.  This allows the inner loop
 * of forEach to run without any further checks, and
 * simplifies lambda-resolution. While this does entail a
 * number of checks, note that in the common case of
 * list.stream().forEach(a), no checks or other computation
 * occur anywhere other than inside forEach itself.  The other
 * less-often-used methods cannot take advantage of most of
 * these streamlinings.
 */

sorted() 를 추가했을때 어떻게 변화되는지 보겠습니다:

1
2
3
4
list.stream()
    .sorted()
    .peek(list::remove)
    .forEach(System.out::println);

기대대로 아웃풋이 생성됩니다.

0
1
2
3
4
5
6
7
8
9

스트림을 소비한후에 리스트는? 네 비어있습니다.

[]

모든 요소들이 소모되었습니다. 그리고 정확히 제거되었지요. sorted() 연산자는“stateful intermediate operation” 입니다.  이 의미는  다음차례들의  연산들이 더 이상  backing 콜렉션으로 작동하지 않고 내부 상태하에 있는것이죠. 이것이 리스트의 요소를 제거할때 안전하게 만들어주게 됩니다. 

정말 그렇다면   parallel()sorted() 를 사용하고 제거해볼까요:

1
2
3
4
5
list.stream()
    .sorted()
    .parallel()
    .peek(list::remove)
    .forEach(System.out::println);

다음과 같이 나오고 

7
6
2
5
8
4
1
0
9
3

리스트에는 하나 남아있습니다.

[8]

헐... 모두 지워지지 않았네요. !? 

이런것들 모두 꽤 미묘하고 랜덤하게 나타납니다.  결국 우리는 스트림을 소비하는동안 backing collection을 수정하는 짓을 웬만하면 삼가하거나 조심히(?) 해야할 거 같습니다. 


9. 스트림을 소비하는것을 까먹음 

다음 예를 보면 어떤 생각이 드나요?

1
2
3
4
5
6
IntStream.range(1, 5)
         .peek(System.out::println)
         .peek(i -> {
              if (i == 5)
                  throw new RuntimeException("bang");
          });

(1 2 3 4 5) 를 프린트하고나서 예외를 던질것으로 예상할수 있는데요. 그러나 틀렸습니다. 이것은 아무것도 하질 않아요. 스트림은 결코 소비되지 않습니다.

이것은  jOOQ  를 실행할때도 동일한데요. execute() or fetch() 를 까먹었다면 말이죠:

1
2
3
4
5
DSL.using(configuration)
   .update(TABLE)
   .set(TABLE.COL1, 1)
   .set(TABLE.COL2, "abc")
   .where(TABLE.ID.eq(3));

헐!!!! 아무것도 실행되지 않습니다.

10. 병렬 스트림의 데드락 

마지막을 장식하기에 딱 좋은 아이템입니다.
만약 당신이 적절하게 동기화 방식을 사용하지 않는다면 모든 병렬 시스템들은 데드락에 걸릴수 있는데요. 실세계에서 사용되는 프로그램에선 명백하지 않을때가 많습니다.  데드락에 걸리는 전형적인 예를 보시죠.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Object[] locks = { new Object(), new Object() };
 
IntStream
    .range(1, 5)
    .parallel()
    .peek(Unchecked.intConsumer(i -> {
        synchronized (locks[i % locks.length]) {
            Thread.sleep(100);
 
            synchronized (locks[(i + 1) % locks.length]) {
                Thread.sleep(50);
            }
        }
    }))
    .forEach(System.out::println);

위 쓰레드가 각각 첫번째 synchronized 에 진입한후에 두번째 synchronized 에 진입하기위해 무한정 기다립니다.  



MDB(엑세스) 나 엑셀(XLS) 을 자바로 읽기위해서 , 먼저 JDBC-ODBC 를 알아보았는데

제어판에 32비트 , 64비트 ODBC 설정하는 부분에서  엑셀 , 엑세스에 대한 드라이버가 32비트만 설정가능한듯..

이것저것 찾아봤는데,  결국 나의 64비트 윈도우즈 8 에서는 답이 없는거 같아서 또 찾아보다가 발견한것.

생각해보니 한 7년전쯤에 , MFC 로 MDB 읽어서 사용하는 어플을 만들었다가, 32비트 OS 에서만 되서 망했던 

기억이 떠오르기도 하고... 


Apache POI (http://poi.apache.org/)


이런게 있더군요. 역시나 MS 세상에서  없을리가...한글 블로그도 엄청 많고..ㅎㅎ 

추천 블로그 : http://yanggoony.tistory.com/3


하지만 먼가 쿼리처럼 검색해서 데이터를 가져오고싶은데 그건 안되는것같네요. 그래서 일단 메모리로 올려서

검색기능을 직접만들어서 사용함.


P.S  

파이썬으로 MDB 읽어왔더니 (따로 ODBC 제어판에서 설정같은거 안하고) 바로됨 +.+  왜 자바로는 안됬을까? 

아마 자바가 64비트 버전이고 파이썬은 32비트 버전으로 윈도우에 깔아놔서 그런게 아닌가 싶습니다.

자바 32비트 버전깔아서 확인해보면 되는데.. 귀찮아서 ;;

import pypyodbc 
             
conn = pypyodbc.win_connect_mdb('D:\\Share\\EC_content\\ec_qst_20151104.mdb')

#connection_string = 'Driver={Microsoft Access Driver (*.mdb)};DBQ=D:\\database.mdb'

#connection = pypyodbc.connect(connection_string)

cur = conn.cursor()

SQL = 'select Question from EC_QST where seq > 3000 and seq < 3010'


cur.execute(SQL)

print "column name"


for d in cur.description :
    print d[0]



print "data---"

for row in cur.fetchall() :
    for field in row :
            print (field, ' ')
    
    print (' ')

cur.close()
conn.close()


https://code.google.com/p/pypyodbc/

+ Recent posts