34일차 - Lv1~Lv5 복습

이해찬·2023년 9월 16일
0

항해일지

목록 보기
25/35

2023.09.16

📙 3주에 걸친 스프링 과제 > lv1~lv5 => 한 번에 실습하기 위해서 정리

양이 많은 만큼 글이 길다 -> 구현하면서 헷갈리거나 자세히 알고 싶은 부분 위주로 다시 정리



과제 요구사항

  1. UseCase & ERD & API 설계
  2. 회원가입 & 로그인 구현
  3. 게시글 작성/조회/수정/삭제
  4. 댓글 작성
  5. JWT , SpringSecurity 활용 -> 검사 및 인증
  6. 게시글 & 댓글 좋아요
  7. 예외 처리 (AOP)

각 과정별 상세 요구사항

📟회원가입, 로그인

📟게시글 작성/조회/수정/삭제

📟댓글 작성/조회/수정/삭제

📟게시글 & 댓글 좋아요

📟예외 처리

🤷‍♂️ 추가 기능 구현


1. UseCase 설계

클라이언트의 요청 -> Filter(jwt) -> DispatchServlet-> HandlerMapping -> AOP(컨트롤러 전,후, 중간 등) -> Controller->Service -> Repository -> EntityMapping -> 응답 반환 -> Filter(로깅, 헤더 등) -> 클라이언트 응답

1-1. ERD 설계

  • Many - > 다 측이 연관관계의 주인으로서 다른 쪽의 외래 키를 참조한다.
    ex) User <-> Post = Post가 다측으로(many) User의 user_id를 참조한다. / user는 mappedby = user(post 측의 변수명)를 통해 연관관계임을 알린다.

🤷‍♂️왜? 많은 쪽에서 외래키를 참조하고 있나?

데이터를 많이 다루는 측에서 외래키를 가지고 있어야 작업하는데 효율적이다.
-> 게시글을 작성을 할 때, 게시글 측에서 외래키(user_id)를 참조한다면 게시글을 작성함과 동시에 외래키를 참조해서 INSERT 쿼리를 날리는데,
(-------------------------------------------------)
-> 반대로 유저 측에서 외래키(post_id)를 가지고 있다면 게시글을 작성하고 INSERT 날리고, 그 다음에 유저 정보를 다시 참조해서 UPDATE를 해야 하는 번거로움이 생기고 비효율적이다. (게시글 측에는 유저의 외래키가 없기 때문 -> 연관관계의 주인이 아니기 때문)

ex) 10개의 게시글 -> 어떤 유저가 작성한건지 알려면? -> 유저 아이디를 알아야 누가 작성한건지 쉽게 구분 가능


🏃‍♂️
🏃‍♂️
🏃‍♂️

2. 회원가입 & 로그인

  • 상태코드 반환 -> ResponseEntity<> 활용 -> 객체에 담아서 반환
  • enum -> 회원의 권한 부여 -> enum은 한정된 값의 집합 / @Enumerated
  • 회원가입 정규 표현식 @Valid -> 예외 처리 -> @RestControllerAdvice
  • 회원가입 비밀번호 -> passwordencoder
  • 예외처리 -> ResponseEntity직접처리 / throw 예외처리 의 차이

🙆‍♂️예외처리의 종류

NullPointerException: 객체 참조가 null인 상태에서 메서드를 호출할 때 발생합니다.
ArrayIndexOutOfBoundsException: 배열의 유효하지 않은 인덱스에 접근하려고 할 때 발생합니다.
ClassCastException: 적절하지 않은 타입으로 객체를 변환하려고 할 때 발생합니다.
NumberFormatException: 숫자 형식이 아닌 문자열을 숫자로 변환하려고 시도할 때 발생합니다.
IllegalArgumentException: 메서드에 잘못된 인수가 전달될 경우 발생하는 런타임 예외입니다.
IllegalStateException: 객체가 메서드 호출을 받기 위한 적절한 상태가 아닐 경우 던져집니다.


>>오류 🤷‍♂️

1. jwt토큰을 활용하지 않고 회원가입만 먼저 구현하는 상황

=> 콘솔에 오류가 찍히지 않고, body에도 뜨지 않음 근데 세션을 받아오는 상황


