1. 회원가입
- User
package shop.mtcoding.blog.user;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import java.sql.Timestamp;
@NoArgsConstructor
@Getter
@Table(name = "user_tb")
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(unique = true)
private String username;
private String password;
private String email;
@CreationTimestamp
private Timestamp createdAt;
@Builder
public User(Integer id, String username, String password, String email, Timestamp createdAt) {
this.id = id;
this.username = username;
this.password = password;
this.email = email;
this.createdAt = createdAt;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", email='" + email + '\'' +
", createdAt=" + createdAt +
'}';
}
}
- UserRequest
package shop.mtcoding.blog.user;
import lombok.AllArgsConstructor;
import lombok.Data;
public class UserRequest {
@AllArgsConstructor
@Data
public class JoinDTO {
private String username;
private String password;
private String email;
public User toEntity() {
return User.builder().username(username).password(password).email(email).build();
}
}
@Data
public class LoginDTO {
private String username;
private String password;
public User toEntity() {
return User.builder().username(username).password(password).build();
}
}
}
- UserController
@GetMapping("/join-form")
public String joinForm(){
return "user/join-form";
}
@PostMapping("/join")
public String join(UserRequest.JoinDTO joinDTO) {
userService.회원가입(joinDTO);
return "redirect:/login-form";
}
- UserService
@Transactional
public void 회원가입(UserRequest.JoinDTO joinDTO) {
User user = joinDTO.toEntity();
userRepository.insertUser2(user);
// user 객체 : 영속 & 동기화됨
}
- UserRepository
public void insertUser(User user) {
Query q = em.createNativeQuery("insert into user_tb(username, password, email, created_at) values(?, ?, ?, now());");
q.setParameter(1, user.getUsername());
q.setParameter(2, user.getPassword());
q.setParameter(3, user.getEmail());
q.executeUpdate();
}
// 2. User 영속
public void insertUser2(User user)
em.persist(user);
}
// 3. User는 DB와 동기화

