[Spring Boot] properties 등록하기 (@PropertySource)

개요

Spring Boot로 프로젝트를 실행하면 기본적으로 application.properties를 참조하여 스프링이 실행 되지만, 스프링에 관련 없는 properties는 따로 설정파일을 만들어 빼고 싶은데, xml 설정파일이 없으니 어디에 추가해야 할지 막연했다. 검색을 하다보니 어노테이션에 properties 파일을 등록하면 사용이 가능했다.

예제

먼저 config.properties라는 파일을 resource 폴더에 생성했다.


config.properties

name=jekalmin


그리고 Application.java에 어노테이션을 이용하여 등록해주었다.


Application.java

package com.tistory.jekalmin;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

@Configuration
@ComponentScan
@EnableAutoConfiguration
@PropertySource("config.properties")
public class Application {

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

마지막으로 컨트롤러를 생성하여 잘 가져오는지 테스트하였다.


TestController.java

package com.tistory.jekalmin;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

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

    @Value("${name}")
    String name;

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


결과는 다음과 같았다.

name : jekalmin

config.properties가 무사히 스프링에 등록된 것을 확인할 수 있다.

커스텀 ReturnValueHandler 등록하기

개요

이전에는 컨트롤러에 들어오는 파라미터를 가공하는 방법에 대해 알아보았는데, 이번에는 반대로 리턴타입을 기준으로 분기할 수 있는 ReturnValueHandler를 구현해보려 한다. 기존에는 ModelAndViewResolver를 구현해서 AnnotationMethodHandlerAdapter에 등록을 해야 했는데, 스프링 3.2버전 부터는 deprecated 된 상태다. 지금은 ModelAndViewResolver는 남아 있기는 하나 앞으로 HandlerMethodReturnValueHandler 사용하기를 권하고 있다. 모든 HandlerMethodReturnValueHandler를 검색해봐도 리턴된 클래스를 처리 하는 핸들러를 못 찾을 경우에만 ModelAndViewResolver를 검색하기 시작한다.

예제

이전에 파라미터를 Map 형태로 수집하는 클래스를 만들었는데, 이번에는 그 안에 내용에 따라 view를 분기 시켜보려 한다. 파라미터 수집하는 예제는 (http://jekalmin.tistory.com/entry/%EC%BB%A4%EC%8A%A4%ED%85%80-ArgumentResolver-%EB%93%B1%EB%A1%9D%ED%95%98%EA%B8%B0) 이 글을 참고하길 바란다. 먼저 HandlerMethodReturnValueHandler를 구현한 클래스를 생성하고 dispatcherServlet에 등록해주자.


ParamCollectorReturnValueHandler.java

package com.tistory.jekalmin;

import org.springframework.core.MethodParameter;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;

public class ParamCollectorReturnValueHandler implements HandlerMethodReturnValueHandler {

    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        // TODO Auto-generated method stub
        return ParamCollector.class.isAssignableFrom(returnType.getParameterType());
    }

    @Override
    public void handleReturnValue(Object returnValue,
            MethodParameter returnType, ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest) throws Exception {
        // TODO Auto-generated method stub
        ParamCollector collector = (ParamCollector)returnValue;
        if(collector.get("viewType").equals("json")){
            // json View 등록
            System.out.println("json View 등록하는 곳");
        }
    }

}

다음 dispatcherServlet의 설정이다.


mvc-config.xml

<?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:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
        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">

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


    <mvc:annotation-driven>
        <mvc:argument-resolvers>
            <bean class="com.tistory.jekalmin.ParamCollectorArgumentResolver"></bean>
        </mvc:argument-resolvers>
        <mvc:return-value-handlers>
            <bean class="com.tistory.jekalmin.ParamCollectorReturnValueHandler"></bean>
        </mvc:return-value-handlers>
    </mvc:annotation-driven>

    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
            <!-- Example: a logical view name of 'showMessage' is mapped to '/WEB-INF/jsp/showMessage.jsp' -->
            <property name="prefix" value="/WEB-INF/view/"/>
            <property name="suffix" value=".jsp"/>
    </bean>

</beans>

마지막으로 TestController 이다.


TestController.java

package com.tistory.jekalmin;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

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

