- @SessionAttribute 사용 권장 안함
- @SessionAttribute는 주로 세션에 저장된 모델 속성을 뷰에서 다시 사용할 때 활용됩니다.
- 인증 정보를 관리하는 용도로는 적합하지 않으며, 세션에 해당 속성이 없을 경우 예외가 발생할 수 있습니다
- HttpSession을 멤버 필드로 주입받는 것의 문제점이 발생 될 수 있음
- Spring MVC 컨트롤러는 기본적으로 싱글톤(하나의 인스턴스)으로 관리됩니다.
- HttpSession을 멤버 필드로 주입받으면 여러 요청 사이에 공유될 수 있어 쓰레드 안전성 문제가 발생할 수 있습니다.
- 세션은 각 사용자마다 별도로 관리되어야 하므로, 멤버 필드로 주입받는 것은 적절하지 않습니다.
- 메서드 파라미터로 HttpSession을 받아 사용하는 것의 장점
- HttpSession을 메서드 파라미터로 받아 사용하면, 각 요청마다 개별적인 세션 객체에 접근할 수 있어 쓰레드 안전성이 확보됩니다.
- 필요한 메서드에서만 세션을 사용하므로 코드의 명확성과 가독성이 향상됩니다.
- Spring MVC는 메서드 파라미터로 HttpSession을 제공하도록 지원하고 있습니다.
webConfig.java 부분 수정
더보기
package com.tenco.blog_v1.common.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
// @Component // IOC 하나만 메모리
@Configuration // 메서드에서 @bean을 해야한다면 써야 한다.
public class webConfig implements WebMvcConfigurer {
@Autowired // DI 처리
private LoginInterceptor loginInterceptor;
/**
* 인터셉터를 등록하고 적용할 URL 패턴을 설정하는 메서드이다.
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/protected/**", "/user", "/reply/**") // 인터셉터를 적용시킬 경로 패턴 설정
.excludePathPatterns("/board/{id:\\d+}"); // 인터셉터를 제외할 경로 패턴 설정
// 인터셉터 적용에서 제외할 URL 패턴 설정
// /board/1, /board/33 <-- 로그인 인터셉터에서 제외
// \d+ 숫자 하나 이상을 의미하는 정규 표현식 패턴
// 관리자용 인터셉터 등록
}
}
UserController.java 부분 수정
/**
* 회원 정보 페이지 요청
* 주소 설계 : http://localhost:8080/user/update-form
*
* @param
* @return 문자열
* 반환되는 문자열을 뷰 리졸버가 처리하면
* 머스태치 템플릿 엔진을 통해서 뷰 파일을 렌더링 합니다.
*/
@GetMapping("/user/update-form")
// public String updateForm(HttpServletRequest request, @SessionAttribute(name = "sessionUser") User sessionUser) {
public String updateForm(HttpServletRequest request, HttpSession session) {
log.info("회원 수정 페이지");
// model.addAttribute("name", "회원 수정 페이지");
// User sessionUser = (User)session.getAttribute("sessionUser");
// @SessionAttribute(name = "sessionUser") User sessionUser
// 위에 있는 어노테이션은 모델에 저장되어 있는 세션 값을 바로 가지고 오는 어노테이션이다.
// 단, 이녀석은 뷰, 템플릿 엔진에서 접근하도록 설계 되어 있다. 권장 X
User sessionUser = (User) session.getAttribute("sessionUser");
User user = userService.readUser(sessionUser.getId());
request.setAttribute("user", user);
return "user/update-form"; // 템플릿 경로: user/update-form/mustache
}
UserController.java 전체코드
더보기
package com.tenco.blog_v1.user;
import com.tenco.blog_v1.common.errors.Exception401;
import com.tenco.blog_v1.common.errors.Exception500;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@Slf4j
@Controller
@RequiredArgsConstructor
public class UserController {
// DI 처리
// private final UserRepository userRepository;
private final UserService userService;
private final HttpSession session;
/**
* 회원 정보 페이지 요청
* 주소 설계 : http://localhost:8080/user/update-form
*
* @param
* @return 문자열
* 반환되는 문자열을 뷰 리졸버가 처리하면
* 머스태치 템플릿 엔진을 통해서 뷰 파일을 렌더링 합니다.
*/
@GetMapping("/user/update-form")
// public String updateForm(HttpServletRequest request, @SessionAttribute(name = "sessionUser") User sessionUser) {
public String updateForm(HttpServletRequest request, HttpSession session) {
log.info("회원 수정 페이지");
// model.addAttribute("name", "회원 수정 페이지");
// User sessionUser = (User)session.getAttribute("sessionUser");
// @SessionAttribute(name = "sessionUser") User sessionUser
// 위에 있는 어노테이션은 모델에 저장되어 있는 세션 값을 바로 가지고 오는 어노테이션이다.
// 단, 이녀석은 뷰, 템플릿 엔진에서 접근하도록 설계 되어 있다. 권장 X
User sessionUser = (User) session.getAttribute("sessionUser");
User user = userService.readUser(sessionUser.getId());
request.setAttribute("user", user);
return "user/update-form"; // 템플릿 경로: user/update-form/mustache
}
/**
* 사용자 정보 수정
* @param reqDTO
* @return 메인 페이지
*/
@PostMapping("/user/update")
public String update(@ModelAttribute(name = "updateDTO") UserDTO.UpdateDTO reqDTO) {
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null) {
return "redirect:/login-form";
}
// 유효성 검사는 생략
// 사용자 정보 수정
User updatedUser = userService.updateUser(sessionUser.getId(), reqDTO);
// 세션 정보 동기화 처리
session.setAttribute("sessionUser", updatedUser);
return "redirect:/";
}
/**
* 회원가입 페이지 요청
* 주소 설계 : http://localhost:8080/join-form
*
* @param model
* @return 문자열
* 반환되는 문자열을 뷰 리졸버가 처리하면
* 머스태치 템플릿 엔진을 통해서 뷰 파일을 렌더링 합니다.
*/
@GetMapping("/join-form")
public String joinForm(Model model) {
log.info("회원가입 페이지");
model.addAttribute("name", "회원가입 페이지");
return "user/join-form"; // 템플릿 경로: user/join-form/mustache
}
/**
* 회원 가입 기능 요청
* @param reqDto
* @return
*/
@PostMapping("/join")
public String join(@ModelAttribute(name = "joinDTO") UserDTO.JoinDTO reqDto) {
try {
userService.signUp(reqDto);
} catch(DataIntegrityViolationException e) {
// DataIntegrityViolationException :제약 조건 위반
throw new Exception500("동일한 유저네임이 존재 합니다.");
}
return "redirect:/login-form";
}
/**
* 로그인 페이지 요청
* 주소 설계 : http://localhost:8080/login-form
*
* @param model
* @return 문자열
* 반환되는 문자열을 뷰 리졸버가 처리하면
* 머스태치 템플릿 엔진을 통해서 뷰 파일을 렌더링 합니다.
*/
@GetMapping("/login-form")
public String loginForm(Model model) {
log.info("로그인 페이지");
model.addAttribute("name", "로그인 페이지");
return "user/login-form"; // 템플릿 경로: user/login-form/mustache
}
/**
* 자원에 요청은 GET 방식이지만 보안의 이유로 예외!
* 로그인 처리 메서드
* 요청 주소 : POST http://localhost:8080/login
* @param reqDTO
* @return
*/
@PostMapping("/login")
public String login(UserDTO.LoginDTO reqDTO) {
try {
User sessionUser = userService.signIn(reqDTO);
session.setAttribute("sessionUser", sessionUser);
return "redirect:/";
} catch (Exception e) {
// 쿼리 스트링으로 error 이 들어오면 따로 처리하는 방식도 있다.
// 로그인 실패
throw new Exception401("유저이름 또는 비밀번호가 틀렸습니다.");
}
}
/**
* 로그아웃
* 주소 : http://localhost:8080/logout
* @return
*/
@GetMapping("/logout")
public String logout() {
session.invalidate(); // 세션을 무효화 한다. (로그아웃)
return "redirect:/";
}
}
결론
로그인 처리 후 사용자 정보를 세션 메모리에 저장하는 것 자체는 문제없습니다.
이때 세션 객체는 JSESSIONID를 통해 사용자별로 관리되기 때문에 안전합니다.
하지만 세션 정보를 컨트롤러의 멤버 필드로 저장하게 되면, 컨트롤러의 싱글톤 특성 때문에 해당 필드가 여러 사용자 요청 간에 공유되면서 쓰레드 안전성 문제가 발생할 수 있습니다. 따라서, 세션 정보를 관리할 때는 멤버 필드로 사용하지 말고, 메서드 파라미터로 세션 객체에 접근하여 개별적으로 사용하는 것이 더 안전하고 권장되는 방법입니다.
'Spring boot' 카테고리의 다른 글
| 2024.10.17 Blog 프로젝트 만들기(JPA) 댓글 쓰기 및 삭제 인터셉터 적용 (2) | 2024.10.17 |
|---|---|
| 2024.10.17 Blog 프로젝트 만들기(JPA) 게시글 삭제 오류 해결 (미완성) (0) | 2024.10.17 |
| 2024.10.17 Blog 프로젝트 만들기(JPA) 댓글 목록 보기 (0) | 2024.10.17 |
| 2024.10.16 Blog 프로젝트 만들기(JPA) 댓글 테이블 설계 (엔티티) (1) | 2024.10.16 |
| 2024.10.15 Blog 프로젝트 만들기(JPA) Service 레이어 만들기 (0) | 2024.10.15 |