🔥 공대생은 성장 중/강의

THE RED 백명석, 최범균 - 백발의 개발자를 꿈꾸며 : 코드리뷰, 레거시와 TDD : 강의 회고 및 개인 요약 정리(2)

민돌v 2022. 6. 28. 00:20
728x90

이전 글에 이어서 

이번에는 최범균님의, 레거시 코드 리팩토링 방법 과 TDD 에 대해서 정리해보겠습니다..!

 

 


 

3부 레거시와 리팩토링

 

1) 레거시 코드란

레거시 코드의 몆가지 정의

  • 오래되었지만 여전히 사용되는 것
  • 테스트가 없는 코드
  • 모든 코드가 레거시(극단적인 주장)

 

레거시는 피할 수 없다 - 대부분의 회사는, 레거시 코드로 인해 돌아감 (월급의 원천...!!)

 


 

2) 레거시 특징

  1. 긴 메서드, 긴 클래스 - 복잡하고 이해하기 힘든 코드
  2. 이상한 이름 - 이름과 행동이 다른 메소드, 객체
  3. 테스트 코드가 없다...

 

레거시의 수정은, 코드 이해가 부족한 상태에서 일어나는 경우가 많음

 


 

3) 악순환 줄이기

서비스는, 배포될 때 마다, 개발 비용이 증가함 (레거시의 증가로 인해)

  1. 악순환을 줄이려면 코드 변경 비용을 낮춰야함
  2. 변갱 비용을 낮추려면 변경하기 쉬운 구조로 점진적으로 리펙토링 해야함
  3. 리펙토링해도 이전과 동일하게 동작해야 함
  4. 이전과 동일하게 동작하는지 확인할 수 있는 테스트가 필요함
  5. 테스트를 만들려면 기능이 어떻게 동작하는지 분석해야함
  6. 즉 악순환을 줄이려면 레거시를 분석하고 테스트를 만들고 리팩토링 해야함 

 


 

4) 레거시 분석

모든 코드를 분석하고 이해하고 기억할 수는 없다.

 

코드 분석에 도움이 되는 보조 수단

  • 코드 시각화
    1. 다이어그램 - 실행 흐름
      • 액티비티 다이어그램 -:단계적인 코드 실행 흐름
      • 시퀀스 다이어그램 : 구성 요소간 연동 흐름
      • 의존/ 호출 관계 그래프 : 구조와 영향도 분석

논리적 분석 및 분기문 체크

 

시퀀스 다이어그램

 

영향도 체크

  • 코드 출력, 형광팬
    • 여러 메소드의 코드를 펼쳐놓고 편하게 봐보자
  • 함께 코드보며 분석하기
  • 스크래치 리펙토링
    1. 코드 이해 목적으로 진행하는 리랙토링
    2. 효울적으로 리팩토링하기 보다는, 코드를 이해하는데가 목적
    3. 리팩토링으로 코드 이해 후, 다시 되돌린 후 리팩토링 한다.

 


 

5) 레거시에 테스트 코드 만들기

레거시 분석이 끝났다면, 레거시 테스트 코드를 만들어야 한다.

레거시 테스트 코드 만드는 방법

  1. 범위를 좁혀서 테스트 작성
  2. 대체 구현을 이용해서 테스트 작성
  3. 범위를 넓혀서 테스트 작성

 

 

1) 범위를 좁혀서 테스트하는 방법 (단순 로직 리펙토링)

  • 테스트 할 대상을 기존 코드와 분리해서 테스트 작성
  • 일부 코드/ 로직/ 기능만 테스트하고 싶을 때
  • 새로 추가한 코드 테스트 시 (ex 계산 로직)

1. 리펙토링 할 대상 코드의, 필수로직 모으기

  • 로직과 불필요하게 중복되는 부분을, 따로 빼거나, 한 부분에서 정리가능한 부분(반복적으로 들어가는 조건문 안으 로직) 등을 분리해서, 리펙토링할 대상을 정리

2. 입력값과, 출력값을 체크해서, 로직부분을 메소드로 분리

3. 분리한 메소드를 테스트 코드로 작성해서, 리펙토링 실시

 


 

2) 대체 구현을 사용해서 테스트 만들기 (다른 객체에 의존하고 있는 코드 리펙토링 하기)

