[maven] Profile로 설정파일 나누기

개요

로컬에서 상용되는 데이터베이스에 붙는 일은 거의 없을 것이다. 로컬에서는 테스트용 데이터베이스에 연결되고, 실서버에서는 리얼 데이터베이스에 연결하면서 소스는 수정되지 않길 원한다. 그래서 설정파일로 빼고, 예전에는 수동으로 분기해주는 코드를 만들었는데, 이제는 메이븐에서 profile을 사용하면 간단하게 해결된다.
profile은 로컬과 리얼의 설정파일 뿐만 아니라 dependency도 분기할 수 있게 해준다.
예제에서는 설정파일만 나눠보자.

예제

먼저 메이븐 프로젝트를 생성하고, src/main 밑에 resource-local과 resource-real 폴더를 생성하자.

다음 pom.xml 을 아래와 같이 수정해준다.



pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.tistory.jekalmin</groupId>
  <artifactId>profile-test</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>war</packaging>
  <profiles>
    <profile>
        <id>local</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
        <properties>
            <environment>local</environment>
        </properties>
    </profile>
    <profile>
        <id>real</id>
        <properties>
            <environment>real</environment>
        </properties>
    </profile>

  </profiles>

  <build>
    <resources>
        <resource>
            <directory>src/main/resource-${environment}</directory>
        </resource>
    </resources>
  </build>
</project>

수정한 후에,

프로젝트 우클릭 -> Maven -> Update Project를 실행하자.
실행하고 나면 resource-local이 소스폴더로 추가될 것이다.

이제 resource-local과 resource-real에 같은 파일명을 사용하더라도, 파일 안에서 다른 설정이 가능하다.
예를 들어 war로 리얼 설정을 담아서 만들고 싶다면,

mvn -Preal clean package

이렇게 사용하면 된다.

ps : 이유는 잘 모르겠으나 clean을 안하면 자꾸 local 설정 파일이 포함됐다. 나중에 원인을 알게되면 수정해야겠다.

[spring security] SecurityContext 가져오기

SecurityContext가 생성되는 시점은 시큐리티가 생성한 필터들 중에 두 번째 쯔음 호출되는 SecurityContextPersistenceFilter 필터 안에서 HttpSessionSecurityContextRepository 클래스를 사용하여 생성한다.
그래서 그 필터를 지난 이후에는 SecurityContextHolder에서 가져올 수 있다.

SecurityContext securityContext = SecurityContextHolder.getContext();

하지만 그 필터보다 더 앞에 호출되는 필터에서는 SecurityContext가 생성되기 전이기 때문에 SecurityContextHolder에서 가져올 수 없다.
그래도 결국 SecurityContext도 세션에 저장되기 때문에, 아래와 같이 세션에서 직접 가져올 수 있다.

Object securityContextObject =  session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
if(securityContextObject != null){
    SecurityContext securityContext = (SecurityContext)securityContextObject;
}

Session에 저장되는 키는 “SPRING_SECURITY_CONTEXT”인데, 이 키는 HttpSessionSecurityContextRepository 클래스에 정의 되있다.

[spring boot] application.properties 가능한 설정들

복사 붙여넣기 하지말고 필요한 부분만 가져다가 써야한다.

# ===================================================================
# COMMON SPRING BOOT PROPERTIES
#
# This sample file is provided as a guideline. Do NOT copy it in its
# entirety to your own application.               ^^^
# ===================================================================

# ----------------------------------------
# CORE PROPERTIES
# ----------------------------------------

# SPRING CONFIG (ConfigFileApplicationListener)
spring.config.name= # config file name (default to 'application')
spring.config.location= # location of config file

# PROFILES
spring.profiles.active= # comma list of active profiles

# APPLICATION SETTINGS (SpringApplication)
spring.main.sources=
spring.main.web-environment= # detect by default
spring.main.show-banner=true
spring.main....= # see class for all properties

# LOGGING
logging.path=/var/logs
logging.file=myapp.log
logging.config= # location of config file (default classpath:logback.xml for logback)
logging.level.*= # levels for loggers, e.g. "logging.level.org.springframework=DEBUG" (TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF)

# IDENTITY (ContextIdApplicationContextInitializer)
spring.application.name=
spring.application.index=

# EMBEDDED SERVER CONFIGURATION (ServerProperties)
server.port=8080
server.address= # bind to a specific NIC
server.session-timeout= # session timeout in seconds
server.context-path= # the context path, defaults to '/'
server.servlet-path= # the servlet path, defaults to '/'
server.ssl.client-auth= # want or need
server.ssl.key-alias=
server.ssl.key-password=
server.ssl.key-store=
server.ssl.key-store-password=
server.ssl.key-store-provider=
server.ssl.key-store-type=
server.ssl.protocol=TLS
server.ssl.trust-store=
server.ssl.trust-store-password=
server.ssl.trust-store-provider=
server.ssl.trust-store-type=
server.tomcat.access-log-pattern= # log pattern of the access log
server.tomcat.access-log-enabled=false # is access logging enabled
server.tomcat.internal-proxies=10\.\d{1,3}\.\d{1,3}\.\d{1,3}|\
        192\.168\.\d{1,3}\.\d{1,3}|\
        169\.254\.\d{1,3}\.\d{1,3}|\
        127\.\d{1,3}\.\d{1,3}\.\d{1,3} # regular expression matching trusted IP addresses
server.tomcat.protocol-header=x-forwarded-proto # front end proxy forward header
server.tomcat.port-header= # front end proxy port header
server.tomcat.remote-ip-header=x-forwarded-for
server.tomcat.basedir=/tmp # base dir (usually not needed, defaults to tmp)
server.tomcat.background-processor-delay=30; # in seconds
server.tomcat.max-threads = 0 # number of threads in protocol handler
server.tomcat.uri-encoding = UTF-8 # character encoding to use for URL decoding

# SPRING MVC (HttpMapperProperties)
http.mappers.json-pretty-print=false # pretty print JSON
http.mappers.json-sort-keys=false # sort keys
spring.mvc.locale= # set fixed locale, e.g. en_UK
spring.mvc.date-format= # set fixed date format, e.g. dd/MM/yyyy
spring.mvc.message-codes-resolver-format= # PREFIX_ERROR_CODE / POSTFIX_ERROR_CODE
spring.view.prefix= # MVC view prefix
spring.view.suffix= # ... and suffix
spring.resources.cache-period= # cache timeouts in headers sent to browser
spring.resources.add-mappings=true # if default mappings should be added

