Notice
Recent Posts
Recent Comments
Link
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Archives
Today
Total
관리 메뉴

밤빵's 개발일지

[TIL]20241118 확장성과 오버엔지니어링 본문

Kotlin

[TIL]20241118 확장성과 오버엔지니어링

최밤빵 2024. 11. 18. 01:31

확장성과 오버엔지니어링.이 두 개념은 개발자로서 코드 설계를 고민할 때 자주 맞닥뜨리게 되는 문제이지만, 초입자인 내가 이해하기엔 어려운 주제였다. 이번 개발일지를 통해 확장성과 오버엔지니어링이 무엇인지 학습하고, 이 두 개념의 균형을 어떻게 잡아야 하는지 이해하려고 한다.


▶ 확장성(Scalability)이란?

확장성(Scalability)이란 소프트웨어 시스템이나 코드가 성장하는 요구사항에 맞추어 쉽게 확장될 수 있는 능력을 의미한다. 여기서 확장성은 단순히 더 많은 기능을 추가하는 것뿐만 아니라, 현재의 코드 구조를 크게 변경하지 않고도 요구사항에 유연하게 대응할 수 있는 것을 말한다. 예를 들어, 특정 기능이 추가될 때 기존 코드를 크게 수정하지 않고 새로운 기능을 쉽게 추가할 수 있다면 그 코드의 확장성이 좋다고 할 수 있다. 확장성 있는 코드를 작성하면 유지보수와 기능 추가가 수월해진다. 팀 프로젝트나 장기적인 시스템 개발에서는 확장성을 고려한 코드 설계가 필수적이다. 다만, 초기에 확장성을 너무 강조하다 보면 아직 필요하지 않은 복잡한 설계를 하게 되기 쉽다. 이는 바로 오버엔지니어링으로 이어질 수 있다.


▶오버엔지니어링(Overengineering)이란?

오버엔지니어링(Overengineering)이란 말 그대로 필요 이상의 설계와 복잡도를 추가하는 것이다. 코드에 필요한 기능 이상을 과도하게 고려하거나, 미래에 생길 수 있는 모든 가능성을 대비한 구조를 만들어버리는 것을 말한다. 오버엔지니어링은 현재의 문제를 해결하는 것보다 미래에 발생할 수 있는 문제를 대비하려는 지나친 시도로 인해 발생하며, 결과적으로 코드의 복잡성을 증가시키고 가독성과 유지보수를 어렵게 만든다. 특히 개발 초기에 오버엔지니어링은 비용이 크다. 아직 구체화되지 않은 요구사항을 위해 복잡한 구조를 미리 설계하면, 실제로 그 요구사항이 발생하지 않을 경우 그 코드는 불필요한 비용만 증가시키고 유지보수를 힘들게 한다. 따라서 확장성과 오버엔지니어링의 균형을 맞추는 것이 중요하다.


▶예시 코드 분석 (프리코스 1주차 과제)

과제를 위해 문자열 덧셈 계산기를 구현한 예시 코드로, 이 코드에서 확장성과 오버엔지니어링을 모두 찾아볼 수 있다.

▷ 기능 요구 사항
→입력한 문자열에서 숫자를 추출하여 더하는 계산기를 구현한다.
→ 쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환한다.                               
 ex :  "" => 0, "1,2" => 3, "1,2,3" => 6, "1,2:3" => 6                                                
→앞의 기본 구분자(쉼표, 콜론) 외에 커스텀 구분자를 지정할 수 있다. 커스텀 구분자는 문자열 앞부분의 "//"와 "\n" 사이에 위치하는 문자를 커스텀 구분자로 사용한다.
→예를 들어 "//;\n1;2;3"과 같이 값을 입력할 경우 커스텀 구분자는 세미콜론(;)이며, 결과 값은 6이 반환되어야 한다.
→사용자가 잘못된 값을 입력할 경우 IllegalArgumentException을 발생시킨 후 애플리케이션은 종료되어야 한다.
▷ 구현 기능 목록
→사용자 입력으로부터 커스텀 구분자를 추가하는지 아닌지를 확인할 수 있는 기능
→커스텀 구분자를 추가하면 기존의 구분자 + 커스텀 구분자 또는 커스텀 구분자가 없다면 기존의 → 구분자로 문자열을 문자열 리스트로 분리하는 기능
→문자열 리스트의 각 요소를 더한 결과를 반환하는 기능
fun main() {
    // TODO: 프로그램 구현
    println("덧셈할 문자열을 입력해 주세요.")

    val str = readLine()
    try {
        val sum = if (hasCustomDelimiter(str)) {
            val (newDelimiter, s) = splitByCustomDelimiter(str)
            sumOfStringList(s.split(",", ":", newDelimiter))
        } else {
            sumOfStringList(str.split(",", ":"))
        }
        println("결과 : $sum")
    } catch (e: Exception) {
        throw IllegalArgumentException("잘못된 입력 형식입니다.")
    }
}

fun readLine(): String = camp.nextstep.edu.missionutils.Console.readLine()
fun hasCustomDelimiter(str: String) = Regex("//.*\n").containsMatchIn(str.replace("\n", "\n"))

