Java/Spring2019. 3. 10. 23:26
반응형

 Spring Security(스프링 시큐리티)를 사용하는 경우 단위테스트를 하는 방법을 알아보겠습니다. 

※이글에서 소개하는 내용은 Spring Security 4.1에서 추가된 내용입니다.


 Spring Security는 SecurityContext를 통해서 현재 실행중인 스레드와 연관된 인증정보를 관리합니다. 따라서 테스트 실행시에 SecurityContextHolder를 통해서 인증정보를 설정하면 로그인된 상태등의 인증관련 테스트가 가능합니다.


    //...
    @Before
    public void setUp() {
        SecurityContextHolder.getContext()
            .setAuthentication(new UsernamePasswordAuthenticationToken("username", "password", Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"))));
    }
    //...

※위와 같이 단위테스트 실행전에 인증정보를 설정하면 로그인된 상태로 테스트가 가능합니다. 하지만 위와 같은 코드는 역시 스프링 스럽지 못하다는 왠지 느낌적인 느낌이 듭니다.



 Spring Security 4.1부터 아래와 같은 테스팅 피처가 지원되기 시작했습니다.


  • @WithAnonymousUser - 익명유저의 인증정보를 설정하기 위한 어노테이션
  • @WithUserDetails - UserDetailsService를 통해서 유저정보를 취득하여 설정하기 위한 어노테이션(4.0부터 지원하고 4.1부터 Spring빈의 이름을 지정할 수 있도록 추가됨)
  • @WithMockUser - 별도의 UserDetailsService와 같은 스텁을 제공하지 않아도 간단하게 인증정보를 설정하기 위한 어노테이션(4.0부터 지원되었으나 4.1부터 작동하는것으로 보임)



그럼 각 어노테이션들의 사용법에 대해서 간단하게 알아보도록 하겠습니다.



 첫번째로 @WithAnonymousUser 의 경우 인증되지 않은 상태를 설정하기 위한 어노테이션입니다. 인증하지 않는 경우를 나타내는 어노테이션이 왜 필요할까하지만 위에서 언급한것 처럼 Spring Security는 현재 실행중인 스레드에 연관지어 인증정보를 관리하므로 한번 인증정보를 설정해서 테스트를 진행한 경우에 인증상태를 초기화 시키는데 필요합니다.

    //...
    @Test
    @WithAnonymousUser
    public void some_test() {
        ...
    }
    //...



 두번째로 @WithUserDetails 의 경우는 Spring Security에서 유저정보를 조회하기 위해서 사용하는 UserDetailsService를 이용하여 인증정보를 설정하는 어노테이션입니다. 


    //...
    @Bean
    @Profile("test")
    public UserDetailsService userDetailsService() {
        return new UserDetailsService() {
            @Override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                return User
                        .withUsername(username)
                        .password("password")
                        .authorities(new SimpleGrantedAuthority("ROLE_USER"))
                        .build();
            }
        };
    }
    //...
 위와같이 테스트용 UserDetailsService빈을 등록하여 사용합니다.
    //...
    @Test
    @WithUserDetails("test_user")
    public void some_test() {
        ...
    }
    //...



 세번째로 @WithMockUser는 단순하게 username과 role정도만 설정하면 인증정보를 설정해주는 어노테이션입니다. 별다른 설정을 추가하지 않고도 사용할 수 있어서 매우 편리할 듯 합니다.


    //...
    @Test
    @WithMockUser(username = "username", roles = "USER")
    public void some_test() {
        ...
    }
    //...


 대부분의 경우는 위의 어노테이션을 이용하면 테스트가 가능할 것으로 생각되지만 인증주체에 관한정보를 UserDetails를 사용하지 않는 경우에는 별도의 커스텀 어노테이션을 만들어서 사용할 수 있습니다.


@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithCustomMockUser {

    String userNo() default "1";

    String userId() default "user";

    String name() default "name";

}
public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithCustomMockUser> {
    @Override
    public SecurityContext createSecurityContext(WithCustomMockUser user) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        CustomUser principal = new CustomUser(Long.valueOf(user.memberNo()), user.userId(), user.name());
        Authentication auth = new UsernamePasswordAuthenticationToken(principal, "password", Arrays.asList(new SimpleGrantedAuthority("ROLE_MEMBER")));
        context.setAuthentication(auth);
        return context;
    }
}

 위와같이 커스텀 어노테이션을 만든 후 기본 어노테이션과 동일한 방법으로 사용하면 커스텀 인증정보가 설정된 상태로 테스트를 진행할 수 있습니다.

    //...
    @Test
    @WithCustomMockUser
    public void some_test() {
        ...
    }
    //...


 또한 아래와 같이 어노테이션을 정의하여 반복되는 설정을 간소화 할 수도 있습니다.

