JAVA

스트링부트 블로그 만들기 – 서비스 레이어 적용하기

blog app

스트링부트 블로그 만들기 – 서비스 레이어 적용하기

spring
스프링부트 서비스 레이어 사용하는 이유컨트롤러에서 이러한 트렌젝션을 관리하게 되면 데이터베이스에 write하는 동안 다른 사용자의 접근이 불가능해진다. 이러한 문제를 해결하기 위해서 데이터베이스 접근을 하지 못하게 만드는 트렌젝션을 서비스 레이어에서 관리하게한다. ...

서비스 레이어 적용하기

1.service 패키지를 만들어 게시글 서비스 클래스(BoardService.java)를 추가하고 컨트롤러에서 데이터베이스에 wirte 하는 작업을 모두 서비스 레이어로 이동한다.
package com.cos.blogapp.service;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;

import com.cos.blogapp.domain.board.Board;
import com.cos.blogapp.domain.board.BoardRepository;
import com.cos.blogapp.domain.comment.Comment;
import com.cos.blogapp.domain.comment.CommentRepository;
import com.cos.blogapp.domain.user.User;
import com.cos.blogapp.handler.ex.MyAsyncNotFoundException;
import com.cos.blogapp.handler.ex.MyNotFoundException;
import com.cos.blogapp.web.dto.BoardSaveReqDto;
import com.cos.blogapp.web.dto.CommentSaveReqDto;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class BoardService {

  // 생성자 주입(DI)
  private final BoardRepository boardRepository;

  @Transactional(rollbackFor = MyAsyncNotFoundException.class) 
  public void 게시글수정(int id, User principal, BoardSaveReqDto dto) {


    // 권한 체크
    Board boardEntity = boardRepository.findById(id)
        .orElseThrow(() -> new MyAsyncNotFoundException("해당 게시글을 찾을 수 없습니다."));
    if (principal.getId() != boardEntity.getUser().getId()) {
      throw new MyAsyncNotFoundException("해당 게시글을 수정할 권한이 없습니다.");
    }

    // 영속화된 데이터를 변경하면!!
    boardEntity.setTitle(dto.getTitle());
    boardEntity.setContent(dto.getContent());
  } // 트랜젝션 종료(더티체킹) 

  public Board 게시글수정페이지이동(int id, Model model) {
   
    Board boardEntity = boardRepository.findById(id)
        .orElseThrow(() -> new MyNotFoundException(id + "번의 게시글을 찾을 수 없습니다."));
    
    return boardEntity;
  }

  @Transactional(rollbackFor = MyAsyncNotFoundException.class) 
  public void 게시글삭제(int id, User principal) {

    Board boardEntity = boardRepository.findById(id)
        .orElseThrow(() -> new MyAsyncNotFoundException("해당글을 찾을 수 없습니다."));
    if (principal.getId() != boardEntity.getUser().getId()) {
      throw new MyAsyncNotFoundException("해당글을 삭제할 권한이 없습니다.");
    }

    try {
      boardRepository.deleteById(id);
    } catch (Exception e) {
      throw new MyAsyncNotFoundException(id + "를 찾을 수 없어서 삭제할 수 없습니다.");
    }
  }

  public Board 게시글상세보기(int id, Model model) {
    Board boardEntity = boardRepository.findById(id).orElseThrow(() -> new MyNotFoundException(id + "를 못 찾았어요."));
    return boardEntity;
  }


  @Transactional(rollbackFor = MyNotFoundException.class) 
  public void 게시글등록(User principal, BoardSaveReqDto dto) {
    boardRepository.save(dto.toEntity(principal));
  }

  public Page<Board> 게시글목록보기(int page) {

    PageRequest pageRequest = PageRequest.of(page, 3, Sort.by(Direction.DESC, "id"));
    Page<Board> boardsEntity = boardRepository.findAll(pageRequest); 
    return boardsEntity;
  }

} // end of BoardService
package com.cos.blogapp.web;

import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpSession;
import javax.validation.Valid;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.DeleteMapping;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;

