개요

Spring, 또는 SpringBoot 둘 중 하나를 택해 웹 서비스를 개발하는 수업의 과제물로 제출한 프로젝트이다.

이전에 기능을 구현할 때에는 과제 마감일에 쫓겨 가독성이 좋지 못한 코드를 작성했었다. 금번 리팩토링에서는 이러한 문제를 해결하고 코드의 품질을 높이는 방법에 대해 팀원들과 함께 공부해보았다. 회의는 특별한 이변이 없는 한 매주 진행하였다.
총 3명의 팀원이 함께하였으며, 로버트 C. 마틴의 저서 <클린 코드(애자일 소프트웨어 장인 정신)>를 읽고
각자 학습한 바를 정리하고 이를 바탕으로 테스트 작성과 리팩토링을 하였다.
여기에 팀장으로서 전반적인 개발 과정을 검토하고, 코드 리뷰와 회의를 주도하였다.

2편은 테스트 작성과 리팩토링을 다룬다. 🔗1편 링크는 이곳

 

✨Git 링크

https://github.com/jinju9553/SOMusic-SpringBoot

 

수행 기간

2022.01.08 ~ 2023.05.10

 

기술 스택

Language: Java 1.8
Framework : SpringBoot, Spirng Data JPA
View: JSP, ThymeLeaf
DB: Oracle Database
Test: JUnit5, Mockito

 

수행 과정

  • 시작 전에 점검한 것들
    • 프로그램 구조도를 수정해야 하는지 고민했으나, 리팩토링 과정에 설계와 기능 수정은 포함되지 않는다.
    • 15라인이 넘어가는 함수 분리, 들여쓰기 수정, 불필요한 주석 삭제, 메소드 파라미터의 개수 줄이기 등 코드의 가독성을 높일 수 있는 방안을 검토하였다.
    • <클린 코드> (로버트 C. 마틴), <리팩토링 1판> (마틴 파울러), <객체지향의 사실과 오해> (조영호) 중에서 언어에 구애받지 않고 여러 코드 리팩토링에 가장 적용하기 쉬운 첫 번째 서적을 선택하였다.
  • 1주차
    • 각 클래스 별로 어느 메소드를 어떻게 수정해야 할지, 리팩토링할 목록을 정리하였다.
  • 2주차
    • <클린 코드>를 읽고 중요한 내용들을 문서화시켜 정리하였다.
  • 3주차~8주차
    • 각 클래스 별로 테스트 코드를 작성하였다. 본래 TDD 개발방식에 따르면 기능 개발에 앞서 테스트를 미리 작성하는 것이 맞지만, 본 프로젝트 는 스프링 프레임워크를 배우는 강의의 과제물로써 개발한 것이고, 일전에 별도의 테스트 작성법을 학습한 적이 없기에 부득이하게 기능 개발 이후에 테스트를 작성하게 되었다. 코드리뷰는 2~3주에 한 번씩 시행한다.
    • 테스트 작성 과정 중에 맞닥뜨린 오류는 다음 항목에 정리한다.
  • 9주차~15주차
    • 완성된 테스트 내용을 바탕으로 코드를 리팩토링한다. 상기한 내용처럼 길이가 긴 함수를 최대한 분리하고 조건문을 캡슐화하여 가독성을 향상시키고 의미를 파악하기 쉽도록 하였다. 또, 강의 수강 당시 학습용으로 작성해두었던 주석들을 모두 삭제하였다. 마찬가지로 코드리뷰는 2~3주에 한 번씩 시행한다.
    • 리팩토링 과정 중 맞닥뜨린 오류는 다음 항목에 정리한다.
  • 16주차
    • 코드 리뷰와 회고를 작성한다.

 

