Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] : 3주차과제 수정 #6

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open

[feat] : 3주차과제 수정 #6

wants to merge 1 commit into from

Conversation

woals2840
Copy link
Collaborator

No description provided.

@woals2840
Copy link
Collaborator Author

📘UserRepository

public interface UserRepository extends JpaRepository<UserEntity, Long> {
    Optional<UserEntity> findByEmail(String email);
}
  • JpaRepository : Spring Data JPA로 기본적인 CRUD작업을 포함한 여러 데이터베이스 작업을 자동으로 처리해주는 인터페이스입니다.
  • Optional<UserEntity> : UserEntiy 객체를 Optional로 감싸서 반환합니다
    • Optional : 값이 있을 수도, 없을 수도 있는 상황을 처리시 사용하는 방법으로 null을 직접 사용하지 않고도 안전하게 객체를 다루게 해줍니다
  • findByEmail(String email) : 주어진 이메일을 사용하여 사용자 엔티티를 검색합니다.

✅회원가입

    //UserController
    @PostMapping("/user/signup")
    ResponseEntity<String> signUp(@Valid @RequestBody SignupRequest signupRequest, BindingResult result){
        if(result.hasErrors()){
            List<String> errors = result.getAllErrors().stream()
                    .map(error -> error.getDefaultMessage())
                    .collect(Collectors.toList());
            return ResponseEntity.badRequest().body(String.join(", ", errors));

        }
        long userId = userService.signUp(signupRequest);
        return ResponseEntity.created(URI.create(String.format("/user/signup/%d", userId)))
                .build();

    }
  • @valid 어노테이션을 통해서 이메일,패스워드,이름,닉네임의 요청 데이터가 올바른지 확인합니다
  • BindingResult result: @Valid와 함께 사용되어 유효성 검증 결과를 담는 객체입니다. 요청 본문 데이터가 유효성 검증을 통과하지 못한 경우, result 객체에 오류가 담기게 됩니다.
  • BindingResult의 메서드와 Stream API를 통해서 오류들을 처리하였습니다
    • .map(error -> error.getDefaultMessage()) : getDefaultMessage()를 통해 오류 메세지를 추출하여 스트림을 생성하였습니다.
    • .collect(Collectors.toList()) : 생성된 스트림을 문자열 리스트로 변환하여 수집하였습니다
    //UserService
    @Transactional
    public long signUp(SignupRequest req) {
        Optional<UserEntity> userEntityOptional =
                userRepository.findByEmail(req.getEmail());
        if(userEntityOptional.isPresent()){
            throw new CustomException(ErrorCode.EMAIL_ALREADY_EXISTS);
        }
        String name = req.getName();
        String password = req.getPassword();
        String email = req.getEmail();
        String nickname = req.getNickname();
        UserEntity newUser = new UserEntity(email, name, nickname, password);

        userRepository.save(newUser);
        return newUser.getUserId();

    }
  • userRepository.findByEmail(req.getEmail()): userRepository 에서이메일을 검색합니다. 주어진 이메일을 가진 사용자 엔티티가 존재하는지를 검사하고, 결과는 Optional<UserEntity>로 반환됩니다
  • throw new CustomException(ErrorCode.EMAIL_ALREADY_EXISTS): 이메일이 중복될 경우, 커스텀에러인 CustomException을 발생시킵니다.
  • 이후 사용자 엔티티를 생성한 뒤 userRepository 에 저장합니다.