💻 1. 해결

  • 회원가입 -> PasswordEncoder -> security 기능을 사용 중
  • Spring Security가 classpath에 존재하면, 기본적으로 모든 요청에 대해 인증을 요구
  • 비인증에 대한 처리를 해줘야 한다.
  • exclude = SecurityAutoConfiguration.class -> 비활성화 해줬어야 했다.

2. @Valid의 예외처리 message가 콘솔에만 찍히는 상황 -> body로

=> @ExceptionHandler를 통해서 따로 메시지 처리를 하려는데, 제대로 안나오는 상황

@Valid -> message 가 콘솔이 아닌 body에 보이게 하고 싶은 상황

👇 오류 상황

👉 오류 코드

    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<Map<String, String>> handleValidationException(ConstraintViolationException e) {
        Map<String, String> errors = new HashMap<>();
        for (ConstraintViolation<?> violation : e.getConstraintViolations()) {
            // e.~() -> 예외에 걸린 모든 정보  -> <> -> 집합, for문 돌림
            // 각각에 맞는 필드명 propertyPath = username / password
            // 각각에 맞는 메시지 = 오류메시지

            String propertyPath = violation.getPropertyPath().toString();
            String message = violation.getMessage();
            errors.put(propertyPath, message);
        }
        return ResponseEntity.badRequest().body(errors);
    }

💻 2. 해결

  • ConstraintViolationException -> @Valid 와 같은 유효성 검사에 실패했을 때 발생하는 예외
  • 그러나 실제로는 Validator.validate() 와 같은 메서드를 통해 객체의 유효성 검사(직접)할 때 일어나는 예외
  • @ValidMethodArgumentNotValidException 에서 발생 한다.
    ->그래서 제대로된 메시지를 반환하지 못함
  • ConstraintViolationException -> Bean Validation API에 의해
  • MethodArgumentNotValidException -> Spring MVC에 의해

👉 수정 코드

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(
            MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();

        ex.getBindingResult().getAllErrors().forEach((error) -> {
            // BindeResult 에서 모든 에러를 가지고 온다.
            // 해당 필드의 이름과 메시지를 담는다.
            
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return ResponseEntity.badRequest().body(errors);
    }

ConstraintViolationException 사용했을 때
Spring에서 @Valid 어노테이션을 사용하면, 유효성 검사에 실패한 경우 자동으로 MethodArgumentNotValidException이 발생 => 이 예외는 Spring MVC의 기본 예외 처리기인 DefaultHandlerExceptionResolver에 의해 처리되어 HTTP 상태 코드 400(Bad Request)를 반환 (만약 따로 커스텀을 안해준다면)

MethodArgumentNotValidException 사용했을 때

비즈니스 로직에서 처리해도 되지만, 코드가 복잡해져 따로 빼놓아서 처리해 봤는데, 좋다.



🏃‍♂️
🏃‍♂️
🏃‍♂️

3. 게시글 API

  • 토큰 검사 및 인증 -> 작성/수정/삭제
  • @AuthenticationPrincipal @CookieValue
  • 만약 애플리케이션이 Spring Security를 이용해 인증을 처리하고 있다면, @AuthenticationPrincipal을 사용하는 것이 좋다. 이미 인증된 사용자의 세부 정보가 필요한 경우 이 어노테이션을 통해 직접 접근할 수 있기 때문이다.
  • 반면에 Spring Security가 아닌 다른 방식(예: JWT 토큰)으로 인증을 처리하거나, 요청 쿠키에 저장된 일반적인 데이터가 필요한 경우에는 @CookieValue를 선택하는 것이 좋다.
  • 토큰 인증 방식
    1. 📟 @AuthenticationPrincipal
//게시글 작성 -컨트롤러
    @PostMapping("/board")
    public BoardResponseDto createBoard(
            @RequestBody BoardRequestDto boardRequestDto,
            @AuthenticationPrincipal UserDetailsImpl userDetails) {
        return boardService.createBoard(boardRequestDto, userDetails.getUser());
    }
    
    
 // 서비스 로직
     public BoardResponseDto createBoard(BoardRequestDto boardRequestDto, User user) {
        Board board = boardRepository.save(new Board(boardRequestDto, user));
        return new BoardResponseDto(board);
    }

2. 📟 method -> 직접 컨텍스트 홀더에서 가져옴

// 게시글 작성
    @PostMapping("/post")
    public ResponseDto createPost(@RequestBody RequestDto requestDto) {
        return postService.createPost(requestDto);
    }
    
    
 // 서비스 로직
     public ResponseDto createPost(RequestDto requestDto) {
        User currentUser = getCurrentUser();

        // entity 관계 설정 추가
        Post post = new Post(requestDto);
        post.setUser(currentUser);


        // db에 저장
        Post savePost = postRepository.save(post);

        // 반환
        ResponseDto responseDto = new ResponseDto(savePost);
        return responseDto;

    }
 
 
 
    public User getCurrentUser() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication.getPrincipal() instanceof UserDetails) {
            UserDetails userDetails = (UserDetails) authentication.getPrincipal();
            String currentUsername = userDetails.getUsername();
            return userRepository.findByUsername(currentUsername).orElseThrow(
                    () -> new IllegalArgumentException("인증된 사용자를 찾을 수 없습니다.")
            );
        } else {
            throw new IllegalStateException("올바른 인증 정보가 아닙니다.");
        }
    }

