본문 바로가기
BE 📙/디자인패턴의 아름다움

[CH 05.01, 05.02] 리팩토링, 단위테스트

by 경아ㅏ 2025. 8. 12.

 

리팩토링을 하기 위해서는 설계 원칙, 디자인 패턴, 코딩 규칙 등을 능숙하게 사용할 수 있어야 하기 때문에 문제를 해결하는 것보다 더 많은 능력이 요구된다.

 

리팩토링의 목적

 

리팩토링의 정의

 

리팩터링은 코드에 대한 이해를 쉽게 하기 위해 소프트웨어의 내부 구조를 개선하는 것으로, 소프트웨어의 외부 동작을 변경하지 않고 수정 비용을 줄이는 것을 목적으로 한다. (마틴 파울러)

"외부동작을 변경하지 않는다"

- 함수를 리팩토링 한다면, 함수의 정의를 변경하지 않는다.

- 라이브러리를 리팩토링 한다면, 외부에 노출된 API, 메서드를 변경하지 않는다.

 


리팩토링이 필요한 이유

 

1) 코드 품질 저하를 효과적으로 방지한다.

2) 고품질 코드는 한번의 설계에 나오는 것이 아니라, 지속적인 작업의 결과로 나오는 것이다.

3) 초기의 리팩토링으로 과도한 설계를 방지 할 수 있다.

4) 기술적 성장을 연습하는 중요한 수단이다.

 

 

리팩토링의 대상

 

대규모 리팩토링

 

시스템, 모듈, 코드 구조, 클래스 간 관계 등을 포함하여 최상위 설계를 리팩토링 하는 것

코드 변경이 많고 영향이 크기 때문에 시간이 많이 소요되고 버그가 발생할 확률이 높다.

 

소규모 리팩토링

 

주로 클래스, 함수, 변수 수준에서 코드 세부 사항을 리팩토링 하는 것

프로세스가 간단하여 소요 시간이 짧고 버그가 발생할 위험이 낮다.


리팩터링의 시기

 

코드가 이미 망가진 후에(유지 보수가 어렵고 버그가 빈번하게 발생) 리팩터링을 하려고 한다면 이미 늦었다...

평소에는 별 생각 없이 임의로 코드를 변경하다가 코드가 망가진 후에 리팩터링으로 해결해야지~ (X)
지속가능하고 진화적인 리팩터링을 진행하면서 코드 품질을 보장해야 한다 (O)

 

 

리팩터링의 방법

 

대규모 리팩토링은 많은 코드 변경이 필요 + 소요되는 시간이 길어지기 때문에 제 시간에 리팩터링을 완료하지 못하고 새로운 스펙과 충돌하면서 리팩터링 코드를 취소하는 상황이 발생하게 된다. (나도... 이런적 있슨)

따라서 사전에 종합적인 리팩토링 계획을 수립해 단계 별로 쪼개고, 각 단계에서 일부만 리팩토링하여 적용해본 후 이상이 없으면 그 다음 단계 리팩토링을 진행하는 것이 좋다. 각 단계 리팩토링에서 영향을 받는 코드 범위를 파악하고 제안해야 한다.

 

소규모 리팩토링은 영향도와 변경사항이 적기 때문에 시간이 있을 때마다 수행 가능하다. Checkstyle, FindBugs, PMD 를 사용하여 코드 분석을 자동으로 수행한 후 해당 부분만 리팩토링도 가능하다.

 

*깨진 유리창 효과 : 누군가가 저수준의 코드를 프로젝트에 추가하면, 점점 더 많은 사람들이 저수준의 코드를 추가하게 되는 현상
(나도.. 코드를 딱 보고... 해당 부분의 청결도(?)에 따라서 코드 수준이 달라지는 것 같기도 하다 ... ㅋㅅㅋ)
코드 품질을 중요하게 생각하는 분위기를 조성하여 팀원들이 적극적으로 품질에 관심을 가질 수 있도록 해야 한다.


단위 테스트

 

통합 테스트

 

요청을 시작하여 코드가 실행 결과를 반환하는 전체 경로에 대한 테스트

사용자 등록, 로그인과 같은 기능 단위를 테스트하는 것을 목적으로 한다.

 

단위 테스트

 

클래스 또는 함수를 대상으로 하는 테스트

코드 수준을 테스트하는 것을 목적으로 한다.

 

리팩토링을 수행했을 때 단위 테스트를 모두 통과한다면 코드가 수행해야 하는 기능을 모두 올바르게 수행하고 있음을 알 수 있다.

단위테스트는 잘못된 리팩토링을 막는 효과적인 수단이다.

 

*(p.201) 문자열을 숫자로 변환하는 코드에 대한 단위테스트 작성 예시

 

