본문 바로가기
Swift

[Swift] 프로젝트에 SRP와 CleanCode적용해보기

by Jimmy_iOS 2023. 11. 12.

CleanCode는 소프트웨어 개발에서 가독성과 유지보수성을 높이기 위해 적용되는 개발 방법론입니다.

CleanCode의 함수 파트를 읽으면서 제가 작성한 코드를 반성하게 되었는데요.

클린코드에서 함수를 깨끗하게 작성하기 위한 여러 방법들 중 가장 인상 깊었던 규칙 몇가지를 제 프로젝트에 적용해봤습니다.

가장 인상 깊었던 규칙 은 다음과 같습니다.

1. 함수는 작게 만들어라

2. 함수 내 모든 문장의 추상화 수준은 동일해야 한다

3. 서술적인 이름을 사용해라

4. 함수는 한 가지만 해야한다(SRP)

먼저 저를 반성하게 만든 문제의 코드를 보겠습니다.

Before

final class WorkoutLogTableViewCell: UITableViewCell {
    var workout: Workout? {
        didSet {
            guard let workout else { return }
            guard let exercise = RealmManager.shared.oldRealm.object(ofType: Exercise.self, forPrimaryKey: workout.exerciseReference) else { return }
            let bodyPartList = exercise.bodyPart.map { $0.localized }
            let bodyPartString = bodyPartList.joined(separator: ", ")
            let equipmentTypeString = exercise.equipmentType.localized
            var secondaryString: String {
                if equipmentTypeString == "none" {
                    return bodyPartString
                } else {
                    return bodyPartString + " / " + equipmentTypeString
                }
            }
            let weightFloat = workout.exerciseSets
                .filter {$0.isFinished}
                .map { $0.weight * $0.repetitionCount}
                .map { Float($0) }
                .reduce(0,+)
            let setCount = workout.exerciseSets.count
            let setFinishedCount = workout.exerciseSets
                .filter { $0.isFinished }
                .count
            let progression = Float(setFinishedCount) / Float(setCount)
            titleLabel.text(exercise.exerciseName)
            titleLabel.sizeToFit()
            secondaryLabel.text(secondaryString)
            let weightInTons = weightFloat > 999 ? weightFloat / 1000 : weightFloat
            let weightUnit = weightFloat > 999 ? "ton" : "kg"
            weightLabel.text = String(format: "%.0f", weightInTons) + " " + weightUnit
            setLabel.text(String(describing: setCount) + " set")
            progressLabel.text(" " + String(Int(progression * 100)) + "%")
            DispatchQueue.main.async {
                self.progressView.setProgress(progression, animated: true)
            }
        }
    }
    // ...
}

위의 코드는 UITableViewCellworkout 로직을 담고 있습니다.

ViewController에서 TableViewCell의 Data를 전달해주고 UI를 업데이트 해주는 로직인데요.

너무 길고 어떤 기능을 수행하는지 이해하기도 난해한 코드입니다.

위 코드를 리팩토링하기 위해 함수를 기능별로 분리하는 작업을 시도했습니다.

분리한 각 기능은 한 가지 일을 수행하는 작은 함수로 만들었고,

함수의 기능을 서술적인 이름으로 명시했습니다.

After

첫째로, workout 변수에 Workout 데이터가 입력됐을 때 어떤 기능을 수행할지 메서드 명을 통해 추론할 수 있게 되었습니다.

final class WorkoutLogTableViewCell: UITableViewCell {
    var workout: Workout? {
        didSet {
            guard let workout else { return }
            updateUI(with: workout)
        }
    }
    // ...
}

다음으로 updateUI 메서드가 어떤 기능을 수행하는지 각 기능별로 분리된 메서드명을 보면서 추론이 가능해졌습니다.

extension WorkoutLogTableViewCell {

    private func updateUI(with workout: Workout) {
        updateTitleLabel(with: exercise.exerciseName)
        updateSecondaryLabel(
						with: exercise.bodyPart, 
						equipmentType: exercise.equipmentType
				)
        updateWeightLabel(with: workout.exerciseSets)
        updateSetLabel(with: workout.exerciseSets)
        updateProgress(with: workout.exerciseSets)
    }
// ...
}
    

다음 updateProgressBar 메서드 내부에는 if else문이 있었습니다.

exerciseSets.isEmpty가 true일 때와 false일 때의 기능을 메서드로 분리하여

코드를 직관적으로 이해할 수 있도록 로직을 수정했습니다.

// ...
    private func updateProgressBar(with exerciseSets: List<ExerciseSet>) {
        if exerciseSets.isEmpty {
            updateProgressBarWhenEmpty()
        } else {
            updateProgressBarWhenNotEmpty(with: exerciseSets)
        }
    }
    
    private func updateProgressBarWhenEmpty() {
        progressLabel.text = "0 %"
        progressBar.setProgress(0, animated: true)
    }
    
    private func updateProgressBarWhenNotEmpty(with exerciseSets: List<ExerciseSet>) {
        let setCount = exerciseSets.count
        let setFinishedCount = exerciseSets.filter { $0.isFinished }.count
        let progression = Float(setFinishedCount) / Float(setCount)
        progressLabel.text = " " + String(Int(progression * 100)) + "%"
        DispatchQueue.main.async {
            self.progressBar.setProgress(progression, animated: true)
        }
    }
}

클린코드를 적용해 코드를 작성하면서

코드의 가독성을 높이는 동시에 유지 보수성이 올라가는 효과를 볼 수 있었습니다.

프로젝트의 작은 부분인 함수를 작게 만들면서 SRP를 적용해보니

객체 단위에서도 SRP를 적용해보면 좋을 것 같다는 생각을 하게 되었습니다.

짐핏 - 근육 회복율 페이지 리팩토링

얼마전 제작한 운동 기록앱인 짐핏에는 기록한 운동에서 사용한 근육을 날짜별로 저장하고

시간이 지남에 따라 근육 회복율을 보여주는 화면이 있습니다.

기존 프로젝트에서 근육 회복율을 보여주는 로직은 다음과 같습니다.

ImageBlender의 책임은 이미지를 합성하는 것 뿐만 아니라 Realm에서 운동 데이터를 추출하여 이미지를 합성합니다.

그러나 이러한 로직을 수행하는 객체의 이름이 ImageBlender이므로

시간이 지나면서 개발자가 로직을 수정해야 하는 경우에도

Realm에서 운동 데이터를 추출하는 기능이 있는지 알지 못하고

중복된 로직을 만들게 될 수 있으며, Realm에서 운동 데이터를 추출하는 기능이

별도로 필요하여 중복으로 만들어질 수 있습니다.

이와 같은 불상사를 예방하기 위해 프로젝트에 SRP(단일 책임 원칙)을 적용하면 로직은 다음과 같습니다.

이미지 합성 기능과 데이터 추출 기능을 별도의 객체로 분리하여 가독성 및 유지 보수성을 향상시킬 수 있었습니다.

따라서 다음과 같이 코드를 직관적으로 작성할 수 있습니다.

let exerciseInfoArray = exerciseInfoExtractor.getExerciseInfoArray(period: .threeDays)
let imageBlender = ImageBlender(exerciseInfos: exerciseInfoArray)
let (frontImage, backImage): (UIImage, UIImage) = imageBlender.getBlendedImage()

결론

Clean Code와 SRP를 적용하면 코드의 가독성과 유지보수성을 향상시킬 수 있으며,

작은 함수로 분리하고 명확한 이름으로 기능을 서술하여 코드의 이해도를 높일 수 있습니다.


Uploaded by N2T