본문 바로가기
프로그래밍/테스트

[kotlin] 레거시(legacy) 코드 리팩토링하기 1편 - 테스트 코드를 작성해야하는 이유

by 사바라다 2022. 4. 16.

안녕하세요. 서비스의 시간이 오래되면 레거시 코드에 대해 고민이 생기기 마련입니다. 특히 이제는 퇴사한 사람이 남기고간 장황하고 이유를 알 수 없는 코드에 수정이 필요하게 되면 덜컥 겁이나기 시작합니다. 그런 코드들은 생성된 시간이 오래되면 오래될수록 공포감도 커지고 건들고 싶지 않아집니다. 하지만 그렇다고 수정을 안할수도 없습니다. 무작정 수정하게되면 실수 등에 의해서 잘 되던게 오히려 잘못 동작할수도 있습니다. 때문에 우리는 수정하기에 앞서 어느정도 안전장치를 만들어둘 필요가 있습니다. 그것이 바로 테스트 코드를 작성하는 것입니다.

오늘은 레거시 코드에 테스트 코드를 작성하고 커버리지를 올리는 방법에 대해서 알아보는 시간을 가져보도록 하겠습니다.

레거시 코드에 유닛 테스트 코드를 추가하면서 얻을 수 있는 이득

레거시 코드에 유닛 테스트 코드를 추가해서 이용하면 어떤 이득이 있을까요? 가장 눈에 띄게 얻을 수 있는 효과는 크게 코드 분석적인 이유, 그리고 실수를 방지해준다는 이유가 있습니다.

첫번째, 테스트 코드를 작성하는 과정에서 레거시 코드에 대한 이해도가 상승하게 됩니다. 어떤 코드든 수정을 위해서는 기존 코드에 대한 분석이 필요합니다. 이러한 분석을 하는 방법은 코드를 눈으로 보고 이해하는 것도 있습니다만 이럴경우 놓칠 수 있는 부분이 있습니다. 그런데 만약 테스트 코드를 작성하고 있는 중 이라면 디버깅 모드를 이용한다면 한줄씩 넘어가며 어떤 식으로 로직이 실행되는지 실제로 경험할 수 있습니다. 이는 분명히 눈으로 훑어 보는 것보다 코드를 이해하는데 도움이 됩니다. 그렇기 때문에 분석적인 부분에 있어서 큰 효과를 얻을 수 있다고 생각합니다.

두번째, 테스트 코드는 리팩토링 중 일어날 수 있는 실수를 바로잡아줄 수 있습니다. 레거시 코드 활용 전략에 의하면 리팩토링 중 개발자가 가장 많이 하는 실수는 아래와 같습니다.

  • 추출한 메서드에 변수를 전달하는 것을 잊기 쉽다. 대부분의 경우 컴파일러가 변수의 누락된 사실을 알려주지만, 지역 변수여야 한다고 생각한 나머지 신규 메서드 내에 선언해버리는 실수를 저지를 수 있다.
  • 추출된 메서드에 붙인 이름이 상속을 사용하고 있어서 기초(Base) 클래스 내에 있는 동일한 이름의 메서드를 은폐하거나 재정의할 가능성이 있다.
  • 매개변수를 전달하거나 반환 값을 대입할 때 실수를 저지르기 쉽다. 잘못된 값을 반환하는 것과 같은 정말 어리석은 일을 저지르기도 한다. 좀 더 포착하기 힘든 경우로, 신규 메서드에서 잘못된 타입을 반환하거나 전달받을 가능성도 있다.

쉽게 저지를 수 있는 실수 예시

코드로도 쉽게 일어날 수 있는 실수를 한번 보도록 하겠습니다. 아래와 같은 코틀린 코드가 있습니다. 이 코드를 다른 레거시 클래스에서 주입받아서 사용한다고 했을 때 match라는 메서드를 사용함에 있어서 실수가 발생할 수 있습니다.

class TripRepository {
    fun match(tripId: Long, userId: Long) {
        // ..[로직]..
    }
}

아래 코드를 보도록 하겠습니다. 아래 코드는 잘못된 것입니다. 왜냐구요? 잘 보시면 userId와 tripId의 순서가 바뀐것을 확인하실 수 있습니다. 이런 실수들은 컴파일 타임에 잡아줄 수 없습니다. namedParameter를 사용하더라도 그것은 가이드 일 뿐 컴파일 타임에 잡아주지 않습니다.

tripRepository.match(userId, tripId)
// tripRepository.match(tripId, userId) 여야 정상

하지만 만약 테스트 코드가 있었다면 이런 실수는 컴파일 타임에 unitTest를 돌리면서 잡아낼 수 있습니다.

레거시 샘플 코드

그렇다면 이제 레거시 코드에 한번 테스트를 작성해보도록 하겠습니다. 먼저 아래 코드는 Testing and Refactoring Legacy Code에 있던 Java 샘플 코드를 Kotlin으로 포팅한 것임을 알려드립니다.

@Throws(UserNotLoggedInException::class)
fun getTripByUser(user: User): List<Trip> {
    var tripList: List<Trip> = ArrayList()
    val loggedUser: User = UserSession.getInstance().getLoggedUser()
    var isFriend = false
    return if (loggedUser != null) {
        for (friend in user.getFriends()) {
            if (friend == loggedUser) {
                isFriend = true
                break
            }
        }
        if (isFriend) {
            tripList = tripRepository.findTripsByUser(user)
        }
        tripList
    } else {
        throw UserNotLoggedInException()
    }
}

intellij에서 코드 커버리지 보는 방법

테스트 코드가 정상적으로 레거시 코드를 커버하는지 판단은 coverage를 통해서 진행합니다. intellij를 사용하고 있으시면 intellij의 기본으로 제공하는 기능을 사용하시면 될것 같으며 그렇지 않다면 jacoco 등을 이용하는 것을 추천드립니다. 아래 이미지는 intellij에서 기본으로 제공하는 코드 커버리지를 보는 방법을 이용하는 방법입니다. 테스트 코드를 실행하는 UI를 클릭하시면 Run with Coverage가 있습니다.

 

이를 클릭하시면 테스트 코드가 어느정도 커버하고 있는지를 보여주며 커버되고 있지 않는 부분은 빨간색으로, 커버되고 있는 부분은 초록색으로 표시됩니다. 테스트를 작성하며 어느정도 커버하고 있는지를 바로바로 확인할 수 있습니다.

 

x

마무리

오늘은 이렇게 레거시 코드를 수정하기 전 작업으로 테스트 코드를 작성해야하는 이유와 작성하기 위한 방법, 그리고 어느정도의 테크닉에 대해서 알아보았습니다.

그래서 너는 어느정도 테스트 코드를 만들고 리팩토링을 하느냐고 물어보시면 리팩토링 하는 부분에 있어서는 최대한 라인 커버리지로 100%는 채우려고 합니다. 무조건 버그가 안생긴다는 것은 아니지만 확실히 내가 실수할 수 있는 부분에 있어서 방지의 효과는 경험도 해보았고 효과적이기 때문입니다.

다음 시간에는 실제로 테스트코드를 작성하고 위 코드의 테스트 커버리지를 100%로 맞춰보는 작업을 진행해보도록 하겠습니다.

감사합니다.

참조

[1] https://www.youtube.com/watch?v=LSqbXorkyfQ

[2] 레거시 코드 활용 전략

 

댓글