예외 처리 1. 아이디 중복체크
- 아이디가 중복될 경우 가입이 불가하도록 조치
- join-form.mustache
- 중복 확인 : permitRegister 함수 작동
- permitRegister()
- username을 받아온 후, DB에서 중복된 데이터가 있는지 체크
- JSON 데이터를 요청 (”/permit-register/"+username) : GET 요청
- 받은 JSON 데이터를 JS Object 데이터로 파싱
- 내부 available 값에 따라 가입 허가 (true면 회원가입 가능)
- username 입력 칸에 keyup 이벤트 리스너 추가
- username 내 키 입력 감지 시 체크 변수인 isPermitted = false
- submit 시 valid 체크 : isPermitted가 true이면 가입 (/join으로 POST 요청)
- false일 시 alert 출력
{{> layout/header}}
<div class="container p-5">
<!-- 요청을 하면 localhost:8080/join POST로 요청됨
username=사용자입력값&password=사용자값&email=사용자입력값 -->
<div class="card">
<div class="card-header"><b>회원가입을 해주세요</b></div>
<div class="card-body">
<form action="/join" method="post" enctype="application/x-www-form-urlencoded" onsubmit="return valid()">
<div class="mb-3">
<input type="text" class="form-control" placeholder="Enter username" name="username" id="username">
<button type="button" class="btn btn-warning" onclick="permitRegister()">중복확인</button>
</div>
<div class="mb-3">
<input type="password" class="form-control" placeholder="Enter password" name="password">
</div>
<div class="mb-3">
<input type="email" class="form-control" placeholder="Enter email" name="email">
</div>
<button type="submit" class="btn btn-primary form-control">회원가입</button>
</form>
</div>
</div>
</div>
<script>
let isPermitted = false;
let usernameInput = document.querySelector("#username");
usernameInput.addEventListener("keyUp",()=>{
isPermitted = false;
});
function valid() {
if(!isPermitted) {
alert("아이디 중복체크를 해주세요.");
usernameInput.readOnly = false;
return false;
}
return true;
}
async function permitRegister() {
let username = usernameInput.value;
let response = await fetch("/permit-register/"+username);
let responseBody = await response.json();
// status = 200, msg = '성공', body : { available = true }
isPermitted = responseBody.body.available;
if(isPermitted) {
alert("아이디 사용이 가능합니다.");
// usernameInput.readOnly = true;
} else {
alert ("존재하는 아이디가 있습니다. 다시 입력해주세요.");
}
}
</script>
{{> layout/footer}}
- UserController
- Resp 객체를 반환 : JSON 형태
- @ResponseBody : Controller 내에서 데이터를 반환할 때 사용
- 보통 APIController를 따로 만들어서 @RestController 속성을 부여한 후 사용
- 존재 여부만 확인하므로 DTO를 따로 만들지 않고, Map 컬렉션을 활용
@GetMapping("/permit-register/{username}")
public @ResponseBody Resp<?> permitRegister(@PathVariable("username") String username) {
Map<String, Object> dto = userService.유저네임중복체크(username);
return Resp.ok(dto);
}
- Resp
- 응답 형태에 따라 각기 다른 JSON을 반환하기 위한 Response
- 제네릭 클래스를 활용하여 body의 타입에 따라 동적으로 타입 부여
- new 객체 생성을 피하기 위한 static 선언
- 제네릭 클래스의 특징인 동적 타입 바인딩에 반하는 특성 : 오류 발생
- 예외 문법 : <T> 타입을 제네릭 클래스 앞에 붙임 : Object 타입으로 데이터를 받음
- 나중에 body가 들어오면 그 타입에 맞추어서 자동으로 다운캐스팅
package shop.mtcoding.blog._core;
import lombok.Data;
@Data
public class Resp<T> {
private Integer status;
private String msg;
private T body;
private Resp(Integer status, String msg, T body) {
this.status = status;
this.msg = msg;
this.body = body;
}
public static <T> Resp<?> ok(T body) {
return new Resp<>(200, "성공", body);
}
public static Resp<?> fail(Integer status, String msg) {
return new Resp(status, msg, null);
}
}
- UserService
- username을 통해 해당하는 유저가 있는지 탐색
- Map 컬렉션 생성 : body에 key : value 꼴의 HashMap 삽입
- user가 없으면 (중복 유저가 없음) available : true 삽입 후 반환
- user가 존재하면 available : false 삽입 후 반환
public Map<String, Object> 유저네임중복체크(String username) {
User user = userRepository.findByUsernameV2(username);
Map<String, Object> dto = new HashMap<>();
if(user == null) dto.put("available", true);
else dto.put("available", false);
return dto;
}
결과
- 중복되는 username이 존재할 경우, alert 출력

- username이 중복되지 않으면, 아이디 사용이 가능하다는 alert 출력

- 중복확인을 누르지 않고 회원가입 시, 아이디 중복체크 확인을 하라는 alert 출력

- 중복 확인 후 중복되는 아이디로 사용자가 값을 변경 후 회원가입을 하게 되면, Exception 발생
- 에러 메시지 직접 출력으로 stacktrace 비출력

- 이후 UX를 위해서, 아이디 중복확인 후 해당 입력창 속성을 readOnly로 변경할 수 있음
- 정상적 접근의 경우 중복 확인이 된 후 아이디를 변경할 수 없음
- 그럼에도 값을 변경하는 경우, bad request이므로 이에 대한 추가 조치 가능
2. 로그인
- UserController
@GetMapping("/login-form")
public String loginForm() {
return "user/login-form";
}
@PostMapping("/login")
public String login(UserRequest.LoginDTO loginDTO, HttpSession session) {
User user = userService.로그인(loginDTO);
session.setAttribute("validatedUser", user);
// System.out.println(session.getAttribute("validatedUser"));
return "redirect:/";
}
- UserService
public User 로그인(UserRequest.LoginDTO loginDTO) {
User user = loginDTO.toEntity();
User foundUser = userRepository.findByUsernameV2(user);
if (foundUser == null) throw new RuntimeException("일치하는 유저가 없습니다.");
if(!(foundUser.getPassword().equals(loginDTO.getPassword()))) throw new RuntimeException("아이디 또는 비밀번호가 틀립니다.");
return foundUser;
}
- UserRepository
public User findByUsernameV2(User user) {
Query q = em.createQuery("select u from User u where u.username = :username", User.class);
q.setParameter("username", user.getUsername());
return (User) q.getSingleResult();
}
// JPA
public User findByUsernameV3(User user) {
return em.find(User.class, user.getUsername());
}

- session 정보 (User.toString())

예외 처리 1. Username 저장
- 체크박스 체크 후 로그인을 하면, 이후 다시 로그인을 할 때에도 아이디가 자동 작성되도록 설정
- login-form.mustache
- checkbox의 체크 여부에 따라 값을 저장할 지 결정해야 함 : checkbox의 값을 받아와야 함
- 체크가 되어 있으면 “on”, 되어있지 않으면 null 반환
- loginDTO에 저장
- 브라우저의 cookie 탐색 : “username” = value 꼴의 쿠키 탐색
- 쿠키가 존재해서 username ≠ null 일 경우 : value 대입
{{> layout/header}}
<div class="container p-5">
<div class="card">
<div class="card-header"><b>로그인을 해주세요</b></div>
<div class="card-body">
<form action="/login" method="post" enctype="application/x-www-form-urlencoded">
<div class="mb-3">
<input type="text" class="form-control" placeholder="Enter username" name="username" id="username">
</div>
<div class="mb-3">
<input type="password" class="form-control" placeholder="Enter password" name="password">
</div>
<!-- ✅ 공개 여부 체크박스 -->
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="rememberMe" id="rememberMe" checked>
<label class="form-check-label" for="isUsernameCheck">
아이디를 기억하겠습니까?
</label>
</div>
<button type="submit" class="btn btn-primary form-control">로그인</button>
</form>
</div>
</div>
</div>
<script>
let username = getCookie("username");
if (username != null) document.querySelector("#username").value = username;
function getCookie(key) {
const cookies = document.cookie.split('; ');
for (let cookie of cookies) {
const [k, v] = cookie.split('=');
if (k == key) {
return v;
}
}
return null; // 해당 키가 없으면 null 반환
}
</script>
{{> layout/footer}}
- UserController
- 체크박스 여부에 따라 cookie의 만료 기간 및 유효 Path를 설정
- cookie.setPath(”/”) : 사이트 전역에서 쿠키 접근이 가능
- cookie.setMaxAge(int) : 쿠키의 만료 시간 설정 (seconds)
- 0일 경우 즉시 만료 : 바로 삭제됨
- response에 쿠키 추가
public String login(UserRequest.LoginDTO loginDTO, HttpSession session, HttpServletResponse response) {
User user = userService.로그인(loginDTO);
session.setAttribute("validatedUser", user);
// System.out.println(session.getAttribute("validatedUser"));
if (loginDTO.getRememberMe() == null) {
Cookie cookie = new Cookie("username", null);
cookie.setMaxAge(0); // 0으로 설정하면 즉시 삭제
cookie.setPath("/"); // 경로를 명확히 지정해야 삭제 가능
response.addCookie(cookie);
} else {
Cookie cookie = new Cookie("username", loginDTO.getUsername());
cookie.setMaxAge(60 * 60 * 24 * 7);
cookie.setPath("/");
response.addCookie(cookie);
}
return "redirect:/";
}
- UserRequest
- String rememberMe : 체크박스 여부에 따라 on 또는 null 저장
@Data
public class LoginDTO {
private String username;
private String password;
private String rememberMe; // on if checked, else null
public User toEntity() {
return User.builder().username(username).password(password).build();
}
}
3. 로그아웃
- UserController
@GetMapping("/logout")
public String logout(HttpSession session) {
session.invalidate();
return "redirect:/";
}
4. Layout : Header
<!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">
<style>
.my-like-heart {
font-size: 24px;
color: gray;
cursor: pointer;
}
.my-like-heart.liked {
color: red;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-sm bg-dark navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/">Metacoding</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">
{{#validatedUser}}
<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>
{{/validatedUser}}
{{^validatedUser}}
<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>
{{/validatedUser}}
</ul>
</div>
</div>
</nav>
5. 회원정보 수정
- BoardController
- 글을 작성하고 글쓰기완료 버튼을 누르면 session 정보와 DTO를 전송
@GetMapping("/board/save-form")
public String saveForm(HttpSession session) {
User validatedUser = (User) session.getAttribute("validatedUser");
if (validatedUser == null) throw new RuntimeException("인증이 필요합니다.");
return "board/save-form";
}
@PostMapping("/board/save")
public String save(BoardRequest.SaveDTO saveDTO, HttpSession session) {
User validatedUser = (User) session.getAttribute("validatedUser");
if (validatedUser == null) throw new RuntimeException("인증이 필요합니다.");
boardService.글쓰기(saveDTO, validatedUser);
return "redirect:/";
}
- BoardRequest
- user 정보를 세션에서 받아옴
@Data
public static class SaveDTO {
private String title;
private String content;
private String isPublic;
public Board toEntity(User user) {
return Board.builder()
.title(title)
.content(content)
.isPublic(isPublic == null ? false : true)
.user(user)
.build();
}
}
- BoardService
@Transactional
public void 글쓰기(BoardRequest.SaveDTO saveDTO, User user) {
Board board = saveDTO.toEntity(user);
boardRepository.write(board);
}
- BoardRepository
public void write(Board board) {
em.persist(board);
}
- user/update-form.mustache
{{> layout/header}}
<div class="container p-5">
<div class="card">
<div class="card-header"><b>회원수정을 해주세요</b></div>
<div class="card-body">
<form action="/user/update" method="post" enctype="application/x-www-form-urlencoded">
<div class="mb-3">
<input value="{{validatedUser.username}}" type="text" class="form-control" placeholder="Enter username" disabled>
</div>
<div class="mb-3">
<input type="password" class="form-control" placeholder="Enter password" name="password">
</div>
<div class="mb-3">
<input value="{{validatedUser.email}}" type="email" class="form-control" placeholder="Enter email"
name="email">
</div>
<button type="submit" class="btn btn-primary form-control">회원가입수정</button>
</form>
</div>
</div>
</div>
{{> layout/footer}}
결과
- 회원 정보 수정 반영


Share article