군만두의 IT 공부 일지

[스터디7] 05. 스프링 웹 스코프 본문

프로그래밍/Java

[스터디7] 05. 스프링 웹 스코프

mandus 2025. 5. 15. 09:55

목차

    9장. 스프링 웹 스코프

    이 장에서 다룰 내용
    - 스프링 웹 스코프 사용하기
    - 웹 앱에서 간단한 로그인 기능 구현하기
    - 웹 앱에서 한 페이지에서 다른 페이지로 리디렉션하기

    9.2 스프링 웹 앱에서 세션 스코프 사용

    • 세션 스코프 빈: 스프링에서 관리되는 객체. 스프링이 인스턴스를 생성하고 이를 HTTP 세션에 연결하는 역할을 한다.
    • 클라이언트가 서버에 요청을 보내면 서버는 세션의 전체 기간 동안 이 요청을 위한 메모리 공간을 예약한다.
    • 스프링은 특정 클라이언트에 대해 HTTP 세션이 활성화되어 있는 동안 동일한 클라이언트에서 재사용될 수 있다.
    • 세션 스코프 빈 속성에 저장된 데이터는 HTTP 세션 동안 클라이언트의 모든 요청에 사용할 수 있다.
    • 이 방식을 통해 사용자가 앱의 웹 페이지를 서핑하는 동안 수행하는 작업 정보의 저장이 가능하다.

    세션 스코프 빈과 요청 스코프 빈을 비교하면 다음과 같다.

    • 요청 스코프 빈: 스프링은 매 HTTP 요청마다 새로운 인스턴스를 생성한다.
    • 세션 스코프 빈: 스프링은 HTTP 세션당 하나의 인스턴스만 생성한다. 세션 스코프 빈에 동일한 클라이언트의 여러 요청 사이에 공유할 데이터를 저장할 수 있다.

    ▲ 요청 스코프 빈과 세션 스코프 빈의 차이점


    세션 스코프 빈을 사용하여 구현할 수 있는 기능은 다음과 같다.

    • 로그인: 인증된 사용자가 앱의 여러 부분을 탐색하고 요청 전송하는 동안 그 사용자의 세부 정보를 유지한다.
    • 온라인 쇼핑 장바구니: 사용자가 앱의 여러 곳을 방문하여 장바구니에 추가할 제품을 검색한다. 장바구니는 고객이 추가한 모든 제품을 보관한다.

    세션 스코프 빈의 핵심 관점

    사실 결과 고려 사항 기피 사항
    • 세션 스코프 빈의 인스턴스는 전체 세션 기간 동안 유지된다.
    • 여러 요청이 세션 스코프 빈의 인스턴스를 공유할 있다.
    • 세션 스코프 빈은 데이터를 서버 측에 보관하여 요청 데이터를 공유하는 방법이다.
    • 수명이 길고 요청 스코프 빈보다 가비지 컬렉션 빈도가 낮다.
    • 동일한 클라이언트가 인스턴스의 데이터를 변경하는 요청을 동시에 수행하려는 경쟁 조건 같은 멀티스레딩 관련 문제가 발생할 있다.
    • 구현하는 논리에 따라 요청이 서로 깊숙이 연관될 있다.
    • 앱은 세션 스코프 빈에 저장한 데이터를 오랜 기간 동안 유지한다.
    • 이런 시나리오가 가능하다는 것은 기본적으로 동기화 기술을 사용할 있어서 동시성을 피해야 수도 있다는 의미다. 일반적으로 이런 상황을 피할 있는지 확인하고 파악 후에만 최소한의 수준으로 동기화를 유지하는 것이 좋다.
    • 앱의 메모리에 세부 정보를 상태(stateful)하여 유지하게 하면 클라이언트는 해당 앱의 특정 인스턴스에 종속된다.
      세션 스코프 빈으로 특정 기능을 구현하려고 결정하기 전에 공유하려는 데이터는 세션이 아닌 데이터베이스에 저장하는 방안을 고려하라. 이렇게 하면 HTTP 요청을 서로 독립적으로 유지할 있다.
    • 세션에 너무 많은 데이터를 보관해서는 된다. 이는 잠재적으로 성능 문제를 유발한다. 세션 스코프 속성에는 비밀번호, 개인 키, 기타 사적인 비밀 정보 같은 민감한 정보를 저장하면 된다.

    9.1에서 구현한 애플리케이션을 로그인한 사용자만 액세스할 수 있는 웹 페이지를 표시하도록 변경히려면 다음과 같이 수행해야 한다.

    1. 로그인한 사용자의 상세 정보를 유지할 세션 범위의 빈을 생성한다.
    2. 사용자가 로그인한 후에만 액세스할 수 있는 웹 페이지를 만든다.
    3. 사용자가 먼저 로그인하지 않으면 2.에서 만든 웹 페이지에 액세스할 수 없게 한다.
    4. 인증에 성공하면 사용자를 로그인에서 메인 페이지로 리디렉션한다.
    // 1단계: 로그인 상세 정보를 유지하려고 세션 스코프 빈을 생성한다.
    package com.example.services;
    
    import org.springframework.stereotype.Service;
    import org.springframework.web.context.annotation.SessionScope;
    
    @Service
    @SessionScope
    public class LoggedUserManagementService {
    
      private String username;
    
      public String getUsername() {
        return username;
      }
    
      public void setUsername(String username) {
        this.username = username;
      }
    }
    <!-- 2단계: 로그인했을 때만 액세스할 수 있는 웹 페이지를 만든다. -->
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Login</title>
    </head>
    <body>
        <h1>Welcome, <span th:text="${username}"></span></h1>
        <a href="/main?logout">Log out</a>
    </body>
    </html>
    // 3단계: 먼저 로그인하지 않고 2단계에서 만든 웹 페이지에 액세스할 수 없는지 확인한다.
    package com.example.controllers;
    
    import com.example.services.LoggedUserManagementService;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    
    @Controller
    public class MainController {
    
      private final LoggedUserManagementService loggedUserManagementService;
    
      public MainController(LoggedUserManagementService loggedUserManagementService) {
        this.loggedUserManagementService = loggedUserManagementService;
      }
    
      @GetMapping("/main")
      public String home(
          @RequestParam(required = false) String logout,
          Model model
      ) {
        if (logout != null) {
          loggedUserManagementService.setUsername(null);
        }
    
        String username = loggedUserManagementService.getUsername();
    
        if (username == null) {
          return "redirect:/";
        }
    
        model.addAttribute("username" , username);
        return "main.html";
      }
    }
    // 4단계: 인증을 성공하면 로그인에서 메인 페이지로 리디렉션한다.
    package com.example.controllers;
    
    import com.example.model.LoginProcessor;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    
    @Controller
    public class LoginController {
    
      private final LoginProcessor loginProcessor;
    
      public LoginController(LoginProcessor loginProcessor) {
        this.loginProcessor = loginProcessor;
      }
    
      @GetMapping("/")
      public String loginGet() {
        return "login.html";
      }
    
      @PostMapping("/")
      public String loginPost(
          @RequestParam String username,
          @RequestParam String password,
          Model model
      ) {
        loginProcessor.setUsername(username);
        loginProcessor.setPassword(password);
        boolean loggedIn = loginProcessor.login();
    
        if (loggedIn) {
          return "redirect:/main";
        }
    
        model.addAttribute("message", "Login failed!");
        return "login.html";
      }
    }

    ▲ 두 페이지 사이의 흐름

    9.3 스프링 웹 앱에서 애플리케이션 스코프 사용

    • 애플리케이션 스코프(application scope): 싱글톤 작동 방식과 비슷하다. 컨텍스트에 동일한 타입의 인스턴스가 없고, 웹 스코프(애플리케이션 스코프 포함)의 라이프사이클을 논의할 때 항상 HTTP 요청을 참조 기준점으로 사용한다.
    • 빈의 속성을 불변으로 만들면 싱글톤 빈을 직접 사용할 수 있다.
    • 일반적으로 애플리케이션 빈 대신에 데이터베이스와 같은 영속성(persistence) 계층을 직접 사용하는 편이 좋다.

    ▲ 애플리케이션 스코프 빈

    로그인 시도 횟수를 계산하는 기능을 추가하려면 다음과 같다.

    // 1. 로그인 시도 횟수 세기
    package com.example.services;
    
    import org.springframework.stereotype.Service;
    import org.springframework.web.context.annotation.ApplicationScope;
    
    @Service
    @ApplicationScope
    public class LoginCountService {
    
      private int count;
    
      public void increment() {
        count++;
      }
    
      public int getCount() {
        return count;
      }
    }
    // 2. 모든 로그인 요청에 대한 로그인 횟수 구현하기
    package com.example.model;
    
    import com.example.services.LoggedUserManagementService;
    import com.example.services.LoginCountService;
    import org.springframework.stereotype.Component;
    import org.springframework.web.context.annotation.RequestScope;
    
    @Component
    @RequestScope
    public class LoginProcessor {
    
      private final LoggedUserManagementService loggedUserManagementService;
      private final LoginCountService loginCountService;
    
      private String username;
      private String password;
    
      public LoginProcessor(LoggedUserManagementService loggedUserManagementService, LoginCountService loginCountService) {
        this.loggedUserManagementService = loggedUserManagementService;
        this.loginCountService = loginCountService;
      }
    
      public boolean login() {
        loginCountService.increment();
    
        String username = this.getUsername();
        String password = this.getPassword();
    
        boolean loginResult = false;
        if ("natalie".equals(username) && "password".equals(password)) {
          loginResult = true;
          loggedUserManagementService.setUsername(username);
        }
    
        return loginResult;
      }
    
      public String getUsername() {
        return username;
      }
    
      public void setUsername(String username) {
        this.username = username;
      }
    
      public String getPassword() {
        return password;
      }
    
      public void setPassword(String password) {
        this.password = password;
      }
    }
    // 3. 컨트롤러의 로그인 횟수 값을 메인 페이지에 표시하려고 전송하기
    package com.example.controllers;
    
    import com.example.services.LoggedUserManagementService;
    import com.example.services.LoginCountService;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    
    @Controller
    public class MainController {
    
      private final LoggedUserManagementService loggedUserManagementService;
      private final LoginCountService loginCountService;
    
      public MainController(LoggedUserManagementService loggedUserManagementService, LoginCountService loginCountService) {
        this.loggedUserManagementService = loggedUserManagementService;
        this.loginCountService = loginCountService;
      }
    
      @GetMapping("/main")
      public String home(
          @RequestParam(required = false) String logout,
          Model model
      ) {
        if (logout != null) {
          loggedUserManagementService.setUsername(null);
        }
    
        String username = loggedUserManagementService.getUsername();
        int count = loginCountService.getCount();
    
        if (username == null) {
          return "redirect:/";
        }
    
        model.addAttribute("username" , username);
        model.addAttribute("loginCount", count);
    
        return "main.html";
      }
    }
    <!-- 4. 메인 페이지에 로그인 횟수 표시하기 -->
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Login</title>
    </head>
    <body>
        <h1>Welcome, <span th:text="${username}"></span>!</h1>
        <h2>Your login number is <span th:text="${loginCount}"></span></h2>
        <a href="/main?logout">Log out</a>
    </body>
    </html>

    ▲ 모든 사용자 로그인의 총 횟수 표시

     

    이 글은 『스프링 교과서』 책을 학습한 내용을 정리한 것입니다.
    Comments