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

6주차 : 로그인기능 #82

Open
wants to merge 37 commits into
base: tmxhsk99
Choose a base branch
from
Open

Conversation

tmxhsk99
Copy link

@tmxhsk99 tmxhsk99 commented Aug 7, 2023

안녕하세요!
리뷰 요청드립니다
완성은 못했지만 일단 올리고
시간 날 때 추가하겠습니다
감사합니다.

@tmxhsk99 tmxhsk99 changed the base branch from main to tmxhsk99 August 7, 2023 23:22
Comment on lines 45 to 46
Assertions.assertThat(accessToken).isNotNull();
Assertions.assertThat(accessToken).isEqualTo(VALID_TOKEN);
Copy link
Contributor

Choose a reason for hiding this comment

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

아래에서 문자열과 비교하고 있으니 null인지는 확인하지 않아도 될 것 같아요. 그리고 null이 반환될 수 있다는 것 자체가 코드를 이해하기 어렵게 만드는데, 아예 null을 반환하지 않도록 하는게 좋아요. 물론 코틀린처럼 null을 허용하는 문법을 제공한다면 좀 다르긴 하지만요.

Comment on lines 12 to 15
private final String SECRET = "12345678901234567890123456789010";
private final String VALID_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjF9.neCsyNLzy3lQ4o2yliotWT06FwSGZagaHpKdAkjnGGw";
JwtUtil jwtUtil = new JwtUtil(SECRET);
private final Long USER_ID = 1L;
Copy link
Contributor

Choose a reason for hiding this comment

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

JwtUtil jwtUtil = new JwtUtil(SECRET);은 상수는 아니라서 구분해 주는 게 좋을 것 같아요'

    private final String SECRET = "12345678901234567890123456789010";
    private final String VALID_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjF9.neCsyNLzy3lQ4o2yliotWT06FwSGZagaHpKdAkjnGGw";
    private final Long USER_ID = 1L;

    JwtUtil jwtUtil = new JwtUtil(SECRET);

@hannut91
Copy link
Contributor

hannut91 commented Aug 9, 2023

커밋을 작게 나눌려고 노력하시는 군요. 좋은 커밋을 작성하는 방법 중 하나를 소개해 드리고 싶네요.

커밋은 제목과 본문이 있는데요. 제목은 가장 위에, 본문은 한 줄 띄고 그 다음부터가 본문이죠.

커밋의 제목은 이 커밋을 추가한 이유가 드러나야하고, 본문은 그 이유를 달성하기 위해서 실제로 한 일을 구체적으로 적어주어야 해요.

test : SessionController login 테스트 수정

수정이라는 말은 너무 보편적인 말이에요. 실제로 커밋의 한 일을 보면 @SpringBootTest대신에 @WebMvcTest를 사용하도록 바꿨고, authenticationService을 실제 객체를 사용하던 것에서 Mock 객체를 사용하도록 바꿨으며, 응답을 검사할 때 문자열을 검사하는 것에서 jsonPath로 검사하는 것으로 바꿧어요.

위 일들을 의미를 커밋에 담으려면 지금 하신 것처럼 수정이라는 말 밖에 담지 못하게 돼죠. 즉 내가 너무 보편적인 말을 사용하고 있다면 너무 많은 일을 하는 것이 아닌가? 생각해바야 해요. 이건 함수 이름이나 변수 이름을 지을때도 마찬가지에요. 함수의 이름을 짓는게 너무 어렵다면, 너무 많은 일을 하는 것이 아닌가 의심을 해봐야 해요

In other cases, the verb is vague because the operations performed by the routine are vague. - 코드 컴플리트 2 - 7.3 좋은 루틴 이름

그래서 정말 이유가 드러나도록 커밋을 작성한다면 커밋은 다음과 같이 나눌 수 있을것 같아요.

불필요한 통합 테스트 대신 레이어만 테스트하라

세션 컨트롤러에서 통합 테스트를 작성하고 있었습니다.
서비스 레이어에서 통합 테스트로 작성하고 있기 때문에 컨트롤러에서는 
해당 레이어만 테스트만 해도 충분합니다.
따라서 `@SpringBootTest`를 `@WebMvcTest`로 교체하고 `AuthenticationService`를 Mocking하여 테스트를 작성했습니다.

본문은 이런식으로 쓰면 좋아요.
현재는 ~~~~ 하다. 그런데 ~~~~해야 한다.
그래서 ~~~~, ~~~, ~~~ 를 했다.

대부분 사람들이 다른 사람들도 내가 아는 지식을 알 것이라고 가정하는 실수를 하기 때문에 기존이라는 말을 많이 사용하는데요.
이건 위험합니다. 대신에 현재 상황을 짚어주고, 왜 이걸 바꿔야 했는지 설명해주고, 그걸 달성하기 위해서 구체적으로 무엇을 했는지 작성해 주는 것이 좋습니다