# THYMELEAF (ThymeleafAutoConfiguration)
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.content-type=text/html # ;charset=<encoding> is added
spring.thymeleaf.cache=true # set to false for hot refresh

# FREEMARKER (FreeMarkerAutoConfiguration)
spring.freemarker.allowRequestOverride=false
spring.freemarker.cache=true
spring.freemarker.checkTemplateLocation=true
spring.freemarker.charSet=UTF-8
spring.freemarker.contentType=text/html
spring.freemarker.exposeRequestAttributes=false
spring.freemarker.exposeSessionAttributes=false
spring.freemarker.exposeSpringMacroHelpers=false
spring.freemarker.prefix=
spring.freemarker.requestContextAttribute=
spring.freemarker.settings.*=
spring.freemarker.suffix=.ftl
spring.freemarker.templateLoaderPath=classpath:/templates/
spring.freemarker.viewNames= # whitelist of view names that can be resolved

# GROOVY TEMPLATES (GroovyTemplateAutoConfiguration)
spring.groovy.template.cache=true
spring.groovy.template.charSet=UTF-8
spring.groovy.template.configuration.*= # See Groovy's TemplateConfiguration
spring.groovy.template.contentType=text/html
spring.groovy.template.prefix=classpath:/templates/
spring.groovy.template.suffix=.tpl
spring.groovy.template.viewNames= # whitelist of view names that can be resolved

# VELOCITY TEMPLATES (VelocityAutoConfiguration)
spring.velocity.allowRequestOverride=false
spring.velocity.cache=true
spring.velocity.checkTemplateLocation=true
spring.velocity.charSet=UTF-8
spring.velocity.contentType=text/html
spring.velocity.dateToolAttribute=
spring.velocity.exposeRequestAttributes=false
spring.velocity.exposeSessionAttributes=false
spring.velocity.exposeSpringMacroHelpers=false
spring.velocity.numberToolAttribute=
spring.velocity.prefix=
spring.velocity.properties.*=
spring.velocity.requestContextAttribute=
spring.velocity.resourceLoaderPath=classpath:/templates/
spring.velocity.suffix=.vm
spring.velocity.viewNames= # whitelist of view names that can be resolved

# INTERNATIONALIZATION (MessageSourceAutoConfiguration)
spring.messages.basename=messages
spring.messages.cacheSeconds=-1
spring.messages.encoding=UTF-8


# SECURITY (SecurityProperties)
security.user.name=user # login username
security.user.password= # login password
security.user.role=USER # role assigned to the user
security.require-ssl=false # advanced settings ...
security.enable-csrf=false
security.basic.enabled=true
security.basic.realm=Spring
security.basic.path= # /**
security.headers.xss=false
security.headers.cache=false
security.headers.frame=false
security.headers.contentType=false
security.headers.hsts=all # none / domain / all
security.sessions=stateless # always / never / if_required / stateless
security.ignored=false

# DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
spring.datasource.name= # name of the data source
spring.datasource.initialize=true # populate using data.sql
spring.datasource.schema= # a schema (DDL) script resource reference
spring.datasource.data= # a data (DML) script resource reference
spring.datasource.sqlScriptEncoding= # a charset for reading SQL scripts
spring.datasource.platform= # the platform to use in the schema resource (schema-${platform}.sql)
spring.datasource.continueOnError=false # continue even if can't be initialized
spring.datasource.separator=; # statement separator in SQL initialization scripts
spring.datasource.driverClassName= # JDBC Settings...
spring.datasource.url=
spring.datasource.username=
spring.datasource.password=
spring.datasource.max-active=100 # Advanced configuration...
spring.datasource.max-idle=8
spring.datasource.min-idle=8
spring.datasource.initial-size=10
spring.datasource.validation-query=
spring.datasource.test-on-borrow=false
spring.datasource.test-on-return=false
spring.datasource.test-while-idle=
spring.datasource.time-between-eviction-runs-millis=
spring.datasource.min-evictable-idle-time-millis=
spring.datasource.max-wait=

# MONGODB (MongoProperties)
spring.data.mongodb.host= # the db host
spring.data.mongodb.port=27017 # the connection port (defaults to 27107)
spring.data.mongodb.uri=mongodb://localhost/test # connection URL
spring.data.mongo.repositories.enabled=true # if spring data repository support is enabled

# JPA (JpaBaseConfiguration, HibernateJpaAutoConfiguration)
spring.jpa.properties.*= # properties to set on the JPA connection
spring.jpa.openInView=true
spring.jpa.show-sql=true
spring.jpa.database-platform=
spring.jpa.database=
spring.jpa.generate-ddl=false # ignored by Hibernate, might be useful for other vendors
spring.jpa.hibernate.naming-strategy= # naming classname
spring.jpa.hibernate.ddl-auto= # defaults to create-drop for embedded dbs
spring.data.jpa.repositories.enabled=true # if spring data repository support is enabled

# SOLR (SolrProperties})
spring.data.solr.host=http://127.0.0.1:8983/solr
spring.data.solr.zkHost=
spring.data.solr.repositories.enabled=true # if spring data repository support is enabled

# ELASTICSEARCH (ElasticsearchProperties})
spring.data.elasticsearch.cluster-name= # The cluster name (defaults to elasticsearch)
spring.data.elasticsearch.cluster-nodes= # The address(es) of the server node (comma-separated; if not specified starts a client node)
spring.data.elasticsearch.repositories.enabled=true # if spring data repository support is enabled



# FLYWAY (FlywayProperties)
flyway.locations=classpath:db/migrations # locations of migrations scripts
flyway.schemas= # schemas to update
flyway.initVersion= 1 # version to start migration
flyway.sql-migration-prefix=V
flyway.sql-migration-suffix=.sql
flyway.enabled=true
flyway.url= # JDBC url if you want Flyway to create its own DataSource
flyway.user= # JDBC username if you want Flyway to create its own DataSource
flyway.password= # JDBC password if you want Flyway to create its own DataSource

