Java Version Change on Mac

그동안 Java 8 을 사용하다가, 2018년 3월에 Java 10 이 릴리즈되고 팀에서 새로운 프로젝트를 시작하게 되어서 Java 10 을 제 Mac 에 설치하였습니다.

Mac 에는 Java 8, Java 10 이 모두 설치되어 있습니다. ‘/usr/libexec/java_home -V’ 명령을 터미널에서 실행하면


$ /usr/libexec/java_home -V
10.0.1, x86_64: "Java SE 10.0.1" /Library/Java/JavaVirtualMachines/jdk-10.0.1.jdk/Contents/Home
1.8.0_162, x86_64: "Java SE 8" /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home

2개의 Java 버전이 표시됩니다. 그런데 Java 10 으로 업그레드를 하여 코드를 실행하니 몇 개의 라이브러리에 버전 이슈가 발생했습니다. 그래서 다시 Java 8 으로 버전을 낮추려고 했습니다. 이럴 때 사용할 수 있는 방법이 아래와 같습니다.

export JAVA_HOME=`/usr/libexec/java_home -v 1.8`

-v 뒤에 버전에는 ‘jdk1.8.0_162’ 와 같이 정확한 버전을 넣어도 되고 간략하게 ‘1.8’ 과 같이 넣어도 됩니다.

[OAuth2] 자사 서비스에 적합한 Grant Type

OAuth2 에서 정의하는 Grant Type 에는 아래와 같이 5 가지가 있다.

  1. Authorization Code
  2. Implicit
  3. Resource Owner Password Credentials
  4. Client Credentials
  5. Refresh Token

각각은 용도에 맞게 적절히 선택해야 한다.

  1. Authorization Code: 서버에서 인증을 대신해주는 flow 에 적합
  2. Implicit: Authorization Code 를 좀더 단순화해서 모바일과 같이 클라이언트에서 사용하는 flow 에 적합
  3. Resource Owner Password Credentials: 서비스의 가입자가 자신의 아이디/비밀번호 를 가지고 접근하는 flow 에 적합
  4. Client Credentials: 클라이언트(앱) 인증만으로 서비스에 접근하는 flow 에 적합
  5. Refresh Token: 이전 로그인에서 발급받은 refresh token 을 사용해서 새로운 access token 을 발급

그렇다면 OAuth2 인증 서버를 구현하고 자사의 앱(모바일, 웹) 만 이 인증 서버를 사용하는 경우에는 어떤 grant type 을 써야할까? oauth.com 사이트에서 공식적으로 설명하는 Password Grant 에는 아래와 같이 쓰여있다.

The Password grant is used when the application exchanges the user’s username and password for an access token. This is exactly the thing OAuth was created to prevent in the first place, so you should never allow third-party apps to use this grant.

A common use for this grant type is to enable password logins for your service’s own apps. Users won’t be surprised to log in to the service’s website or native application using their username and password, but third-party apps should never be allowed to ask the user for their password.

“Password grant type 주 용도는 서비스가 소유하고 있는 앱을 로그인하는 것이다”

 

부수 효과 (Side Effect), 참조 투명성 (Referential Transparency)

함수형 프로그래밍 (Functional Programming) 의 정의를 설명할 때, 중요하게 언급되는 2가지 개념을 설명하려고 한다. 바로 “부수 효과” 와 “참조 투명성” 이다.

먼저 “부수 효과” 는 함수 내의 실행으로 인해 함수 외부가 영향을 받는 것을 의미한다. 함수의 매개 변수의 값이 변경되어, 이로 인해 함수를 사용하는 코드에 영향을 주거나, 함수의 외부 세계인 데이터베이스, 파일 시스템, 네트워크로 데이터 이동이 함수 실행 중에는 발생하지 않아야 함수형 코드가 된다.

public static int add(int a, int b) {
  while (b > 0) {
    a++;
    b--;
  }
  return a;
}

