2016/11/08 에 Spring Boot 1.4.2 Available Now 가 출시되었다.over 100 fixes, improvements and 3rd party dependency updates 100 여개의 결함을 수정하고 구현하고, 서드파티에 대한 의존성 업데이트가 있었다고 한다.

그와 관련된 변화 중 하나가 spring-boot 라는 플러그인 아이디가org.springframework.boot 로 변경되었다.

이와 관련된 정보는 프로젝트 빌드를 실행해보면,

...
The plugin id 'spring-boot' is deprecated. Please use 'org.springframework.boot' instead.
...

와 같은 메시지가 출력되는 것을 확인했다. 이에 대한 정보를 찾아보았다. 이에 대해서 정보를 찾아보던 중에

64. Spring Boot Gradle plugin 페이지에서 힌트를 발견했다.

buildscript {
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.4.2.RELEASE")
    }
}
apply plugin: 'org.springframework.boot'

와 같은 형태로 되어 있는 것을 확인했다. 내가 사용하고 있는 build.gradle 에는 다음과 같이 선언되어 있다.

buildscript {
    ext {
        springBootVersion = '1.4.2.RELEASE'
    }
    repositories {
        jcenter()
        maven { url "https://repo.spring.io/snapshot" }
        maven { url "https://repo.spring.io/milestone" }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'spring-boot' 
apply plugin: 'spring-boot' 만 apply plugin: 'org.springframework.boot'으로 변경하면 된다.

해결책

buildscript {
    ext {
        springBootVersion = '1.4.2.RELEASE'
    }
    repositories {
        jcenter()
        maven { url "https://repo.spring.io/snapshot" }
        maven { url "https://repo.spring.io/milestone" }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'org.springframework.boot' 
잘 찾았길 바란다.


발생문제

org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [entity.list_collection_a, entity.list_collection_b]
@ElementCollection(targetClass = EnumType.class, fetch = FetchType.EAGER)
@Enumerated(EnumType.STRING)

해당필드는 enum 타입의 목록을 가지는 필드였고, 프로젝트의 의존성 라이브러리 버전들을 업그레이드 하면서org.hibernate.loader.MultipleBagFetchException 이 발생했다.

  • 업그레이드

    • org.hibernate:hibernate-entitymanager:5.1.0.Final →org.hibernate:hibernate-entitymanager:5.2.4.Final

해결방법

이 문제를 해결하는 방법은 enum 타입 컬렉션 필드에 정의를 다음과 같이 변경했다.

@ElementCollection(targetClass = EnumType.class)
@Enumerated(EnumType.STRING)
@LazyCollection(LazyCollectionOption.FALSE)

@ElementCollection 는 기본적으로 LAZY 값을 가진다. 그러나 한단에 선언된@LazyCollection(LazyCollectionOption.FALSE) 을 통해서 EAGER 로 처리된다.@LazyCollection 는 컬렉션 타입에 대한 LAZY 여부를 결정하는 기능을 한다.


스프링부트 기본 에러페이지는 ‘whitelabel’ error page 이다. 이와 관련된 내용은 ErrorMvcAutoConfiguration 을 살펴보면 찾아볼 수 있다.

private final SpelView defaultErrorView = new SpelView(
"<html><body><h1>Whitelabel Error Page</h1>"
+ "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
+ "<div id='created'>${timestamp}</div>"
+ "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
+ "<div>${message}</div></body></html>");

이 기본적인 화이트레이블 페이지 대신에 사용자가 지정한 에러페이지로 변경이 가능하다. 이를 위해서는

server.error.include-stacktrace=never # When to include a "stacktrace" attribute.
server.error.path=/error # Path of the error controller.
server.error.whitelabel.enabled=true # Enable the default error page displayed in browsers in case of a server error.

위 세가지 속성에 대한 조작을 해야 한다. 우선 사용자지정 에러페이지를 사용하기 위해서는 화이트레이블 출력을 비활성화해야 한다.

server.error.whitelabel.enabled=false

로 변경한다. 그리고 에러페이지에서 발생한 에러의 스택트레이스를 출력하기 위해서는 server.error.include-stacktrace 을 never 이외의 것으로 변경해야 한다.

server.error.include-stacktrace=always or on_trace_param

이와 관련된 값은 ErrorProperties 를 살펴보면 된다.

사용가능한 속성들을 템플릿(Thymeleaf) 에 적용해본 예다.

<div class="container error-404">
    <h1 th:text="${status}">Status</h1>
    <h2>Houston, we have a problem([[${error}]])</h2>
    <p th:text="${message}"> Error Message</p>
    <p>
        <a href="index.html" th:href="@{/}" class="btn red btn-outline"> Return home </a>
        <br>
    </p>
    <div>
        <label>Time Stamp</label>
        <div th:text="${timestamp}"></div>
    </div>
    <div>
        <label>Exception name</label>
        <div th:text="${exception}"></div>
    </div>
    <div>
        <label>Trace</label>
        <div th:utext="${trace}"></div>
    </div>
</div>

화면에 출력가능한 속성값들에 대해서는 DefaultErrorAttributes 를 살펴보면 이용할 수 있는 정보를 확인가능하다. 그러면 다음과 같은 화면을 볼 수 있게 된다.



스프링 시큐리티와 연계하여 로그인에 실패했을 경우 로그인화면으로 리다이렉트 시키면서 이메일을 다시 입력폼에 유지하려고 여러가지 시도를 해봤다.

스프링 시큐리티에서 로그인 실패를 담당하는 인터페이스는 AuthenticationFailureHandler 이다.

public interface AuthenticationFailureHandler {
 
/**
 * Called when an authentication attempt fails.
 * @param request the request during which the authentication attempt occurred.
 * @param response the response.
 * @param exception the exception which was thrown to reject the authentication
 * request.
 */
void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOExceptionServletException;
}

위의 형태를 가지는 간단한 인터페이스로, 스프링 시큐리티 설정에서 formLogin() 에 설정하면 로그인이 실패했을 떄 호출되며 이에 대한 처리를 수행한다.

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .anyRequest().authenticated()
        // form 로그인 
        .and()
        .formLogin()
        .loginPage("/login").permitAll()
        .usernameParameter(USERNAME_PARAMETER)
        .passwordParameter(PASSWORD_PARAMETER)
        .successHandler(authenticationSuccessHandler()) (1)
        .failureHandler(authenticationFailureHandler()) (2)
}
 
// 생략 
@Bean
AuthenticationSuccessHandler authenticationSuccessHandler() {
    return new HoneymonUserAuthenticationSuccessHandler();
}
 
@Bean
AuthenticationFailureHandler authenticationFailureHandler() {
    return new HoneymonUserAuthenticationFailureHandler();
}
로그인 성공시 후속처리
로그인 실패시 후속처리

그리고 HoneymonUserAuthenticationFailureHandler가 호출되면 /login?error={message}&email={email}의 형식으로 리다이렉트되도록 해뒀다. 그런데 암만 로그인 실패가 나도 /login?error= 로 리다이렉트 되지를 않는 것이다. 이 문제 떄문에 한참 씨름을 했다.

public class HoneymonUserAuthenticationFailureHandler implements AuthenticationFailureHandler {
 
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOExceptionServletException {
        if(exception instanceof BadCredentialsException) {
            exception = new BadCredentialsException("security.exception.bad_credentials");
        }
        response.sendRedirect("/login?error=" + exception.getMessage() + "&email=" + request.getParameter(USERNAME_PARAMETER));
    }
}

옆에 누군가가 봐줬으면 참 좋았을텐데…​

그러다가 정말 우연찮게

'아, 내가 LoginWeb' 클래스를 지우고 ViewController 로 처리하도록 했지?

하는 생각이 미쳤다. 리다이렉트가 되지 않고 있는 url에 대해서는 다음과 같이 설정되어 있었다.

@Configuration
public class WebConfig  extends WebMvcConfigurerAdapter {
  @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("front/login");  // 로그인 
        registry.addViewController("/signup").setViewName("front/signup");  // 회원가입 
    }
}

이 간단한 설정이 나를 괴롭혔던 주 원인이었다.

ViewController 에서 /login 요청에 대한 처리를 수행하기는 하지만 뒤에 전달되는 파라미터들을 모두 소거시킨다는 것을 이해하지 못한 것이다.

어떤 장애든지 기본적인 접근은 그 장애에 영향을 주는 설정부터 살펴보도록 하자. ㅡ_-);; 내가 허비한 시간 우짤꺼야!!

This is a shortcut for defining a ParameterizableViewController that immediately forwards to a view when invoked. Use it in static cases when there is no Java controller logic to execute before the view generates the response.



HttpServletResponse 의 sendRedirect 메서드를 사용하다가 다음과 같은 메시지가 나왔다.

java.lang.IllegalStateException: Cannot call sendRedirect() after the response has been committed
at org.apache.catalina.connector.ResponseFacade.sendRedirect(ResponseFacade.java:494)
at javax.servlet.http.HttpServletResponseWrapper.sendRedirect(HttpServletResponseWrapper.java:138)
at javax.servlet.http.HttpServletResponseWrapper.sendRedirect(HttpServletResponseWrapper.java:138)
at org.springframework.security.web.firewall.FirewalledResponse.sendRedirect(FirewalledResponse.java:41)
at javax.servlet.http.HttpServletResponseWrapper.sendRedirect(HttpServletResponseWrapper.java:138)
at org.springframework.security.web.util.OnCommittedResponseWrapper.sendRedirect(OnCommittedResponseWrapper.java:128)
at javax.servlet.http.HttpServletResponseWrapper.sendRedirect(HttpServletResponseWrapper.java:138)
at org.springframework.security.web.util.OnCommittedResponseWrapper.sendRedirect(OnCommittedResponseWrapper.java:128)
  // 중략

이런 코드가 발생하는 경우를 찾아보면, 다음처럼 되어 있었다.

String redirectUrl = request.getRequestURI();
 
if(user.getStatus() == UserStatus.EMAIL_VERIFY) {
    response.sendRedirect(URL_SIGNUP_PROGRESS);
}
 
if(request.getRequestURI().contains("login") || request.getRequestURI().contains("sign")) {
    response.sendRedirect(URL_ROOT);
}
 
response.sendRedirect(redirectUrl);

예외 메시지를 살펴보니 sendRedirect 메서드가 호출된 이후에 재호출하려고 하면 java.lang.IllegalStateException: Cannot call sendRedirect() after the response has been committed 가 출력되는 것이다. 호출된 이후에는 다시 호출할 수 없다는 뜻으로 변경할 수 없다는 뜻이다.

이에 대한 해결책을 좀 찾아봤다.

이걸 해결항 수 있는 방법으로는,

String redirectUrl = request.getRequestURI();
 
if(user.getStatus() == UserStatus.EMAIL_VERIFY) {
    response.sendRedirect(URL_SIGNUP_PROGRESS);
} else if(request.getRequestURI().contains("login") || request.getRequestURI().contains("sign")) {
    response.sendRedirect(URL_ROOT);
} else {
    response.sendRedirect(redirectUrl);
}

와 같은 방식으로 완전히 조건문 내에서 한번만 호출되도록 처리하거나,

String redirectUrl = request.getRequestURI();
 
if(user.getStatus() == UserStatus.EMAIL_VERIFY) {
    redirectUrl = URL_SIGNUP_PROGRESS;
}
 
if(request.getRequestURI().contains("login") || request.getRequestURI().contains("sign")) {
    redirectUrl = URL_ROOT;
}
 
response.sendRedirect(redirectUrl);

위의 코드처럼 조건을 모두 마친 후에 마지막에 sendRedirect 를 호출하는 것이다. 마지막 방법이 좀 더 괜찮은 처리방법이라는 생각이…​


+ Recent posts