Design Pattern - Adapter

해당 포스팅은 Java 언어로 배우는 디자인 패턴 입문 책을 참고해 작성했습니다.
사용된 코드는 jongmin92/Design-Pattern repository 에서 확인할 수 있습니다.

Adapter 패턴

이미 제공되어 있는 것을 그대로 사용할 수 없을 때, 필요한 형태로 교환하고 사용하는 일이 자주 있습니다. ‘이미 제공되어 있는 것’과 ‘필요한 것’ 사이(서로 다른 두 개)의 ‘차이’를 없애주는 디자인 패턴이 Adapter 패턴 입니다.

Adapter 패턴은 Wrapper 패턴으로 불리기도 하며, 다음과 같은 두 가지 종류가 있습니다.

  • 클래스에 의한 Adapter 패턴 (상속을 사용한 Adapter 패턴)
  • 인스턴스에 의한 Adapter 패턴 (위임을 사용한 Adapter 패턴)

예제 프로그램(1) - 상속을 사용한 Adapter 패턴

만들 예제 프로그램은 주어진 문자열을 아래와 같이 표시하는 간단한 것입니다.

1
2
(Hello)
*Hello*

Banner 클래스에는 문자열을 괄호로 묶어서 표시하는 showWithParen 메소드와 문자열 전후에 *를 붙여 표시하는 showWithAster 메소드가 준비되어 있습니다. (이미 제공되어 있는 것)

Print 인터페이스에는 문자열을 느슨하게(괄호 사용) 표시하기 위한 printWeak 메소드와 문자열을 강하게 표시하기 위한 (* 표시를 앞뒤에 붙여 강조) printStrong 메소드가 선언되어 있습니다.

지금 하고 싶은 일은 Banner 클래스를 사용해서 Print 인터페이스를 충족시키는 클래스를 만드는 일입니다.

PrintBanner 클래스가 어댑터의 역할을 담당합니다. 이 클래스는 제공되어 있는 Banner 클래스를 상속해서, 필요로 하는 Print 인터페이스를 구현합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Banner {
private String string;

public Banner(String string) {
this.string = string;
}

public void showWithParen() {
System.out.println('(' + string + ')');
}

public void showWithAster() {
System.out.println('*' + string + '*');
}
}
1
2
3
4
5
public interface Print {
void printWeak();

void printStrong();
}

PrintBanner 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class PrintBanner extends Banner implements Print {
public PrintBanner(String string) {
super(string);
}

@Override
public void printWeak() {
showWithParen();
}

@Override
public void printStrong() {
showWithAster();
}
}

Main 클래스

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
Print p = new PrintBanner("Hello");
p.printWeak();
p.printStrong();
}
}

Main 클래스는 Print 인터페이스를 사용하고 있습니다. Banner 클래스나 showWithParen 메소드나 showWithAster 메소드는 Main 클래스 소스 코드 상에서는 완전히 감추어져 있습니다.

예제 프로그램(2) - 위임을 사용한 Adapter 패턴

Main 클래스, Banner 클래스, Print 인터페이스는 예제 프로그램(1)과 동일합니다.

PrintBanner 클래스는 banner 필드에서 Banner 클래스의 인스턴스를 가집니다. 이 인스턴스는 PrintBanner 클래스의 생성자에서 생성합니다.

이전 예와는 달리, 이번에는 필드를 경우해서 호출하고 있습니다. 즉 위임을 하고 있습니다.

PrintBanner 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class PrintBanner implements Print {
private Banner banner;

public PrintBanner(String string) {
this.banner = new Banner(string);
}

@Override
public void printWeak() {
banner.showWithParen();
}

@Override
public void printStrong() {
banner.showWithAster();
}
}

Adapter 패턴의 구성요소

  • Target(대상)의 역할
    지금 필요한 메소드를 결정합니다. 예제 프로그램에서 Print 인터페이스(상속의 경우)나 Print 클래스(위임의 경우)가 이 역할을 합니다.
  • Client(의뢰자)의 역할
    Target 역할의 메소드를 사용해서 일을 합니다. 예제 프로그램에서 Main 클래스가 이 역할을 합니다.
  • Adaptee(개조되는 쪽)의 역할
    Adaptee는 이미 준비되어 있는 메소드를 갖고 있는 역할입니다. Adaptee역의 메소드가 Target 역할의 메소드와 일치하면 다음 Adapter의 역할은 필요없습니다.
  • Adapter의 역할
    Adapter 패턴의 주인공입니다. Adaptee 역할의 메소드를 사용해서 어떻게든 Target 역할을 만족시키기 위한 것이 Adapter 패턴의 목적이며, Adapter 역할의 임무입니다. 예제 프로그램에서는 PrintBanner 클래스가 Adapter의 역할을 합니다.

사고 넓히기

어떤 경우에 사용할까?

Adapter 패턴은 기존의 클래스를 개조해서 필요한 클래스를 만듭니다. 이 패턴으로 필요한 메소드를 빠르게 만들 수 있습니다.

이미 만들어진 클래스를 새로운 인터페이스(API)에 맞게 개조시킬 때 기존 클래스의 소스를 바꾸어서 ‘수정’하려고 합니다. 그러나 그렇게하면 테스트가 이미 끝난 기존의 클래스를 수정한 후에 다시 한 번 테스트 해야 합니다. Adapter 패턴은 기존의 클래스를 전혀 수정하지 않고 목적한 인터페이스(API)에 맞추려는 것입니다.