@tmxhsk99
Copy link
Author

tmxhsk99 commented Aug 9, 2023

뭔가 커밋 메시지를 작성하면서도 항상 탐탁치 않은 부분이 있었는데
좋은 말씀 감사드립니다!

AuthenticationService 의 테스트에는 해피테스트만 존재하는 상태이므로 존재하지 않은 유저의 요청정보가 오는 경우의 테스트 추가
자바의 IllegalArgumentException을 발생시키는 예외처리에서 커스텀 예외인 UserNotFoundException으로 명시적으로 예외를 던져주기 위해 UserNotFoundException의 생성자로 Email을 받아 해당 메시지를 표시하도록 수정하였습니다.
예외테스트 시 authenticationService가 생성하지 않아서 상위레벨에서 할당하도록 변경
Comment on lines 56 to 58
private UserLoginData IS_NOT_EXISTS_USER_DATA = UserLoginData.builder()
.email("InvalidEmail")
.password(AUTH_PASSWORD).build();
Copy link
Contributor

Choose a reason for hiding this comment

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

로그인한 경우와 로그인 하지 않은 경우를 테스트를 작성해 주셨네요. 위에선는 AUTH_USER_DATA을 미리 만들어 놓고 사용했었는데, 여기서는 주어진 상황이 달라짐을 명시하기 위해서 IS_NOT_EXISTS_USER_DATA 여기서 만들어주신 것 같아요.

테스트용 데이터를 미리 만들어 놓으려면 둘다 미리 만들어 놓던지 아니면은 각 Context마다 만들던지 일관성있게 맞춰주는게 좋을 것 같아요.

