이론 참고
[Spring 동작 원리]
- Apach Tomcat은 HTTP 요청(헤더, 바디)을 받아
HttpServletRequest객체를 생성해서 헤더와 바디를 담고,HttpServletResponse객체를 생성해 Spring DispatcherServlet, Session으로 전달 - Session은 싱글턴으로 관리됨 (한 번 생성된걸로 재사용)
- Spring IoC 컨테이너는 싱글턴으로 관리되는 Bean과 의존성을 주입(DI)하며 애플리케이션의 객체를 관리
- 어노테이션이 붙은 클래스, @Bean 메소드, Session 담고 있음
HttpServletRequest,HttpServletResponse객체는 Bean으로 관리하지 않아서 의존성 주입 받을 수 없지만, 메소드 파라미터에서 사용할 수 있도록 Spring이 지원
- DispatcherServlet은 HTTP 요청을 처리하고 적절한 컨트롤러와 뷰를 호출
- 컨트롤러에서 반환된 데이터 기반으로 View 랜더링하거나 JSON 응답 생성
- BufferedReader/BufferedWriter는 요청 및 응답 데이터를 효율적으로 처리
- BufferedReader : Buffer(문자열 담음) 만들어 놓고 요청 헤더와 바디 담아서(읽어서) Tomcat 통해서 요청으로 들어감
- BufferedWriter : Buffer에 응답 담아서(작성해서) Tomcat 통해서 응답으로 나감
- 세션과 요청 객체는 특정 컨텍스트에서 주입받아 사용 가능
- Session : 클라이언트와 서버 간 상태 정보 유지
- Spring은 Session 관리를 위해 HttpSession 객체 제공
- Tomcat이 관리, 필요하면 Spring에서 주입 받아 사용 가능
[프로토콜 - 쿠키와 세션]
Cookie
- 서버가 클라이언트(브라우저)에게 전달하고 이후 요청 시 이 데이터를 다시 서버에 보냄
- 사용자 식별, 세션 유지, 개인화된 설정 저장에 사용
- 만료 시간 지정 가능하고 만료 시간이 없으면 브라우저 종료 시 삭제
Session
- 사용자가 서버와 상호작용 하는 동안 상태 유지
- 서버에서 일정 시간 동안 유지되고 클라이언트가 활동하지 않으면 만료됨
- 클라이언트 식별을 위한 고유한 세션 ID 생성
- 세션 ID를 클라이언트에게 전달, 이를 쿠키에 저장하여 이후 요청 시 서버가 세션 식별 가능 ex) JSESSION이라는 이름의 쿠키에 세션 ID 저장
Cookie와 Session의 상관관계 - 로그인 기능 예시
- 사용자가 로그인하면 서버가 세션을 생성하고 세션 ID 발급
- 클라이언트(브라우저) → 서버 : Post 요청 /login
- 세션 ID xyz123 생성, 세션에 사용자 데이터 저장
session.setAttribute("user", "john");- 서버는 Set-Cookie 헤더를 통해 클라이언트(브라우저)에게 세션 ID 전달
- 서버가 세션 ID를 쿠키로 설정
Set-Cookie: JSESSIONID=xyz123; Path=/; HttpOnly
- 클라이언트는 세션 ID가 저장된 쿠키를 포함해 서버에 요청 보냄
Cookie: JSESSIONID=xyz123
- 서버는 세션 ID를 기반으로 해당 사용자의 세션 데이터를 가져옴
- 세션 데이터 : user = john
[인증(Authentication)과 인가(Authorization)]
- 인증
- 세션에 값만 있으면 됨
- 로그인이 되면 세션에 로그인 정보 담김
- 인가 (권한 부여)
- 세션에 들어있는 값과 정보 다 필요
- 로그인(인증) 했다고 다 권한이 생기는 건 아님

