이전에 만들었던 Lv4는 의존성 역전 원칙(DIP, Dependency Inversion Principle) 을 수행한 내용이지만 OOP의 원칙 중 하나인 개방-폐쇄 원칙(OCP, Open-Closed Principle)을 어긴 사례이다,,,

class Calculator {

    fun calculator(num1: Double, num2: Double, op: AbstractOperation): Double {
        return when (op) {
            is AddOperation -> {op.operation(num1,num2)}
            is SubstractOperation -> {op.operation(num1,num2)}
            is MultiflyOperation -> {op.operation(num1,num2)}
            is DivideOperation -> {op.operation(num1,num2)}
            else -> throw IllegalArgumentException("잘못된 연산자 입력값입니다.")
        }
    }
}

 

추상화 클래스를 적용했을 때 Lv3 보다는 깔끔해지기도 했고 해서 괜찮게 했다 생각했으나, 마음 한 편으로는 기능 추가할 때 한 줄씩 계속 추가해야 한다면 기능이 100개가 추가가 된다면 100줄 추가해야하는데 그게 유지 보수가 간편하다 말해도 되나? 싶었다. 물론 그 전보다는 코드량이 꽤 줄었지만 상위 모듈은 코드 수정 필요없이 작동시키고 싶어서 찾아봤다.

 

찾아보니 개방-폐쇄 원칙이라는게 있었다. 개방-폐쇄 원칙이란 '소프트웨어 개체(클래스, 모듈, 함수 등등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다'는 프로그래밍 원칙이다.

 

즉, 기능이 변하거나 확장 가능하지만, 함수의 본질적인 코드(해당 기능의 코드)는 수정하면 안된다는 원칙이다.

 

하지만, 나는 기능이 확장되거나 변하면 calculator 함수의 내용을 수정해야 한다. 앞선 글에서 이야기한거와 같이

제곱연산자 기능을 넣으면 is SquareOperation -> {op.operation(num1,num2)} 이 내용도 상위모듈에 추가해야 한다.

이 자체가 개방-폐쇄 원칙을 위반한 코드다.

 

원칙에 부합하게 열심히 방법을 찾아보고 코드를 눈 뚫어져라 쳐다보니 이를 획기적으로 고치는 방법이 있었다.

class Calculator {

    fun calculator(num1: Double, num2: Double, op: AbstractOperation): Double {
        return op.operation(num1,num2)
    }
}

 

생각해보니 op를 받아올 때 이미 연산 클래스를 받아오기에 그 안에 있는 연산 메서드를 이용하면 끝이었다.

이 한줄을 통해 연산 클래스의 기능이 확장이 되거나 변경이 되어도 같은 기능을 수행하게 된다.

즉, 상위 모듈의 코드 변경없이 기능 확장이 가능해졌다.

//main 안에 op 입력 구현 부분

fun isValidOperator() : AbstractOperation {
    while (true) {
        try {
            var op = readLine()!!.toInt()
            if (op in 1..4) {
                when(op) {
                    1 -> return AddOperation()
                    2 -> return SubstractOperation()
                    3 -> return MultiflyOperation()
                    4 -> return DivideOperation()
                }
            }
            else println("1부터 4까지 연산자에 해당하는 숫자를 입력하세요")
        } catch (e: NumberFormatException) {
            println("입력값 오류! 숫자를 입력해주세요.")
        }
    }
}

 

실행 결과

 

이번 개인 과제에서 정말 많은 내용들을 공부할 수 있었고 앞으로 어떻게 어려움들을 헤쳐나가야 할지 어느정도 감이 잡혔다. 이해를 도와주신 장세진 튜터님께 감사의 말씀 올립니다!!

'MyProject' 카테고리의 다른 글

개인 과제 계산기 만들기 Lv4  (0) 2023.11.30
개인 과제 계산기 만들기 Lv3  (0) 2023.11.29
개인 과제 계산기 만들기 Lv2  (0) 2023.11.29
개인 과제 계산기 만들기 Lv1  (0) 2023.11.28

