"Spring MVC 4 익히기" 라는 이름으로 번역한 책이 2016/08/25 에 출간되었습니다. ^^

종이책은 9월 중순 이후에 나올 예정입니다.


쉽게쉽게할 수 있을거라고 생각하고 시작했는데 쉽지 않았습니다. ㅎ.

처음 번역할 때보다는 절반이상 시간을 줄였습니다.


@_@); 이제 스프링부트 책을 한번 써보지 않겠냐는 제의가 주변에서 쏟아지고 있는데... 부담이 되네요.

써야지 하고 뼈대는 잡아놨는데...


다음 책의 이름은 "Boot Spring Boot" 입니다. 뒤집어 읽어도 "Boot Spring Boot!"


과연 완료할 수 있을지!!


스프링부트 오류출력 페이지

스프링부트에서 기본설정된 오류페이지는 'Whitelable' 페이지다.


스프링부트에는 'whitelabel' 페이지에 대한 언급이 그렇게 상세히 되어있지는 않다( Customize the ‘whitelabel’ error page).

ErrorMvcAutoConfiguration 을 보면 SpelView 를 이용해서 생성하는 Whitelabel Error Page 를 볼 수 있을 것이다.

이 페이지를 이뻐보이는 페이지로 대체하는 것은 나중에 다루기로 하고…​

스프링부트 ErrorMvcAutoConfiguration 에서 다룰 수 있는 속성들을 살펴보도록 하자. ErrorMvcAutoConfiguration 를 살펴보면 그 중에 DefaultErrorAttributes 를 살펴볼 수 있다. 문서에서 보면 알 수 있듯이 오류와 관련된 속성 중에 다룰 수 있는 것들에는:

  • timestamp - 오류가 발생한 시간을 추출

  • status - 상태코드

  • error - 에러 발생원인

  • exception - 상위 예외 클래스명

  • message - 예외 메시지

  • errors - BindingResult 예외가 던져주는 ObjectErrors

  • trace - Exception stack trace

  • path - 예외가 발생한 URL 경로(되돌아가거나? 어디서 발생했는지 파악하는 용도)

를 활용하면 스프링부트에서 발생한 예외에 대한 분석이 용이해진다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head lang="en">
    <meta charset="utf-8"/>
    <!-- 중략 -->
</head>
<body class=" page-404-3">
<div class="page-inner">
    <img src="/assets/layout/img/earth.jpg" class="img-responsive" alt=""></div>
<div class="container error-404">
    <h1 th:text="${status}">Status</h1>
    <h2>Houston, we have a problem([[${error}]])</h2>
    <p th:text="${message}"> Error Message</p>
    <p>
        <a href="index.html" th:href="@{/}" class="btn red btn-outline"> Return home </a>
        <br>
    </p>
    <div th:text="${timeStamp}"></div>
    <div th:text="${exception}"></div>
    <div th:utext="${trace}">Trace!</div>
</div>
</body>
</html>

이와 같은 형태로 오류를 탐색하고 분석하는데 용이하도록 구성할 수 있을 것이고 방법은 다양하니 자신에게 맞는 방법을 찾기 바란다.


기본은 스프링 부트 레퍼런스 문서(http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/)를 

기준으로 해서 이런저런 이야기를 풀어가는 식으로 쓰려고 하는데...

올해 중에 마칠 수 있을까??


이걸 쓰려면 놀 시간을 줄여야 하는데...

놀시간 줄이는 건 뭔가 아깝단 말이지. @_@);;


어느버전을 기준으로 할지부터 잡아야겠는데...

Spring Boot 1.4.0.RELEASE  를 기준으로 해야겠다.

  1. Favicon of http://blog.outsider.ne.lr BlogIcon Outsider 2016.08.10 21:56 신고

    그럴때는 업무시간을 줄이면 됩니다!!

최근 팀 커뮤니케이션으로 많은 사람들의 사랑을 받고 있는 슬랙(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 를 추가하는 것으러 처리가 될 것이다.

흠냐릿!!


+ Recent posts