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 값을 입력하면
$2aaxYOxKOX5USMsqU.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개의 코드를 제출할 수 있었는데 서로 어떤 코드가 좋을까 고민하고 얘기해보는 시간에 분명히 제출하고 싶었던 부분이 있었음에도 말이 안 나왔다.. 다른 분들이 더 어려운 부분을 했는데 그 부분을 받는 게 좋지 않을까? 하는 생각을 포함해서 이런 저런 생각들때문에..
결론적으로 다른 팀원분이 그 부분 어려워 했던 거 같은데 질문해보는 거 어떠냐 제안해주셔서 제출할 수 있었다. 너무 감사했고 덕분에 잘 제출해 피드백을 받았지만 개인적으로는 이런 모습이 조금 아쉬운 부분으로 남는다.
이전에 아쉽게 느껴서 절대 되풀이하지 않겠다고 다짐해놓고 같은 모습이 나온 거 같아서 마음이 안 좋았다..🥺 말이라도 해보는 게 어떤가 싶다! 다음에 이런 일이 또 있다면 그냥 정말 말이라도 잘 해봐야겠다!