# LIQUIBASE (LiquibaseProperties)
liquibase.change-log=classpath:/db/changelog/db.changelog-master.yaml
liquibase.contexts= # runtime contexts to use
liquibase.default-schema= # default database schema to use
liquibase.drop-first=false
liquibase.enabled=true
liquibase.url= # specific JDBC url (if not set the default datasource is used)
liquibase.user= # user name for liquibase.url
liquibase.password= # password for liquibase.url

# JMX
spring.jmx.enabled=true # Expose MBeans from Spring

# RABBIT (RabbitProperties)
spring.rabbitmq.host= # connection host
spring.rabbitmq.port= # connection port
spring.rabbitmq.addresses= # connection addresses (e.g. myhost:9999,otherhost:1111)
spring.rabbitmq.username= # login user
spring.rabbitmq.password= # login password
spring.rabbitmq.virtualHost=
spring.rabbitmq.dynamic=

# REDIS (RedisProperties)
spring.redis.host=localhost # server host
spring.redis.password= # server password
spring.redis.port=6379 # connection port
spring.redis.pool.max-idle=8 # pool settings ...
spring.redis.pool.min-idle=0
spring.redis.pool.max-active=8
spring.redis.pool.max-wait=-1

# ACTIVEMQ (ActiveMQProperties)
spring.activemq.broker-url=tcp://localhost:61616 # connection URL
spring.activemq.user=
spring.activemq.password=
spring.activemq.in-memory=true # broker kind to create if no broker-url is specified
spring.activemq.pooled=false

# HornetQ (HornetQProperties)
spring.hornetq.mode= # connection mode (native, embedded)
spring.hornetq.host=localhost # hornetQ host (native mode)
spring.hornetq.port=5445 # hornetQ port (native mode)
spring.hornetq.embedded.enabled=true # if the embedded server is enabled (needs hornetq-jms-server.jar)
spring.hornetq.embedded.serverId= # auto-generated id of the embedded server (integer)
spring.hornetq.embedded.persistent=false # message persistence
spring.hornetq.embedded.data-directory= # location of data content (when persistence is enabled)
spring.hornetq.embedded.queues= # comma separate queues to create on startup
spring.hornetq.embedded.topics= # comma separate topics to create on startup
spring.hornetq.embedded.cluster-password= # customer password (randomly generated by default)

# JMS (JmsProperties)
spring.jms.pub-sub-domain= # false for queue (default), true for topic

# SPRING BATCH (BatchDatabaseInitializer)
spring.batch.job.names=job1,job2
spring.batch.job.enabled=true
spring.batch.initializer.enabled=true
spring.batch.schema= # batch schema to load

# AOP
spring.aop.auto=
spring.aop.proxy-target-class=

# FILE ENCODING (FileEncodingApplicationListener)
spring.mandatory-file-encoding=false

# SPRING SOCIAL (SocialWebAutoConfiguration)
spring.social.auto-connection-views=true # Set to true for default connection views or false if you provide your own

# SPRING SOCIAL FACEBOOK (FacebookAutoConfiguration)
spring.social.facebook.app-id= # your application's Facebook App ID
spring.social.facebook.app-secret= # your application's Facebook App Secret

# SPRING SOCIAL LINKEDIN (LinkedInAutoConfiguration)
spring.social.linkedin.app-id= # your application's LinkedIn App ID
spring.social.linkedin.app-secret= # your application's LinkedIn App Secret

# SPRING SOCIAL TWITTER (TwitterAutoConfiguration)
spring.social.twitter.app-id= # your application's Twitter App ID
spring.social.twitter.app-secret= # your application's Twitter App Secret

# SPRING MOBILE SITE PREFERENCE (SitePreferenceAutoConfiguration)
spring.mobile.sitepreference.enabled=true # enabled by default

# SPRING MOBILE DEVICE VIEWS (DeviceDelegatingViewResolverAutoConfiguration)
spring.mobile.devicedelegatingviewresolver.enabled=true # disabled by default
spring.mobile.devicedelegatingviewresolver.normalPrefix=
spring.mobile.devicedelegatingviewresolver.normalSuffix=
spring.mobile.devicedelegatingviewresolver.mobilePrefix=mobile/
spring.mobile.devicedelegatingviewresolver.mobileSuffix=
spring.mobile.devicedelegatingviewresolver.tabletPrefix=tablet/
spring.mobile.devicedelegatingviewresolver.tabletSuffix=

# ----------------------------------------
# ACTUATOR PROPERTIES
# ----------------------------------------

# MANAGEMENT HTTP SERVER (ManagementServerProperties)
management.port= # defaults to 'server.port'
management.address= # bind to a specific NIC
management.contextPath= # default to '/'
management.add-application-context-header= # default to true

# ENDPOINTS (AbstractEndpoint subclasses)
endpoints.autoconfig.id=autoconfig
endpoints.autoconfig.sensitive=true
endpoints.autoconfig.enabled=true
endpoints.beans.id=beans
endpoints.beans.sensitive=true
endpoints.beans.enabled=true
endpoints.configprops.id=configprops
endpoints.configprops.sensitive=true
endpoints.configprops.enabled=true
endpoints.configprops.keys-to-sanitize=password,secret
endpoints.dump.id=dump
endpoints.dump.sensitive=true
endpoints.dump.enabled=true
endpoints.env.id=env
endpoints.env.sensitive=true
endpoints.env.enabled=true
endpoints.health.id=health
endpoints.health.sensitive=false
endpoints.health.enabled=true
endpoints.info.id=info
endpoints.info.sensitive=false
endpoints.info.enabled=true
endpoints.metrics.id=metrics
endpoints.metrics.sensitive=true
endpoints.metrics.enabled=true
endpoints.shutdown.id=shutdown
endpoints.shutdown.sensitive=true
endpoints.shutdown.enabled=false
endpoints.trace.id=trace
endpoints.trace.sensitive=true
endpoints.trace.enabled=true

# MVC ONLY ENDPOINTS
endpoints.jolokia.path=jolokia
endpoints.jolokia.sensitive=true
endpoints.jolokia.enabled=true # when using Jolokia

