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

Yuri Lee·2020년 11월 6일
0

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

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

테스트 할 것

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

리팩토링

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

리팩토링이란 무엇인가?

리팩토링은 외부동작을 바꾸지 않으면서 내부 구조를 개선하는 방법으로, 소프트웨어 시스템을 변경하는 프로세스이다.

⭐ 리팩토링시 중요한 점
소프트웨어를 보다 이해하기 쉽고, 수정하기 쉽도록 만드는 것, 겉으로 보이는 소프트웨어의 기능을 변경하지 않는 것이다. 따라서, 리팩토링을 할 때는 기능을 추가해서는 안되고, 단지 코드의 구조에만 신경 써야한다. 리팩토링은 가동중인 프로그램을 취해서, 동작을 바꾸는 것이 아니라 우리가 빠른 속도로 개발할 수 있도록 하는 특성을 좀더 많이 주어,프로그램의 가치를 높이는 것이다.

JUnit5 이란?

JUnit5는 다양한 어노테이션들이 추가되었다. 그중에 Junit5를 도입할 만큼 매력 있는 어노테이션 @DisplayName! 😀😀 단순한 테스트 이외에는 테스트 코드 네이밍으로 테스트하고자 하는 의미를 전달하기가 매우 어렵다. 이때 아주 유용하게 사용할 수 있다.

JUnit Assert

JUnit에서 assert는 테스트에 넣을 수 있는 정적 메서드 호출이다. 각 Assert 구문은 어떤 조건이 참인지 검증하는 방법이다. 단언한 조건이 참이 아니면 테스트는 그 자리에서 멈추고 실패한다. Unit은 크게 두 가지 Assert 스타일을 제공한다. 전통적인 스타일의 Assert는 JUnit의 원래 버전에 포함되어 있으며, 새롭고 좀 더 표현력이 좋은 햄크레스트라고 알려진 Assert 구문도 있다.

이를 통해 가입한 회원 데이터가 존재하는가 테스트 완료함!

MockMvc란?

실제 객체와 비슷하지만 테스트에 필요한 기능만 가지는 가짜 객체를 만들어서 애플리케이션 서버에 배포하지 않고도 스프링 MVC 동작을 재현할 수 있는 클래스를 의미한다.


현재 프로젝트 내에서는 스프링 시큐리티를 적용하고 있고, 지금은 별다른 설정을 하지 않고 authorizeRequests 만 했다.

🤔 여기서 잠깐, csrf란 무엇인가 ?

스프링 시큐리티를 적용하면, 별다른 설정을 하지 않더라도 기본적으로 csrf라는 기능이 활성화되어 있다. Cross-site request forgery의 약자로 타사이트에서 본인의 사이트로 form 데이터를 사용하여 공격하려고 할 때, 그걸 방지하기 위해 csrf 토큰 값을 사용하는 것이다. 타임리프 템플릿으로 form 생성시 타임리프, 스프링 MVC, 스프링 시큐리티가 조합이 되어 자동으로 csrf 토큰 기능을 지원해준다. 개발자 도구로 확인해보면, 타임리프로 form만 만들어주면, 자동으로 hidden으로 csrf 토큰 값이 생성되어 있음을 볼 수 있다.

<input type="hidden" name="_csrf" value="83836c37-a1fe-4a1e-81f5-48b5993021be">

다음과 같이 csrf 토큰값이 들어있다. 히든값으로! 이 토큰값이 같이 서버쪽으로 전송된다. 그럼 이 토큰값을 보고 내가 만들어준 form에서 온 데이터구나, 받아도 되겠구나 그렇게 해서 사용한다. 하지만 csrf 토큰이 제공이 안되면 404에러가 난다. SecurityConfig.java에서 "/sign-up" 를 허용했지만 form data는 csrf 토큰이 없기 때문에 안전하지 않다고 판단하는 것 😀

.with(csrf())) // 이것을 넣어주면 테스트게 제대로 동작함!!


mockMvc의 메소드

1) perform()
요청을 전송하는 역할을 한다. 결과로 ResultActions 객체를 받으며, ResultActions 객체는 리턴 값을 검증하고 확인할 수 있는 andExcpect() 메소드를 제공해준다.

2) get("/mock/blog")
HTTP 메소드를 결정할 수 있다. ( get(), post(), put(), delete() )
인자로는 경로를 보내준다.

3) params(info)
키=값의 파라미터를 전달할 수 있다.
여러 개일 때는 params()를, 하나일 때에는 param()을 사용한다.

4) andExpect()
응답을 검증하는 역할을 힌다.

