안드로이드와 앱/프로젝트

개인 프로젝트 [콘솔형 계산기]

정혜현 2024. 6. 5. 13:47

계산기 만들기 

계산기 앱을 만들라는 줄 알고 조건이 잘 이해되지 않아 매니저님께 확인했는데 코드로만 계산기를 만드는 거였고 제약조건이 있는 게 아니었다. 너무 어렵게 생각하고 있었다. 

Q. 입력값에 대해... 정수만? 정수 실수 모두? 입력개수나 입력방법은?

A. 입력에 특정한 제한은 없었다.

Q. '-1을 입력 할 때까지 계산 반복하기 (1번 +, 2번 -, 3번 *, 4번 /, 5번 %)' 의 의미?

A. 반복한다는 의미가 '자동으로 무한반복한다'는 해석돼서 1을 입력하면 해당 연산을 무한반복 시키는 건지, 각 연산을 돌아가며 반복하라는건지 의문이었는데 무한반복이 아니었고, 명령어였다. 1을 입력하면 더하기가 시행되도록 하는 거였다. 

 

입력값은 사용자의 타이핑으로 받기로 하고 각 레벨에 맞는 코드를 작성해보자.

var dataNumber = readLine()!!.toInt()

 

 

 

 

 

 

 


 

Lv1 Calculator 클래스 생성

조건 : 사칙연산을 수행하는 클래스 생성하기

 

첫 시작은 기본형태에 충실하게 만들었다. 

fun main() {
}

class Calculator(a:Int, b:Int) {
	//더하기
	fun add(a:Int, b:Int): Int {
	var addResult = a + b
	return addResult}
	//빼기
	fun subtract(a:Int, b:Int): Int {
	var subtractResult = a - b
	return subtractResult}
	//곱하기
	fun multiple(a:Int, b:Int): Int {
	var multipleResult = a * b
	return multipleResult}
    //나누기
	fun divide(a:Int, b:Int): Int {
	var divideResult = a / b
	return divideResult}
}

 

코틀린에서는 함수의 마지막 줄을 반환 값으로 하기 때문에 return을 생략 가능하고, 한줄이므로 중괄호도 생략 가능하다. Lv3에서 변경됐다.

 

 

 

 

 

 

 

Lv2 추가연산, 나머지연산자

조건 : Lv1 계산기에서 출력한 값에 추가 연산하기(1을 눌러야 추가연산 종료), 나머지 연산자(%) 추가하기

 

 

1. 추가연산

클래스의 개념이 어려워서 일단 주석처리하고 함수호출로 시도했고 첫 조건인 추가연산을 우선순위로 두었다. 나머지 연산자를 추가하는 건 간단하니까 조건과 반복을 먼저 처리하고 싶었다. 클래스 스코프 안은 각각의 값을 따로 담을 필요가 없어보여서 좀 더 간단하게 바꿨다. 

fun main() {

    println("첫번째 숫자를 입력해주세요.")
    var num1 = readLine()!!.toInt()

    println("${num1}에 무엇을 해드릴까요?")
    println("1 : 더하기")
    println("2 : 빼기")
    println("3 : 곱하기")
    println("4 : 나누기")
    println("-1 : 그만하기")
    var operator = readLine()!!.toInt()

    println("두번째 숫자를 입력해주세요.")
    var num2 = readLine()!!.toInt()

      while(operator != -1) {
       when (operator) {
            1 -> num1 = add(num1, num2)
            2 -> num1 = subtract(num1, num2)
            3 -> num1 = multiple(num1, num2)
            4 -> num1 = divide(num1, num2)
            -1 -> break
        }
    println("계산결과는 ${num1} 입니다.")
	}
}
//class Calculator(a:Int, b:Int) {
    //더하기
    fun add(a:Int, b:Int): Int {
        return a + b}
    //빼기
    fun subtract(a:Int, b:Int): Int {
        return a - b}
    //곱하기
    fun multiple(a:Int, b:Int): Int {
        return a * b}
    //나누기
    fun divide(a:Int, b:Int): Int {
        return a / b}
