Contents
화면 만들기1. 다운 받은 폼 templates폴더에 user폴더 만들어서 넣기 2.연결하는 컨트롤러 만들기3. layout header로 이동4. 회원가입 시작함 유저레파지토리로 이동5.테스트번외 @6. 유저 레파지토리 가서 timestamp 지우기7. user 클래스 가서8. 유저 컨트롤러 이동()여기까지 함9. DTO (data transfer object) 클래스로 받기로그인 1. 유저리포지토리 이동2. 테스트3. 컨트롤러 이동4. 해더로 이동5. 유저컨트롤러 이동6. 보드 컨트롤러 이동7. 보드리퀘스트 클래스 만들기8. 보드에 timestemp9. 보드 리퀘스트 수정10. 다시 컨트롤러 11. 보드 레파지 토리 이동12. 인증 체크바꿀 것 현 시점(잘 못 적어서 바꿔야함)(해야할 것)다시 돌아가서 리팩토링 해야함13. 보드 컨트롤러로 이동14. 보드 디테일 머스테치15. 로그인 폼 머스테치 이동16. 디테일로 가서서비스에 대해 책임?@DI@17. DI 해준다보드에 서비스클래스 넣기18. 상세보기19. DTO 만들자최종 코드?이전 코드최종 정리마지막 프로젝트 설명번외화면 만들기
1. 다운 받은 폼 templates폴더에 user폴더 만들어서 넣기

