[spring] static에서 spring bean 가져오기

먼저 목적은 아래와 같은 코드가 작동하게 하는 것이다.

@Component
public class Foo {
    public void doA(){
        System.out.println("doA");
    }
}

public class Boo {
    @Autowired
    public static Foo foo;
}

public class ApplicationTests {
    @Test
    public void contextLoads() {
        Boo.foo.doA();
    }
}

물론 디자인 상으로는 클래스에 저장되는 static 자원에서 객체를 가리키게 하는 것은 좋지 않지만, 이런 상황에서 그나마 깔끔하게 처리하는 방법은 없을까?

문제점 :

  1. 먼저 @Autowired는 Spring에 의해서 관리되야 주입을 해주는데, Boo는 현재 그렇지 않다.
  2. 아무리 Boo에서는 클래스 레벨에서 Foo를 사용하고 싶다고 하더라도 객체는 클래스 로딩된 후에 생성되기 때문에 Boo 클래스가 로딩될 당시에 Foo 객체는 존재하지 않는다.

해결책 :

  • Boo 도 싱글턴 객체로 스프링에서 관리되게 등록해준다.
  • Boo 객체가 생성될 때 Foo 객체를 가져오고, static 필드에 넘겨준다.

먼저 첫번째 방법은 @PostContruct를 이용해서 static 필드에 값을 넘겨주는 방법이다.

@Component
public class Boo {

    public static Foo foo;

    @Autowired
    private Foo fooInstance;

    @PostConstruct
    private void init() {
        foo = fooInstance;
    }
}

두번째 방법은 생성자에 @Autowired를 사용하는 방법이다.

@Component
public class Boo {

    public static Foo foo;

    @Autowired
    private Boo(Foo foo) {
        this.foo = foo;
    }
}

생성자나 init 같은 함수들은 private으로 하더라도 Spring이 알아서 접근해서 호출해준다. 생성자에 @Autowired는 파라미터로 들어오는 bean을 주입시켜 준다.

참고 : http://stackoverflow.com/questions/17659875/autowired-and-static-method

[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 + 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를 만들어서 바로 사용하니 된다.

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

[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

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를 부모로 가지고 있는다.