    protected final Logger logger = LoggerFactory.getLogger(getClass());

    @RequestMapping("/index")
    public ParamCollector index(ParamCollector collector){
        System.out.println(collector);
        return collector;
    }
}


http://localhost:8080/test/index?viewType=json 일때만 아래 글이 출력되는 것을 확인할 수 있다.

json View 등록하는 곳

결론

HandlerMethodReturnValueHandler를 구현하면 리턴타입이나 리턴된 데이터에 따라서 내가 원하는 view로 보낼 수 있다. 그 View는 흔히 사용하는 JstlView가 될 수도 있지만, AbstractView 등을 구현하여 커스터마이징 해서 사용할 수도 있다.

커스텀 ArgumentResolver 등록하기

개요

스프링 사용시 컨트롤러에 들어오는 파라미터나 리턴타입을 나한테 맞게 가공해서 사용하고 싶을 때가 있다.. 기존에는 WebArgumentResolver를 구현해서 AnnotationMethodHandlerAdapter에 등록을 해야 했는데, 스프링 3.2버전 부터는 deprecated 된 상태다. 지금은 RequestMappingHandlerAdapter 클래스가 담당하는데, 이 클래스에서는 WebArgumentResolver가 HandlerMethodArgumentResolver로 바뀌었다.

예제

request 파라미터 수집을 Map 형태로 하는 클래스를 만들어서 등록해보자. 먼저 Spring Project로 생성한 뒤, 파라미터 수집하는 클래스를 생성하자.



ParamCollector.java

package com.tistory.jekalmin;

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

public class ParamCollector {

    Map<String, String> map = new HashMap<String, String>();

    public String get(String key){
        return map.get(key);
    }

    public void put(String key, String value){
        map.put(key, value);
    }

    public String toString() {
        return map.toString();
    }


}

여기서 주의해야 할 점은 Map을 상속 받거나 구현하면, 다른 ArgumentResolver로 갈 수도 있다. 다음은 ArgumentResolver를 구현해서 DispatcherServlet에 등록해주자.



ParamCollectorArgumentResolver.java

package com.tistory.jekalmin;

import java.util.Iterator;

import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

public class ParamCollectorArgumentResolver implements HandlerMethodArgumentResolver{

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        // TODO Auto-generated method stub
        return ParamCollector.class.isAssignableFrom(parameter.getParameterType());
    }

    @Override
    public Object resolveArgument(MethodParameter parameter,
            ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
            WebDataBinderFactory binderFactory) throws Exception {
        // TODO Auto-generated method stub
        ParamCollector collector = new ParamCollector();
        for(Iterator<String> iterator = webRequest.getParameterNames(); iterator.hasNext();){
            String key = iterator.next();
            collector.put(key, webRequest.getParameter(key));
        }
        return collector;
    }

}


mvc-config.xml (dispatcherServlet 설정파일)

<?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:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
        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">

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


    <mvc:annotation-driven>
        <mvc:argument-resolvers>
            <bean class="com.tistory.jekalmin.ParamCollectorArgumentResolver"></bean>
        </mvc:argument-resolvers>
    </mvc:annotation-driven>

    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
            <!-- Example: a logical view name of 'showMessage' is mapped to '/WEB-INF/jsp/showMessage.jsp' -->
            <property name="prefix" value="/WEB-INF/view/"/>
            <property name="suffix" value=".jsp"/>
    </bean>

</beans>

이제 테스트를 위한 컨트롤러를 생성한다.



TestController.java

package com.tistory.jekalmin;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

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

    @RequestMapping("/index")
    public void index(ParamCollector collector){
        System.out.println(collector);
    }
}



다음 http://localhost:8080/test/index?aaa=bbb&ccc=ddd로 호출을 해결과는 다음과 같다.

{aaa=bbb, ccc=ddd}

결론

ArgumentResolver를 활용하면 파라미터 수집 이 외에도 다양하게 커스터마이징 할 수 있다. 어떻게 활용하느냐는 여러분에게 달려있다.

