과제 트러블 슈팅 정리 및 회고(PasswordEncoder의 encode, matches 메서드)

iy·2024년 2월 29일
1

과제에서 맡았던 부분

  • 마이페이지 내 정보 조회
  • 마이페이지 한 줄 소개 수정
    - 비밀번호 입력 후 일치하면 수정 가능
  • 마이페이지 비밀번호 수정
    - 현재 비밀번호가 일치해야 수정 가능
    - 비밀번호 직전에 3회 변경 기록이 있는 비밀번호로 수정 불가능
  • 마이페이지 내 작성글 목록 조회
  • 마이페이지 내 댓글 목록 조회
  • 맡은 기능 slice test 작성
    마이페이지 서비스 부분
    해당 과제 링크

해결했지만 어려웠던 점

1. 마이페이지 비밀번호 수정
: 비밀번호를 이전 3회 사용한 비밀번호 재사용 불가능, 현재 비밀번호가 일치해야 수정이 가능하게 구현했다. 하지만 이 조건을 만족해 수정한 후에는 로그인이 되지 않는 상황이 발생했다.

  • 구체적인 상황 및 원인
    : User 테이블과 PasswordHistory 테이블이 존재하고 회원가입이 되었을 때 이 두 테이블에 비밀번호가 encoding된 값으로 저장이 된다. 이후 비밀번호 수정이 되면 수정된 값 모두 PasswordHistory 테이블에 저장되고 최신 값으로 User 테이블은 수정해서 들어간다.
    지금 상황은 두 테이블에 상황에 맞게 데이터가 각각 저장되는데 수정된 후 로그인이 되지 않는 상황이다.
    그래서 DB에 알맞게 저장되는지 확인해보았고 User테이블에 encoding값이 아닌 사용자가 입력한 값이 그대로 들어가고 있는 상황을 확인했다.

  • 기존 코드

	@Transactional
    public void updateMyPassword(User user, UpdatePasswordRequestDto updatePasswordRequestDto) {
        String encryptNewPassword = passwordEncoder.encode(
            updatePasswordRequestDto.getNewPassword());
        PasswordHistory passwordHistory = new PasswordHistory();

        user = userRepository.findById(user.getId())
            .orElseThrow(() -> new InvalidInputException(ErrorCode.USER_NOT_FOUND));

        // 비밀 번호 사용자 입력값과 DB의 저장된 값 비교
        if (!isPasswordMatches(user.getPassword(), updatePasswordRequestDto.getCurrentPassword())) {
            throw new InvalidInputException(ErrorCode.INVALID_PASSWORD);
        }

        // 최근 3번 안에 사용된 비밀번호 재사용 제한
        if (!isPasswordPreviouslyUsed(user, updatePasswordRequestDto)) {
            throw new InvalidInputException(ErrorCode.REUSED_PASSWORD);
        }

        // 수정한 비밀번호 User와 PasswordHistory에 저장
        user.updatePassword(updatePasswordRequestDto.getNewPassword());
        PasswordHistory newPasswordHistory = passwordHistory.toPasswordHistory(user,
            encryptNewPassword);
        passwordHistoryRepository.save(newPasswordHistory);
    }

작성한 코드를 보면 직접 만든 isPasswordMatches(), isPasswordPreviouslyUsed() 메서드로 비밀번호 조건을 검증한다. 이 두 상황을 확인한 후에

user.updatePassword(updatePasswordRequestDto.getNewPassword());

