Pagination / Entity Association / Filter / AuthenticationFailureHandler (항해일지 23일차)

김형준·2022년 5월 31일
0

TIL&WIL

목록 보기
23/45

1. 학습일지


1) Pagination

API 설계

  • Client -> Server

      1. 페이징
        1. page : 조회할 페이지 번호 (1부터 시작)
        1. size : 한 페이지에 보여줄 상품 개수 (10개로 고정!)
      1. 정렬
        1. sortBy (정렬 항목)
          1. id : Product 테이블의 id
          1. title : 상품명
          1. lprice : 최저가
          1. createdAt : 생성일 (보통 id 와 동일)
        1. isAsc (오름차순?)
          1. true: 오름차순 (asc)
          1. false : 내림차순 (desc)
  • Server -> Client

    • number: 조회된 페이지 번호 (0부터 시작)
    • content: 조회된 상품 정보 (배열)
    • size: 한 페이지에 보여줄 상품 개수
    • numberOfElements: 실제 조회된 상품 개수
    • totalElement: 전체 상품 개수 (회원이 등록한 모든 상품의 개수)
    • totalPages: 전체 페이지 수
    • first: 첫 페이지인지? (boolean)
    • last: 마지막 페이지인지? (boolean)

Pagination controller-service-repository 적용

  • controller
    • @RequestParam으로 받아와서 service에 인자로 넘겨주기
@RequestParam("page") int page,
@RequestParam("size") int size,
@RequestParam("sortBy") String sortBy,
@RequestParam("isAsc") boolean isAsc,
  • service
    • controller가 던져준 인자를 파라미터로 받아 값을 사용한다
    • 필요한 값은 repository에 던져줄 Pageable 타입의 객체이다.
    • direction = Sort.Direction Enum을 사용하여 삼항연산자로 올바른 정렬 방향(asc, desc) 값을 담아준다.
    • sort = Sort 타입의 객체에 Sort.by(direction, 정렬 기준이 되는 property)를 넣어 생성
    • Pageable pagealbe = PagerRequest.of(page, size, sort);
    • return 타입은 모두 Page로 바꿔준다. (controller도)
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

    public Page<Product> getProducts(Long userId, int page, int size, String sortBy, boolean isAsc) {
        Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC;
        Sort sort = Sort.by(direction, sortBy);
        Pageable pageable = PageRequest.of(page, size, sort);
        return productRepository.findAllByUserId(userId, pageable);
    }
  • repository
    • JPA는 pagination 기능 또한 구현되어있다.
      • Page<@Entity 클래스> 메서드(Pageable pageable) 로 작성하면 끝!
      • repository에 따로 구현해준 메서드가 아닌 기본 제공 메서드에는 인자로 pageable만 넣어주면 됨
Page<Product> findAllByUserId(Long userId, Pageable pageable);

Pagination 검증을 위한 데이터 삽입

  • Data를 넣어줄 클래스 구현
    • @Component 등록
    • implements ApplicationRunner하고, run메서드를 오버라이드 하면 스프링 기동 시 해당 메서드를 실행한다.
    • 구현에 필요한 빈들을 @Autowired 해주고, 데이터를 넣는 코드 구현

2) 폴더 기능 구현

  • 폴더 테이블 설계

    • 기존 상품 테이블 설계 방식과 동일하게 사용한다면, 사실상 DB에서는 두 테이블 간 연관관계가 없다.
    • 따라서 연관관계를 명시해주는 것이 좋다.
  • JPA 연관관계를 이용한 폴더 테이블 설계

    • 회원 1명이 여러개의 폴더를 가질 수 있다. @OneToMany
    • 폴더 1개는 하나의 회원을 저장할 수 있다. @ManyToOne (회원은 폴더를 여러개 가질 수 있기에 Many)
    • 연관관계를 명시하면 (위 어노테이션을 붙이면) 아래와 같이 Spring data JPA의 기능도 사용 가능하다.
List<Folder> folders = user.getFolders();
User user = folder.getUser();
  • 기존의 방식보다 훨씬 간편하게 구현 가능하다.
  • JPA 연관관계 Column 설정 방법
@ManyToOne
@JoinColumn(name = "USER_ID", nullable = false)
private User user;
  • @JoinColumn 내 속성값 설정
    - name: 외래키 명
    - nullable: 외래키 null 허용 여부
    - false (default)
    - 예) 폴더는 회원에 의해서만 만들어짐. user 값이 필수
    - true
    - 예) 공용폴더의 경우, 폴더의 user 객체를 null 로 설정하기로 함

3) AuthenticationFailureHandler

스프링 시큐리티 동작구조

  • Spring Security의 의존성을 추가하면 위의 그림과 같이 WebSecurityConfigurerAdapter 클래스가 실행된다.
  • WebSecurityConfigurerAdapter의 내부 메서드인 getHttp()가 실행될 때 HttpSecurity를 생성한다.
  • HttpSecurity는 인증 API와 인가 API들을 제공한다.
  • 지금까지 내가 구현했던 WebSecurityConfig 클래스는 (그림에서 빨간 네모 박스) WebSecurityConfigurerAdapter를 상속받아, configure 메서드에 HttpSecurity의 API들을 사용했던 것이다.

