웹 페이지 구축과 컨트롤러 구현

dropKick·2020년 8월 19일
0

웹 계층에서 템플릿 엔진을 사용하는 방식을 기록하는 건 생각을 해봐야겠다.
서버 사이드 렌더링 방식을 아직 사용하기도하지만 대부분 MSA의 확산에 따라 버전 별 API 개발 방법을 사용한다.

thymeleaf 뷰 템플릿과 레이아웃

bootstrap css 미적용 문제

 <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
          integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">

헤더를 통해 다음과 같이 부트스트랩 css를 적용하는데 다운로드 된 부트스트랩 라이브러리 버전과 적혀있는 integrity가 다를 경우 적용되지 않는다
라이브러리 버전의 integrity를 부트스트랩 CDN에서 복붙

도메인 컨트롤러

엔티티 NPE 에러

에러 중 가장 쉬우면서 모르면 가장 어려운 NPE 에러
분명 중간중간 테스트도 다 pass했는데 상큼하게

에러 메소드

Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.

Fri Aug 21 00:12:05 KST 2020
There was an unexpected error (type=Internal Server Error, status=500).
No message available
java.lang.NullPointerException
	at jpabook.jpashop.controller.MemberController.create(MemberController.java:42)

Null Pointer Exception이 42번째 줄 MemberController.create()에서 발생

NPE 발생 42번째 줄 create()

@PostMapping(value = "/members/new") // HTTP POST, 데이터 삽입
    public String create(@Valid MemberForm form, BindingResult result) {

        if (result.hasErrors()) {
            return "members/createMemberForm";
        }

        Address address = new Address(form.getCity(), form.getStreet(), form.getZipcode());

        Member member = new Member();
        member.setName(form.getName());
        member.setAddress(address);

        memberService.join(member); // NPE 

        return "redirect:/";
    }

MemberService의 NPE 발생 메소드

MemberContorller -> MemberService 

 @Transactional // 읽기 전용 아님
    public Long join(Member member) {
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();
    }

join 메소드는 단순히 중복을 체크하고 엔티티를 저장한 뒤 엔티티의 Id를 가져오는데
NPE에러가 뜬다는 건 엔티티를 저장하지 못했거나, Id가 null인 경우를 생각했다.
체크 후 다시 글 작성

해결

NPE가 뜰 수 있는 상황을 일단 생각해봤다.

  • Member 자체가 null인 경우
  • 저장된 엔티티의 Id를 가져오지 못하는 경우
  • 엔티티 자체가 persiste 되지 않은 경우
MemberContorller -> MemberService 

 @Transactional // 읽기 전용 아님
    public Long join(Member member) {
    if (member == null) {
            throw new IllegalStateException("회원 정보가 null입니다.");
        }
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();
    }

member null 체크를 해봤으나 예외가 발생하지 않았다
member가 null이 아닌 이상 정상적으로 persist 된다면 .getId()에서는 문제가 발생할 수가 없어서 .save() 체크

public Long save(Member member) {
        em.persist(member);
        return member.getId();
    }

단순히 persist만 해줘야하는 .save()에 반환값이 있어 그런 줄 알았는데..
안되네 ㅎㅎ?
정말 값이 제대로 들어가는지 Junit 테스트

@Test
    public void 회원가입() throws Exception {
        //given
        Member member = new Member();
        member.setName("kim");
        member.setAddress(new Address("seoul", "11111", "123-45"));

        //when
        Long saveId = memberService.join(member);

        //then
        assertEquals(member, memberRepository.findOne(saveId));
        System.out.println(memberRepository.findOne(saveId).getId());
        System.out.println(memberRepository.findOne(saveId).getName());
        System.out.println(memberRepository.findOne(saveId).getAddress().getCity());
        System.out.println(memberRepository.findOne(saveId).getAddress().getStreet());
        System.out.println(memberRepository.findOne(saveId).getAddress().getZipcode());
    }

1
kim
seoul
11111
123-45
아주.. 잘된다
그렇다면 html Form에서 데이터를 받아오지 못하는걸까?

2020-08-21 23:11:58.484  INFO 51610 --- [nio-8080-exec-1] j.jpashop.controller.HomeController      : home controller
userA
seoul
11111
123-45

잘.. 받아온다..
무슨 문제일까 계속 생각했을 때 문제점들은

  • Form에서 데이터를 받아오지 못한다
  • 엔티티가 persist 되지 못한다
  • 비즈니스 메소드가 null을 반환한다

위 세 가지 였는데 전부 아니라서 검색해보니 가장 NPE가 흔하게 뜨는 일은 DI
그렇다면 객체 주입, 그러니까 생성자 주입에 대한 문제가 아닐까 생각하고 전부 체크

@Controller
@RequiredArgsConstructor
public class MemberController {

    private MemberService memberService;

등잔 밑이 어둡다더니 MemberController가 주입 받아야 할 MemberSerivce는
@RequiredArgsConstructor를 통해 생성자를 주입 받는데 final이 없었고 추가하니 해결

왜 그랬나?

  • @RequiredArgsConsturctor는 Lombok에서 지원하는 어노테이션인데 final을 사용하는 필드의 경우 자동으로 생성자를 생성해준다.
일반 생성자 주입
private final MemberService  memberService;

    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }

lombok 생성자 주입
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;

별도의 수정이 필요없는 필드의 경우 final과 @RequiredArgsConsturctor를 사용하여 코드를 줄이는데 final이 없었으니 당연히 final만 받는 어노테이션은 생성자 주입을 안하고 주입 된 객체가 없으니 null Pointer가 되버린 것이었다.
NPE 에러답게 상황에 대한 경험만 있었다면 참 쉽게 찾을 수 있었을 건데 아쉬움이 크다.

기본적으로 내부 클래스는 외부에서 참조 한 객체가 수정되지 않도록 final 사용
MemberService는 MemberRepsitory를 사용할 수 있게 해주는 위임 클래스에 가깝다

참고 글

디버깅은 틀린 그림 찾기가 아니기 때문에, 대충 되는 그림이 이런데 안되는 소스는 뭐가 문제인지 비교해본다던지, 이런 메시지가 나오면 저걸 의심한다던지 그런식으로 접근하는 건 좋은 방법이 아닙니다.

예외 트레이스만 보면 바로 문제가 되는 위치를 알 수 있는 경우이지만, 본문의 소스의 행번호는 트레이스와 일치하지 않아 소용이 없습니다.

아마도 글을 올리신 분이 아직 트레이스 읽는 법을 익히지 않아서 그 중요성을 인지하지 못하셨을 듯 합니다.

만일 추정이 맞다면, 이건 예외 트레이스를 읽을 줄 알고 NPE의 뜻을 안다면 한눈에 보고 바로 'incidentLogDAO'가 널이라는 것까지 확신하고, 그럼 해당 인스턴스는 언제 누가 초기화 하는가에까지 생각이 미친다면 바로 @Autowire를 확인해야 하는 문제입니다.

개인적으로 안타까운 점이 30분이면 배울 수 있는 예외 트레이스 읽는 법과 NPE의 의미를 학원 등에서 제대로 가르치지 않아서, 많은 입문자 분들이 이런 문제에 맞닥드리면 막연하게 구글링을 하거나 기존 소스와 일일이 비교를 해보거나 하는 식으로 시간 낭비하면서 고생을 한다는 점입니다.

그래서 오지랖이긴 합니다만, 전 이번 기회에 예외 읽는 법은 꼭 찾아서 익혀 두고 논리적인 접근을 통한 문제 해결 방법을 연습하시는 걸 추천드리고 싶습니다.
예외 스택 트레이스 파악하기

0개의 댓글