user엔티티에 updatePassword()를 통해 수정해주는 부분에서 encoding 되지 않은 사용자 입력값을 그대로 저장해주는 것을 볼 수 있다.
회원가입을 통해 인코딩해서 DB에 저장해주고 로그인 때 사용자가 작성한 비밀번호를 인코딩해 DB와 비교해준다. 비밀번호를 수정하면 encoding되지 않은 값으로 수정되기 때문에 인코딩한 값으로 비교해주는 로그인에서 문제가 발생한 것이다.

  • 수정 코드
    @Transactional
    public void updateMyPassword(User user, UpdatePasswordRequestDto updatePasswordRequestDto) {
        String encryptNewPassword = passwordEncoder.encode(
            updatePasswordRequestDto.getNewPassword());
        PasswordHistory passwordHistory = new PasswordHistory();

        user = userRepository.findById(user.getId())
            .orElseThrow(() -> new InvalidInputException(ErrorCode.USER_NOT_FOUND));

        // 비밀 번호 사용자 입력값과 DB의 저장된 값 비교
        if (!isPasswordMatches(user.getPassword(), updatePasswordRequestDto.getCurrentPassword())) {
            throw new InvalidInputException(ErrorCode.INVALID_PASSWORD);
        }

        // 최근 3번 안에 사용된 비밀번호 재사용 제한
        if (!isPasswordPreviouslyUsed(user, updatePasswordRequestDto)) {
            throw new InvalidInputException(ErrorCode.REUSED_PASSWORD);
        }

        // 수정한 비밀번호 User와 PasswordHistory에 저장
        user.updatePassword(encryptNewPassword);
        PasswordHistory newPasswordHistory = passwordHistory.toPasswordHistory(user,
            encryptNewPassword);
        passwordHistoryRepository.save(newPasswordHistory);
    }
  • 해결 방안
String encryptNewPassword = passwordEncoder.encode(
            updatePasswordRequestDto.getNewPassword());

사용자의 입력값인 updatePasswordRequestDto.getNewPassword()을 인코딩 한 encryptNewPassword으로 수정해서 저장해주었다. 이후 로그인할 때 인코딩된 값을 DB와 잘 비교해주는 것을 확인할 수 있었다.

  • 배운 점

PasswordEncoder 내에 encode 메서드를 배우고 사용했다.

String encryptNewPassword = passwordEncoder.encode(
            updatePasswordRequestDto.getNewPassword());

메서드에 Parameter로 String 값을 입력하면
$2a1010axYOxKOX5USMsqU.EvnDxO35DZWCx3pnyI... 이런식으로 인코딩 된 값을 String type으로 리턴해준다.

2. 비밀번호 수정 중 입력값 비교 과정

  • 구체적인 상황 및 원인
    사용자가 비밀번호를 입력하면 해당 입력값과 DB에 저장된 비밀번호를 비교해줘야 하는데 기존에 있던 matches메서드 사용에 있어서 DB의 값과 입력값을 각각 어디에 입력해 사용해야 하는지에 대해 어려움을 느꼈다.

  • 기존 코드

private boolean isPasswordMatches(String password1, String password2) {
        return passwordEncoder.matches(password2, password1);
    }

boolean 형식의 isPasswordMatches를 만들었고 passwordEncoder 내에 matches()를 이용해 값을 return해줬다. 이때 password1, password2에 어떤 값을 줘야 하는지에 대해 혼란을 많이 느꼈다.

  • 수정 코드
private boolean isPasswordMatches(String passwordInDB, String inputPassword) {
        return passwordEncoder.matches(inputPassword, passwordInDB);
    }
  • 해결 방안

    matches에 대해 공부하기 위해 검색을 많이 해봤는데 한 벨로그 글에서 힌트를 크게 얻을 수 있었다. 해당 글을 쓴 사람은 나처럼 password1, password2처럼 명확하지 않은 변수명을 지정하지 않고 DB값이 들어가야 하는 곳, 입력값이 들어가야 하는 곳의 파라미터 명을 명확하게 구분가게 지어줬다.
    덕분에 어디에 무슨 값이 들어가야 하는지 확실히 알 수 있었고 코드도 그에 맞게 수정할 수 있었다.
    matches메서드 참고 블로그

  • 배운 점

    • passwordEncoder.matches(inputPassword, passwordInDB)
      DB의 encoding되어 저장된 비밀번호와 일반 String값을 비교해주는 메서드이다. 앞으로 비밀번호에 대한 구현할 부분이 생기면 유용하게 사용할 수 있을 거 같다.
    • 파라미터 값에 들어가는 등 변수명 설정에 조금 더 신경써서 네이밍해줘야 겠다는 점을 배웠다. 정말 오랜 시간 고민한 부분을 네이밍 잘 지어준 블로그 글을 보고 바로 깨달았기 때문이다.. 이번 과제에선 내가 만든 메서드를 해당 클래스 안에서 나만 사용했지만 나중에 협업을 할 때 함께 사용하는 메서드를 구현했을 때 정확한 네이밍을 해야 함께 일하는 팀원에게 혼란을 주지 않아 더 좋은 협업을 할 수 있을 거 같다 생각했다.

