[TIL]Kotlin 계산기 구현하기 2

지혜·2023년 11월 30일

Android_TIL

목록 보기
8/70

✏231130 목요일 TIL(Today I learned) 오늘 배운 것

📖Lv3.클래스의 책임(단일책임원칙)

package com.example.mycalculator

class AddOperation {
    fun addOperation(num1 : Double,num2 : Double) = num1 + num2
}
package com.example.mycalculator

class SubtractOperation {
    fun substractOperation(num1 : Double,num2 : Double) = num1 - num2
}
package com.example.mycalculator

class MultiplyOperation {

    fun multiplyOperation(num1 : Double,num2 : Double) = num1 * num2
}
package com.example.mycalculator

class DivideOperation {
    fun divideOperation(num1 : Double,num2 : Double) = num1 / num2
}

package com.example.mycalculator

class Calculator {//각 클래스의 메서드를 사용하는 역할만 하고 있다.

    fun addOperation(num1: Double, num2: Double) : Double = AddOperation().addOperation(num1, num2)

    fun subtractOperation(num1: Double, num2: Double) = SubtractOperation().substractOperation(num1, num2)

    fun multiplyOperation(num1: Double, num2: Double) = MultiplyOperation().multiplyOperation(num1, num2)

    fun divideOperation(num1: Double, num2: Double) = DivideOperation().divideOperation(num1, num2)

}

package com.example.mycalculator

fun main() {

    var select : Int = 0
    var calc : Double = 0.0
    var num1 : Double = 0.0
    var operator : String = ""
    var num2 : Double = 0.0

    while(select != 3) {
        println("")
        println("원하는 계산을 선택하세요")
        println("[1] 새로 계산하기 [2] 이어서 계산하기 [3] 그만두기 ")
        select = readLine()!!.toInt()

        if(select == 1) {
            println("첫번째 수를 입력하세요.")
            num1 = readLine()!!.toDouble()
            println("수행할 연산자를 고르세요 : +  -  *  /")

        } else if (select == 2) {
            num1 = calc
            println("")
            println("첫번째 수는 $calc 입니다.")
            println("")
            println("추가로 수행할 연산자를 고르세요 : +  -  *  /")

        } else if (select ==3) {
            println("계산기를 종료합니다.")
            break
        }

        operator = readLine()!!

        println("두번째 수를 입력하세요.")
        num2 = readLine()!!.toDouble()

        when(operator) {
            "+" -> {
                calc = Calculator().addOperation(num1,num2)
            }

            "-" -> {
                calc = Calculator().subtractOperation(num1,num2)
            }

            "*" -> {
                calc = Calculator().multiplyOperation(num1,num2)
            }

            "/" -> {
                if(num2 == 0.0) {
                    println("0으로 나눠지지 않습니다.")
                } else {
                    calc = Calculator().divideOperation(num1, num2)
                }
            }
        }

        println("${num1} ${operator} ${num2} =  ${calc} 입니다.")

    }
}
  • Lv3 : AddOperation(더하기), SubstractOperation(빼기), MultiplyOperation(곱하기), DivideOperation(나누기) 연산 클래스를을 만든 후 클래스간의 관계를 고려하여 Calculator 클래스와 관계를 맺기

- [객체 지향 프로그래밍 설계원칙] 단일 책임 원칙 SRP (Single Responsibility Principle)

  • 객체는 단하나의 책임(기능)만 가져야 한다.
    => 기능 변경(수정)시에 파급효과가 얼마인지가 기준 척도.
    => 분산시킬수록 파급효과가 적어지고, 유지보수에 용이해진다.

- 좋은 설계란? 결합도가 낮고, 응집도가 높은 설계.

  • 결합도 : 하나의 클래스가 다른 클래스와 얼마나 많이 연결 되어 있는지에 대한 표현.
    결합도가 낮다. = 조금 연결되었다.
    => 결합도가 낮은 프로그램일 수록 유지보수가 쉬워진다.

  • 응집도 : 클래스에 포함된 내부 요소들이 하나의 책임/목적을 위해 연결되어있는 연관된 정도.
    응집도가 높다 = 변경대상/범위가 명확해진다.
    => 응집도가 높은 프로그램일수록 유지보수가 쉬워진다.

