Contents
1. 쿠키와 세션1️⃣ 쿠키 (Cookie)2️⃣ 세션 (Session)2. 인증(Authentication) vs 권한 체크(Authorization)1️⃣ 🔐 인증 (Authentication)2️⃣ 🛂 권한 체크 (Authorization)3. request / session / cookie에 무엇을 넣나?1️⃣ request2️⃣ session3️⃣ cookie4. 세션 기반 인증의 핵심 구조1️⃣ 왜 세션을 쓰는가?2️⃣ 서버가 여러 대일 때 문제5. 최종 정리 (클라이언트 ↔ 서버 흐름)🔁 로그인 흐름🔁 로그인 이후 요청 (글쓰기 등)브라우저에서 확인하기6. 한 문장 요약1. 쿠키와 세션
1️⃣ 쿠키 (Cookie)
- 저장 위치 : 브라우저(클라이언트)
- 정체 : 그냥 문자열 저장소
- 특징
- 매 요청마다 자동으로 서버에 같이 전송됨
- 보안에 취약 → 민감한 정보 저장 ❌
- 주 용도
- 서버가 발급한 세션 ID(키) 저장
➡️ 쿠키 자체는 인증도 아니고 권한도 아님
➡️ “서버가 나를 식별하기 위한 표식” 정도

