1. 개요
웹 애플리케이션을 개발할 때 여러 뷰에서 동일한 속성을 참조해야 하는 경우가 많습니다. 예를 들어 여러 페이지에 표시해야 하는 장바구니 콘텐츠가 있을 수 있습니다.
이러한 속성을 저장하기에 좋은 위치는 사용자의 세션입니다.
이 예제에서는 간단한 예제에 초점을 맞추고 세션 속성으로 작업하기 위한 2가지 다른 전략을 검토합니다 .
- 범위 지정 프록시 사용
- @SessionAttributes 어노테이션 사용
2. 메이븐 설정
Spring Boot 스타터를 사용하여 프로젝트를 부트스트랩하고 필요한 모든 의존성을 가져옵니다.
설정에는 부모 선언, 웹 스타터 및 thymeleaf 스타터가 필요합니다.
또한 단위 테스트에서 몇 가지 추가 유틸리티를 제공하기 위해 스프링 테스트 스타터를 포함합니다.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.2</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
이러한 의존성의 최신 버전은 Maven Central 에서 찾을 수 있습니다 .
3. 사용 사례 예
우리의 예는 간단한 "TODO" 애플리케이션을 구현할 것입니다. TodoItem 인스턴스를 생성하기 위한 양식 과 모든 TodoItem 을 표시하는 List 보기가 있습니다.
양식을 사용하여 TodoItem 을 생성 하면 양식의 후속 액세스는 가장 최근에 추가된 TodoItem 값으로 미리 채워집니다 . 그의 기능을 사용 하여 세션 범위에 저장된 양식 값 을 "기억"하는 방법을 보여줍니다 .
2개의 모델 클래스는 간단한 POJO로 구현됩니다.
public class TodoItem {
private String description;
private LocalDateTime createDate;
// getters and setters
}
public class TodoList extends ArrayDeque<TodoItem>{
}
TodoList 클래스 는 ArrayDeque 를 확장하여 peekLast 메서드 를 통해 가장 최근에 추가된 항목에 편리하게 액세스할 수 있도록 합니다 .
우리는 2개의 컨트롤러 클래스가 필요합니다: 우리가 살펴볼 각 전략에 대해 1개입니다. 미묘한 차이가 있지만 핵심 기능은 둘 다에 표시됩니다. 각각에는 3개의 @RequestMapping 이 있습니다 .
- @GetMapping(“/form”) – 이 메서드는 폼을 초기화하고 폼 뷰를 렌더링하는 역할을 합니다. 이 메서드는 TodoList 가 비어 있지 않은 경우가장 최근에 추가된 TodoItem 으로 양식을 미리 채웁니다 .
- @PostMapping(“/form”) – 이 메서드는 제출된 TodoItem 을 TodoList 에 추가 하고 List URL로 리디렉션하는 역할을 합니다.
- @GetMapping(“/todos.html”) – 이 메서드는 표시를 위해 TodoList 를 모델 에 추가 하고 List 보기를 렌더링합니다.
4. 범위가 지정된 프록시 사용
4.1. 설정
이 설정에서 TodoList 는 프록시가 지원 하는 세션 범위 @Bean 으로 구성됩니다. @Bean 이 프록시 라는 사실은 우리가 그것을 싱글톤 범위의 @Controller 에 주입할 수 있다는 것을 의미합니다 .
컨텍스트가 초기화될 때 세션이 없기 때문에 Spring은 의존성으로 주입할 TodoList 의 프록시를 생성합니다. TodoList 의 대상 인스턴스는 요청에 의해 필요할 때 필요에 따라 인스턴스화됩니다.
Spring의 bean 범위에 대한 보다 심층적인 논의는 주제에 대한 기사를 참조하십시오 .
먼저 @Configuration 클래스 내에서 빈을 정의합니다.
@Bean
@Scope(
value = WebApplicationContext.SCOPE_SESSION,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public TodoList todos() {
return new TodoList();
}
다음으로 빈을 @Controller 에 대한 의존성으로 선언하고 다른 의존성과 마찬가지로 주입합니다.
@Controller
@RequestMapping("/scopedproxy")
public class TodoControllerWithScopedProxy {
private TodoList todos;
// constructor and request mappings
}
마지막으로, 요청에서 bean을 사용하는 것은 단순히 해당 메소드를 호출하는 것과 관련됩니다.
@GetMapping("/form")
public String showForm(Model model) {
if (!todos.isEmpty()) {
model.addAttribute("todo", todos.peekLast());
} else {
model.addAttribute("todo", new TodoItem());
}
return "scopedproxyform";
}
4.2. 단위 테스트
범위 지정 프록시를 사용하여 구현을 테스트하기 위해 먼저 SimpleThreadScope 를 구성합니다 . 이렇게 하면 단위 테스트가 테스트 중인 코드의 런타임 조건을 정확하게 시뮬레이트할 수 있습니다.
먼저 TestConfig 및 CustomScopeConfigurer 를 정의합니다 .
@Configuration
public class TestConfig {
@Bean
public CustomScopeConfigurer customScopeConfigurer() {
CustomScopeConfigurer configurer = new CustomScopeConfigurer();
configurer.addScope("session", new SimpleThreadScope());
return configurer;
}
}
이제 양식의 초기 요청에 초기화되지 않은 TodoItem이 포함되어 있는지 테스트하여 시작할 수 있습니다.
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@Import(TestConfig.class)
public class TodoControllerWithScopedProxyIntegrationTest {
// ...
@Test
public void whenFirstRequest_thenContainsUnintializedTodo() throws Exception {
MvcResult result = mockMvc.perform(get("/scopedproxy/form"))
.andExpect(status().isOk())
.andExpect(model().attributeExists("todo"))
.andReturn();
TodoItem item = (TodoItem) result.getModelAndView().getModel().get("todo");
assertTrue(StringUtils.isEmpty(item.getDescription()));
}
}
또한 제출이 리디렉션을 발행하고 후속 양식 요청이 새로 추가된 TodoItem 으로 미리 채워져 있음을 확인할 수 있습니다 .
@Test
public void whenSubmit_thenSubsequentFormRequestContainsMostRecentTodo() throws Exception {
mockMvc.perform(post("/scopedproxy/form")
.param("description", "newtodo"))
.andExpect(status().is3xxRedirection())
.andReturn();
MvcResult result = mockMvc.perform(get("/scopedproxy/form"))
.andExpect(status().isOk())
.andExpect(model().attributeExists("todo"))
.andReturn();
TodoItem item = (TodoItem) result.getModelAndView().getModel().get("todo");
assertEquals("newtodo", item.getDescription());
}
4.3. 논의
범위 지정 프록시 전략을 사용하는 주요 기능은 요청 매핑 메서드 서명에 영향을 미치지 않는다는 것입니다. 이것은 @SessionAttributes 전략 에 비해 매우 높은 수준의 가독성을 유지합니다 .
컨트롤러에는 기본적으로 싱글톤 범위가 있다는 점을 기억하면 도움이 될 수 있습니다.
프록시되지 않은 세션 범위 빈을 단순히 주입하는 대신 프록시를 사용해야 하는 이유입니다. 더 작은 범위의 빈을 더 큰 범위의 빈에 주입할 수 없습니다.
이 경우 그렇게 하려고 시도하면 다음 메시지가 포함된 예외가 트리거됩니다. Scope 'session' is not active for the current thread .
컨트롤러를 세션 범위로 정의하려는 경우 proxyMode 지정을 피할 수 있습니다. 특히 각 사용자 세션에 대해 컨트롤러 인스턴스를 만들어야 하므로 컨트롤러를 만드는 데 비용이 많이 드는 경우에는 단점이 있을 수 있습니다.
TodoList 는 주입 을 위해 다른 구성 요소에 사용할 수 있습니다. 이는 사용 사례에 따라 장점이 될 수도 단점이 될 수도 있습니다. 빈을 전체 애플리케이션에서 사용할 수 있게 만드는 것이 문제가 있는 경우 다음 예제에서 볼 수 있듯이 @SessionAttributes 를 사용하는 대신 인스턴스 범위를 컨트롤러로 지정할 수 있습니다 .
5. @SessionAttributes 어노테이션 사용
5.1. 설정
이 설정에서는 TodoList 를 Spring 관리 @Bean 으로 정의하지 않습니다 . 대신 @ModelAttribute 로 선언 하고 @SessionAttributes 어노테이션을 지정 하여 컨트롤러의 세션으로 범위를 지정합니다 .
컨트롤러에 처음 액세스하면 Spring은 인스턴스를 인스턴스화하고 Model 에 배치합니다 . 또한 @SessionAttributes 에 빈을 선언하므로 Spring은 인스턴스를 저장할 것입니다.
Spring 의 @ModelAttribute 에 대한 보다 심층적인 논의 는 주제에 대한 기사를 참조하십시오 .
먼저 컨트롤러에 메서드를 제공하여 빈을 선언하고 메서드에 @ModelAttribute 어노테이션을 추가합니다 .
@ModelAttribute("todos")
public TodoList todos() {
return new TodoList();
}
다음으로 컨트롤러에 @SessionAttributes 를 사용하여 TodoList 를 세션 범위로 처리하도록 알립니다 .
@Controller
@RequestMapping("/sessionattributes")
@SessionAttributes("todos")
public class TodoControllerWithSessionAttributes {
// ... other methods
}
마지막으로 요청 내에서 bean을 사용하기 위해 @RequestMapping 의 메소드 서명에 참조를 제공합니다 .
@GetMapping("/form")
public String showForm(
Model model,
@ModelAttribute("todos") TodoList todos) {
if (!todos.isEmpty()) {
model.addAttribute("todo", todos.peekLast());
} else {
model.addAttribute("todo", new TodoItem());
}
return "sessionattributesform";
}
@PostMapping 메서드에서 RedirectView를 반환하기 전에 RedirectAttributes를 주입 하고 addFlashAttribute 를 호출 합니다 . 이것은 첫 번째 예와 비교하여 구현의 중요한 차이점입니다.
@PostMapping("/form")
public RedirectView create(
@ModelAttribute TodoItem todo,
@ModelAttribute("todos") TodoList todos,
RedirectAttributes attributes) {
todo.setCreateDate(LocalDateTime.now());
todos.add(todo);
attributes.addFlashAttribute("todos", todos);
return new RedirectView("/sessionattributes/todos.html");
}
Spring은 URL 매개변수의 인코딩을 지원하기 위해 리다이렉트 시나리오에 대한 모델 의 특수한 RedirectAttributes 구현을 사용합니다. 리디렉션 중에 모델 에 저장된 속성 은 일반적으로 URL에 포함된 경우에만 프레임워크에서 사용할 수 있습니다.
addFlashAttribute 를 사용하여 우리는 TodoList가 URL에서 인코딩할 필요 없이 리디렉션에서 살아남기를 원한다고 프레임워크 에 알립니다 .
5.2. 단위 테스트
폼 뷰 컨트롤러 메서드의 단위 테스트는 첫 번째 예제에서 살펴본 테스트와 동일합니다. 그러나 @PostMapping 의 테스트는 동작을 확인하기 위해 플래시 속성에 액세스해야 하기 때문에 약간 다릅니다.
@Test
public void whenTodoExists_thenSubsequentFormRequestContainsesMostRecentTodo() throws Exception {
FlashMap flashMap = mockMvc.perform(post("/sessionattributes/form")
.param("description", "newtodo"))
.andExpect(status().is3xxRedirection())
.andReturn().getFlashMap();
MvcResult result = mockMvc.perform(get("/sessionattributes/form")
.sessionAttrs(flashMap))
.andExpect(status().isOk())
.andExpect(model().attributeExists("todo"))
.andReturn();
TodoItem item = (TodoItem) result.getModelAndView().getModel().get("todo");
assertEquals("newtodo", item.getDescription());
}
5.3. 논의
세션에 속성을 저장하기 위한 @ModelAttribute 및 @SessionAttributes 전략은 추가 컨텍스트 구성이나 Spring 관리 @Bean이 필요하지 않은 간단한 솔루션 입니다 .
첫 번째 예제와 달리 @RequestMapping 메서드 에 TodoList 를 주입해야 합니다 .
또한 리디렉션 시나리오에 대해 플래시 속성을 사용해야 합니다.
6. 결론
이 기사에서는 범위가 지정된 프록시와 @SessionAttributes 를 Spring MVC에서 세션 속성으로 작업하기 위한 두 가지 전략으로 사용하는 방법을 살펴보았습니다. 이 간단한 예에서 세션에 저장된 모든 속성은 세션 수명 동안만 유지됩니다.
서버 재시작 또는 세션 시간 초과 사이에 속성을 유지해야 하는 경우 Spring 세션을 사용하여 정보 저장을 투명하게 처리하는 것을 고려할 수 있습니다. 자세한 내용은 Spring 세션에 대한 기사 를 참조하십시오.
항상 그렇듯이 이 기사에 사용된 모든 코드 는 GitHub 에서 사용할 수 있습니다 .