본문 바로가기

Dot Programming/Spring Clone

[스프링 웹앱 프로젝트 #24] 프로필 수정 처리

24. 프로필 수정 처리

정말로 쉬운 폼 처리
 > 비어있는 값을 허용한다. (기존에 있던 값을 삭제하고 싶을 수도 있기 때문에...)
 > 중복된 값을 고민하지 않아도 된다.
 > 확인할 내용은 입력 값의 길이 정도.

폼 처리
 > 에러가 있는 경우 폼 다시 보여주기
 > 에러가 없는 경우
   >> 저장하고
   >> 프로필 수정 페이지 다시 보여주기 (리다이렉트)
   >> 수정 완료 메시지

리다이렉트 시에 간단한 데이터를 전달하고 싶다면?
 > RedirectAttributes, addFlashAttribute()
 > https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/support/RedirectAttributes.html

 

 

Controller 작성

1. 일단 Controller에서 수정된 데이터 폼(Profile)와 현재 Account계정 정보를 받는 메소드를 작성한다

@Controller
@RequiredArgsConstructor
public class SettingsController {
    private static final String SETTINGS_PROFILE_VIEW_NAME = "settings/profile";
    private static final String SETTINGS_PROFILE_URL = "/settings/profile";

    private final AccountService accountService;


// Errors 혹은 BingResult는 Form을 받는(@ModelAttribute Profile) 오른쪽에 두어야함
    // @ModelAttribute 생략가능
    @PostMapping(SETTINGS_PROFILE_URL)
    public String updateProfile(@CurrentUser Account account, @Valid Profile profile, Errors errors, Model model){
        if(errors.hasErrors()){
            model.addAttribute(account);
            return SETTINGS_PROFILE_VIEW_NAME;
        }

        accountService.updateProfile(account, profile);
        return "redirect:" + SETTINGS_PROFILE_URL;
    }
}

 

2. 데이터 수정은 Service에서 이루어지기 때문에 updateProfile()은 Service에 선언한다

@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class AccountService implements UserDetailsService {

...
    public void updateProfile(Account account, Profile profile) {
        account.setUrl(profile.getUrl());
        account.setOccupation(profile.getOccupation());
        account.setLocation(profile.getLocation());
        account.setBio(profile.getBio());
    }
}

 

 

여기서 크게 두가지 문제가 있다.

첫 번째는, Controller에서 Profile을 바인딩으로 받을 때 NullpointException이 발생한다

  • 왜냐하면 현재 Profile(DTO)에  account를 받는 생성자가 있는데 기본 생성자가 없기 때문이다
  • Spring MVC가 Model Attribute를 받아오려고 할 때 Profile 인스턴스를 만든 다음에 Setter로 사용하여 주입을 하려고 하는데 account가 없기 때문에 인스턴스 생성이 안된다.
  • 그래서 Profile클래스에 기본생성자(NoArgs)를 선언해줘야 한다.
/**
 * setting form을 채울 Data (DTO)
 */
@Data
@NoArgsConstructor  // 기본 생성자 생성
public class Profile {

    private String bio;

    private String url;

    private String occupation;

    private String location;

    public Profile(Account account) {
        this.bio = account.getBio();
        this.url = account.getUrl();
        this.occupation = account.getOccupation();
        this.occupation = account.getOccupation();
        this.location = account.getLocation();
    }
}

 

 

두 번째는, 그런데 이렇게 해도 아직 update가 되지 않는다

왜일까? completeSignUp()도 update하는 메소드인데 이는 작동한다

 

차이점은 completeSignUp()은 이미 들어오기 전부터 Controller에서 repository.find()로 호출된 account을 다루기 때문에 이미 영속성이있는 상태이다 (repositroy가 제공하는 모든 메소드는 기본적으로 다 트랜잭션 처리가 되어있음)

 

updateProfile()에서 사용한 account객체는 http세션 인증 정보(principal)에 들어있던 데이터이다.( 트랜잭션이 끝난 Detached 상태 )

 

바로 이 데이터가 Controller에서 updateProfile()로 넘어가게 되는데, 이 Detached상태의 데이터는 아무리 변경을 하더라도 트랜잭션이 끝난 후에도 DB에 반영이 되지 않는다.

 

그럼 어떻게 싱크를 맞춰야할까? 간단하다

 

repository의 메소드를 불러와 Detached인 상태의 객체를 저장해주면 된다.

  • save()안에서 해당 객체의 Id값이 있는지 없는지 확인하고 있으면 기존 객체와 merge를 시켜 update가 일어난다.
public void updateProfile(Account account, Profile profile) {
        account.setUrl(profile.getUrl());
        account.setOccupation(profile.getOccupation());
        account.setLocation(profile.getLocation());
        account.setBio(profile.getBio());
        accountRepository.save(account); // save : 기존 데이터에 merge를 시킴 -> update발생
    }
! 항상 영속상 상태를 잘 생각하고 코딩해야 한다.

 

마지막으로 update가 되었을 때 확인하는 메세지를 설정해준다.

  • addFlashAttribute() : MVC에서 제공. flash 데이터. redirect되는 곳에 자동으로 들어가고 한번 쓰고 사라짐
  @PostMapping(SETTINGS_PROFILE_URL)
    public String updateProfile(@CurrentUser Account account, @Valid Profile profile, Errors errors,
                                Model model, RedirectAttributes attributes){
        if(errors.hasErrors()){
            model.addAttribute(account);
            return SETTINGS_PROFILE_VIEW_NAME;
        }

        accountService.updateProfile(account, profile);

        // MVC에서 제공. 잠깐 한 번 쓰고 버리는 데이터(flash). Model에 잠깐 들어가고 없어진다
        attributes.addFlashAttribute("message", "프로필을 수정했습니다.");

        return "redirect:" + SETTINGS_PROFILE_URL;
    }

 

결과화면

 

프로필 수정 폼

 

 

프로필 화면 (수정 완료)

 

 

 

프로필 수정 부분 TODO

// TODO: 프로필 이미지
// TODO : 문제가 하나 더 남음 (프로필 이미지 변경할 때 발견)

 


참고

인프런 강의 - 스프링과 JPA 기반 웹 애플리케이션 개발