리펙토링 시, 의존하는 객체 or 메소드 대역으로 만들어서 테스트하는 방법

  • 테스트 대상이 사용(의존)하는 객체/ 기능이 존재할 때 사용
  • 의존 대상의 구현을 대체할 대역을 만들어서 테스트 작성
  • 필요한 것 두 가지
    • 의존 대상 : 대역을 만들 수 있는 구조로 변경 필요
    • 테스트 대상 : 대역을 사용할 수 있는 구조로 변경 필요

 

 

먼저 대역을 만들 수 있는 구조로 변경 

 

1. 의존하는 구현 코드를 새타입으로 이동 -> 테스트 작성

 

 

2. 의존 대상에서 인터페이스 추출 -> 인터페이스 사용

  • 클래스에 대역을 쉽게 생성할 수 있기 때문에, 사용빈도가 낮지만 설계 관점에서 구조 변화가 필요할 때 사용되는 방법이다.
  • 의존성이 있는 메소드를, 인터페이스로 추출해서, 구현체로 만든 후 / 인터페이스를 사용하도록 변경

 

 

3. 테스트 대상에서 구현을 대체할 부분을 protected 메서드로 분리

  • 대체 구현을 제공할 하위 클래스에서 메서드 재구현

 

 

테스트할(리펙토링할) 객체가 의존하는 대상을 대역할 수 있는 대상으로 바꾸는 방법이었습니다.

이렇게 일단 의존하고 있는 객체들을 실체 가짜 객체같은 개념으로 사용할 수 있도록, 대체 구현해서 테스트할 객체만 볼 수 있도록 해줍니다.

 

테스트 대상이 대역을 사용할 수 있는, 구조로 변경하는 방법

 

1. 생성자나 세터 메서드 이용 (의존 주입)

  • 테스트할 대상이 의존하는 객체들을 대역 객체로 만들어 준 후
  • 그 객체들을 의존성 주입해, 테스트 시 기능 상 이상이 없도록 합니다.
  • 세터, 혹은 생성자 주입을 사용하며 / 가짜 객체도 사용됩니다.

 

 


3) 범위를 넓혀서 테스트 만들기 (통합 테스트)

  1. 좁은 범위 테스트가 어려운경우
    • 의존 대상이 많아 특정 범위만 테스트하기 어려운 경우
    • 테스트 만들기 위해 변경해야 하는 코드가 너무 많은 경우
    • 코드 의미를 알 수 없어 테스트 대상 범위를 좁히기 어려운 경우
    • 일부 로직이 쿼리에 위치한 경우 사용되는 방법입니다.
  2. 범위를 가능한 넓혀 다양한 구성 요소 간 연동을 포함하는 테스트 코드 작성
    • DB 연동 / 외부 연동을 포함하기도 함
    • API 또는 제공 기능 단위로 통합 테스트
  3. 주요 목적
    • 레거시 코드 동작 이해 (Characterization Test)
    • 리팩토링 사전 작업

 

➡️ 다양한 구성요소를 모두 아우르는 수준의 넓은 범위 테스트 ( API, 큰 메소드 수준 등등)

 

 

  • 테스트 실행 환경 구성 - DB 연동 등등 
  • 처음에는, 테스트 입력값, 출력값, 호출하는 파리미터, 실행 결과로 바뀌는 결과값을 모르기 때문에, 반복적으로 하면서 코드를 이해하는 목적

 

 

레거시 통합테스트 만들기 예시

 

로컬에서 나만의 디비를 사용함으로써, 다른 개발자에 영향이 가지 않도록하는 방법

 

 

테스트 마다, 디비에 영향을 주지 않기위해, 매 테스트 마다 로컬 데이터 날려버리기 위한 메소드 작성

 

 

레거시 통합테스트 구현을 위해  필요한 테스트 코드

  1. 테스트 실행 테스트
  2. 결과 확인 테스트
  3. 쿼리에 숨겨진 상황이나 결과 찾기
  4. 정상 작동 테스트 완성 후, 예외 케이스 작성

 

 

 


 

6) 리팩토링

레거시에서 사용할 만한, 리팩토링 기법 소개

  1. 테스트가 있다면 과감하게 리팩토링 가능
    • 코드 변경 후와 전이 동일하게 동작함을 확인할 수 있기 때문에
  2. 테스트가 없어도 필요하면 리팩토링 진행해야됨

 


리펙토링 방법

1) 미사용 코드 삭제

  1. 주석으로 되어있는 코드 삭제하기
    • 주석 처리한 날짜를 기록하고 일정기간 뒤에 삭제

 

2) 매직 넘버

  • 상수는, 의미있는 변수명으로 대체(상수로)