@Nested
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class login_메서드는 {
  private AuthenticationService authenticationService = new AuthenticationService(getUserRepository(), getJwtUtil());

  UserLoginData loginData;

  @Nested
  @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
  class 유효한_유저로그인정보_요청를_받으면 {
  @BeforeEach
  void setUp() {
    getUserRepository().deleteAll();
    getUserRepository().save(User.builder()
      .name(AUTH_NAME)
      .email(AUTH_EMAIL)
      .password(AUTH_PASSWORD)
      .build());

    loginData = UserLoginData.builder()
        .email(AUTH_EMAIL)
        .password(AUTH_PASSWORD)
        .build();
  }

  @DisplayName("인증토큰을 반환한다.")
  @Test
  void It_returns_token() {
    String accessToken = authenticationService.login(loginData);
    Assertions.assertThat(accessToken).isEqualTo(VALID_TOKEN);
  }
}

@Nested
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class 유효하지_않은_로그인정보를_받으면 {
  @BeforeEach
  void setUp() {
    loginData = UserLoginData.builder()
        .email("InvalidEmail")
        .password(AUTH_PASSWORD).build();
  }

  @DisplayName("해당 정보의 회원이 존재하지 않으면 UserNotFoundException을 반환한다.")
  @Test
  void It_throws_UserNotFoundException() {
    Assertions.assertThatThrownBy(() -> authenticationService.login(loginData)).isInstanceOf(UserNotFoundException.class);
  }
}

픽스쳐 위치가 상위 컨텍스트에 공통으로 있는 부분 컨텍스트에 있는 부분 통일 되지 않아서 각 테스트 문맥에 맞도록 픽스쳐 위치 이동 하였습니다
비밀번호가 일치하지 않은 경우 예외 테스트가 존재하지않아 추가
비밀번호가 일치하지 않은 경우 예외처리가 기본 IllegalArgumentException 을 사용하여 명확하지 않은 모호하므로 커스텀에러를 추가
비밀번호가 일치하지 않은 경우를 체크하려면 해당 유저가 존재해야 하므로 해당 정보를 미리 넣어 두도록 수정

@Nested
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class 유효한_유저로그인정보_요청를_받으면 {
Copy link
Contributor

Choose a reason for hiding this comment

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

유효하지 않은 경우도 테스트를 작성해서 스펙을 정의해줘도 좋겠네요

Comment on lines 60 to 66
private UserLoginData IS_NOT_EXISTS_USER_DATA = UserLoginData.builder()
.email(INVALID_EMAIL)
.password(AUTH_PASSWORD).build();
private UserLoginData INVALID_PASSWORD_USER_DATA = UserLoginData.builder()
.email(AUTH_EMAIL)
.password(INVALID_PASSWORD)
.build();
Copy link
Contributor

Choose a reason for hiding this comment

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

이렇게 반복으로 사용되는 테스트용 데이터를 위에서 미리 정의해놓고, 여기서는 사용만 하면 이 부분이 좀 더 단순해질 것 같아요.
테스트용 데이터를 fixture라고 부르는데, 믿고 사용할 수 있는 테스트용 데이터를 미리 정의해놓는 거죠.

그런데 이러한 테스트용 데이터도 한계가 생길 수 있는데 지금처럼 테스트에 필요한 데이터가 점점 증가하기 때문이에요.
이메일이 없는 경우
이메일 형식이 잘못된 경우
비밀번호가 틀린 경우
비밀번호 형식이 틀린경우

이렇게 나열하다보면 끝없이 테스트용 데이터를 정의해줘야하고 그러다 보면 구분하기 힘들어질 수도 있어요. 그러면 아마도
테스트 데이터를 만들어주는 헬퍼 함수같은 것을 도입하고 싶은 마음이 생길거예요

테스트용 픽스처가 테스트코드와 함께 존재하는 상태에서
TestHelper 코드에 픽스쳐 분리하여 리팩터링
SessionControllerTest의 픽스쳐도 TestHelper로 대체 가능한 부분이 있기때문에
해당 부분 TestHelper 사용으로 대체
AccessTokenNotFoundException(인증토큰 없음 예외)
InvalidAccessTokenException(인증토큰 유효성 예외)
누락된 @configuration 어노테이션 추가
기존의 path 패턴의 경우 해당 인증 확인 로직을 타지 않는 경우가 있기 때문에
수정
계층형 테스트 구조로 수정 하고,
인터셉터 추가에 따른 인증로직 검증 테스트 추가
유효하지 않은 유저로그인 요청인 경우의 테스트가 없었기 때문에
해당 테스트 추가 및 테스트 케이스에 따른 픽스쳐를 TestHelper 클래스에 추가 및 이동시켰습니다.
Comment on lines 24 to 27
isGetMethod(request);
if (isPostMethod(request) || isPatchMethod(request) || isDeleteMethod(request)) {
checkAccessToken(request);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

isGetMethod가 삐져나왔네요. GET 메서드일 때도 인증을 확인하는거죠?

Copy link
Author

Choose a reason for hiding this comment

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

앗 이것은 실수입니다;;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        if (isGetMethod(request)) {
            return true;
        }
        
        if (isPostMethod(request) || isPatchMethod(request) || isDeleteMethod(request)) {
            return checkAccessToken(request);
        }

        return true;
    }

이렇게 하려고 했습니다;;;


private boolean checkAccessToken(HttpServletRequest request) throws InvalidAccessTokenException, AccessTokenNotFoundException {
String authorization = request.getHeader("Authorization");
if (authorization == null) throw new AccessTokenNotFoundException();
Copy link
Contributor

Choose a reason for hiding this comment

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

        if (authorization == null) {
            throw new AccessTokenNotFoundException();
        }

한 줄이라도 중괄호를 써주는게 더 보기 좋을 수도 있어요

Comment on lines 52 to 57
try {
jwtUtil.decode(accessToken);
} catch (Exception e) {
throw new InvalidAccessTokenException(e.getMessage());
}
return true;
Copy link
Contributor

Choose a reason for hiding this comment

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

try catch문 아래에 코드가 있는 것은 이해하기 어려울 수 있어요.
try 안에 있는 값을 바로 반환하게 하는게 좋을 것 같아요


@BeforeEach
void setUp() {
getUserRepository().deleteAll();
Copy link
Contributor

Choose a reason for hiding this comment

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

실제 db를 지우지 않도록 조심해야겠네요.

저는 테스트를 실행할 때는 Profile을 test로 하게 하고, 설정에서 테스트 DB를 사용하도록 했어요.

// build.gradle.kts
tasks.withType<Test> {
    useJUnitPlatform()
    environment(mapOf("SPRING_PROFILES_ACTIVE" to "test"))
}
spring:
  datasource: ...

---

spring:
  profiles: test
  datasource:
    url: jdbc:mariadb://localhost:3306/codesoomtest
    username: root
    password: root1234
    driver-class-name: org.mariadb.jdbc.Driver

Copy link
Author

Choose a reason for hiding this comment

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

H2를 보통 테스트 환경에서 쓰다 보니까
인지 못했네요 실제 회사에서는
신경써서 하겠습니다!

@tmxhsk99
Copy link
Author

tmxhsk99 commented Aug 14, 2023

한 주 동안 고생하셨습니다!
평일에 많이 해놔야 되는데 생각보다 그렇게 못했네요;;
윤석님 내일 인프콘 발표하시는 것도 잘 되시기를 바라겠습니다
저도 인프콘 갑니다 기회 되면 인사 드리겠습니다
수고하셨습니다

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.

None yet

2 participants