회원가입, 내 정보 조회, 비밀번호 변경하기 기능을 개발하면서
클로드에게 질문하고, 혼자 생각했던 내용을 정리한 기록
예전에 언젠가 봤던 테스트 코드 강의에서 TDD는 red-green-refactor 패턴으로 하는게 정석(?)이라는 말을 들어본 기억이 났다.
그래, 그럼 빨간불을 볼 수 있는 테스트 코드 먼저 짜자!
회원가입 시 비밀번호를 암호화해서 저장하는 기능을 기준으로, 테스트 코드의 껍데기부터 만들어봤다.
@Test
@DisplayName("회원가입시 비밀번호를 암호화해서 저장한다.")
void signUp_encryptsPassword() {
// arrange
// act
// assert
}
첫 번째 의문. 이 상태도 red인가? 위에 상태는 아무리 그래도 좀 아닌 것 같았다.
@Test
@DisplayName("회원가입시 비밀번호를 암호화해서 저장한다.")
void signUp_encryptsPassword() {
// arrange
SignUpCommand command = new SignUpCommand(
...생략
);
// act
signUpService.signUp(command);
// assert
User savedUser = userRepository.findByLoginId("testUser123").orElse(null);
assertThat(savedUser).isNotNull();
assertThat(savedUser.getPassword()).isNotEqualTo("ValidPass1!");
}
두 번째 의문. 구현체들이 거의 없는 상태인데, 이걸 red라고 부를 수 있을까?
정리해보니, 내가 이해한 TDD에서의 red는
모든 클래스가 비어 있는 상태를 의미하는 게 아니라,
외부에서 호출하는 구조는 갖춰져 있지만,
테스트가 기대하는 내부 구현이 아직 존재하지 않아 실패하는 상태에 더 가까웠다.
즉, SignUpCommand나 signUpService 같은 진입점은 존재하되,
실제로 비밀번호를 암호화하는 로직은 아직 구현되지 않은 상태.
그 상태에서 테스트가 실패하는 것이 내가 받아들인 red였다.
나만 몰랐던 내용일지도...
클로드에게 PasswordEncoder 사용해서 비밀번호 변경하는 기능을 구현하라고 시켰더니 아래처럼 코드를 짜주었는데,
if (passwordEncoder.matches(command.newPassword(), user.getPassword())) {
throw new CoreException(ErrorType.BAD_REQUEST, "현재 비밀번호와 동일한 비밀번호로 변경할 수 없습니다.");
}
user에서 가져온 비밀번호는 앞서 구현한 정책에 따르면 이미 암호화된 비밀번호일텐데,
raw 데이터를 encode해서 비교하도록 넘겨야 하는거 아닌가 하는 생각이 들었다. 바로 질문!

내가 더이상 토달지 않을 거란걸 알고, 클로드가 '아 그렇구나'까지 미리 입력해놓은 것이 웃겼다.
비밀번호를 최초 생성/변경할 때는 세가지 정도의 요구사항이 있었다.
사실 처음에는 회원가입 먼저 구현하면서 SignUpValidator를 생성했었고, private 메서드로 validatePassowrd()를 추가했었다. 그리고 그 메서드 내부에서 위 1,2번 사항을 검증한다.
그런데 비밀번호를 변경할 때도 비밀번호 검증이 필요하므로 별도의 클래스 분리는 불가피한 상황이 되었다.
이래서 시간이 걸리더라도 차분히 시간을 갖고 설계를 먼저하는 것이 좋은 것 같다고 느꼈다...
우선은 PasswordPolicyValidator의 validate()로 1,2번을 검증하는 로직을 이동시켰다.
그리고 3번 사항은 서비스 레이어에서 디비 조회를 통해 검증하도록 했다.
이제 끝?인가 하고 코드를 가만히 바라보니, 왠지 모르게 의존성 주입없이 PasswordPolicyValidator.validate() 이렇게 호출하고 싶은 충동(?)이 생겨났다.
나의 바보같은 질문도 한심해하지 않고 바라봐줄 클로드에게 질문을 던졌다.
어떤.. 성능.. 이런 개발자스러운 질문이 아니라 예쁠 것 같아서라니. 나 괜찮겠지?!

사실은 질문도 왠지모르게 했듯, 답변도 왠지 모르게 바꾸지 않는게 좋다고 할 줄 알았다.
그런데 static 변경에 대해 반대의견으로 말해준 내용들이 딱히 바로 납득이 되진 않았다.
비밀번호 정책이 추가 혹은 변경된다면 그에 따라 검증 내용이 바뀌는 것인데, static으로 선언하는 것에 대한 답이 맞나? DB 조회가 필요한 검증은 지금도 해당 validator가 아닌 서비스 레이어에서 하고 있는데?
흠. 그래서 추가 질문.

"응 바꿔줘" 이번에도 내가 이쯤되면 바꾸라고 하겠다는걸 눈치챈 클로드이다.
변경 후 테스트를 다시 한 번 돌려서 확인했다. 성공!
하지만 찜찜함이 남았다.
그동안 나는 static 메서드는 클래스 로딩 시점에 메모리에 올라가고,
한 번 올라가면 내려가지 않는다는 이유로
막연히 “남용하면 메모리 낭비가 된다”는 인식을 가지고 있었다.
그래서 재사용성이 매우 높은 경우에만
static을 사용하는 것이 맞다고 생각해왔다.
이런 전제가 있었기 때문에,
“의존성 주입 없이 바로 호출할 수 있어서 코드가 예뻐 보인다”는 이유로 한
static 변경이 설계적으로 어떤 의미를 가지는지 확신할 수 없었다. 그래서 솔직하게 질문을 다시 던졌다.

중요했던 건 메모리 관점 자체보다도,
이 메서드가 어떤 성격을 가지는지 그 의도를 드러내는 일이었다.
실제로 JVM 환경에서 요청마다 객체를 생성하는 비용은 크게 문제가 되지 않는다고 한다.
PasswordPolicyValidator의 validate 메서드는
즉, 객체로 존재해야 할 이유가 없었다.
‘코드가 예뻐 보여서’라는 감각도 완전히 틀린 건 아니었지만,
그런 선택이 설계로서 의미를 가지려면 왜 그런 선택을 했는지 설명할 수 있어야 한다는 걸 배웠다.
아직 판단 기준이 완벽하다고 생각하지는 않는다.
그래도 이렇게 하나씩 이유를 찾아가다 보면
조금씩 더 나은 선택을 할 수 있지 않을까 싶다.
기죽지 말고, 정진하자!
화이팅입니다.