선택 구현 기능

  • Lv4 : AddOperation(더하기), SubtractOperation(빼기), MultiplyOperation(곱하기), DivideOperation(나누기) 연산 클래스들을 AbstractOperation라는 클래스명으로 만들어 사용하여 추상화하고 Calculator 클래스의 내부 코드를 변경합니다.
    • Lv3 와 비교해서 어떠한 점이 개선 되었는지 스스로 생각해 봅니다.
      • hint. 클래스간의 결합도, 의존성(의존성역전원칙)
//main 안에 op 입력 구현 부분

fun isValidOperator() : AbstractOperation {
    while (true) {
        try {
            var op = readLine()!!.toInt()
            if (op in 1..4) {
                when(op) {
                    1 -> return AddOperation()
                    2 -> return SubstractOperation()
                    3 -> return MultiflyOperation()
                    4 -> return DivideOperation()
                }
            }
            else println("1부터 4까지 연산자에 해당하는 숫자를 입력하세요")
        } catch (e: NumberFormatException) {
            println("입력값 오류! 숫자를 입력해주세요.")
        }
    }
}

 

기존에는 op의 타입과 isValidOperator()의 반환 타입이 Double이었지만 지금은 AbstractOperation이라는 추상 클래스로 바뀐 점이 주목할 부분이다.

class Calculator {

    fun calculator(num1: Double, num2: Double, op: AbstractOperation): Double {
        return when (op) {
            is AddOperation -> {op.operation(num1,num2)}
            is SubstractOperation -> {op.operation(num1,num2)}
            is MultiflyOperation -> {op.operation(num1,num2)}
            is DivideOperation -> {op.operation(num1,num2)}
            else -> throw IllegalArgumentException("잘못된 연산자 입력값입니다.")
        }
    }
}

 

Operation 클래스들을 초기화해서 호출 하는 부분이 전부 날아갔다. op에 들어오는 클래스에 따라 그 클래스에 맞는 operation을 진행해준다. is 연산자를 통해 자식 클래스인 연산클래스들을 다운캐스팅해주어야 한다.

abstract class AbstractOperation {
    abstract fun operation(num1:Double, num2:Double) : Double
}

class AddOperation : AbstractOperation() {
    override fun operation(num1:Double, num2:Double) :Double = (num1 + num2)
}

class SubstractOperation : AbstractOperation() {
    override fun operation(num1:Double, num2:Double) :Double = (num1 - num2)
}

class MultiflyOperation : AbstractOperation() {
    override fun operation(num1:Double, num2:Double) :Double = (num1 * num2)
}

class DivideOperation : AbstractOperation() {
    override fun operation(num1:Double, num2:Double) :Double = (num1 / num2)
}

 

 

들어가기에 앞서 이번 과제의 목표는 의존성 역전 원칙( Dependency Injectoin Principle ) 을 이해하는 것이다.

의존성 역전 원칙이란 객체는 저수준 모듈 보다 고수준 모듈에  의존해야 한다는 것이다. 

 

지금 이해를 토대로 간단히 말하자면, 변하기 쉬운 코드에 의존하는 것(저수준 모듈 : 메인클래스, 객체 등)이 아닌 거의 변하지 않는 개념(고수준 모듈: 추상 클래스, 인터페이스)에 의존해야 한다는 것이다.

 

더 쉽게 말해보자면, 객체의 상속은 인터페이스 또는 추상 클래스을 통해 이루어져야 한다.

즉, 그냥 클래스 직접 상속받지 마라!! 인터페이스나 추상 클래스 만들어서 상속받아라!! 이다.

 

그렇다면 왜 그렇게 해야하느냐??

지금 하고 있는 계산기를 예시로 생각해보자

abstract class AbstractOperation 추상클래스 그리고 그 안에 abstract fun operation 추상 메소드를 만들어 줌으로써 이 클래스를 상속받는 모든 클래스들은 미완성된 추상 메소드를 오버라이딩하여 기능을 완성하게끔 강제한다.

 

이를 통해 클래스간의 조직화가 가능해졌고 존재의 이유를 명확하게 할 수 있다. 또한 다른 기능의 클래스(예를 들어 제곱을 해주는 연산)가 생겼을 때 추가적으로 수정해야할 부분은 calculator 함수에 밑에 코드 한 줄이면 된다.