✅로그인

    @PostMapping("user/login")
    ResponseEntity<String> login(@Valid @RequestBody LoginRequest loginRequest){
        UserEntity userEntity = userService.login(loginRequest.getEmail(), loginRequest.getPassword());
        return ResponseEntity.ok()
                .header("UserId", String.valueOf(userEntity.getUserId()))
                .build();
    }
  • @valid 를 통해 회원가입과 마찬가지로 validation을 진행하였습니다.
  • RequestBody에서 받은 이메일과 비밀번호를 추출하여 UserService로 넘겨주었습니다
    public UserEntity login(String email, String password){
        return userRepository.findByEmail(email)
                .filter(userEntity -> password.equals(userEntity.getPassword()))
                .orElseThrow(()-> new CustomException(ErrorCode.INVALID_USER));
    }
  • 사용자조회
    • userRepository.findByEmail(email) : 이메일로 사용자를 조회하여 Oprtional를 반환합니다.
  • 비밀번호 검증
    • filter(userEntity -> password.equals(userEntity.getPassword()))
      • Optionalnull이 아닌 경우에만 실행됩니다
      • 조건을 만족하지 않는다면 빈 Optional을 반환합니다
  • 예외처리
    • orElseThrow(() -> new CustomException(ErrorCode.INVALID_USER))
      • Optional이 null이거나 filter 조건(비밀번호일치)인 경우에 실행됩니다
      • 커스텀에러인 CustomException(ErrorCode.INVALID_USER)) 를 발생시켰습니다

✅일기 수정(기존코드 + 로그인)

    @PatchMapping("/diaries/{id}")
    ResponseEntity<Void> patchDiary( @RequestHeader("User-Id") long userId,
            @PathVariable final Long id, @RequestBody DiaryPatchRequest diaryPatchRequest){

        diaryService.patchDiary(id, userId, diaryPatchRequest);

        return ResponseEntity.ok().build();
    }
  • Request header로 userid를 받습니다
  • diaryService에 diaryid, userid, diaryPatchRequest를 인자값으로 넘겨줍니다
    public void patchDiary(final long id, long userId, DiaryPatchRequest diaryPatchRequest){
        DiaryEntity diaryEntity = diaryRepository.findById(id).orElseThrow(()-> new RuntimeException("해당하는 일기가 없습니다"));
        if(!diaryEntity.getUser().getUserId().equals(userId)){
            throw new CustomException(ErrorCode.NON_AUTHORIZED);
        }
        String name = diaryPatchRequest.getName();
        String content = diaryPatchRequest.getContent();
        diaryEntity.setContent(content);
        diaryEntity.setName(name);
        diaryRepository.save(diaryEntity);
    }
  • if (!diaryEntity.getUser().getUserId().equals(userId)):
    • 일기의 소유자가 요청한 사용자(userId)와 일치하는지 검증합니다.
    • 일기의 소유자 ID(diaryEntity.getUser().getUserId())와 입력된 사용자 ID(userId)를 일치하는지 비교합니다
    • 일치하지 않는다면 커스텀에러인 CustomException(ErrorCode.NON_AUTORIZED) 를 발생시킵니다.

✅일기 삭제(기존코드 + 로그인)

    @DeleteMapping("/diaries/{id}")
    ResponseEntity<Void> delete(@RequestHeader("User-Id") String userId, @PathVariable Long id){
        Long parsedUserId =  Long.parseLong(userId);
        diaryService.deleteDiary(parsedUserId, id);
        return ResponseEntity.noContent().build();
    }
    public void deleteDiary(Long userId, Long id){
        DiaryEntity diaryEntity = diaryRepository.findById(id).orElseThrow(()-> new RuntimeException("존재하지 않습니다"));
        if(userRepository.findById(userId).isEmpty()){
            throw new CustomException(ErrorCode.INVALID_USER);
        } else if(!diaryEntity.getUser().getUserId().equals(userId)){
            throw new CustomException(ErrorCode.NON_AUTHORIZED);
        } else {
            diaryRepository.deleteById(id);
        }
    }
  • 사용자인증
    • if (userRepository.findById(userId).isEmpty()): 주어진 사용자 ID가 데이터베이스에 존재하는지 확인합니다. 사용자가 존재하지 않는 경우, 커스텀에러를 발생시킵니다
  • 일기 소유자 검증
    • else if (!diaryEntity.getUser().getUserId().equals(userId)): 일기의 소유자 ID(diaryEntity.getUser().getUserId())와 입력된 사용자 ID(userId)가 일치하지 않으면 커스텀에러를 발생시킵니다.

