11-4. (실습) 스프링 V2 (2)

박은서's avatar
Feb 06, 2026
11-4. (실습) 스프링 V2 (2)

7. 회원가입 및 로그인

1) UserRequest.java

package com.example.boardv1.user; import lombok.Data; 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 password; } }

2) UserController.java

package com.example.boardv1.user; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Controller public class UserController { private final UserService userService; private final HttpSession session; // 조회인데, 예외로 post 요청 @PostMapping("/login") public String login(UserRequest.LoginDTO reqDTO) { // HttpSession session = req.getSession(); User sessionUser = userService.로그인(reqDTO.getUsername(), reqDTO.getPassword());\ session.setAttribute("sessionUser", sessionUser); // http Response header에 Set-Cookie: sessionKey 저장되서 응답됨. return "redirect:/"; } @PostMapping("/join") public String join(UserRequest.JoinDTO reqDTO){ userService.회원가입(reqDTO.getUsername(), reqDTO.getPassword(), reqDTO.getEmail()); return "redirect:/login-form"; } @GetMapping("/login-form") public String loginForm(){ return "user/login-form"; } @GetMapping("/join-form") public String joinForm(){ return "user/join-form"; } }

3) UserService.java

package com.example.boardv1.user; import java.util.Optional; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Service public class UserService { private final UserRepository userRepository; @Transactional public void 회원가입(String username, String password, String email) { // 1. username 중복 체크 Optional<User> optUser = userRepository.findByUsername(username); if (optUser.isPresent()) { throw new RuntimeException("유저네임이 중복되었습니다"); } // 2. 비영속 객체 User user = new User(); user.setUsername(username); user.setPassword(password); user.setEmail(email); // 3. save() 호출 userRepository.save(user); } public User 로그인(String username, String password) { User findUser = userRepository.findByUsername(username) .orElseThrow(() -> new RuntimeException("username을 찾을 수 없어요")); if (!findUser.getPassword().equals(password)) { throw new RuntimeException("패스워드가 일치하지 않아요"); } return findUser; } }

8. Optional 적용

1) UserRepository.java - 수정

package com.example.boardv1.user; import java.util.Optional; import org.springframework.stereotype.Repository; import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Repository public class UserRepository { // DI private final EntityManager em; // 회원가입할 때 insert public User save(User user) { em.persist(user); return user; } // 로그인할때 username으로 조회해서 password 검증 public Optional<User> findByUsername(String username) { return em.createQuery("select u from User u where u.username = :username", User.class) .setParameter("username", username) .getResultStream() .findFirst(); } public Optional<User> findById(int id) { User findUser = em.find(User.class, id); return Optional.ofNullable(findUser); } }

2) UserRepositoryTest.java - 수정

package com.example.boardv1.user; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest; import org.springframework.context.annotation.Import; @Import(UserRepository.class) @DataJpaTest // EntityManger가 ioc에 등록됨 public class UserRepositoryTest { @Autowired private UserRepository userRepository; @Test public void findById_test() { int id = 5; User user = userRepository.findById(id) .orElseThrow(() -> new RuntimeException("해당 아이디로 유저를 찾을 수 없어요")); System.out.println("user : " + user); } @Test public void save_fail_test() { // given User user = new User(); // 비영속 객체 user.setUsername("cos"); user.setPassword("1234"); user.setEmail("cos@nate.com"); // when User findUser = userRepository.save(user); // 영속화됨 // eye System.out.println(findUser); } @Test public void save_test() { // given User user = new User(); // 비영속 객체 user.setUsername("love"); user.setPassword("1234"); user.setEmail("love@nate.com"); // when User findUser = userRepository.save(user); // 영속화됨 // eye System.out.println(findUser); } @Test public void findByUsername_test() { // given String username = "good"; // when (ssar, 1234) User findUser = userRepository.findByUsername(username) .orElseThrow(() -> new RuntimeException("해당 user를 찾을 수 없어요")); // eye System.out.println(findUser); } }

3) BoardRepository.java - 수정