위의 메쏘드는 함수적이다. 정수형 a, b 를 매개 변수로 받은 후, 그것들의 값을 변경하지 않고 결과를 반환한다. a, b 값을 변경한다고 해도 함수적이다. 왜냐하면 Java 에서는 원시 타입의 매개 변수는 pass by value 형태로 전달되기 때문에, 함수 내에서 값을 변경해도 함수를 벗어난 후에는 영향을 주지 않는다.

public static int div(int a, int b) {
  return a / b;
}

위의 함수는 함수적이지 않다. b의 값이 0인 경우 divide by zero 예외가 발생하는데, 예외를 발생시키는 것은 함수적이지 않다. 위의 코드는 아래와 같이 변경할 수 있다.

public static int div(int a, int b) {
  return (int) (a / (float) b);
}

b의 값이 0이어도 예외가 발생하지 않으며, 특별한 값이(2147483647) 반환된다. 이 값을 처리하는 것은 함수 사용자의 몫이다.

함수형 프로그래밍 (Functional Programming) 의 중요한 특징 중에서 2번째로 언급되는 것은 바로 “참조 투명성” 이다. “참조 투명성”은 함수 (또는 메쏘드) 가 함수 외부의 영향을 받지 않는 것을 의미한다. 다른 말로 하면, 함수의 결과는 입력 파라미터에만 의존하고, 함수의 외부 세계인 입력 콘솔, 파일, 원격 URL, 데이터베이스, 파일 시스템 등에서 데이터를 읽지 않는다. 함수 외부의 값을 변경하거나, 외부 세계의 의존적이지 않은 코드를 가리켜 “참조 투명성 있다” 라고 말한다.

참조 투명성을 가진 코드는 아래와 같은 특징들을 지닌다.

  • 자기 충족적이다 (self-contained). 함수 외부에 의존하는 코드가 없고, 함수 사용자 입장에서는 유효한 매개변수만 전달하면 된다.
  • 결정론적이다 (deterministic). 동일한 매개변수에 대해서는 항상 동일한 결과가 나온다.
  • 예외 (Exception) 를 던지지 않는다. out of memory error 혹은 stack overflow error 는 발생할 수 있지만, 이러한 에러들은 버그로 취급되며, 함수의 사용자가 다룰 수 있는 것은 아니다.
  • 다른 코드가 예기치 않게 실패하는 조건을 만들지 않는다. 예를 들어, 참조 투명성을 가진 함수는 매개 변수의 값을 변경하거나 함수 외부의 데이터를 변경하지 않는다.
  • 데이터베이스, 파일 시스템, 네트워크 등의 외부 기기로 인해 동작이 멈추지 (hang) 않는다.

함수형 프로그래밍을 통해서 얻을 수 있는 이득은 아래와 같다.

  • 함수형 프로그램은 결정론적이기 때문에, 원인을 찾기가 더 쉽다. 함수형 코드는 특정한 입력에 대해서 항상 동일한 결과를 반환한다. 많은 경우 프로그램을 광범위하게 테스트하는 대신 올바른 프로그램임을 증명할 수 있으며, 예기치 않은 상황에서 프로그램이 중단될지 여부를 여전히 확신하지 못할 수 있다.
  • 함수형 프로그램은 테스트 하기가 쉽다. 부수 효과가 없기 때문에, mock 을 만들지 않아도 된다.
  • 함수형 프로그램은 조립하기가 더 쉽다. 함수형 프로그램은 함수들의 조합으로 구성된다. 다루어야할 부수 효과가 없고, 예외가 없으며, 값의 변경이 일어나지 않는다. 동시성 문제도 발생하지 않는다.
  • 함수형 프로그램은 구성, 재구성이 쉽다. 함수형 프로그램을 작성할 때, 기반이 되는 함수들을 먼저 작성한 후에, 상위 레벨에서 함수들을 조합한다. 원하는 함수를 가질 때까지 함수들을 조합한다. 모든 함수는 참조 투명하기 때문에, 다른 프로그램을 작성할 때도 코드의 변경없이 재사용하기가 쉽다.