만약 버그가 발생해도 기존의 클래스(Adaptee의 역할)에는 버그가 없으므로 Adapter 역할의 클래스를 중점적으로 조사하면 되고, 프로그램 검사도 상당히 쉬워집니다.

(웹 프로그래머를 위한 서블릿 컨테이너의 이해) 리뷰


한빛미디어 - 웹 프로그래머를 위한 서블릿 컨테이너의 이해

스프링을 이용해 개발하고 있지만 스프링이 구동되는 서블릿 컨테이너에 대한 이해가 부족해 읽게 되었다. (어떻게 돌아가고 있는지 궁금했다…)

책의 구성은 서블릿 컨테이너를 학습해야 하는 이유를 시작으로 HTTP 프로토콜에 대한 이해 그리고 서블릿의 이해로 이어진다.
그 후에는 실제 서블릿 컨테이너에서 HTTP 프로토콜을 어떻게 분석해 서블릿에 전달해주는지, 스레드 풀을 이용해 동시에 들어오는 request 들을 어떻게 처리하는지에 대한 내용을 다루고 있다.

도입 부분(서블릿 컨테이너를 학습해야 하는 이유)에 다음과 같은 이야기가 있다.

특히 성능과 관련된 문제가 발생했을 때, 웹 기반 시스템의 하위 레벨 영역인 웹 애플리케이션 서버가 담당하는 부분을 모르고서는 근본적인 원인 규명 자체가 불가능합니다. 웹 애플리케이션 서버의 내부구조와 동작 원리를 이해하지 못하는지가 웹 프로그램의 고성능, 고가용성에 대한 요구를 충족시킬 수 있는지 결정한다고 할 수 있습니다.

위의 말에 전적으로 동의한다. 스프링을 이용해 웹 애플리케이션을 만들기 위해 스프링을 공부하고 이해하듯, 스프링이 실행되는 서블릿 컨테이너에 대한 공부와 이해도 필요하다. 아마 나와 같이 서블릿을 이용한 개발 경험 없이, 스프링을 시작했다면 이 책을 읽어보기를 더욱 추천하고 싶다.

Design Pattern - Iterator

해당 포스팅은 Java 언어로 배우는 디자인 패턴 입문 책을 참고해 작성했습니다.
사용된 코드는 jongmin92/Design-Pattern repository 에서 확인할 수 있습니다.

Iterator 패턴

Java 언어에서 배열 arr의 모든 요소를 표시하기 위해서는 다음과 같이 for문을 사용합니다.

1
2
3
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}

for문의 i++에서 i를 하나씩 증가시키면서 배열 arr의 요소 전체를 처음부터 차례대로 검색하게 됩니다. 여기서 사용되고 있는 변수 i의 기능을 추상화해서 일반화한 것을 디자인 패턴에서는 Iterator 패턴이라고 합니다.

Iterator 패턴이란, 무엇인가 많이 모여있는 것들을 순서대로 지정하면서 전체를 검색하는 처리를 실행하기 위한 것입니다. Iterator는 무엇인가를 ‘반복한다’라는 의미이며, 반복자라고도 합니다.

예제 프로그램

Iterator 패턴을 사용해 책장에 꽂혀 있는 책들을 하나씩 검색해 책 이름을 출력해보는 예제 프로그램을 작성해 보겠습니다.

Aggregate 인터페이스

Aggregate 인터페이스는 요소들이 나열되어 있는 ‘집합체’를 나타냅니다. 이 인터페이스를 구현하고 있는 클래스는 배열과 같이 무엇인가가 많이 모여 있습니다.

1
2
3
public interface Aggregate {
Iterator iterator();
}

Aggregate 인터페이스에 선언되어 있는 메소드는 iterator 메소드 하나뿐입니다. 이 메소드는 집합체에 대응하는 Iterator를 1개 작성하기 위한 것입니다.

Iterator 인터페이스

Iterator 인터페이스는 요소를 하나씩 나열하면서 루프 변수와 같은 역할을 수행합니다.

1
2
3
4
public interface Iterator() {
boolean hasNext();
Object next();
}

hasNext 메소드는 다음 요소가 존재하는지를 조사하기 위한 메소드입니다. 다음 요소가 존재하면 true를 반환하고, 다음 요소가 존재하지 않는 마지막 요소라면 false를 반환합니다. 즉, hasNext는 루프의 종료 조건으로 사용됩니다.

next 메소드는 집합체의 요소를 1개 반환합니다. 또한 next 메소드를 호출했을 때 다음 요소를 반환하도록 내부 상태를 다음으로 진행시켜 두는 역할도 함께합니다.

Book 클래스

Book 클래스는 책을 나타내는 클래스입니다.