Gson을 이용한 json을 객체에 담기

JsonObject -> 객체 변환

Gson 라이브러리는 json으로 받은 데이터를 내가 만든 객체에 자동으로 set 해주는 기능을 제공한다. 먼저 간단한 JsonObject 형태의 json을 객체로 변환해보자.

Member.java

package com.tistory.jekalmin;
public class Member {

    private String id;
    private String name;
    private int age;
    private String address;

    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    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;
    }
    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }
    @Override
    public String toString() {
        return "Member [id=" + id + ", name=" + name + ", age=" + age
                + ", address=" + address + "]";
    }



}

GsonTest.java

package com.tistory.jekalmin;

import com.google.gson.Gson;

public class GsonTest {

    public static void main(String[] args) {

        Gson gson = new Gson();
        String jsonString = "{'id':'jekalmin','name':'Min','age':26,'address':'Seoul'}";
        System.out.println(gson.fromJson(jsonString, Member.class));

    }
}

결과는 다음과 같다.

Member [id=jekalmin, name=Min, age=26, address=Seoul]



JsonArray -> List 변환

이번엔 조금더 까다로운 JsonList를 List로 변환해보자. Member.java 클래스는 동일하다.

GsonTest.java

package com.tistory.jekalmin;

import java.util.Arrays;
import java.util.List;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

public class GsonTest {

    public static void main(String[] args) {

        Gson gson = new Gson();
        String jsonString = "[{'id':'jekalmin','name':'Min','age':26,'address':'Seoul'},{'id':'park','name':'park','age':27,'address':'Seoul'},{'id':'kim','name':'kim','age':28,'address':'Incheon'}]";

        // 방법1
        Member[] array = gson.fromJson(jsonString, Member[].class);
        List<Member> list = Arrays.asList(array);

        // 방법2
        List<Member> list2 = gson.fromJson(jsonString, new TypeToken<List<Member>>(){}.getType());

        System.out.println(list);
        System.out.println(list2);

    }
}

두 방법의 결과는 같은 것을 알 수 있다.

[Member [id=jekalmin, name=Min, age=26, address=Seoul], Member [id=park, name=park, age=27, address=Seoul], Member [id=kim, name=kim, age=28, address=Incheon]]
[Member [id=jekalmin, name=Min, age=26, address=Seoul], Member [id=park, name=park, age=27, address=Seoul], Member [id=kim, name=kim, age=28, address=Incheon]]

방법은 두가지 중에 아무거나 사용해도 될 것 같다.

Hibernate + Maven + HSQL 설정

  • 먼저 maven으로 simple project를 만든다.
  • pom.xml에 hsqldb와 하이버네이트 dependency를 추가한다.

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>hibernate-test</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>4.3.6.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
            <version>2.3.2</version>
        </dependency>
    </dependencies>
</project>
  • src/main/resources 밑에 hibernate.cfg.xml을 생성한다.
    • JBoss Tool 이클립스 플러그인안에 Hibernate Tools를 설치했다면 new > Other... > Hibernate Configuration File로 가서 생성할 수 있다.
    • 플러그인 설치를 안하셨다면 그냥 xml 만들어서 붙여넣자.

hibernate.cfg.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<!-- 하이버네이트 설정 파일 -->
<hibernate-configuration>
    <session-factory>
        <property name="hibernate.connection.driver_class">org.hsqldb.jdbcDriver</property>
        <property name="hibernate.connection.url">jdbc:hsqldb:mem:test</property>
        <property name="hibernate.connection.username">sa</property>

        <!-- 하이버네이트 엔티티를 hsql에 맞게 변환해주는 클래스 -->
        <property name="hibernate.dialect">org.hibernate.dialect.HSQLDialect</property>


        <!-- 커넥션 풀 개수 -->
        <property name="hibernate.connection.pool_size">1</property>

        <!-- hsql에 있는 마지막 연결이 끊어지면 데이터베이스 shutdown 하는 플래그 -->
        <property name="hibernate.connection.shutdown">true</property>

        <!-- 등록된 엔티티의 테이블이 없을 경우 자동으로 생성해주는 설정 -->
        <property name="hibernate.hbm2ddl.auto">create</property>

        <!-- db에 요청한 sql 출력 -->
        <property name="hibernate.show_sql">true</property>

        <!-- 엔티티 등록 -->
        <mapping class="com.test.Member"/>
        <!-- 엔티티 등록 끝 -->
    </session-factory>
