블로그에 프리프로젝트 회고글을 쓴지 꽤나 많은 시간이 지났다 그동안 메인프로젝트(깃헙링크)가 시작되고 마무리되었다. 그 이후 커리어 세션을 통해서 이력서 작성법, 자기소개서 등에 대한 내용을 배우고 코드스테이츠 백엔드 부트캠프의 6개월 과정을 모두 수료했다. 그동안 프로젝트를 통해서 알게 된 내용들에 대해서 블로그로 너무 정리하고 싶었는데 사실 엄청나게 긴장하며 몰입했던 메인프로젝트가 끝나니 팽팽했던 고무줄을 탁 놓은 것처럼 마음이 풀린 것과
아내가 메인 프로젝트 동안 굉장히 힘들어했다. 직장으로 따지면 밤낮 없는 야근하는 남편을 한달동안 옆에서 지켜본 거니 눈 앞에 있는데 여러모로 단절되어있는 것이 정서적으로나 육아를 홀로 담당하며 누적된 육체적 피로도 굉장히 높았을 것이다. 2주 동안은 가족과의 시간에 집중하고 보내길 원해서 2보 전진을 위한 1보 후퇴 하는 시간을 가졌다.
이제부터 하나씩 아래의 내용들 중에 공유하고 싶은 내용들을 블로그에 작성할 예정이다.
위의 사진을 보면 꽤나 많은 내용들을 프로젝트를 통해 부딪치며 배웠다. 이중에서는 실제로 팀원들에게도 유용하게 도움이 될 것 같아서 잘 정리해서 링크를 공유한 것들도 있다. 간단하게 작성한 것들이 많지만 이중에서 더 공부하거나 가다듬어 블로그로 공유하려고 한다.
첫번째 문제 공유. @RestController , @Validation 어노테이션 충돌
충돌할리가 없는데???
사실 이 두 어노테이션이 충돌할 일이 거의 없다. 이제까지 너무 당연하게 써왔고 Request Body 가 포함된 요청에 대한 검증을 하거나 Query Param으로 오는 요청에 대해 검증하기 위해서 필요한 어노테이션인데 @Validation 어노테이션만 쓰면 의존성 객체들에 대한 Null 익셉션 에러가 발생했다. 이에 대해 문제를 찾고 해결한 과정을 공유하고자 한다.
🔻컨트롤러 코드
@RestController
@Validated
@Slf4j
public class QuestionController {
private final QuestionService questionService;
private final QuestionMapper mapper;
public QuestionController(QuestionService questionService, QuestionMapper mapper) {
this.questionService = questionService;
this.mapper = mapper;
}
//TODO : 멤버에서 포스트를 생성 할 지 여기서 생성할지 의논
@PostMapping("/{member-id}/questions")
private ResponseEntity postQuestion(@PathVariable("member-id") @Positive long memberId,
@RequestBody QuestionDto.post questionPostDto){
Question createdQuestion = questionService.createQuestion(
mapper.questionPostDtoToQuestion(questionPostDto), memberId);
return new ResponseEntity(mapper.questionToQuestionResponseDto(createdQuestion), HttpStatus.CREATED);
}
에러내용 : NullPointException
앱을 실행하고 해당 메소드를 포스트맨을 통해 테스트 했을 때 500 에러가 발생했다. 원인을 찾기 위해서 디버깅을 시도했는데 원인은 컨트롤러에 DI를 통해 주입한 서비스 클래스와 mapper 클래스의 빈이 주입되지 않았고 모두 Null로 떴다.
위의 사진을 보면 mapper와 questionService 모두 null 로 되어있는 모습을 확인할 수 있다. 빈주입이 되지 않는다는 사실을 확인했다. 그렇다면 이제 어떻게 할 것인가??
문제해결 - 테스트 : RestController 어노테이션을 제외한 나머지 어노테이션 삭제
@RestController
// @Validated 삭제
// @Slf4j 삭제
public class QuestionController {
private final QuestionService questionService;
private final QuestionMapper mapper;
public QuestionController(QuestionService questionService, QuestionMapper mapper) {
this.questionService = questionService;
this.mapper = mapper;
}
//TODO : 멤버에서 포스트를 생성 할 지 여기서 생성할지 의논
@PostMapping("/{member-id}/questions")
private ResponseEntity postQuestion(@PathVariable("member-id") @Positive long memberId,
@RequestBody QuestionDto.post questionPostDto){
Question createdQuestion = questionService.createQuestion(
mapper.questionPostDtoToQuestion(questionPostDto), memberId);
return new ResponseEntity(mapper.questionToQuestionResponseDto(createdQuestion), HttpStatus.CREATED);
}
@RestController 어노테이션만 남겼을시 정상적으로 작동했다. 그 이후 다른 어노테이션을 추가하면서 @Validated 어노테이션을 사용했을 때 해당 문제가 일어나는 것을 발견했다.
문제 이유 조사 및 해결
해당 부분에 대해 stackoverflow 라던지 구글 검색등을 해보았지만 대부분 Validated를 쓰지말고 그럼 Valid를 써보아라 라는 코멘트들이 많았다. 이유나 어떤 것이 문제인지에 대해서 알려주는 것이 크게 없었고 초반에 방향성을 의존하는 라이브러리 충돌로 보고 build.gradle 에 대한 내용을 여러가지로 바꿔보았다.
시도한 방법
- build.gradle dependency를 기존 파일과 동일하게 바꿔봄 → 실패 ❌
- Invalidate Caches 를 통해서 캐시 초기화 하고 다시 의존성 설치 → 실패 ❌
@Valid와 @Validated 의 차이점에 대해 기술한 블로그
https://medium.com/javarevisited/are-you-using-valid-and-validated-annotations-wrong-b4a35ac1bca4
위의 링크를 통해서 많은 사람들이 @Valid로 사용하라고해서 둘의 차이가 무엇인지 궁금해서 유효성 검사에 대한 여러 글들을 보았다 그중에 어느 때에 @Valid와 @Validated 를 써야하는가에 대한 글도 보게 되면서 새로운 부분에 대해서 더욱 자세히 알게되었다.
하지만 중요한 것은 아직 문제 해결을 하지 못했다는 것!
정확한 문제 원인 및 문제 해결 방법
문제해결에 도움된 레퍼런스 : https://stackoverflow.com/questions/70143003/null-exception-service-in-a-spring-restcontroller-using-constructor-injection
진정한 원인은 컨트롤러 클래스에서 메소드를 public으로 지정한 것이 아닌 private 으로 지정한 것이었다.
@RestController
@Validated
@Slf4j
public class QuestionController {
private final QuestionService questionService;
private final QuestionMapper mapper;
public QuestionController(QuestionService questionService, QuestionMapper mapper) {
this.questionService = questionService;
this.mapper = mapper;
}
@PostMapping("/{member-id}/questions")
// 🔽 바로 요부분 요부분이 문제였다!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
private ResponseEntity postQuestion(@PathVariable("member-id") @Positive long memberId,
@RequestBody QuestionDto.post questionPostDto){
Question createdQuestion = questionService.createQuestion(
mapper.questionPostDtoToQuestion(questionPostDto), memberId);
return new ResponseEntity(mapper.questionToQuestionResponseDto(createdQuestion), HttpStatus.CREATED);
}
해당 부분을 다시 public 으로 바꾸고 나니 Validated 도 정상 작동하고 컨트롤러도 정상 작동하였다. 하지만 왜 Validated가 없을 때는 private 상황에서도 정상 작동을 했는데 @Validated 를 사용하면 작동이 안되지는 여전히 의문이다. 그래서 해당 내용을 chatGPT 한테 물어봤다. 요약해보면 아래와 같다.
@Validated 어노테이션은 스프링의 AOP 기능을 사용하여 동작하는데 AOP는 프록시 객체를 생성해서 메소드 호출을 가로채는 방식으로 동작합니다. 이를 위해서는 프록시 객체가 메소드에 접근 할 수 있어야합니다. 하지만 private 메소드는 프록시 객체에서 접근할 수 없으므로 @Validated 어노테이션이 제대로 동작하지 않게 됩니다. 그래서 그로 인해 QuestionMapper 와 QuestionService Bean 들이 제대로 바인드 되지 않을 가능성이 있다.
결론
알고보면 간단한 문제였다. 하지만 오히려 메소드를 private으로 설정하는 것을 통해서 일어나는 에러들을 만나면서 빈이 등록되는 원리들 그리고 사용되는 원리들에 대해서 마지막으로 접근 제어자에 대한 부분을 조금 더 깊이 찾아봐야겠다는 생각이 들었다. 결국 요약하자면 프록시 객체가 메소드에 접근해야 제대로 Mapper와 Service클래스가 바인드가 되는데 private 메소드는 해당 클래스 내에서 접근이 가능하기 때문에 빈이 등록되지 못한 것이었다.
'개발일지' 카테고리의 다른 글
Java Map 깊이 이해하기: HashMap vs TreeMap vs ConcurrentHashMap (0) | 2024.11.28 |
---|---|
Java Set: HashSet, TreeSet, LinkedHashSet 완벽 가이드 (0) | 2024.11.28 |
[pre-project 회고] 경험치를 올려 레벨업을 했다 feat.코드스테이츠 (0) | 2023.06.30 |
[프리프로젝트] 프론트, 백엔드 서버 배포 관련 조사 (0) | 2023.06.29 |
[솔로 프로젝트] TodoList Test 작성 및 API 문서화 하기 2탄 / API문서화편 (0) | 2023.06.10 |