@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value="admin",roles="ADMIN")
public @interface WithCustomMockAdmin {
}
//...
@Test
@WithCustomMockAdmin
public void some_test() {
    ...
}
//...



 기존에는 로그인 상태를 테스트 할때 장황한 코드가 필요했었는데 Spring Security에서 제공하는 테스팅 피처를 이용하면 간단하게 로그인 상태 별로 테스트가 가능하므로 어플리케이션을 좀더 안전한상태로 개발할 수 있을것 같습니다.




Spring Security 레퍼런스

Posted by Reiphiel
Java/Spring2015. 11. 17. 08:01
반응형

 Spring 프레임워크로 @Value 어노테이션(Annotation)을 이용하여 프로퍼티 값을 필드에 인젝션(Injection)하기 위해서 PropertyPlaceHolder관련 설정을 추가하고 어플리케이션을 재기동하자 해당 설정 파일(@Configuration 어노테이션을 이용한 Java based config)에서 @PostConstruct 어노테이션을 지정한 메소드가 실행되지 않는 현상이 발생했습니다.



@PostConstruct


 먼저 @PostConstruct 어노테이션에 대해서 간단하게 알아보도록 하겠습니다. 이 어노테이션은 @PreDestroy과 쌍을 이루는 어노테이션으로 xml기반의 Bean 설정에서 각각 'init-method'와 'destroy-method' 해당합니다. @PostConstruct를 이용하면 Bean 객체를 생성한 이후에 초기화를 수행할 메소드를 지정할 수 있으며 @PreDestroy는 Bean 객체의 라이프 사이클이 종료되기 직전에 사용한 자원의 해제할 필요가 있을 경우등에 사용합니다.


※ @PostConstruct와 @PreDestroy는 Spring이 아닌 javax패키지로 Java의 표준 어노테이션이라는 점에 주의하시기 바랍니다.




@PostConstruct를 지정한 메소드가 호출되지 않는 원인


 원인을 명확히 하기 위해서 전체 설정중 관련부분중 필수적인 부분만 발췌하여 코드를 작성해서 단위테스트를 실행해 보았습니다. 



설정 클래스

//...package
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
//...import

@Configuration
@ComponentScan(basePackages = "test")
public class TestConfig {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestConfig.class);

    @Value("${testkey}")
    private String value;

    @Bean
    public PropertyPlaceholderConfigurer propertyPlaceholderConfigurer() {
        final Properties properties = new Properties();
        properties.setProperty("testkey", "testvalue");
        PropertyPlaceholderConfigurer configurer = new PropertyPlaceholderConfigurer();
        configurer.setProperties(properties);
        return configurer;
    }

    @PostConstruct
    public void post() {
        LOGGER.debug(">> Post Construct - start");
        LOGGER.debug(">> Post Construct - injected value : {}", this.value);
        LOGGER.debug(">> Post Construct - finish");
    }
}



단위 테스트 클래스

