1. [문제 인식 및 정의]
더보기
package com.example.plan.member7.service;
import com.example.plan.base.BaseEntity;
import com.example.plan.comment7.entity.Comments;
import com.example.plan.comment7.repository.CommentRepository;
import com.example.plan.config.PasswordEncoder;
import com.example.plan.member7.dto.response.*;
import com.example.plan.member7.entity.Member;
import com.example.plan.member7.repository.*;
import com.example.plan.plan7.entity.Plan;
import com.example.plan.plan7.repository.PlanRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
private final PlanRepository planRepository;
private final CommentRepository commentRepository;
private final PasswordEncoder passwordEncoder;
@Transactional
@Override
public MemberResponseDto signUp(
String username,
String email,
String password
) {
String encodedPassword = passwordEncoder.encode(password);
Member member = new Member(
username,
email,
encodedPassword
);
Member savedMember = memberRepository.save(member);
return MemberResponseDto.toDto(savedMember);
}
@Transactional
@Override
public SignInMemberResponseDto signIn(
String email,
String password
) {
Member foundMember = memberRepository.findByEmail(email)
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.UNAUTHORIZED
, "Email does not Match"
)
);
boolean isPasswordDifferent = !passwordEncoder
.matches(
password
, foundMember.getPassword()
);
if (isPasswordDifferent) {
throw new ResponseStatusException(
HttpStatus.UNAUTHORIZED
, "Password does not match"
);
}
return new SignInMemberResponseDto(foundMember.getId());
}
@Transactional(readOnly = true)
@Override
public List<MemberResponseDto> readAllMembers() {
List<MemberResponseDto> memberList = new ArrayList<>();
memberList = memberRepository.findAllByIsDeletedFalse()
.stream()
.map(MemberResponseDto::toDto)
.toList();
return memberList;
}
@Transactional(readOnly = true)
@Override
public MemberResponseDto readMemberById(Long memberId) {
Member foundMember = findMemberById(memberId);
return MemberResponseDto.toDto(foundMember);
}
@Transactional
@Override
public MemberResponseDto updateMember(
Long memberId,
String username,
String email
) {
Member foundMember = findMemberById(memberId);
foundMember.update(username, email);
return MemberResponseDto.toDto(foundMember);
}
@Transactional
@Override
public void deleteMember(Long memberId) {
Member foundMember = findMemberById(memberId);
if (foundMember.getIsDeleted()) {
throw new ResponseStatusException(
HttpStatus.CONFLICT,
"The requested data has already been deleted"
);
}
foundMember.markAsDeleted();
List<Plan> planList = new ArrayList<>();
planList = planRepository
.findAllByMemberIdAndIsDeletedFalse(memberId);
planList.stream()
.peek(BaseEntity::markAsDeleted)
.forEach(plan -> {
List<Comments> commentsList = commentRepository
.findAllByPlanIdAndIsDeletedFalse(
plan.getId()
);
commentsList.forEach(BaseEntity::markAsDeleted);
}
);
}
private Member findMemberById(Long memberId) {
return memberRepository
.findByIdAndIsDeletedFalse(memberId)
.orElseThrow(
() -> new ResponseStatusException(
HttpStatus.NOT_FOUND,
"Id doest not exist"
)
);
}
}
더보기
package com.example.plan.comment7.service;
import com.example.plan.comment7.dto.response.CommentResponseDto;
import com.example.plan.comment7.entity.Comments;
import com.example.plan.comment7.repository.CommentRepository;
import com.example.plan.plan7.entity.Plan;
import com.example.plan.plan7.repository.PlanRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.util.ArrayList;
import java.util.List;
@RequiredArgsConstructor
@Service
public class CommentServiceImpl implements CommentService {
private final PlanRepository planRepository;
private final CommentRepository commentRepository;
@Transactional
@Override
public CommentResponseDto createComment(
String content,
Long planId
) {
Plan foundPlan = planRepository
.findByIdAndIsDeletedFalse(planId)
.orElseThrow(
() -> new ResponseStatusException(
HttpStatus.NOT_FOUND,
"Id does not exist"
)
);
Comments commentToSave = new Comments(content);
commentToSave.updatePlan(foundPlan);
commentToSave.updateMember(foundPlan.getMember());
Comments savedComment = commentRepository.save(commentToSave);
return CommentResponseDto.toDto(savedComment);
}
@Transactional(readOnly = true)
@Override
public List<CommentResponseDto> readAllComments() {
List<CommentResponseDto> allComments = new ArrayList<>();
allComments = commentRepository
.findAllByIsDeletedFalse()
.stream()
.map(CommentResponseDto::toDto)
.toList();
return allComments;
}
@Transactional(readOnly = true)
@Override
public CommentResponseDto readCommentById(Long commentId) {
Comments foundComment = findCommentById(commentId);
return CommentResponseDto.toDto(foundComment);
}
@Transactional
@Override
public CommentResponseDto updateComment(
Long commentId
, String content
) {
Comments foundComment = findCommentById(commentId);
foundComment.updateContent(content);
Comments updatedComment = commentRepository.save(foundComment);
return CommentResponseDto.toDto(updatedComment);
}
@Transactional
@Override
public void deleteComment(Long commentId) {
Comments foundComment = findCommentById(commentId);
if (foundComment.getIsDeleted()) {
throw new ResponseStatusException(
HttpStatus.CONFLICT,
"Requested data has already been deleted"
);
}
foundComment.markAsDeleted();
}
private Comments findCommentById(Long commentId) {
return commentRepository
.findByIdAndIsDeletedFalse(commentId)
.orElseThrow(
() -> new ResponseStatusException(
HttpStatus.NOT_FOUND,
"Id does not exist"
)
);
}
}
더보기
package com.example.plan.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.server.ResponseStatusException;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@RestControllerAdvice
public class LayerExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponseMessage> handleValidationException(
MethodArgumentNotValidException ex
) {
HttpStatusCode statusCode = ex.getStatusCode();
List<String> errors = new ArrayList<>();
errors = ex.getFieldErrors()
.stream()
.map(error ->
error.getField()
+ " field has an error. "
+ error.getDefaultMessage()
)
.toList();
return new ResponseEntity<>(
new ErrorResponseMessage(
statusCode.value(),
String.join(". ", errors)
),
statusCode
);
}
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<ErrorResponseMessage> handleResponseStatusException(
ResponseStatusException ex
) {
HttpStatusCode statusCode = ex.getStatusCode();
return new ResponseEntity<>(
new ErrorResponseMessage(
statusCode.value(),
ex.getReason()
),
statusCode
);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponseMessage> handleException(
Exception ex
) {
log.error(ex.getMessage(), ex);
HttpStatusCode statusCode = HttpStatusCode.valueOf(500);
return new ResponseEntity<>(
new ErrorResponseMessage(
statusCode.value(),
"Error has occurred."
),
statusCode
);
}
}
예외 처리를 쭉 검토하다가 개발자가 예외 처리를 잘못할 수 있다는 문제를 발견했다. 첫 번째로는 예외 처리를 하나씩 찾으며 수정하다가 Http Status를 잘못 골라서 CONFLICT인데 NOT_FOUND를 고를 가능성이 있었다. 두 번째로는 오류 메시지가 일관되게 전송되어야 하는데 수정하다가 메시지 몇 개를 놓치거나 오탈자를 낼 수 있었다. 즉, 인적 오류가 발생할 수 있는 부분이 총 두 가지였다. 이 두 가지 부분을 개발자가 일일이 찾아서 수정하지 않도록 해야 인적 오류 발생 가능성을 낮출 수 있었다.
[의사 결정]
[해결 과정]
더보기
package com.example.plan.exception;
public class ErrorMessage {
public static final String MEMBER_NOT_FOUND = "Member does not exist";
public static final String PLAN_NOT_FOUND = "Plan does not exist";
public static final String COMMENT_NOT_FOUND = "Comment does not exist";
public static final String EMAIL_NOT_MATCH = "Email does not Match";
public static final String PASSWORD_NOT_MATCH = "Password does not match";
public static final String DATA_ALREADY_DELETED = "Requested data has already been deleted";
public static final String INVALID_PATH = "Please check input path";
public static final String ERROR_MEMBER_NOT_FOUND = "ERROR_MEMBER_NOT_FOUND";
public static final String ERROR_PLAN_NOT_FOUND = "ERROR_PLAN_NOT_FOUND";
public static final String ERROR_COMMENT_NOT_FOUND = "ERROR_COMMENT_NOT_FOUND";
public static final String ERROR_EMAIL_NOT_MATCH = "ERROR_EMAIL_NOT_MATCH";
public static final String ERROR_PASSWORD_NOT_MATCH = "ERROR_PASSWORD_NOT_MATCH";
public static final String ERROR_DATA_ALREADY_DELETED = "ERROR_DATA_ALREADY_DELETED";
public static final String ERROR_INVALID_PATH = "ERROR_INVALID_PATH";
public static final String ERROR_INVALID_INPUT = "ERROR_INVALID_INPUT";
}
더보기
package com.example.plan.exception;
public class AlreadyDeletedException extends RuntimeException {
public AlreadyDeletedException(String message) {
super(message);
}
}
package com.example.plan.exception;
public class CommentNotFoundException extends RuntimeException {
public CommentNotFoundException(String message) {
super(message);
}
}
package com.example.plan.exception;
public class EmailMismatchException extends RuntimeException {
public EmailMismatchException(String message) {
super(message);
}
}
package com.example.plan.exception;
public class MemberNotFoundException extends RuntimeException {
public MemberNotFoundException(String message) {
super(message);
}
}
package com.example.plan.exception;
public class PasswordMismatchException extends RuntimeException {
public PasswordMismatchException(String message) {
super(message);
}
}
package com.example.plan.exception;
public class PlanNotFoundException extends RuntimeException {
public PlanNotFoundException(String message) {
super(message);
}
}
더보기
package com.example.plan.exception;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidationException(
MethodArgumentNotValidException e
) {
List<String> errors = new ArrayList<>();
errors = e.getFieldErrors()
.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage
)
.toList();
return handleException(
new Exception(String.join(". ", errors)),
ErrorMessage.ERROR_INVALID_INPUT,
HttpStatus.BAD_REQUEST
);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleOtherException() {
return handleException(
new Exception(ErrorMessage.INVALID_PATH),
ErrorMessage.ERROR_INVALID_PATH,
HttpStatus.NOT_FOUND
);
}
@ExceptionHandler(EmailMismatchException.class)
public ResponseEntity<Map<String, Object>> handleEmailMismatchException(
EmailMismatchException ex
) {
return handleException(
ex,
ErrorMessage.ERROR_EMAIL_NOT_MATCH,
HttpStatus.UNAUTHORIZED
);
}
@ExceptionHandler(PasswordMismatchException.class)
public ResponseEntity<Map<String, Object>> handlePasswordMismatchException(
PasswordMismatchException ex
) {
return handleException(
ex,
ErrorMessage.ERROR_PASSWORD_NOT_MATCH,
HttpStatus.UNAUTHORIZED
);
}
@ExceptionHandler(MemberNotFoundException.class)
public ResponseEntity<Map<String, Object>> handleMemberNotFoundException(
MemberNotFoundException ex
) {
return handleException(
ex,
ErrorMessage.ERROR_MEMBER_NOT_FOUND,
HttpStatus.NOT_FOUND
);
}
@ExceptionHandler(PlanNotFoundException.class)
public ResponseEntity<Map<String, Object>> handlePlanNotFoundException(
PlanNotFoundException ex
) {
return handleException(
ex,
ErrorMessage.ERROR_PLAN_NOT_FOUND,
HttpStatus.NOT_FOUND
);
}
@ExceptionHandler(CommentNotFoundException.class)
public ResponseEntity<Map<String, Object>> handleCommentNotFoundException(
CommentNotFoundException ex
) {
return handleException(
ex,
ErrorMessage.ERROR_COMMENT_NOT_FOUND,
HttpStatus.NOT_FOUND
);
}
@ExceptionHandler(AlreadyDeletedException.class)
public ResponseEntity<Map<String, Object>> handleAlreadyDeletedException(
AlreadyDeletedException ex
) {
return handleException(
ex,
ErrorMessage.ERROR_DATA_ALREADY_DELETED,
HttpStatus.CONFLICT
);
}
private ResponseEntity<Map<String, Object>> handleException(
Exception ex,
String errorCode,
HttpStatus status
) {
String errorMessage = ex.getMessage();
Map<String, Object> errorResponse = new LinkedHashMap<>();
errorResponse.put("errorCode", errorCode);
errorResponse.put("errorMessage", errorMessage);
return new ResponseEntity<>(errorResponse, status);
}
}
더보기
package com.example.plan.exception;
public class AlreadyDeletedException extends RuntimeException {
public AlreadyDeletedException() {
super(ErrorMessage.DATA_ALREADY_DELETED);
}
}
package com.example.plan.exception;
public class CommentNotFoundException extends RuntimeException {
public CommentNotFoundException() {
super(ErrorMessage.COMMENT_NOT_FOUND);
}
}
package com.example.plan.exception;
public class EmailMismatchException extends RuntimeException {
public EmailMismatchException() {
super(ErrorMessage.EMAIL_NOT_MATCH);
}
}
package com.example.plan.exception;
public class MemberNotFoundException extends RuntimeException {
public MemberNotFoundException() {
super(ErrorMessage.MEMBER_NOT_FOUND);
}
}
package com.example.plan.exception;
public class PasswordMismatchException extends RuntimeException {
public PasswordMismatchException() {
super(ErrorMessage.PASSWORD_NOT_MATCH);
}
}
package com.example.plan.exception;
public class PlanNotFoundException extends RuntimeException {
public PlanNotFoundException() {
super(ErrorMessage.PLAN_NOT_FOUND);
}
}
---
더보기
package com.example.plan.exception.notfound;
import lombok.Getter;
import org.springframework.http.HttpStatus;
@Getter
public class NotFoundException extends RuntimeException {
private final String errorCode;
private final HttpStatus httpStatus;
public NotFoundException(String message) {
super(message);
this.errorCode = "ERROR_NOT_FOUND";
this.httpStatus = HttpStatus.NOT_FOUND;
}
}
더보기
package com.example.plan.exception.notfound;
import com.example.plan.exception.ErrorMessage;
public class CommentNotFoundException extends NotFoundException {
public CommentNotFoundException() {
super(ErrorMessage.COMMENT_NOT_FOUND);
}
}
package com.example.plan.exception.notfound;
import com.example.plan.exception.ErrorMessage;
public class MemberNotFoundException extends NotFoundException {
public MemberNotFoundException() {
super(ErrorMessage.MEMBER_NOT_FOUND);
}
}
package com.example.plan.exception.notfound;
import com.example.plan.exception.ErrorMessage;
public class PlanNotFoundException extends NotFoundException {
public PlanNotFoundException() {
super(ErrorMessage.PLAN_NOT_FOUND);
}
}
더보기
package com.example.plan.exception;
public class ErrorMessage {
public static final String MEMBER_NOT_FOUND = "Member is not found";
public static final String PLAN_NOT_FOUND = "Plan is not found";
public static final String COMMENT_NOT_FOUND = "Comment is not found";
public static final String EMAIL_NOT_MATCH = "Email mismatches";
public static final String PASSWORD_NOT_MATCH = "Password mismatch";
public static final String DATA_ALREADY_DELETED = "Requested data has already been deleted";
public static final String INVALID_PATH = "Please check input path";
}
더보기
package com.example.plan.exception;
import com.example.plan.exception.mismatch.MismatchException;
import com.example.plan.exception.notfound.NotFoundException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidationException(
MethodArgumentNotValidException e
) {
List<String> errors = new ArrayList<>();
errors = e.getFieldErrors()
.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage
)
.toList();
return handleException(
new Exception(String.join(". ", errors)),
"ERROR_INVALID_INPUT",
HttpStatus.BAD_REQUEST
);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleOtherException() {
return handleException(
new Exception(ErrorMessage.INVALID_PATH),
"ERROR_INVALID_PATH",
HttpStatus.NOT_FOUND
);
}
@ExceptionHandler(MismatchException.class)
public ResponseEntity<Map<String, Object>> handleUnAuthorizedException(
MismatchException ex
) {
return handleException(
ex,
ex.getErrorCode(),
ex.getHttpStatus()
);
}
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<Map<String, Object>> handleNotFoundException(
NotFoundException ex
) {
return handleException(
ex,
ex.getErrorCode(),
ex.getHttpStatus()
);
}
@ExceptionHandler(AlreadyDeletedException.class)
public ResponseEntity<Map<String, Object>> handleAlreadyDeletedException(
AlreadyDeletedException ex
) {
return handleException(
ex,
ex.getErrorCode(),
ex.getHttpStatus()
);
}
private ResponseEntity<Map<String, Object>> handleException(
Exception ex,
String errorCode,
HttpStatus status
) {
String errorMessage = ex.getMessage();
Map<String, Object> errorResponse = new LinkedHashMap<>();
errorResponse.put("errorCode", errorCode);
errorResponse.put("errorMessage", errorMessage);
return new ResponseEntity<>(errorResponse, status);
}
}
'Troubleshooting: 무엇이 문제였는가? > 본캠프 5주차: Spring 심화 프로젝트' 카테고리의 다른 글
6단계: 일정이 반복된다면? (수정 중) (0) | 2025.01.06 |
---|---|
6단계: 예외 처리 메서드는 어떻게? (수정 중) (0) | 2025.01.06 |
3단계: "아니, '/%08auth/sign-up'이라니. /%08, 넌 누구냐?" (수정 중) (0) | 2025.01.06 |