AuthenticationFailureHandler

  • 오늘은 HttpSecurity의 인증 API인 http.formLogin()부분을 커스터마이징 구현했다.
  • 그 이유는 로그인 실패 처리가 디폴트로 이루어지기 때문에, 아이디 혹은 비밀번호를 잘못 입력한 경우 경고 메세지를 띄워달라는 요청이 있었기 때문이다.
  • 이.. 하나의 요청으로.. 5시간을 헤맸다. 아래는 구현 과정을 기술한다.
  • 먼저 .formLogin() 아래 failureHandler()를 작성한 뒤 인자 값으로 AuthenticationFailureHandler를 implements한 클래스를 넣어준다.
  • 해당 클래스를 SecurityConfig 클래스에 빈으로 DI한다.
  • 설정은 끝이다. 이제 로그인 실패 시 내가 정의한 AuthenticationFailureHandler 자식 클래스의 onAuthenticationFailure() 메서드가 실행된다.
  • 문제는 지금부터 시작된다.
  • 일단 HttpServletRequest/Response에 대한 개념이 없었다.
  • 각각 어떠한 필드값을 갖는지, 어떠한 메서드를 제공하는 지 몰랐다.
  • 그래서 구글링에 의존할 수 밖에 없었는데, 구글에 구현된 방식은 대부분 JSP에 해당 값을 포워드 해주는 방식이었다.
request.setAttribute("LoginFailMessage", "아이디 또는 비밀번호가 일치하지 않습니다.");
request.getRequestDispatcher("/user/login?error").forward(request,response);
  • 이렇게 작성하면 내가 지정한 FailureDefaultPage("/user/login?error)로 이동하지 않고 정상 로그인 url("/user/login)으로 리다이렉트 되었다.
  • 관련해서 열심히 구글링 했으나 질문도 많이 없었고, 해결방법 또한 나한텐 먹히지 않았다.
  • 그래서 결국 response를 이용하여 redirect하고 query로 오류메시지를 넘겨줬다.
  • 이부분은 따로 공부하고, 기술매니저님께 여쭤보자!
@Override
    public void onAuthenticationFailure(
            HttpServletRequest request,
            HttpServletResponse response,
            AuthenticationException exception) throws IOException, ServletException {


        if(exception instanceof BadCredentialsException) {
            String errorMessage = exception.getMessage() + "아이디 혹은 비밀번호를 확인해주세요.";
            ObjectMapper objectMapper = new ObjectMapper();
            String encodedErrMsg = URLEncoder.encode(errorMessage, "utf-8");
            response.sendRedirect("/user/login?error=" + encodedErrMsg);

            // 아래 방식으로 하다가 6시간이 날아갔습니다 선생님. 아래 방식은 왜 안되는건가요?.....
//            request.setAttribute("LoginFailMessage", "아이디 또는 비밀번호가 일치하지 않습니다.");


//            request.getRequestDispatcher("/user/login/" + errorMessage).forward(request,response);
//            System.out.println(exception.getMessage());

        }
//        else if(exception instanceof AuthenticationServiceException) {
//            request.setAttribute("LoginFailMessage", "죄송합니다. 시스템에 오류가 발생했습니다.");
//        }
//        else if(exception instanceof DisabledException) {
//            request.setAttribute("LoginFailMessage", "현재 사용할 수 없는 계정입니다.");
//        }
//        else if(exception instanceof LockedException) {
//            request.setAttribute("LoginFailMessage", "현재 잠긴 계정입니다.");
//        }
//        else if(exception instanceof AccountExpiredException) {
//            request.setAttribute("LoginFailMessage", "이미 만료된 계정입니다.");
//        }
//        else if(exception instanceof CredentialsExpiredException) {
//            request.setAttribute("LoginFailMessage", "비밀번호가 만료된 계정입니다.");
//        }
//        else request.setAttribute("LoginFailMessage", "계정을 찾을 수 없습니다.");

//        response.sendRedirect("/users/login");


    }

Form Login 과정 추가정리

  • Form Login을 사용하면 인증 필터인 UsernamePasswordAuthenticationFilter가 실행된다.
  • AntPathRequestMatcher는 요청 정보의 url이 해당 값(/login)으로 시작되는 지 체크하며, 요청 정보와 불일치 시 2-1 chain.doFilter로 넘기고, 일치 시 Authentication 객체를 생성하여 Authentication Manager에게 넘긴다.
    ** url 값은 .loginProcessingUrl("/login")의 값 변경에 따라 변경된다.
  • 이후의 과정은 공부했던 것과 유사하다.

2. 코멘트

  • 오늘도 역시 에러에서 벗어나지 못했다.
  • 하루하루 마주하는 에러들이 이제는 익숙하다.
  • 오류가 많이 나도 좋다. 오히려 더 공부하게 되고 다음에 비슷한 상황에서 유연한 대처가 가능해지기 때문이다.
  • 틀리는걸 두려워하지 말자. 아무리 큰 오류라 해도 프로젝트 삭제하면 그만이다.
  • 오늘도 고생 많았다. 내일도 힘내자!!👍
profile
BackEnd Developer

0개의 댓글