# JMX ENDPOINT (EndpointMBeanExportProperties)
endpoints.jmx.enabled=true
endpoints.jmx.domain= # the JMX domain, defaults to 'org.springboot'
endpoints.jmx.unique-names=false
endpoints.jmx.staticNames=

# JOLOKIA (JolokiaProperties)
jolokia.config.*= # See Jolokia manual

# REMOTE SHELL
shell.auth=simple # jaas, key, simple, spring
shell.command-refresh-interval=-1
shell.command-path-patterns= # classpath*:/commands/**, classpath*:/crash/commands/**
shell.config-path-patterns= # classpath*:/crash/*
shell.disabled-plugins=false # don't expose plugins
shell.ssh.enabled= # ssh settings ...
shell.ssh.keyPath=
shell.ssh.port=
shell.telnet.enabled= # telnet settings ...
shell.telnet.port=
shell.auth.jaas.domain= # authentication settings ...
shell.auth.key.path=
shell.auth.simple.user.name=
shell.auth.simple.user.password=
shell.auth.spring.roles=

# GIT INFO
spring.git.properties= # resource ref to generated git info properties file

원본 : http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html

[spring boot + security oauth] 세팅 + 테스트

개요

먼저 OAuth의 개념에 관한 자료는 http://helloworld.naver.com/helloworld/textyle/24942http://earlybird.kr/1584 를 참고하길 바란다. 이 글에서는 개념보다는 Provider쪽 세팅 예제와 테스트를 해보겠다. 테스트는 client_credentials, password, authorization_code 인증 방식을 사용하겠다.

참고 : 예제에서 curl 을 사용하기 때문에 윈도우용 curl을 설치 하시거나 cygwin 혹은 git bash 에서 curl을 사용하셔도 된다.

예제

먼저 STS 플러그인에 있는 Spring Starter Project로 프로젝트를 생성하자.

Web과 Security를 체크하고 생성한 다음, spring-security-oauth2 라이브러리를 추가한다. 그러면 pom.xml은 다음과 같다.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.tistory.jekalmin</groupId>
    <artifactId>boot_oauth</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>boot_oauth</name>
    <description></description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.8.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.0.3.RELEASE</version>
        </dependency>
    </dependencies>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>com.tistory.jekalmin.Application</start-class>
        <java.version>1.8</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

다음 Application.java를 아래와 같이 수정한다.


Application.java

package com.tistory.jekalmin;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Configuration
@ComponentScan
@EnableAutoConfiguration
@RestController
public class Application {

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

    @RequestMapping("/")
    public String home() {
        return "Hello World";
    }

    @Configuration
    @EnableResourceServer
    protected static class ResourceServer extends ResourceServerConfigurerAdapter {

        @Override
        public void configure(HttpSecurity http) throws Exception {
            // @formatter:off
            http
                // Just for laughs, apply OAuth protection to only 2 resources
                .requestMatchers().antMatchers("/","/admin/beans").and()
                .authorizeRequests()
                .anyRequest().access("#oauth2.hasScope('read')");
            // @formatter:on
        }

        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            resources.resourceId("sparklr");
        }

    }

    @Configuration
    @EnableAuthorizationServer
    protected static class OAuth2Config extends AuthorizationServerConfigurerAdapter {

        @Autowired
        private AuthenticationManager authenticationManager;

        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.authenticationManager(authenticationManager);
        }

        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            // @formatter:off
            clients.inMemory()
                .withClient("my-trusted-client")
                    .authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
                    .authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
                    .scopes("read", "write", "trust")
                    .resourceIds("sparklr")
                    .accessTokenValiditySeconds(60)
            .and()
                .withClient("my-client-with-registered-redirect")
                    .authorizedGrantTypes("authorization_code")
                    .authorities("ROLE_CLIENT")
                    .scopes("read", "trust")
                    .resourceIds("sparklr")
                    .redirectUris("http://localhost:8080")
            .and()
                .withClient("my-client-with-secret")
                    .authorizedGrantTypes("client_credentials", "password")
                    .authorities("ROLE_CLIENT")
                    .scopes("read")
                    .resourceIds("sparklr")
                    .secret("secret");
        // @formatter:on
        }

    }

    @Configuration
    @EnableWebMvcSecurity
    protected static class SecurityConfig extends WebSecurityConfigurerAdapter{

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication().withUser("min").password("min").roles("USER");
        }

        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean()
                throws Exception {
            return super.authenticationManagerBean();
        }

    }

}

이러면 서버쪽 세팅은 끝이다. 이제 AccessToken을 받아오자.



인증 방법

  • client_credentials

    my-client-with-secret 라는 클라이언트에 client_credentials 인증방법으로 인증해보자.

      curl -u my-client-with-secret:secret http://localhost:8080/oauth/token -d "grant_type=client_credentials"
    

    결과는 다음과 같았다.

      {"access_token":"4444455c-a8a5-4988-ae61-c4b15080e433","token_type":"bearer","expires_in":43200,"scope":"read"}
    



  • password

    이번엔 my-trusted-client 클라이언트에 password 방법으로 인증해보자.

      curl -u my-trusted-client: http://localhost:8080/oauth/token -d "grant_type=password&username=min&password=min"
    

    결과는 다음과 같다.

      {"access_token":"c09bfac0-274b-4dc6-a15c-ec46d2680d44","token_type":"bearer","refresh_token":"9b5c996c-eede-4111-b3d1-aaff28e5c063","expires_in":49,"scope":"read write trust"}
    



  • authorization_code

    my-client-with-registered-redirect 클라이언트에 authorization_code 방법으로 인증하자.
    이전과는 다르게 AccessToken을 발급 받기 위해서는 권한 승인해서 코드를 가져오는 과정이 필요하다. http://localhost:8080/oauth/authorize?client_id=my-client-with-registered-redirect&response_type=code 로 가서 먼저 등록된 min / min 계정으로 로그인 하면 다음과 같은 창이 뜬다.

    둘 다 허용하고 승인하면 아래 화면으로 이동한다.

    보시는 바와 같이 http://localhost:8080/?code=h6cdAM 로 리다이렉트 된다. 이제 code를 이용해서 AccessToken을 발급 받아보자.

      curl -u my-client-with-registered-redirect: http://localhost:8080/oauth/token -d "grant_type=authorization_code&code=h6cdAM"
    

    결과는 다음과 같았다.

      {"access_token":"1ce32b00-05fa-40f2-a1f5-f6ed2719df4e","token_type":"bearer","expires_in":43199,"scope":"trust read"}
    

