프로젝트 정리/스프링과 JPA 기반 웹 애플리케이션 개발

7. 회원 가입 : 리팩토링 및 테스트

출처 : www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-JPA-%EC%9B%B9%EC%95%B1/dashboard

 

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

이 강좌에서 여러분은 실제로 운영 중인 서비스를 스프링, JPA 그리고 타임리프를 비롯한 여러 자바 기반의 여러 오픈 소스 기술을 사용하여 웹 애플리케이션을 개발하는 과정을 학습할 수 있습

www.inflearn.com

 

리팩토링 하기전에 테스트 코드를 먼저 작성하자.

  • 그래야 코드를 변경한 이후에 불안하지 않다.
  • 변경한 코드가 무언가를 깨트리지 않았다는 것을 확인할 수 있다.

테스트 할 것

  • 폼에 이상한 값이 들어간 경우에 다시 폼이 보여지는가?
  • 폼에 값이 정상적인 경우
    • 가입한 회원데이터나 존재하는가?
    • 이메일이 보내지는가?

리팩토링

  • 메소드가 너무 길지 않은가?
  • 코드를 읽기 쉬운가?
    • 내가 작성한 코드를 내가 읽기 어렵다면 남들에겐 훨씬 더 어렵다.
  • 코드가 적절한 위치에 있는가?
    • 객체들 사이의 의존관계
    • 책임이 너무 많지 않은지

실습

 

테스트 부터

코드를 변경한 이후에 변경한 코드가 아무것도 깨트리지 않음을 증명

 

이상  입력값 테스트 -  폼에 이상한 값이 들어간 경우에 다시 폼이 보여지는가?

@DisplayName("회원 가입 처리 -  입력값 오류")
    @Test
    void singUpSubmit_wrong_input() throws Exception {
        mockMvc.perform(post("/sign-up")
                .param("nickname", "jungi")
                .param("email", "email..")
                .param("password", "12345"))
                .andExpect(status().isOk())
                .andExpect(view().name("account/sign-up"));
    }
    
    mockMvc.perform으로 post방식으로 sign-up에 파라미터들을 담아서보내는데
    기댓값이 200 그리고 보여지는 뷰가 다시 가입 페이지로 보여지는 것
    
    

 

 

 

 

그리고 이런 에러가 나오게 되었다.

200을 기대했지만 403이 나와버림

상태 200을 기대했지만 403이 나왔다.

403에러는 권한이 부족하여 나오는 에러이다.

이유는 CSRF(Cross-site request forgery)설정이 켜져있어서 이다.

타 사이트에서 공격하는 사이트 대상으로 form-data를 보내는 것
은행 계좌 이체 데이터를 타 사이트에서 은행 사이트에 보내서 유출을 시도하는것
이를 방지하기 위해 CSRF토큰을 사용한다.
ThymeLeaf Template - Spring security - Spring Mvc
이 세 가지가 조합되어서 CSRF 기능을 지원해준다.

 

csrf 토큰 값이 같이 서버 쪽으로 전송이 된다.

이를 이용해서 내가 만들어준 폼에서 온 데이터임을 검증하고 사용한다.

csrf토큰 없이 폼 만 오거나 csrf 토큰값이 다르게 온다면 권한이 없기 때문에 403에러가 나오게 된다.

 

@DisplayName("회원 가입 처리 -  입력값 오류")
    @Test
    void singUpSubmit_wrong_input() throws Exception {
        mockMvc.perform(post("/sign-up")
                .param("nickname", "jungi")
                .param("email", "email..")
                .param("password", "12345")
                .with(csrf())) <-추가
                .andExpect(status().isOk())
                .andExpect(view().name("account/sign-up"));
    }

 

폼 데이터 전송하는 테스트에서는 .with(csrf())를 추가해주자.

// public static SecurityMockMvcRequestPostProcessors.CsrfRequestPostProcessor csrf()

csrf토큰을 포함해주어서 검사해야 안정적인 검사가 가능하다.

 

입력값 정상 테스트 - 가입한 회원데이터나 존재하는가?

@DisplayName("회원 가입 처리 -  입력값 정상")
    @Test
    void singUpSubmit_correct_input() throws Exception {
        mockMvc.perform(post("/sign-up")
                .param("nickname", "jungi")
                .param("email", "jungi@email.com")
                .param("password", "12345678")
                .with(csrf()))
                .andExpect(status().is3xxRedirection())
                .andExpect(view().name("redirect:/"));

        assertTrue(accountRepository.existsByEmail("jungi@email.com"));
    }

입력값 정상 테스트 - 메일이 보내지는가?

 

@DisplayName("회원 가입 처리 -  입력값 정상")
    @Test
    void singUpSubmit_correct_input() throws Exception {
        mockMvc.perform(post("/sign-up")
                .param("nickname", "jungi")
                .param("email", "jungi@email.com")
                .param("password", "12345678")
                .with(csrf()))
                .andExpect(status().is3xxRedirection())
                .andExpect(view().name("redirect:/"));

        assertTrue(accountRepository.existsByEmail("jungi@email.com"));
        then(javaMailSender).should().send(any(SimpleMailMessage.class));
    }

그리고 나서 javaMailSender에서 어떤 SimpleMailMessage의 클래스 타입의 클래스라도 무조건 보내라.