//}

 

조건문으로는 when, 반복문으로는 while을 택했다. 

반복문 for와 while
for : 일반적으로 반복 횟수가 정해져 있는 경우, 컬렉션, for (요소 in 컬렉션)
while : 참인 경우 반복, while (조건) {증감식}

 

-1이 아닐 때까지 반복하고 싶어서 조건에 적었고 -1일 때는 멈추고 싶어서 break를 적었는데 원하는대로 돌아가지 않았다.

 

분석한 세가지 원인을 바탕으로 코드를 수정했다. 

  • 반복문의 위치가 잘못됐다.
    연산자 선택부터 반복시키고 싶으니까 while을 println("${num1}에 무엇을 해드릴까요?")앞으로 올렸더니 operator변수의 선언이 되어있지 않아 불가능했다. 그렇다고 operator변수를 while 위로 바꾸니 입력창이 먼저 뜨고 설명이 나중에 나와서 순서가 맞지 않다.
  • 선언과 동시에 값을 담지 않아도 된다.
    operator를 최상단에 초기화하고 선언해두니 while을 원하는 위치에 두고 시작할 수 있게 됐다. 보기 편하게 다른 값들도 같이 선언해뒀다.
  • break 위치가 잘못됐다. 
    조건문이 아니라 반복문을 멈추려는 용도니까 while에 써야한다. if문을 추가해 break 조건을 걸었다. 
fun main() {

    var num1 : Int = 0
    var num2 : Int = 0
    var operator : Int = 0

    println("첫번째 숫자를 입력해주세요.")
    num1 = readLine()!!.toInt()

    while (operator != -1) {
        println("${num1}에 무엇을 해드릴까요?")
        println("1 : 더하기")
        println("2 : 빼기")
        println("3 : 곱하기")
        println("4 : 나누기")
        println("5 : 나머지 구하기")
        println("-1 : 그만하기")
        operator = readLine()!!.toInt()
    
        if(operator == -1) {
            println("계산을 종료합니다.")
            break
            }
    
        println("두번째 숫자를 입력해주세요.")
        num2 = readLine()!!.toInt()
    
        when (operator) {
            1 -> num1 = add(num1, num2)
            2 -> num1 = subtract(num1, num2)
            3 -> num1 = multiple(num1, num2)
            4 -> num1 = divide(num1, num2)
            5 -> num1 = remainder(num1, num2)
           -1 -> operator
            }
        println("계산결과는 ${num1} 입니다.")
    }
}

 

 

 

 

2. 나머지 연산자

while문 조건에는 true를 적으니 operator 없이 반복할 수 있게 돼서 코드를 줄이고자 변수들은 다시 선언과 동시에 값을 담기로 했다. 나머지 연산자를 추가하면서 모두 한줄짜리 계산식이므로 중괄호를 빼고 간결하게 바꿨다. 클래스의 주석을 지우고 함수를 호출하던 부분은 생성자와 클래스 내 메소드를 호출하도록 바꿨는데, 동시에 할당하고 있으니 코드가 좀 지저분해보인다. 

fun main() {

	println("첫번째 숫자를 입력해주세요.")
    var num1 = readLine()!!.toInt()

    while (true) {
        println("${num1}에 무엇을 해드릴까요?")
        println("1 : 더하기, 2 : 빼기, 3 : 곱하기, 4 : 나누기, 5 : 나머지 구하기, -1 : 그만하기")
        var operator = readLine()!!.toInt()

        if(operator == -1) {
            println("계산을 종료합니다.")
            break
            }

        println("두번째 숫자를 입력해주세요.")
        var num2 = readLine()!!.toInt()

        when (operator) {
            1 -> num1 = Calculator(num1, num2).add(num1, num2)
            2 -> num1 = Calculator(num1, num2).subtract(num1, num2)
            3 -> num1 = Calculator(num1, num2).multiple(num1, num2)
            4 -> num1 = Calculator(num1, num2).divide(num1, num2)
            5 -> num1 = Calculator(num1, num2).remainder(num1, num2)
           -1 -> operator
            }
        println("계산결과는 ${num1} 입니다.")
    }
}




