회사에서 프로젝트를 진행하던 중 계속 DTO에서 NPE가 발생한다고 로그에 기록되었습니다. 아래 코드는 메시지를 발송할 때 필요한 메서드입니다. 하지만 메시지를 발송하지 않는 경우에도 자꾸 이 메서드가 호출되어서 오류가 발생했습니다. 디버깅 결과 @ResponseBody와 연관이 있다는 것을 발견하고 모르던 부분이 있어서 글로 정리하게 되었습니다.
public String getAgencyInfo() {
try {
return this.agencyResDto.getAgencyName() + " / " + this.agencyResDto.getRepresentTel();
} catch (Exception e) {
log.warn("[LectureResDto.getAgencyInfo()] ERROR :: ", e.getMessage());
return "";
}
}
@ResponseBody
@ResponseBody 어노테이션은 자바 객체를 HttpResponse의 본문 ResponseBody 내용으로 매핑하는 역할을 합니다. 주로 Jackson 라이브러리를 이용합니다.
SpringBoot에서는 spring-boot-starter-web 의존성을 설치하면 자동으로 Jackson 라이브러리가 포함되지만, 그렇지 않거나 레거시 스프링을 이용하는 경우에는 직접 maven 혹은 gradle을 통해 의존성 설치가 필요합니다.
@ResponseBody의 대략적인 실행 원리
아래와 같이 /rest/user로 Get 메서드로 호출하는 간단한 예제가 있습니다. 디버그를 이용해 어떤 과정을 거쳐 User 객체를 반환하는지 살펴보겠습니다.
@Controller
@RequestMapping("/rest")
public class RestController {
@GetMapping("/user")
@ResponseBody
public User getUser() {
return new User("1", "potatowoong");
}
}
@AllArgsConstructor
public class User {
private String id;
private String name;
public String getId() {
System.out.println("getId() called");
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
System.out.println("getName() called");
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
'}';
}
}
1. DispatcherServlet의 doService()
특정 컨트롤러로 요청을 보내면 DispatcherServlet의 doService() 메서드가 호출된 후 여러 과정을 거쳐 this.doDispatch() 메서드를 실행합니다. 그 후, 또 여러 과정을 거쳐 컨트롤러의 역할을 수행합니다.
2. InvocableHandlerMethod의 doInvoke()
3. BeanPropertyWriter의 serializeAsField()
위 사진을 보면 this._accessorMethod.invoke(bean, (Object[]) null); 부분이 있습니다. 자바에는 메서드를 실행할 수 있는 Reflection의 Invoke라는 기능이 있습니다. Reflection에 의해 User 객체의 getId() 메소드가 호출된 것을 볼 수 있습니다.
이러한 실행 과정때문에 제가 의도하지 않은 DTO의 메서드가 호출되어 NPE가 발생했던 것이었습니다.
@ResponseBody 사용 시 주의할 점
public class User {
private String id;
private String name;
private Book book;
public String getBookName() {
return book.getName();
}
}
위 코드와 같이 책의 이름을 가져와 사용하려고 메서드를 추가했습니다. (물론 User가 아닌 Book에 저 Getter 메서드가 있어야 하지만 예시를 위한 것이므로 이해해 주세요)
하지만 다른 곳에서는 Book이 담겨 있지 않은 User의 정보만 결과값으로 반환하려 합니다. 그 결과 Null 값이 있다며 JSON으로 변환하지 못하고 있습니다.
결과를 반환하는 DTO(ResponseDto)에는 가급적 정말 필요한 값만 들어 있어야 하며, NPE가 발생할 수 있는 코드는 지양하는 편이 좋을 것 같습니다. 그리고 계층적 구조를 가지게 되면 저런 메서드는 각 계층을 지키는 것이 좋을 것 같습니다. 저는 그나마 try~catch를 사용해 뒀으니 조금이라도 안전했지 아무 조치도 안 해뒀으면 오류 찾느라 몇 시간은 날렸을 것 같습니다...
'Programming > SpringBoot' 카테고리의 다른 글
[SpringBoot] JPA 동적 스키마 (1) | 2024.11.06 |
---|---|
[SpringBoot] @ModelAttribute 작동 원리 (0) | 2024.07.17 |
[SpringBoot] Entity의 ID를 테스트에서 삽입하는 방법 (0) | 2024.06.02 |
[SpringBoot] Model VS ModelMap (0) | 2023.12.26 |
[SpringBoot] JSP에서의 예외 처리 (0) | 2023.12.19 |