어떤 방법이든 인증에 성공했으면, AccessToken을 받급 받았을 것이다. 토큰을 가지고 테스트 하기에 앞서 Application.java에서 설정했던 부분을 다시 살펴보자.

@RequestMapping("/")
public String home() {
    return "Hello World";
}

접근에 성공하면 Hello World가 출력되야 한다.
이제 http://localhost:8080 에 접근해보자. 먼저 토큰을 사용하지 않고 접근해서 보호된 자원임을 확인하자.

C:\Users\Min>curl http://localhost:8080
{"error":"unauthorized","error_description":"An Authentication object was not found in the SecurityContext"}

접근이 안된다. 이제 위에서 받았던 AccessToken을 사용해서 접근해보자.

C:\Users\Min>curl -H "authorization: bearer 1ce32b00-05fa-40f2-a1f5-f6ed2719df4e" http://localhost:8080
Hello World

AccessToken을 가지고 요청할 때에만 접근이 되는 것을 확인할 수 있다.

결론

인증 방법에 따라 파라미터로 넘겨야 하는 필수 항목들이 있는데, 최소한의 파라미터로 테스트를 했다. 필수 파라미터들은 http://tutorials.jenkov.com/oauth2/authorization-code-request-response.html 에서 확인할 수 있다.

spring boot를 사용해서 spring security oauth 설정해서 간단해 보이지만, xml 설정을 해보면 TokenEndPoint, TokenStore, Filter 등 설정해야 할 부분이 많다. 그렇게 설정을 해도 결과가 좋지 않았다. 나중에 xml 기반 세팅도 완료 되는데로 포스팅 하겠다.

ps : 예제는 https://github.com/dsyer/sparklr-boot 소스에서 security 세팅을 추가했다.

참고

[spring boot] 로그 설정

간단하지만 매번 까먹어서 새로운 프로젝트 생성할 때마다 찾게 되는 스프링 부트의 로그 설정 방법이다. 물론 resources 밑에 logback.xml에 설정할 수도 있겠지만, 이 글에서는 application.properties에 설정하는 예제이다.

logging.level.org.hibernate.SQL: DEBUG
logging.level.org.springframework: DEBUG

예제와 같이 logging.level 다음에 패키지명으로 설정하면 된다.

[hibernate] 단방향/양방향 연관성 구현 (OneToMany, ManyToOne)

개요

Object Relational Mapping(ORM)을 사용하기 위해 도메인 주도 설계(Domain Driven Design)를 공부하던 중에 단방향/양방향 연관성에 대해 알게 되었고, 그것을 구현하려면 OneToOne, OneToMany, ManyToOne, ManyToMany를 사용해야 한다.
그러나 막상 모델을 저장하려 했더니 원하는 테이블과 컬럼에 저장하는 것이 쉬운일이 아니였다. 예제를 보면서 확인해보자.

예제

먼저 예제는 부서인 Department 안에 속하는 사람들을 Member 로 표현하겠다.


테이블 구조


 tbl_dept

 dept_no


 tbl_member

 member_no

dept_no 


목표 : 객체 간의 양방향/단방향 레퍼런스를 위 테이블 구조로 저장


예제1 : 양방향 (Department <-> Member)


Department.java

package com.tistory.jekalmin.domain;

import java.util.Collection;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;

@Entity
@Table(name="tbl_dept")
public class Department {

    @Id
    @GeneratedValue
    private int deptNo;

    @OneToMany(mappedBy="dept")
    private Collection<Member> members;


}

mappedBy 이후에 들어오는 문자열은 Member.java에서 Department를 가리키는 변수명과 일치해야한다.

Member.java

package com.tistory.jekalmin.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Entity
@Table(name="tbl_member")
public class Member {

    @Id
    @GeneratedValue
    private int memberNo;

    @ManyToOne
    @JoinColumn(name="dept_no")
    private Department dept;

}

JoinColumn은 명시하지 않아도 알아서 ID를 찾지만, 그럴 경우 컬럼 명이 원하는 대로 생성되지 않기 때문에 JoinColumn을 이용해 명시해주었다.



예제2 : 단방향 (Department <- Member)


이번 예제에서는 Member에서만 Department를 참조하는 단방향 예제이다.

Department.java

package com.tistory.jekalmin.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name="tbl_dept")
public class Department {

    @Id
    @GeneratedValue
    private int deptNo;

}

Member.java

package com.tistory.jekalmin.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Entity
@Table(name="tbl_member")
public class Member {

    @Id
    @GeneratedValue
    private int memberNo;

    @ManyToOne
    @JoinColumn(name="dept_no")
    private Department dept;

}

양방향과 다른 점이라면 Department에서는 Member를 더이상 가지고 있지 않다.



예제3 : 단방향 (Department -> Member)


이번에는 Department에서만 Member를 참조하는 단방향 예제이다.

Department.java

package com.tistory.jekalmin.domain;

import java.util.Collection;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.Table;

@Entity
@Table(name="tbl_dept")
public class Department {

    @Id
    @GeneratedValue
    private int deptNo;

    @OneToMany
    @JoinColumn(name="dept_no")
    private Collection<Member> members;

}

Member.java

package com.tistory.jekalmin.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Entity
@Table(name="tbl_member")
public class Member {

    @Id
    @GeneratedValue
    private int memberNo;

}

이전 예제들과 약간 다른 점이라면, 이전에는 JoinColumn을 썼던 이유가 원하는 컬럼명을 명시하기 위해서 였는데, 이번 예제에서는 JoinColumn을 사용하지 않으면 전혀 다른 테이블 구조가 생성이 된다. 그리고 Department.java의 JoinColumn 안에 들어가는 컬럼명은 tbl_member 에서 tbl_dept를 참조할 컬럼명이다.

JoinColumn을 제외하고 실행하면 아래와 같은 테이블이 생성된다.

create table tbl_dept (dept_no integer generated by default as identity (start with 1), primary key (dept_no));

create table tbl_dept_members (tbl_dept_deptNo integer not null, members_memberNo integer not null);

create table tbl_member (member_no integer generated by default as identity (start with 1), primary key (member_no));

tbl_dept_members라는 원하지 않았던 테이블이 생기고, tbl_member는 dept_no를 가지고 있지 않은 구조로 된다.

그래서 JoinColumn을 이전 두 예제에서는 생략해도 컬럼명만 바뀌지만, 이 경우엔 테이블 구조가 예상치 못한 결과가 나왔다.



결론

OneToMany, ManyToOne 어노테이션은 위에 달아만 주면 알아서 구조를 만들어 주기는 하지만, 그 구조가 내가 생각했던 테이블과 컬럼 구조 인지는 잘 확인해 보시길 바란다. 위 예제는 전부 같은 테이블 구조를 생성한다.


spring boot + spring-data-jpa 설정

개요

이전 글인 (http://jekalmin.tistory.com/entry/springdatajpa-%EA%B8%B0%EB%B3%B8-%EC%98%88%EC%A0%9C) 에서 봤듯이 spring-data-jpa 를 설정하려면 xml에 상당히 많은 설정이 필요한 것을 볼 수 있다. 이런 많은 설정들이 spring boot를 사용하면서 기본으로 많이 제공해준다. 예제를 보면서 얼마나 간편해졌는지 확인해보자.

예제

먼저 eclipse를 쓰고 있다면 STS(Spring Tool Suite) 플러그인을 설치하자. 설치하고 나면 New > Project > Spring Starter Project 로 프로젝트를 생성하자.

생성할 때 스타일을 사용할 것인지 선택할 수 있다. 예제에서는 JPA와 Web만 선택하겠다.

먼저 hsqldb 라이브러리를 추가하자.

<dependency>
    <groupId>org.hsqldb</groupId>
    <artifactId>hsqldb</artifactId>
</dependency>

자, 이제 설정 준비가 끝났다. 클래스를 작성해보자.


Member.java

package com.tistory.jekalmin.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Member {

    @Id
    @GeneratedValue
    private int memberSeq;
    private String name;
    private int age;

    /**
     * 다른 생성자를 만들었다면 기본 생성자를 따로 만들어 주는 것을 잊지말자.
     */
    public Member(){}

    public Member(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }

    public int getMemberSeq() {
        return memberSeq;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Member [memberSeq=" + memberSeq + ", name=" + name + ", age="
                + age + "]";
    }

}


MemberRepository.java

package com.tistory.jekalmin.repository;

import org.springframework.data.repository.CrudRepository;

import com.tistory.jekalmin.domain.Member;

public interface MemberRepository extends CrudRepository<Member, Integer>{

}


MemberController.java

package com.tistory.jekalmin.controller;

import javax.annotation.Resource;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import com.tistory.jekalmin.domain.Member;
import com.tistory.jekalmin.repository.MemberRepository;

@Controller
public class MemberController {

    @Resource
    private MemberRepository repository;

    @RequestMapping(value="/member/join")
    public void join(Member member){
        System.out.println( repository.save(member) );
    }

}


save 하고나서 저장된 객체를 리턴해주기 때문에 바로 print 찍어보았다.

http://localhost:8080/member/join?name=min&age=26 요청을 해본 결과는 다음과 같았다.

Member [memberSeq=1, name=min, age=26]

결론

이전의 어마어마한 세팅들이 다 어디갔나 싶을 정도로 boot에서는 기본 설정이 많이 내장되어 있는 것 같다. boot를 이용해서 만들면 설정은 hsqldb 라이브러리 추가하는 것이 전부였고, entity와 repository를 만들어서 바로 사용하니 된다.

설정을 변경할 필요가 있다면 지원하는 어노테이션을 검색해 봐야 겠지만, 기본 설정하나는 정말 쉬워진 것 같다.

[Java] Http 요청 보내기 (HttpClient)

개요

자바스크립트에서는 태그에 넣으면 host, pathname, protocol, port, search 등을 지원해서 url 관리하기가 편리하다.

하지만 자바에서는 Url을 핸들링하기가 쉽지않다. 그래서 아파치에서 제공하는 유틸을 소개하려 한다.

예제

  • Uri 생성하기

    먼저 http 요청을 하게 해주는 라이브러리를 추가해야 한다.

      <dependency>
          <groupId>org.apache.httpcomponents</groupId>
          <artifactId>httpclient</artifactId>
          <version>4.3.5</version>
      </dependency>
    

    그리고 아래와 같이 코드를 작성해준다.

    HttpTest.java

      package com.tistory.jekalmin;
    
      import java.io.IOException;
      import java.net.URI;
      import java.net.URISyntaxException;
    
      import org.apache.http.client.ClientProtocolException;
      import org.apache.http.client.utils.URIBuilder;
    
      public class HttpTest {
    
          public static void main(String[] args) throws URISyntaxException, ClientProtocolException, IOException {
    
              URI uri = new URI("http://jekalmin.tistory.com");
              uri = new URIBuilder(uri).addParameter("aaa", "bbb").addParameter("ccc", "ddd").build();
              System.out.println(uri);
    
          }
    
      }
    

    URIBuilder 클래스를 사용해서 처음부터 host, protocol, port 까지 정해주며 생성할 수도 있고, 예제와 같이 파라미터만 추가도 가능하다. 결과는 다음과 같다.

      http://jekalmin.tistory.com?aaa=bbb&ccc=ddd
    
  • http 요청하기

    이제 생성한 uri를 가지고 http 요청을 해보자. 아래와 같이 코드를 추가해준다.

    HttpTest.java

      package cpackage com.tistory.jekalmin;
    
      import java.io.IOException;
      import java.net.URI;
      import java.net.URISyntaxException;
    
      import org.apache.http.HttpEntity;
      import org.apache.http.HttpResponse;
      import org.apache.http.client.ClientProtocolException;
      import org.apache.http.client.HttpClient;
      import org.apache.http.client.methods.HttpGet;
      import org.apache.http.client.utils.URIBuilder;
      import org.apache.http.impl.client.HttpClientBuilder;
      import org.apache.http.util.EntityUtils;
    
      public class HttpTest {
    
          public static void main(String[] args) throws URISyntaxException, ClientProtocolException, IOException {
    
              URI uri = new URI("http://jekalmin.tistory.com");
              uri = new URIBuilder(uri).addParameter("aaa", "bbb").addParameter("ccc", "ddd").build();
    
              HttpClient httpClient = HttpClientBuilder.create().build();
              HttpResponse response = httpClient.execute(new HttpGet(uri)); // post 요청은 HttpPost()를 사용하면 된다. 
              HttpEntity entity = response.getEntity();
              String content = EntityUtils.toString(entity);
              System.out.println(content);
    
          }
    
      }
    

    4.0부터는 DefaultHttpClient 클래스를 사용했으나 4.3부터는 deprecated 된 상태이다. 대신 HttpClientBuilder를 사용하여 생성하기를 권장하고 있다.
    http 요청후 내용 받아오는 부분은 원래 entity.getContent() 인데 getContent()는 InputStream을 반환한다. 바로 String으로 받기 위해 EntityUtils를 사용했다.

  • Uri에서 파라미터 파싱하기

    host, port, path 등의 정보는 URI 객체에서 getHost() 등과 같은 메소드로 바로 가져올 수 있다. 하지만 파라미터의 경우는 제공하지 않는다.
    그래서 아파치에서 제공하는 유틸을 사용했다. 아래 예제를 보자.

    HttpTest.java

      package com.tistory.jekalmin;
    
      import java.io.IOException;
      import java.net.URI;
      import java.net.URISyntaxException;
      import java.util.List;
    
      import org.apache.http.NameValuePair;
      import org.apache.http.client.ClientProtocolException;
      import org.apache.http.client.utils.URIBuilder;
      import org.apache.http.client.utils.URLEncodedUtils;
    
      public class HttpTest {
    
          public static void main(String[] args) throws URISyntaxException, ClientProtocolException, IOException {
    
              URI uri = new URI("http://jekalmin.tistory.com");
              uri = new URIBuilder(uri).addParameter("aaa", "bbb").addParameter("ccc", "ddd").build();
              System.out.println(uri);
    
              List<NameValuePair> paramList = URLEncodedUtils.parse(uri, "utf-8");
              for(NameValuePair param : paramList){
                  System.out.println(param.getName() + " = " + param.getValue());
              }
          }
    
      }
    

    결과는 아래와 같다.

      http://jekalmin.tistory.com?aaa=bbb&ccc=ddd
      aaa = bbb
      ccc = ddd
    

    아파치에서는 URLEncodedUtils 라는 클래스를 제공한다. 위의 parse 외에도 거꾸로 변환해주는 format 기능도 있다.
    두 메소드는 오버로딩을 통해 다양한 파라미터 타입을 제공하기도 한다.

결론

예전에는 자바에서 http 요청을 보낼 때, string을 결합하여 uri를 직접 생성하거나 그 작업을 도와주는 유틸을 만들곤 했었는데, 이미 다 제공되고 있었다. EntityUtil과 URLEncodedUtils는 4.0 버전부터 지원한다.
코딩하다가 불편하다고 느낄때 무작정 유틸을 만드는 것은 많은 테스트를 안하면 안전하지 않을 뿐만 아니라 많은 시간이 소요된다. 내가 불편함을 느낀다면, 다른 사람들도 똑같이 느꼈을 것이다. 직접 만드는 것은 최후의 보루로 남겨두자.

[spring-data-jpa] 기본 예제

소개

spring-data-jpa는 jpa 기반 저장소를 쉽게 구현하도록 만들어준다. 이중에서도 눈에 띄는 특징들은

  • type-safe한 쿼리가 가능하다.
  • 공통된 저장소 기능들(CRUD)을 제공해준다.
  • @Query 어노테이션을 제공한다.

그 외에도 querydsl의 predicate 제공이나 xml 기반의 entity 매핑등의 특징들도 보인다.

예제

먼저 Spring Project로 프로젝트를 생성하고 dependency를 추가하자.

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>1.7.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.hsqldb</groupId>
    <artifactId>hsqldb</artifactId>
    <version>2.3.2</version>
</dependency>


스프링 프로젝트가 기본 dependency를 등록해주므로 우리는 두개만 더 등록해주자.

다음 Article.java 라는 엔티티를 만들어보자.


Article.java

package com.tistory.jekalmin.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Article {

    @Id        // primary key
    @GeneratedValue        // auto increment
    private int articleNo;
    private int memberNo;
    private String title;
    private String content;
    /* 컬럼명은 필드명이랑 동일하게 된다. 따로 명시해주고 싶으면 @Column을 사용하자 */

    /**
     * 하이버네이트가 파라미터 없는 생성자를 호출하기 때문에, 파라미터 있는 생성자를 생성했을 경우
     * 꼭 파라미터 없는 생성자를 만들어 줘야 한다.
     */
    public Article(){}

    public Article(int memberNo, String title, String content) {
        this.memberNo = memberNo;
        this.title = title;
        this.content = content;
    }

    public int getArticleNo() {
        return articleNo;
    }
    @SuppressWarnings("unused")
    private void setArticleNo(int articleNo) {
        this.articleNo = articleNo;
    }
    public int getMemberNo() {
        return memberNo;
    }
    public void setMemberNo(int memberNo) {
        this.memberNo = memberNo;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }

    @Override
    public String toString() {
        return "Article [articleNo=" + articleNo + ", memberNo=" + memberNo
                + ", title=" + title + ", content=" + content + "]";
    }


}


그리고 Article 엔티티의 저장소를 만든다.


ArticleRepository.java

package com.tistory.jekalmin.repository;

import org.springframework.data.repository.CrudRepository;

import com.tistory.jekalmin.domain.Article;

public interface ArticleRepository extends CrudRepository<Article, Integer> {

}


이제 applicationContext 설정만 하면 된다.


<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xmlns:jpa="http://www.springframework.org/schema/data/jpa"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.0.xsd
        http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">

    <context:component-scan base-package="com.tistory.jekalmin"/>
    <context:annotation-config/>

    <jdbc:embedded-database id="dataSource" type="HSQL"></jdbc:embedded-database>

    <!-- spring data jpa 설정 -->

    <!-- jpa repository가 위치한 패키지 경로 등록 -->
    <jpa:repositories base-package="com.tistory.jekalmin.repository" entity-manager-factory-ref="entityManagerFactory"/>

    <!-- 하이버네이트의 SessionFactory 에 상응하는 jpa의 EntityManagerFactory 등록 -->
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
        </property>
        <property name="dataSource" ref="dataSource"/>
        <property name="jpaProperties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.HSQLDialect</prop> <!-- Hsql 형식에 맞게 변환해주는 클래스 -->
                <prop key="hibernate.connection.pool_size">1</prop>
                <prop key="hibernate.connection.shutdown">true</prop> <!-- hsql에 있는 마지막 연결이 끊어지면 데이터베이스 shutdown 하는 플래그 -->
                <prop key="hibernate.show_sql">true</prop> <!-- SQL 출력 -->
                <prop key="hibernate.hbm2ddl.auto">create</prop> <!-- 테이블 자동 생성 -->
            </props>
        </property>
        <!-- 엔티티 정의된 클래스들이 있는 패키지 등록 -->
        <property name="packagesToScan" value="com.tistory.jekalmin.domain"/>
    </bean>
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"></property>
    </bean>
    <!-- spring data jpa 설정 끝 -->



</beans>


설정 부분이 쉽지 않은데, 약간의 주석들이 도움이 되길 바란다.

이제 테스트할 클래스만 만들고 테스트하면 된다.


ArticleRepositoryTest.java

package com.tistory.jekalmin.test;

import javax.annotation.Resource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.tistory.jekalmin.domain.Article;
import com.tistory.jekalmin.repository.ArticleRepository;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring/application-config.xml")
public class ArticleRepositoryTest {

    @Resource
    ArticleRepository articleRepository;

    @Test
    public void write(){

        articleRepository.save(new Article(11, "제목", "내용"));

        System.out.println(articleRepository.findAll());

    }


}


spring-data-jpa의 가장 강력한 기능중 하나가 Repository의 기본 기능을 제공하는 것 같다. 다만 인터페이스만 선언한 만큼, 나만의 메소드를 추가하는 방법이 조금 까다롭다. 메소드를 추가하는 방법은 다음에 작성하겠다.

[Spring Boot] yaml 설정파일 불러오기(@ConfigurationProperties)

개요

properties를 사용하면 map처럼 key와 value 형식으로 가능하지만, list 형태로 쓰려면 prefix로 구분해야 하는 불편함이 있다. 계층형 구조를 나타내는 설정은 yaml이 매우 편리해 보인다. Spring Boot에서 yaml을 불러올 때 제약사항이 꽤 많아서 정리해본다.

예제1:Map

먼저 src/main/resource 아래에 yml 파일을 하나 생성했다.


servers.yml

servers:
    local:
        ip: 127.0.0.1
        port: 8080
    real:
        ip: xxx.xxx.xxx.xxx
        port: 80

그 다음, yml 파일을 불러올 클래스를 생성한다.


ServerHolder.java

package com.tistory.jekalmin;

import java.util.HashMap;
import java.util.Map;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(locations="classpath:servers.yml")
public class ServerHolder {

    public Map<String, Map<String, String>> servers = new HashMap<String, Map<String, String>>();

    public Map<String, Map<String, String>> getServers() {
        return servers;
    }

}

이제 외부에서 데이터가 잘 들어갔는지 확인한다.


TestController.java

package com.tistory.jekalmin;

import javax.annotation.Resource;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/test")
public class TestController {

    @Resource
    ServerHolder serverHolder;

    @RequestMapping("/index")
    public void index(){
        System.out.println("servers : " + serverHolder.getServers());
    }
}


결과는 다음과 같다.

servers : {real={ip=xxx.xxx.xxx.xxx, port=80}, local={port=8080, ip=127.0.0.1}}



에제2:List

위와 같은 예제를 리스트 형식으로 바꿔보자.


server.yml

servers:
  - name: local
    ip: 127.0.0.1
    port: 8080
  - name: real
    ip: xxx.xxx.xxx.xxx
    port: 80


ServerHolder.java

package com.tistory.jekalmin;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(locations="classpath:servers.yml")
public class ServerHolder {

    public List<Map<String, String>> servers = new ArrayList<Map<String, String>>();

    public List<Map<String, String>> getServers() {
        return servers;
    }

}


TestController.java는 그대로 사용해서 출력했더니 결과는 아래와 같았다.

servers : [{ip=127.0.0.1, port=8080, name=local}, {name=real, ip=xxx.xxx.xxx.xxx, port=80}]

간단한 예제인데도 많은 삽질을 했는데, 그 내용들은 다음과 같았다.

주의사항:
  • yaml 언어는 공백 하나에도 민감하다. 일단 하위 계층으로 내려갈 때 tab이 아닌 스페이스바를 사용하고, 콜론(:)이나 하이픈(-) 이후에 공백 한칸도 필요하다. yaml이 민감한건지 아니면 자바에서 사용한 라이브러리인 snakeyaml이 민감한건지는 잘 모르겠다.
  • 다음은 위에 ServerHolder.java에 해당하는 @ConfigurationProperties를 사용한 클래스이다. 먼저 어노테이션 안에 locations="classpath:servers.yml" 이 되기 전에 locations="servers.yml" 로 테스트를 했었는데, 유닛테스트 할때는 잘 작동하다가 Application.java를 이용해서 접근하려 하면 파일을 찾을 수 없었다.
  • ServerHolder.java 안에 servers가 List이던 Map이던 new를 꼭 시켜줘야 한다.
  • ServerHolder.java 안에 getServer()와 같은 getter가 꼭 있어야 한다.

결론

주의사항에 나열한 내용들과 yml의 민감한 문법 때문에 오늘 하루종일 삽질한 것 같다. 이 글을 읽고 계신 분들은 조금이나마 수월하게 진행 하셨으면 하는 바람이다. 그리고 지금은 Map<Strig, String>으로 받았는데, 클래스로 받을 수도 있다.


참고 : http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-external-config-yaml