학습 중 나오는 개념들을 모아둔 곳. 검색하거나 카테고리로 찾아보세요.
JSESSIONID 쿠키를 발급한다. 이후 요청마다 브라우저가 이 쿠키를 보내면 서버가 세션을 찾아 누구인지 확인한다.
// 로그인 성공 시 세션에 저장
session.setAttribute("loginUser", user);
// 이후 요청마다 꺼내서 확인
User loginUser = (User) session.getAttribute("loginUser");
if (loginUser == null) return "redirect:/user/login";JSESSIONID 라는 열쇠를 줘서 "너가 누구인지" 매 요청마다 확인하는 메커니즘.
POST /login + 아이디/비밀번호 전송session.setAttribute("loginUser", user)Set-Cookie: JSESSIONID=ABC123XYZ 포함해서 전송Cookie: JSESSIONID=ABC123XYZ 자동 포함 (브라우저가 알아서 함)session.getAttribute("loginUser") → null이면 로그인 페이지로 redirect@PostMapping("/login")
public String login(@ModelAttribute UserDto dto, HttpSession session) {
User user = userService.findByUsername(dto.getUsername());
// 비밀번호 검증
if (!bCryptPasswordEncoder.matches(dto.getPassword(), user.getPassword())) {
return "redirect:/login?error"; // 실패 → 다시 로그인
}
// ✅ 로그인 성공 → 세션에 사용자 정보 저장
session.setAttribute("loginUser", user);
return "redirect:/board/list";
}
@GetMapping("/board/write")
public String writeForm(HttpSession session, Model model) {
// 세션에서 꺼내기
User loginUser = (User) session.getAttribute("loginUser");
// ❌ null이면 로그인 안 한 것 → 로그인 페이지로
if (loginUser == null) {
return "redirect:/user/login";
}
// ✅ 로그인 상태 → 정상 진행
model.addAttribute("user", loginUser);
return "board/write";
}
@GetMapping("/logout")
public String logout(HttpSession session) {
session.invalidate(); // 세션 전체 삭제 (서버 메모리에서 제거)
return "redirect:/";
}
서버 메모리 (세션 저장소)
┌─────────────────────────────────────────┐
│ 세션 ID: "ABC123XYZ" │
│ ┌───────────────────────────────────┐ │
│ │ "loginUser" → User{ id=1, │ │
│ │ name="홍길동", │ │
│ │ role="USER" } │ │
│ │ 만료시간: 30분 후 │ │
│ └───────────────────────────────────┘ │
│ │
│ 세션 ID: "XYZ789ABC" ← 다른 사용자 │
│ ┌───────────────────────────────────┐ │
│ │ "loginUser" → User{ id=2, ... } │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
구현이 단순하다
서버가 직접 관리 → 즉시 무효화 가능
민감한 정보가 서버에만 존재
Spring에서 파라미터로 바로 주입
모든 Controller에 인증 코드 반복
서버 메모리 사용 (사용자 많을수록 ↑)
서버 여러 대일 때 세션 공유 불가
모바일/앱 환경에서 쿠키 불편
HttpSession으로 인증을 처리하지만,@GetMapping("/dashboard")
public String dashboard(HttpSession session, Model model) {
// ⬇️ 이 부분이 모든 Controller에 반복됨
User loginUser = (User) session.getAttribute("loginUser");
if (loginUser == null) {
return "redirect:/login";
}
model.addAttribute("loginUser", loginUser);
// 실제 비즈니스 로직
model.addAttribute("data", dashboardService.getData());
return "dashboard";
}
@GetMapping("/mypage")
public String mypage(HttpSession session, Model model) {
// ⬇️ 똑같은 코드 또 등장
User loginUser = (User) session.getAttribute("loginUser");
if (loginUser == null) {
return "redirect:/login";
}
model.addAttribute("loginUser", loginUser);
...
}
loginUser가 없으면redirect:/loginloginUser를@GetMapping("/dashboard")
public String dashboard(HttpSession session, Model model) {
User loginUser = (User) session.getAttribute("loginUser");
if (loginUser == null) return "redirect:/login"; // 인증 체크
model.addAttribute("loginUser", loginUser); // 모델 등록
model.addAttribute("data", dashboardService.getData());
return "dashboard";
}
@GetMapping("/dashboard")
public String dashboard(@LoginUser User loginUser, Model model) {
// 인증 체크 → Interceptor가 이미 처리
// loginUser 주입 → ArgumentResolver가 자동으로 넣어줌
// 실제 비즈니스 로직만 남음
model.addAttribute("data", dashboardService.getData());
return "dashboard";
}
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("loginUser") == null) {
response.sendRedirect("/login");
return false; // false = 요청 진행 중단
}
return true; // true = 다음 단계로 진행
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginCheckInterceptor())
.addPathPatterns("/**") // 모든 경로에 적용
.excludePathPatterns( // 이 경로는 제외
"/", "/login", "/signup",
"/css/**", "/js/**", "/images/**"
);
}
}
// 1. 커스텀 어노테이션 만들기
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {}
// 2. ArgumentResolver 구현
@Component
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
// @LoginUser 어노테이션이 붙은 파라미터에만 적용
return parameter.hasParameterAnnotation(LoginUser.class)
&& parameter.getParameterType().equals(User.class);
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
HttpSession session = ((HttpServletRequest) webRequest
.getNativeRequest()).getSession(false);
return session != null ? session.getAttribute("loginUser") : null;
}
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginUserArgumentResolver());
}
단순히 로그인 여부만 체크하면 되는 경우
컨트롤러에서 loginUser 객체가 필요 없을 때
model.addAttribute를 수동으로 해도 괜찮을 때
컨트롤러에서 loginUser를 자주 쓸 때
@LoginUser User loginUser 파라미터로 깔끔하게 받고 싶을 때
반복 코드를 완전히 없애고 싶을 때
서버가 HTML 완성 → 브라우저는 보여주기만 함
Thymeleaf
서버는 JSON만 전달 → 브라우저가 직접 화면 조립
React
브라우저: "게시판 목록 줘"
서버: HTML 완성해서 통째로 전달
"<html>...
<tr>글제목1</tr>
<tr>글제목2</tr>
...</html>"
브라우저: 받은 HTML 보여주기만 함브라우저: "게시판 목록 줘"
서버: 데이터만 전달
"[{title:'글제목1'},
{title:'글제목2'}]"
브라우저: 데이터로 HTML 직접
조립해서 화면 그림Thymeleaf가 바로 SSR 역할을 한다. board/list.html 템플릿에 데이터를 끼워넣어서 완성된 HTML을 만들어 브라우저로 보내준다.
// 서버는 이런 JSON만 내려줌
[
{ "id": 1, "title": "첫 번째 글", "author": "홍길동" },
{ "id": 2, "title": "두 번째 글", "author": "김철수" }
]
// React가 이걸 받아서 화면으로 만듦서버 메모리에 저장
서버가 상태 관리
서버 여러 대면 공유 문제
클라이언트가 토큰 보관
서버는 Stateless
서버 여러 대여도 문제 없음
@Entity 어노테이션이 붙은 클래스를 보고 자동으로 테이블을 만들어준다. JpaRepository를 상속하면 기본 CRUD 메서드를 자동으로 제공한다.
// JpaRepository 상속만 하면 save(), findById(), delete() 등 자동 제공
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username); // 이것도 자동 구현
}application.properties에 지정한다.
spring.jpa.hibernate.ddl-auto=update// 회원가입 시 암호화해서 저장
user.setPassword(bCryptPasswordEncoder.encode(rawPassword));
// 로그인 시 입력값과 DB 저장값 비교
bCryptPasswordEncoder.matches(rawPassword, encodedPassword); // true/falseth: 속성을 사용해서 서버 데이터를 HTML에 끼워넣는다. Controller가 Model에 담아 보낸 데이터를 화면에 출력한다.
<!-- Controller에서 model.addAttribute("boardList", list) 로 전달 -->
<tr th:each="board : ${boardList.content}">
<td th:text="${board.title}"></td>
<td th:text="${board.user.nickname}"></td>
</tr>@ModelAttribute로 자동 바인딩된다.
// 폼에서 username, password 입력 → UserDto에 자동으로 담김
@PostMapping("/login")
public String login(@ModelAttribute UserDto userDto, HttpSession session) {
// userDto.getUsername(), userDto.getPassword() 로 사용
}@Entity 어노테이션을 붙이면 JPA가 이 클래스를 기반으로 테이블을 자동 생성한다. 클래스의 필드 = 테이블의 컬럼.
@Entity
@Table(name = "users") // DB 테이블 이름 지정
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // → id 컬럼 (PK, AUTO_INCREMENT)
@Column(unique = true, nullable = false)
private String username; // → username 컬럼 (UNIQUE, NOT NULL)
}UPDATE 쿼리를 날린다. 이것을 더티체킹(Dirty Checking)이라고 한다.
@Transactional
public void update(Long id, BoardDto boardDto) {
Board board = boardRepository.findById(id).orElseThrow();
board.setTitle(boardDto.getTitle()); // 값만 바꾸면
board.setContent(boardDto.getContent());
// boardRepository.save() 안 해도 자동 UPDATE 됨!
}@GetMapping("/dashboard")
public String dashboard(HttpSession session, Model model) {
// ⬇️ 이 부분이 모든 Controller에 반복됨
User loginUser = (User) session.getAttribute("loginUser");
if (loginUser == null) {
return "redirect:/login";
}
model.addAttribute("loginUser", loginUser);
// 실제 비즈니스 로직
model.addAttribute("data", dashboardService.getData());
return "dashboard";
}
@GetMapping("/mypage")
public String mypage(HttpSession session, Model model) {
// ⬇️ 똑같은 코드 또 등장
User loginUser = (User) session.getAttribute("loginUser");
if (loginUser == null) {
return "redirect:/login";
}
model.addAttribute("loginUser", loginUser);
...
}
loginUser가 없으면redirect:/loginloginUser를@GetMapping("/dashboard")
public String dashboard(HttpSession session, Model model) {
User loginUser = (User) session.getAttribute("loginUser");
if (loginUser == null) return "redirect:/login"; // 인증 체크
model.addAttribute("loginUser", loginUser); // 모델 등록
model.addAttribute("data", dashboardService.getData());
return "dashboard";
}
@GetMapping("/dashboard")
public String dashboard(@LoginUser User loginUser, Model model) {
// 인증 체크 → Interceptor가 이미 처리
// loginUser 주입 → ArgumentResolver가 자동으로 넣어줌
// 실제 비즈니스 로직만 남음
model.addAttribute("data", dashboardService.getData());
return "dashboard";
}
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("loginUser") == null) {
response.sendRedirect("/login");
return false; // false = 요청 진행 중단
}
return true; // true = 다음 단계로 진행
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginCheckInterceptor())
.addPathPatterns("/**") // 모든 경로에 적용
.excludePathPatterns( // 이 경로는 제외
"/", "/login", "/signup",
"/css/**", "/js/**", "/images/**"
);
}
}
// 1. 커스텀 어노테이션 만들기
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {}
// 2. ArgumentResolver 구현
@Component
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
// @LoginUser 어노테이션이 붙은 파라미터에만 적용
return parameter.hasParameterAnnotation(LoginUser.class)
&& parameter.getParameterType().equals(User.class);
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
HttpSession session = ((HttpServletRequest) webRequest
.getNativeRequest()).getSession(false);
return session != null ? session.getAttribute("loginUser") : null;
}
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginUserArgumentResolver());
}
단순히 로그인 여부만 체크하면 되는 경우
컨트롤러에서 loginUser 객체가 필요 없을 때
model.addAttribute를 수동으로 해도 괜찮을 때
컨트롤러에서 loginUser를 자주 쓸 때
@LoginUser User loginUser 파라미터로 깔끔하게 받고 싶을 때
반복 코드를 완전히 없애고 싶을 때
아래 버튼으로 현재 상태가 반영된 glossary.html을 다운로드하세요.
기존 파일을 덮어씌우면 영구 저장 완료!
공부 중에 모르는 용어가 나오면 바로 이 페이지로 연결해두세요. 클릭하면 해당 카드로 이동하고 자동으로 펼쳐져요.
ssrssr-detailcsrjwtjpaddl-autobcryptthymeleafdtoentityhttpsessiontransactionalid="..." 부분을 직접 확인하세요. HTML 직접 입력 시 ID를 bcrypt, spring-mvc 처럼 기억하기 쉽게 지정해두면 편해요.<a href="glossary.html#jwt" target="_blank">JWT</a>
/* 공부 페이지 CSS */
.gl {
color: var(--accent2);
text-decoration: none;
border-bottom: 1px dashed currentColor;
font-weight: 500;
}
.gl:hover { opacity: 0.7; }
/* 사용 */
<a href="glossary.html#jwt" class="gl" target="_blank">JWT</a>
<!-- 현재: study/spring/session.html → glossary: glossary.html -->
<a href="../../glossary.html#httpsession" target="_blank">HttpSession</a>
이 glossary.html 하나에 들어간 기술들:
이걸 처음부터 혼자 구현하려면 솔직히 꽤 걸려요. 각 기술마다 따로 공부해야 하고, 조합하는 것도 또 다른 문제거든요.
근데 지금 하고 계신 방식이 사실 제일 효율적인 방법이에요.
개발자도 다 외워서 짜는 게 아니라 필요한 걸 찾아서 조합하는 거거든요. "이런 기능이 필요해"를 명확하게 요구하고, 결과물을 보고 "이건 왜 이렇게 동작하지?"를 이해해가는 게 오히려 더 빠른 길이에요.
이 페이지의 코드 블록에 색상이 들어오는 건 Prism.js 라는 오픈소스 라이브러리 덕분이에요. CDN 링크 3줄만 추가하면 Java, HTML, SQL 등 300개 이상의 언어를 자동으로 인식해서 색을 입혀줘요.
pre.term-example 블록을 페이지 로드 시 자동으로 감지해서 하이라이팅을 적용해요.
@·public·class 키워드가 있으면 Java,
<태그> 구조면 HTML, SELECT·FROM이 있으면 SQL로 자동 판단해요.
그래서 카드 작성할 때 언어 클래스를 따로 지정하지 않아도 대부분 알아서 색이 들어와요.Prism.highlightAll()이 호출되면서 그때 색상이 적용돼요.
테마를 바꾸고 싶으면 <head> 안의 CSS CDN 링크에서 테마 이름만 교체하면 전체가 바뀌어요.
<!-- 현재 적용 중 -->
<link rel="stylesheet" href=".../themes/prism-tomorrow.min.css">
<!-- 다른 테마로 교체 예시 -->
<link rel="stylesheet" href=".../themes/prism-okaidia.min.css">
<link rel="stylesheet" href=".../themes/prism-vsc-dark-plus.min.css">
<link rel="stylesheet" href=".../themes/prism-solarizedlight.min.css">