is SquareOperation -> {op.operation(num1,num2)}

 

변경 사항에 대처하는 것이 유연해져서 유지 보수가 간결해지고 확장성이 용이하다.

main 함수 코드

package com.example.mycalcurator

import java.lang.NumberFormatException

//Lv3 : AddOperation(더하기), SubstractOperation(빼기), MultiplyOperation(곱하기), DivideOperation(나누기) 연산 클래스를 만든 후 클래스간의 관계를 고려하여 Calculator 클래스와 관계를 맺기
// 입력값 안정성 높이기 -> 완료

fun main() {
    var num1: Double
    var num2: Double
    var op: Int
    var keepOrExit: Int

    println("-------------------계산기-------------------")
    println("--------연산할 첫 번째 숫자를 입력하세요--------")
    num1 = isNumber()
    println("-------연산자에 해당하는 숫자를 입력하세요-------")
    println("-----1(더하기) 2(빼기) 3(곱하기) 4(나누기)-----")
    op = isValidOperator()
    println("--------연산할 두 번째 숫자를 입력하세요--------")
    num2 = isNumber()

    val cal = Calculator()
    var result = cal.calculator(num1, num2, op)
    println("-----------연산 결과 : ${result} -----------")

    while (true) {
        println("-이어서 연산 하시려면 1 종료하시려면 2 입력해주세요-")
        while (true) {
            try {
                keepOrExit = readLine()!!.toInt()
                if (keepOrExit == 1) break
                else if (keepOrExit == 2) {
                    println("---------------계산기 종료----------------")
                    return
                }
                else println("입력값 오류! 이어서 연산하려면 1 종료하려면 2를 입력해주세요")
            } catch (e: NumberFormatException) {
                println("입력값 오류! 숫자를 입력해주세요. 1 -> 계속 2 -> 종료")
            }
        }
        println("-------연산자에 해당하는 숫자를 입력하세요-------")
        println("-----1(더하기) 2(빼기) 3(곱하기) 4(나누기)-----")
        op = isValidOperator()
        println("--------연산할 두 번째 숫자를 입력하세요--------")
        num2 = isNumber()
        result = cal.calculator(result, num2, op)
        println("--------------연산 결과 : ${result} --------------")
    }
}

fun isNumber() : Double{
    while (true) {
        try {
            var num = readLine()!!.toDouble()
            return num
        } catch (e: NumberFormatException) {
            println("입력값 오류! 숫자를 입력해주세요.")
        }
    }
}

fun isValidOperator() : Int {
    while (true) {
        try {
            var op = readLine()!!.toInt()
            if (op in 1..4) return op
            else println("1부터 4까지 연산자에 해당하는 숫자를 입력하세요")
        } catch (e: NumberFormatException) {
            println("입력값 오류! 숫자를 입력해주세요.")
        }
    }
}

 

Calculator 클래스 코드

package com.example.mycalcurator

class Calculator {

    fun calculator(num1: Double, num2: Double, op: Int): Double {
        var result:Double = 0.0
        val addOperation = AddOperation()
        val substractOperation = SubstractOperation()
        val multiflyOperation = MultiflyOperation()
        val divideOperation = DivideOperation()

        when (op) {
            1 -> {result = addOperation.operation(num1,num2)}
            2 -> {result = substractOperation.operation(num1,num2)}
            3 -> {result = multiflyOperation.operation(num1,num2)}
            4 -> {result = divideOperation.operation(num1,num2)}
        }
        return result
    }
}

 

AddOperation 클래스, SubstractOperation 클래스 , MultiflyOperation 클래스 , DivideOperation 클래스

class AddOperation {
    fun operation(num1:Double, num2:Double) :Double = (num1 + num2)
}

class SubstractOperation {
    fun operation(num1:Double, num2:Double) :Double = (num1 - num2)
}

class MultiflyOperation {
    fun operation(num1:Double, num2:Double) :Double = (num1 * num2)
}

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

 