//...package
//...import

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = TestConfig.class)
public class TestClass {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestClass.class);

    @Value("${testkey}")
    private String value;

    @Test
    public void test() {
        LOGGER.debug(">> Test - start");
        LOGGER.debug(">> Test - injected value : {}", this.value);
        LOGGER.debug(">> Test - finish");
    }

    @PostConstruct
    public void post() {
        LOGGER.debug(">> Test Post Construct - start");
        LOGGER.debug(">> Test Post Construct - injected value : {}", this.value);
        LOGGER.debug(">> Test Post Construct - finish");
    }
}



 위와 같이 설정한 후 테스트를 실행한 결과 @PostConstruct가 지정된 메소드가 실행되지 않아 로그가 출력되지 않는 현상을 확인할 수 있었습니다. 반면에 PropertyPlaceholder관련 설정 빼고 테스트를 실행했을 경우는 해당 메소드가 실행되어 로그가 출력되었습니다. 단위 테스트 클래스에서 @PostConstruct를 지정한 메소드는 두 케이스 모두 정상적으로 실행되었으므로 해당 설정은 Bean단위로 영향을 미친다는 것을 확인 할 수 있었습니다.


※ Java 기반의 설정 클래스도 하나의 빈으로 등록되어 처리되는 점에 유의하시기 바랍니다.



 현상을 특정하고 원인을 찾아보던중 허무하게도 Spring의 JavaDoc에서 원인을 파악할 수 있었습니다. 해당 문서는 영문인 관계로 발번역이지만 간단하게 번역해 보았습니다.



 Special consideration must be taken for @Bean methods that return Spring BeanFactoryPostProcessor (BFPP) types. Because BFPP objects must be instantiated very early in the container lifecycle, they can interfere with processing of annotations such as @Autowired, @Value, and @PostConstruct within @Configuration classes. To avoid these lifecycle issues, mark BFPP-returning @Bean methods as static.


  첫번째 단락은 BeanFatoryPostProcess(BFPP) 타입을 반환하는 @Bean 메소드의 경우는 컨테이너의 라이프 사이클에서 매우 이른 시점에 초기화 되어야 하므로 @Configuration 클래스 내부의 @Autowired, @Value, @PostConstruct 와 같은 어노테이션 기반 프로세싱을 방해할 수 있다는 내용으로 회피하기 위에서는 @Bean 메소드를 static 메소드로 정의하여야 한다는 내용입니다.




  By marking this method as static, it can be invoked without causing instantiation of its declaring @Configuration class, thus avoiding the above-mentioned lifecycle conflicts. Note however that static @Bean methods will not be enhanced for scoping and AOP semantics as mentioned above. This works out in BFPP cases, as they are not typically referenced by other @Bean methods. As a reminder, a WARN-level log message will be issued for any non-static @Bean methods having a return type assignable to BeanFactoryPostProcessor.


 정적 메소드로 정의함으로서 @Configuration 클래스의 인스턴스를 생성하지 않고도 초기화가 가능하기 때문에 위에서 언급한 라이프 사이클 문제를 회피할 수 있지만 Inter-bean references를 사용할 수 없다는 문제가 있습니다. 하지만 BeanFactoryPostProcess 타입의 빈은 일반적으로 Inter-bean references를 사용하지 않기 때문에 특별히 문제되지 않는다고 기술되어 있습니다.




해결책


 위의 인용구에서 언급된 것과 같이 BeanFactoryPostProcessor 타입의 Bean을 선언할 경우에는 해당 선언 메소드를 static 메소드로 선언하면 해당 문제를 회피할 수 있습니다.




 위의 인용구 중 마지막 문장에 @Bean 메소드중 리턴타입이 BeanFactoryPostProcess이면서 정적메소드가 아닌경우 WARN레벨의 로그 메시지를 출력한다는 부분이 있어 확인해보았습니다.