import com.cos.blogapp.domain.user.User;
import com.cos.blogapp.handler.ex.MyAsyncNotFoundException;
import com.cos.blogapp.handler.ex.MyNotFoundException;
import com.cos.blogapp.service.BoardService;
import com.cos.blogapp.service.CommentService;
import com.cos.blogapp.util.Script;
import com.cos.blogapp.web.dto.BoardSaveReqDto;
import com.cos.blogapp.web.dto.CMRespDto;
import com.cos.blogapp.web.dto.CommentSaveReqDto;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor 
@Controller 
public class BoardController {

  // DI : Ioc에 있는거 가져옴
  private final BoardService boardService;
  private final CommentService commentService;
  private final HttpSession session;
  
  // 댓글달기
  @PostMapping("/board/{boardId}/comment")
  public String commentSave(@PathVariable int boardId, CommentSaveReqDto dto) {
    User principal = (User) session.getAttribute("principal");
    
    commentService.댓글등록(boardId, dto, principal);
    return "redirect:/board/"+boardId;  
  }
  
  // 게시글 수정하기
  @PutMapping("/board/{id}")
  public @ResponseBody CMRespDto<String> update(@PathVariable int id, 
      @RequestBody @Valid BoardSaveReqDto dto, BindingResult bindingResult){

    //유효성검사
    if (bindingResult.hasErrors()) {
      Map<String, String> errorMap = new HashMap<>();
      for (FieldError error : bindingResult.getFieldErrors()) {
        errorMap.put(error.getField(), error.getDefaultMessage());
      }
      throw new MyAsyncNotFoundException(errorMap.toString());
    }
    
    User principal = (User) session.getAttribute("principal");
    
    boardService.게시글수정(id, principal, dto);
    
    return new CMRespDto<String>(1, "업데이트 성공",null);
    
  }

  // 게시글 수정하기 페이지 이동
  @GetMapping("/board/{id}/updateForm")
  public String boardUpdateForm(@PathVariable int id, Model model) {
    
    model.addAttribute("boardEntity", boardService.게시글수정페이지이동(id, model));
    
    return "board/updateForm";
    
  }

  // 게시글 삭제하기 
  @DeleteMapping("/board/{id}")
  public @ResponseBody CMRespDto<String> deleteById(@PathVariable int id) {
    User principal = (User) session.getAttribute("principal");
    
    boardService.게시글삭제(id, principal);
    
    return new CMRespDto<String>(1, "성공", null); // @ResoponseBody : 데이터 리턴
  }

  // 게시글 상세보기 
  @GetMapping("/board/{id}") 
  public String detail(@PathVariable int id, Model model) {
    // Board객체에 존재하는 것(Board(o),User(o),List<Comment>(x : LAZY))
    model.addAttribute("boardEntity", boardService.게시글상세보기(id, model));
    return "board/detail"; // ViewResolver
  }

  // 게시글 등록하기
  @PostMapping("/board")
  public @ResponseBody String save(@Valid BoardSaveReqDto dto, BindingResult bindingResult) {

    User principal = (User) session.getAttribute("principal");

    if (bindingResult.hasErrors()) {
      Map<String, String> errorMap = new HashMap<>();
      for (FieldError error : bindingResult.getFieldErrors()) {
        errorMap.put(error.getField(), error.getDefaultMessage());
      }
      return Script.back(errorMap.toString());
    }

  // 핵심 로직 
    boardService.게시글등록(principal, dto);
    
    return Script.href("/", "글쓰기 성공"); // 데이터를 모델에서 들고 와야 한다.
  }

  // 게시글 등록하기 페이지 이동하기
  @GetMapping("/board/saveForm")
  public String saveForm() {
    return "/board/saveForm";
  }

  // 게시글 목록보기 
  @GetMapping("/board")
  public String home(Model model, int page) {
    model.addAttribute("boardsEntity", boardService.게시글목록보기(page));
    return "board/list";
  }

}
2.사용자 서비스 클래스(UserService.java)를 추가하고 컨트롤러에서 데이터베이스에 wirte 하는 작업을 모두 서비스 레이어로 이동한다.
package com.cos.blogapp.service;

