최근 팀 커뮤니케이션으로 많은 사람들의 사랑을 받고 있는 슬랙(Slack).

슬랙에서는 외부에서 슬랙채널에 메시지를 보낼 수 있는 WebHook API를 제공하고 있다. 웹훅은 슬랙으로 데이터를 보내는 Incoming WebHook 과 특정조건에 부합되었을 때 외부의 데이터를 가져오는 Outgoing WebHook 이 있다.

웹애플리케이션에서 슬랙채널로 메시지를 보내는 것은 Incoming WebHook을 이용하게 된다.

그러기 위해서는 우선 팀슬랙에 Incomming WebHook을 설정한다.

NOTE

Slack: Incoming WebHook 설정

작업을 진행하기에 앞서서 채널을 하나 개설한다. 그후 통합Integration 으로 이동하여 'incoming webhook' 을 검색하여 설치하고 채널을 지정한다. 필요하다면 아이콘을 변경하는 작업을 한다. 화면에 나오는 웹훅 URL 을 복사해둔다.


스프링부트 프로젝트 생성

스프링부트 프로젝트를 생성한다.


build.gradle
buildscript {
	ext {
		springBootVersion = '1.3.5.RELEASE'
	}
	repositories {
		mavenCentral()
	}
	dependencies {
		classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
	}
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'spring-boot'

jar {
	baseName = 'slack-incoming-webhook'
	version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
	mavenCentral()
}


dependencies {
	compile('org.projectlombok:lombok:1.16.8')
	compile('org.springframework.boot:spring-boot-starter-web')
	compile('com.google.guava:guava:19.0')
	testCompile('org.springframework.boot:spring-boot-starter-test')
}

RestTemplate 빈 선언

WebConfiguration
package io.honeymon.springboot.slack.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class WebConfiguration {

	@Bean
	RestTemplate restTemplate() {
		return new RestTemplate();
	}
}

SlackNotifier 컴포넌트 생성

SlackNotifier
package io.honeymon.springboot.slack.integration;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import com.google.common.collect.Lists;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * Slack Notifier
 *
 * @author honeymon
 *
 */
@Slf4j
@Component
public class SlackNotifier {

	@Autowired
	private RestTemplate restTemplate;

	public enum SlackTarget {
		// TODO webHookUrl 은 자신의 슬랙 IncomingWebHookAPI로 변경하세요.
		CH_INCOMING("https://hooks.slack.com/services/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", "incoming");

		String webHookUrl;
		String channel;

		SlackTarget(String webHookUrl, String channel) {
			this.webHookUrl = webHookUrl;
			this.channel = channel;
		}
	}

	@Data
	@AllArgsConstructor
	@NoArgsConstructor
	@Builder
	public static class SlackMessageAttachement {
		private String color;
		private String pretext;
		private String title;
		private String title_link;
		private String text;
	}

	@Data
	@AllArgsConstructor
	@NoArgsConstructor
	@Builder
	public static class SlackMessage {
		private String text;
		private String channel;
		private List<SlackMessageAttachement> attachments;

		void addAttachment(SlackMessageAttachement attachement) {
			if (this.attachments == null) {
				this.attachments = Lists.newArrayList();
			}
			this.attachments.add(attachement);
		}
	}

	public boolean notify(SlackTarget target, SlackMessageAttachement message) {
		log.debug("Notify[target: {}, message: {}]", target, message);

		SlackMessage slackMessage = SlackMessage.builder().channel(target.channel)
				.attachments(Lists.newArrayList(message)).build();
		try {

			restTemplate.postForEntity(target.webHookUrl, slackMessage, String.class);
			return true;
		} catch (Exception e) {
			log.error("Occur Exception: {}", e);
			return false;
		}

	}
}

SlackController 생성

SlackSenderController
package io.honeymon.springboot.slack.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import io.honeymon.springboot.slack.integration.SlackNotifier;
import io.honeymon.springboot.slack.integration.SlackNotifier.SlackMessageAttachement;
import io.honeymon.springboot.slack.integration.SlackNotifier.SlackTarget;

@RestController
public class SlackSendController {
	@Autowired
	private SlackNotifier slackNotifier;

	@RequestMapping(value = "/", method = RequestMethod.POST)
	public ResponseEntity<Boolean> send(@RequestBody SlackMessageAttachement message) {  (1)
		return ResponseEntity.ok(slackNotifier.notify(SlackTarget.CH_INCOMING, message));
	}
}
  1. POST ` 방식으로 전송을 할 때 `@RequestBody 로 클래스를 정의하면 자동으로 매핑된다.

slack-incoming-webhook 실행

$ git clone https://github.com/ihoneymon/slack-incoming-webhook
$ cd slack-incoming-webhook
$ ./gradlew springboot

포스트맨을 이용한 실행 확인

실제 슬랙 화면


팀채널로 많이 사용하는 슬랙.

배포한 앱에서 중요한 사항(항상 상태를 체크해야하는 상황)에 대해서 슬랙 채널로 공지하도록 하는 기능을 간단하게 구현해봤다. @_@)> 생각보다 쉽다. 많이.