package com.example.boardv1.board; import java.util.List; import java.util.Optional; import org.springframework.stereotype.Repository; import jakarta.persistence.EntityManager; import jakarta.persistence.Query; import lombok.RequiredArgsConstructor; /** * 하이버네이트 기술 */ @RequiredArgsConstructor // final이 붙어 있는 모든 필드를 초기화하는 생성자를 만들어줌. @Repository public class BoardRepository { private final EntityManager em; // DI = 의존성 주입 (의존하고 있는게 IoC에 떠있어야됨) // public BoardRepository(EntityManager em) { // this.em = em; // } public Optional<Board> findById(int id) { // select * from board_tb where id = 1; // ResultSet rs -> Board 객체 옮기기 (Object Mapping) // Board board = new Board(); // board.id = rs.getInt("id"); Board board = em.find(Board.class, id); return Optional.ofNullable(board); } public List<Board> findAll() { return em.createQuery("select b from Board b order by b.id desc", Board.class) .getResultStream().toList(); } public Board save(Board board) { em.persist(board); // 영속화(영구히 저장하다.) return board; } public void delete(Board board) { em.remove(board); } }

4) BoardService.java - 수정

package com.example.boardv1.board; import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Service public class BoardService { private final BoardRepository boardRepository; public List<Board> 게시글목록() { return boardRepository.findAll(); } public Board 상세보기(int id) { return boardRepository.findById(id) .orElseThrow(() -> new RuntimeException("게시글을 찾을 수 없어요")); } @Transactional // update, delete, insert 할때 붙이세요!! public void 게시글수정(int id, String title, String content) { // Board board = boardRepository.findById(id) .orElseThrow(() -> new RuntimeException("수정할 게시글을 찾을 수 없어요")); board.setTitle(title); board.setContent(content); } // 원자성(모든게 다되면 commit, 하나라도 실패하면 rollback) // 트랜잭션 종료시 flush 됨. @Transactional public void 게시글쓰기(String title, String content) { // 1. 비영속 객체 Board board = new Board(); board.setTitle(title); board.setContent(content); System.out.println("before persist " + board.getId()); // 2. persist boardRepository.save(board); System.out.println("after persist " + board.getId()); } @Transactional public void 게시글삭제(int id) { // 영속화 Board board = boardRepository.findById(id) .orElseThrow(() -> new RuntimeException("삭제할 게시글을 찾을 수 없어요")); boardRepository.delete(board); } // 자동 flush }

5) BoardRepositoryTest.java - 수정

package com.example.boardv1.board; import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest; import org.springframework.context.annotation.Import; import jakarta.persistence.EntityManager; @Import(BoardRepository.class) @DataJpaTest // EntityManger가 ioc에 등록됨 public class BoardRepositoryTest { @Autowired // 어노테이션 DI 기법 private BoardRepository boardRepository; @Autowired private EntityManager em; @Test public void save_test() { // given Board board = new Board(); board.setTitle("title7"); board.setContent("content7"); System.out.println("===before persist"); System.out.println(board); // when boardRepository.save(board); // eye (board객체가 DB데이터와 동기화 되었음.) System.out.println("===after persist"); System.out.println(board); } @Test public void findById_test() { // given int id = 1; // when Board board = boardRepository.findById(id) .orElseThrow(() -> new RuntimeException("게시글을 찾을 수 없어요")); // boardRepository.findById(1); // eye System.out.println(board); } @Test public void findAll_test() { // given // when List<Board> list = boardRepository.findAll(); // eye for (Board board : list) { System.out.println(board); } } @Test public void delete_test() { // given int id = 1; Board board = boardRepository.findById(id) .orElseThrow(() -> new RuntimeException("게시글을 찾을 수 없어요")); // when boardRepository.delete(board); // eye em.flush(); } @Test public void update_test() { // given int id = 1; Board board = boardRepository.findById(id) .orElseThrow(() -> new RuntimeException("게시글을 찾을 수 없어요요")); // when board.setTitle("title1-update"); // eye em.flush(); List<Board> list = boardRepository.findAll(); // eye for (Board b : list) { System.out.println(b); } } @Test public void findByIdV2_test() { // given int id = 1; // when boardRepository.findById(id); em.clear(); boardRepository.findById(id); } }

9. Board 와 User의 관계 정리

1) Board.java - 수정