then - BDDMokito

mokito any<T>

 

 

메일을 보내지 않았을 경우 테스트 했을 때 나오는 에러

메일을 보낼때 인터페이스만 관리하고 실제로 보내는 것은 외부 서비스

메일은 smtp와 연결을 해서 gmail로 이메일 발송할 예정

이 과정을 테스트로 작성하기엔 당장엔 너무 많기 떄문에 외부 서비스로 맛만 보는것

 

 

리팩토링

 

@PostMapping("/sign-up")
    public String signUpSubmit(@Valid SignUpForm signUpForm, Errors errors) {
        if(errors.hasErrors()){
            return "account/sign-up";
        }

        Account account = Account.builder()
                .email(signUpForm.getEmail())
                .nickname(signUpForm.getNickname())
                .password(signUpForm.getPassword()) 
                .studyCreateByWeb(true)
                .studyEnrollmentResultByWeb(true)
                .studyUpdateByWeb(true)
                .build();
        Account newAccount = accountRepository.save(account);


        newAccount.generateEmailCheckToken(); //UUID
        SimpleMailMessage mailMessage = new SimpleMailMessage();
        mailMessage.setTo(newAccount.getEmail());
        mailMessage.setSubject("스터디올래, 회원 가입 인증");
        mailMessage.setText("/check-email-token?token=" +
                newAccount.getEmailCheckToken() + "&email=" + newAccount.getEmail());

        javaMailSender.send(mailMessage);
        return "redirect:/";

    }

-> 메서드로 기능 별 분리

@PostMapping("/sign-up")
    public String signUpSubmit(@Valid SignUpForm signUpForm, Errors errors) {
        if(errors.hasErrors()){
            return "account/sign-up";
        }

        Account newAccount = saveNewAccount(signUpForm);
        newAccount.generateEmailCheckToken(); //UUID
        sendSignUpConfirmEmail(newAccount);

        return "redirect:/";

    }

    private Account saveNewAccount(@Valid SignUpForm signUpForm) {
        Account account = Account.builder()
                .email(signUpForm.getEmail())
                .nickname(signUpForm.getNickname())
                .password(signUpForm.getPassword())
                .studyCreateByWeb(true)
                .studyEnrollmentResultByWeb(true)
                .studyUpdateByWeb(true)
                .build();
        return accountRepository.save(account);
    }

    private void sendSignUpConfirmEmail(Account newAccount) {
        SimpleMailMessage mailMessage = new SimpleMailMessage();
        mailMessage.setTo(newAccount.getEmail());
        mailMessage.setSubject("스터디올래, 회원 가입 인증");
        mailMessage.setText("/check-email-token?token=" +
                newAccount.getEmailCheckToken() + "&email=" + newAccount.getEmail());
        javaMailSender.send(mailMessage);
    }

-> Controller에 맞지 않는 책임 이나 과한 역할

-> 역할 분리 AccountService로 이전

package com.studyolle.account;

@Controller
@RequiredArgsConstructor
public class AccountController {

    private final SignUpFormValidator signUpFormValidator;
    private final AccountService accountService;

    @InitBinder("signUpForm")
    public void initBinder(WebDataBinder webDataBinder) {
        webDataBinder.addValidators(signUpFormValidator);
    }

    @GetMapping("/sign-up")
    public String signUpForm(Model model) {
        model.addAttribute(new SignUpForm()); 
        return "account/sign-up";
    }

    @PostMapping("/sign-up")
    public String signUpSubmit(@Valid SignUpForm signUpForm, Errors errors) {
        if(errors.hasErrors()){
            return "account/sign-up";
        }

        accountService.processNewAccount(signUpForm);
        return "redirect:/";
    }


}

 

package com.studyolle.account;

import com.studyolle.domain.Account;
import lombok.RequiredArgsConstructor;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;

import javax.validation.Valid;

@Service
@RequiredArgsConstructor
public class AccountService {

    private final AccountRepository accountRepository;
    private final JavaMailSender javaMailSender;

    public void processNewAccount(SignUpForm signUpForm) {
        Account newAccount = saveNewAccount(signUpForm);
        newAccount.generateEmailCheckToken(); //UUID
        sendSignUpConfirmEmail(newAccount);
    }

    private Account saveNewAccount(@Valid SignUpForm signUpForm) {
        Account account = Account.builder()
                .email(signUpForm.getEmail())
                .nickname(signUpForm.getNickname())
                .password(signUpForm.getPassword()) // TODO encoding 해야 함 (hash로 바꿔서)
                .studyCreateByWeb(true)
                .studyEnrollmentResultByWeb(true)
                .studyUpdateByWeb(true)
                .build();
        return accountRepository.save(account);
    }

    private void sendSignUpConfirmEmail(Account newAccount) {
        SimpleMailMessage mailMessage = new SimpleMailMessage();
        //Subject -> 제목 설정 | setText -> 본문설정
        mailMessage.setTo(newAccount.getEmail());
        mailMessage.setSubject("스터디올래, 회원 가입 인증");
        mailMessage.setText("/check-email-token?token=" +
                newAccount.getEmailCheckToken() + "&email=" + newAccount.getEmail());
        javaMailSender.send(mailMessage);
    }


}