게시판 서비스 - Service 계층

Sarah·2026년 4월 27일

커스텀 예외

  • 초반에는 단순히 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,
                    "이미 존재하는 이메일입니다.");
        }
    }
  • 중복 이메일 검증 로직 : email로 user를 찾아서 비어 있다면 log.warn으로 어느 이메일이 중복인지 알 수 있도록 하였고, 커스텀 예외 처리한 DuplicateResourceException을 던지도록 리팩터링

🛠 전체 서비스 계층 코드

UserService 전체 코드 보기 (클릭)

User Service

@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 전체 코드 보기 (클릭)

Board Service

@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 전체 코드 보기 (클릭)

Comment Service

@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, "해당 댓글의 수정 권한이 없습니다.");
       }
   }

}
profile
헤맨 만큼 내 땅

0개의 댓글