✅일기 생성(기존코드 + 로그인)

    @PostMapping("/diaries")
    ResponseEntity<DiaryResponse> post(@RequestHeader("User-Id") String userId,
              @RequestBody DiaryCreateRequest diaryCreateRequest) {
        //글자수 30자 제한
        long parsedUserId =  Long.parseLong(userId);
        UserEntity user = userService.findById(parsedUserId);

        if (diaryCreateRequest.getContent().length() > 30) {
            throw new CustomException(ErrorCode.INVALID_SIZE);
        }
        else{
            DiaryEntity savedDia = diaryService.createDiary(user, diaryCreateRequest.getTitle(), diaryCreateRequest.getContent(), diaryCreateRequest.getScope());
            return ResponseEntity
                    .created(URI.create("/diaries" + savedDia.getId()))
                    .build();
        }

    }
  • UserEntity user = userService.findById(parsedUserId);: 사용자 ID를 이용하여 사용자 정보를 데이터베이스에서 조회합니다. 해당 ID에 대한 사용자가 존재하지 않을 경우, 커스텀에러를 발생시켰습니다.
  • 성공하였을 경우 201created를 와 함께 저장된 다이어리의 location을 uri에 포함하여 반환하였습니다
    public DiaryEntity createDiary(UserEntity userEntity, String title, String content, DiaryScope scope) {
        DiaryEntity diaryEntity = new DiaryEntity(title, content, scope, LocalDate.now(), userEntity );
        diaryRepository.save(diaryEntity);
        return diaryEntity;
    }
  • 기존 코드에서 scope(private, public)를 추가하였습니다

✅일기 조회(기존코드 + scope)

    public List<Diary> getList(){
        //(1) repository로부터 DiaryEntity를 가져옴
        final List<DiaryEntity> diaryEntityList = diaryRepository.findAll();

        final List<Diary> diaryList = new ArrayList<>();

        for(DiaryEntity diaryEntity : diaryEntityList){
            if(diaryEntity.getScope().equals(DiaryScope.PRIVATE)){
                continue;
            }
            diaryList.add(
                    new Diary(diaryEntity.getId(),diaryEntity.getName())
            );
        }
        return new ArrayList();
    }
  • DiaryEntity 에 scope를 추가하여 private은 보이지 않도록 하고 public인 경우에만 보이도록 하였습니다

✅예외처리

에러코드(ENUM)

  • 커스텀하고자 하는 에러코드들을 enum으로 정의해두었습니다
public enum ErrorCode {
    EMAIL_ALREADY_EXISTS(HttpStatus.BAD_REQUEST,"40900","이메일 중복입니다" ),
    NICK_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "40901", "닉네임 중복입니다"),
    INVALID_EMAIL_TYPE(HttpStatus.UNAUTHORIZED, "40000", "이메일 형식이 올바르지 않습니다"),
    INVALID_SIZE(HttpStatus.UNAUTHORIZED, "40001", "글자수를 확인하세요"),
    INVALID_METHOD(HttpStatus.METHOD_NOT_ALLOWED, "40500", "지원하지 않는 HTTP 메소드입니다"),
    INVALID_USER(HttpStatus.UNAUTHORIZED, "40100", "비밀번호가 잘못되었습니다"),
    NON_AUTHORIZED(HttpStatus.UNAUTHORIZED, "40300", "권한이 없습니다");

    private final HttpStatus httpStatus;
    private final String errorCode;
    private final String message;

    public String getErrorCode() {
        return errorCode;
    }

    public HttpStatus getHttpStatus() {
        return httpStatus;
    }

    public String getMessage() {
        return message;
    }

    ErrorCode(HttpStatus httpStatus, String errorCode, String message) {
        this.httpStatus = httpStatus;
        this.errorCode = errorCode;
        this.message = message;
    }
}