단위 테스트 작성에는 고도의 기술이 필요 없으며, 가능한 모든 정상/비정상 테스트 케이스를 추가하면 된다.

 

문자열이 숫자 “123”만 포함된 경우에 toNumber() 함수는 정수 123을 반환한다.

문자열에 숫자가 없거나 null이면 toNumber() 함수는 null을 반환한다.

“123”, “ 123”, “123 “과 같이 문자열의 앞이나 뒤에 공백이 있는 경우에 toNumber() 함수는 정수 123을 반환한다.

“123”, “ 123”, “123 “과 같이 문자열의 앞이나 뒤에 여러 개의 공백이 있는 경우에 toNumber() 함수는 정수 123을 반환한다.

“123a”, “a123”와 같이 문자열에 숫자 이외의 문자가 포함된 경우 toNumber() 함수는 null을 반환한다.

 

 

단위 테스트 코드를 작성해야 하는 이유

 

1) 버그를 찾는데 도움이 된다.

2) 설계에서 문제를 찾는데 도움이 된다. 특정 코드에 대해 단위 테스트를 작성하려고 하는데 작성하기 어렵다면? 의존성 주입을 사용하지 않거나 전역 변수/전역 함수를 많이 사용하거나 결합성이 높은 코드일 가능성이 높으므로 설계가 비합리적임을 찾아낼 수 있다.

3) 단위테스트는 통합테스트를 보완하는 강력한 도구다. 통합테스트로는 일부 경계 조건이나 비정상적인 상황을 모두 재현하여 테스트하기 어렵기 때문에 단위테스트를 통해 보완하자.

4) 단위테스트를 작성하는 것은 코드 리팩터링 과정이 해당한다.

5) 프로그래머가 코드에 빠르게 익숙해지도록 도와준다. 단위테스트는 코드 수행 작업과 사용 방법을 반영하는 사용 사례에 해당하므로, 단위테스트를 먼저 읽음으로써 코드를 빠르게 파악할 수 있다.

6) 테스트 주도 개발을 개선하고 대체할 수 있다. TDD(테스트 주도 개발)은 사실상 실현하기 어렵기 때문에, 코드 개발 - 테스트 코드 작성 - 테스트 코드를 작성하면서 느낀 설계 및 코드 문제점 개선 과정을 통해 테스트 주도 개발을 보완/대체하여 개발 가능하다.

 

 

단위 테스트를 설계 하는 방법

 

*(p.206) JUnit 을 이용한 테스트케이스 작성 예시

 

1) 단위테스트를 설계하는 것은 시간이 많이 걸리는 일인가?

- 시간이 많이 필요하지 않고, 반복되는 테스트 코드가 많으므로 재활용이 가능하다. (이거슨 주관적인 것 아닌가요)


2) 테스트 코드의 품질에 대한 요구 사항이 있는가?
- 비즈니스 코드가 아니기 때문에 품질 수준을 낮추어 작성해도 괜찮다.


3) 커버리지가 높으면 그것만으로 충분한가?

- JaCoCo, Cobertura, EMMA, Clover 

- 테스트 커버리지가 100%라도 하나의 기능에 하나의 메서드만 작성해놓는다면 커버리지가 의미가 없다.

- 커버리지 자체보다는 가능한 모든 케이스를 포함하면서 테스트를 진행하고 있는지에 더 주의를 기울여야 한다.

- getter, setter 에는 굳이 단위테스트를 추가할 필요가 없다. 불필요한 테스트를 추가하지 말자.

 

4) 단위 테스트 코드를 작성할 때 코드의 구현 논리를 이해하는 것이 필요한가?

- 아니다. 구현 논리에 의존하지 않고 테스트 하려는 모듈의 기능에만 초점을 두어 테스트 코드를 작성해야한다.

 

 

단위 테스트를 작성하기 어려운 이유

 

단위테스트는 개발자 입장에서 특별한 기술을 습득할 수 있는 영역이 아니고 인내심이 필요한 작업이기 때문에 잘 작성하지 않게 된다. 더불어 테스트 팀이 별도로 존재하는 경우가 있기 대문에 리뷰나 테스트 코드 작성을 하지 않고 테스트 팀에 의존하여 버그를 발견, 수정하는 팀들이 많다. 이미 팀의 코드 양은 많은데 테스트 코드가 없다면 새로운 코드에 테스트 코드를 추가해보자. 테스트팀이 아닌 개발팀 수준에서 코드의 정확성을 보장할 수 있도록 하자.

 

 

'BE 📙 > 디자인패턴의 아름다움' 카테고리의 다른 글

[CH03.01] 설계 원칙 - 단일 책임 원칙  (1) 2025.08.05
[CH02.09] 상속보다 합성  (1) 2025.08.04
[CH03.07-03.08] 설계원칙  (4) 2025.07.31
[CH01] 개요  (5) 2025.07.21

댓글