fun splitByCustomDelimiter(str: String): Pair<String, String> {
    val splitList = str.split("//", "\n")
    return splitList[1] to splitList[2]
}

fun sumOfStringList(list: List<String>) =
    list.sumOf {
        val n = it.toInt()
        if (n <= 0) throw IllegalArgumentException("잘못된 입력 형식입니다.")
        n
    }

→ 사용자가 입력한 문자열을 덧셈하는 기능을 제공한다. 기본 구분자(,, :) 외에도 커스텀 구분자를 지원하도록 설계되어 있다. 예를 들어, //; 1;2;3과 같은 형식으로 입력하면 ;를 구분자로 하여 숫자들을 덧셈할 수 있다. 내 코드에서 확장성과 오버엔지니어링을 생각해볼 수 있는 몇 가지 포인트가 있다.


▶확장성 측면

  • 내 코드의 확장성 있는 부분은 커스텀 구분자를 지원하는 기능이다. 기본 구분자 외에도 사용자가 원하는 구분자를 사용할 수 있게 하여, 다양한 입력 형식에 대응할 수 있다. 이는 코드가 더 다양한 입력을 처리할 수 있는 유연성을 제공하며, 사용자의 요구사항 변화에 쉽게 적응할 수 있게 한다.
  • 또한, 구분자 관련 로직을 별도로 함수로 나누어 두었기 때문에, 새로운 구분자를 추가하거나 구분자 처리 로직을 수정할 때 전체 코드를 수정할 필요 없이 해당 함수만 수정하면 된다. 이는 유지보수성과 확장성을 높여주는 좋은 설계 방식이다.

▶오버엔지니어링 측면

  • 그러나, 내 코드에서 확장성을 위해 추가된 커스텀 구분자 처리가 과연 꼭 필요했는지 고민해볼 필요가 있다. 만약 이 프로그램의 목적이 단순히 몇 개의 숫자를 덧셈하는 것이라면, 기본 구분자만으로 충분할 수 있다. 커스텀 구분자 기능을 추가하는 것은 코드의 복잡도를 높이고, 추가적인 예외 처리와 검증을 필요로 하게 만든다.
  • 특히, splitByCustomDelimiter 함수처럼 입력을 여러 단계로 분리하고 검증하는 과정은 코드의 이해도를 낮추고, 이후 유지보수 시 더 많은 시간과 노력이 필요하게 될 수 있다. 커스텀 구분자 기능을 추가한 것이 현재 요구사항에 비해 지나친 설계일 가능성이 있으며, 이는 오버엔지니어링의 사례가 될 수 있다.

▶확장성과 오버엔지니어링의 균형 잡기

확장성과 오버엔지니어링 사이의 균형을 잡기 위해서는 현재 요구사항에 충실하되, 미래의 변화에 대비할 수 있는 최소한의 설계를 고려해야 한다. 내 코드를 개선하기 위해 다음과 같은 접근이 가능하다.

  1. 요구사항 명확히 하기: 프로그램의 요구사항이 단순히 문자열 덧셈이라면, 커스텀 구분자 기능은 필요하지 않을 수 있다. 이 경우 기본 구분자만 사용하는 간단한 구조로 유지하는 것이 오히려 더 좋은 선택이다.
  2. 단순화와 필요에 따른 확장: 처음에는 간단한 기능으로 시작하고, 이후 실제로 커스텀 구분자가 필요하다는 요구사항이 추가되면 그때 확장하는 것이 좋다. 이처럼 현재의 요구에 맞는 단순한 설계를 먼저 하고, 실제 필요에 따라 기능을 확장해 나가는 것이 과도한 오버엔지니어링을 피할 수 있는 방법이다.
  3. SOLID 원칙 적용: 내 코드에서 구분자 처리와 덧셈 로직이 어느 정도 결합되어 있다. 이를 분리하여 각각의 기능이 독립적으로 동작하게 만들면 확장성과 유지보수성을 더 높일 수 있다. 예를 들어, 구분자 관련 로직을 별도의 클래스나 함수로 추출하여 관리하면 더 명확한 책임을 부여할 수 있다.

확장성과 오버엔지니어링은 개발자가 코드 설계 시 항상 고민해야 하는 문제이다. 확장성을 고려하지 않으면 코드가 변화에 유연하지 못하고, 반대로 확장성을 너무 과도하게 고려하면 오버엔지니어링으로 인해 코드가 불필요하게 복잡해질 수 있다. 이번 개발일지를 통해 확장성 있는 코드와 오버엔지니어링의 경계를 이해하고, 현재 요구사항에 맞는 최적의 설계를 고민하는 것이 중요하다는 점을 배웠다. 앞으로 코드를 작성할 때, 현재와 미래의 요구사항을 균형 있게 고려하며 불필요한 복잡성을 추가하지 않도록 노력해야겠다. 특히, 작은 기능부터 시작하여 필요에 따라 확장해 나가는 접근 방식을 통해, 코드의 유연성과 단순함을 동시에 추구하는 것이 좋은 설계라는 것을 다시 한번 다짐하게 되었다.