위의 내용은 Functional Programming in Java: How functional techniques improve your Java programs 1st Edition 에서 발췌, 번역, 요약한 것입니다.

API 와 커뮤니케이션

프로그래밍 세계에서 API 는 개발자와 개발자간의 대화 방식이다. 라이브러리 API 든 HTTP API 든 API 를 만드는 개발자가 있고, API 를 사용하는 개발자가 있다. 이때 API 를 사용하는 개발자는 일종의 고객이다. 그렇다면 API 를 만들어 제공하는 개발자는 고객을 최대한 만족시키기 위해 노력해야 하고, 품질의 지표는 고객 만족이어야 한다. API 개발자의 목표는 자기가 하고 싶은 말을 하는 것이 아니라, API 를 사용하게될 고객의 입장에 서야한다. API 명명 규칙, 파라미터, 리턴 결과 등은 사용자가 봤을 때 일관적이고 직관적이어야 한다. 최대한 모호함을 없애야 한다. 주석이든 문서든 최대한 설명하려 노력해야 한다. API 개발자는 사용자의 질문에도 명쾌하게 답변해줘야 한다.

현실에서의 커뮤니케이션도 이와 비슷하다고 말하고 싶다. 내용을 전달하는 책임은 전달자에게 있는 것이지, (글 또는 언어를 매개로) 수신자에게 있지 않다. 그런데, 우리는 이런 얘기를 너무 많이 듣는다.

개떡같이 말했어도 철떡같이 알아들어야지

하나 하나를 다 일일이 얘기해야 해? 알아서 해야지?

커뮤니케이션은 쌍방의 문제이기 때문에, 물론 수신자도 적극적으로 이해하려고 노력해야 하고, 그 순간 이해가 되지 않았다면 질문을 해야 한다.

훌륭한 커뮤니케이터는 상대의 언어를 사용한다

마셜 맥루한

전달자는 충분히 내용을 전달하려 노력하지 않고, 듣는/읽는 사람은 질문하지 않는다. 우리 사회가 양방향 커뮤니케이션이 막혀있는 것 같아 답답함을 느낄 때가 많다. 인터넷 속도는 빨라졌지만, 현실 세계의 커뮤니케이션은 50년, 100년 전과 다를 바가 없다.

 

[Java] PriorityQueue

Java 에서 PriorityQueue(우선 순위 큐) 는 Heap 의 기능을 가진 Queue 이다.  기본적인 Queue 와 달리 Queue 에 값을 넣은 순서대로 처리되는 것이 아니라, 지정된 우선순위에 따라서 처리가 되는데, PriorityQue 에서는 기본으로 natural ordering(숫자의 오름 차순, 문자의 알파벳순 정렬 등) 을 기반으로 우선 순위가 지정된다.

예를 들어, 일반적인 Queue 에 아래의 순서대로 숫자 값을 넣고,

5 -> 3 -> 1 -> 2 -> 4

Queue 에서 값을 순서대로 꺼내면

5 -> 3 -> 2 -> 1 -> 4

이렇게 된다.

하지만, PriorityQueue 에 동일하게 값을 넣고 꺼내면,

1 -> 2 -> 3 -> 4 -> 5

와 같이 값이 정렬되어 나온다. 이것을 Min Heap 이라고 하는데 Queue 제일 처음(head)에 가장 장 작은 값이 위치한다. 아무런 정렬 방식을 정하지 않으면, 숫자의 경우에는 값을 넣을 때마다 오름 차순으로 PriorityQueue 내에서 정렬이 일어난다.

PriorityQueue 의 생성자 중에는 Comparator 인터페이스를 파라미터를 받는 것이 있는데, Comaprator 구현을 사용해서 정렬 방식을 바꿀 수 있다.

PriorptyQueue<Integer> pq = new PriorityQueue<>(Collections.reverseOrder());

