2020. 9. 17. 17:05ㆍSpring/Spring Boot
1. 리팩토링
1) 리팩토링 코드
일반적으로 안좋은 프로그래밍은 동일한 코드가 반복되어 사용되는 것이다. 이전 글에 개발한 내용에서 중복되는 코드라면, 'SessionUser' 코드이다. 첫 화면을 불러오는 메서드 외에 다른 Controller와 메서드에서 세션값이 필요하면 그때마다 직접 세션에서 값을 가져와야한다. 이렇게 중복될 코드를 메서드 인자로 세션값을 바로 받도록 변경해주어야 한다.
2) LoginUser.java
config/auth 패키지 하위에 LoginUser 클래스를 생성해준다.
어노테이션 및 코드 | 설명 |
@Target(ElementType.PARAMETER) | 이 어노테이션이 생성될 수 있는 위치를 지정한다. PARAMETER 속성으로 지정할 경우 메서드의 파리미터로 선언된 객체에서만 사용할 수 있다. |
@interface | 파일을 어노테이션 클래스로 지정한다. LoginUser 어노테이션을 생성한다. |
package com.springbook.biz.config.auth;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
}
2) LoginUserArgumentResolver.java
LoginUser 클래스와 동일한 위치에 LoginUserArgumentResolver 클래스를 생성하여 HandlerMethodArguementResolver의 구현체가 지정한 값으로 해당 메서드의 파라미터로 넘길 수 있도록 한다. (HandlerMethodArgumentResolver은 Controller 메서드에서 특정 조건에 맞는 파라미터가 있을 때 원하는 값을 바인딩해주는 인터페이스이다.)
어노테이션 및 코드 | 설명 |
supportsParameter() | Controller 메서드의 특정 파라미터를 지원하는지 판단한다. 여기에서는 @LoginUser 어노테이션이 붙어 있고, 파라미터 클래스 타입이 SessionUser.class인 경우 true를 반환한다. |
resolveArgument() | 파라미터에 전달할 객체를 생성한다. 여기서는 세션에서 객체를 가져온다. |
package com.springbook.biz.config.auth;
import com.springbook.biz.config.auth.dto.SessionUser;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
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;
import javax.servlet.http.HttpSession;
@RequiredArgsConstructor
@Component
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {
private final HttpSession httpSession;
@Override
public boolean supportsParameter(MethodParameter parameter) {
boolean isLoginUserAnnotation = parameter.getParameterAnnotation(LoginUser.class) != null;
boolean isUserClass = SessionUser.class.equals(parameter.getParameterType());
return isLoginUserAnnotation && isUserClass;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
return httpSession.getAttribute("user");
}
}
3) WebMvcConfig.java
생성한 LoginUserArgumentResolver가 스프링에서 인식되도록 config 패키지 하위에 WebMvcConfig 클래스를 생성한다.
package com.springbook.biz.config;
import com.springbook.biz.config.auth.LoginUserArgumentResolver;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {
public final LoginUserArgumentResolver loginUserArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(loginUserArgumentResolver);
}
}
4) Controller
마지막으로 Controller의 파라미터에 반복되는 부분을 어노테이션으로 변경해준다. 앞으로 세션 정보를 가져올 때 @LoginUser 어노테이션으로 해결이 가능하다.
package com.springbook.biz.web;
import com.springbook.biz.config.auth.LoginUser;
import com.springbook.biz.config.auth.dto.SessionUser;
import com.springbook.biz.service.posts.PostsService;
import com.springbook.biz.web.dto.PostsResponseDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import javax.servlet.http.HttpSession;
@RequiredArgsConstructor
@Controller
public class IndexController {
private final PostsService postsService;
private final HttpSession httpSession;
@GetMapping("/")
public String index(Model model, @LoginUser SessionUser user) {
model.addAttribute("posts", postsService.findAllDesc());
if(user != null) {
model.addAttribute("userName", user.getName());
}
return "index";
}
...
}
5) 전체적인 흐름
2. 데이터베이스 세션 저장소
1) 문제점
세션이 내장 톰캣의 메모리에 저장되기 때문에, 지금까지 제작한 서비스에서는 프로젝트를 재실행할 때 마다 로그인이 해제가 된다. 즉, 내장 톰캣의 메모리에 저장되기에 프로젝트를 배포할 때 마다 재실행이 되면서 초기화가 된다. 또한 2개 이상의 서버를 운영할 때, 톰캣마다 세션 동기화 설정을 해주어야 하는 문제점이 있다.
2) 해결 방안
해결 방안 | 설명 |
톰캣 세션 사용 | 기본적으로 선택되는 방식으로, 톰캣에 세션이 저장되기에 2개 이상의 WAS가 구동되는 환경에서는 톰캣간의 세션 공유를 위한 추가 설정이 필요하다. |
데이터베이스 세션 저장소 사용 | 여러 WAS 간의 공용 세션을 사용할 수 있는 쉬운 방법이고 어려운 설정은 없으나, 로그인 요청마다 데이터베이스의 입출력이 발생하여 성능상 이슈가 생길 수 있다. 주로 로그인 요청이 많이 없는 백오피스, 사내 시스템 용도에서 사용된다. |
Redis, Memcached와 같은 메모리 DB를 세션 저장소로 사용 | BC2 서비스에서 가장 많이 사용하는 방식이다. 실제 서비스로 사용하기 위해서는 Embedded Redis와 같은 방식이 아닌 외부 메모리 서버가 필요하다. |
3) 데이터베이스 세션 저장소
MySQL와 같은 데이터베이스를 세션 저장소로, 설정이 간단하고 사용자가 많은 서비스가 아닐 때, 비용 절감을 위해 사용된다. (Redis 서비스의 경우, 별도로 사용료를 지불해야한다.) 만약 프로젝트의 규모가 커질 경우에는 Redis와 같은 메모리 DB를 사용해야한다.
4) build.gradle
'spring-session-jdbc' 의존성을 추가해준다.
compile('org.springframework.session:spring-session-jdbc')
5) application.properties
세션 저장소를 jdbc로 선택할 수 있도록 설정해준다.
spring.session.store-type=jdbc
6) 구현 화면
실행할 경우 h2-console에서 SPRING_SESSION, SPRING_SESSION_ATTRIBUTE 테이블이 생성된 것을 확인할 수 있다. (단, h2 데이터베이스의 경우 스프링을 재시작할 때 같이 재시작되기에 세션이 해제된다.)
[참고] 스프링 부트와 AWS로 혼자 구현하는 웹 서비스
'Spring > Spring Boot' 카테고리의 다른 글
[Spring Boot] 테스트 시큐리티 적용 (0) | 2020.09.17 |
---|---|
[Spring Boot] 스프링 시큐리티, 네이버 OAuth 2.0 로그인 (0) | 2020.09.17 |
[Spring Boot] 스프링 시큐리티, 구글 OAuth 2.0 로그인 (1) (0) | 2020.09.17 |
[Spring Boot] 게시물 화면 구성 (2) (0) | 2020.09.17 |