📖Lv2와 비교하여 개선된 점

  • 위의 3가지의 정의들을 놓고 생각해 봤을 때, Lv2의 Class Calculator는 혼자서 사칙연산의 기능을 전부 책임지고 있기 때문에 응집도는 높으나, 단일 책임 원칙에는 위배되고 있었다.

  • 단일 책임 원칙을 지키는 것은 그렇게 어렵지 않다. 클래스 당 하나의 기능만을 구현하면 되기 때문이다. 이렇게 분리 시켜 놓은 기능들은 변화가 필요할 때 수정을 하면 다른 부분에 영향이 적게 갈 것이다.

  • Lv3에서 단일 책임 원칙에 의해 각각의 연산 기능들을 분리해서 결합도를 낮췄다. 이렇게 결합도를 낮추면 지금은 기능이 복잡하지 않기 때문에, 문제가 생겼거나 변화를 줘야할 때 쉽게 찾아서 유지보수가 가능하지만, 더 많은 기능들이 생긴다면, 흩어져 있는 기능들을 일일이 찾아가며 수정을 해야할 것이다.

  • 그래서 Class Calculator를 통해 높은 응집도를 유지하면서도, 각 기능을 분산시켜 결합도를 낮춰서 유지보수하기에 유리하도록 만들 수 있게 되었다.


📖Lv4.클래스간의 의존성(의존성역전원칙)

package com.example.mycalculator

abstract class AbstractOperation {//추상클래스
    abstract fun operation(num1 : Double, num2 :Double) :Double

}

package com.example.mycalculator

class AddOperation : AbstractOperation() {//사칙연산클래스 더하기

    override fun operation(num1: Double, num2: Double): Double {
        return num1 + num2
    }
}
package com.example.mycalculator

class SubtractOperation : AbstractOperation() {//사칙연산클래스 빼기

    override fun operation(num1: Double, num2: Double): Double {
        return num1 - num2
    }
}
package com.example.mycalculator

class MultiplyOperation : AbstractOperation() {//사칙연산클래스 곱하기

    override fun operation(num1: Double, num2: Double): Double {
        return num1 * num2
    }
}
package com.example.mycalculator

class DivideOperation : AbstractOperation() {//사칙연산클래스 나누기

    override fun operation(num1: Double, num2: Double): Double {
        return num1 / num2
    }
}

package com.example.mycalculator

class Calculator(private val operation: AbstractOperation)  {
//상위모듈인 AbstractOperation을 의존하는 Calculator클래스

	//어쨌든 새로 만든 펑션이므로 생성자 설정과 반환타입 설정을 잘 해줘야한다.
    fun operation(num1 : Double,num2 : Double) : Double {
		
        return operation.operation(num1, num2)
    }

}

package com.example.mycalculator