위의 코드는 숫자의 내림 차순으로 정렬한다. 즉 Max Heap 을 사용한다.


@Test
public void test() {
    // max heap
    PriorityQueue<Integer> pq = new PriorityQueue<>(Collections.reverseOrder());
    pq.add(1);
    pq.add(3);
    pq.add(2);
    pq.add(4);

    assertEquals(4, (int)pq.poll());
    assertEquals(3, (int)pq.poll());
    assertEquals(2, (int)pq.poll());
    assertEquals(1, (int)pq.poll());
}

PriorityQueue를 활용한 알고리즘 문제

중앙값 (median) 찾기

주어진 일련의 숫자 배열에서 중앙값을 찾는 문제

입력) 10, 43, 56, 37, 19, 66, 74, 23, 41, 24

출력) (37 + 41) / 2 -> 39

힌트

Max Heap 과 Min Heap 을 각각 사용해서, 입력 값의 작은 쪽 절반은 Max Heap 에 넣고, 큰 쪽 절반은 Min Heap 에 넣는다. 그러면 Max Heap 의 제일 앞에는 작은 쪽 절반 값에서 가장 큰 값이 저장되고, Min Heap 의 제일 앞에서는 큰 쪽 절반 값 중에서 가장 작은 작은 값이 저장된다.

Max Heap:

37 -> 24 -> 23 -> 19 -> 10

Min Heap:

41 -> 43 -> 56 -> 66 -> 74

Answer

public class MedianFinder {
    private PriorityQueue<Integer> maxHeap;
    private PriorityQueue<Integer> minHeap;

    public MedianFinder() {
        maxHeap = new PriorityQueue<>(Collections.reverseOrder());
        minHeap = new PriorityQueue<>();
    }

    public void addNumber(int num) {
        maxHeap.offer(num);
        minHeap.offer(maxHeap.poll());

        if(maxHeap.size() < minHeap.size()) {
            maxHeap.offer(minHeap.poll());
        }
    }

    public double findMedian() {
        if(maxHeap.size() == minHeap.size()) {
            return (double)((maxHeap.peek() + minHeap.peek()) / 2);
        }

        return maxHeap.peek();
    }
}
@Test
public void test() {
    // max heap
    PriorityQueue<Integer> pq = new PriorityQueue<>(10, (Integer a, Integer b) -> a <= b ? 1 : -1);
    pq.add(1);
    pq.add(3);
    pq.add(2);
    pq.add(4);

    assertEquals(4, (int)pq.poll());
    assertEquals(3, (int)pq.poll());
    assertEquals(2, (int)pq.poll());
    assertEquals(1, (int)pq.poll());
}

Find Median From Data Stream 참고

 

기초 부족

벌써 10년도 넘은 이야기지만, 히딩크가 한국 축구계에 던진 자극은 굉장히 충격적이었다. 그 중에서 가장 기억이 남는 것은 다음의 말이다.

한국 선수들은 기술은 좋은데 기초 체력이 부족하다

그 전까지, 많은 사람들은 그 반대로 얘기해 왔다. “우리 선수들은 기초 체력은 좋은데 유럽 선수들에 비해 기술이 뛰어나지 못하다”. 결과적으로 히딩크의 판단이 옳았고 성공했다.

우리는 이런 말도 많이 들어 왔다.

한국 사람들은 기초 지식은 훌륭한데, 응용을 잘 하지 못한다.

나는 조금 다르게 생각한다.

응용을 잘 하지 못하는 것은 기초 지식이 부족하기 때문이다

결국 기초 지식 또는 기반 지식과 그에 대한 응용은 별개의 것이 아니다. 기초가 잘되어 있어야 응용을 할 수 있는 것이다. 서구식 사고 방식은 “내가 말로 설명하지 못하는 것은 아는 것이 아니다” 라고 한다.

결국 우리가 갈고 닦아야 하는 것은 기본기이다. 개발자로 맞딱뜨리게 되는 문제들은 결국 기본기의 축적없이 왔기 때문이다. 문제는 점점 복잡해 지는데, 해답을 찾을 수 없는 것은 기본기가 제대로 갖추어져 있지 않기 때문이다.