class Calculator(a: Int, b: Int) {
    //더하기
    fun add(a:Int, b:Int) = a + b
    //빼기
    fun subtract(a:Int, b:Int) = a - b
    //곱하기
    fun multiple(a:Int, b:Int) = a * b
    //나누기
    fun divide(a:Int, b:Int) = a / b
    //나머지 구하기
    fun remainder(a:Int, b:Int) = a % b
}

 

인스턴스화해두니 훨씬 깔끔해졌다. 

var calculator = Calcualtor(0,0)
        
when (operator) {
    1 -> num1 = calculator.add(num1, num2)
    2 -> num1 = calculator.subtract(num1, num2)
    3 -> num1 = calculator.multiple(num1, num2)
    4 -> num1 = calculator.divide(num1, num2)
    5 -> num1 = calculator.remainder(num1, num2)
   -1 -> operator
}

 

 

 

 

 

Lv3 클래스간 관계 설정

조건 :  사칙연산 클래스를 각각 만든 후 Calculator 클래스와 관계맺기

세부사항 : 관계를 맺은 후 필요하다면 Calculator 클래스의 내부 코드를 변경하기, 나머지 연산자(%)기능은 제외, Lv2 와 비교하여 개선된 점 생각해보기(hint. 단일책임 원칙)

단일 책임 원칙 SRP Single Responsibility Principle
하나의 기능 또는 책임만 가져야 한다는 것을 의미. 유지보수성, 확장성, 재사용성, 가독성을 높인다. 

 

사칙연산 클래스를 만들어 각 클래스의 개성을 살렸다.  

fun main() {

    var num1 : Int = 0

    println("첫번째 숫자를 입력해주세요.")
    num1 = readLine()!!.toInt()

    while (true) {
        println("[${num1}]에 무엇을 해드릴까요?")
        println("1 : 더하기, 2 : 빼기, 3 : 곱하기, 4 : 나누기, 5 : 나머지 구하기, -1 : 그만하기")
        var operator = readLine()!!.toInt()

        if(operator == -1) {
            println("계산을 종료합니다.")
            break
            }

        println("두번째 숫자를 입력해주세요.")
        var num2 = readLine()!!.toInt()

        var addOperation = AddOperation(0,0)
        var subtractOperation = SubtractOperation(0,0)
        var multipleOperation = MultipleOperation(0,0)
        var divideOperation = DivideOperation(0,0)

        when (operator) {
            1 -> {//더하기
                num1 = addOperation.add(num1, num2)
                addOperation.printResult(num1)}
            2 ->{//빼기
                num1 = subtractOperation.subtract(num1, num2)
                subtractOperation.printResult(num1)}
            3 ->{//곱하기
                num1 = multipleOperation.multiple(num1, num2)
                multipleOperation.printResult(num1)}
            4 ->{//나누기
                num1 = divideOperation.divide(num1, num2)
                divideOperation.printResult(num1)}
          5 -> {//나머지 구하기
                num1 = num1 % num2
                println("나머지는 ${num1} 입니다.")
                println("-----------------------")}
        }
    }
}
open class Calculator() {
   open fun printResult(num1: Int) {
       println("계산결과는 ${num1} 입니다.")
       println("-----------------------")
   }
}
class AddOperation(a:Int, b: Int) : Calculator() {
    fun add(a: Int, b: Int) = a + b
    override fun printResult(num1: Int) {
        println("덧셈 결과는 ${num1} 입니다.")
        println("-----------------------")
    }
}
class SubtractOperation(a: Int, b: Int) : Calculator() {
    fun subtract(a:Int, b:Int) = a - b
    override fun printResult(num1: Int) {
        println("뺄셈 결과는 ${num1} 입니다.")
        println("-----------------------")
    }
}
class MultipleOperation(a: Int, b: Int): Calculator() {
    fun multiple(a:Int, b:Int) = a * b
    override fun printResult(num1: Int) {
        println("곱셈 결과는 ${num1} 입니다.")
        println("-----------------------")
    }
}
class DivideOperation(a: Int, b: Int): Calculator() {
    fun divide(a:Int, b:Int) = a / b
    override fun printResult(num1: Int) {
        println("나눗셈 결과는 ${num1} 입니다.")
        println("-----------------------")
    }
}

 