</hibernate-configuration>
  • 이제 Member 클래스를 만든다. 예제에서는 com.test 패키지 밑에 만들었다.

Member.java

package com.test;

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

@Entity
@Table
public class Member {
    @Id
    @GeneratedValue
    private int seq;
    private String name;
    private int age;

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

    public int getSeq() {
        return seq;
    }

    @SuppressWarnings("unused")
    private void setSeq(int seq) {
        this.seq = seq;
    }

    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 [seq=" + seq + ", name=" + name + ", age=" + age + "]";
    }

}
  • 실행할 Test 클래스를 com.test 패키지 밑에 만든다.

Test.java

package com.test;

import java.util.List;

import org.hibernate.FlushMode;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.Configuration;

public class Test {

    static SessionFactory factory;
    static Session session;

    /**
     * hibernate.cfg.xml의 설정을 읽어서
     * db와 연결하는 설정
     */
    static {
        Configuration configuration = new Configuration().configure("hibernate.cfg.xml");
        StandardServiceRegistryBuilder builder = new StandardServiceRegistryBuilder().applySettings(configuration.getProperties());
        factory = configuration.buildSessionFactory(builder.build());
        session = factory.openSession();
    }

    public static void main(String[] args) {

        insert();
        detail();
        update();
        delete();

        session.flush(); // delete나 update 한 내용을 커밋한다.

        list();

        session.close();
        factory.close();
    }

    /**
     * 등록
     * Member의 seq는 @GenerateValue 어노테이션에 의해 자동 생성된다.
     */
    private static void insert(){
        Member member1 = new Member("Min", 26);
        Member member2 = new Member("Park", 27);
        session.save(member1);
        session.save(member2);
    }

    /**
     * 상세
     */
    private static void detail(){
        Member member = (Member)session.get(Member.class, 2);
        System.out.println("detail member : " + member);
    }

    /**
     * 리스트
     */
    private static void list(){
        Query query = session.createQuery("from Member"); // from Member에서 Member는 클래스명과 동일 (대소문자구분)
        List members = query.list();
        for(Object member : members){
            System.out.println("list : " + member);
        }
    }

    /**
     * 수정
     * 조회를 먼저 해서 Member를 가져온 후 수정한다.
     */
    private static void update(){
        Member member = (Member) session.get(Member.class, 2);
        member.setAge(60);
        session.save(member);
    }

    /**
     * 삭제
     * 조회를 먼저 해서 Member를 가져온 후 삭제한다.
     */
    private static void delete(){
        Member member = (Member) session.get(Member.class, 1);
        session.delete(member);
    }




}

CRUD 코드를 최소화 하기 위해 하나의 session에서 모든 작업을 했다. session은 default로 FlushMode가 AUTO인데, flush 하는 타이밍은 http://docs.jboss.org/hibernate/orm/4.3/manual/en-US/html/ch11.html#objectstate-flushing 여기서 참고하길 바란다.

ApplicationContext를 써야하는 이유

개요

필자는 스프링을 사용할 때, 웹에 관련된 설정들은 DispatcherServlet에 넣고, 순수 자바 설정들은 ApplicationContext에 넣으라고 배웠다. 그러나 모든 설정을 DispatcherServlet안에 넣어도 잘 작동하는데, 왜 굳이 ApplicationContext를 써야 할까?

예제

스프링 시큐리티 작동 방식을 예로 들어보자. 스프링 시큐리티는 필터를 이용해서 사용자의 인증과 권한 여부를 체크한다. 만약 ApplicationContext를 사용하지 않는다면, 스프링 시큐리티의 설정 파일들을 DispatcherServlet에 정의해야한다. 그렇다면 문제는 무엇일까?

이것은 톰캣 같은 WAS(Web Application Server)가 생성하는 컴포넌트 순서가 중요하다. web.xml에 등록된 컴포넌트가 생성되는 순서는 Listener - Filter - Servlet 순서이다. 심지어 서블릿은 load-on-startup 같은 설정을 해주지 않으면 호출되기 전까지 생성되지도 않는다. 근데 스프링 시큐리티에서 사용할 설정을 서블릿인 DispatcherServlet 안에 넣을 경우, WAS에서 필터를 생성할 때는 아직 DispatcherServlet이 생성되지 않았으므로 시큐리티에 관련된 필터를 생성할 수 없다.

그래서 필터를 생성하기 전에 스프링 관련 설정을 적용하려면 listener에 등록해야 한다.

그렇다면 스프링에서 제공하는 CharacterEncodingFilter는 어떨까? 인코딩 필터는 스프링에서 제공하긴 하지만 encoding 변수만 설정해주면 되기 때문에 web.xml 안에서 설정이 가능하다. 그래서 스프링과는 별도로 작동할 수 있었다.

결론

ApplicationContext를 안썼을 때 문제점이 더 많을 수도 있다. 필자는 이것을 쓰는 것이 싫은 것이 아니다. 과연 어떤 경우에 ApplicationContext가 꼭 필요한가 궁금했었는데, 스프링에서 설정을 기반으로 필터를 등록해야 한다면, ApplicationContext를 Listener로 올리는 것은 선택이 아닌 필수이다.


ApplicationContext vs DispatcherServlet 2 : 컨텍스트 간의 bean 공유?

ApplicationContext와 DispatcherServlet은 부모와 자식 관계로 연결되어 있는데, 왜 component-scan은 양쪽 다 해야할까? 두 컨텍스트는 같은 빈들을 가리키는 걸까? 밑에 예제에서는 ApplicationContextHolder 라는 클래스를 만들어서 두 컨텍스트를 가지고 있다가, 두 컨텍스트가 가지고 있는 빈의 인스턴스가 같은지 비교해보았다.


application-context.xml


<?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"

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">

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

    

    <bean class="com.tistory.jekalmin.common.ApplicationContextHolder"/>

<bean class="com.tistory.jekalmin.dao.TestJdbcTemplate"/>

</beans>

TestJdbcTemplate은 applicationContext에만 등록하고, dispatcherServlet에는 등록하지 않기 위해 어노테이션을 사용하지않고 application-context.xml에서 bean으로 등록하였다.


spring-servlet.xml


<?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:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context"

xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd

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">

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

<context:annotation-config/>

<bean class="com.tistory.jekalmin.common.ApplicationContextHolder"/>

<bean class="com.tistory.jekalmin.dao.TestJdbcTemplate"/>

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">

<property name="prefix" value="/WEB-INF/view/"/>

<property name="suffix" value=".jsp"/>

</bean>

</beans>

application-context.xml과 spring-servlet.xml 양쪽에 component-scan을 하였다.


ApplicationContextHolder.java


package com.tistory.jekalmin.common;


import java.util.ArrayList;

import java.util.List;


import org.springframework.beans.BeansException;

import org.springframework.context.ApplicationContext;

import org.springframework.context.ApplicationContextAware;


public class ApplicationContextHolder implements ApplicationContextAware{

public static List<ApplicationContext> list = new ArrayList<ApplicationContext>();

@Override

public void setApplicationContext(ApplicationContext ctx)

throws BeansException {

list.add(ctx);

}


}

두 컨텍스트에서 ApplicationContextHolder 빈을 각각 생성할 때, ApplicationContextHolder 클래스에서 컨텍스트에 접근할 수 있도록 컨텍스트를 등록하였다.


TestDao.java


package com.tistory.jekalmin.dao;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Component;

@Repository

public class TestDao {

@Autowired

public TestJdbcTemplate jdbcTemplate;

}

TestDao는 어노테이션을 이용해 등록했으므로 양쪽 컨텍스트에서 component-scan을 이용해 등록될 것이다.


TestJdbcTemplate.java


package com.tistory.jekalmin.dao;

public class TestJdbcTemplate {

}

TestJdbcTemplate은 위에 application-context.xml에서만 등록하였다.


TestController.java


package com.tistory.jekalmin.controller;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.ApplicationContext;

import org.springframework.stereotype.Controller;

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

import com.tistory.jekalmin.common.ApplicationContextHolder;

import com.tistory.jekalmin.dao.TestDao;

@Controller

public class TestController {

@Autowired

private TestDao testDao;

@RequestMapping("/")

public void index(){

for(ApplicationContext ctx : ApplicationContextHolder.list){

System.out.format("[%s] testController hashCode : %d\n", ctx.getId(), ctx.getBean("testController").hashCode());

System.out.format("[%s] testDao hashCode : %d\n", ctx.getId(), ctx.getBean("testDao").hashCode());

System.out.format("[%s] testJdbcTemplate hashCode : %d\n", ctx.getId(), testDao.jdbcTemplate.hashCode());

}

}

}

이제 url을 호출하여 테스트 해보자. 테스트 내용은 기존에 ApplicationContext에서 가져온 빈과 DispatcherServlet에서 가져온 빈의 hashCode를 비교해 인스턴스가 같은지 다른지 확인하였다.

url 호출 결과는 아래와 같다.


[org.springframework.web.context.WebApplicationContext:] testController hashCode : 631271355

[org.springframework.web.context.WebApplicationContext:] testDao hashCode : 1666005388

[org.springframework.web.context.WebApplicationContext:] testJdbcTemplate hashCode : 1060031077

[org.springframework.web.context.WebApplicationContext:/dispatcherServlet] testController hashCode : 319851274

[org.springframework.web.context.WebApplicationContext:/dispatcherServlet] testDao hashCode : 896263012

[org.springframework.web.context.WebApplicationContext:/dispatcherServlet] testJdbcTemplate hashCode : 1060031077

결과를 보면 testJdbcTemplate만 hashCode가 같고, testController와 testDao는 다른 것을 확인 할 수 있다.

결론 : ApplicationContext와 DispatcherServlet에서 component-scan을 이용해서 등록된 빈들은 다른 인스턴스이다. 다만 ApplicationContext에만 등록된 빈의 경우 DispatcherServlet에서 검색해보고 없으면 부모인 ApplicationContext에서 가져온다.

ApplicationContext vs DispatcherServlet : 컨텍스트 간의 관계

웹쪽 자원을 관리하는 DispatcherServlet과 그 외의 자원을 관리하는 ApplicationContext는 어떤 관계가 있을까? 예제에서는 DispatcherServlet과 ApplicationContext의 정보를 가져오기 위해 ApplicationContextAware를 구현한 클래스를 양쪽에 등록하고, 두 컨텍스트의 정보를 출력해봤다.

ApplicationContextHolder.java


package com.tistory.jekalmin.common;


import org.springframework.beans.BeansException;

import org.springframework.context.ApplicationContext;

import org.springframework.context.ApplicationContextAware;


public class ApplicationContextHolder implements ApplicationContextAware{

@Override

public void setApplicationContext(ApplicationContext ctx)

throws BeansException {

System.out.println("================================================");

System.out.println("applicationContext : " + ctx.getId());

System.out.println("applicationContext hashCode : " + ctx.hashCode());

ApplicationContext parent = ctx.getParent();

if(parent != null){

System.out.println("parent : " + parent.getId());

System.out.println("parent hashCode : " + parent.hashCode());

}

System.out.println("================================================");

}


}



web.xml


<context-param>

        <param-name>contextConfigLocation</param-name>

        <param-value>

        classpath:spring/application-context.xml

        </param-value>

    </context-param>


    <listener>

        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>

    </listener>

    

    <servlet>

        <servlet-name>dispatcherServlet</servlet-name>

        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

        <init-param>

            <param-name>contextConfigLocation</param-name>

            <param-value>classpath:spring/spring-servlet.xml</param-value>

        </init-param>

        <load-on-startup>1</load-on-startup>

    </servlet>


