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