문제 해결

  • Controller 테스트 코드의 404번 오류
    • 참고한 링크: https://sanghaklee.tistory.com/61
    • REST API에서는 주로 경로가 존재하지 않거나 요청한 자원이 존재하지 않을 떄 404번 상태 코드를 응답한다.
    • 본 테스트에서는 테스트 코드와 연결된 컨트롤러 명에 오탈자가 존재해서 404번 응답을 받게 되었다.
    • 따라서 Controller 테스트 클래스의 @WebMvcTest 어노테이션 안의 컨트롤러 명칭의 오탈자를 수정해주었더니 해결되었다.
    • 하지만 이어서 아래 오류를 받게 된다.
    • Failed to load ApplicationContext 에러
      • 테스트 코드에서 AccountFormValidator의 선언 및 Bean 등록이 빠져있어서 발생한 오류이다.
      • 협력 객체들을 테스트 코드에서도 모두 선언하고 MockBean으로 등록해주어야 테스트가 정상적으로 구동한다.
      • 모든 협력 객체를 등록하고 API로부터 200 OK 응답을 받은 것을 확인하였다.
  • Enum의 도입
    • 본 웹 서비스의 상품 주문 기능에서는 shippingCost(배송비) 라는 데이터를 활용한다. 이 배송비는 총 3가지 존재하고 사용자가 원하는 배송 방식을 선택하여 그에 따라 각기 다른 배송비가 최종 주문 금액에 합산된다.
    • 이 배송비는 ModelAttribute로 설정하여 클라이언트 단에 전송되고, 리턴 타입은 Object[]이다. 그런데 이전에는 이 배송비와 배송비 표시 텍스트("준등기", "택배" 등의 글씨) 등을 ModelAttribute 함수 내에 배열로 생성하여 리턴하였다.
    • 그러한 방식을 사용하다보니 다른 컨트롤러에서 똑같이 배송비를 클라이언트에 전송하는 작업을 수행하려면 코드를 복사 + 붙여넣기 해야 했고, 불필요한 코드 반복이 나타났다.
    • 이 문제를 해결하기 위해 ShippingCost라는 Enum을 정의하여 그 안에 배송비와 텍스트, 고유 번호 등을 선언하고 상수처럼 값을 꺼내 쓰는 형태로 리팩토링 하였다. 모든 값을 리스트로 반환하는 비즈니스 메소드도 함께 정의하였다.
    • 이렇게 하면 배송비 데이터를 필요로하는 컨트롤러에서 ShippingCost.getCostList() 한 줄로도 우리가 원하는 데이터를 전송할 수 있다. 일일이 데이터를 타이핑하거나 붙여넣기 하다가 오탈자가 생길 염려를 하지 않아도 된다.
  • Optional 클래스의 활용
    • 참고한 링크: https://mangkyu.tistory.com/70
    • Stream을 통해 데이터를 처리할 때 타입 오류를 맞닥뜨리고서 Optional 타입이 무엇인지 알게 되었다.
    • Optional 타입은 쉽게 말하면 null이 발생할 수 있는 값을 감싸는 Wrapper 클래스이다. 불필요한 null 처리를 줄일 수 있다.
    • 이때, StreamfindAny()로 발견한 값들도 Optional 타입으로 반환된다.
    • 따라서 Optional<T>.get()으로 안에 들어있는 내용물을 별도로 꺼내주어야 한다.
  • BindingResultHttpSession 객체를 테스트에 활용하는 방법
    • 이러한 객체들은 단순히 인스턴스를 생성하는 방식으로는 사용할 수 없다. 대신 Mockito.mock()으로 Mock 객체(가짜 객체)를 만들어주면 테스트에 활용할 수 있다.
    • MockHttpSession 객체는 다음과 같은 방식으로 생성한다.
      Login userSession;
      
      MockHttpSession session;
      
      @BeforeEach() //다른 테스트 메소드보다 가장 먼저 실행된다.
      public void setUp() {
      	Account account = new Account();
      	account.setUserId(USER_ID);
          
      	userSession = new Login(account);
          
      	session = new MockHttpSession();
      	session.setAttribute("userSession", userSession);
      }
  • 테스트 함수의 인자로 captor.capture() 와 함께 raw String을 줄 때
    • 참고한 링크: https://github.com/HomoEfficio/dev-tips/blob/master/Mockito Stub 작성 시 주의 사항.md
    • Mockito.verify(accountDao).updatePassword(captor.capture(), NEW_PASSWORD); 에서 발생한 문제이다.
    • 이 경우, 함수의 모든 인자를 Matcher를 통해 전달해주어야 하기 때문에 raw String을 인자로 쓸 수 없다.
    • 따라서 String을 ArgumentMatchers.eq()로 감싸준 뒤 함수의 인자로 넘겨준다.
    • ArgumentMatcherArgumentCaptor를 쓸 때는 그 인자로 반드시 Matcher를 사용한 값을 주어야 한다. any() 메소드 또한 Matcher 메소드의 일종이다.
  • Cannot resolve symbol 'Assert’ 오류
    • 참고한 링크: https://aonee.tistory.com/2
    • 코드에서 사용된 버전의 jUnit 라이브러리가 프로젝트 내에 존재하지 않아서 발생하는 오류이다. 나와는 다른 개발 IDE를 사용하는 팀원이 작성한 테스트 코드를 pull해온 뒤에 이 오류가 발생하였다.
    • Maven dependency(pom.xml)에 다음과 같이 jUnit 4.12버전의 JAR을 import하는 부분을 추가해주면 오류를 없앨 수 있다.
      <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.12</version>
          <scope>test</scope>
      </dependency>

 