기초 체력을 기르자

프로그래밍의 두려움

나는 프로그래밍이 글쓰기와 많이 닮아 있다고 생각하고 있다.

Jacques Baraun 은 그의 책 “Simple and Direct” 에서 모든 훌륭한 문장은 교정을 통해 이뤄진다고 설명하는데, 교정은 다시 보는 것을 뜻한다고 한다.

“Refactoring to Patterns” 라는 책에서 발췌한 것으로 Refactoring 이 교정 과정과 흡사한 것임을 설명하고 있다.

그런데 나는 글쓰기를 무척이나 싫어하는 사람이다. 글쓰기가 그냥 싫어서 또는 막연히 이과는 글쓰는 일이 별로 없을 것이라고 생각해서, 이과 적성이 아닌데도 이과를 선택했다. 난 수학, 과학을 썩 잘하지 못한다. 그런데 지금 직업으로 삼고 있는 프로그래밍이 글쓰기와 흡사하다는 것을 깨닫게 된 것은 직업 프로그래머가 되고 수년 후다.

프로그래머에게 필요한 덕목 중 하나가 “용기다”. 왜냐하면 프로그래밍은 프로그래머에게 끊임없이 질문을 해대고 대답을 요구하는데, 지금은 그 해답을 줄 수 없기 때문이다. 프로그래밍은 그 대답을 찾아가는 과정까지도 포함된다. 지금 답을 알지 못하기 때문에,  주어진 시간 안에 혹시나 답을 찾지 못하면 어떡하지 하는 두려움이 올라오는 것이다. 물론 해답을 찾는다면 그 성취감은 무척이나 크다.

이 두려움 마저 글쓰기랑 닮은 것 같다.

나는 왜 글쓰기를 두려워 할까? 우연히 생각해 보다가 2가지 정도가 떠올랐다.

  • 나는 글을 잘 못 쓴다고 미리 겁부터 먹고 있다. “나는 작가 정도의 글을 쓰지는 못할 거야. 아니 작가는 아니더라도 유명한 블로거 정도의 글도 쓰지 못할 거야. 그러니 안 쓰는 게 나을 거야.” 이렇게 생각하고 있는 것이다.
  • 내 생각과 의도, 내 마음이 들키는 것을 두려워 하고 있다. 글을 쓰게 되면, 자연스레 내가 가진 생각, 의도 등이 고스란히 드러난다. 이러한 나에게 감춰진 것이 노출 되었을 때, 누군가 비난할 것만 같았다.

프로그래머로서 갖는 두려움이 위에서 말한 글쓰기와 다르지 않다.

  • 나는 저 유명한 개발자 또는 오픈 소스 컨트리뷰터와 같은 코드를 만들기는 어려울 거야. 내 실력은 그저 그런 정도야. 코드를 보이는게 부끄러워.
  • 코드는 내가 가진 논리적 사고, 창의적 문제 해결 능력 등이 고스란히 들어날 수 밖에 없다. 누군가 내 코드를 보면 왜 저렇게 코드를 만들었지 비난할 것만 같다.

 

HTTPS 프로세스 이해

