[SOLID] 예제로 SOLID 원칙 파헤치기

JunHyeong Lee·2023년 5월 30일
0

SOLID

목록 보기
1/1
post-thumbnail

SOLID 원칙에 대한 개념들을 읽기만 하면 바로 까먹고 생각이 안나기 때문에 직접 예시를 만들어보며 이해해보도록 하였다.

적당한 예시일진 모르겠지만 내가 이해한 대로 작성해 보겠다.

달리기 게임만들기

먼저 간단하게 사자와 거북이가 달리는 게임을 만든다고 생각해보자.

class Line(
    var point: Int,
    var speed: Int,
    val name: String = "Line"
) {
	// 가속도
    val acceleration = 2

    fun move() {
        speed += acceleration
        point += speed
    }
    fun printPoint() {
    	println("$name: $point")
    }
}
class Turtle(
    var point: Int,
    val speed: Int,
    val name: String = "Turtle"
) {
    fun move() {
        point += speed
        println("turtle: $point")
    }
    fun printPoint() {
    	println("$name: $point")
    }
}

너무 예측이 쉬운 게임은 재미 없기에 가속도 설정을 하나 넣었고 출발지점도 각 동물마다 다르도록 해보았다.

class Game {
    lateinit var line: Line
    lateinit var turtle: Turtle
    val targetPoint: Int = DEFAULT_TARGET

    fun registerUser(line: Line, turtle: Turtle) {
        this.line = line
        this.turtle = turtle
    }

    fun start() {
        runBlocking {
            while (line.point < targetPoint && turtle.point < targetPoint) {
            	// 1초
                delay(1000)
                
                line.move()
                turtle.move()
                
                line.printPoint()
                turtle.printPoint()
            }
        }

        if (line.point < turtle.point) {
            println("Win turtle")
        } else if (line.point > turtle.point) {
            println("Win line")
        } else {
            println("Win line turtle")
        }
    }

    companion object {
        const val DEFAULT_TARGET = 100
    }
}

게임은 타겟 지점과 유저를 등록하고 게임을 시작하는 메소드로 구성되어 있다.
이제 게임을 작동시키면 된다.

fun main() {

    val game = Game()

    game.registerUser(Line(0, 10), Turtle(50, 2))

    game.start()

}

그냥 사자와 거북이의 달리기 시합 게임이라고 했을 때 위의 코드는 아무런 문제가 없다. 하지만 개발자라면 어떠한 상황에서든 대처할 수 있어야 한다. 만약 여기서 달팽이를 추가해야 한다고 해보자.

class Snail(
    var point: Int,
    val speed: Int,
    val name: String = "Snail"
) {
    fun move() {
        point += speed
    }
    fun printPoint() {
    	println("$name: $print")
    }
}
class Game {
    ...
    lateinit var snail: Snail
    ...
    
    fun registerUser(... snail: Snail) {
        ...
        this.snail = snail
    }
    fun start() {
        runBlocking {
            while (... && snail.point < targetPoint) {
                ...
                snail.move()
                snail.printPoint()
            }
        }
        ...
    }

달팽이 하나만 추가하더라도 수정해야할 코드가 산더미다. 이는 수정에 의해 닫혀있지 않기 때문에 OCP(Open-Closed Principle) 개방-폐쇄 원칙을 위반한다. 그냥 달팽이 클래스만 만들면 알아서 작동시키게 하고 싶은 욕구가 치솟는다. OCP 는 객체 지향 프로그램의 핵심 개념인 추상화(Abstraction)와 밀접한 관련이 있다. 그럼 어떻게 하면 될까??
바로 동물들을 User라는 클래스로 추상화 하는 것이다. 추상화(Abstraction)란 객체들의 공통된 속성이나 기능들을 추출하여 정의하는 것이다.

class Turtle(
	override var point: Int, 
    override val speed: Int, 
    override val name: String = "Turtle"
) : User()

abstract class User {
    abstract val name: String
    abstract var point: Int
    abstract val speed: Int

    open fun move() {
        point += speed
    }

    fun printPoint() {
        println("$name: $point")
    }
}

이렇게 추상화는, Turtle클래스 (저수준 클래스)가 User (고수준 클래스)를 의존하게 함으로써 DIP(Dependency Injection Principle) 의존성 역전 원칙을 지키는 예시 이기도 하다.

추상화한 추상클래스(abstract class) User를 다른 동물들의 상위 클래스로 구현한 다면 Game 코드는 다음과 같이 변경할 수 있다.

class Game {
    private val users = mutableListOf<User>()
    private val targetPoint: Int = DEFAULT_TARGET

    fun registerUser(user: User) {
        users.add(user)
    }

    fun start() {
        runBlocking {
            while (isFinished()) {
                delay(1000)
                moveUsers()
                printPointUsers()
            }
        }
        findWinner()
    }

    private fun isFinished() = users.all { user -> user.point < targetPoint }

    private fun moveUsers() {
        users.forEach { user -> user.move() }
    }

    private fun printPointUsers() {
        users.forEach { user -> user.printPoint() }
    }