import javax.servlet.http.HttpSession;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.cos.blogapp.domain.user.User;
import com.cos.blogapp.domain.user.UserRepository;
import com.cos.blogapp.handler.ex.MyAsyncNotFoundException;
import com.cos.blogapp.handler.ex.MyNotFoundException;
import com.cos.blogapp.util.MyAlgorithm;
import com.cos.blogapp.util.SHA;
import com.cos.blogapp.web.dto.JoinReqDto;
import com.cos.blogapp.web.dto.LoginReqDto;
import com.cos.blogapp.web.dto.UserUpdateDto;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class UserService {
  
  private final UserRepository userRepository;
  
  // 이게 하나의 서비스인가? (principal 값 변경, update 하고 세션 값 변경(x))
  // 리퀘스트 관련 코드는 따로 빼준다.
  // principal 값 변경하는 연산하는데 오래 걸리는가?
  
  @Transactional(rollbackFor = MyAsyncNotFoundException.class) 
  public void 회원수정(User principal, UserUpdateDto dto) {
    
    User userEntity = userRepository.findById(principal.getId())
        .orElseThrow(()-> new MyAsyncNotFoundException("회원정보를 찾을 수 없습니다."));
    userEntity.setEmail(dto.getEmail());

    // 원래는 리턴 받아서 성공/실패를 전달해주는 게 좋다.

  } // 더티 체킹(save x) 
  
  @Transactional(rollbackFor = MyNotFoundException.class) 
  public void 회원가입(JoinReqDto dto) {
    // 정상일 때
    String encPassword = SHA.encrypt(dto.getPassword(), MyAlgorithm.SHA256);
    dto.setPassword(encPassword); 
    userRepository.save(dto.toEntity()); 
  }
  
  public User 로그인(LoginReqDto dto) {
    return userRepository.mLogin(dto.getUsername(), SHA.encrypt(dto.getPassword(), MyAlgorithm.SHA256));
  }
  
}
package com.cos.blogapp.web;

import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpSession;
import javax.validation.Valid;

import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;

import com.cos.blogapp.domain.user.User;
import com.cos.blogapp.domain.user.UserRepository;
import com.cos.blogapp.handler.ex.MyAsyncNotFoundException;
import com.cos.blogapp.service.UserService;
import com.cos.blogapp.util.MyAlgorithm;
import com.cos.blogapp.util.SHA;
import com.cos.blogapp.util.Script;
import com.cos.blogapp.web.dto.CMRespDto;
import com.cos.blogapp.web.dto.JoinReqDto;
import com.cos.blogapp.web.dto.LoginReqDto;
import com.cos.blogapp.web.dto.UserUpdateDto;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Controller
public class UserController {

  // DI
  private final UserService userService;
  private final HttpSession session;

  // 회원정보 수정 
  @PutMapping("/user/{id}")
  public @ResponseBody CMRespDto<String> update(@PathVariable int id, @Valid @RequestBody UserUpdateDto dto, BindingResult bindingResult) {

    //유효성검사
    if (bindingResult.hasErrors()) {
      Map<String, String> errorMap = new HashMap<>();
      for (FieldError error : bindingResult.getFieldErrors()) {
        errorMap.put(error.getField(), error.getDefaultMessage());
      }
      throw new MyAsyncNotFoundException(errorMap.toString());
    }
    
    User principal = (User) session.getAttribute("principal");
    
    //권한 체크
    if (principal.getId() != id) {
      throw new MyAsyncNotFoundException("회원정보를 수정할 권한이 없습니다.");
    }

  // 핵심로직 
    userService.회원수정(principal, dto);
    
    // 세션 동기화 해주는 부분 
    principal.setEmail(dto.getEmail());
    session.setAttribute("principal", principal); // 세션값 변경
    
    return new CMRespDto<String>(1,"성공",null);
    
  }

  // 회원정보 보기 
  @GetMapping("/user/{id}")
  public String userInfo(@PathVariable int id) {
    return "user/updateForm";
  }

  // 로그아웃 
  @GetMapping("/logout")
  public String logout() {
    session.invalidate(); 
    return "redirect:/";
  }

  // 로그인 페이지 이동 
  @GetMapping("/loginForm")
  public String login() {
    return "/user/loginForm";
  }

  // 회원가입 페이지 이동 
  @GetMapping("/joinForm")
  public String joinForm() {
    return "/user/joinForm";
  }

  // 회원가입 
  @PostMapping("/join")
  public @ResponseBody String join(@Valid JoinReqDto dto, BindingResult bindingResult) {

    // 실패했을 때
    if (bindingResult.hasErrors()) {
      Map<String, String> errorMap = new HashMap<>();
      for (FieldError error : bindingResult.getFieldErrors()) {
        errorMap.put(error.getField(), error.getDefaultMessage());
      }
      return Script.back(errorMap.toString());
    }

    userService.회원가입(dto);
    return Script.href("/loginForm");
  }