package com.example.boardv1.board; import java.sql.Timestamp; import org.hibernate.annotations.CreationTimestamp; import com.example.boardv1.user.User; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import lombok.Data; import lombok.NoArgsConstructor; /** * 데이터베이스 세상의 테이블을 자바 세상에 모델링한 결과 = 엔티티 */ @NoArgsConstructor // 디폴트 생성자 @Data // getter, setter, toString @Entity // 해당 어노테이션을 보고 컴퍼넌트 스캔 후 데이터베이스에 테이블을 생성 @Table(name = "board_tb") // 테이블명 설정 public class Board { // user 1, Board N @Id // id를 primary key로 설정 @GeneratedValue(strategy = GenerationType.IDENTITY) // auto increment 설정 private Integer id; private String title; private String content; // private Integer userId; @ManyToOne // FK지정 / Board가 many, User가 one private User user; @CreationTimestamp private Timestamp createdAt; // import 주의!! (java.sql) }

2) data.sql - 더미데이터 수정

insert into user_tb (username, password, email, created_at) values ('ssar', '1234', 'ssar@nate.com', now()); insert into user_tb (username, password, email, created_at) values ('cos', '1234', 'cos@nate.com', now()); insert into board_tb (user_id, title, content, created_at) values (1, 'title1', 'content1', now()); insert into board_tb (user_id, title, content, created_at) values (1, 'title2', 'content2', now()); insert into board_tb (user_id, title, content, created_at) values (1, 'title3', 'content3', now()); insert into board_tb (user_id, title, content, created_at) values (2, 'title4', 'content4', now()); insert into board_tb (user_id, title, content, created_at) values (2, 'title5', 'content5', now()); insert into board_tb (user_id, title, content, created_at) values (2, 'title6', 'content6', now());

3) BoardRepository.java - 메서드 추가

package com.example.boardv1.board; import java.util.List; import java.util.Optional; import org.springframework.stereotype.Repository; import com.example.boardv1.reply.Reply; import jakarta.persistence.EntityManager; import jakarta.persistence.Query; import lombok.RequiredArgsConstructor; /** * 하이버네이트 기술 */ @RequiredArgsConstructor // final이 붙어 있는 모든 필드를 초기화하는 생성자를 만들어줌. @Repository public class BoardRepository { private final EntityManager em; // DI = 의존성 주입 (의존하고 있는게 IoC에 떠있어야됨) // public BoardRepository(EntityManager em) { // this.em = em; // } public Optional<Board> findByIdJoinUser(int id) { Query query = em.createQuery("select b from Board b join fetch b.user u where b.id = :id", Board.class); query.setParameter("id", id); try { Board board = (Board) query.getSingleResult(); return Optional.of(board); } catch (Exception e) { return Optional.ofNullable(null); } } public Optional<Board> findById(int id) { // select * from board_tb where id = 1; // ResultSet rs -> Board 객체 옮기기 (Object Mapping) // Board board = new Board(); // board.id = rs.getInt("id"); Board board = em.find(Board.class, id); return Optional.ofNullable(board); } public List<Board> findAll() { return em.createQuery("select b from Board b order by b.id desc", Board.class) .getResultList(); } public Board save(Board board) { em.persist(board); // 영속화(영구히 저장하다.) return board; } public void delete(Board board) { em.remove(board); } }

10. ORM 전략

1) Board.java - 수정

package com.example.boardv1.board; import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; import org.hibernate.annotations.CreationTimestamp; import com.example.boardv1.reply.Reply; import com.example.boardv1.user.User; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; /** * 데이터베이스 세상의 테이블을 자바 세상에 모델링한 결과 = 엔티티 */ @NoArgsConstructor // 디폴트 생성자 @Getter @Setter @Entity // 해당 어노테이션을 보고 컴퍼넌트 스캔 후 데이터베이스에 테이블을 생성 @Table(name = "board_tb") // 테이블명 설정 public class Board { // user 1, Board N @Id // id를 primary key로 설정 @GeneratedValue(strategy = GenerationType.IDENTITY) // auto increment 설정 private Integer id; private String title; private String content; // private Integer userId; @ManyToOne(fetch = FetchType.EAGER) // FK지정 / Board가 many, User가 one private User user; // user_id = 1 (select * from user_tb where id = 1) @CreationTimestamp private Timestamp createdAt; // import 주의!! (java.sql) @Override public String toString() { return "Board [id=" + id + ", title=" + title + ", content=" + content + ", user=" + user + ", createdAt=" + createdAt + "]"; } }

11. 인증 및 권한 체크

1) BoardController.java - 수정