GlobalExceptionHandler

  • @RestControllerAdvice 어노테이션을 사용하여 전역적으로 발생하는 예외를 한곳에서 처리하도록 하였습니다
  • 커스텀 에러를 제외한 에러들은 404와 405에러로 처리하였습니다
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler({CustomException.class, NoHandlerFoundException.class, HttpRequestMethodNotSupportedException.class})
    public ResponseEntity<Object> handleException(Exception e){
        HttpHeaders headers = new HttpHeaders();
        HttpStatus status = determineStatus(e);

        Map<String,Object> errors = new HashMap<>();
        errors.put("Code", getCode(e) );
        errors.put("ErrorMessage", getErrorMessage(e));
        errors.put("Date", String.valueOf(new Date()));

        return new ResponseEntity<>(errors, headers, status);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach(error -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });

        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }

    private HttpStatus determineStatus(Exception e){
        if(e instanceof CustomException){
            return ((CustomException) e).getErrorCode().getHttpStatus();
        } else if (e instanceof NoHandlerFoundException){
            return HttpStatus.NOT_FOUND;
        } else if(e instanceof HttpRequestMethodNotSupportedException){
            return HttpStatus.METHOD_NOT_ALLOWED;
        }
        return HttpStatus.INTERNAL_SERVER_ERROR;
    }

    private int getCode(Exception e){
        if(e instanceof CustomException){
            return ((CustomException) e).getHttpErrorCode();
        }else if ( e instanceof NoHandlerFoundException){
            return HttpStatus.NOT_FOUND.value();
        }else if (e instanceof HttpRequestMethodNotSupportedException){
            return HttpStatus.METHOD_NOT_ALLOWED.value();
        }
        return HttpStatus.INTERNAL_SERVER_ERROR.value();
    }

    private String getErrorMessage(Exception e){
        if(e instanceof CustomException){
            return ((CustomException) e).getErrorCode().getMessage();
        } else if(e instanceof NoHandlerFoundException){
            return "해당 요청을 찾을 수 없습니다";
        } else if(e instanceof HttpRequestMethodNotSupportedException){
            return "지원되지 않는 HTTP 메서드입니다";
        }

        return "Internal ServerError";
    }

}

CustomException(exception)

  • RuntimeException을 상속하였습니다
  • RuntimeException 에는 메세지를 포함하는 생성자가 있어 super(message) 를 통해 전달하자 하는 메세지를 넣어주었습니다.
public class CustomException extends RuntimeException{

    private final ErrorCode errorCode;

    public ErrorCode getErrorCode() {
        return errorCode;
    }

    public int getHttpErrorCode() {
        return Integer.parseInt(errorCode.getErrorCode());
    }

    public CustomException(ErrorCode errorCode) {
        this.errorCode = errorCode;
    }

    public CustomException(Throwable cause, ErrorCode errorCode) {
        super(cause);
        this.errorCode = errorCode;
    }

    public CustomException(String message, ErrorCode errorCode) {
        super(message);
        this.errorCode = errorCode;
    }

    public CustomException(String message, Throwable cause, ErrorCode errorCode) {
        super(message, cause);
        this.errorCode = errorCode;
    }

    public CustomException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace, ErrorCode errorCode) {
        super(message, cause, enableSuppression, writableStackTrace);
        this.errorCode = errorCode;
    }
}

ErrorCode 를 사용하여 에러코드를 정의하고 CustomException ㅇ에서 이를 활용하여 일관된 오류 처리와 응답을 보낼 수 있습니다. 이를 통해 코드의 가독성이 향상되며 유지보수가 쉬워집니다.

→ 또한 Service 계층에서 try-catch문의 사용을 최소화하여 비즈니스 로직에 더욱 집중할 수 있습니다



@Transactional
public long signUp(SignupRequest req) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거도 괜찮은데 따로 서비스용 dto 만드는 것도 알아보세요

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants