[Project] 회원가입, 로그인 기능 구현

bin1225·2024년 11월 29일
0

Project_Only

목록 보기
7/9
post-thumbnail

Dto Validation

회원가입 시 SignUpDto를 받아서 처리한다.
필드는 name, loginId, password로 구성된다.

각 필드마다 제약조건(NotEmty, 길이제한)을 검증하고자 bean validation기능을 사용했다.

@NotBlank
@NotEmpty
@NotNull
@Size
@Min
@Max

[Spring Boot] @NotNull, @NotEmpty, @NotBlank 의 차이점 및 사용법

그리고 검증에 대한 테스트를 작성하는 과정에서 Error를 확인했다.

  • Error Message
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'handlerExceptionResolver' defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Failed to instantiate [org.springframework.web.servlet.HandlerExceptionResolver]: Factory method 'handlerExceptionResolver' threw exception with message: Ambiguous @ExceptionHandler method mapped for [class org.springframework.web.bind.MethodArgumentNotValidException]: {protected org.springframework.http.ResponseEntity com.project.only.error.GlobalExceptionHandler.handleMethodArgumentNotValidException(org.springframework.web.bind.MethodArgumentNotValidException), public final org.springframework.http.ResponseEntity org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler.handleException(java.lang.Exception,org.springframework.web.context.request.WebRequest) throws java.lang.Exception}

@Min, @Max, @Size 차이

signUpDto에 올바른 값을 넣어도 예외가 발생했다.
여러가지 시도를 해보다가 validation 어노테이션을 지워가며 테스트를 수행해봤는데, @Min @Max 가 적용이 안되는 듯 했다.

String type의 크기를 검증할 때는 @Size()어노테이션을 이용해야한다.

@Min @Max는 숫자타입에만 적용된다.

https://stackoverflow.com/questions/11189398/difference-between-sizemax-value-and-minvalue-and-maxvalue

로그인 시 Session 생성 테스트

로그인 과정에서 로그인 성공 시 session을 생성한다.

session이 잘 생성되는지 테스트했다.

 HttpSession session = resultActions.andReturn().getRequest().getSession(false);
        Assertions.assertThat(session).isNotNull();

뒤적거리다가 ResultActions class에 getRequest().getSession()을 통해 session에 대한 값을 얻어올 수 있음을 알았다.

getResponse에도 getCookies 메서드가 있어서 Cookie에 저장된 session값을 통해 테스트해보려 했는데 이 경우는 자꾸 Cookie가 존재하지 않아 테스트가 실패했다.

최근 쿠키와 세션에 대해 배웠을 때 결국 sessionId를 쿠키에 담아 클라이언트에게 전달하고 해당 sessionId로 서버내에서 관련 정보를 찾아 사용한다고 배웠다.

그래서 이론대로라면 getResponse에서 cookie값을 통해 session을 확인할 수 있어야하는데, 그렇지 않아서 의문이었다.

test에서 getSession으로 얻은 세션에 대한 정보를 출력해보니 다음과 같이 나왔다.

session: org.springframework.mock.web.MockHttpSession@16576a6

정보에 mock이 관련된 걸 보니 Mock테스트를 이용하면 session에 대한 정보도 임의로 생성해서 request에 담기 때문에 실제 동작과는 실행 구조가 다를 수 있을 것 같다.


Optional을 사용하는 이유

Optional은 Null이나 Null이 아닌 값을 저장하는 컨테이너 클래스로 값을 한 번 더 감싸 Null로 인한 오류 발생을 방지한다.

Optional 자체가 결과없음을 명확히 드러내는 메소드 반환값으로 사용되도록 설계되었다. 즉 사용 목적 자체가 매우 제한적이다.

Optional을 잘못 사용하게 되면 새로운 문제점들이 생겨난다.

Optional로 발생할 수 있는 문제와 올바른 사용법

https://mangkyu.tistory.com/203

api로 회원가입 기능을 수행해보는 과정에서 Error가 발생했다.

EmptyResultDataAccessException: No result found for query 

query에 대한 응답 결과가 없다는 것.

회원가입을 위해 중복 회원 존재 여부를 판단해야 했고, 그 과정에서 같은 아이디를 가진 회원이 있는지 검색하는 로직이 존재했다. 그리고 당연히 정상적인 상황이라면 아무 결과도 반환하지 않아야 하는데, 그게 Error의 원인이라니 뭔가 싶었다.

이유는
jpa의 getSingleResult()메서드가 반환값이 없는 경우 예외를 발생시키는 것이다.

   public Optional<Member> findByLoginID(String loginID) {
        List<Member> findMember = em.createQuery(
                        "select m from Member m where m.loginId = :loginId", Member.class)
                .setParameter("loginId", loginID)
                .getResultList();

        return findMember.isEmpty() ? Optional.empty() : Optional.of(findMember.getFirst());
    }
    

따라서 위와 같이 코드를 수정했다.

Optional을 활용하여, 결과의 존재 여부에 대해 명확히 드러내도록 반환타입을 설정했다.
getResultList()메서드로 값이 없는 경우에도 예외가 발생하지 않도록 했고, 해당 결과값에 데이터가 들어있는 경우와 아닌 경우를 나눠서 Optional.empty()Optional로 감싼 Member객체를 반환하도록 하였다.


Interceptor로 로그인 인증 구현

로그인한 사용자만 접근할 수 있는 api들이 있다. 각 api에서 직접 로그인한 사용자인지 검증하는 로직을 구현한다면 중복 코드가 다수 발생한다.

이를 개선하기 위해 필터와 인터셉터를 사용한다.

필터와 인터셉터는 작동하는 위치(순서)와 범위 지정, 사용할 수 있는 기능의 차이가 있다.

인터셉터가 제공하는 기능이 다양하고 범위를 세밀하게 지정할 수 있으므로 인터셉터를 사용했다.

또 필터의 경우 직접 컨트롤러를 호출해야 하지만 인터셉터는 return 값에 따라 자동흐로 실행 여부가 결정된다. (true일 때만 실행)

이는 인터셉터의 기능 자체만 고려할 수 있도록 분리해주기 때문에 더 명확하다.

요청 흐름

HTTP요청 - WAS - 필터 - 서블릿 - 스프링 인터셉터 - 컨트롤러

LoginCheckInterceptor


@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String requestURI = request.getRequestURI();

        log.info("로그인 인터셉터 실행 {}", requestURI);
        HttpSession session = request.getSession(false);

        if (session == null || session.getAttribute("member") == null) {
            log.info("미인증 사용자 요청");
            response.sendRedirect("/login?redirect="+requestURI);
            return false;
        }

        return true;
    }
}

WebConfig

직접 구현한 인터셉터는 WebConfig를 통해 등록한다.

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginCheckInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/", "/members/login", "/members/add", "/members/logout", "/error");
    }
}

인터셉터가 여러개인 경우 .order로 순서를 지정할 수 있다.
.excludePathPatterns로 인터셉터가 적용되지 않는 경로도 명확히 설정할 수 있다.


@SessionAttribute

Session을 이용해 로그인을 인증하면, Session에 들어있는 정보를 이용해 Member객체에 대한 정보를 찾을 수 있다.

현재는 Member객체를 저장해뒀지만, 권장되는 것은 꼭 필요한 정보만 저장해두고 필요시 알고 있는 값으로 찾아 쓰는 게 권장된다.

이때 Session에 있는 값을 @SessionAttribute어노테이션을 이용해 받으면 코드를 깔끔하게 작성할 수 있다.

또 받는 과정에서 문제가 발생하면 그에 대한 예외도 발생시킨다.

    @PostMapping("/only/diary/add")
    public ResponseEntity<DiaryResponse> createDiary(@SessionAttribute(SessionConst.LOGIN_MEMBER) Member member, @RequestBody DiaryRequest diaryRequest) {
        Diary diary = diaryService.createDiary(member.getId(), diaryRequest);
        return ResponseEntity.status(HttpStatus.CREATED)
                .body( DiaryResponse.of(diary));
    }

0개의 댓글