스프링과 JPA 기반 웹 애플리케이션 개발 #29 프로필 수정 처리

Jake Seo·2021년 6월 2일
0

스프링과 JPA 기반 웹 애플리케이션 개발 #29 프로필 수정 처리

해당 내용은 인프런, 스프링과 JPA 기반 웹 애플리케이션 개발의 강의 내용을 바탕으로 작성된 내용입니다.

강의를 학습하며 요약한 내용을 출처를 표기하고 블로깅 또는 문서로 공개하는 것을 허용합니다 라는 원칙 하에 요약 내용을 공개합니다. 출처는 위에 언급되어있듯, 인프런, 스프링과 JPA 기반 웹 애플리케이션 개발입니다.

제가 학습한 소스코드는 https://github.com/n00nietzsche/jakestudy_webapp 에 지속적으로 업로드 됩니다. 매 커밋 메세지에 강의의 어디 부분까지 진행됐는지 기록해놓겠습니다.


프로필 수정 처리

  • 인풋 박스에서 해줘야할 것
    • 비어있는 값을 허용
      • 기존에 있던 값을 삭제하고 싶을 수도 있기 때문
    • 중복된 값을 고민하지 않아도 된다.
    • 확인할 내용은 입력 값의 길이 정도이다.
  • 폼 처리
    • 에러가 있는 경우 폼 다시 보여주기
    • 에러가 없는 경우
      • 저장
      • 프로필 수정 페이지 다시 보여주기
      • 수정 완료 메세지 보여주기
  • 리다이렉트 시에 간단한 데이터를 전달하고 싶다면?

SettingsController

updateProfile 메소드 작성

@PostMapping("/settings/profile")
    // Profile 클래스에 생성자 중 빈 생성자가 없으면,
    // NullPointerException 이 날 위험이 있다.
    // 왜냐하면 스프링 컨트롤러는 파라미터로 받은 Profile 타입의 아규먼트를 먼저 new로 생성한 뒤에 setter로 넣으려고 하기 때문
    public String updateProfile(@LoginAccount Account loginAccount,
                                // @ModelAttribute 애노테이션은 생략 가능
                                @Valid @ModelAttribute Profile profile,
                                Model model,
                                Errors errors,
                                RedirectAttributes redirectAttributes) {
        if(errors.hasErrors()) {
            model.addAttribute(loginAccount);
            return "settings/profile";
        }

        accountService.updateProfile(loginAccount, profile);
        // 한번 쓰고 사라지는 데이터라 FlashAttribute 이다.
        redirectAttributes.addFlashAttribute("message", "프로필을 수정했습니다.");

        /** 강의 내용대로하면 여기서 업데이트가 안되지만 나는 수정해놔서 잘 된다.
        *  - Spring JPA Data 에서 기본으로 제공하는 메소드엔 @Transactional 처리가 잘 되어있다.
        *    - 하지만, 우리가 만든 Repository 에는 항상 @Transactional 을 직접 붙여주어야 한다.
        *  - detached(준영속) 상태의 JPA 객체는 `repository.save()` 메소드로  merge 를 해줄 수 있다.
         *   - 하지만, 나는 이러한 패턴 자체를 좋아하지 않는다.
         *   - 값이 다른 곳에서 업데이트 되었더라도, detached 된 객체의 정보는 변하지 않는다.
         *     - 이걸 계속 인지하고 있으면 다시 `repository.findXxx()`등으로 불러와서 업데이트가 가능하지만
         *       시스템에서 실수할 수 있는 부분은 최대한 없애면서 개발해야 된다고 생각하기 때문에 나는 이렇게 하지 않았다.
        *  */

        return "redirect:/settings/profile";
    }

주석때문에 내용이 많아보이는데 딱히 별다른 내용은 없다. Account 객체의 정보와 Profile 객체의 정보를 받아서 Profile 객체와 관련된 해당 Account 객체의 내용을 바꿔주는 .updateProfile()만 작성하면 된다.