  // 로그인 
  @PostMapping("/login")
  public @ResponseBody String login(@Valid LoginReqDto dto, BindingResult bindingResult) {

    // 실패했을 때
    if (bindingResult.hasErrors()) {
      Map<String, String> errorMap = new HashMap<>();
      for (FieldError error : bindingResult.getFieldErrors()) {
        errorMap.put(error.getField(), error.getDefaultMessage());
      }
      return Script.back(errorMap.toString());
    }

    User userEntity = userService.로그인(dto);

    if (userEntity == null) { 
      return Script.back("아이디 혹은 비밀번호를 잘못 입력하였습니다.");
    } else {
      session.setAttribute("principal", userEntity);
      return Script.href("/", "로그인 성공");
    }

  }

}

 

3.댓글 서비스 클래스(CommentService.java)를 추가하고 컨트롤러에서 데이터베이스에 wirte 하는 작업을 모두 서비스 레이어로 이동한다.
package com.cos.blogapp.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.cos.blogapp.domain.board.Board;
import com.cos.blogapp.domain.board.BoardRepository;
import com.cos.blogapp.domain.comment.Comment;
import com.cos.blogapp.domain.comment.CommentRepository;
import com.cos.blogapp.domain.user.User;
import com.cos.blogapp.handler.ex.MyAsyncNotFoundException;
import com.cos.blogapp.handler.ex.MyNotFoundException;
import com.cos.blogapp.web.dto.CommentSaveReqDto;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class CommentService {

  private final CommentRepository commentRepository;
 	private final BoardRepository boardRepository;

 	@Transactional(rollbackFor = MyAsyncNotFoundException.class)
 	public void 댓글삭제(int id, User principal) {
 		Comment commentEntity =  commentRepository.findById(id)
 			.orElseThrow(()-> new MyAsyncNotFoundException("없는 댓글 번호입니다."));

 		if(principal.getId() != commentEntity.getUser().getId()) {
 			throw new MyAsyncNotFoundException("해당 게시글을 삭제할 수 없는 유저입니다.");
 		}

 		commentRepository.deleteById(id);
 	}

 	@Transactional(rollbackFor = MyNotFoundException.class)
 	public void 댓글등록(int boardId, CommentSaveReqDto dto, User principal) {

 		Board boardEntity = boardRepository.findById(boardId)
 				.orElseThrow(() -> new MyNotFoundException("해당 게시글을 찾을 수 없습니다."));

 		Comment comment = new Comment();
 		comment.setContent(dto.getContent());
 		comment.setUser(principal);
 		comment.setBoard(boardEntity);

 		commentRepository.save(comment);
 	} 
  
}
package com.cos.blogapp.web;

import javax.servlet.http.HttpSession;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;

import com.cos.blogapp.domain.user.User;
import com.cos.blogapp.handler.ex.MyAsyncNotFoundException;
import com.cos.blogapp.service.CommentService;
import com.cos.blogapp.web.dto.CMRespDto;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Controller
public class CommentController {
  
  private final CommentService commentService;
  private final HttpSession session;
  
  @DeleteMapping("/comment/{id}")
 	public @ResponseBody CMRespDto<?> deleteById(@PathVariable int id){
 		User principal = (User) session.getAttribute("principal");

 		commentService.댓글삭제(id, principal);
 		return new CMRespDto<>(1, "성공", null);
 	}
  
}

 

코드 풀이

@Service

스프링에서 제공하는 서비스 어노테이션. 이 어노테이션이 걸린 클래스를 IoC 컨테이너에 띄워준다.

@Transactional

트렌젝션의 범위를 설정한다. 하나라도 작업이 실패하면 롤백되게 만들어준다.

(rollbackFor = MyNotFoundException.class)

함수 내부에 하나의 write라도 실패하면 전체를 롤백한다. 롤백시 사용할 익셉션을 걸어주지 않으면 동작하지 않는다.

commentService.댓글등록(boardId, dto, principal);

CommentService에 따로 빼둔 댓글등록 트렌젝션을 가지고 온다.

최신글