예제에서는 컨트롤러에서 요청을 받아서 처리하는 방식으로 구현했다.


내겐 인텔리제이 개발환경은 너무너무 낯설다. ㅡ0-)

이클립스에서 스프링부트 기반으로 개발하던 환경을 벗어나니 너무너무 낯설기만 하다.

지금 진행하고 있는 프로젝트를 war 로 배포하려고 하면서 전과는 다른 개발방식 때문에 이런저런 새로운 상황들이 벌어져서 나를 당황하도록 만든다.

Table of Contents

1. 문제발생

@Entity 선언한 엔티티 객체를 생성한 후 테스트를 위해 실행하려는 순간 다음과 같은 문제가 발생한다.

2016-05-22 21:02:21.993  INFO 11450 --- [           main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@b62fe6d: startup date [Sun May 22 21:02:21 KST 2016]; root of context hierarchy
2016-05-22 21:02:23.603  INFO 11450 --- [           main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
2016-05-22 21:02:23.620  INFO 11450 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
name: default
...]
2016-05-22 21:02:23.687  INFO 11450 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {5.1.0.Final}
2016-05-22 21:02:23.688  INFO 11450 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2016-05-22 21:02:23.690  INFO 11450 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Bytecode provider name : javassist
2016-05-22 21:02:23.722  INFO 11450 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.0.1.Final}
2016-05-22 21:02:23.860  INFO 11450 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2016-05-22 21:02:24.165  WARN 11450 --- [           main] o.h.c.beanvalidation.TypeSafeActivator   : HHH000274: Unable to apply constraints on DDL for io.honeymon.springboot.proto.entity.Article
 