단일책임원칙에 따라 각각의 연산 클래스를 만들고 상속관계를 맺긴 했는데 상속의 의미가 크게 없어보이고 코드가 더 많아져서 좋은 건지 모르겠다. 튜터님께 피드백을 받아보자.

 

연산하는 메소드를 상속받아 overriode할 수 있도록 변경하기

연산 결과값에 추가 연산하는 게 아니라 처음부터 다시 연산할 수 있도록 하기

바뀌지 않는 값은 val로 변경하기

 

피드백에 따라 수정했다. 

fun main() {

    var num1 : Int = 0
    while (true) {
        println("첫번째 숫자를 입력해주세요.")
        num1 = readLine()!!.toInt()


        println("${num1}에 무엇을 해드릴까요?")
        println("1 : 더하기, 2 : 빼기, 3 : 곱하기, 4 : 나누기, 5 : 나머지 구하기, -1 : 그만하기")
        val operator = readLine()!!.toInt()

        if(operator == -1) {
            println("계산을 종료합니다.")
            break
        }

        println("두번째 숫자를 입력해주세요.")
        val num2 = readLine()!!.toInt()

        //인스턴스화
        val addOperation = AddOperation()
        val subtractOperation = SubtractOperation()
        val multipleOperation = MultipleOperation()
        val divideOperation = DivideOperation()

        when (operator) {
            1 -> {//더하기
                num1 = addOperation.calculate(num1, num2)
                addOperation.printResult(num1)}
            2 ->{//빼기
                num1 = subtractOperation.calculate(num1, num2)
                subtractOperation.printResult(num1)}
            3 ->{//곱하기
                num1 = multipleOperation.calculate(num1, num2)
                multipleOperation.printResult(num1)}
            4 ->{//나누기
                num1 = divideOperation.calculate(num1, num2)
                divideOperation.printResult(num1)}
            5 -> {//나머지 구하기
                num1 = num1 % num2
                println("나머지는 ${num1} 입니다.")
                println("-----------------------")}
        }
    }
}
//부모 클래스
open class Calculator {
    open fun calculate(a: Int, b: Int) : Int = 0
    open fun printResult(a:Int){}
}
//더하기 클래스
class AddOperation : Calculator() {
    override fun calculate(a: Int, b: Int): Int  = a + b
    override fun printResult(a: Int) {
        println("덧셈 결과는 ${a} 입니다.")
        println("-----------------------")
    }
}
//빼기 클래스
class SubtractOperation : Calculator() {
    override fun calculate(a: Int,b: Int) : Int = a - b
    override fun printResult(a: Int) {
        println("뺄셈 결과는 ${a} 입니다.")
        println("-----------------------")
    }
}
//곱하기 클래스
class MultipleOperation : Calculator() {
    override fun calculate(a: Int,b: Int) : Int = a * b
    override fun printResult(a: Int) {
        println("곱셈 결과는 ${a} 입니다.")
        println("-----------------------")
    }
}
//나누기 클래스
class DivideOperation : Calculator() {
    override fun calculate(a: Int,b: Int) : Int = a / b
    override fun printResult(a: Int) {
        println("나눗셈 결과는 ${a} 입니다.")
        println("-----------------------")
    }
}

 

연산 메소드를 상속하니 클래스 간의 관계가 보다 명확해졌다. 왜 인스턴스화 했는지 여쭤보셔서 호출할 때 각각 값을 넘겨줘서 길어지는 게 싫어서라고 답변했다. 다행히 맞고 틀리고는 없었고 모든 코드에 이유가 있어야 한다는 특강에 부합해 만족스러웠다.

 

 

 

 

