[v2] Spring 게시판 조금 알고 따라하기 -6

Board 리팩토링 : 서비스 레이어에서 권한 검증을 위한 DTO 활용
HootJem's avatar
Aug 22, 2024
[v2] Spring 게시판 조금 알고 따라하기 -6
Board 리팩토링 : 서비스 레이어에서 권한 검증을 위한 DTO 활용

1. BoardRequest 생성

notion image
toEntity(User sessionUser) 메서드를 사용하여 DTO를 엔티티로 변환합니다. 주의할 점은 sessionUser 가 null 인 경우 작동하지 않기 때문에 로그인 환경에서만 작동합니다.
public class BoardRequest { @Data public static class SaveDTO { private String title; private String content; public Board toEntity(User sessionUser) { return Board.builder() .title(title) .content(content) .user(sessionUser) .build(); } } }
notion image
user 나 session 에서 오류 발생하면 엔티티내부의 빌더에 User 가 추가되어 있지 않기 때문입니다.
따라서 해당 부분을 추가해 주어야합니다.
 
@PostMapping("/board/save") public String save(BoardRequest.SaveDTO saveDTO) { User sessionUser = (User) session.getAttribute("sessionUser"); boardRepository.save(saveDTO.toEntity(sessionUser)); return "redirect:/board"; }
@Transactional public void save(Board board) { Query query = em.createNativeQuery("insert into board_tb (title, content, created_at) values (?, ?,now())"); query.setParameter(1, title); // position : ? 의 순서 query.setParameter(2, content); // 이 쿼리를 query.executeUpdate(); } // 변경 후 @Transactional public void save(Board board) { em.persist(board); }
다소 복잡했던 코드가 단순해 졌습니다.
 

2. 컨트롤러에서 세션 체크

앞서 이야기 한 것 처럼 sessionUser 를 매개변수로 전달하기 때문에 이게 null 값을 가지면 에러가 발생합니다. 따라서 session 에 유저 정보가 있는지 (로그인 정보) 확인을 해야합니다.
@PostMapping("/board/save") public String save(BoardRequest.SaveDTO saveDTO) { //스프링 기본 전략 = x-www-form-urlencoded 작성 /보드 레포지토리 객체는 ioc 에 있음 따라서 autowired 함 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) { throw new RuntimeException("로그인이 필요헙니다."); } boardRepository.save(saveDTO.toEntity(sessionUser)); return "redirect:/board"; }
if문 추가를 통해 세션 정보가 없을 시 예외를 던지도록 추가했습니다.
 
@Test public void save_test() { // 1. given (매개변수를 강제로 만들어주는것) String title = "제목"; String content = "내용 1"; // 2. when boardRepository.save(Board.builder().title(title).content(content).build()); // 3. eye(눈으로 확인) }
테스트 코드 수정을 하고 실행해 보면 무사히 완료 됩니다 ^^*
 

3. 인가설정

notion image
notion image
로그인을 하지 않은 상태이거나 다른 작성자의 글에도 수정 삭제 버튼이 활성화 됩니다.
이때 필요한게 인가설정인데 세션에 저장된 정보와 게시글 작성자 정보를 비교하여 처리하겠습니다.
@GetMapping("/board/{id}") public String detail(@PathVariable("id") Integer id, HttpServletRequest request) { Board board = boardRepository.findById(id); request.setAttribute("model", board); request.setAttribute("isOwner", false); System.out.println(board.toString()); return "board/detail"; }
notion image
notion image
일단 일괄적으로 isOwner 을 확인하여 false 일 때 안보이도록 가려놓았습니다.
하드코딩 해놓았기 때문에 모두 안보이겠죠
일단 다음으로 넘어갑니다.

4. serviec 추가 계속되는 리팩토링.

비즈니스 로직은 보통 serviec 레이어에서 해결하게 됩니다.
해결 못한 인가 설정을 위해 로직을 한 번 작성해 봅니다.
notion image
@RequiredArgsConstructor @Service public class BoardService { private final BoardRepository boardRepository; public Board 상세보기(int id, User sessionUser) { Board board = boardRepository.findById(id); // 조인 (Board - User) boolean isOwner = false; // (findById 에서 처리해서 얘는 절대 null 이 올 수 없음) if (board.getUser().getId() == sessionUser.getId()) { isOwner = true; } return board; } }
id 를 통해 게시물을 찾고 idOwner 을 적절하게 처리하는 과정이 추가되었습니다.
이 코드의 문제점! isOwner 을 리턴하지 못합니다 혹은 board를. (리턴값은 항상 하나이기 때문에)
 
그렇다고 권한 체크하는 부분을 Controller 에 넘길 수 없습니다. 왜냐하면 이러한 권한 체크는 컨트롤러의 권한이 아니기 때문입니다.
이럴때 데이터 가공을 위해 필요한 DTO를 생성합니다.
 

5. BoardDTO 추가 (BoardResponse)

notion image
public class BoardResponse { @Data public static class DetailDTO { // 화면에서 안보여도 pk 는 꼭 넣어줘야함!!! private Integer boardId; private String title; private String content; private Integer userId; private String username; private Boolean isOwner; public DetailDTO(Board board, User sessionUser) { this.boardId = board.getId(); this.title = board.getTitle(); this.content = board.getContent(); this.userId = board.getUser().getId(); this.username = board.getUser().getUsername(); this.isOwner = false; if (board.getUser().getId() == sessionUser.getId()) { isOwner = true; } } } }
DTO 단에서 데이터 가공을 해줍니다.
생성자를 통해 DetailDTO 의 필드를 초기화 합니다. 생성자 내부
if (board.getUser().getId() == sessionUser.getId()) { isOwner = true; }
라인이 isOwner 필드의 값을 결정하게 됩니다. (문제가 해결되었습니다!)
 
리턴 타입이 DetailDTO 가 되며 컬럼명이 변경되었습니다. Board의 id → boardId User 의 id → userId (템플릿 코드 바꿔주어야 합니다.)

5. service 로직 재수정

필요한 데이터를 DetailDTO 에 넣었기 때문에 리턴은 BoardResponse.DetailDTO 가 됩니다.
public BoardResponse.DetailDTO 상세보기(int id, User sessionUser) { Board board = boardRepository.findById(id); // 조인 (Board - User) return new BoardResponse.DetailDTO(board, sessionUser); }
서비스에서 repository 에게 요청을 보내도록 수정했기 때문에 Controller 도 수정을 합니다.
 

6. Controller 수정

@GetMapping("/board/{id}") public String detail(@PathVariable("id") Integer id, HttpServletRequest request) { // Board board = boardRepository.findById(id); // 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"; }
주석된 부분이 기존의 코드입니다. isOwner 을 처리 못하는 코드입니다.
Service 에서 호출한 DTO 를 통해 지금 로그인한 유저와 게시글을 작성한 사람의 정보 비교가 완료되었기 때문에 detailDTO 가 모든 정보를 갖고 있게 됩니다.
notion image

7. 템플릿 수정 및 인가

notion image
변경 후 실행을 합니다.
notion image
notion image
이렇게 인가 설정이 완료되었습니다.
 
스프링부트 게시판 시리즈 v2 -1. https://inblog.ai/hj/27190 (User 테이블 생성 및 쿼리 수정) -2. https://inblog.ai/hj/27193 (User, Board 테이블 조인 과 JPQL) -3. https://inblog.ai/hj/27224 (회원 가입) -4. https://inblog.ai/hj/27225 DTO 를 통한 리팩토링 -5. https://inblog.ai/hj/27310 로그인, 로그아웃 -6. https://inblog.ai/hj/27316 서비스 레이어 추가 및 DTO 활용 -7. https://inblog.ai/hj/27430 예외처리 핸들러 설정과 User 서비스 리팩토링 -8. https://inblog.ai/hj/27431 Board 기능 리팩토링 -9. https://inblog.ai/hj/27560 게시글 수정, 더티체킹(flush) -10. https://inblog.ai/hj/27561 인터셉터, AOP 사용 / 마무리
 
Share article

[HootJem] 개발 기록 블로그