상태 코드 ( status() )
메소드 이름 : 상태코드
isOk() : 200
isNotFound() : 404
isMethodNotAllowed() : 405
isInternalServerError() : 500
is(int status) : status 상태 코드
뷰 ( view() ) : 리턴하는 뷰 이름을 검증
ex. view().name("blog") : 리턴하는 뷰 이름이 blog인가?
리다이렉트 ( redirect() ) : 리다이렉트 응답을 검증
ex. redirectUrl("/blog") : '/blog'로 리다이렉트 되었는가?
모델 정보 ( model() ): 컨트롤러에서 저장한 모델들의 정보 검증
응답 정보 검증 ( content() ) : 응답에 대한 정보를 검증

5) andDo(print())

요청/응답 전체 메세지를 확인할 수 있다.

MockBean 어노테이션 ( 가짜 객체를 만들어 테스트할 수 있게 해줌)

이번에는 이메일이 보내지는지 테스트!

메일을 안보내면 테스트가 깨진다. 즉 AccountController.java에서 javaMailSender.send(mailMessage); 이 부분을 주석처리하면 !

메일 센더와 같은 부분은 인터페이스만 관리를 하고 실제 메일을 발송하는 것은 외부 서비스이다. 나중에는 실제 서비스와 연결할 것이다. 하지만 테스트 코드에서 그렇게 디테일 하게 짜면 .. 복잡함.. 그래서 모킹이용한 것임!

@PostMapping("/sign-up")
    public String signUpSubmit(@Valid SignUpForm signUpForm, Errors errors) {
        if (errors.hasErrors()) {
            return "account/sign-up";
        }
        
        // 실제로 account를 만들어서 저장해야 함. 
        Account account = Account.builder()
                .email(signUpForm.getEmail())
                .nickname(signUpForm.getNickname())
                .password(signUpForm.getPassword()) // TODO encoding 해야함
                .studyCreatedByWeb(true)
                .studyEnrollmentResultByWeb(true)
                .studyUpdatedByWeb(true)
                .build();
        Account newAccount = accountRepository.save(account);
        newAccount.generateEmailCheckToken();
        SimpleMailMessage mailMessage = new SimpleMailMessage();
        mailMessage.setTo(newAccount.getEmail());
        mailMessage.setSubject("스터디올래, 회원 가입 인증");
        mailMessage.setText("/check-email-token?token=" + newAccount.getEmailCheckToken() +
                "&email=" + newAccount.getEmail());
        javaMailSender.send(mailMessage);
        
        // Todo 회원가입 처리
        return "redirect:/";
    }

일단 너무 복잡함, 읽기 시작하자마자 어카운트 객체를 신나게 만들고, 저장을 하고, 이메일 체크 토큰을 만들고, 싱글 메시지 토큰을 만드는 과정들이 일단 너무 길다. 여러가지 역할이 하나의 메서드에 있으니까 문제가 되는 것임, 다 메소드로 빼버리도록 하자. 컨트롤러에 너무 많은 것들이 들어가 있으면 좋지 않다. 컨트롤러가 하는 일이 많아지다보면 모든 의존성을 갖게 된다. 따라서 서비스쪽으로 빼서 옮기도록! AccountService를 만든다. 현재 프로젝트 내에서 인증 이메일을 보내야한다는 것을 컨트롤러가 알아야 할 필요는 없어보인다. 즉 서비스만 알수 있도록 변경하자.

    private final AccountRepository accountRepository;
    private final JavaMailSender javaMailSender;

인증 이메일을 보내야 하는 것을 컨트롤러가 알아야 할 필요는 없다고 본다. 서비스만 알고 있으면 될 것 같으므로 서비스 뒤쪽에다 숨기기

    @PostMapping("/sign-up")
    public String signUpSubmit(@Valid SignUpForm signUpForm, Errors errors) {
        if (errors.hasErrors()) { // 에러가 있을 경우
            return "account/sign-up";
        }
        // 에러가 없을 경우
        accountService.processNewAccount(signUpForm);
        return "redirect:/";
    }
  • 컨트롤러는 새 어카운트를 처리하라는 메서드를 호출 -> 서비스로 이동
  • 서비스에서 로직 처리
  • 이메일을 보내는지 안보내는지는 컨트롤러가 알 필요가 없다. 서비스만 알고 있는 상태

여러 메소드를 만들어 코드를 분리했다. (리팩토링) 그러면 이 과정 중 혹시 오류가 생기지지 않았는지, 깨지지 않았는지 확인하기 위해 test 코드를 돌려보자! 테스트가 모두 잘 통과되었다. 메서드를 빼면서 메서드 이름만 잘 지으면 코드 읽기가 수월해진다.


출처 : 인프런 백기선님의 스프링과 JPA 기반 웹 애플리케이션 개발
https://codereview.tistory.com/3
https://cheese10yun.github.io/spring-jpa-best-14/
https://shinsunyoung.tistory.com/52
https://velog.io/@max9106/Spring-Security-csrf
https://coding-start.tistory.com/259 [코딩스타트]

profile
Step by step goes a long way ✨

0개의 댓글