최원종의 개발 블로그

V3-5 로그인& 로그아웃(세션 기반 사용자 인증) 본문

Spring boot 입문

V3-5 로그인& 로그아웃(세션 기반 사용자 인증)

chl6698 2026. 5. 12. 17:43

로그인화면


DispatcherServlet

DispatcherServlet은 스프링 MVC 프레임워크의 핵심 구성 요소
	
스프링 부트 웹 애플리케이션은 내장형 서블릿 컨테이너를 사용합니다. 
스프링 부트 애플리케이션 시작 시, 내장형 서블릿 컨테이너는 DispatcherServlet의 단일
인스턴스를 생성합니다. 이 DispatcherServlet 인스턴스는 웹 애플리케이션의 
모든 HTTP 요청을 처리하게 됩니다.
DispatcherServlet은 웹 애플리케이션 컨텍스트(WebApplicationContext)와 연결되어
있습니다. 웹 애플리케이션 컨텍스트는 웹 관련 설정과 빈들을 포함합니다.
예를 들어, 웹 요청을 처리하는 컨트롤러(Controller), 서비스(Service), 데이터베이스
상호작용 등과 관련된 빈들이 여기에 포함될 수 있습니다.
따라서, HTTP 요청이 들어오면, DispatcherServlet은 웹 애플리케이션 컨텍스트에서 적절한
빈을 찾아 해당 요청을 처리합니다. 따라서 DispatcherServlet은 웹 요청의 시작점과 끝점,
즉 요청의 수신부터 응답의 전송까지 전체 웹 요청/응답 생명주기를 관리하는 중심적인 역할
을 합니다.

 


DispatcherServlet 구조

 +------------------+
 |     Client       |
 +------------------+
         |
         v
 +------------------+
 |    Apache Web    |
 |      Server      |
 +------------------+
         |
         v (동적 컨텐츠 생성시 동작)
 +------------------+
 |      Tomcat      | WAS 
 |   (Servlet       | 
 |   Container)     | 
 |  *************   | 
 |  *세션 스토리지*  | 
 |  +------------+  |  - 스프링 컨테이너 기반 애플리케이션 - 
 |  |   Filter   |  |  HttpServletRequest, HttpServletResponse 생성하고       
 |  +------------+  |  DispatcherServlet의 service 메서드가 호출됩니다
 |                  |
 |                  | * 동시에 여러 요청이 오면 서블릿 컨테이너의 멀티스레딩 능력 *   
 |                  |   HTTP 요청을 처리하기 위해 스레드를 생성하거나 스레드 풀에서 
 | 		              |   스레드를 할당하는 역할     
 |                  | 
 +-----------|------+     
             |   
             v                  
 +----------------------------+  - 스프링 프레임워크의 핵심 부분 - IoC, DI, AOP ...
 |     Spring                 |  Spring Container: 스프링 프레임워크의 핵심 부분으로 빈(Bean)의 생명 
 |     Container              |  주기를 관리하며 스프링 기반 애플리케이션의 구동 환경입니다.
 |                            |  
 |  +---------------------+   |  
 |  |  DispatcherServlet  |   |  
 |  +---------------------+   |  
 |           |                |  
 |           v                |  
 |  +------------------+      |
 |  |   Interceptor    |      | *추상화된 API 와 인터페이스들을 통해 세션 데이터에 접근*
 |  +------------------+      |
 |  PreHandle                 |
 |           |                |
 |           v                |
 |  +------------------+      |
 |  |    Controller    |      |
 |  +------------------+      |
 |   AOP    |                 |
 |   Advice |                 | 
 |          |                 |
 |          v                 |
 |  Interceptor               |
 |  PostHandle                |
 |          |                 |
 |          v                 |
 |  Interceptor               |
 |  AfterCompletion           |
 |                            |
 |                            |
 +-----------|----------------+
             |
             v
	 +------------------+
	 |      Tomcat      |
	 |   (Servlet       |
	 |   Container)     |
	 |                  |
	 |                  |
	 |   +------------+ |
	 |   |   Filter   | |
	 |   +------------+ |
	 |         |        |
	 |         v        |
	 +------------------+
		 |
                 v
	 +------------------+
	 |     Response     |
	 +------------------+
	          |
		  v
 	 +------------------+
	 |    Apache Web    |
	 |      Server      |
	 +------------------+
	          |
		  v
	 +------------------+
	 |      Client      |
	 +------------------+

application-dev.yml 파일

더보기
server:
  servlet:
    encoding:
      charset: utf-8
      force: true
  port: 8080

logging:
  level:
    root: INFO #모든 라이브러리는 INFO 이상만 출력
    com.tenco: DEBUG # 내 프로젝트는 DEBUG 이상 모두 출력

spring:
  mustache:
    servlet:
      # 머스태치 템플릿 엔진에서 request 객체와 세션 객체에 접근할 수 있도록 허용하는 설정 추가
      expose-session-attributes: true
      expose-request-attributes: true


  #데이터베이스 연결 설정 (MySQL)
  datasource:
#    driver-class-name: com.mysql.cj.jdbc.Driver
#    url: jdbc:mysql://localhost:3306/myblog?serverTimezone=Asia/Seoul
#    username: root
#    password: root
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:test
    username: sa
    password:
  h2:
    console:
      enabled: true

#      초기 데이터 설정
  sql:
    init:
      data-locations:
        - classpath:db/data.sql

  jpa:
    hibernate:
      # create 애플리케이션 시작시 테이블 새로 생성
      # 기존 데이터는 모드 삭제됨 (개발용)
      ddl-auto: create
    #SQL 쿼리를 콘솔에 출력 (개발용)
    show-sql: true
    properties:
      hibernate:
        # SQL 쿼리를 보기 좋게 포맷팅
        format_sql: true
    # data.sql 파일을 하이버네티트 초기화 이후에 실행
    defer-datasource-initialization: true

Userrequest 코드

 

주요 구성요소 요약

1. 로그인 DTO (LoginDTO)
역할: 로그인 시 필요한 username과 password만받음.

유효성 검사 (validate):
아이디나 비밀번호가 비어있는 상태로 DB를 조회하는 리소스 낭비를 입구에서 차단.
공백 제거(trim()) 처리를 통해 단순 스페이스 입력 방지.

2. 회원가입 DTO (JoinDTO)
역할: 회원 정보(ID, PW, Email)를 수집하고 엔티티로 변환.

비즈니스 로직 연계 (toEntity):
수집된 데이터를 바탕으로 User 엔티티 객체를 생성.
빌더 패턴을 활용하여 데이터의 순서나 개수에 구애받지 않고 안전하게 엔티티를 빌드.

형식 검사: 이메일에 @ 포함 여부를 체크하여 기본적인 데이터 품질을 보장.

package com.tenco.blog.user;

import lombok.Data;

public class UserRequest {

    // 로그인 DTO
    @Data
    public static class LoginDTO {
        private String username;
        private String password;

        // 유효성 검사
        public void validate() {
            if(username == null || username.trim().isEmpty()) {
                throw new IllegalArgumentException("사용자명을 입력하세요");
            }
            
            if(password == null || password.trim().isEmpty()) {
                throw new IllegalArgumentException("비밀번호를 입력하세요");
            }
        }
    }

    // 회원가입 DTO
    @Data
    public static class JoinDTO {
        private String username;
        private String password;
        private String email;

        // 편의 기능 추가 - 내가 가지고 있는 멤버 변수에 값으로 User 엔티를 생성
        public User toEntity() {
            return User.builder()
                    .username(username)
                    .password(password)
                    .email(email)
                    .build();
        }

        // 유효성 검사 메서드 만들기
        public void validate() {
            if (username == null || username.trim().isEmpty()) {
                throw new IllegalArgumentException("사용자명은 필수 입니다");
            }

            if(password == null || password.trim().isEmpty()) {
                throw new IllegalArgumentException("비밀번호는 필수 입니다");
            }

            if(email == null || email.trim().isEmpty()) {
                throw new IllegalArgumentException("이메을은 필수 입니다");
            }
            // 입력값 : abc@naver.com --> contains() -->   true   --> ! --> false
            if(email.contains("@") == false) {
                throw new IllegalArgumentException("올바른 이메일 형식이 아닙니다");
            }

        }

    }

}

templates/layout/header.mustache 파일

 

흐름 간단 요약

1. 로그인 전 (sessionUser가 null일 때):
{{^sessionUser}} 영역이 활성화되어 화면에 회원가입과 로그인 링크가 나타남.

2. 로그인 후 (sessionUser에 값이 있을 때):
{{#sessionUser}} 영역이 활성화되어 사용자의 세션 정보가 감지됨.
화면에는 글쓰기, 회원정보보기, 로그아웃 링크가 나타남.

3. 반응형 디자인: navbar-toggler와 collapse 클래스를 사용하여 공간 절약.
더보기
<!DOCTYPE html>
<html lang="en">

<head>
    <title>Blog</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"/>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
</head>

<body class="d-flex flex-column min-vh-100">
<nav class="navbar navbar-expand-sm bg-dark navbar-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="/">Tencoding</a>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#collapsibleNavbar">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="collapsibleNavbar">
            <ul class="navbar-nav">
                {{#sessionUser}}
                    <li class="nav-item">
                        <a class="nav-link" href="/board/save-form">글쓰기</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/user/update-form">회원정보보기</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/logout">로그아웃</a>
                    </li>
                {{/sessionUser}}
                {{^sessionUser}}
                    <li class="nav-item">
                        <a class="nav-link" href="/join-form">회원가입</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/login-form">로그인</a>
                    </li>
                {{/sessionUser}}
            </ul>
        </div>
    </div>
</nav>