3. 이전 3회 사용 이력이 있는 비밀번호 사용 제한

  • 구체적인 상황 및 원인
    과제 요구사항이었는데 사용자 당 3회 사용 기록이 있는 비밀번호 사용을 제한을 줘야 했다.

  • 기존 코드

	private boolean isPasswordPreviouslyUsed(User user,
        UpdatePasswordRequestDto updatePasswordRequestDto) {
        boolean result = true;
        int num = 0;
        int count = 0;
        List<PasswordHistory> passwordList = passwordHistoryRepository.findAllByUserIdOrderByCreatedAtDesc(
            user.getId());
        num = passwordList.size() < 3 ? passwordList.size() : 3;
        for (int i = 0; i < num; i++) {
            count +=
                isPasswordMatches(passwordList.get(i).getPassword(),
                    updatePasswordRequestDto.getNewPassword()) ? 1 : 0;
        }
        result = count == 0 ? true : false;
        return result;
    }

우선 jpa 쿼리 메서드를 이용해 유저의 id별로 PasswordHistory에 저장된 비밀번호를 다 가져와 list에 담아준다.
이 리스트의 값이 제한할 숫자 3보다 작으면 IndexOutOfBoundsException를 방지해 num에 해당 사이즈를 담아주고 3보다 크면 3개까지만 비교를 해주면 되기 때문에 3을 담아준다. 미리 만들어둔 메서드 isPasswordMatches를 이용해 이 값들을 반복문으로 비교해주면서 일치하면 1을 count에 더해준다.
num만큼 반복문을 돌고 나서 count가 0이면 true를 0이 아니면 false를 반환하게 코드를 짰다.

  • 수정 코드
	private boolean isPasswordPreviouslyUsed(User user,
        UpdatePasswordRequestDto updatePasswordRequestDto) {
        List<PasswordHistory> passwordList = passwordHistoryRepository.findTop3ByUserIdOrderByCreatedAtDesc(
            user.getId());
        for (PasswordHistory password : passwordList) {
            if (isPasswordMatches(password.getPassword(),
                updatePasswordRequestDto.getNewPassword())) {
                return false;
            }
        }
        return true;
    }
  • 해결 방안

    수정 전 코드에선 passwordHistoryRepository.findAllByUserIdOrderByCreatedAtDesc(
    user.getId());를 이용해 해당 아이디에 모든 비밀번호 변경값을 가져온다.
    구현한 부분을 튜터님과 다른 수강생 분이 봐주셨는데 passwordHistoryRepository.findTop3ByUserIdOrderByCreatedAtDesc(
    user.getId());이 query메서드를 활용하는 것이 좋겠다고 하셨다.
    이 메서드를 사용하면 전체 리스트가 아닌 비교할 때 필요한 3개의 값만을 가져온다.
    또 return값을 count로 비교할 필요없이 리스트로 값을 비교하는 동안 하나라도 일치하면 false를 반환하고 그렇지 않다면 true로 반환해주는 게 어떻겠냐는 피드백을 받아 수정했다.

  • 배운 점

passwordHistoryRepository.findTop3ByUserIdOrderByCreatedAtDesc(
            user.getId());

최신순으로 정렬해 3개의 값만 가져오는 메서드 : 쿼리 메서드가 정말 생각한 것 보다 많은 부분을 해결해준다는 것을 배웠다. sql적으로 처리해야 하는 부분은 쿼리 메서드를 더 많이 활용해봐야겠다.
return형식을 지정해 주는 부분에서 배운 부분이 많다. 아직 완벽하게 필요한 부분만으로 코드를 짜는 게 어렵지만 조금 더 생각해보고 이 부분이 꼭 필요한지에 대한 생각을 해보면 좋을 거 같다.