1
2
3
4
5
6
7
8
9
10
11
public class Book {
private String name;

public Book(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

BookShelf 클래스

BookShelf 클래스는 책장을 나타내는 클래스입니다. 이 클래스를 집합체로 다루기 위해 Aggregate 인터페이스를 구현합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class BookShelf implements Aggregate {
private Book[] books;
private int last = 0;

public BookShelf(int maxsize) {
this.books = new Book[maxsize];
}

public Book getBookAt(int index) {
return books[index];
}

public void appendBook(Book book) {
this.books[last] = book;
last++;
}

public int getLength() {
return last;
}

@Override
public Iterator iterator() {
return new BookShelfIterator(this);
}
}

iterator 메소드는 BookShelf 클래스에 대응하는 Iterator로서, BookShelfIterator라는 클래스의 인스턴스를 생성해서 그것을 반환합니다.

BookShelfIterator 클래스

BookshelfIterator 를 Iterator로서 다루기 위해 Iterator 인터페이스를 구현합니다. bookShelf 필드는 BookShelfIterator가 검색할 책장이고, index 필드는 현재 가리키는 책을 가리키는 첨자입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 public class BookShelfIterator implements Iterator {
private BookShelf bookShelf;
private int index;

public BookShelfIterator(BookShelf bookShelf) {
this.bookShelf = bookShelf;
this.index = 0;
}

@Override
public boolean hasNext() {
return index < bookShelf.getLength();
}

@Override
public Object next() {
Book book = bookShelf.getBookAt(index);
index++;
return book;
}
}

Main 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Main {
public static void main(String[] args) {
BookShelf bookShelf = new BookShelf(4);
bookShelf.appendBook(new Book("Effective Java"));
bookShelf.appendBook(new Book("Head First Java"));
bookShelf.appendBook(new Book("Thinking In Java"));
bookShelf.appendBook(new Book("Agile Java"));

Iterator it = bookShelf.iterator();
while (it.hasNext()) {
Book book = (Book) it.next();
System.out.println(book.getName());
}
}
}
1
2
3
4
5
// 실행결과
Effective Java
Head First Java
Thinking In Java
Agile Java

Iterator 패턴의 구성요소

  • Iterator(반복자)의 역할
    요소를 순서대로 검색해가는 인터페이스(API)를 결정 (hasNext, next)

  • ConcreteIterator(구체적인 반복자)의 역할
    Iterator가 결정한 인터페이스(API)를 실제로 구현

  • Aggregate(집합체)의 역할
    Iterator 역할을 만들어내는 인터페이스(API)를 결정

  • ConcreteAggregate(구체적인 집합체)의 역할
    Aggregate 역할이 결정한 인터페이스(API)를 실제로 구현

사고 넓히기

구현에 상관 없이 Iterator를 사용할 수 있다.

1
2
3
4
while (it.hasNext()) {
Book book = (Book) it.next();
System.out.println(book.getName());
}

여기서 사용되고 있는 것은 hasNext와 next라는 Iterator의 메소드 뿐입니다. BooShelf의 구현에서 사용되고 있는 메소드는 호출되고 있지 않습니다. 결국 위 코드의 while 루프는 BookShelf의 구현에 의존하지 않습니다.

그렇기 때문에 현재 배열을 사용해 구현하고 있는 BookShelf를 List를 사용하도록 수정해도, 위의 while 루프는 전혀 변경하지 않아도 동작합니다.

Aggregate와 Iterator의 대응

BookShelfIterator는 BookShelf가 어떻게 구현되고 있는지 알기 때문에, ‘다음 책’을 얻기 위해 getBookAt 메소드를 호출할 수 있었습니다.

만약 BookShelf의 구현을 전부 변경하고, getBookAt 메소드라는 인터페이스(API)도 변경된다면 BookShelfIterator의 수정이 필요하게 됩니다.

소나큐브

해당 포스팅은 자바 필수 프로젝트 유틸리티 책의 8.3 소나큐브 부분을 참고해 작성했습니다.

소나큐브

소나큐브를 사용해서 작성한 코드를 정적 분석하고 작성한 테스트로 얼마나 검증했는지 측정할 수 있다.

소나큐브를 로컬 환경에 설치하고 젠킨스와 소나큐브를 연결해서 소나큐브의 기본 퀄리티 게이트(Quality Gate)를 실행시켜보자. 퀄리티 게이트는 조직의 모든 소스가 통과해야만 하는 소스의 품질을 정의해둔 것으로 소스의 품질을 보증하는 수단으로 이용된다.

기능과 특징

소나큐브의 주요 기능은 아래와 같다.

  • 복잡도 확인
  • 코드 중복 검출
  • 코딩 규칙 확인
  • 잠재적 버그 검출
  • 단위 테스트
  • 커버리지

소나큐브 자체에서 지원하는 심플한 UI를 이용해 프로젝트의 소스 코드가 얼마나 개선되고 있는지를 직관적으로 확인할 수 있다. 소나큐브의 특징은 다음과 같다.

  • 서버는 크게 웹 서버, 검색 서버, 연산 서버로 구성된다.
  • 데이터베이스는 소나큐브 자체의 설정과 각 프로젝트의 정보가 저장된다.
  • 플러그인을 추가할 수 있다.
  • 소나큐브 스캐너(SonarQube Scanner)로 코드를 분석한다.

설치

brew를 이용해 설치 후 실행한다.

1
2
$ brew install sonarqube
$ brew services start sonarqube

brew를 이용해 설치하게 되면 다음과 /usr/local/Cellar/sonarqube/7.2.1/libexec/conf 디렉터리에 설치된다. 각 디렉터리의 역할은 다음과 같다.

  • bin : 운영체제별 실행파일이 있다.
  • conf : 소나큐브의 설정 팡리이 있다. 설정 파일에서 데이터베이스 연결과 웹 서버의 설정 등을 한다.
  • data : 기본 데이터베이스인 H2 데이터베이스를 사용할 때 데이터가 저장되는 곳이다. 테스트 목적이 아니라면, 실제 운영할 때는 다른 데이터베이스를 사용해야 한다.
  • elasticsearch : 루씬 기반의 검색 엔진인 elasticsearch가 포함되어 있다.
  • extensions : jdbc-driver와 플러그인이 포함된다.
  • lib : 실제 애플리케이션 바이너리가 포함되어 있다.
  • logs : 각 로그가 출력되는 디렉터리이다. 설정 파일에서 변경 가능하다.
  • temp : 서버 실행 시에 필요한 임시 파일이 저장된다. 실행 중에 삭제하면 안된다.
  • web : UI에 필요한 이미지와 CSS, JS 파일이 있다.

실행 후 http://localhost:9000에 접속하면 다음과 같은 화면을 볼 수 있다.

관리자로 접속해서 젠킨스와 연결할 사용자를 만들어 보자. 오른쪽 상단 위에 있는 ‘Log in’ 을 클릭한 후 초기 ID/PW인 admin/admin을 입력해서 로그인한다.

로그인이 완료되면 튜토리얼 페이지가 나온다. 우측 상단의 ‘Skip this tutorial’ 을 클릭해서 스킵한다.

아래와 같이 프로젝트 페이지를 확인할 수 있다.

젠킨스 사용자 생성

젠킨스에서 소나큐브에 접속할 때 사용할 사용자를 생성한다.

화면 상단의 메뉴에서 ‘Administration’ -> ‘Security’ -> ‘Users’ -> ‘Create User’ 를 클릭해서 사용자 생성 창을 출력한다.

jenkins라는 이름으로 사용자를 생성한다.

젠킨스에서 접속할 때 사용할 토큰을 생성한다. 조금전 생성한 jenkins 유저의 ‘Token’ 을 클릭한다.

‘Enter Token Name’ 에 적당한 이름을 넣고 ‘Generate’ 버튼을 눌러서 생성한다.

생성된 토큰 앞에 있는 ‘Copy’ 버튼을 눌러 젠킨스에서 사용할 키를 저장한다.

소나큐브 스캐너 설정

이번에는 젠킨스의 빌드 과정에서 소나큐브를 연동한다.

‘Jenkins 관리’ -> ‘플러그인 관리’에서 ‘SonarQube Scanner’를 설치 한다.

‘Jenkins 관리’ -> ‘시스템 설정’ 으로 이동하면, ‘SonarQube servers’ 라는 설정이 추가된것을 확인할 수 있다. 만약 플러그인을 설치했는데도 해당 설정이 표시되지 않는다면 젠킨스 서버를 재실행해본다.

‘Add SonarQube’ 를 클릭한다.

‘고급’ 버튼을 클릭한 후 시스템 환경 변수를 사용하도록 각각 다음과 같이 설정한다.

  • Environment variables : 체크 박스 선택
  • Name : SonarQube-Local 입력 (스페이스와 한글을 입력하지 않는다.)
  • Server URL : 소나큐브의 URL 입력(여기서는 default인 http://localhost:9000)
  • Server authentication token : 소나큐브에서 생성한 토큰 입력
  • Additional analysis properties :
    sonar.sources=src sonar.java.binaries=target/classes, target/test-classes

‘저장’ 을 클릭해서 저장한다.

소나큐브 스캐너를 추가하는 작업을 진행한다. ‘Jenkins 관리’ -> ‘Global Tool Configuration’ 을 클릭한다.

‘SonarQube Scanner for MSBuild’와 ‘SonarQube Scanner’ 아래의 버튼을 클릭해서 각각 스캐너를 추가한다.

‘Name’에 적당한 이름을 넣고 ‘Install automatically’ 가 체크되어 있는지 확인 후 저장한다.

새로운 잡을 생성해서 테스트 해보자.

  1. 젠킨스 메인 페이지에서 ‘새로운 Item’을 클릭하고 잡 이름에 ‘SonarQubeTset’를 입력하고 ‘Freestyle project’를 선택 후 저장 버튼을 클릭한다.

  2. 소스 코드 관리 : 테스트 프로젝트의 깃허브 주소를 넣는다. (여기서는 ‘자바 필수 프로젝트 유틸리티‘ 책에서 제공한 ‘spring-mvc-example‘ 프로젝트를 fork해 사용했다.)

  3. Credentials : 저장되어 있는 자격증명에서 선택한다.

  4. 빌드 환경에서 ‘Prepare SonarQube Scanner environment’를 선택한다. (이번 테스트에서는 필요하지는 않지만, ‘빌드 환경’에서 호나경 변수를 사용할 수 있게 하려고 체크하는 것이다.)

  5. Build : ‘Invoke top-level Maven targets’를 선택한다.

  6. Goals : ‘clean install’을 입력한다. (다른 골을 추가해도 된다.)

  7. 다시 Build의 ‘Add build step’을 클릭해서 ‘Execute SonarQube Scaner’를 추가한 후 ‘Analysis properties’에 아래의 정보를 입력한다.

    1
    2
    3
    sonar.projectKey=spring-mvc-example
    sonar.projectName=spring-mvc-example
    sonar.projectVersion=1.0.0

    이 테스트에서는 고정값으로 입력했지만 매개변수로 처리하거나 환경 변수에서 치환하는 방법도 많이 사용한다.

  8. ‘저장’ 버튼을 클릭해서 잡 대시보드로 이동한다.

소나큐브의 아이콘이 추가된것을 확인할 수 있다.

소나큐브 빌드

‘Build Now’ 를 클릭해서 빌드한다. 정상 종료되면 ‘SonarQube Quality Gate’ 가 표시되고 실행 결과를 알려준다.

화면에 표시되는 ‘SonarQube’ 링크나 ‘OK’를 클릭하면 소나큐브의 대시보드로 이동한다.

상단의 메뉴에서 ‘Issues’, ‘Code’를 통해 각 결과를 확인할 수 있다.

젠킨스

젠킨스

젠킨스 는 자바로 작성된 오픈 소스 소트트웨어로 지속적 통합(Continuous Integration, CI)과 지속적 배포(Continuous Delivery, CD)를 제공한다.

웹 애플리케이션 형태로 제공되고 있어서 어떠한 환경에서도 손쉽게 설치할 수 있으며 도커를 사용해 설치할 수도 있다. 또한 천 개 이상의 플러그인으로 다양한 시스템과 연동할 수 있다.

젠킨스의 주요 기능은 다음과 같다.

  • 형상관리 도구와의 연동
  • 소스 코드 체크아웃
  • 웹 인터페이스
  • 테스트 보고서 생성
  • 빌드 및 테스트 자동화
  • 실행 결과 통보
  • 코드 품질 감시
  • 다양한 인증 기반과 결합한 인증 및 권한 관리
  • 배포 관리 자동화
  • 분산 빌드(마스터 슬레이브)
  • 그루비 스크립트를 이용한 자유로운 잡 스케줄링

젠킨스는 개발자가 소스코드를 추가, 수정한 뒤 형상관리 도구에 저장하면 자동으로 읽어 빌드 및 테스트를 실행한다.

젠킨스 설치

젠킨스를 사용하려면 JDK와 메이븐이 필요하다.

macOS에서 brew를 이용해 쉽게 설치가 가능하다.

1
$ brew install jenkins

추후 필요하다면 다음과 같이 삭제할 수도 있다.

1
$ brew remove jenkins

설치 후 다음과 같이 젠킨스를 백그라운드 서비스로 구동 및 중지가 가능하다.

1
2
$ brew services start jenkins
$ brew services stop jenkins

젠킨스를 실행 후 웹사이트에 접속한다. http://localhost:8080

처음 접속 시 Administrator password를 입력하게 되어 있는데 아래의 경로에 있는 파일에서 키를 확인해 입력한다.

/Users/{사용자 계정}/.jenkins/secrets/initialAdminPassword


젠킨스를 어떻게 설치할지 결정할 수 있다. ‘Install suggested plugins’를 선택하면 젠킨스에서 추천하는 플러그인들이 같이 설치되고, ‘Select plugins to install’을 선택하면 필요한 플러그인을 선택하여 설치할 수 있다.


필요한 플러그인들을 자동으로 설치하기 시작한다.


설치가 끝나면 관리자 정보 입력 화면이 나온다. 정보를 입력한 후 ‘Save and Finish’ 버튼을 클릭한다.


빌드 잡(job) 생성하기

  1. 좌측 메뉴 상단에 있는 ‘새로운 Item’을 클릭한다. ‘Enter an item name’에 적당한 이름을 넣고 아래의 템플릿 중 ‘Freestyle project’를 선택한 후 ‘OK’ 버튼을 클릭한다. (Freestyle project는 거의 모든 젠킨스의 설정을 자유롭게 설정할 수 있다.)
  2. Github에 테스트를 위해 간단하게 만들어 놓은 프로젝트를 가져와 빌드를 테스트 한다.
  3. Github에서 프로젝트를 가져오기 위해서는 자격 증명을 추가해야 한다. ssh-key를 등록하거나 Github의 계정을 입력해야 한다.
  4. Credentials 선택 박스에서 조금 전 추가한 계정을 선택한 후 ‘저장’ 버튼을 클릭한다.
  5. ‘Build Now’를 클릭해 빌드를 진행한다.
  6. ‘Console Output’을 누르면 빌드가 성공한 것을 확인할 수 있다.

그레이들 기초

그레이들

그루비와 그레이들

그루비 는 자바 가상머신에서 동작하는 오픈 소스 스크립트 언어이다. 그루비는 자바 문법을 더욱 쉽게 쓰기 위해 스크립트 언어와 비슷한 문법으로 되어 있어서, 대부분의 자바 프로그래머는 자바 코드를 작성하는 느낌으로 그루비를 작성할 수 있다. 동적 언어이며 자바와 달리 작성한 스크립트를 컴파일할 필요 없이 직접 실행할 수도 있다.

또한, 일부 자바 프레임워크에서는 그루비를 지원한다. 대표적인 스프링 프레임워크에서도 그루비를 이용할 수 있다.

이처럼 자바와 거의 같고, 스크립트 언어처럼 부담 없이 작성해서 바로 실행할 수 있는 특징을 고려하면, 그루비를 사용해서 자바 빌드 도구를 만들려는 생각은 자연스럽다.

그루비의 이러한 이점을 최대한 활용해서 개발한 빌드 도구가 그레이들 이다.

그레이들이란

그레이들은 그루비를 사용한 빌드 도구이다. 메이븐은 XML을 이용하여 빌드 정보를 기술했는데, 그레이들은 그루비를 이용해 빌드 정보를 기술하기 한다. 때문에 자바 프로그래머가 좀 더 쉽게 다룰 수 있다.

그레이들의 특징은 다음과 같다.

  • 유연한 언어로 기술
    그루비라는 프로그래밍 언어를 사용해서 기술하기 때문에 유연하게 각종 처리를 수행할 수 있다. 또한 기술하는 내용을 분할하거나 구조화하는 것도 간단하다.
  • 태스크로 처리
    그레이들은 ‘태스크’라는 개념을 이용해 프로그램을 작성한다. 다양한 용도별로 태스크를 만들어서 그 안에 처리를 기술한다.
  • 자바/그루비/스칼라 기본 지원 + 알파
    그레이들은 자바 가상 머신에서 동작하는 언어를 중심으로 지원한다. (별도의 네이티브 코드 플러그인을 사용하면 C/C++ 등, 다른 언어에도 대응할 수 있다.)
  • 각종 도구와 통합
    여러 도구들(앤트, 아파치 아이비 등)과 통합되어 처리를 실행할 수 있다. 또한, 메이븐의 pom.xml을 그레이들용으로 변환하는 도구도 있다.
  • 메이븐 중앙 저장소 대응
    그레이들에서는 메이븐 중앙 저장소를 지원하기 때문에, 중앙 저장소에 있는 라이브러리 모두 그대로 이용 가능하다.

그레이들 사용하기

그레이들 소프트웨어에는 그루비가 포함되어 있기 때문에, 단지 그레이들을 사용하는 용도라면 그루비를 설치할 필요는 없다. (그레이들을 설치하는 것만으로는 그루비 언어를 이용해서 프로그래밍할 수는 없다.)

그레이들 프로젝트 생성

그레이들 명령어를 사용해 프로젝트를 생성할 수 있다.

1
2
3
mkdir gradle-app
cd gradle-app
gradle init --type java-library

gradle init

그레이들 명령어는 gradle OO 형태로 실행한다. 위에서 프로젝트를 생성하며 사용한 init은 그레이들에서 '태스크' 라고 부른다. 이 init은 프로젝트의 기본적은 파일과 폴더를 생성한다.

–type은 생성할 프로젝트의 타입을 지정하는 옵션인데, java-library는 자바 프로젝트임을 나타낸다. 지정한 언어의 샘플 코드를 생성한다. 이 옵션을 생략하면 그레이들 프로젝트의 기본적인 파일들만 생성된다.

그레이들 실행과 태스크

그레이들에서는 다양한 처리를 위해 ‘태스크’를 이용한다. 태스크란, 실행할 처리를 모아놓은 단위로 그레이들에서 처음부터 포함된 것도 있고, 프로그래머가 작성할 수도 있다. 이 태스크를 실행하는 것이 그레이들에서 빌드를 관리하는 기본적인 방법이다. 프로젝트를 컴파일하거나, 실행하는 모든 처리에 태스크를 이용한다.

생성된 gradle-app 폴더 내부의 구조를 살펴보자.

  • .gradle 폴더 : 태스크로 생성된 파일 등을 보존한다.
  • gradle 폴더 : 기본값으로는 그레이들 환경을 모아놓은 wrapper 파일이라고 하는 파일들이 들어 있다.
  • src 폴더 : 소스 코드 관련 파일을 이곳에 작성한다.
  • build.gradle : 그레이들 빌드 파일, 이곳에 프로젝트의 빌드 내용을 기술한다.
  • settings.gradle : 빌드 설정 정보를 기술한 파일. 빌드를 실행하기 전에 읽히기 때문에, 필요한 라이브러리를 읽는 등의 기술을 할 수 있다.

이번에는 생성된 파일의 코드를 확인해보자.

build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 자바 프로그램을 빌드할 경우에는 java 플러그인을 로드한다.
apply plugin: 'java'

// 저장소 정보를 관리하는 프로퍼티이다. 이곳에서 저장소를 설정할 수 있다. (로컬 환경이나 원격 저장소 기술)
// jcenter는 그레이들에서 중앙 저장소로 이용되는 저장소이다.
// 메이븐 중앙 저장소는 mavenCentral 메서드를 이용해서 사용할 수 있다.
repositories {
jcenter()
}

// 의존성에 관한 설정을 관리하는 프로퍼티이다. 필요한 라이브러리등의 정보를 기술한다.
dependencies {
compile 'org.slf4j:slf4j-api:1.7.21'
testCompile 'junit:junit:4.12'
}

settings.gradle

1
2
// 루트 프로젝트의 이름을 설정한다. 루트 프로젝트는 다수의 프로젝트를 관리할 때 기본이 되는 프로젝트를 가리킨다. 여기에서는 '이 빌드 파일로 빌드할 프로젝트의 이름'이라고 생각하면 된다.
rootProject.name = 'gradle-app'

인텔리제이에서 사용하기

인텔리제이는 표준으로 그레이들을 지원한다. 인텔리제이에서 그레이들용 프로젝트를 생성할 경우, gradle init –type java-library 명령어를 사용하는 경우와 결과가 조금 다르다.

build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
group 'com.jongmin'
version '1.0-SNAPSHOT'

apply plugin: 'java'

sourceCompatibility = 1.8

repositories {
mavenCentral()
}

dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
}

시작 부분에 그룹 ID와 버전을 지정하는 문장이 포함되어 있다. gradle init으로 작성한 build.gradle에서는 포함되지 않았는데, 그 이유는 메이븐 저장소를 이용하기 때문이다. 메이븐에는 모든 프로그램에 그룹 ID와 아티팩트 ID가 할당되어 있다.

sourceCompatibility는 자바 소스 코드의 버전을 가리킨다.

태스크 실행

메이븐과 마찬가지로 실행할 내용을 ‘Run…’ -> ‘Edit Configurations’ 메뉴에서 컨피그레이션에 설정하면 인텔리제이의 ‘Run’으로 프로그램의 빌드와 실행, 디버그 등의 기능을 수행할 수 있다.

build.gradle

그래이들은 build.gradle에 기술한 코드를 필요에 따라 실행하여 빌드를 실행한다.

그레이들은 ‘그루비를 사용하는 빌드 도구’이다. 하지만 이는 정확한 설명은 아니다. 그레이들은 ‘그루비 그 자체’는 아니고 **’그루비 기반의 DSL(Domain Specific Language)’**이다.

DSL은 ‘도메인 고유 언어’라고 불리는데, 특정한 용도에 한정된 언어를 말한다. 그 언어 그 자체는 아니고, 특정한 용도에 맞게 해당 언어를 기반으로 각색한 것이다. 그레이들에서 사용되는 언어는 ‘그루비를 기반으로 작성된 Gradle DLS’이다.

그레이들은 기본적으로 태스크를 작성하여 실행한다. 태스크는 실행할 처리를 모아서 명령어로서 실행할 수 있게 한 것이다. 다음과 같은 형식으로 정의한다.

1
2
3
task 이름{
...실행할 처리...
}

이렇게 정의한 태스크는 명령행에서 gradle 이름 형태로 실행할 수 있다.

간단한 예제를 작성해 실행해보자. 다음과 같이 build.gradle을 수정한다.

1
2
3
4
5
6
7
8
9
task hello {
doLast {
println();
println("=================");
println("Welcome to Gradle!");
println("=================");
println();
}
}

수정한 후, 명령행에서 다음과 같이 실행한다.

1
gradle hello

결과는 다음과 같다.

1
2
3
4
5
6
7
8
> Task :hello

=================
Welcome to Gradle!
=================

BUILD SUCCESSFUL in 2s
1 actionable task: 1 executed

그레이들은 기본적으로 빌드 도구이지만, 여기에서는 빌드도 컴파일도 하지 않고, 단지 메시지를 표시했다. 태스크는 작성된 처리를 실행할 뿐이지, 반드시 빌드와 관련된 기능이 포함되어야 하는 것은 아니다.

quiet 모드로 실행

이번에는 quiet 모드로 실행해보자.

1
gradle -q hello

결과는 다음과 같다.

1
2
3
=================
Welcome to Gradle!
=================

이렇게 하면 hello에서 println한 내용만 출력되며, 그 이외의 내용은 표시되지 않는다. -q 옵션은 태스크를 quiet 모드로 실행해 예외 발생 등 중요한 문제 이외의 표시가 제한된다. 태스크의 실행 결과만 알고 싶을 때 편리한 옵션이다.

액션 리스트

태스크는 다양한 ‘액션’을 내부에 가지고 있다. 태스크를 실행하면 준비된 액션이 순서대로 실행된다. 이 액션을 관리하는 것이 ‘액션 리스트’이다. 액션은 어떤 역할을 하는지가 정해져 있다. 필요에 따라 액션에 처리를 추가해 태스크를 조합할 수 있다.

액션 중에서도 doFirstdoLast가 가장 많이 사용된다. 각각 ‘처음에 실행되는 액션’과 ‘마지막에 실행되는 액션’이다. 이들을 이용해 태스크의 처음과 마지막에 처리를 실행할 수 있다.

매개변수 이용

태스크를 실행할 때 어떤 정보를 태스크에 전달하고 싶은 경우도 있다. 이럴 때 매개변수를 이용할 수 있다.

1
2
3
4
5
6
7
8
9
task hello {
doLast {
def total = 0;
for(def i in 1..num.toInteger()) {
total += i;
}
println("total: " + total);
}
}

1에서 num까지 합하는 태스크이다. 여기서 사용되는 num이 매개변수로 전달되는 프로퍼티이다. 이처럼 매개변수에서 전달되는 값을 프로퍼티로 사용할 수 있다. 단, 주의할 점은 값이 String이라는 것이다.

hello 태스크는 다음과 같이 실행한다.

1
gradle hello -q -Pnum=100

이처럼 태스크를 실행하는 동안에는 -P프로퍼티=값 형태로 특정 변수에 값을 전달할 수 있다.

동적 태스크 실행

스크립트를 사용해서 동적으로 태스크를 생성할 수도 있다.

1
2
3
4
5
6
7
8
def arr = ["one", "two", "three"];
arr.each {s ->
task "$s" {
doLast {
println("this is $s task.");
}
}
}

다음과 같이 실행한다.

1
gradle -q one

java 플러그인 사용하기

자바로 개발할 때 필요한 기본적인 기능은 ‘java’ 플러그인에 포함되어 있다. build.gradle에서 로드하여 이용한다.

1
apply plugin: 'java'

다음의 명령어로 빌드한다.

1
gradle java

위 명령어를 실행하면 프로젝트가 컴파일되고 JAR 파일이 생성된다. 컴파일로 생성된 파일들은 build 폴더에 보관된다. 이 안의 libs 폴더 안에 gradle-app.jar 파일이 생성된다.

build 폴더 안에는 classes 폴더도 있는데, 여기에는 컴파일된 클래스 파일이 보관된다. gradle java에서는 우선 소스 코드를 컴파일하여 클래스 파일을 작성한 후, 이를 모아서 JAR 파일로 만드는 일련의 처리가 자동으로 수행된다.

gradle java와 gradle build

grade java는 자바 프로그램의 빌드를 수행하지만, gradle build는 어떤 언어로 작성된 프로젝트라도 빌드한다. (그레이들은 자바 이외에도 그루비나 스칼라 등 많은 언어를 지원하고 각각의 언어에서 빌드를 수행하는 플러그인을 제공한다.)

java 플러그인의 태스크

java 플러그인에는 이 외에도 몇 가지 태스크가 더 있다.

  • java
    자바 소스 코드를 컴파일하고 그 외에 필요한 리소스 파일들을 모아서 JAR 파일을 생성한다. 프로그램을 배포할 때 이 태스크로 JAR 파일을 만들면 유용하다. 단, 이 java 태스크로 생성된 JAR 파일은 Executable이 아니라는 점에 주의해야 한다.
  • compileJava
    자바 소스 코드를 모두 컴파일한다. 보존할 장소(build 안의 classes 폴더)가 없다면 폴더를 자동으로 생성하고 그 안에 클래스 파일을 작성한다.
  • processResources
    리소스 파일을 클래스 디렉터리(classes 폴더) 안에 복사한다.
  • classes
    소스 코드 컴파일과 리소스 파일 복사를 실행한다. compileJava와 processResources가 합쳐진 것이라 생각해도 된다.
  • test
    프로그램 테스트를 실행한다. 소스 코드와 관련된 컴파일을 수행하고 테스트에 필요한 리소스 복사 등을 수행한 뒤 JUnit으로 테스트를 실행한다. JUnit 라이브러리를 이용할 수 없는 상태에서는 테스트를 실행할 때 오류가 발생한다.
  • jar
    프로그램을 컴파일하고 리소스 파일 등을 준비한 뒤, JAR 파일로 패키징한다. 단, 파일을 단순히 JAR 파일에 모을 뿐이며 Executable jar을 작성하는 것은 아니다.
  • javadoc
    소스 코드를 해석하여 Javadoc 파일을 생성한다. build 안의 docs 폴더 안에 javadoc 폴더를 작성하여 파일을 보관한다.
  • clean
    빌드로 생성된 파일을 모두 삭제한다.

java 플러그인의 태스크 이용하기

1
2
3
4
5
task doit(dependsOn: [compileJava, jar]) {
doLast {
println "*** compiled and created jar! ***"
}
}

doit 태스크에는 매개변수가 포함되어 있다. dependsOn은 이 매개변수가 ‘의존성’을 지정하는 것을 가리킨다. ‘compileJava, jar’은 2개의 태스크를 모아놓은 배열이다. 즉, doit이 compileJava와 jar이라는 2개의 태스크에 의존한다.

이 상태에서 dependsOn에서 태스크를 지정하면 그 태스크가 실행되기 전에 의존하는 모든 태스크가 실행된다. 그리고 의존하는 태스크의 실행이 완료된 후에 doit 태스크의 doLast가 호출된다.

의존성을 지정해서 실행하는 방법 이외에 태스크를 직접 실행할 수도 있는데, 태스크의 execute 메서드를 호출하면 된다.

1
2
3
4
5
6
7
8
task doit {
doLast {
println "*** compiled now! ***"
tasks.compileJava.execute()
println "*** create jar! ***"
tasks.jar.execute()
}
}

실행하면 의존성을 지정해서 실행할 때와 같은 결과를 얻지만, “Deprecated Gradle features were used in this build, making it incompatible with Gradle 5.0.” 라는 메시지가 출력된다. 그레이들 버전 5.0 부터는 execute 메서드를 직접 호출하는 것이 호환되지 않기 때문이다.

application 플러그인

java 플러그인에는 프로그램을 실행하는 태스크가 없어 application 플러그인을 사용해 애플리케이션을 실행해야 한다.

1
apply plugin: 'application"

build.gradle에 이처럼 작성하고 그 다음에 mainClassName 프로퍼티에 메인 클래스(실행할 클래스)를 설정한다.

1
mainClassName = "com.jongmin.gradle.App"

application 플러그인에는 run 태스크가 포함되어 있다. 이것을 실행하면 mainClassName에 지정된 클래스가 실행된다.

1
gradle run

다음과 같이 작성하면 jar을 실행한 후 run으로 클래스를 실행할 수 있다.

1
2
3
4
5
6
7
8
9
10
apply plugin: 'java'
apply plugin: 'application'

mainClassName = "com.jongmin.gradle.App"

task doit(dependsOn:[jar, run]) {
doLast {
println("*** do it! ***")
}
}