https://school.programmers.co.kr/learn/courses/30/lessons/159994

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

또 하나 배운 알고리즘 문제였다..

코드 먼저 보자.

class Solution {
    fun solution(cards1: Array<String>, cards2: Array<String>, goal: Array<String>): String {
        var answer: String = "Yes"
        var index1 = 0
        var index2 = 0
        
        goal.forEach {
            if(cards1.size > index1 && cards1[index1] == it){
                index1++
            }
            else if(cards2.size > index2 && cards2[index2] == it){
                index2++
            }
            else {
                return "No"
            }
        }
        return answer
    }
}

 

if 문에서 &&연산자를 사용할 때도 조건의 순서가 중요하다는 점을 배웠다.

처음에는 && 연산자 사이에 조건을 별 생각없이 지금과 반대로 했었다. 

if(cards1[index1] == it && cards1.size > index1)

else if(cards2[index2] == it && cards2.size > index2)

이렇게 하니 돌아오는 것은,,, 런타임 에러,,,

보아 하니 인덱스 값이 길이를 초과한 모양이다.

테스트 2번 예제와 else if 구절을 뜯어보면 이해가 가능하다.

["i", "water", "drink"] ["want", "to"] ["i", "want", "to", "drink", "water"] "No"
else if(cards2[index2] == it && cards2.size > index2){
      index2++
}

 

테스트 2번에서는 cards2의 index2값이 0에서 "want"를 만나 1이 되고 "to"를 만나 2가 된다.

이 후 "drink"를 찾으러 가는데 cards1에서는 못찾으니 else if로 내려오게 되면서 문제가 발생한다.

&&연산자 첫 번째 조건에서 cards2[index2] == it 여기서 cards1의 인덱스는 0과 1 뿐인데 index2의 값인 2가 들어와서 

배열인덱스의 범위를 초과한 값을 넣은 것이다. 

 

&&연산자 양옆의 조건 위치를 반대로 해주니 통과가 되었다.

이를 통해 && 연산자의 첫 번째 조건을 우선적으로 검사를 한다는 것을 알 수 있었고 결과에 큰 영향을 미칠 수 있으므로 순서도 잘 생각해서 배치해주어야 한다는 걸 배웠다.

 

 

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

 

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

 

class Solution {
    fun solution(a: Int, b: Int): String {
        var answer = ""
        var totalDay:Int = b
        
        for(month in 1 until a) {
            when(month) {
                1,3,5,7,8,10,12 -> totalDay += 31
                2 -> totalDay += 29
                4,6,9,11 -> totalDay += 30
            }
        }
        when(totalDay % 7) {
            1 -> answer = "FRI"
            2 -> answer = "SAT"
            3 -> answer = "SUN"
            4 -> answer = "MON"
            5 -> answer = "TUE"
            6 -> answer = "WED"
            else -> answer = "THU"
        }
        return answer
    }
}

 

로직은 어렵지 않은 문제였는데 for문에서 a 해당 월은 빼고 계산하기 위해서 ..  이 아닌 until 을 썼어야 했는데 이 부분을 늦게 눈치챘다.

 

for 문으로 1월부터 a월 전까지 반복을 돌리고 when 문으로 각 월의 요일 수를 더해줬다. totalDay에 이미 b 값이 들어가 있기에 또 더해줄 필요는 없게 했다.

그리고 총 요일 수를 7로 나머지 계산을 해주면 쉽게 요일 수를 알아낼 수 있다. 금요일이 1일이니 totalDay % 7이 1이면 금요일 이런식으로 계산한다.

 

지금 생각해보면서 조금 아쉬운건 코드량을 더 줄일 수 있었다.

val whatDay = listOf("FRI","SAT","SUN","MON","TUE","WED","THU")

answer = whatDay[totalDay%7]

 

이런 식으로 when문을 삭제시킬 수 있었다.

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 값에 맞는 연산을 각 클래스들에게 맡기고 결과값을 리턴해주는 책임만 가지게 된다.

 

코드를 치다가 더 좋은 생각이 들어서 기존에 있던걸 주석 처리를 하고 힐끗힐끗 보면서 새로 코드를 짤 일이 은근 많다.

 

그.러.나 갑자기 잘 되던 ctrl + '/' 주석 처리 단축키도 안되고 ctrl + shift + '/' 주석 처리 단축키도 안되는 것이다...

 

단축키 설정을 바꾸지도 않았는데 갑자기 왜 이러는 것이야,,,하고 구글링을 해보았더니ㅋㅋㅋㅋㅋ

 

나와 같은 사람들이 한 무더기 있었다... 원인은 바로 이녀석

 

 

한컴 입력기 상태여서 안됐던 것이다. Microsoft 입력기로 바꿔주면 잘될 것이다. 윈도우키와 스페이스바를 눌러 바꿔주면 된다.

+ Recent posts