아직 해결하지 못한 부분

1. 여전히 고민하는 비밀번호 사용 제한

과제의 요구 사항은 잘 지켰지만 계속 고민이 되는 부분이 있었다. 지금 코드는 비밀번호를 수정하면 일단 모두 저장하는데 이렇게 되면 DB에 사용자 별로 비밀번호 수정 이력이 모두 저장되게 된다.
과연 이렇게 비밀번호 데이터가 쌓이는 게 맞는걸까? 하는 의문이 들었다.
오늘 다른 팀에게 코드 리뷰를 받을 수 있는 시간이 있어서 이 부분을 요청했는데 내가 질문을 급하게 작성해 궁금한 부분을 잘 전달하지 못 했다! 월요일에 찾아가 다시 한 번 문의를 남겨봐야겠다.
그리고 그 전에 개인적으로 조금 더 고민해보고 해결 방안을 찾아봐야 할 거 같다.

2. 테스트 코드의 어려움

정말... 너무 어렵다!!!!!! 특히 service 단은 결국 mock객체로 findById를 해오는 부분에서 null값이 들어가는 부분을 해결하지 못해 완성하지 못 하고 제출하게 되었다..
에러의 원인은 알게 되었다. mock객체로 repository는 있다 치고 진행하는 서비스 단 테스트 코드에서 왜 null값이 들어갈까 했는데 오늘 팀원들이 알려준 내용은 @SpringBootTest를 붙이면 @Mock 객체가 지정한 대로 작동하지 않을 수 있다고 한다. 실제로 나는 Mock 객체로 given을 통해 findByid로 이 가짜 객체를 이용하고 싶었고 willReturn을 통해 원하는 값 역시 지정을 해뒀는데 계속 null값이 들어간다는 에러만 났다. 코드 어느 부분을 수정해도..
이번 과제에선 짧은 기간으로 시간이 부족해서 급하게 작성한 부분도 있지만 테스트할 때 사용하는 annotation등을 공부하고 써봐야겠다..


🐾이번 과제 전체 회고

  • 😎 발전한 부분

이전 과제들에선 조금 반복적인 코드를 작성하는 시간이었다. 이번에도 큰 부분은 아니었지만 조금 많이 생각할 부분이 있었고 무조건적인 검색으로 코드를 짜는 게 아닌 생각해보면서 직접 짜본 코드라 조금 애정이 간다ㅎㅎ.. 앞으로 더 어려운 코드를 접하겠지만 이렇게 작게 뿌듯함이 모이고 자신감 붙으면 더 잘 할 수 있지 않을까 기대가 된다..🤭

  • 🔥 아쉬웠던 부분

이번 과제 주차는 팀원들도 너무 잘 만났고 새로운 기술(github action, Code with Me 등)에 대해 접하고 배울 수 있어서 너무 만족스러웠다.
하지만 개인적으로 아쉬운 부분이 있는데 바로 코드 리뷰 시간에 제출할 팀별 코드를 고를 때 였다. 팀 별로 3개의 코드를 제출할 수 있었는데 서로 어떤 코드가 좋을까 고민하고 얘기해보는 시간에 분명히 제출하고 싶었던 부분이 있었음에도 말이 안 나왔다.. 다른 분들이 더 어려운 부분을 했는데 그 부분을 받는 게 좋지 않을까? 하는 생각을 포함해서 이런 저런 생각들때문에..
결론적으로 다른 팀원분이 그 부분 어려워 했던 거 같은데 질문해보는 거 어떠냐 제안해주셔서 제출할 수 있었다. 너무 감사했고 덕분에 잘 제출해 피드백을 받았지만 개인적으로는 이런 모습이 조금 아쉬운 부분으로 남는다.
이전에 아쉽게 느껴서 절대 되풀이하지 않겠다고 다짐해놓고 같은 모습이 나온 거 같아서 마음이 안 좋았다..🥺 말이라도 해보는 게 어떤가 싶다! 다음에 이런 일이 또 있다면 그냥 정말 말이라도 잘 해봐야겠다!

0개의 댓글