javax.validation.ValidationException: HV000183: Unable to load 'javax.el.ExpressionFactory'. Check that you have the EL dependencies on the classpath, or use ParameterMessageInterpolator instead
at org.hibernate.validator.internal.engine.ValidatorFactoryImpl.createValidator(ValidatorFactoryImpl.java:339) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.ValidatorFactoryImpl.getValidator(ValidatorFactoryImpl.java:256) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.cfg.beanvalidation.TypeSafeActivator.applyDDL(TypeSafeActivator.java:207) ~[hibernate-core-5.1.0.Final.jar:5.1.0.Final]
at org.hibernate.cfg.beanvalidation.TypeSafeActivator.applyRelationalConstraints(TypeSafeActivator.java:191) ~[hibernate-core-5.1.0.Final.jar:5.1.0.Final]
at org.hibernate.cfg.beanvalidation.TypeSafeActivator.applyRelationalConstraints(TypeSafeActivator.java:150) ~[hibernate-core-5.1.0.Final.jar:5.1.0.Final]
at org.hibernate.cfg.beanvalidation.TypeSafeActivator.activate(TypeSafeActivator.java:98) ~[hibernate-core-5.1.0.Final.jar:5.1.0.Final]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_91]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_91]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_91]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_91]
at org.hibernate.cfg.beanvalidation.BeanValidationIntegrator.integrate(BeanValidationIntegrator.java:132) ~[hibernate-core-5.1.0.Final.jar:5.1.0.Final]
at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:276) ~[hibernate-core-5.1.0.Final.jar:5.1.0.Final]
at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465) [hibernate-core-5.1.0.Final.jar:5.1.0.Final]
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:881) [hibernate-entitymanager-5.1.0.Final.jar:5.1.0.Final]
at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:60) [spring-orm-4.3.0.RC2.jar:4.3.0.RC2]
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:338) [spring-orm-4.3.0.RC2.jar:4.3.0.RC2]
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:373) [spring-orm-4.3.0.RC2.jar:4.3.0.RC2]
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:362) [spring-orm-4.3.0.RC2.jar:4.3.0.RC2]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1637) [spring-beans-4.3.0.RC2.jar:4.3.0.RC2]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1574) [spring-beans-4.3.0.RC2.jar:4.3.0.RC2]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:545) [spring-beans-4.3.0.RC2.jar:4.3.0.RC2]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482) [spring-beans-4.3.0.RC2.jar:4.3.0.RC2]
at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306) [spring-beans-4.3.0.RC2.jar:4.3.0.RC2]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230) [spring-beans-4.3.0.RC2.jar:4.3.0.RC2]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302) [spring-beans-4.3.0.RC2.jar:4.3.0.RC2]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197) [spring-beans-4.3.0.RC2.jar:4.3.0.RC2]
at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1076) [spring-context-4.3.0.RC2.jar:4.3.0.RC2]
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:851) [spring-context-4.3.0.RC2.jar:4.3.0.RC2]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:541) [spring-context-4.3.0.RC2.jar:4.3.0.RC2]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:765) [spring-boot-1.4.0.M3.jar:1.4.0.M3]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:370) [spring-boot-1.4.0.M3.jar:1.4.0.M3]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:314) [spring-boot-1.4.0.M3.jar:1.4.0.M3]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1182) [spring-boot-1.4.0.M3.jar:1.4.0.M3]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1171) [spring-boot-1.4.0.M3.jar:1.4.0.M3]
at io.honeymon.springboot.proto.PrototypeBootApplication.main(PrototypeBootApplication.java:10) [main/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_91]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_91]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_91]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_91]
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144) [idea_rt.jar:na]

이 에러를 뱉으며 애플리케이션이 실행되지 않는다. 이와 관련된 문제를 인터넷을 통해 찾아본 결과, 이 문제의 답은 역시나 스택오버플로우에서 찾았다.

이와 관련해서 스프링부트 프로젝트에서도 이슈로 등록되어 논의가 있었지만…​

인텔리제이의 버그로 정리가 된 듯 싶다. @_@)>

2. 원인

스프링부트를 https://start.spring.io 를 통해서 프로젝트를 생성하면 프로젝트에서 기본내장컨테이너가providedRuntime('org.springframework.boot:spring-boot-starter-tomcat')로 선언이 된다. providedCompile  compile, runtime 에서는 동일한 스코프로 적용이 되지만 war 로 빌드될 때는 제외된다.

providedXXX는 이행성 설정이다. 어떤 라이브러리가 provided로 설정되면 그것이 의존하는 다른 라이브러리도 자동으로 provided가 된다. 강제로 compile 로 지정해도 상관없다.

— http://kwonnam.pe.kr/wiki/gradle/webGradle Web(War) Plugin - 손권남님

providedCompile 로 정의가 된 spring-boot-starter-tomcat 의존성과 관련된 부분들이 war 에서 제외되는 상황이 생긴다.

<dependencies>
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-core</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-el</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-logging-juli</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-websocket</artifactId>
    </dependency>
</dependencies>

위의 내용은 org.springframework.boot : spring-boot-starter-tomcat : 1.3.5.RELEASE 에서 확인가능하다.

providedRuntime('org.springframework.boot:spring-boot-starter-tomcat')으로 인해서 tomcat-embed-el 도 제외가 되어 el 처리를 위한 구현체가 없어서 하이버네이트 validator 에서 예외를 뱉는 것이다. ㅡ_-)

== 해결방법

compile("javax.el:javax.el-api:2.2.5")

을 추가하면 정상적으로 실행이 된다…​. 뭘까? ㅡ_-)?

혹은

providedRuntime('org.springframework.boot:spring-boot-starter-tomcat')