2.연결하는 컨트롤러 만들기
중요
주소에 user를 안 적는다 왜?
앞에 user, 이나 이런 거 적는 이유
1. 어디에 연결됐는지 알려고
2. /user/* -> 인증이 필요해
/board/* -> 인증이 필요해
주소로 통일 할 수 있어서 인증을 위한 기능은 앞에 엔티티명 안 적는게 유리하다
만약 적었다면 /api/*라는 주소는 인증이 필요해 라고 설계한다
로그인이 필요하면 주소 앞에 api넣고 로그인이 필요 없다면 api를 안 붙이면 된다. 개인적으로는 안 붙이는게 좋다
package shop.mtcoding.blog.user;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class UserController {
@GetMapping("/join-form")
public String joinForm() {
return "user/join-form";
}
@GetMapping("/login-form")
public String login() {
return "user/login-form";
}
}
3. layout header로 이동

4. 회원가입 시작함 유저레파지토리로 이동
레파지토리에는 메서드명을 기능명으로 넣지 마
- JPQL말고 하이버네이트 기술로 사용할 거임
평소에 하이버네이트라고 했지만 정확하게 퍼시스턴스컨택스트(persistence(영구히 기록한다) Context(문맥))라 한다
- 영속성 컨택스트(persistence context)
영구히 기록되는 왔다 갔다 상황을 모두 알고있다는 것임
여기 user들어오면 user객체로 들어온다
new User하면 빈 생성자 → 비영속 객체 라고 한다
- 비영속 객체
아직 적시지도 않았다? new User하면 만드는 빈 생성자를 말함
- 영속 객체
조회 돼서 들어간 데이터(DB데이터에 동기화 돼있음)

중요
비영속 객체(빈 생성자)에 프라이머리 객체 빼고 하이버네이트에 넣으면 알아서 insert날려버린다
package shop.mtcoding.blog.user;
import jakarta.persistence.EntityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.sql.Timestamp;
import java.time.LocalDateTime;
@Repository
public class UserRepository {
@Autowired
private EntityManager em;
//이 메서드에서 한 것 쿼리 없고 프라이머리키 빼고 넣어서 오토 인크리먼트 될거임 -> 알아서 insert함
//빈 객체를 적시면 id없네 하고 insert함
@Transactional
public void save(String username, String password, String email) {
Timestamp now = Timestamp.valueOf(LocalDateTime.now());
User user = new User();
user.setUsername(username);
user.setPassword(password);
user.setEmail(email);
user.setCreatedAt(now);
em.persist(user);
}
}
5.테스트
user폴더 만들고 UserRepositoryTest 클래스 만들어주자
하이버 네이트에 객체만 던져주면 알아서 insert해준다
알아서 인서트 해줌
인포트는 springframework로

package shop.mtcoding.blog.user;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Import;
@DataJpaTest
@Import(UserRepository.class)
public class UserReopsitoryTest {
@Autowired
private UserRepository userRepository;
@Test
public void save_test() {
String username = "haha";
String password = "1234";
String email = "haha@gmail.com";
userRepository.save(username, password, email);
}
}
여기서는 @RequiredArgusConstructor이 안되네 흐음
번외 @
단일 스레드, 멀티 스레드 설명6. 유저 레파지토리 가서 timestamp 지우기

7. user 클래스 가서

@CreationTimestamp 는 빈 생성자 때 사용가능
8. 유저 컨트롤러 이동()
@Autowired
private UserRepository userRepository;
@PostMapping("/join")
public String join(@RequestParam("username")String username,@RequestParam("passord") String password,@RequestParam("email") String email) {
userRepository.save(username, password, email);
return "redirect:/login-form";
}
@RequestParam이거 원래는 안 붙여도 괜찮은데 spring의 버그 때문에 실행이 안 된다면 일단 붙여주자!
- requestget파라미터로 받기
- 클래스로 받기
여기까지 함
ㅇㅇ9. DTO (data transfer object) 클래스로 받기
2가지로 나뉨
- 요청을 받는 DTO, 2. 응답(response)을 위한DTO가 있다
- 요청 받는 DTO(Repuest DTO)
용도: 클라이언트가 서버에 요청을 보낼 때 필요한 데이터 구조를 정의합니다.
- 예: 사용자가 로그인할 때 아이디와 비밀번호를 보내기 위한 객체, 게시물을 작성할 때 제목과 내용을 보내기 위한 객체 등.
- 특징: 클라이언트가 서버에 요청할 때 필요한 데이터를 담고 있으며, 검증과 데이터 변환을 담당할 수 있습니다
- 응답을 위한 DTO(Response DTO)
용도: 서버가 클라이언트에게 응답할 때 데이터를 담아서 보내기 위한 객체입니다.
- 예: 로그인 성공 후 사용자 정보를 반환하거나, 게시물 목록을 클라이언트에 전달할 때 사용하는 객체 등.
- 특징: 서버에서 클라이언트로 반환할 데이터를 구조화하여 전달합니다. 보통 클라이언트가 필요로 하는 정보를 포함하며, 추가적인 포맷팅이나 변환을 수행할 수 있습니다.
아까 게시글 상세조회했지? userjoin했지? username 이런거 10개들 다 들고왔지?
근데 화면에 이런 것들이 다 필요했나? 아님 title, content username만 필요했지
우리가 과하게 줬음 → 필요한 것만 줘야 한다!!
주는 애도 title, content 받는 바구니를 만드는게 좋다
요청 바디 데이터 받으려고 DTO만든다
9.1 UserRequest클래스 만들기
예전에는 UserLoginRequestDTO, UserJoinRequestDTO 막 다 만들어 줘야 했다
그래서 그냥 스태틱(main시작 전에 만들어짐)으로 만들어서 내부 클래스로 만든다!!
package shop.mtcoding.blog.user;
import lombok.Data;
//요청 바디 데이터 받으려고 DTO만듬
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;
}
}
9.2 유저리퀘스트 조금 손 볼거다
package shop.mtcoding.blog.user;
import lombok.Data;
//요청 바디 데이터 받으려고 DTO만듬
public class UserRequest {
//게터 세터 만들기
@Data
public static class JoinDTO {
private String username;
private String password;
private String email;
//컨버팅된다
//빌더패턴 유저 new해주는 것
//책임 -> DTO를 유저 오브잭트(엔티티)로 만들어줌
public User toEntity() {
return User.builder().username(username).password(password).email(email).build();
}
}
@Data
public static class LoginDTO {
private String username;
private String password;
}
}
9.3 다시 유저 컨트롤러로
@PostMapping("/join")
public String join(UserRequest.JoinDTO joinDTO) {
userRepository.save(joinDTO.toEntity());
return "redirect:/login-form";
}
전달 받은 객체는 비 영속 객체다
9.4 다시 유저 리포지토리로
package shop.mtcoding.blog.user;
import jakarta.persistence.EntityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
@Repository
public class UserRepository {
@Autowired
private EntityManager em;
//이 메서드에서 한 것 쿼리 없고 프라이머리키 빼고 넣어서 오토 인크리먼트 될거임 -> 알아서 insert함
//빈 객체를 적시면 id없네 하고 insert함
@Transactional
public void save(User user) {
//지금은 비영속 객체
//프라이머리 키 뺀 이름, 비번, 이메일을 가지고 있다.
//담기 전에는 heap에 있다
System.out.println("담기기전 : " + user.getId());
em.persist(user);
//담긴 후 영속 객체다
System.out.println("담긴 후 : " + user.getId());
}
}
로그인
1. 유저리포지토리 이동
//조회 커리
public User findByUsernameAndPassword(String username, String password) {
Query query = em.createQuery("select u from User u where u.username=:username and u.password=:password", User.class);
query.setParameter("username", username);
query.setParameter("password", password);
User user = (User) query.getSingleResult();
return user;
}

2. 테스트
3. 컨트롤러 이동
@Autowired
private HttpSession session;
@PostMapping("/login")
public String login(UserRequest.LoginDTO loginDTO) {
User sessionUser = userRepository.findByUsernameAndPassword(loginDTO.getUsername(), loginDTO.getPassword());
//null이 아니면 세션에 넣어준다
session.setAttribute("sessionUser", sessionUser);
return "redirect:/board";
}
찾았으면 락카에 넣기 안 찾아지면 인증 안 된거임
헬스장에 놀러와서 자기 정보 넣고 간거임
번외
http해더가 중요하다


아직까지 세션 없다
로그인하면 세션 아이디 받고 쿠키에 세션아이디 받는다 application에 가면 있음


락카 key를 받은거임
상세보기 클릭하면 5 클릭하면 requestHeader에 쿠키라는 것을 가지고 간다.(요청할 때마다key를 가지고 간다 ) → 이런게 프로토콜이다


머스테치 문법 설명
머스테치 문법
- {{key}} → 출력
- for문 if문(연산X)
- 컬렉션타입 {{#컬렉션}} {{/컬렉션}} → 반복문 오브젝트 타입 {{#오브젝트}} {{/오브젝트}} → 존재하면 실행, 아니면 실행안함(완벽한 반복문은 아니다) Boolean타입 {{#Boolean}} {{/Boolean}} → true면 실행 false는 실행안함 session → num = 1일 때 이거는 될까? {{#num ==1 }} 안됨!! 만약 {{model.user.id == sessionUserId}} 이것도 안됨!!
- 꺽쇠{{>}}
- 컬렉션 타입이 단순 자료형이면 .
- 컬렉션의 타입이 Object가 아닌 경우, 일반 타입일 경우 컬렉션<Stirng> {{#컬렉션}} {{.}} {{/컬렉션}} . 을 사용한다 컬렉션<Board> {{#컬렉션}} 중간에 {{title}} {{/컬렉션}} 넣기만 하면 됨
4. 해더로 이동

if else사용 {{^}}를 사용한다

상대 IP에 들어가서 상대 application가서 name, value에 넣어주면 로그인 가능하다
엑세스 공격도 해보자
5. 유저컨트롤러 이동

세션 모든거 remove시킬거임
request요청하러 헬스장에 갔어 (카드 들고있어) 그리고 집 갈께 하고 키 들고 락카 날리는 것
스스로는 날릴 수 없다. 락카 전체를 날리는 방법뿐이 없다?
쿠키는 내 호주머니
세션은 락카
request는 가방
면접질문 보드 컨트롤러에서 왜 /board/save save를 붙였는가?
나만의 약속이다 http1.0버전으로 post(모든 것 다 할 거니까)와 get만 사용해서 만들기로 했음
4가지 방식은 1.1버전에 나옴
최신은 3.0
6. 보드 컨트롤러 이동
@PostMapping("/board/save")
public String save(@RequestParam("title") String title, @RequestParam("content") String content) { //스프링 기본 견략 = x-www-form-urlencoded 파싱 매개변수만 같으면 됨 화면에 폼테그 name 과 반드시 같아야 한다!!
boardRepository.save(title, content); //보드 레파지토리 객체가 어디있나? Ioc에 있다 autoWrid해서 가져옴
return "redirect:/board";
}
이제 DTO로 바꿀 거다
7. 보드리퀘스트 클래스 만들기
스태틱으로 만들어야 한다!
인서트 할 때 이름 만드는게 좋다(통일해야 함)
shift + enter
package shop.mtcoding.blog.board;
import lombok.Data;
import shop.mtcoding.blog.user.User;
public class BoardRequest {
@Data
public static class SaveDTO {
private String title;
private String content;
//포린키 받아야 하는데 클라이언트는 모름 세션에서 꺼내와야 한다 세션에 id있어서
//퍼시스트 할거다 보드 오브젝트로 해야함
public Board toEntity(User sessionUser) {
return Board.builder()
.title(title)
.content(content)
.user(sessionUser)
.build();
}
}
}
8. 보드에 timestemp
추가

설명

fk는 세션에서 꺼내서 사용할 수 있다 없으면 포린키가 안 잡힌다
9. 보드 리퀘스트 수정

.user 붙이면 빨간줄 생길건데 이거는
보드의 빌더에 user추가 안 돼잇어서 그럼 그래서 생성자 추가해주자 User추가

10. 다시 컨트롤러
save DTO로 바꿀거임

지금은 세션 없어서 오류남
코드 고치면서 만들거임

세션 오토와이어드 해주자
오토와이어드 할 수 있는
IOC컨트롤러에 자동으로 세션 넣어둔다 아니면 내가 불편함 request막 해야 사용할 수 있다
10.2 세션 냅다 넣으면
그냥 냅다 넣으면? 안됨

11. 보드 레파지 토리 이동
비 영속 객체를 넣어서 자동으로 insert 되게 만드는 것

12. 인증 체크
문제가
글쓰기 하려면 로그인 안 했다면 못한다 하지만 주소를 쳐서 강제로 들어갈 수 있다 → null이 들어가서 터진다 시스템 망가짐 막아야 해서
인증 체크 해야한다!! 인증체크 안 하면 null이 뜬다
if(sessionUser == null){
}이거 해줌

@PostMapping("/board/save")
public String save(BoardRequest.SaveDTO saveDTO) { //스프링 기본 견략 = x-www-form-urlencoded 파싱 매개변수만 같으면 됨 화면에 폼테그 name 과 반드시 같아야 한다!!
//세션유저가 null이면 인증 안됨 null아니면 인증됨
User sessionUser = (User) session.getAttribute("sessionUser");
//인증 체크 필요함
if (sessionUser == null) {
throw new RuntimeException("로그인이 필요합니다");
}
boardRepository.save(saveDTO.toEntity(sessionUser)); //보드 레파지토리 객체가 어디있나? Ioc에 있다 autoWrid해서 가져옴
return "redirect:/board";
}
바꿀 것 현 시점(잘 못 적어서 바꿔야함)
application.properties가서 session바꿔주자!!

세션 값 받아오는 것을 다른 것으로 해와서 쿠키는 들어가는데 로그인이 실행이 안됐던 거임!
(해야할 것)다시 돌아가서 리팩토링 해야함
- 인증 체크가 안돼있다
- 인증은 돼있는데 권한 체크가 안돼 있다
인증체크 안되면 401 뜨고 throw로 떤져야함
권한체크 안돼있으면 403 throw로 떤져야 함
유효성 검사(username이 4자이하라고 설정해놨는데 엄청 많이 적었을 때 ) 400
인증체크
컨트롤러는 값을 잘 받아야 해서 유효성 검사를 해야한다!
예로 A가 B한테 이체하는 이체 시스템을 만들거임
A 5000
B 5000
A가 2000원 이체한다는 어마운트라는 바디에 담겨 줘야함
0원이 이체된다? -금액 이체? 6000원 이체? 말이 안됨
쫌 다른게 있다 - 금액, 0원은 컨트롤러가 잘 받을 수 있다 DTO로 받아서 if로 -든 0이든 가능
하지만 6000을 컨트롤러가 못 한다 왜? 컨트롤러는 자기가 얼마나 가지고 있는지 모른다
알려면 DB에서 알아봐야 해서 → 그래서 DB정보가 필요해서 컨트롤러에서 못 한다.(기반 정보가 필요함)
- 권한 정보는 DB가 꼭 필요하다
인증체크는 세션것만 보면돼서
레파지토리책임은 CRUD하는것 권한정보는 아님
새로운 레이어가 필요하다
13. 보드 컨트롤러로 이동

이쪽으로 이 게시글의 주인인가요? 아니? 불리언 타입 만들었음

14. 보드 디테일 머스테치

조건문 걸음 (가짜 데이터 강제로 넣음)

15. 로그인 폼 머스테치 이동
value값을 미리 넣어 로그인이나 회원가입 테스트 할 때 또 적지 않게 하기 위해


재실행 하고 회원가입 넣기


왜 하는가? 귀찮아서 넣으면 바로 들어갈 수 있게 적어둬야함
그러면 글쓰기도 적어두면 좋지 않을까?
16. 디테일로 가서

불리언 할 때 이게 상수가 되면 안된다 이거는 새로운 레이어가 필요하다
서비스에 대해 책임?@
서비스의 책임과 레이지 로딩 설명 및 예제DI@
DI17. DI 해준다
보드 컨트롤러

지금까지 한 것들 다 DI로 바꿔주자!!
보드에 서비스클래스 넣기
18. 상세보기
왜 세션을 그대로 안받나?
User user = (User)session.getAttribute(”sessionUser”) 이거 또 해줘야 해서!
public Board 상세보기(int id, User sessionUser) {
Board board = boardRepository.findById(id); //조인해서 보드안에 User포함돼 있다. 이것만 리턴하면 끝인가? 1. 보드가 널인가? 이미 처리해줌 비지니스로직 꼼꼼하게 해야
//2. 이 게시글에 get유저 가 session(로그인 한 사람)과 같은지 지금 세션 정보 없으니까 User sesswionUser 컨트롤러한테 받아오자
//같으면 주인 아니면 주인X
boolean isOwner = false;
if (board.getUser().getId() == sessionUser.getId()) {
isOwner = true;
}
// return board; 하면 isOwner못 돌려주는 억울한 일이 생긴다 board, isOwner둘 다 받아야 한다 그래서 DTO를 만든다!
//이유 2. 레이지 로딩 끌거니까
return board;
}
결정체를 서비스에서 받아와야 한다
필요한 것만 전달해 줘야한다!!
package shop.mtcoding.blog.board;
//지금
// C -> S -> R
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import shop.mtcoding.blog.user.User;
@RequiredArgsConstructor
@Service
public class BoardService {
private final BoardRepository boardRepository;
//기능명을 한글로적는다 정확해야해서 그냥 마음대로 해도 됨
public Board 상세보기(int id, User sessionUser) {
Board board = boardRepository.findById(id); //조인해서 보드안에 User포함돼 있다. 이것만 리턴하면 끝인가? 1. 보드가 널인가? 이미 처리해줌 비지니스로직 꼼꼼하게 해야
//2. 이 게시글에 get유저 가 session(로그인 한 사람)과 같은지 지금 세션 정보 없으니까 User sesswionUser 컨트롤러한테 받아오자
//같으면 주인 아니면 주인X
boolean isOwner = false;
if (board.getUser().getId() == sessionUser.getId()) {
isOwner = true;
}
// return board; 하면 isOwner못 돌려주는 억울한 일이 생긴다 board, isOwner둘 다 받아야 한다 그래서 DTO를 만든다!
//이유 2. 레이지 로딩 끌거니까
return board;
}
}
생각 해야 할 것
- 화면에는 어떤 필드가 필요한가? title, content, username, isOwner 필요함 아이디 잇으면 if할거잖아
- User에 패스워드도 있어서 필요 없다
19. DTO 만들자
package shop.mtcoding.blog.board;
import lombok.Data;
import shop.mtcoding.blog.user.User;
public class BoardResponse {
//보드 응답 DTO 상세보기
@Data
public static class DetailDTO {
private Integer boardId;
private String title;
private String content;
private Boolean isOwner;
private Integer userId;
private String username;
//가장 중요한 것 entitiy 총 2개 board, user
//프라이머리키는 반드시 줘야해 프론트가 뭘 할 수도 있으니까
public DetailDTO(Board board, User sessionUser) {
this.boardId = board.getId();
this.title = board.getTitle();
this.content = board.getContent();
this.isOwner = false;
if (board.getUser().getId() == sessionUser.getId()) {
isOwner = true;
}
this.userId = board.getUser().getId();
this.username = board.getUser().getUsername();
}
}
}
서비스에서 isOwner빼오자 이게 편하다
응답 DTO는 서비스에서 하나의 결정채 만들어서 넣는 거임
옮기기만 하면 된다 왜 만드냐 근거 → 화면에 보여주려고
다시 서비스로
package shop.mtcoding.blog.board;
//지금
// C -> S -> R
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import shop.mtcoding.blog.user.User;
@RequiredArgsConstructor
@Service
public class BoardService {
private final BoardRepository boardRepository;
//기능명을 한글로적는다 정확해야해서 그냥 마음대로 해도 됨
public BoardResponse.DetailDTO 상세보기(int id, User sessionUser) {
Board board = boardRepository.findById(id); //조인해서 보드안에 User포함돼 있다. 이것만 리턴하면 끝인가? 1. 보드가 널인가? 이미 처리해줌 비지니스로직 꼼꼼하게 해야
//2. 이 게시글에 get유저 가 session(로그인 한 사람)과 같은지 지금 세션 정보 없으니까 User sesswionUser 컨트롤러한테 받아오자
//같으면 주인 아니면 주인X
// return board; 하면 isOwner못 돌려주는 억울한 일이 생긴다 board, isOwner둘 다 받아야 한다 그래서 DTO를 만든다!
//이유 2. 레이지 로딩 끌거니까
// 3. 뭐였지
return new BoardResponse.DetailDTO(board, sessionUser);
}
}
화면에 필요한 것만 넣으려고
앱이든 웹이든 상관없어짐.. 한방에 리턴해서
마무리
컨트롤러 가서 리포지토리가 아닌 서비스로 이동하게 만들어야 한다

detail가서

앱은 JSON으로 바꿔 줘야 하는데 애매한 데이터로 받아서 또 앱에서 연산해야 한다
srp 안해서 그럼 구분 안 시켜 줘서 그럼
브라우저는 HTML 받아야 한다
야구 게임을 만든다 야구 관련 데이터가 있는데 화면에 뿌려야 하는게 점수가 몇점났는지 뿌려야 한다 단순한 프로그램이다.
→ DB에 요청 하려면 C → S → R → DB(DB에 5점이라는 데이터가 없고 1루(true), 2루(true), 3루(true)라는 컬럼이 있고 전부 트루고 행위라는 데이터에 1루타 가 있다. 이러면 1점이고 행위가 홈런이면 4점
데이터가 들어가 있는데 이제 HN로 들어가서 컨트롤러가 트루트루트루 홈런을 받았다
HTML코드로 만들어서 (점수:) 여기까지 고
머스테치로 컨트롤러로 만들어서 집어 넣어야 하는데 머스테치에 막 if 트루가 1점이면 1점 2개면 2점 이렇게 막 코드를 넣어버림 → 화면에서 연산 해야함 그래서 화면에 넣음
아직까진 괜찮았어
ssr 서버 사이트 랜더링(서버쪽에서 해석함) 여기서는 서버에서 코드 해석해서 화면에 뿌림
갑자기 앱을 해야해 ! 앱한테 필요한 것은
모델로 받아 온 데이터가 5점이 아니고 트루트루트루 홈런을 그대로 받는다
현재 컨트롤러는 view만 해서
아 컨트롤러 하나 더 만들어야겠네(데이터 넣는 역할로) 해서 service에 연결
똑같이 TTT홈런을 주면 좋아할까? 완벽하게 만들어 줘야함 이렇게 되면 따로 또 연산해서 줘야한다.
어자피 컨트롤러는 해야하는데 모든 것을 서비스에 끝내서 컨트롤러에서는 리턴만 해주면 된다.
이렇게 안 하면 시간이 엄청 오래 걸린다..
서비스 전까지 잘 만들면 컨트롤러만 바꿔주면 된다.
view를 단순하게 랜더링(화면 뿌리는 용도로만) 하자!

이런것 처럼 이미 연산 다 해서 보여주기만 하면 된다.
@ResponseBody를 넣으면 JSON으로 바꿔 적는다.

최종 코드?
이전 코드
- 보드 컨트롤러
package shop.mtcoding.blog.board;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import shop.mtcoding.blog.user.User;
import java.util.List;
//식별자 요청 받기, 응답하기가 이 친구의 책임
//1. 이거 안붙이면 식별자 요청 못 받는다
@RequiredArgsConstructor
@Controller //2. 식별자 요청을 받을 수 있다 이런거를 어노테이션이라 한다 왜 식별자 요청이 되는가 이거는 나중에 설명해준다
public class BoardController {
private final BoardRepository boardRepository;
private final HttpSession session;
private final BoardService boardService;
@GetMapping("/test/board/1")
public @ResponseBody void testBoard() {
//여기까지는 레이지 유저 안 땡겨옴
List<Board> boardList = boardRepository.findAll();
System.out.println("---------------------------------");
//보드 리스트를 리턴해버리면? getter다 때려서 json로 바꿔야 함
System.out.println(boardList.get(2).getUser().getPassword());
System.out.println("---------------------------------------------");
}
// url : http://localhost:8080/board/1/update
//바디 데이터가 title=제목1 변경&content=내용1변경
//content-type : x-www-form-urlencoded
@PostMapping("/board/{id}/update")
public String update(@PathVariable("id") int id, @RequestParam("title") String title, @RequestParam("content") String content) {
boardRepository.updateById(title, content, id);
//상세보기로 가야함
return "redirect:/board/" + id;
}
@PostMapping("/board/{id}/delete")
public String delete(@PathVariable("id") int id) {
//원래는 조회를 하고 삭제해야하는데 V1이라 그냥 함
boardRepository.deleteById(id);
return "redirect:/board";
}
//글쓰기 할게 -> 글쓰기 완료되면 메인으로 보내는게 좋다
@PostMapping("/board/save")
public String save(BoardRequest.SaveDTO saveDTO) { //스프링 기본 견략 = x-www-form-urlencoded 파싱 매개변수만 같으면 됨 화면에 폼테그 name 과 반드시 같아야 한다!!
//세션유저가 null이면 인증 안됨 null아니면 인증됨
User sessionUser = (User) session.getAttribute("sessionUser");
//인증 체크 필요함
if (sessionUser == null) {
throw new RuntimeException("로그인이 필요합니다");
}
boardRepository.save(saveDTO.toEntity(sessionUser)); //보드 레파지토리 객체가 어디있나? Ioc에 있다 autoWrid해서 가져옴
return "redirect:/board";
}
/*
외부에서 이 메서드 어떻게 때리냐면
방법 4가지 get(가지고 오고 요청할 때), post(보내고 insert, delete update 요청할 때), put, delete(지울때)
*/
//리플랙션은 메서드 이름 필요 없다 주소만 필요하지
@GetMapping("/board")
public String list(HttpServletRequest request) {
List<Board> boardList = boardRepository.findAll();
//key값 모델스
//외부에서 /board요청 -> 톰캣에 감 rqeust객체로 만들어둠 -> 때림 -> Model에 rqeust객체 주입됨 -> 이 데이터를 reqeust객체에 넣어버림
request.setAttribute("models", boardList);
return "board/list";//파일의 경로 넣으면 되는데 고정적으로 되어 있음 확장자 자동으로 해주는가? 라이브러리로 머스테치 설정해줘서(templates에 머스테치로 찾아줌)
}
/*
1. 메서드 : get을 사용
2. 주소 : /board/1
3. 응답 : board/detail
1. 메서드 : get을 사용
2. 주소 : /board/1/save-form
3. 응답 : board/save-form
1. 메서드 : get을 사용
2. 주소 : /board/1/update-form
3. 응답 : board/update-form
*/
@GetMapping("/board/{id}")
public String detail(@PathVariable("id") Integer id, HttpServletRequest request) {
Board board = boardRepository.findById(id);
//한건이니까 model
//여러건이면 models
request.setAttribute("model", board);
request.setAttribute("isOwner", false);
User sessionUser = (User) session.getAttribute("sessionUser");
// BardResoponse.DetailDTO detailDTO = boardService.상세보기(id, sessionUser);
//request.setAttribute("model", detailDTO);
return "board/detail";
}
@GetMapping("/board/save-form")
public String saveForm() {
return "board/save-form";
}
@GetMapping("/board/{id}/update-form")
public String updateForm(@PathVariable("id") int id, HttpServletRequest request) {
//못찾으면 터진다! null 안들어옴 우리가 설정해서
Board board = boardRepository.findById(id);
//리퀘스트에 담는다
request.setAttribute("model", board);
return "board/update-form";
}
}
바뀐 보드컨트롤러
package shop.mtcoding.blog.board;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import shop.mtcoding.blog.user.User;
import java.util.List;
//식별자 요청 받기, 응답하기가 이 친구의 책임
//1. 이거 안붙이면 식별자 요청 못 받는다
@RequiredArgsConstructor
@Controller //2. 식별자 요청을 받을 수 있다 이런거를 어노테이션이라 한다 왜 식별자 요청이 되는가 이거는 나중에 설명해준다
public class BoardController {
private final BoardRepository boardRepository;
private final HttpSession session;
private final BoardService boardService;
@GetMapping("/test/board/1")
public @ResponseBody void testBoard() {
//여기까지는 레이지 유저 안 땡겨옴
List<Board> boardList = boardRepository.findAll();
System.out.println("---------------------------------");
//보드 리스트를 리턴해버리면? getter다 때려서 json로 바꿔야 함
System.out.println(boardList.get(2).getUser().getPassword());
System.out.println("---------------------------------------------");
}
// url : http://localhost:8080/board/1/update
//바디 데이터가 title=제목1 변경&content=내용1변경
//content-type : x-www-form-urlencoded
@PostMapping("/board/{id}/update")
public String update(@PathVariable("id") int id, @RequestParam("title") String title, @RequestParam("content") String content) {
boardRepository.updateById(title, content, id);
//상세보기로 가야함
return "redirect:/board/" + id;
}
@PostMapping("/board/{id}/delete")
public String delete(@PathVariable("id") int id) {
//원래는 조회를 하고 삭제해야하는데 V1이라 그냥 함
boardRepository.deleteById(id);
return "redirect:/board";
}
//글쓰기 할게 -> 글쓰기 완료되면 메인으로 보내는게 좋다
@PostMapping("/board/save")
public String save(BoardRequest.SaveDTO saveDTO) { //스프링 기본 견략 = x-www-form-urlencoded 파싱 매개변수만 같으면 됨 화면에 폼테그 name 과 반드시 같아야 한다!!
//세션유저가 null이면 인증 안됨 null아니면 인증됨
User sessionUser = (User) session.getAttribute("sessionUser");
//인증 체크 필요함
if (sessionUser == null) {
throw new RuntimeException("로그인이 필요합니다");
}
boardRepository.save(saveDTO.toEntity(sessionUser)); //보드 레파지토리 객체가 어디있나? Ioc에 있다 autoWrid해서 가져옴
return "redirect:/board";
}
/*
외부에서 이 메서드 어떻게 때리냐면
방법 4가지 get(가지고 오고 요청할 때), post(보내고 insert, delete update 요청할 때), put, delete(지울때)
*/
//리플랙션은 메서드 이름 필요 없다 주소만 필요하지
@GetMapping("/board")
public String list(HttpServletRequest request) {
List<Board> boardList = boardRepository.findAll();
//key값 모델스
//외부에서 /board요청 -> 톰캣에 감 rqeust객체로 만들어둠 -> 때림 -> Model에 rqeust객체 주입됨 -> 이 데이터를 reqeust객체에 넣어버림
request.setAttribute("models", boardList);
return "board/list";//파일의 경로 넣으면 되는데 고정적으로 되어 있음 확장자 자동으로 해주는가? 라이브러리로 머스테치 설정해줘서(templates에 머스테치로 찾아줌)
}
/*
1. 메서드 : get을 사용
2. 주소 : /board/1
3. 응답 : board/detail
1. 메서드 : get을 사용
2. 주소 : /board/1/save-form
3. 응답 : board/save-form
1. 메서드 : get을 사용
2. 주소 : /board/1/update-form
3. 응답 : board/update-form
*/
@GetMapping("/board/{id}")
public String detail(@PathVariable("id") Integer id, HttpServletRequest request) {
//Board board = boardRepository.findById(id);
//한건이니까 model
//여러건이면 models
// request.setAttribute("model", board);
request.setAttribute("isOwner", false);
User sessionUser = (User) session.getAttribute("sessionUser");
BoardResponse.DetailDTO detailDTO = boardService.상세보기(id, sessionUser);
request.setAttribute("model", detailDTO);
return "board/detail";
}
@GetMapping("/board/save-form")
public String saveForm() {
return "board/save-form";
}
@GetMapping("/board/{id}/update-form")
public String updateForm(@PathVariable("id") int id, HttpServletRequest request) {
//못찾으면 터진다! null 안들어옴 우리가 설정해서
Board board = boardRepository.findById(id);
//리퀘스트에 담는다
request.setAttribute("model", board);
return "board/update-form";
}
}
서비스
package shop.mtcoding.blog.board;
//지금
// C -> S -> R
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import shop.mtcoding.blog.user.User;
@RequiredArgsConstructor
@Service
public class BoardService {
private final BoardRepository boardRepository;
//기능명을 한글로적는다 정확해야해서 그냥 마음대로 해도 됨
public BoardResponse.DetailDTO 상세보기(int id, User sessionUser) {
Board board = boardRepository.findById(id); //조인해서 보드안에 User포함돼 있다. 이것만 리턴하면 끝인가? 1. 보드가 널인가? 이미 처리해줌 비지니스로직 꼼꼼하게 해야
//2. 이 게시글에 get유저 가 session(로그인 한 사람)과 같은지 지금 세션 정보 없으니까 User sesswionUser 컨트롤러한테 받아오자
//같으면 주인 아니면 주인X
// return board; 하면 isOwner못 돌려주는 억울한 일이 생긴다 board, isOwner둘 다 받아야 한다 그래서 DTO를 만든다!
//이유 2. 레이지 로딩 끌거니까
// 3. 뭐였지
return new BoardResponse.DetailDTO(board, sessionUser);
}
}
보드 리스폰스
package shop.mtcoding.blog.board;
import lombok.Data;
import shop.mtcoding.blog.user.User;
public class BoardResponse {
//보드 응답 DTO 상세보기
@Data
public static class DetailDTO {
private Integer boardId;
private String title;
private String content;
private Boolean isOwner;
private Integer userId;
private String username;
//가장 중요한 것 entitiy 총 2개 board, user
//프라이머리키는 반드시 줘야해 프론트가 뭘 할 수도 있으니까
public DetailDTO(Board board, User sessionUser) {
this.boardId = board.getId();
this.title = board.getTitle();
this.content = board.getContent();
boolean isOwner = false;
if (board.getUser().getId() == sessionUser.getId()) {
isOwner = true;
}
this.userId = board.getUser().getId();
this.username = board.getUser().getUsername();
}
}
//V2는 업그래이드 버전 (보기가 편해진다)
@Data
public static class DetailDTOV2 {
private Integer id;
private String title;
private String content;
private Boolean isOwner;
private UserDTO user;
public DetailDTOV2(Board board, User sessionUser) {
this.id = board.getId();
this.title = board.getTitle();
this.content = board.getContent();
this.isOwner = false;
if (board.getUser().getId() == sessionUser.getId()) {
isOwner = true;
}
this.user = new UserDTO(board.getUser());
}
@Data
public class UserDTO {
private Integer id;
private String username;
public UserDTO(User user) {
this.id = user.getId();
this.username = user.getUsername();
}
}
}
}
실수 @
실수최종 정리
- 레이어! 명확하게 알아야 한다
- 톰켓 → DS → C → S → R
C 책임
- 요청(유효성 검사, 인증체크, 주소 잘 받기, 바디 데이터(title, content 같이) 잘 받기 등), 2. 응답(view(@Controller), 데이터(@RestController))
S(기능) 책임
이체하기 하면 db에 두 가지 update해야한다 일의 최소 단위를 생각해야 한다.
업데이트 2번 치는게 최종 업데이트다 2가지 일 해야하고 이게 트랜잭션이다
- 트랜잭션 관리(단순함 모든 일 다 되면 커밋 안되면 롤백) 왜 레파지토리에서 트랜잭션 걸면 안됨? 일의 최소단위가 레파지토리 하나가 아니기 때문이다!
- 비지니스 로직관리(즉 트랜잭션 관리임) 이체한다 put controller에서 put (/account/transfer) 그냥 기능이여서 동사 때려버림 서비스 메서드이름은 한글로하면 이체하기, 영어로 하면 transfer 레파지토리는 update하나 있으면 된다. 서비스에서 해야 할 것 1. 1번 업데이트, 2번 업데이트, 3번 히스토리 input 이것들을 합쳐서 트랜잭션 즉 비지니스 로직이다
- 레이지 로딩 완료하기 (응답의 DTO 만들기) 왜 여기서 완료해라 하냐면 화면에 필요한 것들을 여기서 완료해라!!
OSIV종료 왜하냐? 예상치 못한 LazyLoding때문에
account 테이블이 있다면 계좌 히스토리 테이블이 있을 것이다. 이 두개가 핵심 테이블이다
레파지토리 책임
- DB와 일하기
- 파싱
할 것들
예외 나오면 전부 throw해줘서 ds가 해결할 수 있게 만들기
인증처리는 DS앞에 필터링 레이어를 만들거다 매번 확인하는 것 보다 앞단에서 한번에 하느게 좋다
필터도 프록시 패턴같은건가? 할 필요는 없음 자바 보면 필터 꼽아 넣을 자리가 있다
나중에 컨트롤러에 1번 메서드가 있고 3번 메서드가 있다면 1번 3번 앞에서만 hello하고싶으면 이 때 프록시를 사용한다 이거를 AOP로 해볼거다 힌트가 있어야 해서 @ 어노테이션 사용해서 할 수 있다.
마지막 프로젝트 설명
API서버 앱 , Template서버(관리자) 연동되는 브라우저
OAuth, JWT, OPENAI 사용, flutter 사용할거다
다 배워야 한다!! 알아야 하니까 지금 못함
V1의 목적 JPA가 아닌 그냥 CRUD되는지 확인
V2 하이버네이트, DTO 익셉션 핸들러, 리턴될 때 DTO잘되는 방법들 이거 잘 하면 프로젝트 할 수 있다
JS 특강 css특강 클라이언트 사이드 랜더링도 배워야지요.. CSR배우기
V3
V4…
번외
머스테치
중괄호 2개짜리 > 들어오면 예외처리 해버림 글자 그대로 처리해버림
중괄호 3개면 {{{}}} 이스케이프 처리 안한다 꺽쇠 다 막아야 한다.!
JSP는 바로 뿌려진다
공격 가능 어떻게?
중괄호가 3개여서!
패치(url할 때 사용함)로 공격함
<script>fetch(”/board/save”, {method: “POST”,headers: {”Content-Type”: “application/x-www-form0urlencoded; charset=utf-8”},body: “title=코스바보&content=진짜바보”});}</script>
js는 ‘’ 백틱을 사용하면
유효성 검사로 막아야 한다 바디데이터로 막아야 한다
controller에서 했음
만약 꺽쇠가 있다면 바꿔서 넣는다
saveDTO.setContent(saveDTO.getContent().replace(”<”, “<”));
이러면 문 앞에서 막는게 아니다. 굳이 controller하나 하나에 적을 필요 없이 ds는 우리가 손 못대서 filter로
리플랙션 postMapping, putMapping 때 도 할 수 있다. 프록시로 처리가능함 이 프록시를 ds와 controller사이에 AOP로 처리할 수 있다.
이런 공격을 내 쿠키 훔치는 것은 아니니까 의도치 않게 실행하게 만들었다.
xss공격이라 한다
더미에
{{{}}} 넣으면 쫘쫘쫚 나옴
insert into board_tb(title, content, created_at, user_id)
values ('이글을 보는자 블로그를 지배한다',
'<script>for(let i=1; i<6; i++){fetch(`/board/${i}/update`, {method: "POST",headers: {"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"},body: "title=너희글은내가지배한다&content=진짜바보"});}</script>',
now(), 2);
'<script>location.href="http://toto.com";</script>', '내용1', now(), 1); 이러면 사이트로 갈 수가 없다
Share article