이전에 만들었던 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)}

 

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

+ Recent posts