Lv4 추상화

조건 : AbstractOperation라는 클래스명으로 만들어 사용하여 추상화하고 Calculator 클래스의 내부 코드를 변경하기

세부사항 : Lv3 과 비교하여 개선된 점 생각해보기(hint. 의존성 역전 원칙)

 

의존성 역전 원칙 DIP Dependency Inversion Principle
고수준 모듈이 저수준 모듈에 의존해서는 안 되며 둘 모두 추상화에 의존해야 한다는 원칙. 결합 및 의존성을 낮추고 유연성을 높인다.

고수준 모듈 High-level modules : 애플리케이션의 비즈니스 로직을 포함하고 있는 모듈. 추상화와 정책
저수준 모듈 Low-level modules : 고수준 모듈에 의존하여 서비스를 제공하고 있는 모듈. 구체적인 구현 담당

 

3레벨까지가 필수사항이었고 4레벨은 강의에 없는 내용이라 따로 공부 후 구현해야한다. 유튜브로 설명을 들어도 정의나 필요성만 이해되고 코드 작동은 이해되지 않아서 2회 시도 후 튜터님께 피드백을 받았다. 

1차 시도 : Calculator부모 클래스를 완전히 없애고 AbstractOperation추상 클래스를 상속받아 override 

실패원인 : Calculator부모 클래스 상속의 형태와 다를 바 없음

2차 시도 : 추상 클래스를 부모 클래스 생성자 파라미터에 만들고 부모 클래스를 상속 상속받아 override 

실패원인 : Calculator부모 클래스가 추상클래스로 연결만 해줄 뿐 부모 클래스를 사용하지 않고 있음

이런 식이었다. 재현한거라 당시와 정확히 일치하진 않다.

//부모 클래스
open class Calculator(val abstractOperation: AbstractOperation) {
    open fun calculate(a: Int, b: Int) : Int = abstractOperation.calculate(a, b)
    open fun printResult(a:Int) = abstractOperation.printResult(a)
}
//추상 클래스
abstract class AbstractOperation {
    abstract fun calculate(a: Int, b: Int) : Int
    abstract fun printResult(a:Int)
}
//더하기 클래스
class AddOperation(abstractOperation: AbstractOperation) : Calculator(abstractOperation) {
    override fun calculate(a: Int, b: Int): Int = a + b
    override fun printResult(a: Int) {
        println("덧셈 결과는 ${a} 입니다.")
        println("-----------------------")
    }
}

 

DIP에 대해 좀 더 알아보자

의존성을 역전시키라는 말이다. 하위에서 상위로 의존하던 관계에 추상화라는 중간 매개체를 둬서 상위도 하위도 추상화에 의존하게 한다. 하위 클래스야 원래 상속을 받는 입장이니까 상속자만 추상클래스로 변경하면 되는데 문제는 상위 클래스다. 상위 클래스는 어떻게 의존할까? 상위 클래스 위에 추상 클래스를 두어 상속해야할까? 그러면 중간 매개체가 아니라 또 수직의 의존형태일 뿐이겠다.

그 방법은 상위 클래스 생성자의 파라미터로 추상 클래스를 만들어주면 상위클래스에 의존성을 주입시킬 수 있다. (val 변수 이름추상클래스 : 추상클래스타입)을 입력하면 된다. 여기서 변수 키워드 val을 꼭 적어줘야하는데 초기화할 때는 안써도 되지만 외부에서 사용해야 하니까 변수 키워드를 적어야 된다. 만약 적지 않는다면 외부에서 접근할 수 없는 지역변수로 취급된다. 

 

2차시도의 피드백대로 Calculator부모 클래스를 사용하도록 수정했다. 최종 완성본이다.

