1. 전체 코드

Controller
java/shop.mtcoding.blog/board/BoardController
package shop.mtcoding.blog.board;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import shop.mtcoding.blog.user.User;
import javax.servlet.http.HttpSession;
@RequiredArgsConstructor
@Controller
public class BoardController {
private final HttpSession session;
@GetMapping({ "/", "/board" })
public String index() {
return "index";
}
@GetMapping("/board/saveForm")
public String saveForm() {
return "board/saveForm";
}
@GetMapping("/board/1")
public String detail() {
return "board/detail";
}
}
java/shop.mtcoding.blog/user/User
package shop.mtcoding.blog.user;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import javax.persistence.*;
import java.time.LocalDate;
// 여기 있는 @는 컨포넌트 스캔은 안됨
//db에서 조회된 user 데이터베이스 값을 여기에 받음
@Data //게터세터
@Entity // 이게 붙으면 테이블 생성됨
@Table(name="user_tb") // 테이블명
public class User {
@Id // 프라이머리키 설정
@GeneratedValue(strategy = GenerationType.IDENTITY) //auto_increment
private int id ;
@Column(unique = true) // 유저네임은 유니크
private String username;
@Column(length = 60,nullable = false) //비밀번호 길이는 60, null 불가
private String password;
private String email;
@CreationTimestamp
private LocalDate createdAt ;
}
java/shop.mtcoding.blog/user/UserController
package shop.mtcoding.blog.user;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
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 javax.servlet.http.HttpSession;
@RequiredArgsConstructor //final 을 붙이고 이걸 사용하면 됨
@Controller
public class UserController {
private final UserRepository userRepository ; // 의존성 주입 받기 위해 만듬. 의존성 주입을 받을 떄 final을 붙여서 사용함
private final HttpSession session ;
@PostMapping("/join")
public String join(UserRequest.JoinDTO requsetDTO){ // 클래스로 매개변수로 한방에 받기.
System.out.println(requsetDTO);
//1. 유효성 검사
if(requsetDTO.getUsername().length()<3){
return "error/400";
}
//DB 인서트 - 모델에게 위임하기
userRepository.save(requsetDTO) ; // 위임
return"redirect:/loginForm";
}
@PostMapping("/login")// select 는 get 요청을 해야함. 하지만 로그인은 민감한 정보기 때문에 get 요청을 하면 쿼리스트링으로 오기 때문에 post 요청으로 함
public String login(UserRequest.JoinDTO requsetDTO){
if(requsetDTO.getUsername().length()<3){
return "error/400";
}
// 2. 모델 연결
User user = userRepository.findByUsernameAndPaaword(requsetDTO);
if(user==null){
return "error/401";
}else {
session.setAttribute("sessionUser",user); // setAttribute 해쉬맵 키 : 값
return "redirect:/"; // 메인으로 연결
}
}
@GetMapping("/joinForm")
public String joinForm() {
return "user/joinForm";
}
@GetMapping("/loginForm")
public String loginForm() {
return "user/loginForm";
}
@GetMapping("/user/updateForm")
public String updateForm() {
return "user/updateForm";
}
@GetMapping("/logout")
public String logout() {
return "redirect:/";
}
}
java/shop.mtcoding.blog/user/UserRequest
package shop.mtcoding.blog.user;
import lombok.Data;
// 요청DTO - data transfer object
public class UserRequest { // 요청데이터를 여기 받음
@Data // 얘가 게터세터 다 가지고 있음.
public static class JoinDTO{
private String username ;
private String password;
private String email ;
}
@Data //
public static class LoginDTO{
private String username ;
private String passwoRErd;
}
}
View
resources/templates/board/detail.mustache
{{> layout/header}}
<div class="container p-5">
<!-- 수정삭제버튼 -->
<div class="d-flex justify-content-end">
<button class="btn btn-warning me-1">수정</button>
<button class="btn btn-danger">삭제</button>
</div>
<!-- 게시글내용 -->
<div>
<h2><b>제목자리</b></h2>
<hr />
<div class="m-4 p-2">
내용입니다
</div>
</div>
<!-- 댓글 -->
<div class="card mt-3">
<!-- 댓글등록 -->
<div class="card-body">
<form action="/reply/save" method="post">
<textarea class="form-control" rows="2" name="comment"></textarea>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-outline-primary mt-1">댓글등록</button>
</div>
</form>
</div>
<!-- 댓글목록 -->
<div class="card-footer">
<b>댓글리스트</b>
</div>
<div class="list-group">
<!-- 댓글아이템 -->
<div class="list-group-item d-flex justify-content-between align-items-center">
<div class="d-flex">
<div class="px-1 me-1 bg-primary text-white rounded">cos</div>
<div>댓글 내용입니다</div>
</div>
<form action="/reply/1/delete" method="post">
<button class="btn">🗑</button>
</form>
</div>
<!-- 댓글아이템 -->
<div class="list-group-item d-flex justify-content-between align-items-center">
<div class="d-flex">
<div class="px-1 me-1 bg-primary text-white rounded">ssar</div>
<div>댓글 내용입니다</div>
</div>
<form action="/reply/1/delete" method="post">
<button class="btn">🗑</button>
</form>
</div>
</div>
</div>
</div>
{{> layout/footer}}
resources/templates/board/saveForm.mustache
{{> layout/header}}
<div class="container p-5">
<!-- 요청을 하면 localhost:8080/board/save POST로 요청됨
title=사용자입력값&content=사용자값 -->
<div class="card">
<div class="card-header"><b>글쓰기 화면입니다</b></div>
<div class="card-body">
<form action="/board/save" method="post" enctype="application/x-www-form-urlencoded">
<div class="mb-3">
<input type="text" class="form-control" placeholder="Enter title" name="title">
</div>
<div class="mb-3">
<textarea class="form-control" rows="5" name="content"></textarea>
</div>
<button type="submit" class="btn btn-primary form-control">글쓰기완료</button>
</form>
</div>
</div>
</div>
{{> layout/footer}}
resources/templates/error/400.mustache
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>클라이언트가 요청을 잘못하였습니다.</h1>
</body>
</html>
resources/templates/error/400.mustache
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>클라이언트가 요청을 잘못하였습니다.</h1>
</body>
</html>
resources/templates/layout/header.mustache
<!DOCTYPE html>
<html lang="en">
<head>
<title>Blog</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<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>
</head>
<body>
<nav class="navbar navbar-expand-sm bg-primary navbar-primary">
<div class="container-fluid">
<a class="navbar-brand" href="/">Home</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">
{{#sessionUser}}
<li class="nav-item">
<a class="nav-link" href="/board/saveForm">글쓰기</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/user/updateForm">회원정보보기</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/logout">로그아웃</a>
</li>
{{/sessionUser}}
{{^sessionUser}}
<li class="nav-item">
<a class="nav-link" href="/joinForm">회원가입</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/loginForm">로그인</a>
</li>
{{/sessionUser}}
<!-- 이렇게 적으면 세션, 리퀘스트에 접근. #은 if문 ^는 else 문 -->
</ul>
</div>
</div>
</nav>
resources/templates/layout/footer.mustache
<div class="bg-light p-5 text-center">
<h4>Created by Metacoding</h4>
<h5>☎ 010-1111-2222</h5>
<button class="btn btn-outline-primary">고객센터</button>
<button class="btn btn-outline-primary">오시는길</button>
</div>
</body>
</html>
resources/templates/user/joinForm.mustache
{{> 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">
<div class="mb-3">
<input type="text" class="form-control" placeholder="Enter username" name="username" value="ssar">
</div>
<div class="mb-3">
<input type="password" class="form-control" placeholder="Enter password" name="password" value="1234">
</div>
<div class="mb-3">
<input type="email" class="form-control" placeholder="Enter email" name="email" value="ssar@nate.com">
</div>
<button type="submit" class="btn btn-primary form-control">회원가입</button>
</form>
</div>
</div>
</div>
{{> layout/footer}}
resources/templates/user/loginForm.mustache
{{> layout/header}}
<div class="container p-5">
<!-- 요청을 하면 localhost:8080/login POST로 요청됨
username=사용자입력값&password=사용자값 -->
<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" value="ssar">
</div>
<div class="mb-3">
<input type="password" class="form-control" placeholder="Enter password" name="password" value="1234">
</div>
<button type="submit" class="btn btn-primary form-control">로그인</button>
</form>
</div>
</div>
</div>
{{> layout/footer}}
resources/templates/user/updateForm.mustache
{{> /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="/user/update" method="post" enctype="application/x-www-form-urlencoded">
<div class="mb-3">
<input type="text" class="form-control" placeholder="Enter username" name="username" disabled>
</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" disabled>
</div>
<button type="submit" class="btn btn-primary form-control">회원가입수정</button>
</form>
</div>
</div>
</div>
{{> /layout/footer}}
resources/templates/index.mustache
{{> layout/header}}
<!--머스태치 문법/ 이렇게 적으면 헤더를 매번 포함할 수 있음-->
<div class="container p-5">
<div class="card mb-3">
<div class="card-body">
<h4 class="card-title mb-3">제목1</h4>
<a href="/board/1" class="btn btn-primary">상세보기</a>
</div>
</div>
<ul class="pagination d-flex justify-content-center">
<li class="page-item disabled"><a class="page-link" href="#">Previous</a></li>
<li class="page-item"><a class="page-link" href="#">Next</a></li>
</ul>
</div>
{{> layout/footer}}
Model
java/shop.mtcoding.blog/user/UserRepository
package shop.mtcoding.blog.user;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.Query;
//DAO
@Repository // 레파지토리가 new 됨
public class UserRepository {
private EntityManager em ;
public UserRepository(EntityManager em) {
this.em = em; // 의존성 주입, ioc 컨테이너에 EntityManager가 존재함.
}
@Transactional // 이게 없으면 쿼리문을 전송안한다. select 조회하는거기 때문에 상관없음.
public void save(UserRequest.JoinDTO requestDTO){
System.out.println("userRepository에 save 메서드 호출됨");
Query query = em.createNativeQuery("insert into user_tb(username,password, email) values(?,?,?)");
query.setParameter(1,requestDTO.getUsername());
query.setParameter(2,requestDTO.getPassword());
query.setParameter(3,requestDTO.getEmail());
query.executeUpdate();
}
@Transactional //하이버네이트 사용, 셀렉트에는 필요없음
public void saveV2(UserRequest.JoinDTO requestDTO){
User user = new User();
user.setUsername(requestDTO.getUsername());
user.setPassword(requestDTO.getPassword());
user.setEmail(requestDTO.getEmail());
em.persist(user);
}
public User findByUsernameAndPaaword(UserRequest.JoinDTO requsetDTO) {
Query query = em.createNativeQuery("select * from user_tb where username=? and password=?",User.class); //User.class 알아서 맵핑. ResultSet해서 하나씩 파싱 안해도 됨.
query.setParameter(1,requsetDTO.getUsername());
query.setParameter(2,requsetDTO.getPassword());
User user = (User) query.getSingleResult(); // getSingleResult()의 리턴값이 오브젝트라 다운캐스팅
return user ;
}
}
application
application.yml
spring: profiles: active: - dev
application-dev.yml
server:
servlet:
encoding:
charset: utf-8
force: true
port: 8080
spring:
datasource:
driver-class-name: org.h2.Driver
url : jdbc:h2:mem:test;MODE=MySQL
username : sa
password :
mustache:
servlet:
expose-session-attributes: true
expose-request-attributes: true
#db 연결
h2:
console:
enabled: true
#웹에 연결될 수 있게
jpa:
hibernate:
ddl-auto: create
show-sql: true
properties:
hibernate:
format_sql: true
#서버가 실행될 때 @entity 되어있는걸 크리에이트함.
#hibernate 가 실행될 때 show-sql: true 면 내용 띄워줌
application-prod.yml
server:
servlet:
encoding:
charset: utf-8
force: true
port: 5000
build.gradle
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.2'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}
group = 'shop.mtcoding'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '21'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' //하이버네이트 기술 내장, @Entity 를 찾음. 보고 테이블 생성 쿼리를 만들어냄.
implementation 'org.springframework.boot:spring-boot-starter-mustache'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2' //db연결
runtimeOnly 'com.mysql:mysql-connector-j' //db연결
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
2. DB 연결하기
DB는 H2 데이터베이스를 활용해 사용한다.
application-dev.yml
spring:
datasource:
driver-class-name: org.h2.Driver
url : jdbc:h2:mem:test;MODE=MySQL
username : sa
password :
h2:
console:
enabled: true
jpa:
hibernate:
ddl-auto: create
show-sql: true
properties:
hibernate:
format_sql: true
application-dev 파일에 위의 코드를 추가한다. 이러면 H2 데이터베이스와 연결할 수 있다.

스프링에서 디스패쳐 서블릿은 프로그램이 알아서 실행되기 때문에 개발자는 컨트롤러와 DAO만 만들면 된다.
우선 클라이언트가 입력한 데이터는 오브젝트에 담는다. 이를 DTO 라고 부른다.
package shop.mtcoding.blog.user;
import lombok.Data;
// 요청DTO - data transfer object
public class UserRequest { // 요청데이터를 여기 받음
@Data // 얘가 Getter,Setter 다 가지고 있음.
public static class JoinDTO{
private String username ;
private String password;
private String email ;
}
@Data //
public static class LoginDTO{
private String username ;
private String password;
}
}
JoinDTO 는 회원가입할 때 받을 데이터, LoginDTO 는 로그인할 때 받을 데이터를 담는다. 각각의 매개변수로 받아도 되지만 하나의 오브젝트로 받는게 훨씬 편하다.
package shop.mtcoding.blog.user;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import javax.persistence.*;
import java.time.LocalDate;
// 여기 있는 @는 컨포넌트 스캔은 안됨
//db에서 조회된 user 데이터베이스 값을 여기에 받음
@Data //게터세터
@Entity // 이게 붙으면 테이블 생성됨
@Table(name="user_tb") // 테이블명
public class User {
@Id // 프라이머리키 설정
@GeneratedValue(strategy = GenerationType.IDENTITY) //auto_increment
private int id ;
@Column(unique = true) // 유저네임은 유니크
private String username;
@Column(length = 60,nullable = false) //비밀번호 길이는 60, null 불가
private String password;
private String email;
@CreationTimestamp
private LocalDate createdAt ;
}
테이블 생성을 위해 User 클래스를 생성한다.
@Table(name="user_tb")
를 사용하면 해당 클래스와 매핑되는 테이블을 생성할 수 있다.3. 컨트롤러
@Controller
public class UserController {
private UserRepository userRepository; // 의존성 주입을 위해 객체 생성
public UserController(UserRepository userRepository) { // 생성자 주입
this.userRepository = userRepository;
}
@PostMapping("/join")
public String join(UserRequest.JoinDTO requsetDTO){ // 클래스로 매개변수로 한방에 받기.
//1. 유효성 검사
if(requsetDTO.getUsername().length()<3){
return "error/400";
}
//DB 인서트 - 모델에게 위임하기
userRepository.save(requsetDTO) ; // userRepository 에게 위임
return"redirect:/loginForm";
}
UserController 의 회원가입 메서드다. DTO 에서 받은 데이터를 파싱해 유효성 검사 후 DAO 에 연결한다. 여기서 DAO 는 Repository 라고도 한다.
UserRepository 를 이용하기 위해 의존성 주입을 이용한다. 컨트롤러에 레파지토리의 객체와 생성자를 만들어 사용한다.
/join 는 뷰는 없기 때문에 클라이언트가 데이터를 입력 완료하면 로그인이 가능한 /loginForm로 리다이렉션한다.
4. Repository (DAO)
Repository 는 입력받은 데이터를 이용해 DB 와 상호작용을 한다. 이때 필요한게 쿼리문이다.
package shop.mtcoding.blog.user;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.Query;
@Repository // 레파지토리가 new 됨
public class UserRepository {
private EntityManager em ; // 의존성 주입을 위한 객체 생성
public UserRepository(EntityManager em) {
this.em = em; // 의존성 주입, ioc 컨테이너에 EntityManager가 존재함.
}
@Transactional // 이게 없으면 쿼리문을 전송하지 않는다. select 조회하는거기 때문에 상관없음.
public void save(UserRequest.JoinDTO requestDTO){
System.out.println("userRepository에 save 메서드 호출됨");
Query query = em.createNativeQuery("insert into user_tb(username,password, email) values(?,?,?)");
query.setParameter(1,requestDTO.getUsername());
query.setParameter(2,requestDTO.getPassword());
query.setParameter(3,requestDTO.getEmail());
query.executeUpdate();
}
public User findByUsernameAndPaaword(UserRequest.JoinDTO requsetDTO) {
Query query = em.createNativeQuery("select * from user_tb where username=? and password=?",User.class); //User.class 알아서 맵핑. ResultSet해서 하나씩 파싱 안해도 됨.
query.setParameter(1,requsetDTO.getUsername());
query.setParameter(2,requsetDTO.getPassword());
User user = (User) query.getSingleResult(); // getSingleResult()의 리턴값이 오브젝트라 다운캐스팅
return user ;
}
}
클라이언트가 받은 데이터를 통해 쿼리문을 날린다.
EntityManager
는 Java Persistence API (JPA)에서 엔터티(객체)와 데이터베이스 간의 상호 작용을 관리하는 인터페이스이다.
자바가 컴파일을 시작하면 String 의 톰캣이 컨포넌트 스캔을 시작한다. 컨포넌트 스캔이 시작되면
@Controller
,@Repository
등과 같은 어노테이션을 스캔해 Ioc 컨테이너에 객체를 생성에 담아놓는다. 이때 JPA 를 사용하면 Ioc 컨테이너에 EntityManager 객체를 만들어 사용할 수 있게 된다.JPA 라이브러리
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
//하이버네이트 기술 내장, @Entity 를 찾음. 보고 테이블 생성 쿼리를 만들어냄
build.grade -
dependencies
에 붙여넣으면 된다.JPA(Java Persistence API) 를 사용하면 SQL 쿼리를 직접 작성하지 않고도 객체를 데이터베이스에 저장, 검색, 갱신, 삭제할 수 있다. 또 Hibernate, EclipseLink, OpenJPA 등이 JPA를 구현한 대표적인 ORM 프레임워크입니다. 프로젝트에서 JPA를 사용하면 데이터베이스와의 상호 작용을 추상화하고, 객체 지향적인 코드 작성을 강화하여 개발 생산성을 높일 수 있다.
5. 회원가입

localhost:8080/joinForm 회원가입 페이지에 데이터를 입력하고 회원가입을 누른다.

회원가입을 누르면 회원가입이 완려되면서 /loginForm 로그인 페이지가 뜬다.
오른쪽 검사를 보면 Request URL 은 /join 으로 되어있고, 상태코드는 302로 정상적인 리다이렉션이 일어났다.

H2 데이터베이스에서 조회해보면 정상적으로 데이터가 Insert 되어있다.
Share article