Spring 인증 블로그 로그인 구현
[UserController 클래스]
로그인은 민감한 정보를 담기 때문에 예외적으로 post 요청
컨트롤러는 항상 로그인에 정상적으로 성공했을 때만 객체를 return 받음
⇒ 전달받은 데이터의 유저가 없거나, 아이디 또는 비밀번호가 틀렸을 때
UserService 클래스 또는 UserRepository 클래스에서 예외를 throw 하도록 작성하였기 때문
- @PostMapping("/login")
- Post 요청을 /login 경로로 매핑
- 로그인 요청 처리하는 엔드 포인트
- LoginDTO 객체를 통해(매개변수) 사용자가 로그인할 때 입력한 데이터를 받아 처리
- userService.로그인(loginDTO) 호출
- 로그인 비즈니스 로직 처리하고 인증된 사용자 객체 반환받음
- session.setAttribute("sessionUser", sessionUser) 호출
- 로그인된 사용자 정보 세션 저장
- 세션 유지되는 동안 사용자는 인증된 상태 유지 가능
- return "redirect:/"
- 로그인 성공하면 리다이렉트하여 메인 페이지 이동
@RequiredArgsConstructor
@Controller
public class UserController {
private final UserService userService;
// IOC 컨테이너에서 세션 꺼내옴 (세션은 싱글턴이라서 하나만 있으니까 IOC가 관리해줌)
private final HttpSession session;
@PostMapping("/login")
public String login(UserRequest.LoginDTO loginDTO) {
User sessionUser = userService.로그인(loginDTO);
session.setAttribute("sessionUser", sessionUser);
return "redirect:/"; // 로그인 성공하면 메인 페이지 리다이렉트
}
}[UserService 클래스]
- LoginDTO 객체를 통해(매개변수) 사용자가 로그인할 때 입력한 입력 데이터를 받아 처리
- userRepository.findByUsername(loginDTO.getUsername())
- 데이터베이스에서 username 기준으로 사용자 정보 조회
- if (!userPS.getPassword().equals(loginDTO.getPassword()))
- 비밀번호 검증 - 조회된 사용자의 비밀번호와 입력된 비밀번호가 일치하는지 확인
- 비밀번호가 일치하지 않으면 RuntimeException 발생시켜서 로그인 실패 처리
- 비밀번호가 일치하면 조회된 사용자 객체 반환
@RequiredArgsConstructor
@Service
public class UserService implements UserDetailsService {
private final UserRepository userRepository;
public User 로그인(UserRequest.LoginDTO loginDTO) {
User userPS = userRepository.findByUsername(loginDTO.getUsername());
if (!userPS.getPassword().equals(loginDTO.getPassword())) {
throw new RuntimeException("아이디 혹은 패스워드가 일치하지 않습니다.");
// 실제로는 로그를 남겨서 서버에 패스워드가 실패 카운팅해야 함
// 일단은 alert 보이도록 해놓음
}
return userPS;
}
// post 요청
// /login 일때 호출됨
// key 값 -> username, password
// Content-Type -> x-www-form-urlencoded
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
return user;
}
}[UserRepository 클래스]
- em.createQuery
- JPQL을 사용하여 사용자 이름으로 User 엔티티를 조회하는 쿼리를 생성
- select u from User u where u.username = :username
- 추상적이고 객체 지향적인 방식으로 조회
- JPQL의 :은 SQL의 ?역할
- q.setParameter("username", username)
- 쿼리 파라미터로 사용자 이름 설정
- q.getSingleResult()
- 쿼리를 실행하여 단일 결과 반환
- 결과가 없거나 여러 결과가 있을 경우 예외 발생 할 수 있음
- 예외 처리
- RuntimeException이 발생하면 메세지로 새 예외 던지기 ⇒ 사용자에게 적절한 오류 메세지 전달을 위함
@RequiredArgsConstructor
@Repository
public class UserRepository {
private final EntityManager em;
public User findByUsername(String username) {
// createQuery 사용 / jpql => :이 ?역할
Query q = em.createQuery("select u from User u where u.username = :username", User.class);
q.setParameter("username", username);
try {
// 테스트를 해보니 optinal을 리턴할 필요가 없어져서 리팩토링
// 예외 처리를 try-catch로 하기로 바꿈
return (User) q.getSingleResult();
} catch (RuntimeException e) {
throw new RuntimeException("아이디 혹은 패스워드가 일치하지 않습니다.");
}
}웹 실행해서 로그인 후 Set-Cookie 값 확인
- 웹 실행해서 로그인하면 관리자 모드(F12)의 Headers에서 확인 가능

Postman을 활용한 로그인 테스트
- ‘username = ssar, password = 1234’ 더미데이터가 h2DB에 저장되어있는 상태
- 로그인 요청
- http://localhost:8080/login 경로로 HTTP POST 요청
- JSESSIONID 생성
- 서버는 사용자의 인증 정보 확인 후 로그인에 성공하면 고유한 세션ID(JSESSIONID) 생성
- 생성된 세션ID는 서버 측 세션 스토리지에 저장됨
- 쿠키를 통해 세션ID 전달
- 서버는 응답으로 JSESSIONID를 쿠키에 담아 클라이언트(브라우저)로 전송
- 클라이언트는 쿠키 저장하고 이후 모든 요청에 대해 해당 쿠키를 자동으로 서버에 전달
- 세션 확인
- 클라이언트가 이후 다른 요청을 보낼 때마다, 브라우저는 JSESSIONID 쿠키를 함께 전송
- 서버는 요청에 포함된 JSESSIONID 쿠키 값을 확인하여 해당 세션 ID가 유효한지 확인
- 유효한 세션 ID가 있으면, 서버는 사용자가 로그인된 상태로 간주하고 요청을 처리

- POST 요청 후 다른 경로 요청했을 때
- POST 요청에서 전달 받은 JSESSIONID 의 Value 값을 쿠키로 함께 전송하는 것을 확인

Share article