WARN  o.s.c.a.ConfigurationClassEnhancer - @Bean method TestConfig.propertyPlaceholderConfigurer is non-static and returns an object assignable to Spring's BeanFactoryPostProcessor interface. This will result in a failure to process annotations such as @Autowired, @Resource and @PostConstruct within the method's declaring @Configuration class. Add the 'static' modifier to this method to avoid these container lifecycle issues; see @Bean javadoc for complete details.



 실제로 로그 메시지를 확인한 결과 위와 같이 WARN 레벨의 로그가 출력되고 있는 것을 확인할 수 있었습니다. 로그 메시지를 잘 확인하고 있었다면 문제가 발생한 시점에 대응책을 확인할 수 있었을 걸 하는 생각도 들었지만 사실 개발 단계에서는 DEBUG 레벨의 로그를 출력하도록 설정해 놓기 때문에 출력되는 로그가 방대하여 특별히 에러가 발생하기 전에는 인지하기 어려운 것도 현실인 듯 합니다. 따라서 주기적으로 로그를 확인하는 것 보다 설정 관련 부분은 사소한 부분도 단위 테스트를 이용하여 설정 미스나 다른 설정에 영향을 받아 상정하고 있던 것과 다른 동작을 하는 경우를 즉시 확인할 수 있도록 하는 것이 문제 예방의 지름길이 아닐까 생각해 보았습니다.




▷ 본문에서 인용한 JavaDoc은 아래의 링크를 통해서 확인하실 수 있습니다.

http://docs.spring.io/spring/docs/3.2.x/javadoc-api/org/springframework/context/annotation/Bean.html


Posted by Reiphiel
Java/Spring2015. 2. 9. 01:09
반응형

 Spring Boot를 이용하여 웹 어플리케이션을 작성할 경우 일반적인 웹 어플리케이션과 같이 이미 구동된 WAS(Web Application Server)에 반영(Deploy)하는 절차를 통해서 구동되는 것과는 달리 어플리케이션 자체에 WAS를 내장하고 구동하는 형태를 띠고 있기 때문에 수정된 내용을 반영하기 위해서는 가상머신(JVM)을 재기동할 필요가 있었습니다. 이런 경우 Spring에서 제공하고 있는 Spring Loaded를 이용하면 가상머신을 재기동 하지 않고 개발을 진행할 수 있어 개발 생산성을 높일 수 있을 듯 합니다. Spring 레퍼런스 문서상에는 이러한 기능을 Hot Swapping이라는 타이틀로 기술하고 있습니다. 물론 Spring Loaded가 웹 어플리케이션만을 위한 기술은 아닙니다. 데몬과 같이 상주형 어플리케이션을 개발할 경우에도 유용하게 사용할 수 있습니다.