Understanding HTTPS Protocol 를 읽고, HTTPS 프로세스의 중요 부분을 정리해 봅니다.

  1. 브라우저 (또는 http client) 가 https 요청을 서버에 보냅니다. 서버는 https 연결을 위해 443 포트를 기본으로 사용합니다.
  2. 브라우저와 서버간의 연결이 맺어지면, SSL handshake 가 시작됩니다. SSL handshake 는 브라우저와 서버간에 데이터를 어떻게 암호화하고 복호화할지를 결정하는 프로세스입니다. SSL handshake 프로세스는 다음과 같습니다.
    • [browser] 브라우저가 서버에 hello message 를 보냅니다. 이 메시지에는 브라우저가 지원하는 SSL 버전등의 브라우저의 상세 내용이 포함됩니다.
    • [server] 서버 역시 비슷한 hello message 를 응답으로 보냅니다. 여기에는 서버가 사용할 SSL 버전등의 서버 상세 내용이 포함됩니다.
    • [server] 서버가 자신의 인증서를 브라우저에 전송합니다. 인증서에는 데이터를 암호화할 때 사용할 public key 가 포함됩니다. 뿐만 아니라 인증서에는 인증서 발행 기관, 인증서 유효 기간, 서버의 신원 정보 등의 상세 내용이 포함됩니다. 인증서를 통해서 브라우저는 자신이 접속하려고 한 서버가 맞는지를 검증할 수 있습니다.
    • [browser] 브라우저가 서버의 인증서를 검증한 후에, certificate verify message 를 서버에 보냅니다.
    • [browser] 브라우저가 서버에게 change cipher spec command 를 보냅니다. 이 메시지는 서버에게 이제부터 데이터를 암호화 해서 보낼테니 준비하라고 알리는 것입니다.
    • [server] 서버도 마찬가지로 비슷한 change cipher spec command 를 브라우저에 보냅니다. 이 메시지는 브라우저에게 이제부터 데이터를 암호화 해서 보내겠다라는 것을 알리는 것입니다.
  3. SSL handshake가 끝나면 브라우저는 데이터 암호화에 사용할 공통의 키(symmetric key)를 생성합니다. 그리고 이 공통 키를 서버의 인증서로부터 얻은 public key 를 사용해서 암호화를 합니다.  브라우저는 암호화된 공통 키를 서버에 보냅니다. 서버는 암호화된 공통 키를 받아서 자신의 private key 로 복호화 합니다. 이제 브라우저와 서버는 이 공통의 키를 가지고 데이터를 암호화, 복호화 합니다.

[Docker on Mac] container to host 연결하기

Docker container 내부에서 host (Mac) 에 실행중인 서비스를 호출하려고 하니, 잘 되지 않았습니다. 한참을 구글링을 하다가 다음을 발견하였습니다. (원문 링크)

I WANT TO CONNECT FROM A CONTAINER TO A SERVICE ON THE HOST

The Mac has a changing IP address (or none if you have no network access). From 17.06 onwards our recommendation is to connect to the special Mac-only DNS name docker.for.mac.localhost which will resolve to the internal IP address used by the host.

Docker container 내부에서 host 인 Mac 으로 네트워크 통신을 하기 위해서는 특별한 DNS 이름인 docker.for.mac.localhost”를 사용해야 합니다.

예를 들어, dockerized web app 이 있고, 역시 dockerized mysql 이 있을 때에, dockerized web app 에서 dockerized mysql 을 사용하려고 하면, data source 주소가 다음과 같아야 합니다.

jdbc:mysql://docker.for.mac.localhost:3306/db

Spring Boot with Docker

최근에 Spring Boot 으로 만든  어플리케이션을 Docker 를 사용하여 배포를 할 일이 생겨서 Spring Boot with Docker 의 내용을 요약 정리해 봅니다.

1. Simple Spring Boot Application

Spring Boot을 사용하여 “Hello Docker World”를 출력하는 간단한 웹 어플리케이션을 다음과 같이 작성합니다.

@SpringBootApplication
@RestController
public class DockerApplication {

   @GetMapping
   public String home() {
      return "Hello Docker World";
   }

   public static void main(String[] args) {
      SpringApplication.run(DockerApplication.class, args);
   }
}
$ http localhost:8080

Hello Docker World

2. Dockerfile 작성

Docker 이미지를 생성하기 위해 Dockerfile을 작성합니다. 파일의 위치는 src/main/docker/Dockerfile 입니다.

