v4
★ MILESTONE · BOARD

깔끔한 로그인

인터셉터 + 공통 레이아웃

학습 목표

  • HandlerInterceptor 로 로그인 가드 자동화
  • JSP include 로 공통 헤더·푸터 분리
  • Controller 마다 박혀있던 if(session) 코드를 제거
  • v3 와 v4 의 코드 차이를 비교한다

⚠️ v3 의 답답함

컨트롤러마다 반복되는 코드

@PostMapping("/board/write")
public String write(...) {
    if (session.getAttribute("loginUser") == null) return "redirect:/login";
    ... 작성 처리 ...
}

@PostMapping("/board/update")
public String update(...) {
    if (session.getAttribute("loginUser") == null) return "redirect:/login";
    ... 수정 처리 ...
}

@PostMapping("/board/delete")
public String delete(...) {
    if (session.getAttribute("loginUser") == null) return "redirect:/login";
    ... 삭제 처리 ...
}

같은 코드가 컨트롤러마다 반복. 5 곳에 있다면 5 번, 10 곳에 있다면 10 번. 한 곳 빠뜨리면 보안 구멍.

⚠️ 그리고 화면 표시 — 「OO님 환영합니다」

아이디 「hong123」 을 그대로 노출하긴 어색

로그인 후 헤더에 「hong123 님 환영합니다」 — 아이디를 그대로 노출. 표시용 닉네임이 필요해진 시점.

→ Member 에 nick 필드 추가 + 테이블 ALTER ADD COLUMN.

① 스키마 마이그레이션 — nick 추가


-- v3 → v4: 표시용 닉네임 컬럼 추가
ALTER TABLE mymember ADD COLUMN nick VARCHAR(30) AFTER pwd;

package com.smhrd.domain;

@Data @AllArgsConstructor @NoArgsConstructor
public class Member {
    private String id;
    private String pwd;
    private String nick;     // ← 추가
}

👉 회원가입 폼에도 <input name="nick"> 한 줄 추가. 폼의 name 만 맞으면 백엔드는 자동.

🛠️ HandlerInterceptor

컨트롤러 진입 전 가드

요청이 컨트롤러 메서드에 도달하기 전에 끼어들어서, 비로그인 사용자를 차단하는 부품. 「식당 입구의 가드」 비유.

v3 (인터셉터 없음) ────────────────── 요청 → DispatcherServlet → Controller (안에서 직접 체크) v4 (인터셉터 있음) ────────────────── 요청 → DispatcherServlet → 🛡️ Interceptor → Controller ↓ false 반환 시 /login 으로 리다이렉트 Controller 호출 안 됨

② LoginInterceptor 작성


package com.smhrd.interceptor;

import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.*;

public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                              HttpServletResponse response,
                              Object handler) throws Exception {
        HttpSession session = request.getSession();

        if (session.getAttribute("loginUser") == null) {
            // 비로그인 → 로그인 페이지로
            response.sendRedirect(request.getContextPath() + "/login");
            return false;   // ⭐ Controller 호출 안 함
        }

        return true;        // ⭐ 통과 — Controller 호출
    }
}

② preHandle 의 핵심

  • preHandle = 컨트롤러 호출 「전」 실행
  • return true → 컨트롤러 호출
  • return false → 컨트롤러 호출 안 함 (가드 차단)
  • false 반환 시 응답을 직접 만들어줘야 함 (sendRedirect 등)

👉 다른 메서드도 있음 (postHandle, afterCompletion) 이지만 인증 가드엔 preHandle 만으로 충분.

③ 인터셉터 등록 (XML 방식)


<!-- servlet-context.xml -->
<beans xmlns:mvc="http://www.springframework.org/schema/mvc" ...>

    <mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/board/write"/>
            <mvc:mapping path="/board/update"/>
            <mvc:mapping path="/board/delete"/>
            <mvc:mapping path="/mypage/**"/>

            <!-- 예외 -->
            <mvc:exclude-mapping path="/login"/>
            <mvc:exclude-mapping path="/signup"/>

            <beans:bean class="com.smhrd.interceptor.LoginInterceptor"/>
        </mvc:interceptor>
    </mvc:interceptors>

</beans>

③ 매핑 패턴