※Hot Swap이란 Java 1.4에서 추가된 Java Platform Debugger Architecture (JPDA)의 기능의 하나입니다.



 먼저 git저장소(https://github.com/spring-projects/spring-loaded)에서 라이브러리를 다운로드 받아서 적당한 위치에 저장합니다.



커맨드 라인 실행

java -javaagent:<path>/springloaded-{버전}.jar -noverify JavaEntryClass

※JavaEntryClass는 main메소드가 있는 진입점 클래스를 의미합니다.


 위의 실행 명령어를 입력하여 Spring Boot 어플리케이션을 실행하면 소스코드 수정후 저장하면 즉시 소스 코드가 반영됩니다. 정확한 내용은 소스코드를 분석해 보지 않아서 모르겠지만 기반이 되는 기술은 위에서 언급한 JPDA인듯하고 실제 반영되는 부분은 Grails2를 이용해서 수행하고 있는듯 합니다. 사실 JPDA에서 제공하는 Hot Swap은 클래스 구조에 영향을 끼치지 않는 범위(메소드 바디 수정)에서만 동작하는데 Spring Loaded의 경우는 메소드/필드/생성자 추가 삭제가 가능합니다. 동작원리는 본 포스팅의 범위를 벗어나므로 따로 분석하지 않겠습니다.(사실은 능력이...) 관심있는 분들은 소스코드를 분석해 보는것도 좋은 공부가 되지 않을지 싶습니다.

 사실 일반적인 프로젝트에서 처럼 WAS에서 어플리케이션 단위로 디플로이 되는 것이 아닌 런타임에 Java 소스 코드 반영이 프로덕트 환경에서 재기동 없는 Hot Swapping이 정확한 동작을 보증할지 의문이 생기기도 하고 위의 커맨드 라인 실행에서 추가한 -noverify 옵션은 바이트 코드의 검증을 생략한다는 무시무시한 설명만 있어서 먼가 꺼림직 하기도 합니다.



IDE환경에서의 실행

 사실 일반적으로 개발을 진행할 경우 커맨드 라인 환경에서 어플리케이션을 실행하는 경우는 드물다고 생각되므로 IDE(통합 개발 환경)환경하에서 실행하는 방법에 대해서 자세히 알아보도록 하겠습니다.


※본 포스팅에서는 일반적으로 많이 사용하는Eclipse(이클립스)를 기준으로 설명하도록 하겠습니다.



Run > Run Configuration 메뉴에서 가상머신 인자로 위와 같이 지정해서 실행하면 위에서 소개한 커맨드라인 실행과 동일하게 작동가능합니다. Main탭에서 엔트리 클래스를 별도로 지정하므로 인자에 엔트리 클래스 명을 기술할 필요가 없다는 점이 다릅니다.



 위에서 소개한 Run Configuration상에 지정하는 방법은 각 개발자가 개별로 설정을 진행해야 하기때문에 프로젝트를 진행하는 관점에서 생각하면 아직 불편합니다. 전체 개발자의 개발환경을 동일하게 구성하기 위해서는 역시 빌드 툴을 통해서 작동할 수 있도록 해주어야 합니다. Maven이용해서 Spring Loaded를 적용하는 방법을 살펴보도록 합시다.


Maven 이용하여 실행하기


	<build>
		<plugins>
			<plugin>
				<groupid>org.springframework.boot</groupid>
				<artifactid>spring-boot-maven-plugin</artifactid>
				<dependencies>
					<dependency>
						<groupid>org.springframework</groupid>
						<artifactid>springloaded</artifactid>
						<version>1.2.1.RELEASE</version>
					</dependencies>
			</plugin>
		</plugins>
	</build>



 위처럼 pom.xml에 플러그인 설정을 추가하고 Maven Build를 이용하여 실행하면 적용됩니다. Maven Build의 Goals에 spring-boot:run 을 지정하여 빌드를 실행하면 Spring Loaded가 적용된 상태로 어플리케이션이 기동됩니다. Gradle의 경우에는 Intellij의 플러그인을 통해서 Spring Loaded가 적재된 상태로 가능한 듯 합니다.



 다음으로 알아볼 내용은 정적 리소스(엄밀한 의미에서 정적 리소스가 아닌 템플릿 엔진의 템플릿 파일을 의미함)의 재로딩 방법입니다. Spring Boot에서는 다양한 템플릿 엔진을 기본적으로 지원하고 있는데 Spring Boot의 기본 설정 파일인 'application.properties'에 아래와 같은 간단한 설정만으로 재로딩이 가능합니다.


 Thymeleaf

spring.thymeleaf.cache=false



 FreeMarker

spring.freemarker.cache=false



 Groovy

spring.groovy.template.cache=false



 Velocity

spring.velocity.cache=false



 본 포스팅에서 소개한 Spring Loaded를 이용하여 Spring Boot를 이용한 개발에 생산성을 더해보도록 합시다. Spring Boot자체가 개발의 편의성을 위해서 나왔는데 이를 좀 더 편리하게 하는 무엇인가가 필요하다는게 좀 아이러니 하기도 합니다.


Posted by Reiphiel