를 제거하면 된다. javax.el-api 혹은 org.apache.tomcat.embed:tomcat-embed-el 를 추가하는 것으러 처리가 될 것이다.

흠냐릿!!


1. PID란?

PID(Process Identifier)는 각 프로세스/스레드를 구분해주는 번호다.

— PID란?

2. 스프링부트에서 PID 생성하도록 하기

스프링부트는 실행되고 있는 애플리케이션의 pid 를 생성하도록 설정하는 것이 쉽다.

2.1. 설정

  • application.yml  spring.pid.file 속성을 정의

spring:
  pid:
    file: springboot-app.pid

spring.pid.file 속성을 정의하지 않으면 application.pid 라는 기본이름으로 생성한다.

  • @SpringBootApplication 이 붙은 Application 클래스 수정

    • 기존

public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
}
  • 변경

public static void main(String[] args) {
    SpringApplication app = new SpringApplication(Application.class);
    app.addListeners(new ApplicationPidFileWriter());   // pid 를 작성하는 역할을 하는 클래스 선언
    app.run(args);
}

2.2. 확인

  1. 간단하게 실행

$ ./gradlew bootrun &

3. PID를 이용한 프로세스 죽이기

  1. 스프링부트 앱 실행

$ ./gradlew bootrun &
  1. 생성된 pid 파일 확인

$ ls
springboot-app.pid  bin  build  build.gradle  gradle  gradlew  gradlew.bat  src
$ cat application.pid
{pid}
  1. 프로세스 죽이기

kill -9 $(cat ./springboot-app.pid)
// 앱이 실행되면서 생성된

4. 정리

스프링부트 1.3.x 버전에 들어서면서 스프링부트리를 유닉스/리눅스 서비스로 등록할 수 있는 기능이 추가되었다. 여기서 사용하기 위해 추가된 기능이라고 보면 될듯 싶다. 애플리케이션이 실행되면 실행되는 프로세스ID를 발급받고 이 프로세스ID를 이용해서 애플리케이션을 죽일 수 있게 된다. 이는 컨테이너나 마이크로서비스 용 애플리케이션에 유용한 기술이라고 생각한다.


[스프링부트] 빌드시 깃 커밋버전 정보 포함시키기

지금 개발하고 있는 애플리케이션은 깃 플로우git flow 를 이용해서 출시하고 있다. 출시할 때는 출시release 기능을 이용해서 master 브랜치에 태그를 생성하고 이를 배포하는 형태로 개발하고 있다.

애플리케이션을 사용하고 있는 필드가 늘어나고 있는데, 이 필드에 배포된 변경이력만으로는 어디까지 개발된 애플리케이션인지 알 수가 없다. 그러다가 스프링부트에서 Git commit information 를 빌드파일이 포함시킬 수 있는 부분을 확인한다. 빌드되는 시점에 커밋정보git.properties 에 저장하여 함께 배포하는 기능이다.

이 기능을 사용하려면,

JDK8 에서 빌드되어야 한다.


gradle-git-properties 사용절차

gradle-git-properties 플러그인 추가

  • 참고: com.gorylenko.gradle-git-properties

    Produce git.properties for spring-boot-actuator

  • build.gradle 수정

    buildscript {
        ext {
          springBootVersion = '1.3.2.RELEASE'
        }
        repositories {
          mavenCentral()
          maven {
                url "https://plugins.gradle.org/m2/"  //gradle 플러그인 URL
              }
        }
        dependencies {
          classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
          classpath "gradle.plugin.com.gorylenko.gradle-git-properties:gradle-git-properties:1.4.11" // gradle-git-properties
        }
    }
     
    apply plugin: "com.gorylenko.gradle-git-properties"


빌드!


빌드 후 결과

  • 빌드된 배포파일 압축을 해제해보면 다음과 같은 구조로 되어 있다. gradle build 생성물 압축해제 그림

    • git.properties
      #
      #Fri Feb 26 10:40:36 KST 2016
      git.commit.id=bad6de66e0ecc9a2f2e2402fdac88f63d88a2305
      git.commit.time=1456448574
      git.commit.user.name=ihoneymon
      git.commit.id.abbrev=bad6de6
      git.branch=release/1.0.1.RELEASE
      git.commit.message.short=\#38 Code cleaning
      git.commit.user.email=ihoneymon@gmail.com
      git.commit.message.full=\#38 Code cleaning\n


