점진적 리팩터링

 💡 우아한테크코스 과정에서 학습했던 점진적 리팩터링에 대해 복습하였습니다!

 

레거시 코드 리팩토링

  • 레거시 코드 리팩토링은 기존의 코드베이스를 개선하는 과정입니다.
  • 주된 목적은 코드의 기능을 변경하지 않으면서 코드의 구조를 더 깔끔하게하고 유지보수가 용이하게 만드는 것입니다.
  • 보통 오래된 시스템에서 작성된 코드에 필요합니다.

레거시 코드

  • 읽기 어려움
    • 코드가 복잡하거나 문서화가 부족하여 이해하기 어렵다
  • 유지보수 어려움
    • 코드의 수정이나 기능 추가가 힘들고 버그가 발생하기 쉽다
  • 테스트 부족
    • 충분한 테스트가 없어서 코드 변경 시 예상치 못한 문제를 일으킬 수 있다

점진적인 리팩터링

  • 한 번에 많은 코드를 바꾸는 것이 아닌, 여러 작은 단계로 나누어 변경하는 방법입니다.
  • 기존의 테스트 코드가 깨지지 않는 상태로 리팩터링을 실행합니다.
  • 컴파일 에러 발생을 최소화하면서 리팩터링합니다.
  • 코드의 작은 부분만 변경하고, 매번 변경 후에 코드를 컴파일하고 테스트를 실행해 변화가 문제를 일으키지 않음을 확인합니다.
  • 지속적인 테스트와 작은 단위의 수정으로 리스크를 줄이는 방식입니다.
    • 리스크 관리: 문제가 발생해도 쉽게 원인을 추적하고 수정 가능

점진적인 리팩터링이 필요한 이유

  • 코드를 작성하다보면 아래와 같은 상황을 마주할 수 있습니다.
  • 이는 빅리팩터링이라고 하며, 코드의 변경을 한 번에 많은 부분에서 수행하고, 그에 따른 컴파일 에러와 테스트 실패를 한꺼번에 해결하려는 접근 방식입니다.
  • 아래와 같은 리팩터링을 진행하면 테스트에 대한 부정적인 측면이 발생할 수 있습니다.

함수에 인자가 추가 됨

  • 함수를 사용하는 모든 곳에서 컴파일 에러 발생
  • 테스트 코드 포함 컴파일 에러를 해결
  • 테스트를 실행했더니 테스트가 깨지는 현상 발생
  • 디버깅 시도

인스턴스 변수의 타입 변경

  • String → Int로 인스턴스 타입 변경
  • 변수를 사용하는 모든 곳에서 컴파일 에러 발생
  • 테스트 코드 포함 커파일 에러 해결
  • 테스트를 실행했더니 테스트가 깨지는 테스트 케이스 발생
  • 디버깅 시도

점진적인 리팩토링 시도

  • 아래와 같은 코드가 있다고 가정합니다.
  • CardSelector의 match 함수는 기존에 가지고있는 cards와 새로 들어온 카드 더미를 비교하여 같은 수를 반환합니다.
class CardSelector(private val cards: List<Long>) {
    fun match(cardDumy: List<Long>): Int =
        cards.count { card ->
            cardDumy.contains(card)
        }
}

fun main() {
    CardSelector(listOf(1,2,3,4,5,6))
        .match(listOf(1,2,3))
}

  • 이 때 match() 메서드 인자 cardDumy를 CardDumy 클래스로 바꿔야 하는 상황이 발생했다고 가정하였습니다.
data class CardDumy(val cards: List<Long>)
  • 이를 진행하면, 해당 메서드를 사용하고 있는 테스트 코드와 의존 객체에서 컴파일 에러가 발생할 것입니다.
  • 이 때 아래의 단계로 점진적 리팩토링을 시도할 수 있습니다.

새로운 메서드 추가

  • 기존 메서드를 제거하지 않고 오버로드 메서드를 추가하면, 기능을 확장하면서 아직 기존 기능을 사용할 수 있습니다.
  • 이로 인해서 의존하는 클래스와 테스트 코드에서 컴파일 에러가 발생하지 않습니다.
class CardSelector(private val cards: List<Long>) {
    fun match(cardDumy: List<Long>): Int =
        cards.count { card ->
            cardDumy.contains(card)
        }
    
    // 아래 메서드는 사실상 match2가 됩니다.
    fun match(cardDumy: CardDumy): Int =
        cards.count { card ->
            cardDumy.cards.contains(card)
        }
}

기존 메서드 대체

  • 현재 match() 메서드를 오버로드 하고 있습니다.
  • 따라서 list 대신에 CardDumy 객체를 전달하도록 대체하면 컴파일 에러가 발생하지 않습니다.
fun main() {
		// match를 사용하는 부분에서 CardDumy를 전달
    CardSelector(listOf(1,2,3,4,5,6))
        .match(CardDumy(listOf(1,2,3)))
}

기존 메서드 제거

  • 더 이상 리스트를 받는 match 메서드는 사용되지 않으므로 기존 메서드를 제거할 수 있습니다.
  • 기존 메서드가 사용하지 않음을 뜻하는 회색처리가 된 것을 확인할 수 있습니다.
class CardSelector(private val cards: List<Long>) {
    fun match(cardDumy: CardDumy): Int =
        cards.count { card ->
            cardDumy.cards.contains(card)
        }
}
  • 이러한 단계를 반복해서 리팩터링을 진행하면 점진적인 리팩토링을 진행할 수 있습니다.

결론

  • 한 번에 많은 레거시 코드를 변경하려고 하면, 의도치 않은 오류나 컴파일 에러가 발생할 수 있습니다.
  • 점진적 리팩터링을 통해서 기존 메서드를 유지하고, 대체 메서드를 생성하여 기존 메서드를 삭제하는 과정을 거칩니다.
  • 빅 리팩터링 방식보다 리스크가 적고, 전체 코드베이스에 적은 영향을 미칠 수 있습니다.
  • 덕분에 안정적이고 예측 가능하며, 장기적으로 유지보수가 용이한 코드를 작성할 수 있었습니다 !!