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

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

README.md

# 인증 블로그 v2 ## 1. 기술스택 - session, cookie - orm - lazy loading - response(응답) DTO (왜 필요한지?) - Optional, Stream API(map, filter, 어부(물가, 가공, 수집)) - 권한(403)과 인증(401) ## 2. 리펙토링 - ResponseDTO 내부클래스로 수정 ## 3. 기능 - 회원가입 (아이디 중복체크) - 로그인 (쿠키) - 게시글 쓰기 (인증이 된 사람 - 수정) - 게시글 상세 보기 (인증/권한 체크, DTO 만들기 - 수정) - 게시글 수정/삭제 (인증/권한 체크 - 수정) ## 4. Task ### 1. 회원가입 - 그림 다운로드 (v) - user폴더 UserController 만들어서 그림 연결 (v) - User 테이블 생성 - 더미데이터 - UserRepository만들어서 DB 테스트 코드 - 컨트롤러, 서비스, 레포 연결해서 기능 완료하기 ### 2. 로그인 - 컨트롤러, 서비스, DTO, 세션

1. DTO 생성 및 적용

1) BoardRequest.java

C:\workspace\spring_lab\boardv1\src\main\java\com\example\boardv1\board\BoardRequest.java
package com.example.boardv1.board; import lombok.Data; public class BoardRequest { // 책임 : 클라이언트(브라우저)의 요청 데이터를 저장하는 클래스 @Data public static class SaveOrUpdateDTO { private String title; private String content; } }

2) BoardResponse.java

C:\workspace\spring_lab\boardv1\src\main\java\com\example\boardv1\board\BoardResponse.java
package com.example.boardv1.board; import java.util.List; import com.example.boardv1.reply.ReplyResponse; import lombok.Data; public class BoardResponse { @Data public static class DetailDTO { // 화면에 보이지 않는 것 (PK는 화면에 안보여도 무조건 적어야 함!) private int id; private int userId; // 화면에 보이는 것 private String title; private String content; private String username; // 연산해서 만들어야 되는 것 private boolean isOwner; // 게시글의 주인인가? public DetailDTO(Board board, Integer sessionUserId) { this.id = board.getId(); this.userId = board.getUser().getId(); this.title = board.getTitle(); this.content = board.getContent(); this.username = board.getUser().getUsername(); this.isOwner = board.getUser().getId() == sessionUserId; this.replies = board.getReplies().stream() .map(reply -> new ReplyResponse.DTO(reply, sessionUserId)) .toList(); } } }

2) BoardController.java

C:\workspace\spring_lab\boardv1\src\main\java\com\example\boardv1\board\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. User 관련 templates 추가 및 화면 연결

1) join-form.mustache

C:\workspace\spring_lab\boardv1\src\main\resources\templates\user\join-form.mustache
{{> header}} <div class="container p-5"> <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"> </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 class="btn btn-secondary form-control">회원가입</button> </form> </div> </div> </div> </body> </html>

2) login-form.mustache 추가

C:\workspace\spring_lab\boardv1\src\main\resources\templates\user\login-form.mustache
{{> 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"> </div> <div class="mb-3"> <input type="password" class="form-control" placeholder="Enter password" name="password"> </div> <button class="btn btn-secondary form-control">로그인</button> </form> </div> </div> </div> </body> </html>

3) UserControlloer.java

C:\workspace\spring_lab\boardv1\src\main\java\com\example\boardv1\user\UserControlloer.java
package com.example.boardv1.user; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class UserControlloer { @GetMapping("/login-form") public String loginForm(){ return "user/login-form"; } @GetMapping("/join-form") public String joinForm(){ return "user/join-form"; } }

4) 테스트

join
join
login
login

3. 테이블 세팅

1) User.java

C:\workspace\spring_lab\boardv1\src\main\java\com\example\boardv1\user\User.java
package com.example.boardv1.user; import java.time.LocalDateTime; import org.hibernate.annotations.CreationTimestamp; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @NoArgsConstructor // object mapping을 hibernate가 할 때 디폴트 생성자를 new한다. @Getter @Setter @Entity @Table(name = "user_tb") public class User { @GeneratedValue(strategy = GenerationType.IDENTITY) @Id private Integer id; @Column(unique = true) // 테이블의 제약조건 (pk가 unique일 때 인덱스를 만들어준다) private String username; @Column(nullable = false, length = 100) // password 컬럼은 null일 수 없다! 길이는 100자! private String password; private String email; @CreationTimestamp private LocalDateTime createdAt; @Override public String toString() { return "User [id=" + id + ", username=" + username + ", password=" + password + ", email=" + email + ", createdAt=" + createdAt + "]"; } }

2) 쿼리 테스트

notion image

4. 더미데이터 세팅

1) data.sql 에 추가

insert into user_tb (username, passowrd, email) values ('ssar', '1234', 'ssar@nate.com'); insert into user_tb (username, passowrd, email) values ('cos', '1234', 'cos@nate.com');

5. UserRepository 메서드 생성

1) UserReoitory.java

package com.example.boardv1.user; import org.springframework.stereotype.Repository; import jakarta.persistence.EntityManager; import jakarta.persistence.Query; 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 User findByUsername(String username) { Query query = em.createQuery("select u from User u where u.username = :username", User.class); query.setParameter("username", username); return (User) query.getSingleResult(); } public User findById(int id) { return em.find(User.class, id); } }

6. UserRepository 메서드 테스트

1) 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 // EntityManager가 IoC에 등록됨 public class UserRepositoryTest { @Autowired private UserRepository userRepository; @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 save_fail_test() { // given User user = new User(); // 비영속 객체 user.setUsername("ssar"); user.setPassword("1234"); user.setEmail("ssar@nate.com"); // when User findUser = userRepository.save(user); // 영속화됨 // eye System.out.println(findUser); } @Test public void fintByUsername_test() { // given String useString = "ssar"; // when User findUser = userRepository.findByUsername(useString); // eye System.out.println(findUser); } }

2) save_test() 결과

notion image

3) save_fail_test() 결과

notion image
org.hibernate.exception.ConstraintViolationException: could not execute statement [Unique index or primary key violation: "PUBLIC.CONSTRAINT_2 INDEX PUBLIC.CONSTRAINT_INDEX_2 ON PUBLIC.USER_TB(USERNAME NULLS FIRST) VALUES ( /* 1 */ 'ssar' )"; SQL statement: insert into user_tb (created_at,email,password,username,id) values (?,?,?,?,default) [23505-240]] [insert into user_tb (created_at,email,password,username,id) values (?,?,?,?,default)]
➡️ 데이터베이스의 유니크 제약조건(UNIQUE / PRIMARY KEY)을 위반했다는 뜻

4) findByUsername_test() 결과

notion image
 
Share article