2️⃣ 세션 (Session)
- 저장 위치: 서버 메모리 (또는 Redis 같은 서버 저장소)
- 정체:
Map<세션ID, 값>
- 특징
- 클라이언트는 세션 내용을 절대 모름
- 서버만 접근 가능
- 주 용도
- 로그인 상태 유지
- 사용자 정보 저장
➡️ 세션은 “서버 쪽 상태(State)”
2. 인증(Authentication) vs 권한 체크(Authorization)
1️⃣ 🔐 인증 (Authentication)
1) 인증이란?
“너 로그인 한 사람 맞아?”
- 기준 : 세션이 존재하느냐
- 극단적으로 말하면
session.setAttribute("anything", true);이것만 있어도 인증은 됨
📌 인증은 값의 의미는 중요하지 않음
- 세션에 값이 있다 → 인증됨
- 누군지는 몰라도 “로그인된 사용자”임은 알 수 있음
2) 실습
package com.example.boardv1.board;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.example.boardv1.reply.ReplyRequest;
import com.example.boardv1.user.User;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor // final이 적혀있는 애로 생성자 만들어줌
@Controller // @Controller 적어야 리턴값이 파일이 됨 (외부진입점)
public class BoardController {
private final BoardService boardService;
private final HttpSession session;
// title : title=title7&content=content7 (x-www-form)
@PostMapping("/boards/save")
public String save(BoardRequest.SaveOrUpdateDTO reqDTO){ // new해서 넣어줌! 필드가 많을 때 상태만 추가하면 되기 때문에 매우 편함! 재사용 가능!(필드명 잘 적어야 함)
// 인증(v) 권한(x)
// 인증
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null)
throw new RuntimeException("인증되지 않았습니다.");
boardService.게시글쓰기(reqDTO.getTitle(), reqDTO.getContent(), sessionUser.getId());
return "redirect:/";
}
// body : title=제목&content=내용
@PostMapping("/boards/{id}/update")
public String update(@PathVariable("id") int id, BoardRequest.SaveOrUpdateDTO reqDTO){
// 인증(v) 권한(v)
// 인증
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null)
throw new RuntimeException("인증되지 않았습니다.");
boardService.게시글수정(id,reqDTO.getTitle(), reqDTO.getContent(), sessionUser.getId());
return "redirect:/boards/"+id;
}
@GetMapping("/")
public String index(HttpServletRequest req){
List<Board> list = boardService.게시글목록();
req.setAttribute("models", list);
return "index";
}
@GetMapping("/boards/save-form")
public String saveForm(){
// 인증(v) 권한(x)
// 인증
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null)
throw new RuntimeException("인증되지 않았습니다.");
return "board/save-form";
}
@GetMapping("/boards/{id}/update-form")
public String updateForm(@PathVariable("id") int id, HttpServletRequest req){
// 인증(v) 권한(v)
// 인증
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null)
throw new RuntimeException("인증되지 않았습니다.");
Board board = boardService.수정폼게시글정보(id, sessionUser.getId());
req.setAttribute("model", board);
return "board/update-form";
}
@GetMapping("/boards/{id}")
public String detail(@PathVariable("id") int id, HttpServletRequest req){
User sessionUser = (User) session.getAttribute("sessionUser"); // Object타입으로 반환하기 때문에 다운캐스팅해야 함
Integer sessionUserId = sessionUser == null ? null : sessionUser.getId();
BoardResponse.DetailDTO dto = boardService.상세보기(id, sessionUserId);
req.setAttribute("model", dto);
return "board/detail"; // mustache 파일의 경로
}
@PostMapping("/boards/{id}/delete")
public String delete(@PathVariable("id") int id){
// 인증(v) 권한(v)
// 인증
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null)
throw new RuntimeException("인증되지 않았습니다.");
boardService.게시글삭제(id, sessionUser.getId());
return "redirect:/";
}
@GetMapping("/api/boards/{id}")
public @ResponseBody BoardResponse.DetailDTO apiDetail(@PathVariable("id") int id) {
User sessionUser = (User) session.getAttribute("sessionUser");
Integer sessionUserId = sessionUser == null ? null : sessionUser.getId();
BoardResponse.DetailDTO dto = boardService.상세보기(id, sessionUserId);
return dto;
}
}
2️⃣ 🛂 권한 체크 (Authorization)
1) 권한 체크란?
“너 이 작업 해도 돼?”
- 기준 : 세션 안의 정보
- 예 :
session.setAttribute("userId", 3);
session.setAttribute("role", "ADMIN");📌 권한 체크는 세션에 ‘의미 있는 정보’가 있어야 가능
- userId → DB 조회 후 권한 확인 가능
- role → 바로 권한 판단 가능
➡️ 그래서
“아무 값이나 세션에 넣으면 안 된다”는 말이 나오는 것
2) 실습
① 서버에서 차단
package com.example.boardv1.board;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.boardv1.reply.Reply;
import com.example.boardv1.user.User;
import com.example.boardv1.user.UserRepository;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
// 책임 : 트랜잭션 관리 + DTO 만들기 + 권한 체크(DB정보가 필요하기 때문)
@RequiredArgsConstructor
@Service
public class BoardService {
private final BoardRepository boardRepository;
private final EntityManager em;
public List<Board> 게시글목록() {
return boardRepository.findAll();
}
public Board 수정폼게시글정보(int id, int sessionUserId) {
Board board = boardRepository.findById(id)
.orElseThrow(() -> new RuntimeException("게시글을 찾을 수 없어요"));
// 권한
if (sessionUserId != board.getUser().getId())
throw new RuntimeException("수정할 권한이 없습니다");
return board;
}
public BoardResponse.DetailDTO 상세보기(int id, Integer sessionUserId) {
Board board = boardRepository.findById(id)
.orElseThrow(() -> new RuntimeException("게시글을 찾을 수 없어요"));
return new BoardResponse.DetailDTO(board, sessionUserId);
}
@Transactional // update, delete, insert 할때 붙이세요!!
public void 게시글수정(int id, String title, String content, int sessionUserId) {
Board board = boardRepository.findById(id)
.orElseThrow(() -> new RuntimeException("수정할 게시글을 찾을 수 없어요"));
// 권한
if (sessionUserId != board.getUser().getId())
throw new RuntimeException("수정할 권한이 없습니다");
board.setTitle(title);
board.setContent(content);
}
// 원자성(모든게 다되면 commit, 하나라도 실패하면 rollback)
// 트랜잭션 종료시 flush 됨.
@Transactional
public void 게시글쓰기(String title, String content, int sessionUserId) {
User user = em.getReference(User.class, sessionUserId);
// 1. 비영속 객체
Board board = new Board();
board.setTitle(title);
board.setContent(content);
board.setUser(user);
System.out.println("before persist " + board.getId());
// 2. persist
boardRepository.save(board);
System.out.println("after persist " + board.getId());
}
@Transactional
public void 게시글삭제(int id, int sessionUserId) {
// 영속화
Board board = boardRepository.findById(id)
.orElseThrow(() -> new RuntimeException("삭제할 게시글을 찾을 수 없어요"));
// 권한
if (sessionUserId != board.getUser().getId())
throw new RuntimeException("삭제할 권한이 없습니다");
boardRepository.delete(board);
} // 자동 flush
}② 프론트엔드에서 차단(수정/삭제버튼 숨김)
package com.example.boardv1.board;
import lombok.Data;
public class BoardResponse {
@Data
public static class DetailDTO {
// 화면에 보이지 않는 것 (PK는 화면에 안보여도 무조건 적어야 함!)
private int id;
private int userId;
// 화면에 보이는 것
private String title;
private String contetn;
private String username;
// 연산해서 만들어야 되는 것
private boolean isOwner; // 게시글의 주인인가?
public DetailDTO(Board board, Integer sessionUserId) {
this.id = board.getId();
this.userId = board.getUser().getId();
this.title = board.getTitle();
this.contetn = board.getContent();
this.username = board.getUser().getUsername();
this.isOwner = board.getUser().getId() == sessionUserId;
}
}
}{{> header}}
<div class="container p-5">
{{#model.isOwner}}
<!-- 수정삭제버튼 -->
<div class="d-flex justify-content-end">
<a href = "/boards/{{model.id}}/update-form" class="btn btn-secondary me-1">수정</a>
<form action = "/boards/{{model.id}}/delete" method="post">
<button class="btn btn-outline-secondary">삭제</button>
</form>
</div>
{{/model.isOwner}}
<!-- 게시글내용 -->
<div>
<h2><b>{{model.title}}</b></h2>
<hr />
<div class="d-flex justify-content-end">
작성자 : {{model.username}}
</div>
<div class="m-4 p-2">
{{model.content}}
</div>
</div>
<!-- 댓글 -->
<div class="card mt-3">
<!-- 댓글등록 -->
<div class="card-body">
<form action="/replies/save" method="post">
<input type="hidden" name="boardId" value="{{model.id}}">
<textarea id="comment" class="form-control" rows="2" name="comment"></textarea>
<div class="d-flex justify-content-end">
<button class="btn btn-secondary mt-1">
댓글등록
</button>
</div>
</form>
</div>
<!-- 댓글목록 -->
<div class="card-footer">
<b>댓글리스트</b>
</div>
<div class="list-group">
{{#model.replies}}
<!-- 댓글아이템 -->
<div class="list-group-item d-flex justify-content-between align-items-center">
<div class="d-flex">
<div class="px-1 me-1 bg-secondary text-white rounded">{{replyUsername}}</div>
<div>{{comment}}</div>
</div>
{{#isReplyOwner}}
<form action="/replies/{{id}}/delete?boardId={{model.id}}" method="post">
<input type="hidden" name="boardId" value="{{model.id}}">
<button class="btn">🗑</button>
</form>
{{/isReplyOwner}}
</div>
{{/model.replies}}
</div>
</div>
</div>
{{^sessionUser}}
<script>
// 1. 조건 (querySelector 사용, textArea에 comment라는 id 하나 추가하기)
const textArea = document.querySelector("#comment");
// 2. textArea에 마우스로 클릭했을 때의 이벤트 리스너가 필요함
textArea.addEventListener("click", function () {
// 3. 이 때, 행위가 하나 필요함 -> 알림창을 띄워서 '로그인하세요'라고 알려줘야 함
alert("로그인하세요");
});
</script>
{{/sessionUser}}
</body>
</html>3. request / session / cookie에 무엇을 넣나?
1️⃣ request
- 수명: 요청 1번
- 용도
- 컨트롤러 → 서비스 → 뷰로 전달할 데이터
- 특징
- 요청 끝나면 즉시 사라짐
- ❌ 로그인 정보 저장하면 안 됨
2️⃣ session
- 수명: 브라우저 닫거나 만료 전까지
- 넣는 것
- userId
- 사용자 역할(role)
- 로그인 여부
- 목적
- 인증 유지
- 권한 체크
3️⃣ cookie
- 넣는 것
- 세션 ID 하나
- ❌ userId, role, password 절대 금지
4. 세션 기반 인증의 핵심 구조
1️⃣ 왜 세션을 쓰는가?
- HTTP는 무상태(stateless)
- 하지만 로그인은 상태(stateful) 가 필요
→ 세션으로 상태를 서버에 저장
2️⃣ 서버가 여러 대일 때 문제
1) 문제점
서버 A에서 로그인 → 서버 B로 가면 세션 없음 ❌
2) 해결책
- Sticky Session
- 항상 같은 서버로 요청
- 세션 동기화
- 서버끼리 세션 복제
- 공유 세션 저장소
- Redis (가장 일반적)
5. 최종 정리 (클라이언트 ↔ 서버 흐름)
🔁 로그인 흐름
1) 클라이언트
ID / PW 입력 → 로그인 요청2) 서버
ID / PW 검증
세션 생성
세션에 사용자 정보 저장
- userId
- role
세션 ID 발급3) 서버 → 클라이언트
Set-Cookie: JSESSIONID=abc1234) 브라우저
쿠키에 세션 ID 자동 저장
🔁 로그인 이후 요청 (글쓰기 등)
5) 클라이언트
요청 + 쿠키(JSESSIONID)
6) 서버
세션 ID로 세션 조회 ✔ 세션 있으면 → 인증 성공 ✔ 세션 값으로 → 권한 체크
브라우저에서 확인하기




6. 한 문장 요약
- 쿠키: 세션 ID를 들고 다니는 운반책
- 세션: 서버에 저장된 로그인 상태
- 인증: 세션이 있냐 없냐
- 권한 체크: 세션 안에 뭐가 들어 있냐
- request: 잠깐 쓰고 버리는 데이터
Share article