package com.example.boardv1.board; import java.util.List; 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 org.springframework.web.bind.annotation.ResponseBody; import com.example.boardv1.reply.ReplyRequest; import com.example.boardv1.user.User; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor // final이 적혀있는 애로 생성자 만들어줌 @Controller // @Controller 적어야 리턴값이 파일이 됨 (외부진입점) public class BoardController { private final BoardService boardService; private final HttpSession session; // title : title=title7&content=content7 (x-www-form) @PostMapping("/boards/save") public String save(BoardRequest.SaveOrUpdateDTO reqDTO){ // new해서 넣어줌! 필드가 많을 때 상태만 추가하면 되기 때문에 매우 편함! 재사용 가능!(필드명 잘 적어야 함) // 인증(v) 권한(x) // 인증 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new RuntimeException("인증되지 않았습니다."); boardService.게시글쓰기(reqDTO.getTitle(), reqDTO.getContent(), sessionUser.getId()); return "redirect:/"; } // body : title=제목&content=내용 @PostMapping("/boards/{id}/update") public String update(@PathVariable("id") int id, BoardRequest.SaveOrUpdateDTO reqDTO){ // 인증(v) 권한(v) // 인증 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new RuntimeException("인증되지 않았습니다."); boardService.게시글수정(id,reqDTO.getTitle(), reqDTO.getContent(), sessionUser.getId()); return "redirect:/boards/"+id; } @GetMapping("/") public String index(HttpServletRequest req){ List<Board> list = boardService.게시글목록(); req.setAttribute("models", list); return "index"; } @GetMapping("/boards/save-form") public String saveForm(){ // 인증(v) 권한(x) // 인증 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new RuntimeException("인증되지 않았습니다."); return "board/save-form"; } @GetMapping("/boards/{id}/update-form") public String updateForm(@PathVariable("id") int id, HttpServletRequest req){ // 인증(v) 권한(v) // 인증 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new RuntimeException("인증되지 않았습니다."); Board board = boardService.수정폼게시글정보(id, sessionUser.getId()); req.setAttribute("model", board); return "board/update-form"; } @GetMapping("/boards/{id}") public String detail(@PathVariable("id") int id, HttpServletRequest req){ User sessionUser = (User) session.getAttribute("sessionUser"); // Object타입으로 반환하기 때문에 다운캐스팅해야 함 Integer sessionUserId = sessionUser == null ? null : sessionUser.getId(); BoardResponse.DetailDTO dto = boardService.상세보기(id, sessionUserId); req.setAttribute("model", dto); return "board/detail"; // mustache 파일의 경로 } @PostMapping("/boards/{id}/delete") public String delete(@PathVariable("id") int id){ // 인증(v) 권한(v) // 인증 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new RuntimeException("인증되지 않았습니다."); boardService.게시글삭제(id, sessionUser.getId()); return "redirect:/"; } @GetMapping("/api/boards/{id}") public @ResponseBody BoardResponse.DetailDTO apiDetail(@PathVariable("id") int id) { User sessionUser = (User) session.getAttribute("sessionUser"); Integer sessionUserId = sessionUser == null ? null : sessionUser.getId(); BoardResponse.DetailDTO dto = boardService.상세보기(id, sessionUserId); return dto; } }

2) BoardService.java - 수정

package com.example.boardv1.board; import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.example.boardv1.reply.Reply; import com.example.boardv1.user.User; import com.example.boardv1.user.UserRepository; import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; // 책임 : 트랜잭션 관리 + DTO 만들기 + 권한 체크(DB정보가 필요하기 때문) @RequiredArgsConstructor @Service public class BoardService { private final BoardRepository boardRepository; private final EntityManager em; public List<Board> 게시글목록() { return boardRepository.findAll(); } public Board 수정폼게시글정보(int id, int sessionUserId) { Board board = boardRepository.findById(id) .orElseThrow(() -> new RuntimeException("게시글을 찾을 수 없어요")); // 권한 if (sessionUserId != board.getUser().getId()) throw new RuntimeException("수정할 권한이 없습니다"); return board; } public BoardResponse.DetailDTO 상세보기(int id, Integer sessionUserId) { Board board = boardRepository.findById(id) .orElseThrow(() -> new RuntimeException("게시글을 찾을 수 없어요")); return new BoardResponse.DetailDTO(board, sessionUserId); } @Transactional // update, delete, insert 할때 붙이세요!! public void 게시글수정(int id, String title, String content, int sessionUserId) { Board board = boardRepository.findById(id) .orElseThrow(() -> new RuntimeException("수정할 게시글을 찾을 수 없어요")); // 권한 if (sessionUserId != board.getUser().getId()) throw new RuntimeException("수정할 권한이 없습니다"); board.setTitle(title); board.setContent(content); } // 원자성(모든게 다되면 commit, 하나라도 실패하면 rollback) // 트랜잭션 종료시 flush 됨. @Transactional public void 게시글쓰기(String title, String content, int sessionUserId) { User user = em.getReference(User.class, sessionUserId); // 1. 비영속 객체 Board board = new Board(); board.setTitle(title); board.setContent(content); board.setUser(user); System.out.println("before persist " + board.getId()); // 2. persist boardRepository.save(board); System.out.println("after persist " + board.getId()); } @Transactional public void 게시글삭제(int id, int sessionUserId) { // 영속화 Board board = boardRepository.findById(id) .orElseThrow(() -> new RuntimeException("삭제할 게시글을 찾을 수 없어요")); // 권한 if (sessionUserId != board.getUser().getId()) throw new RuntimeException("삭제할 권한이 없습니다"); boardRepository.delete(board); } // 자동 flush }