fun main() {

    var select : Int = 0
    var calc : Double = 0.0
    var operator : String = ""
    var num1 : Double = 0.0
    var num2 : Double = 0.0

	//사칙연산 클래스를 변수에 담았다.
    val add = AddOperation()
    val subtract = SubtractOperation()
    val multiply = MultiplyOperation()
    val divide = DivideOperation()

	//변수에 담은 사칙연산 클래스를 Calculator에 주입해서 사용하도록 했다.
    val addOperator = Calculator(add)
    val subtractOperator = Calculator(subtract)
    val multiplyOperator = Calculator(multiply)
    val divideOperator = Calculator(divide)

    while(select != 3) {
        println("")
        println("원하는 계산을 선택하세요")
        println("[1] 새로 계산하기 [2] 이어서 계산하기 [3] 그만두기 ")
        select = readLine()!!.toInt()

        if(select == 1) {
            println("첫번째 수를 입력하세요.")
            num1 = readLine()!!.toDouble()
            println("수행할 연산자를 고르세요 : +  -  *  /")

        } else if (select == 2) {
            num1 = calc
            println("")
            println("첫번째 수는 $calc 입니다.")
            println("")
            println("추가로 수행할 연산자를 고르세요 : +  -  *  /")

        } else if (select ==3) {
            println("계산기를 종료합니다.")
            break
        }

        operator = readLine()!!

        println("두번째 수를 입력하세요.")
        num2 = readLine()!!.toDouble()

        when(operator) {
            "+" -> {
                calc =addOperator.operation(num1,num2)
            }

            "-" -> {
                calc = subtractOperator.operation(num1,num2)
            }

            "*" -> {
                calc = multiplyOperator.operation(num1,num2)
            }

            "/" -> {
                if(num2 == 0.0) {
                    println("0으로 나눠지지 않습니다.")
                } else {
                    calc =  divideOperator.operation(num1,num2)
                }
            }
        }

        println("${num1} ${operator} ${num2} =  ${calc} 입니다.")

    }
}
  • Lv4 : AddOperation(더하기), SubtractOperation(빼기), MultiplyOperation(곱하기), DivideOperation(나누기) 연산 클래스들을 AbstractOperation라는 클래스명으로 만들어 사용하여 추상화하고 Calculator 클래스의 내부 코드를 변경합니다.

[추상클래스]

  • 추상클래스는 abstract 키워드로 선언한다. 필수적으로 구현해야할 메서드가 있을 때 사용한다.(단일 상속만 가능)

  • 추상클래스나 추상메서드,추상프로퍼티는 상속을위해 open키워드가 필요없다.
    (다만 open class와 다르게 단독으로 객체가 되어 사용할 수 없다.)

  • 추상클래스는 인터페이스와 다르게 프로퍼티의 초기화도 가능하다. 즉, 공통 프로퍼티와 메서들을 미리 만들어 둘 수 있다. (여기서는 필요없어서 굳이 쓰지는 않았다.)

  • 추상클래스에서 abstract로 정의한 프로퍼티나 메소드들은 자식클래스에서 반드시 재정의 되어야한다.

  • ※참고로 추상클래스와 인터페이스가 다형성을 구현하는데 유용하다는 점에서 자주 비교가 되는데, 단일모듈에서는 인터페이스 사용을 지양하는 것이 좋다. 인터페이스는 애초에 다른 모듈을 위한 의사소통방식이다.

  • 추상화는 핵심적인 요소를 강조하고 불필요한 세부사항을 숨기는데 초점이 맞춰져 있고, 상속은 이미 존재하는 클래스의 기능을 확장,수정(재사용성 및 유지보수용이)하는데 초점이 맞춰져있다.

[객체 지향 프로그래밍 설계원칙] 의존 역전 원칙 DIP (Dependency Inversion Principle)

  • 의존관계를 맺을 때, 변화하기 쉬운 하위 모듈에 의존하기보다는 변화하기 어려운 (거의 변화가 없는) 것에 의존하라는 원칙.
  • 자식클래스에서의 변경이 부모클래스에 영향을 주어선 안된다.

📖Lv3와 비교하여 개선된 점

  • Lv3에서 class Calculator는 추상클래스 없이 각 연산 클래스들을 직접 의존하고 있다. 여기서 Calculator클래스는 각 연산클래스들보다 상위 계층(더 많은 정보가 있음)이다. 만약 여기서 class AddOperation에 변화가 생긴다면, 상위 계층인 Calculator클래스도 함께 변경이 될 것이다.
  • 그러나 추상 클래스가 생기면서 class Calculator와 의존관계를 맺으면, 하위 클래스인 class AddOperation을 비롯한 사칙연산 클래스를 수정하더라도, class Calculator는 변함이 없다.
    =>결국, 수정 및 변화가 발생 시 다른 클래스에 영향을 적게 주게 되어 유지 보수에 아주 용이해지는 것이다.