소감

  • 학교 과제와 무관하게 진행된 스터디 모임이었다. 그 덕분에 마감일에 쫓기지 않고 공부를 할 수 있었다.
  • JUnit을 이용해 테스트 함수를 작성해본 것은 이번이 두 번째였다. 그러나 JUnit 테스트를 처음 활용해보았을 때는 MVC 구조의 웹 어플리케이션이 아닌, 일반적인 자바 객체의 유효성을 검증하는 등의 기능만 사용해보았던 탓에 웹 어플리케이션의 테스트 코드는 어떻게 작성해야 하는지 헤메기도 하였다.
  • 다행히도 인터넷에 SpringBoot 기반으로 테스트 코드를 작성하는 방법을 잘 정리해놓은 자료들이 제법 있어 도움이 많이 되었다. 검색의 힘이 가장 컸던 것 같다. 블로그 게시글들을 보면서 차츰 라이브러리 내의 메소드 사용법에 익숙해지도록 하였다.
  • 그 다음에는 given - when - then 패턴을 핵심으로 삼아 테스트코드를 작성하였다. 이러한 뼈대가 있다고 생각하니 테스트 작성에 막연함이 덜한 느낌이었다. (참고한 블로그 링크는 이곳)
  • 위 패턴에 따라 테스트 코드를 작성하고, 작성 중 모호한 부분이 있다면 클린코드 17장: '냄새와 휴리스틱'에 수록된 원칙들을 따랐다. 가능한 클래스 내의 모든 메소드를 테스트해보려 하였다. 주로 참고한 원칙들은 다음과 같다.
    • T1: 불충분한 테스트는 독이다.
    • T3: 사소한 테스트도 건너뛰지 마라.
    • T4: 무시한 테스트는 모호함을 뜻한다.
    • T5: 경계 조건을 각별히 신경 써서 테스트하라.
    • T7: 실패 패턴을 살펴라.
  • 다만 `T2: 커버리지 도구를 사용하라` 의 원칙은 고려하지 못한 것이 아쉽다. 테스트 메소드 작성과 리팩토링에 급급해 커버리지를 미처 신경쓰지 못하였다. 회고를 작성한 후에도 시간을 내어 테스트 커버리지를 확인해보고 싶다.
  • 테스트 작성을 마치고, 리팩토링 과정에서는 상기한 내용처럼 코드의 가독성을 높이는 데에 집중했다. 메소드명을 통해서 의미를 드러내고, 메소드가 수행하는 일을 문장으로서 나타내는 것이 참 중요하다고 느꼈다. 팀원의 코드 중 몇몇 부분은 잘 이해하지 못하기도 했었는데, 그 또한 메소드명을 수정하고 몇몇 코드를 캡슐화한 덕분에 그것이 의미하는 바를 쉽게 이해할 수 있게 되었다.
  • <클린 코드> 내에서도 강조했던 내용이지만, 결과적으로 프로그램의 코드 또한 사람이 읽는 문서의 일종이고, 그만큼 협업하는 사람들이 이해하기 쉽도록 작성하는 것이 중요하다는 것을 다시 느꼈다. 

+ Recent posts