3. 📟 @CookieValue

//게시글 작성 -컨트롤러
    @PostMapping("/post")
    public ResponseEntity<?> createPost(@RequestBody PostRequestDto requestDto,
                                      @CookieValue(JwtUtil.AUTHORIZATION_HEADER) String tokenValue)  {
        return postService.createPost(requestDto,tokenValue);
    }
    
    
 // 서비스 로직
     public ResponseEntity<?>  createPost(PostRequestDto requestDto, String tokenValue) {
        User principal = SecurityUtil.getPrincipal().get();
        String username = principal.getUsername();

        //RequestDto -> Entity
        Post post = new Post(requestDto, username);
        //DB 저장
        Post savePost = postRepository.save(post);

        //Entity -> ResponseDto
        return new ResponseEntity<>(new PostResponseDto(savePost),null, HttpStatus.OK);
    }
    

💻 어떤 인증 방식의 구조인가?

1,2번은 -> securityconfig 다른 요청이 들어오면 인증 과정을 거치는데, 굳이
인증된 사용자 정보를 가져오는 이유는??
인증된 토큰을 위조하지는 않는지, 제대로된 데이터를 보내고 있는지 2차적으로 검증하는 효과
3번은 말그대로 토큰자체를 가지고와서 비교하는 방식

적용 범위: getCurrentUser()는 어플리케이션의 여러 부분에서 사용될 수 있습니다. 반면, @AuthenticationPrincipal은 주로 컨트롤러에서 사용됩니다.
확장성: getCurrentUser()는 로직을 중앙화하여 관리하기 때문에, 추후 로직 변경이나 추가 처리가 필요할 때 용이합니다.
편의성: @AuthenticationPrincipal은 컨트롤러에서 직접적으로 사용자 정보를 주입받을 수 있어 간단한 경우에 편리합니다.

  • 최초의 컨트롤러 매핑 -> anyRequest().authenticated() 그 외 모든 요청 인증처리 ->
  • 1차적으로 "이 사용자는 누구인가? -> 2차적으로 서비스 로직 딴에서 검증하는 이유는
  • @AuthenticationPrincipal or getCurrentuser() -> 이 사용자의 세부 정보나 권한은 무엇인가?를 확인



요약하면, anyRequest().authenticated() 설정은 모든 요청이 인증되어야 함을 나타내며, 
이에 따라 필터 체인 내의 JWT 관련 필터들이 실행됩니다. 
이 필터들을 통과한 후에는 사용자의 인증 정보가 SecurityContextHolder에 저장되고, 
이 정보를 @AuthenticationPrincipal이나 getCurrentUser()를 통해 불러와 사용하게 됩니다. 
이렇게 두 번째로 인증 정보를 확인하는 것은 보안을 강화하기 위한 방법입니다.

  • 게시글을 작성할 때, 따로 서비스 로직을 작성해야 되는건가?? 라고 생각
  • 그러나 이미 필터딴에서 다른 요청이 들어 올 때, 검증을 하기 때문에 또 토큰을 비교하는 로직은 필요없다
  • 그렇다면 굳이 가져오는 이유는 사용자의 정보를 이용하기 위해서
profile
디자인에서 개발자로

0개의 댓글