결과는 다음과 같다.


================================================ applicationContext : org.springframework.web.context.WebApplicationContext: applicationContext hashCode : 966881334 ================================================ ================================================ applicationContext : org.springframework.web.context.WebApplicationContext:/dispatcherServlet applicationContext hashCode : 1233412676 parent : org.springframework.web.context.WebApplicationContext:/SpringTest parent hashCode : 966881334 ================================================


applicationContext 먼저 생성되고, dispatcherServlet은 applicationContext를 부모로 가지고 있는다.

여러개의 Spring xml 설정파일, ApplicationContext는 몇개인가

보통 스프링 프로젝트를 만들 때 xml 기반으로 ApplicationContext를 생성하는 경우가 많다. 그리고 스프링 설정파일들도 ContextLoaderListener 라는 리스너에 여러개 등록할 수 있는데, 과연 이 경우에 컨텍스트가 여러개 생셩되는지, 하나의 컨텍스트가 생성되는 지 궁금했다. 

결론부터 말하자면 리스너에 아무리 많은 xml 파일들을 등록해도 하나의 ApplicationContext만 올라간다. 

아래 예제에서는 bean을 생성해준 컨텍스트 정보를 알 수 있도록 ApplicationContextAware를 구현한 클래스를 양쪽 xml에 등록하고 컨텍스트 정보를 출력해서 비교하였다.


web.xml


<context-param>

    <param-name>contextConfigLocation</param-name>

    <param-value>

    classpath:spring/application-context.xml

    classpath:spring/spring-security.xml

    </param-value>

</context-param>

application-context.xml


<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"

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">

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

    <bean class="com.tistory.jekalmin.common.ApplicationContextHolder"/>

</beans>


spring-security.xml


<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"

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">

    

    <bean class="com.tistory.jekalmin.common.ApplicationContextHolder"/>


</beans>


ApplicationContextHolder.java


package com.tistory.jekalmin.common;


import org.springframework.beans.BeansException;

import org.springframework.context.ApplicationContext;

import org.springframework.context.ApplicationContextAware;


public class ApplicationContextHolder implements ApplicationContextAware{

@Override

public void setApplicationContext(ApplicationContext ctx)

throws BeansException {

System.out.println("applicationContext : " + ctx);

System.out.println("applicationContext hashCode : " + ctx.hashCode());

}


}


결과는 다음과 같다.


applicationContext : Root WebApplicationContext: startup date [Tue Sep 30 10:27:17 EDT 2014]; root of context hierarchy

applicationContext hashCode : 12545140

applicationContext : Root WebApplicationContext: startup date [Tue Sep 30 10:27:17 EDT 2014]; root of context hierarchy

applicationContext hashCode : 12545140





두개의 다른 xml에서 ApplicationContextAware를 구현한 클래스를 등록하고 hashCode를 출력하였는데, 같은 객체임을 알 수 있다.

sublime enter키 수정

이클립스의 자바쪽에서 auto-complete을 이용해 파라미터를 입력한 후 enter키를 누르면 End키를 눌렀을 때처럼 커서가 줄의 마지막으로 이동하는데, 이것에 익숙해져있다보니 sublime에서도 하고싶어서 검색하다가 비슷한 것을 발견했다.


[

  { "keys": ["enter"], "command": "move_to", "args": {"to": "eol", "extend": false}, "context":

    [

        { "key": "following_text", "operator": "regex_contains", "operand": "^[)\\]'\"]", "match_all": true },

        { "key": "preceding_text", "operator": "regex_contains", "operand": "[(['\"]", "match_all": true },

        { "key": "auto_complete_visible", "operator": "equal", "operand": false }

    ]

  }

]

이 코드를 서브라임에서 Preferences > Key Bindings - User 안에 붙여넣으면 된다.

만약 정말 줄바꿈이 하고 싶을 때는 Shift + Enter 를 사용하면 된다.

참고 : https://coderwall.com/p/td7e-w