여기서 하나 큰 이슈가 생기는데, 강의를 들으면서 이전에 해결했던 SecurityContextprincipal 내부에 detached 상태인 JPA 객체가 들어있기 때문에 변경 내용이 디비에 적용이 안되는 문제가 있다.

이 문제를 해결하기 위해서, 나는 매번 UserAccount에서 account를 불러올 때, 리포지토리에서 영속(persistent) 상태인 JPA 객체를 불러오는 방향으로 수정했다.

그런데 강의 내용에서는 그냥 준영속 상태인 객체를 갖고 있다가 .save() 메소드로 덮어씌우는 방향으로 진행했다. 나는 이런 방식은 나중에 업데이트된 데이터를 조회하거나 할 때, 매번 신경써주거나 실수하기 쉽다고 생각하여 매번 persistent 상태인 JPA 객체를 불러오는 방식으로 진행했다.

RedirectAttributes.addFlashAttribute()

redirectAttributes.addFlashAttribute() 메소드는 리다이렉트 시에 모델에 넘길 값을 잠시 메모리에 저장해두는 역할이다.

새로고침 후에는 모델에서 사라지는 값이다.

AccountService

updateProfile 메소드 작성

    public void updateProfile(Account account, Profile profile) {
        account.setUrl(profile.getUrl());
        account.setBio(profile.getBio());
        account.setLocation(profile.getLocation());
        account.setOccupation(profile.getOccupation());
        // TODO: 프로필 이미지
        // TODO: 중요한 문제가 하나 더 남았다.
    }

Profile 클래스 내용 수정

@Data
@NoArgsConstructor
public class Profile {
    private String bio;
    private String url;
    ...

Profile 클래스의 내용에 수정된 부분은 @NoArgsConstructor인데, 이 애노테이션을 추가해준 이유는 컨트롤러단에서 @ModelAttribute로 사용자 정의 클래스를 사용할 때, 스프링은 내부적으로 해당 클래스를 파라미터가 없는 기본 생성자로 생성하고, 내부에 정의된 setter를 통해 값을 넣는다.

그런데 이 때, 기본 생성자가 없으면 스프링은 아직 파라미터들을 얻기 전이기 때문에 데이터를 넣어줄 클래스를 제대로 생성할 수 없다. 그래서 에러가 나게 된다.

There was an unexpected error (type=Internal Server Error, status=500).
Failed to instantiate [com.jakestudy.settings.Profile]: Constructor threw exception; nested exception is java.lang.NullPointerException: Cannot invoke "com.jakestudy.domain.Account.getBio()" because "account" is null
org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.jakestudy.settings.Profile]: Constructor threw exception; nested exception is java.lang.NullPointerException: Cannot invoke "com.jakestudy.domain.Account.getBio()" because "account" is null
	at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:225)
	at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.constructAttribute(ModelAttributeMethodProcessor.java:329)

위와 같은 에러가 나는데, 제대로 안 읽으면 에러의 내용만으로 에러의 원인을 알기가 조금 힘들 수 있다.

Failed to instantiate [com.jakestudy.settings.Profile]: Constructor threw exception;...
org.springframework.beans.BeanInstantiationException...

기본 생성자가 없는 클래스로 @ModelAttribute 기능을 이용하려 하면 위와 같은 에러 메세지가 나오는 것을 기억하자.

Profile.html 에 내용 추가

            <div th:if="${message}" class="alert alert-info alert-dismissible fade show mt-3" role="alert">
                <span th:text="${message}">메시지</span>
                <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                    <span aria-hidden="true">x</span>
                </button>
            </div>

프로필 수정 시에 제대로 수정되었다고 알리는 메세지를 추가했다.

profile
풀스택 웹개발자로 일하고 있는 Jake Seo입니다. 주로 Jake Seo라는 닉네임을 많이 씁니다. 프론트엔드: Javascript, React 백엔드: Spring Framework에 관심이 있습니다.

0개의 댓글