📖Lv4.발생 문제 및 해결 및 느낀점

  1. 일단 상위모듈인 추상클래스에 의존관계를 주입한다는게 너무 어려웠다. 보통 추상클래스보다 인터페이스의 경우를 많이 사용해서 추상클래스 예시를 찾기 너무 어려웠다. 구글링을 열심히 해봤지만 바로 이해하고 적용하기에는 무리가 있어서, 오랜만에 챗GPT를 사용했다.
    =>[Kotlin] 추상클래스 의존 역전 원칙 예시 : 일단 이 예시를 보고 그대로 따라하는 것을 목표로 했다.

  1. 따라하는 도중에 중간 리뷰 시간이 약속되어서 마무리를 하지 못했다.

    package com.example.mycalculator
    
    class Calculator(private val operation: AbstractOperation)  {
    
       fun operation() {
    	   operation.operation(num1 = 0.0, num2 = 0.0)
       }
    }

    이 상태에서 일단 오류는 없어서 계산을 돌렸는데, 메인에서 펑션을 실행하면 전부 0.0값이 나왔다. 나는 저 num1 = 0.0 , num2= 0.0 부분이 메인 펑션과 operation을 이어주는 부분이라고 생각했는데, fun opration( )에 생성자를 걸어야 메인 펑션에서 기능을하는거지, 위의 코드는 그냥 num1= 0.0이라고 고정값을 준거나 다름없다고 팀장님이 알려주셨다.
    => 팀장님의 조언으로 fun operation에 생성자를 걸어주니, 이 모습을 완성할 수 있었다.

    package com.example.mycalculator
    
    class Calculator(private val operation: AbstractOperation)  {
    
       fun operation(num1 : Double,num2 : Double) : Double {
    
           return operation.operation(num1, num2)
       }
    }

    +그리고 이름을 너무 다 비슷하게 설정해 놓아서 헷갈리는 것도 한 몫하는 것 같다. 설명하기도 애매해지고.. 개발자는 이름 설정하는데 고민이 한세월이라더니, 좀 더 좋은 이름을 붙일 수 있도록 노력해야겠다..


  1. 느낀점 : 단일클래스원칙부터 의존역전원칙 까지 유지보수를 용이하게 하고 파악을 쉽게 도와줄수있도록 하기 위한 객체지향프로그래밍 설계의 기본 원칙이라는 것을 배웠다. 그냥 말로만 보기쉬운코드, 이해하기좋은코드 했었지 원칙을 지켜서 설계를 하는 것은 생각보다 어려운 것 같다. 나중에 더 코드가 복잡해질 때 이러한 원칙을 최대한 살릴 수 있도록 해야겠다. 객체 지향 프로그래밍 설계는 기본 5원칙(SOLID)으로, 오늘은 그 중 2가지를 배웠다. 나머지 원칙들도 공부를 해서 한번 제대로 정리를 해야겠다.

[피드백]

  • readline( ) 에서 입력받을 때 !!는 안정성 원칙으로 사용하지 않는 추세라고 사용하지 않는 방법을 고민해보라고 하셨다.
    => !!사용을 지양해야한다고는 들었는데, 다음부터는 차라리 변수 타입에 ?를 넣는 방법을 써봐야겠다.
  • operator는 코틀린 예약어라서 변수명으로 적합하지 않다고 하셨다.
    => 이건.. 생각도 못한 부분이라 앞으로 변수명을 작성할 때 좀 더 주의해야겠다.
  • 문자열 내의 단일 변수 출력에 대해, 중괄호를 생략하는 것을 추천하셨다.
    => 안드로이드스튜디오에서 코드 스타일 및 컨벤션 가이드를 제공한다고 알려주셨는데, 이것도 처음.. 알았다. 앞으로 적극 참고해야곘다.
profile
파이팅!

0개의 댓글