패턴의미
/board/write정확히 이 URL
/board/*/board/ 직속 한 단계
/mypage/**/mypage/ 하위 모든 깊이
/api/boards/*/comments중간 한 단계 와일드카드

④ Controller — v3 직접 체크


@PostMapping("/board/write")
public String write(Board b, HttpSession session) {

    if (session.getAttribute("loginUser") == null) {
        return "redirect:/login";   // ← 가드 코드
    }

    Member u = (Member) session.getAttribute("loginUser");
    b.setWriter(u.getId());
    service.insert(b);
    return "redirect:/board/list";
}

👉 글쓰기·수정·삭제·마이페이지마다 같은 if 가 반복됩니다.

④ Controller — v4 인터셉터가 가드


@PostMapping("/board/write")
public String write(Board b, HttpSession session) {

    // if 체크 사라짐 — 인터셉터가 보장
    Member u = (Member) session.getAttribute("loginUser");
    b.setWriter(u.getId());
    service.insert(b);
    return "redirect:/board/list";
}

👉 가드 코드가 사라졌습니다. 컨트롤러는 「업무 그 자체」만 표현.

요청이 거치는 경로

비로그인 사용자가 /board/write 요청 ↓ DispatcherServlet ↓ 🛡️ LoginInterceptor.preHandle() ├─ session.getAttribute("loginUser") == null ├─ response.sendRedirect("/login") └─ return false ← 여기서 끝 ↓ (여기 도달 안 함) ❌ BoardController.write() — 호출 안 됨 ──────────────────────────── 로그인 사용자가 /board/write 요청 ↓ DispatcherServlet ↓ 🛡️ LoginInterceptor.preHandle() └─ return true ↓ ✅ BoardController.write() 정상 실행

⑤ 공통 레이아웃 — 헤더 분리


<!-- /WEB-INF/views/common/header.jsp -->
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<header>
    <a href="/">홈</a>
    <a href="/board/list">게시판</a>

    <c:choose>
        <c:when test="${not empty sessionScope.loginUser}">
            <span>${sessionScope.loginUser.nick}님</span>
            <a href="/logout">로그아웃</a>
            <a href="/mypage">마이페이지</a>
        </c:when>
        <c:otherwise>
            <a href="/login">로그인</a>
            <a href="/signup">회원가입</a>
        </c:otherwise>
    </c:choose>
</header>

⑤ 각 페이지에서 include


<!-- /WEB-INF/views/board/list.jsp -->
<%@ page contentType="text/html; charset=UTF-8" %>
<html>
<body>

<%@ include file="/WEB-INF/views/common/header.jsp" %>

<h1>게시판</h1>
<table>...</table>

<%@ include file="/WEB-INF/views/common/footer.jsp" %>

</body>
</html>

👉 모든 JSP 가 같은 헤더를 공유. 헤더 수정 시 한 곳만 고치면 모든 페이지가 즉시 반영.

두 가지 include — <%@ include vs

방식<%@ include %><jsp:include>
처리 시점컴파일 타임런타임
변수 공유가능불가
성능빠름약간 느림
수정 반영전체 재컴파일즉시
쓰임정적 영역 (헤더/푸터)동적 영역

👉 본 과정의 헤더/푸터는 <%@ include %> 로 충분.

v3 → v4 효과

측면v3v4
로그인 가드 코드각 컨트롤러에 반복인터셉터 한 곳
URL 추가 시가드 코드도 추가 (잊으면 보안 구멍)매핑 한 줄만
화면 헤더각 JSP 에 반복한 파일
로그인 사용자 표시없음모든 페이지에 자동

실험 — 인터셉터 동작 확인

  1. 로그아웃 상태에서 /board/write 직접 접속 → 자동으로 /login 으로 이동
  2. 로그인 후 같은 URL → 정상 표시
  3. 로그아웃 후 다시 → 또 차단
  4. F12 Network 탭에서 — 차단 시 응답 Status 가 302, Location 헤더에 /login

인터셉터의 다른 활용

  • 접근 로그 — 모든 요청을 콘솔에 기록
  • 실행 시간 측정 — preHandle 에서 시작 시간, postHandle 에서 종료
  • 다국어 처리 — Accept-Language 헤더 보고 로케일 설정
  • CSRF 토큰 검증 — POST 요청에 토큰 검증

👉 「공통 처리」가 필요한 모든 곳에 인터셉터.

v4 의 의미

v4 는 「코드 정돈」의 차시입니다. 새 기능 추가는 없지만:

  • 중복 코드 제거
  • 보안 일관성 보장
  • 각 컨트롤러가 본질적 로직에만 집중

동작은 같지만 코드는 훨씬 깔끔. 실무에서 매일 보는 코드 품질의 단계.

🔄 Before / After

v3 끝

인증은 동작한다. 하지만 컨트롤러마다 가드 코드가 반복. 헤더에 로그인 표시 없음.

v4 끝

인터셉터 한 곳이 모든 가드. 헤더 한 곳이 모든 페이지의 로그인 상태 표시. 코드 깔끔.

이번 차시의 데이터 흐름

요청
Interceptor
preHandle
Controller
Service
DB
Controller 앞에 「가드」 박스 — 모든 요청이 통과해야 함

정리

오늘 들고 가는 것

  • HandlerInterceptor.preHandle 로 가드 자동화
  • servlet-context.xml 의 <mvc:interceptors> 등록
  • JSP <%@ include %> 로 공통 레이아웃
  • 각 페이지 헤더에서 sessionScope 로 로그인 상태 표시
  • v4 — 코드 품질의 단계

다음: v5 최소 게시판 — Create + Read 만 (인가 없음).