fun main() {

    var num1 : Int = 0
    while (true) {
        println("첫번째 숫자를 입력해주세요.")
        num1 = readLine()!!.toInt()


        println("${num1}에 무엇을 해드릴까요?")
        println("1 : 더하기, 2 : 빼기, 3 : 곱하기, 4 : 나누기, 5 : 나머지 구하기, -1 : 그만하기")
        val operator = readLine()!!.toInt()

        if(operator == -1) {
            println("계산을 종료합니다.")
            break
        }

        println("두번째 숫자를 입력해주세요.")
        val num2 = readLine()!!.toInt()

        //인스턴스화
        val addOperation = Calculator(AddOperation())
        val subtractOperation = Calculator(SubtractOperation())
        val multipleOperation = Calculator(MultipleOperation())
        val divideOperation = Calculator(DivideOperation())

        when (operator) {
            1 -> {//더하기
                num1 = addOperation.calculate(num1, num2)
                addOperation.printResult(num1)}
            2 ->{//빼기
                num1 = subtractOperation.calculate(num1, num2)
                subtractOperation.printResult(num1)}
            3 ->{//곱하기
                num1 = multipleOperation.calculate(num1, num2)
                multipleOperation.printResult(num1)}
            4 ->{//나누기
                num1 = divideOperation.calculate(num1, num2)
                divideOperation.printResult(num1)}
            5 -> {//나머지 구하기
                num1 = num1 % num2
                println("나머지는 ${num1} 입니다.")
                println("-----------------------")}
        }
    }
}
//부모 클래스
open class Calculator(val abstractOperation: AbstractOperation) {
    open fun calculate(a: Int, b: Int) : Int = abstractOperation.calculate(a, b)
    open fun printResult(a:Int) = abstractOperation.printResult(a)
}
//추상 클래스
abstract class AbstractOperation {
    abstract fun calculate(a: Int, b: Int) : Int
    abstract fun printResult(a:Int)
}
//더하기 클래스
class AddOperation : AbstractOperation() {
    override fun calculate(a: Int, b: Int): Int = a + b
    override fun printResult(a: Int) {
        println("덧셈 결과는 ${a} 입니다.")
        println("-----------------------")
    }
}
//빼기 클래스
class SubtractOperation : AbstractOperation() {
    override fun calculate(a: Int,b: Int) : Int = a - b
    override fun printResult(a: Int) {
        println("뺄셈 결과는 ${a} 입니다.")
        println("-----------------------")
    }
}
//곱하기 클래스
class MultipleOperation : AbstractOperation() {
    override fun calculate(a: Int,b: Int) : Int = a * b
    override fun printResult(a: Int) {
        println("곱셈 결과는 ${a} 입니다.")
        println("-----------------------")
    }
}
//나누기 클래스
class DivideOperation : AbstractOperation() {
    override fun calculate(a: Int,b: Int) : Int = a / b
    override fun printResult(a: Int) {
        println("나눗셈 결과는 ${a} 입니다.")
        println("-----------------------")
    }
}

 

추상화 유무가 결과에 변화를 주진 않지만, 클래스 간 관계나 의미론적인 관점에서 해석했을 때 훨씬 바람직하다.

 

 

몇 가지 개선점을 찾아보고 고쳐보자. 이하는 내일 수정할 예정

open 지우기 : 상속을 끊었는데 부모 클래스가 맞는지 의문이었는데 해설강의에서 답변이 나왔다. 상속을 하고 있는 것이 아니므로 지우기로 한다.

Q. 연산을 문자형으로 바꾸려면?

Q. 실수도 처리하도록 하려면?
Q. 코드를 더 간결하게 바꾸려면?

Q. 예외처리?

 

 

 

 


 

 

 

회고

반복문에서 어려웠고 추상화와 의존성 역전 원칙을 이해하고 적용하는데 많이 어려웠다. 클래스 내 기능이 빈약해서 추상화 없이도 유지보수가 어렵지 않다보니 추상화의 필요성이 크게 와닿지는 않지만 코드를 전체적으로 어떻게 써야 하는지와 코틀린 문법을 좀 더 배울 수 있는 시간이었다.