FROM frolvlad/alpine-oraclejdk8:slim
VOLUME /tmp
ADD gs-spring-boot-docker-0.0.1-SNAPSHOT.jar app.jar
RUN sh -c 'touch /app.jar'
ENV JAVA_OPTS=""
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar"]
  • FROM: jdk 를 컨테이너에 포함합니다.
  • VOLUME: embedded tomcat 이 사용할 temp 디렉토리 입니다.
  • ADD: artifact 이름을 버전과 함께 명시하고 app.jar 라는 이름으로 컨테이너에 포함합니다.
  • RUN: app.jar 에 touch 명령을 실행시켜서 파일의 수정날짜를 변경합니다. 여기서는 큰 의미가 없지만, 파일의 수정 날짜가 요구되는 경우가 있습니다.
  • ENV: Java의 실행 옵션을 지정합니다.
  • ENTRYPOINT: Docker가 실행될 때 시작되는 명령입니다. 기본적으로  java -jar 과 다른 것이 없습니다. 시스템 환경 변수로 java.security.egd=file:/dev/./urandom 을 정의하고 있습니다. /dev/urandom 은 random number generator로 사용됩니다. tomcat의 startup 시간을 줄이는 효과가 있다고 합니다. 자세한 내용은 /dev/./urandom 을 참고하시기 바랍니다.

3. docker-maven-plugin 설정

docker-maven-plugin을 사용하여 Docker 이미지를 만들 수 있습니다.

<groupId>jade</groupId>
<artifactId>gs-spring-boot-docker</artifactId>
.....
<properties>
   <docker.image.prefix>jade</docker.image.prefix>
</properties>
......
<build>
.......
<plugins>
  <plugin>
   <groupId>com.spotify</groupId>
   <artifactId>docker-maven-plugin</artifactId>
   <version>0.4.14</version>
   <configuration>
      <imageName>${docker.image.prefix}/${project.artifactId}</imageName>
      <dockerDirectory>src/main/docker</dockerDirectory>
      <resources>
         <resource>
            <targetPath>/</targetPath>
            <directory>${project.build.directory}</directory>
            <include>${project.build.finalName}.jar</include>
         </resource>
      </resources>
   </configuration>
  </plugin>
 </plugins>
</build>

4. Docker Build & Run

4.1 Build

$ mvn package docker:build

다음과 같이 Maven이 빌드됩니다.

[INFO] --- docker-maven-plugin:0.4.14:build (default-cli) @ gs-spring-boot-docker ---
[INFO] Copying /Users/jadeim/Playground/gs-spring-boot-docker/target/gs-spring-boot-docker-0.0.1-SNAPSHOT.jar -> /Users/jadeim/Playground/gs-spring-boot-docker/target/docker/gs-spring-boot-docker-0.0.1-SNAPSHOT.jar
[INFO] Copying src/main/docker/Dockerfile -> /Users/jadeim/Playground/gs-spring-boot-docker/target/docker/Dockerfile
[INFO] Building image springio/gs-spring-boot-docker
Step 1/6 : FROM frolvlad/alpine-oraclejdk8:slim
---> 354831d86a05
Step 2/6 : VOLUME /tmp
---> Using cache
---> b209c9570887
Step 3/6 : ADD gs-spring-boot-docker-0.0.1-SNAPSHOT.jar app.jar
---> 143a9cb7b3a1
Removing intermediate container 40c57077703e
Step 4/6 : RUN sh -c 'touch /app.jar'
---> Running in f8789a71a32e
---> c9f46c427f78
Removing intermediate container f8789a71a32e
Step 5/6 : ENV JAVA_OPTS ""
---> Running in 4a9e5b610b76
---> fc0092b4459b
Removing intermediate container 4a9e5b610b76
Step 6/6 : ENTRYPOINT sh -c java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar
---> Running in db23115803ef
---> c80937db215d

4.2 Run

$ docker run -p 8080:8080 -t jade/gs-spring-boot-docker

이제 컨테이너로 어플리케이션 배포 및 실행이 되었습니다.
http://localhost:8080 을 실행하면 “Hello Docker World”가 출력됩니다.