Service 레이어는 애플리케이션의 비즈니스 로직을 담당하는 계층입니다. Controller는 클라이언트의 요청을 받고 응답을 반환하는 역할을 하며, Repository는 데이터베이스와의 상호작용을 담당합니다. 이 두 계층 사이에 Service 레이어를 도입함으로써 여러 이점을 얻을 수 있습니다.
UserDTO 코드 추가
package com.tenco.blog_v2.user;
import lombok.Data;
@Data
public class UserDTO {
// 정적 내부 클래로 모우자
@Data
public static class LoginDTO {
private String username;
private String password;
}
// 정적 내부 클래로 모우자
@Data
public static class JoinDTO {
private String username;
private String password;
private String email;
public User toEntity() {
return User.builder()
.username(username)
.password(password)
.role("USER")
.email(email)
.build();
}
}
@Data
public static class UpdateDTO {
private String password;
private String email;
}
}
UserService 생성하기
package com.tenco.blog_v2.user;
import com.tenco.blog_v2.common.errors.Exception400;
import com.tenco.blog_v2.common.errors.Exception401;
import com.tenco.blog_v2.common.errors.Exception404;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Optional;
@RequiredArgsConstructor
@Service // IoC
public class UserService {
// @Autowired
private final UserJPARepository userJPARepository;
/**
* 회원 가입 서비스
*/
@Transactional
public void signUp(UserDTO.JoinDTO reqDto ) {
// 1. username <-- 유니크 확인
Optional<User> userOp = userJPARepository.findByUsername(reqDto.getUsername());
if(userOp.isPresent()) {
throw new Exception400("중복된 유저네임입니다");
}
// 회원 가입
userJPARepository.save(reqDto.toEntity());
}
/**
* 로그인 서비스
*
*/
public User signIn(UserDTO.LoginDTO reqDTO) {
User seessionUser = userJPARepository
.findByUsernameAndPassword(reqDTO.getUsername(), reqDTO.getPassword())
.orElseThrow( () -> new Exception401("인증되지 않았습니다"));
return seessionUser;
}
/**
* 회원 정보 조회 서비스
*
* @param id 조회할 사용자 ID
* @return 조회된 사용자 객체
* @throws Exception404 사용자를 찾을 수 없는 경우 발생
*/
public User readUser(int id){
User user = userJPARepository.findById(id)
.orElseThrow(() -> new Exception404("회원정보를 찾을 수 없습니다"));
return user;
}
/**
* 회원 정보 수정 서비스
*
* @param id 수정할 사용자 ID
* @param reqDTO 수정된 사용자 정보 DTO
* @return 수정된 사용자 객체
* @throws Exception404 사용자를 찾을 수 없는 경우 발생
*/
@Transactional // 트랜잭션 관리
public User updateUser(int id, UserDTO.UpdateDTO reqDTO){
// 1. 사용자 조회 및 예외 처리
User user = userJPARepository.findById(id)
.orElseThrow(() -> new Exception404("회원정보를 찾을 수 없습니다"));
// 2. 사용자 정보 수정
user.setPassword(reqDTO.getPassword());
user.setEmail(reqDTO.getEmail());
// 더티 체킹을 통해 변경 사항이 자동으로 반영됩니다.
return user;
}
}
Board 엔티티 코드 추가
package com.tenco.blog_v2.board;
import com.tenco.blog_v2.user.User;
import jakarta.persistence.*;
import lombok.*;
import java.sql.Timestamp;
@NoArgsConstructor
@Entity
@Table(name = "board_tb")
@Getter
@Setter
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // 기본키 전략 db 위임
private Integer id;
private String title;
@Lob // 대용량 데이터 저장 가능
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user; // 게시글 작성자 정보
// created_at 컬럼과 매핑하며, 이 필드는 데이터 저장시 자동으로 설정 됨
@Column(name = "created_at", insertable = false, updatable = false)
private Timestamp createdAt;
// 코드 추가
// 해당 테이블에 컬럼을 만들지 마
// 즉, JPA 메모리상에서만 활용 가능한 필드 이다.
@Transient
boolean isBoardOwner;
@Builder
public Board(Integer id, String title, String content, User user, Timestamp createdAt) {
this.id = id;
this.title = title;
this.content = content;
this.user = user;
this.createdAt = createdAt;
}
}
BoardService 생성
package com.tenco.blog_v2.board;
import com.tenco.blog_v2.common.errors.Exception403;
import com.tenco.blog_v2.common.errors.Exception404;
import com.tenco.blog_v2.user.User;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import java.util.List;
@RequiredArgsConstructor
@Service // IoC 처리
public class BoardService {
private final BoardJPARepository boardJPARepository;
/** 추가해 주세요
* 새로운 게시글을 작성하여 저장합니다.
*
* @param reqDTO 게시글 작성 요청 DTO
* @param sessionUser 현재 세션에 로그인한 사용자
*/
@Transactional // 트랜잭션 관리: 데이터베이스 연산이 성공적으로 완료되면 커밋, 실패하면 롤백
public void createBoard(BoardDTO.SaveDTO reqDTO, User sessionUser){
// 요청 DTO를 엔티티로 변환하여 저장합니다.
boardJPARepository.save(reqDTO.toEntity(sessionUser));
}
/**
* 게시글 ID로 조회 서비스
*/
public Board getBoard(int boardId) {
return boardJPARepository
.findById(boardId)
.orElseThrow(() -> new Exception404("게시글을 찾을 수 없어요"));
}
/**
* 게시글 상세보기 서비스, 게시글 주인 여부 판별
*/
public Board getBoardDetails(int boardId, User sessionUser) {
Board board = boardJPARepository
.findById(boardId)
.orElseThrow(() -> new Exception404("게시글을 찾을 수 없어요"));
// 현재 사용자가 게시글을 작성했는지 여부 판별
boolean isBoardOwner = false;
if(sessionUser != null ) {
if(sessionUser.getId().equals(board.getUser().getId())) {
isBoardOwner = true;
}
}
board.setBoardOwner(isBoardOwner);
return board;
}
/**
* 게시글 삭제 서비스
*/
public void deleteBoard(int boardId, int sessionUserId) {
// 1.
Board board = boardJPARepository.findById(boardId).orElseThrow(() -> new Exception404("게시글을 찾을 수 없습니다"));
// 2. 권한 처리 - 현재 사용자가 게시글 주인이 맞는가?
if(sessionUserId != board.getUser().getId()) {
throw new Exception403("게시글을 삭제할 권한이 없습니다");
}
// 3. 게시글 삭제 하기
boardJPARepository.deleteById(boardId);
}
/**
* 게시글 수정 서비스
*/
@Transactional
public void updateBoard(int boardId, int sessionUserId, BoardDTO.UpdateDTO reqDTO) {
// 1. 게시글 존재 여부 확인
Board board = boardJPARepository.findById(boardId).orElseThrow(() -> new Exception404("게시글을 찾을 수 없습니다"));
// 2. 권한 확인
if(sessionUserId != board.getUser().getId()) {
throw new Exception403("게시글 수정 권한이 없습니다");
}
// 3. 게시글 수정
board.setTitle(reqDTO.getTitle());
board.setContent(reqDTO.getContent());
// 더티 체킹 처리
}
/**
* 모든 게시글 조회 서비스
*/
public List<Board> getAllBoards() {
// 게시글을 ID 기준으로 내림차순으로 정렬해서 조회 해라.
Sort sort = Sort.by(Sort.Direction.DESC, "id");
return boardJPARepository.findAll(sort);
}
}
UserController 수정
package com.tenco.blog_v2.user;
import com.tenco.blog_v2.common.errors.Exception401;
import com.tenco.blog_v2.common.errors.Exception500;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@RequiredArgsConstructor
@Slf4j
@Controller
public class UserController {
// DI 처리
private final UserService userService;
private final HttpSession session;
/**
* 회원 정보 수정 페이지 요청
* 주소설계 : http://localhost:8080/user/update-form
*
*
* @return 문자열
* 반환되는 문자열을 뷰 리졸버가 처리하며
* 머스태치 템플릿 엔진을 통해서 뷰 파일을 렌더링 합니다.
*/
@GetMapping("/user/update-form")
public String updateForm(HttpServletRequest request) {
log.info("회원 수정 페이지");
User sessionUser = (User) session.getAttribute("sessionUser");
if(sessionUser == null) {
return "redirect:/login-form";
}
User user = userService.readUser(sessionUser.getId());
request.setAttribute("user", user);
return "user/update-form"; // 템플릿 경로 : user/join-form.mustache
}
/**
* 사용자 정보 수정
* @param reqDTO
* @return 메인 페이지
*/
@PostMapping("/user/update")
public String update(@ModelAttribute(name = "updateDTO") UserDTO.UpdateDTO reqDTO) {
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null) {
return "redirect:/login-form";
}
User updatedUser = userService.updateUser(sessionUser.getId(), reqDTO);
// 세션 정보 동기화 처리
session.setAttribute("sessionUser", updatedUser);
return "redirect:/";
}
/**
* 회원가입 페이지 요청
* 주소설계 : http://localhost:8080/join-form
*
* @param model
* @return 문자열
* 반환되는 문자열을 뷰 리졸버가 처리하며
* 머스태치 템플릿 엔진을 통해서 뷰 파일을 렌더링 합니다.
*/
@GetMapping("/join-form")
public String joinForm(Model model) {
log.info("회원가입 페이지");
model.addAttribute("name", "회원가입 페이지");
return "user/join-form"; // 템플릿 경로 : user/join-form.mustache
}
/**
* 회원 가입 기능 요청
* @param reqDto
* @return
*/
@PostMapping("/join")
public String join(@ModelAttribute(name = "joinDTO") UserDTO.JoinDTO reqDto) {
// 유효성 검사 생략 ...
try {
userService.signUp(reqDto);
} catch (DataIntegrityViolationException e) {
throw new Exception500("동일한 유저네임이 존재 합니다.");
}
return "redirect:/login-form";
}
/**
* 로그인 페이지 요청
* 주소설계 : http://localhost:8080/login-form
*
* @param model
* @return 문자열
* 반환되는 문자열을 뷰 리졸버가 처리하며
* 머스태치 템플릿 엔진을 통해서 뷰 파일을 렌더링 합니다.
*/
@GetMapping("/login-form")
public String loginForm(Model model) {
log.info("로그인 페이지");
model.addAttribute("name", "로그인 페이지");
return "user/login-form"; // 템플릿 경로 : user/join-form.mustache
}
/**
* 자원에 요청은 GET 방식이지만 보안에 이유로 예외 !
* 로그인 처리 메서드
* 요청 주소 POST : http://localhost:8080/login
* @param reqDto
* @return
*/
@PostMapping("/login")
public String login(UserDTO.LoginDTO reqDto) {
try {
User sessionUser = userService.signIn(reqDto);
session.setAttribute("sessionUser", sessionUser);
return "redirect:/";
} catch (Exception e) {
throw new Exception401("유저이름 또는 비밀번호가 틀렸습니다.");
}
}
@GetMapping("/logout")
public String logout() {
session.invalidate(); // 세션을 무효화 (로그아웃)
return "redirect:/";
}
}
BoardController 코드 수정
package com.tenco.blog_v2.board;
import com.tenco.blog_v2.common.errors.Exception404;
import com.tenco.blog_v2.user.User;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import java.util.List;
import java.util.Objects;
@Slf4j
@RequiredArgsConstructor
@Controller
public class BoardController {
private final BoardService boardService; // BoardService 주입
private final HttpSession session;
/**
* 게시글 수정 폼을 표시하는 메서드
* 요청 주소: **GET http://localhost:8080/board/{id}/update-form**
* 글 수정하기 페이지 요청 메서드
*
* @param id 수정할 게시글의 ID
* @param request HTTP 요청 객체
* @return 게시글 수정 페이지 뷰
*/
@GetMapping("/board/{id}/update-form")
public String updateForm(@PathVariable(name = "id") Integer id, HttpServletRequest request) {
// 세션에서 로그인한 사용자 정보 가져오기
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null) {
return "redirect:/login-form"; // 로그인하지 않은 경우 로그인 페이지로 리다이렉트
}
// 게시글 상세 조회 서비스 호출
Board board = boardService.getBoardDetails(id, sessionUser);
if (board == null) {
throw new Exception404("게시글이 존재하지 않습니다");
}
// 조회한 게시글을 요청 속성에 추가
request.setAttribute("board", board);
// 수정 폼 템플릿 반환
return "board/update-form";
}
/**
* 게시글 수정 처리 메서드
* 요청 주소: **POST http://localhost:8080/board/{id}/update**
*
* @param id 수정할 게시글의 ID
* @param updateDTO 수정된 데이터를 담은 DTO
* @return 게시글 상세보기 페이지로 리다이렉트
*/
@PostMapping("/board/{id}/update")
public String update(@PathVariable(name = "id") Integer id, @ModelAttribute(name = "updateDTO") BoardDTO.UpdateDTO updateDTO) {
// 세션에서 로그인한 사용자 정보 가져오기
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null) {
return "redirect:/login-form"; // 로그인하지 않은 경우 로그인 페이지로 리다이렉트
}
// 게시글 수정 서비스 호출
boardService.updateBoard(id, sessionUser.getId(), updateDTO);
// 수정 완료 후 게시글 상세보기 페이지로 리다이렉트
return "redirect:/board/" + id;
}
/**
* 게시글 삭제 처리 메서드
* 요청 주소: **POST http://localhost:8080/board/{id}/delete**
*
* @param id 삭제할 게시글의 ID
* @return 메인 페이지로 리다이렉트
*/
@PostMapping("/board/{id}/delete")
public String delete(@PathVariable(name = "id") Integer id) {
// 세션에서 로그인한 사용자 정보 가져오기
User sessionUser = (User) session.getAttribute("sessionUser");
// 세션 유효성 검증
if (sessionUser == null) {
return "redirect:/login-form"; // 로그인 페이지로 리다이렉트
}
// 게시글 삭제 서비스 호출
boardService.deleteBoard(id, sessionUser.getId());
// 메인 페이지로 리다이렉트
return "redirect:/";
}
/**
* 게시글 작성 폼을 표시하는 메서드
* 요청 주소: **GET http://localhost:8080/board/save-form**
*
* @return 게시글 작성 페이지 뷰
*/
@GetMapping("/board/save-form")
public String saveForm() {
// 게시글 작성 폼 템플릿 반환
return "board/save-form";
}
/**
* 게시글 작성 처리 메서드
* 요청 주소: **POST http://localhost:8080/board/save**
*
* @param dto 게시글 작성 요청 DTO
* @return 메인 페이지로 리다이렉트
*/
@PostMapping("/board/save")
public String save(@ModelAttribute BoardDTO.SaveDTO dto) {
// 세션에서 로그인한 사용자 정보 가져오기
User sessionUser = (User) session.getAttribute("sessionUser");
// 세션 유효성 검증
if (sessionUser == null) {
return "redirect:/login-form"; // 로그인 페이지로 리다이렉트
}
// 게시글 작성 서비스 호출
boardService.createBoard(dto, sessionUser);
// 메인 페이지로 리다이렉트
return "redirect:/";
}
/**
* 게시글 상세보기 처리 메서드
* 요청 주소: **GET http://localhost:8080/board/{id}**
*
* @param id 게시글의 ID
* @param request HTTP 요청 객체
* @return 게시글 상세보기 페이지 뷰
*/
@GetMapping("/board/{id}")
public String detail(@PathVariable Integer id, HttpServletRequest request) {
// 세션에서 로그인한 사용자 정보 가져오기
User sessionUser = (User) session.getAttribute("sessionUser");
Board board = boardService.getBoardDetails(id, sessionUser);
// 현재 사용자가 게시글의 작성자인지 확인하여 isOwner 필드 설정
boolean isOwner = false;
if (sessionUser != null) {
if (Objects.equals(sessionUser.getId(), board.getUser().getId())) {
isOwner = true;
}
}
// 뷰에 데이터 전달
request.setAttribute("isOwner", isOwner);
request.setAttribute("board", board);
return "board/detail";
}
/**
* 메인 페이지를 표시하는 메서드
* 요청 주소: **GET http://localhost:8080/**
*
* @param model 뷰에 전달할 모델 객체
* @return 메인 페이지 뷰
*/
@GetMapping("/")
public String index(Model model) {
// 모든 게시글 조회 서비스 호출
List<Board> boardList = boardService.getAllBoards();
// 조회한 게시글 목록을 모델에 추가
model.addAttribute("boardList", boardList);
// 메인 페이지 템플릿 반환
return "index";
}
}
'Spring boot > 개념 공부' 카테고리의 다른 글
댓글 목록 보기 (0) | 2024.11.08 |
---|---|
댓글 테이블 설계 (엔티티) (0) | 2024.11.08 |
JPARepository란? (1) | 2024.10.20 |
리팩토링 (0) | 2024.10.20 |
인터셉터 만들어 보기 (0) | 2024.10.20 |