우선, 입력값 오류에 대한 안정성을 while문, try-catch문을 통해서 높였다. 그대로 적어놓으니 너무 지저분하여 2번 이상 쓰인 부분은 isNumber(), isValidOperator() 함수로 빼내어 코드량을 많이 줄였다.

 

단일 책임 원칙 (SRP : Single Responsibility Principle)에 따라 Calculator 클래스에서 각 연산자 클래스로 나누어 하나의 클래스가 하나의 기능만 책임질 수 있게  해주었다. 이로서 Calculator 클래스는 연산할 클래스들을 인스턴스화하여 op 값에 맞는 연산을 각 클래스들에게 맡기고 결과값을 리턴해주는 책임만 가지게 된다.

 

Lv2 : Lv1에서 만든 Calculator 클래스에 출력 이후 추가 연산을 가능하도록 코드를 추가하고, 연산 진행 후 출력하기

//Lv2 추가 연산 가능하게 하고 출력하기
fun main() {
    println("-------------------계산기-------------------")
    println("--------연산할 첫 번째 숫자를 입력하세요--------")
    var num1 = readLine()!!.toDouble()
    println("--------연산할 두 번째 숫자를 입력하세요--------")
    var num2 = readLine()!!.toDouble()
    println("-------연산자에 해당하는 숫자를 입력하세요-------")
    println("-----1(더하기) 2(빼기) 3(곱하기) 4(나누기)-----")
    var op = readLine()!!.toInt()
    var cal = Calculator()
    var result = cal.calculator(num1, num2, op)
    println("--------------연산 결과 : ${result} --------------")
    do {
        println("-이어서 연산 하시려면 1 종료하시려면 2 입력해주세요-")
        var keepOrExit = readLine()!!.toInt()
        if (keepOrExit == 2) {println("--------계산기 종료--------"); break}
        println("--------연산할 숫자를 입력하세요--------")
        num2 = readLine()!!.toDouble()
        println("-------연산자에 해당하는 숫자를 입력하세요-------")
        println("-----1(더하기) 2(빼기) 3(곱하기) 4(나누기)-----")
        op = readLine()!!.toInt()
        result = cal.calculator(result, num2, op)
        println("--------------연산 결과 : ${result} --------------")
    }while(true)
}
class Calculator {
    fun calculator(num1: Double, num2: Double, op: Int): Double {
        var result:Double = 0.0
        when (op) {
            1 -> {result = num1 + num2}
            2 -> {result = num1 - num2}
            3 -> {result = num1 * num2}
            4 -> {result = num1 / num2}
        }
        return result
    }
}

while문으로 무한 추가 연산 가능하게 만들기

if문으로 추가 연산 의사여부 확인 및 여부에 따른 진행 또는 종료 기능 추가

 

Lv3로 갈 때 입력값에 대한 안정성을 높여야겠다.

 

Lv1 : 더하기, 빼기, 나누기, 곱하기 연산을 수행할 수 있는 Calculator 클래스를 만들고, 클래스를 이용하여 연산을 진행하고 출력하기

fun main() {
    println("-------------------계산기-------------------")
    println("--------연산할 첫 번째 숫자를 입력하세요--------")
    val num1 = readLine()!!.toDouble()
    println("--------연산할 두 번째 숫자를 입력하세요--------")
    val num2 = readLine()!!.toDouble()
    println("-------연산자에 해당하는 숫자를 입력하세요-------")
    println("-----1(더하기) 2(빼기) 3(곱하기) 4(나누기)-----")
    val op = readLine()!!.toInt()

    var cal = Calculator()
    var result = cal.calculator(num1, num2, op)
    println("--------------연산 결과 : ${result} --------------")
}
class Calculator {
    fun calculator(num1: Double, num2: Double, op: Int): Double {
        var result:Double = 0.0
        when (op) {
            1 -> {result = num1 + num2}
            2 -> {result = num1 - num2}
            3 -> {result = num1 * num2}
            4 -> {result = num1 / num2}
            else -> {result = -1.0}
        }
        return result
    }
}

 

 

코드를 보면 알겠지만 입력에 대한 예외 처리, 추가 연산 모두 구현 안되어 있다.

점차 발전 시켜나갈 예정이다.

+ Recent posts