3) header.mustache - 수정 (로그인 여부에 따른 버튼)

<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> </head> <body> <nav class="navbar navbar-expand-sm" style="background-color: grey;"> <div class="container-fluid"> <ul class="navbar-nav"> <li class="nav-item"> <a class="nav-link" href="/" style="font-weight: bold; color: white">Metacoding</a> </li> {{#sessionUser}} <li class="nav-item"> <a class="nav-link" href="/boards/save-form" style="color: white">글쓰기</a> </li> <li class="nav-item"> <a class="nav-link" href="/logout" style="color: white">로그아웃</a> </li> {{/sessionUser}} {{^sessionUser}} <li class="nav-item"> <a class="nav-link" href="/join-form" style="color: white">회원가입</a> </li> <li class="nav-item"> <a class="nav-link" href="/login-form" style="color: white">로그인</a> </li> {{/sessionUser}} </ul> </div> </nav> </div>

4) detail.mustache - 수정 (프론트엔드에서 권한 체크)

{{> header}} <div class="container p-5"> {{#model.isOwner}} <!-- 수정삭제버튼 --> <div class="d-flex justify-content-end"> <a href = "/boards/{{model.id}}/update-form" class="btn btn-secondary me-1">수정</a> <form action = "/boards/{{model.id}}/delete" method="post"> <button class="btn btn-outline-secondary">삭제</button> </form> </div> {{/model.isOwner}} <!-- 게시글내용 --> <div> <h2><b>{{model.title}}</b></h2> <hr /> <div class="d-flex justify-content-end"> 작성자 : {{model.username}} </div> <div class="m-4 p-2"> {{model.content}} </div> </div> <!-- 댓글 --> <div class="card mt-3"> <!-- 댓글등록 --> <div class="card-body"> <form action="/replies/save" method="post"> <input type="hidden" name="boardId" value="{{model.id}}"> <textarea id="comment" class="form-control" rows="2" name="comment"></textarea> <div class="d-flex justify-content-end"> <button class="btn btn-secondary mt-1"> 댓글등록 </button> </div> </form> </div> <!-- 댓글목록 --> <div class="card-footer"> <b>댓글리스트</b> </div> <div class="list-group"> {{#model.replies}} <!-- 댓글아이템 --> <div class="list-group-item d-flex justify-content-between align-items-center"> <div class="d-flex"> <div class="px-1 me-1 bg-secondary text-white rounded">{{replyUsername}}</div> <div>{{comment}}</div> </div> {{#isReplyOwner}} <form action="/replies/{{id}}/delete?boardId={{model.id}}" method="post"> <input type="hidden" name="boardId" value="{{model.id}}"> <button class="btn">🗑</button> </form> {{/isReplyOwner}} </div> {{/model.replies}} </div> </div> </div> {{^sessionUser}} <script> // 1. 조건 (querySelector 사용, textArea에 comment라는 id 하나 추가하기) const textArea = document.querySelector("#comment"); // 2. textArea에 마우스로 클릭했을 때의 이벤트 리스너가 필요함 textArea.addEventListener("click", function () { // 3. 이 때, 행위가 하나 필요함 -> 알림창을 띄워서 '로그인하세요'라고 알려줘야 함 alert("로그인하세요"); }); </script> {{/sessionUser}} </body> </html>
Share article