3) 이름 변경

  • 객체, 메소드가 하는 책임과 행동에 맞게 이름 변경

4) 변수 선언과 사용

  • 변수는 사용 직전 위치로 이동
  • 코드의 명확한 이해를 위해서, 최대한 사용 직전으로 이동
  • 값이 계속 바뀌는 변수는 코드 추적이 어려움
  • 필요하지 않은 변수는 사용 x  -> 변수는 최대한 줄일 수 있도록
  • 하나의 변수를 여러의미로 사용하지 않기

5) if 줄이기

  • if 가 길고 else 가 짧은 경우 역조건 사용해서 구조 단순화 
  • return 사용

6) 메소드 분리

같은 조건의 if - else가 한 메소드 안에서 출현할 때 분리

  • 같은 조건의 if else 가 몆 군데 출연
  • 일부 비슷하게 동작하는 두 기능을 한 메소드에서 구현했는지 확인
  • 두 기능을 한 메소드에서 구현했다면, 점진적을 ㅗ메소드 분리

7) 클래스 분리

  • 클래스가 계속 커져서, 분리해서 발생하는 복잡도보다 커질 때 분리

8) 메소드 추출

  • 의도가 들어나는 이름으로, 특정 코드를 분리
  • 코드 가독성도 높아지고, 분석하기도 쉬어지는 방법이다

9) 클래스로 추출

  • 코드 일부를 클래스로 추출
  • 테스트 용이성이 증가

10) 파라미터 값 정리

  • 파라미터는 적을수록 좋다
  • 사용하지 않는 파라미터는 바로바로 삭제
  • DTO로 넘어온다면, 사용하지 않는 프로퍼티는 삭제 혹은 새 타입 사용

 

 

 

 


 

4부 TDD

 

1) TDD 란

Test Driven Developnet - 테스트 주도개발

테스트로부터 시작하는 개발이라는 뜻으로, 예외 테스트 케이스를 먼저 작성하고, 테스트를 통과할 만큼만 코드를 작성하는 것을 의미합니다.

 

TDD 의 예시

 

TDD 순서

  1. 위의 순서에 맞게 한단계, 한단계 씩 테스트 코드 작성
  2. 컴파일에러 없에보기 (Import, Type, new 객체 등등)
  3. 실행 -> 기대값 확인
  4. 기대값에 맞도록 코드 수정
  5. 기대값을 충족한다면 - 코드에 리펙토링할게 있는지 확인
  6. 1~4단계, 계속계속 반복

 

👍 예외케이스를 계속 추가하면서, 결과값을 일반화 시키는 것이  TDD의 목적

가능한 빠르게 테스트를 통고할 수 있는 코드를 작성하는 것이 TDD 이다. ( 처음에는 불안정하고, 단편적으로만 ...  점점 로직에 맞도록 수정해 나가는 과정)


TDD를 해나가는 과정

  1. 예외 케이스를 통과하도록 작성하고
  2. 작성한 코드를 리팩토링하고
  3. 점점 결과값을 일반화 시킨다.
  4. 그 과정에서 코드가 변경될 때 마다, 테스트를 진행해보는 것이 TDD의 과정이다.
  5. 모든 코드가, Test 폴더 영역에서 완성이 되면, 메인영역에 하나씩 옮겨가며 다시 테스트를 돌려서 이상이 없는지 확인하며 다 이동시키면 코드의 완성이다...!

 


TDD 와 설계

TDD를 하게되면, 자연스럽게 코드의 기대값과 결과값을 생각하게 되므로, 설걔의 관점에서 코드를 작성하게 된다는 장점이 있습니다.

 

 

 

조금 더 큰 API 통합 테스트

 

통합테스트는, 통합 실행 환경을 먼저 만들어 준다. (ex - @SpringBootTest, @AutoConfigyreMovkMvc, @Sql)

 

통합 테스트를 진행하면서, 유닛테스트가 가능한 단위가 나왔을 때 유닛테스트 진행

큰 통합테스트 안에, 많은 유닛 테스트가 발생

 

Controller 유닛 테스트

시나리오데로, 호출하고, 파미터값이 넘어가는 지

  1. @MvcTest - API 단위 테스트
  2. @MockBean - 호출하는 객체를, 가짜객체로 만들어준다
  3. //then : BDDMockito.then(xxxService).should().Method(파라미터)  - Controller 내부 시나리오(해당 Service의 어떤 메소드를 호출하는 지 테스트)

 

