커스텀 예외
- 초반에는 단순히
IllegalStateException을 던졌다. 하지만 어떤 비즈니스 오류인지 파악하기 어려웠다.- 이를 개선하기 위해 커스텀 예외를 도입하여
DuplicateResourceException,ResourceNotFountException처럼 의미가 명확한 예외를 사용했다.orElseThrow를 사용해서 null이 반환되면 예외 처리 되도록 하고,log.warn을 통해 어떤 데이터에서 문제가 발생했는지 추적 가능하도록 구성하여 유지보수성을 높였다.
객체 지향적 설계 - 엔티티로 위임
- 본인 확인 로직에서
board.isNotOwner(loginEmail)으로 해당 게시물의 작성자가loginEmail과 동일한지 검증하는 로직을 Board 엔티티 내부에 설계하여 객체 지향적인 설계를 하였다.
@Transactional 전략
- Service 클래스는 기본적으로
@Transactional(readOnly=true)로 하고, 데이터가 변경되는 로직 (생성, 수정, 삭제) 만@Transactional설정
// 중복 이메일 검증 로직
private void validateDuplicateUser(String email) {
if(!userRepository.findByEmail(email).isEmpty()) {
throw new IllegalStateException("이미 존재하는 이메일입니다.");
}
}
// 중복 이메일 검증 로직
private void validateDuplicateUser(String email) {
if(!userRepository.findByEmail(email).isEmpty()) {
log.warn("[중복 이메일] - 이메일 : {}", email);
throw new DuplicateResourceException(ErrorCode.DUPLICATE_EMAIL,
"이미 존재하는 이메일입니다.");
}
}
log.warn으로 어느 이메일이 중복인지 알 수 있도록 하였고, 커스텀 예외 처리한 DuplicateResourceException을 던지도록 리팩터링@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder passwordEncoder;
// 회원 가입
@Transactional
public Long join(UserJoinRequestDto dto) {
//중복 이메일 검증
validateDuplicateUser(dto.getEmail());
// 비밀번호 암호화
String encodedPassword = passwordEncoder.encode(dto.getPassword());
User user = User.createUser(dto.getName(), dto.getEmail(), encodedPassword, LocalDateTime.now(), Role.USER);
userRepository.save(user);
return user.getId();
}
// 중복 이메일 검증 로직
private void validateDuplicateUser(String email) {
if(!userRepository.findByEmail(email).isEmpty()) {
log.warn("[중복 이메일] - 이메일 : {}", email);
throw new DuplicateResourceException(ErrorCode.DUPLICATE_EMAIL,
"이미 존재하는 이메일입니다.");
}
}
// 회원 조회
public UserResponseDto findUser(Long userId) {
User user = userRepository.findOne(userId).orElseThrow(
()-> {
log.warn("[회원 조회 실패] 존재하지 않는 회원 - 회원 ID : {}", userId);
return new ResourceNotFoundException(ErrorCode.USER_NOT_FOUND,
"해당 회원이 존재하지 않습니다. 회원 ID: " + userId);
});
return new UserResponseDto(user);
}
// 전체 회원 조회
public List<UserResponseDto> findUsers() {
return userRepository.findAll().stream()
.map(UserResponseDto::new)
.toList();
}
// 이메일로 회원 조회
public UserResponseDto findByEmail(String email) {
User user = userRepository.findByEmail(email).orElseThrow(
()-> {
log.warn("[회원 조회 실패] 존재하지 않는 회원 - 회원 Email : {}", email);
return new ResourceNotFoundException(ErrorCode.USER_NOT_FOUND,
"해당 회원이 존재하지 않습니다. 회원 Email: " + email);
});
return new UserResponseDto(user);
}
}
BoardService 전체 코드 보기 (클릭)
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class BoardService {
private final BoardRepository boardRepository;
private final UserRepository userRepository;
private final UserService userService;
// 게시글 작성
@Transactional
public Long write(String email, BoardSaveRequestDto dto) {
User user = userRepository.findByEmail(email).orElseThrow(
()->{
log.warn("[회원 조회 실패] 존재하지 않는 회원 - 회원 Email : {}", email);
return new ResourceNotFoundException(ErrorCode.USER_NOT_FOUND,
"해당 회원이 존재하지 않습니다. 회원 Email: " + email);
});
Board board = Board.createBoard(dto.getTitle(), dto.getContent(), user, LocalDateTime.now());
boardRepository.save(board);
return board.getId();
}
// 게시글 수정
@Transactional
public void updateBoard(Long boardId, BoardUpdateRequestDto dto, String loginEmail) {
Board board = findBoardOrThrow(boardId);
isOwnerForUpdate(loginEmail, board);
board.updateBoard(dto.getTitle(), dto.getContent());
}
// 게시글 삭제
@Transactional
public void removeBoard(Long boardId, String loginEmail) {
Board board = findBoardOrThrow(boardId);
isOwnerForDelete(loginEmail, board);
board.remove();
boardRepository.remove(board);
}
// 게시글 목록 조회
public List<BoardListResponseDto> findBoards() {
return boardRepository.findAll().stream()
.map(BoardListResponseDto::new)
.toList();
}
// 게시글 단 건 조회
public BoardDetailResponseDto findBoard(Long boardId) {
Board board = findBoardOrThrow(boardId);
return new BoardDetailResponseDto(board);
}
// 작성자 별 게시글 조회
public List<BoardListResponseDto> findBoardsByUser(Long userId) {
return boardRepository.findByUser(userId).stream()
.map(BoardListResponseDto::new)
.toList();
}
// 제목으로 게시글 검색하기
public List<BoardListResponseDto> searchBoards(String keyword) {
if(keyword == null && keyword.trim().isEmpty()) {
return boardRepository.findAll().stream().map(BoardListResponseDto::new).toList();
}
return boardRepository.findByTitle(keyword).stream().map(BoardListResponseDto::new).toList();
}
//게시물 존재 확인
private Board findBoardOrThrow(Long boardId) {
Board board = boardRepository.findOne(boardId).orElseThrow(
()->{
log.warn("[게시글 조회 실패] 존재하지 않는 게시글 - 게시글 ID : {}", boardId);
return new ResourceNotFoundException(ErrorCode.BOARD_NOT_FOUND,
"해당 게시글이 존재하지 않습니다. 게시글 ID : " + boardId);
});
return board;
}
// 본인 확인 로직
private static void isOwnerForDelete(String loginEmail, Board board) {
if (board.isNotOwner(loginEmail)) {
log.warn("[삭제 권한 없음] - 사용자 : {}, 작성자 : {}, 게시글 ID : {}",
loginEmail, board.getUser().getEmail(), board.getId());
throw new AccessDeniedException(ErrorCode.ACCESS_DENIED, "해당 게시글의 삭제 권한이 없습니다.");
}
}
private static void isOwnerForUpdate(String loginEmail, Board board) {
if (board.isNotOwner(loginEmail)) {
log.warn("[수정 권한 없음] - 사용자 : {}, 작성자 : {}, 게시글 ID : {}",
loginEmail, board.getUser().getEmail(), board.getId());
throw new AccessDeniedException(ErrorCode.ACCESS_DENIED, "해당 게시글의 수정 권한이 없습니다.");
}
}
}
CommentService 전체 코드 보기 (클릭)
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Slf4j
public class CommentService {
private final CommentRepository commentRepository;
private final UserRepository userRepository;
private final BoardRepository boardRepository;
// 댓글 생성
@Transactional
public Long write(String email, Long boardId, CommentSaveRequestDto dto) {
User user = userRepository.findByEmail(email).orElseThrow(
()->{
log.warn("[회원 조회 실패] 존재하지 않는 회원 - 회원 Email : {}", email);
return new ResourceNotFoundException(ErrorCode.USER_NOT_FOUND,
"해당 회원이 존재하지 않습니다. 회원 email: " + email);
});
Board board = boardRepository.findOne(boardId).orElseThrow(
()-> {
log.warn("[게시글 조회 실패] 존재하지 않는 게시글 - 게시글 ID : {}", boardId);
return new ResourceNotFoundException(ErrorCode.BOARD_NOT_FOUND,
"해당 게시글이 존재하지 않습니다. 게시글 ID : " + boardId);
});
Comment comment = Comment.createComment(dto.getContent(), user, board, LocalDateTime.now());
commentRepository.save(comment);
return comment.getId();
}
// 댓글 수정
@Transactional
public void updateComment(Long commentId, CommentUpdateRequestDto dto, String loginEmail) {
Comment comment = findCommentOrThrow(commentId);
isOwnerForUpdate(loginEmail, comment);
comment.updateComment(dto.getContent(), LocalDateTime.now());
}
// 댓글 삭제
@Transactional
public void removeComment(Long commentId, String loginEmail) {
Comment comment = findCommentOrThrow(commentId);
isOwnerForDelete(loginEmail, comment);
comment.remove();
commentRepository.remove(comment);
}
// 댓글 조회
public CommentResponseDto findComment(Long commentId) {
Comment comment = findCommentOrThrow(commentId);
return new CommentResponseDto(comment);
}
// 게시글 별 댓글 조회
public List<CommentResponseDto> findCommentsByBoard(Long boardId) {
return commentRepository.findByBoard(boardId).stream()
.map(CommentResponseDto::new)
.toList();
}
// 작성자 별 댓글 조회
public List<CommentResponseDto> findCommentsByUser(Long userId) {
return commentRepository.findByUser(userId).stream()
.map(CommentResponseDto::new)
.toList();
}
// 댓글 존재 여부
private Comment findCommentOrThrow(Long commentId) {
Comment comment = commentRepository.findOne(commentId).orElseThrow(
()-> {
log.warn("[댓글 조회 실패] 존재하지 않는 댓글 - 댓글 ID : {}", commentId);
return new ResourceNotFoundException(ErrorCode.COMMENT_NOT_FOUND,
"해당 댓글이 존재하지 않습니다. 댓글 ID : " + commentId);
});
return comment;
}
// 본인 확인 로직
private static void isOwnerForDelete(String loginEmail, Comment comment) {
if(comment.isNotOwner(loginEmail)) {
log.warn("[삭제 권한 없음] - 사용자 : {}, 작성자 : {}, 댓글 ID : {}",
loginEmail, comment.getUser().getEmail(), comment.getId());
throw new AccessDeniedException(ErrorCode.ACCESS_DENIED, "해당 댓글의 삭제 권한이 없습니다.");
}
}
private static void isOwnerForUpdate(String loginEmail, Comment comment) {
if(comment.isNotOwner(loginEmail)) {
log.warn("[수정 권한 없음] - 사용자 : {}, 작성자 : {}, 댓글 ID : {}",
loginEmail, comment.getUser().getEmail(), comment.getId());
throw new AccessDeniedException(ErrorCode.ACCESS_DENIED, "해당 댓글의 수정 권한이 없습니다.");
}
}
}