액츄에이터 기능 중 info 가 활성화 되어 있다면!!

다양한 버전의 애플리케이션 배포본이 존재할 때 어떤 버전으로 운영되고 있는지 확인하는데 요긴할 듯 하다.


STS의 스프링부트 대시보드: 1부 - 로컬 부트앱

스프링 커뮤니티 유저 여러분.

이 글은 STS에 추가된 스프링부트 대시보드에 관한 소식을 전하기 위한 세 개의 글 중에서 처음으로 작성된 글이다. IDE에서 스프링부트 대시보드를 어떻게 사용하는지를 살펴보고, 그것이 제공하는 다양한 기능들, 스프링부트 기반의 애플리케이션을 개발할 때,을 어떻게 사용할지를 배운다. 스프링부트 대시보드는 얼마전 출시된 STS 3.7.1에서 소개되었으며, 이 연재 블로그를 따라하기 위해서는 해당버전 이상을 필요하다. 만약 설치하지 않았다면,http://spring.io/tools/ 으로 가셔서 내려받아 설치하기 바란다.

1. 소개

현재 스프링부트는 엔터프라이즈 환경을 위한 애플리케이션 구현을 간단하게 해주는 범용적인 기술이 되었다. 마이크로서비스 기반 애플리케이션을 만들어내는데 적합하며, 스프링 애플리케이션 설정을 쉽고 작게 생각하도록 극적인 변화를 이끌었다. 진정으로 클라우드 친화적인 애플리케이션으로 문을 열고 스프링 클라우드와 더불어 아마 많은 마이크로서비스로 구성될 것이다.

이 극적인 이동은 아키텍처에 대한 우리의 생각들이 IDE에도 반영되길 원하게 되었다. 전통적인 자바 통합개발도구IDE 대부분이 클라우드 친화적인 혹은, 클라우드 이전 그리고 분산 프로그래밍 시대 이전에 만들어졌다. 이는 이클립스 IDE가 그러하고 그것을 기반으로 하는 Spring Tool Suite(STS) 또한 그러하다. 이 IDE들은 클라우드 친화적인 시대에 들어섰다. 이는 스프링부트 대시보드의 시작점이 되었다. 스프링부트 대시보드는 수많은 스프링부트 기반의 마이크로서비스 애플리케이션을 관리하고 IDE 내 프로젝트들을 동작시키는 개발자들의 삶을 윤택하게 만들기 위해 만들어졌다.

2. 스프링부트 대시보드

작게 점진적으로 시작하자면, 스프링부트 대시보드는 STS/Eclipse의 추가적인 뷰로 구성됬다. 기본 툴바의 스프링부트 버튼을 누르면 열어볼 수 있다. 간단한 뷰가 열리고 작업공간의 (스프링부로 구성된)프로젝트들이 동기화되며 나타난다.

01-dashboard.png

스프링부트 대시보드의 주요목적은 매일, 매시간, 매번 빈번하게 살펴보는 것들에 대해서 빠르게 접근할 수 있도록 하는데 있다. 대시보드에 있는 부트앱을 선택하고 툴바에서"run" 혹은 "debug"를 눌러 빠르게 시작할 있다. 부트앱을 실행할 수 있는 이보다 빠른 방법이 있을까? 이미 실행중인 앱의 코드가 변경되어 재시작하고 싶다면 그렇게 하면 된다. 코드가 변경되었을 때, 대시보드의 "run" 버튼을 눌러, 앱을 정지 또는 재시작시킬 수 있다. 다시말하지만, 한번 클릭으로 간단하게 할 수 있게 되었다.

02-start-single.png

부트 대시보드의 이런 동작들은 단일 혹은 다중 프로젝트에서도 동일하게 수행된다. 만약 동시에 다수의 부트 앱, 협업을 위해 필요한 서비스의 묶음,을 시작하거나 정지하길 원한다면, 대시보드에서 그것들을 모두 선택하고 "run"을 누르면 된다. 그것만 하면 된다.