Service 유닛 테스트

에러 케이스 테스트 코드 작성 방법

  1. Assertions.assertThatCode(() -> { 실행되는 메소드 }). isInstanceOf(xxxxException,class)
  2. 혹은 AssertThatThrown()~ 예외를 던지는, 2가지 테스트코드 메소드!!

 

Service 단위 테스트에서의, Repository 구현 방법

  1. 멤버 필드에 RepositoryInterface 변수 = new RepositoryImpl 선언
  2. RepositoryImpl 을 테스트를 위해 Map<> 으로 구현
    • ex - Map<String, Domain> domains = new HashMap<> 이런식으로 DB에서의 종속성을 제거하고, Service의 테스트만 테스트 할 수있도록 Repository를 직접 구현하는게 좋다.
  3. 실제 DB를 같이 주입해서 통합테스트를 하면 (ex - @JpaTest, @WebMvcTest, @SpringBootTest) 디비에 문제가 생겼을 때, 테스트코드까지 실패하게 되버린다. (항상 성공하는 테스트코드를 짜라..를 위반!!)
  4. 그 후, DB 종속성을 빼고, Service Test 를 진행한다...!

 

나는 인수테스트를, Service단에서 주로 진했는데, Controller 부분만 인수테스트를 진행하고, Service 부분도, Repository를 직접 구현함으로써, 인수테스트와 단위테스트를 같이 진행할 수 있는 것같다.. 놀라워

Map 으로 직접구현한 Repository 예시

 

이렇게 Repository 처럼, 외부에 종속적인 무언가가 있을 수 있는데, 이때 사용할 수 있는게 대역이다.

 

테스트 대역

API를 직접 호출하는게, 아닌 간단하게 결과값만 반환해주도록, 가짜 대역 객체로 의존성을 끊어준다.

모통, 모의객체가, fake와, spy의 역할을 같이하기 때문에, Mock 객체를 많이 사용한다.

스텁 객체는, 위처럼, Repository를 구현하거나, 가짜 외부 API를 구현하거나 하는 것을 말한다.

 

 

 

TDD 테스트 작성 순서!

1. 당장 빠르게 구현할 수 있는 것부터 고민

2. 예외적인 경우를 먼저 테스트

  • 예외적인 구조는 코드 구조에, 영향을 주기 때문입니다.

 

 


 

마지막으로 말씀해주신 백엔드 면접 질문 정리

 

  1. 경력위주의 자기 소개
  2. 개발을 왜 하는지 - 이유에 대한 루틴, 행동
  3. 이력서 기입한 것에 대해, 얼마나 알고있는지
  4. 신입 시 - 3, 4 학년 때 들었던 강의 : 힘든 강의를 피하지 않고 잘 들었는가
  5. 기억에 남는 자바의 특징
  6. 기억에 남는 협업 경험 (트러블, 팀장 이슈 등등)
  7. 이직을 얼마나 자주했고, 이직의 이유가 뭔가(내 성장과, 비전, 회사에 대한 관심도 등등)
  8. 회사에 지원한 이유 - 그냥 솔직하게 말해라! (이커머스에 관심이있어서 지원했고, 여기저기 많이 냈습니다..!)
  9. 회사에 와서 기대하는 것
  10.  잘 성장할 수 있는 개발자인가 - 잘 모르는 질문 (솔직하게 모른다고 대답..! 인정하는 모습 굿굿)
  11. 지금 책상에 어떤 책이 있나 (잠깐 공부하는 블로그를 보는 지, 몆 달 지속적으로 공부해야하는 무언가를 보는지)
  12. 토이 프로젝트
  13. 롤모델
  14. 자료구조
  15. 응집도, 결합도, 캡술화
  16. 왜 스프링을 쓰는 지
  17. 의존성 관리, 독립적인 개발. SOLID(LSP)
  18. 프레임워크와 라이브러리의 차이
  19. TDD, 리팩토링, DDD 어느정도 해보았는가 (가산점 느낌)
  20. 남의 나에게 평가한, 장점 단점
  21. 지킬 수 없는 요구에 대해서 어떻게 대답할 것 인가 (본인만의 설득 노하우..?)
  22. 기술 부채
  23. 내가 윗 직급으로 갔을 때 어떤 것을 하고싶나 (보통 시니어에게)
  24. 최고/ 최악의 협업 사례
  25. 남들은 싫다고하는 좋은 Tdd, 리팩토링 같은 거를 성공시킨 사례가 있는가!

 

반응형