내가 처음에 비밀번호 암호화를 추가하기 전에는 잘 됐는데 창고에서 사용자를 불러올때 직렬화에 문제가 생겼었는데 그거는 뭐야?
비밀번호 암호화를 추가하기 전에는 잘 작동하다가, "창고에서 사용자를 불러올 때" (예를 들어, 창고 정보와 함께 담당자 정보를 조회하여 JSON으로 변환하려 할 때) 직렬화 오류가 발생했던 것은 Usermst 엔티티 내의 PasswordHandler 와 관련된 로직 때문일 가능성이 매우 높습니다.
오류 발생 시나리오 추정:
- Usermst 엔티티와 PasswordHandler:
- Usermst.java 엔티티에는 @Transient private PasswordHandler passwordHandler; 필드가 있습니다.
- isPasswordEncoded(), checkPassword() 등 여러 메소드들이 이 passwordHandler가 설정되어 있다고 가정하고 동작합니다.
- 사용자님께서 isPasswordEncoded() 메소드 옆에 // 창고 담당자 불러올때 JSON 직렬화에서 에러발생해서 추가 라고 @JsonIgnore 주석을 달아두셨던 것을 보면, 이 메소드가 문제의 원인이었을 가능성이 큽니다.
- JPA 엔티티 로딩과 @Transient 필드:
- JPA가 데이터베이스에서 Whmst(창고) 엔티티를 로드하고, 이 Whmst 엔티티가 Usermst 타입의 whUser(담당자) 필드를 가지고 있다면, JPA는 Usermst 객체도 함께 로드합니다.
- 이때, JPA는 데이터베이스 컬럼과 매핑된 필드들만 채워줍니다. @Transient로 선언된 passwordHandler 필드는 데이터베이스 컬럼과 매핑되지 않으므로, JPA가 Usermst 객체를 로드할 때 이 필드는 null로 남아있게 됩니다.
- Jackson JSON 직렬화와 Getter 호출:
- 서버가 API 응답을 위해 Whmst 객체 (또는 WhmstDto에 포함된 Usermst 정보)를 JSON으로 변환할 때, Jackson 라이브러리는 해당 객체의 public getter 메소드들을 호출하려고 합니다.
- isPasswordEncoded() 메소드는 이름 규칙상 ("is"로 시작하는 boolean 반환 메소드) Jackson에 의해 getter로 인식되어 호출될 수 있습니다.
- NullPointerException 발생:
- 만약 isPasswordEncoded() 메소드가 호출되었을 때, 해당 Usermst 객체의 passwordHandler 필드가 null이라면 (JPA가 로드한 직후이고, 서비스 등에서 setPasswordHandler를 명시적으로 호출해주지 않았다면), 메소드 내부에서 passwordHandler.isEncoded(...)와 같이 null 객체의 메소드를 호출하려다 NullPointerException이 발생합니다.
- 이 NullPointerException은 Jackson 직렬화 과정 중에 발생하므로, "JSON 직렬화 오류"로 나타나게 됩니다.
사용자님께서 @JsonIgnore를 isPasswordEncoded()에 추가하신 이유: 이것은 Jackson이 해당 메소드를 호출하지 못하도록 막아서 NullPointerException 발생을 회피하기 위한 정확한 임시 조치였습니다.
근본적인 해결 또는 이해:
- UsermstService에서 사용자를 직접 조회하는 메소드들(findAllUsers, getUserByIdx 등)에서는 user.setPasswordHandler(passwordHandler) 코드를 통해 명시적으로 PasswordHandler를 주입해주고 계십니다. 그래서 해당 경로로 사용자를 조회할 때는 문제가 없었을 것입니다.
- 하지만 Whmst와 같이 다른 엔티티를 통해 간접적으로 Usermst가 로드될 때는, WhmstService에서 Usermst 객체의 passwordHandler까지 챙겨서 주입해주지 않으면 null 상태가 됩니다.
- 엔티티 내에 @Transient로 서비스 빈(여기서는 PasswordHandler)을 주입받아 사용하는 패턴은 JPA의 생명주기와 Spring 빈의 생명주기가 다르기 때문에 주의가 필요하며, 일반적으로 권장되지 않는 방식입니다. 엔티티는 데이터의 표현에 집중하고, 비즈니스 로직(비밀번호 암호화/검증 등)은 서비스 계층에서 해당 빈을 직접 사용하여 처리하는 것이 더 일반적입니다.
결론적으로, "창고에서 사용자를 불러올 때 직렬화 문제"는 JPA를 통해 로드된 Usermst 객체의 @Transient PasswordHandler passwordHandler 필드가 초기화되지 않은(null) 상태에서, Jackson이 isPasswordEncoded()와 같이 이 핸들러를 사용하는 메소드를 호출하려다 발생한 NullPointerException일 가능성이 매우 높습니다. @JsonIgnore는 이 특정 메소드의 호출을 막아 당장의 오류를 해결한 것입니다.
그러면 그거를 바꾸고 나서 사용자 추가가 안됐던 이유와 해결 방법은 뭐야?
네, 그 부분을 다시 한번 정리해 드릴게요.
1. 이전 상황: "창고에서 사용자를 불러올 때 직렬화 문제"
- 사용자님께서 비밀번호 암호화 로직(PasswordHandler 및 관련 메소드들)을 Usermst 엔티티에 추가하신 후, 특정 상황(예: 창고 정보와 함께 담당자 정보를 불러와 JSON으로 변환할 때)에서 "직렬화 오류"가 발생했다고 하셨습니다.
- 이 오류의 유력한 원인은, JPA를 통해 로드된 Usermst 객체 인스턴스 내의 @Transient 필드인 passwordHandler가 null인 상태에서, Jackson 라이브러리가 JSON으로 변환하기 위해 isPasswordEncoded()와 같은 passwordHandler를 사용하는 메소드를 호출하려다 NullPointerException이 발생했기 때문입니다.
- 이 문제를 해결하기 위해 isPasswordEncoded() 메소드에 @JsonIgnore를 추가하셨을 수 있습니다. (또는 다른 민감한 정보 관련 메소드나 필드에도 @JsonIgnore를 사용하셨을 수 있습니다.)
2. 새로운 문제: "사용자 추가 시 '비밀번호는 필수입니다' 오류 (400 Bad Request)"
- 이후, 신규 사원을 등록하려고 할 때 "비밀번호는 필수입니다"라는 메시지와 함께 400 오류가 발생했습니다.
- 프론트엔드(hr.js)에서는 console.log를 통해 userPswd 필드에 비밀번호 값이 분명히 담겨 서버로 전송되는 것을 확인했습니다.
- 하지만 백엔드 UsermstController의 createUser 메소드에서 @RequestBody Usermst usermst로 받은 객체의 usermst.getUserPswd() 값을 확인해보니 null이었습니다. (message #65에서 확인)
3. "사용자 추가 안 됐던 이유"
- 가장 직접적인 원인: Usermst.java 엔티티의 userPswd 필드 자체에 @JsonIgnore 어노테이션이 있었기 때문입니다. (message #65에서 이 부분을 수정 제안 드렸습니다.)
- @JsonIgnore 어노테이션은 해당 필드가 JSON으로 변환될 때(직렬화) 뿐만 아니라, JSON으로부터 객체로 변환될 때(역직렬화)에도 해당 필드를 무시하게 만듭니다.
- 따라서 프론트엔드에서 userPswd 값을 JSON에 담아 보냈더라도, 백엔드 컨트롤러에서 @RequestBody Usermst usermst로 객체를 만드는 과정에서 Jackson 라이브러리가 @JsonIgnore가 붙은 userPswd 필드를 무시하고 값을 채우지 않은 것입니다.
- 결과적으로 UsermstService의 addUser 메소드에서는 usermst.getUserPswd()가 null이 되어 "비밀번호는 필수입니다"라는 IllegalArgumentException을 발생시켰고, 컨트롤러는 이를 받아 400 오류로 응답한 것입니다.
4. 해결 방법 (message #65에서 제안 및 적용 후 성공)
- Usermst.java 엔티티의 userPswd 필드에서 @JsonIgnore 어노테이션을 제거하고, 대신 @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) 어노테이션으로 변경했습니다.
- JsonProperty.Access.WRITE_ONLY: 이 설정은 외부에서 오는 JSON 데이터를 객체로 변환할 때(쓰기, 역직렬화)는 userPswd 필드를 사용하지만, 객체를 JSON 데이터로 변환하여 외부로 보낼 때(읽기, 직렬화)는 이 필드를 포함시키지 않도록 합니다.
- 이를 통해 신규 사원 등록 시에는 프론트엔드에서 보낸 비밀번호를 Usermst 객체에 제대로 받을 수 있게 되었고, 동시에 사원 정보를 조회하여 클라이언트로 응답을 보낼 때는 비밀번호가 노출되지 않도록 하는 목적도 달성할 수 있게 되었습니다.
요약하자면, 직렬화 문제를 해결하기 위해 (또는 다른 이유로) userPswd 필드에 적용되었던 @JsonIgnore가 사원 등록 시 비밀번호 값을 받지 못하게 만들었고, 이것이 "비밀번호는 필수입니다" 오류의 원인이었습니다. 이를 @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)로 변경하여 문제를 해결하셨습니다.