03-start-multiple.png

프로젝트들을 스프링부트 1.3 적용하면, 부트 대시보드는 보다 환상적인 것들을 보여줄 것이다. 부트 앱이 구동되고 나면, 부트 대시보드에는 앱이 시동 단계(VM은 실행중이지만, 앱은 아직 초기화중)와 다른 시각적 표현이 나타나며 이는 실행 중인지 완료된 것인지를 명확하게 알려준다. 진행중 아이콘은 앱이 시동주이라는 것을 표시하고, 녹색 "업up" 아이콘은 앱의 초기화 및 실행이 완료되었다는 것을 알려준다. 로그 출력으로 "server started" 메시지가 출력되길 기다리며 살펴보는 등의 행동을 할 필요가 없다. 부트 대시보드 아이콘들을 살펴보자.

04-starting.png

앱이 실행되면, 대시보드에는 사용자의 편의를 위해 동작중인 앱이 사용하는 포트가 표시된다. 앱이 사용하고 있는 포트가 몇번인지 찾기 위해서 로그 출력물을 탐색할 필요가 없다. 대시보드에 그 정보가 자동적으로 표시된다. 그리고 실행중인 부트앱의 콘솔로 빠르게 이동이 가능하며, 그 동작은 매우 신속하고 간단하다.

더이상 실행중인 앱에 바로 브라우저로 접근하기 위해 실행중인 앱의 포트를 알고 있을 필요가 없어졌다. 부트 대시보드에서 프로젝트를 더블클릭하기만 해도 새로운 브라우저가 앱에 기본설정된 URL로 접근할 것이다.

05-browser.png

기본적으로 Eclipse/STS는 내부 인터넷 브라우저 뷰를 사용한다. 만약 외부 인터넷을 사용하려 한다면 설정에서 지정해야 한다.

기본으로 접근해야할 기본 URL이 지정되어있지 않다면, 그것을 사용자설정할 수 있다. 대시보드에서 프로젝트를 선택하고 속성뷰를 열어 URL 경로를 Path에 입력한다. 앱을 더블클릭하면 사용자설정한 URL로 열리는 것을 확인할 수 있을 것이다.

06-path.png

애플리케이션에 정의된 URL 확장이 명확하지 않다면, 속성뷰 창에서 "Request Mapping"을 선택할 수 있다. 실행중인 앱의 모든 리퀘스트 매핑 목록 - 여러분이 정의한 것들은 상단에, 라이브러리들에 의해 생성된 것들은 하단에 위치한다. 리퀘스트 매핑 목록에서 URL 부분을 클릭하면 해당 URL을 브라우저에서 열고, 코드 부분을 클릭하면 해당 프로젝트의 코드 편집기를 통해 해당 코드를 연다.

07-request-mappings.png


역자주

"org.springframework.boot:spring-boot-starter-actuator"를 추가해줘야 /mappingsAPI를 이용해서 앱의 Request Mapping 목록을 조회할 수 있다.

작업공간에서 보다 많은 마이크로서비스를 가지고 있다면, 그것들 모두가 동시에 동작하지 않을 가능성이 있다. 그렇다면 묶음설정을 좋아할 것이다. 예를 들자면 Eclipse/STS에서, 작업 묶음을 만드는 것과 같다. 부트 대시보드는 프로젝트에 임의의 갯수로 태그를 선언하여 태그하는 것을 허용한다(다시 속성 뷰를 보자). 태그는 필터 박스(부트 대시보드 상단에 위치)를 이용해서 부트 대시보드에 보이는 부트 앱을 줄이는데 사용할 수 있다.

08-filter.png


역자주
속성창에서 Tags 항목에 입력이 가능하다. ㅡ_-)

화면으로는 명확하게 안보여.

3. 견해

연재 두번째 글에서는 부트 대시보드의 클라우드 파운더리Cloud Foundry 통합을 소개하면서, 부트 대시보드로부터 CF로 마이크로서비스를 어떻게 배포하는지, 로컬 부트 앱을 사용하여 CF에 배포하는 조합 사용법을 소개하겠다.


+ Recent posts