    private fun findWinner() {
        println("Winner ${users.maxBy { user -> user.point }.name}!!")
    }

    companion object {
        const val DEFAULT_TARGET = 100
    }
}

여기서 users 리스트의 타입을 User로 선언했기 때문에 User를 상속 받은 클래스들을 추가할 수 있었다. 이는 다형성의 정의 중 '한 타입의 참조변수를 통해 여러 타입의 객체를 참조할 수 있도록 만든 것’ 을 지킨 예시이다.

class Rabbit(override val name: String, override var point: Int, override val speed: Int) : User()

이제 위와 같은 Rabbit 클래스만 만든다면 기존의 코드를 건드리지 않고 구현이 가능하다.
이러면 문제가 다 해결된 것 같지만 아니다. 사자 클래스를 보자.

class Line(
    override var point: Int,
    override var speed: Int,
    override val name: String = "Line"
) : User() {
    private val acceleration = 2

    override fun move() {
        speed += acceleration
        point += speed
    }
}

사자는 가속도가 있기 때문에 move 함수를 재정의(Override)해줘야 한다.
이렇게 되면 가속도가 있는 동물을 추가한다면 move 함수를 계속 재정의 해줘야 한다. 그리고 함수가 구현되어 있는 것을 재정의 하는 것 자체가 리스코프 치환 법칙을 위반하는 행위가 될 수 있다. 예를 들어

class Line(...) : User() {
    ...

    override fun move() {
        println("Line $point")
    }
}

이런식으로 부모의 의도와 맞지 않는 함수로 만들 수 있기 때문이다. 따라서 좀 더 개념을 분리해 보겠다.

interface User {
    val name: String
    var point: Int

    fun move()

    fun printPoint() {
        println("$name: $point")
    }
}

abstract class StraightUser: User {
    abstract val speed: Int

    final override fun move() {
        point += speed
    }
}

abstract class AccelerationUser: User {
    abstract var speed: Int
    abstract val acceleration: Int

    final override fun move() {
        speed += acceleration
        point += speed
    }
}

User를 인터페이스로 변경하고 더 최상위 클래스로 추상화 시켰다. 그리고 이를 상속 받는 추상 클래스들은 각각 자신들의 특성에 맞는 move 함수를 구현한다. 또한 아까와 같은 리스코프 치환 원칙을 위배하지 않도록 방지 하기 위해 final 키워드를 사용해 추상 클래스들을 구현하는 클래스들이 이를 재정의(Override)하지 않도록 강제하였다.
이제 추상 클래스를 구현 하고 있는 클래스들을 보자.

class Line(
    override var point: Int,
    override var speed: Int,
    override val name: String = "Line",
    override val acceleration: Int = 2
) : AccelerationUser()

class Turtle(
	override var point: Int, 
    override val speed: Int, 
    override val name: 
    String = "Turtle"
) : StraightUser()

이렇게 인터페이스와 추상 클래스와 같이 구조, 기반을 세우는 코드를 수정할 경우 많은 코드를 수정해야 한다. 그만큼 신중하고 철저하게 만들자.

전체 코드이다.

interface User {
    val name: String
    var point: Int
    fun move()

    fun printPoint() {
        println("$name: $point")
    }

}

abstract class StraightUser: User {
    abstract val speed: Int
    final override fun move() {
        point += speed
    }

}

abstract class AccelerationUser: User {
    abstract var speed: Int
    abstract val acceleration: Int
    final override fun move() {
        speed += acceleration
        point += speed
    }

}

class Line(
    override var point: Int,
    override var speed: Int,
    override val name: String = "Line",
    override val acceleration: Int = 2
) : AccelerationUser()

class Turtle(override var point: Int, override val speed: Int, override val name: String = "Turtle") : StraightUser()
class Snail(override var point: Int, override val speed: Int, override val name: String = "Snail") : StraightUser()
class Rabbit(override val name: String, override var point: Int, override val speed: Int) : StraightUser()

class Game {
    private val straightUsers = mutableListOf<User>()
    private val targetPoint: Int = DEFAULT_TARGET

    fun registerUser(user: User) {
        straightUsers.add(user)
    }

    fun start() {
        runBlocking {
            while (isFinished()) {
                delay(1000)
                moveUsers()
                printPointUsers()
            }
        }
        findWinner()
    }

    private fun isFinished() = straightUsers.all { user -> user.point < targetPoint }

    private fun moveUsers() {
        straightUsers.forEach { user -> user.move() }
    }

    private fun printPointUsers() {
        straightUsers.forEach { user -> user.printPoint() }
    }

    private fun findWinner() {
        println("Winner ${straightUsers.maxBy { user -> user.point }.name}!!")
    }

    companion object {
        const val DEFAULT_TARGET = 100
    }
}

fun main() {

    val game = Game()

    game.registerUser(Line(0, 10))
    game.registerUser(Turtle(50, 2))
    game.registerUser(Snail(70, 1))

    game.start()
}
profile
Android Developer

0개의 댓글