ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 자동완성 없이 코딩하기
    iOS 앱 개발 부트캠프/TIL 2024. 10. 30. 23:42

     

    이번주 들어 계속 계산기 로직 만들기를 하고 있었다. 그러다 문득 든 생각이 '이거 정말 내가 쓴 코드 맞나?' 였다.

    xcode도 swift도 접한지 한 달 반 정도 되었는데 처음 접할때부터 이미 xcode에 ai 자동 완성 기능이 들어있었다.

    내가 알아서 할 게 좀 기다려

    배운 개념을 간단하게 실습해보는 수준일 땐 어렵지는 않은데 문법이 익숙치 않다 싶을 때마다  ai가 자동으로 완성해줘서 편하다고만 생각했었다.

    그런데 개념만 배우는게 아니라 배운 걸 활용해서 간단한 것이라도 무언가 만들어 보려고 하니까, 내가 생각해서 코드를 쓰기 전에 알아서 완성해버리니 실력이 느는건지 알 수 없다고 느껴지기 시작했다. 간단한 코드도 자동완성 없이 내가 진짜 할 수 있나 싶었다.

    그래서 이번주에 하던 계산기 로직을 ai 자동완성을 끄고 처음부터 다시 해보는 시간을 가졌다.


    AI 자동완성 끄기

    자동완성을 끄는 건 간단했다. Xcode의 Settings에서 Components에 들어가서 Other Components에 있는 Predictive Code Completion Model을 선택 후, 좌측 하단의 + - 버튼에서 - 버튼을 누르면 나오는 Disable predictive code completion? 팝업에서 Disable만 누르면 자동완성 기능이 Off 되었다.

    한가지 걱정은 코드를 완성시켜버리는 기능만 끄고 변수명이나 함수명, 클래스명 같은 건 오타로 인한 에러 방지를 위해서라도 계속 켜져있었으면 했는데 전부 다 꺼져서 한글자 한글자 전부 타이핑 해야하나 싶었다.  다행히도 그 기능은 AI 자동완성을 끈 후에도 그대로 작동하였다.


    Lv1 & Lv2) Calculator 클래스를 만들어 사칙연산과 나머지 연산 구현하기

    자동 완성을 끄고 코드를 쓰니 첫 함수부터 바로 에러가 발생했다. 다행히 지금까지의 시간이 헛되진 않았는지 무슨 에러인지 바로 알아챘다. 반환 타입을 작성하지 않고 return을 해서 생긴 에러인 것이다.

    반환 타입을 정해주니 에러가 사라졌다. 이정도는 쉽다.

    이후 사칙연산을 하다보면 소수점 자리도 나오니 Int 타입으로 선언한 함수들의 매개변수와 반환값을 Double로 변경하였다. 애초에 계산도 소수점 자리도 계산하고 싶을 수도 있기 때문이다.

    그리고 연산을 진행할 숫자도 first와 second라는 변수에 저장한 뒤 함수를 호출하며 바로 값을 넣어주었다. 간단한 코드이니 따로 변수에 저장하지 않아도 됐겠지만 일일이 숫자를 넣는 것 보다 변수에 지정해 넣는 게 다른 사람이 보기에도 이해하기 쉽고, 에러 발생시 대처가 더 쉬우니 습관을 들이기 위해서 변수를 만들어 저장하였다.

    print 함수를 쭉 작성하다 보니 모든 print 함수에서 에러가 났는데, 보니까 함수의 매개변수는 일부러 생각해서 Double로 지정해놓고 first와 second의 타입을 실습하던 습관 그대로 Int로 선언한 뒤 값을 집어 넣어 에러가 났다. 

    습관을 들이기 위해 숫자를 일일이 넣지 않고 굳이 변수에 할당한 건데 그러다 에러가 난 것이 웃기지만 아이러니 하게도 습관을 들이려던 의도대로 변수의 타입만 바꿔주면 모든 print함수의 에러가 사라질 것이다.

    잘 작동하는 것을 확인하고 Lv2를 바로 진행하였다. Lv2는 여기에 나머지 연산이 가능하도록 코드를 추가하고 연산을 진행 해 출력하는 것이다.

    전에 해봐서 알지만 Double이나 Float 같은 실수 타입은 % 연산이 안되어서 truncatingRemainder(dividingBy: )를 써야한다. 이 부분만 주의하면 Lv2도 그리 어렵지 않다.

    Calculator 클래스에 나머지 함수 부분을 추가하고, 가독성을 위해 출력을 조금 수정해서 나머지 연산까지 출력하였고, 잘 작동하는 것을 확인하였다.

    Lv3) 사칙연산 클래스를 따로 만들어 Calculator 클래스와 관계 맺기

    여기서부터가 문제이다.. 클래스간의 관계를 고려하여 Calculator 클래스와 관계 맺기.

    정확히 뭘 하라는 걸까? 클래스간의 관계 라는 개념이 존재하는데 내가 모르는가보다.

    검색해보니 클래스간의 관계에는 상속(inheritance)와 구성(composition)이 있다고 한다.

    상속

    상속은 클래스가 다른 클래스로부터 특성을 물려받는 관계를 말하는데, is-a 관계를 표현하며 관계된 클래스들을 부모 클래스와 자식 클래스로 표현할 수 있다.

    계산기를 만드는 해당 과제의 경우, 사칙 연산에 해당하는 각 클래스가 공통된 로직이나 속성을 공유하기 때문에, 기본 연산 기능을 포함하는  Operation 이라는 부모 클래스를 만들고 AddOperation이나 SubtractOperation 같은 하위 클래스가 이를 상속하는 식으로 활용할 수 있을 것이다.

    // 예시
    class Operation {
        func calculate(_ first: Double, _ second: Double) -> Double {
            return 0
        }
    }
    
    class AddOperation: Operation { // Operation을 상속
        override func calculate(_ first: Double, _ second: Double) -> Double {
            return first + second
        }
    }

    위 코드는 적당히 작성한 예시로, 사칙연산의 클래스들은 '숫자 두 개를 받아 연산을 한 뒤 결과값을 반환'한다는 것이 공통된 부분이므로, Operation이라는 부모 클래스를 만들고 해당 부분을 구현한 뒤 AddOperation이 이를 상속받아 override로 return 부분을 바꿔 더하기 연산을 하는 클래스로 만든 것이다.

    지금은 구현하는 로직이 단순하여 체감이 잘 안되지만, 이런식으로 공통된 부분을 부모 클래스에 만들어 두고 상속을 통해 필요한 부분만 일부 바꿔 모듈화 한다면 Calculator 안에 모든 연산 기능이 들어있을 때보다 중복 코드를 줄이고 재사용성을 높이는데 유용하며 가독성도 더 높아진다.

    구성

    구성은 클래스가 다른 클래스의 인스턴스를 포함하여 기능을 사용하는 관계이다. has-a 관계라고도 불리며 특정 기능을 필요에 따라 조합하는 방식이다.

    해당 과제에서 예시로 들면, Calculator 클래스는 AddOperation이나 SubtractOperation 같은 연산 클래스를 인스턴스로 포함하여 구성할 수 있다.

    class Calculator {
        var addOperation: AddOperation
        var subtractOperation: SubtractOperation
    
        init() {
            self.addOperation = AddOperation()
            self.subtractOperation = SubtractOperation()
        }
    }

    위처럼 작성하는 것이 바로 구성인데, 이전에 작성해보던 계산기 로직에서 내가 했던 것이 바로 이것이었다.

    나는 이걸 'Calculator 클래스 안에 각 연산 클래스의 인스턴스를 생성하기는 한데, 이게 관계 맺기 말하는 거 맞나?' 라고 생각 했었는데 '구성' 이라는 관계 맺기를 한 것이었다.

    이러한 구성을 통해 Calculator는 연산을 직접 수행하지 않고, 각 클래스에 구현을 맡긴 뒤 호출을 통해 기능을 가져다 쓰는 형태이다. 따라서 만약 더하기의 로직을 바꾸거나 대체해야 할 때, Calculator를 직접 변경하는 게 아니므로 책임이 분리되고, 유지 보수가 쉬워지며, 각 클래스는 하나의 역할에만 집중할 수 있게 되므로 코드가 더 확장성있고 관리가 쉬워지는 것이다.

    Calculator에 전부 필요한 연산 기능이 들어있던 Lv1이나 Lv2의 코드의 경우, 다른 새로운 클래스를 만든다고 가정할 때 해당 연산 기능이 필요하면 같은 코드를 중복해서 작성해야 한다. 반면 각 클래스로 분리해놓고 구성을 통해 필요한 기능을 가져다 쓴다면 코드 중복을 줄일 수 있고  재사용성이 높아진다고 할 수 있다.

    그런데 한가지 의문이 들었다. 인스턴스를 생성하고 초기화 하는 방법에,

    class Calculator {
        var addOperation: AddOperation = AddOperation()
        var subOperation: SubtractOperation = SubtractOperation()
        var mulOperation: MultiplyOperation = MultiplyOperation()
        var divOperation: DivideOperation = DivideOperation()
        var remOperation: remainderOperation = remainderOperation()
    }
    //혹은 var addOperation = AddOperation() 같은 타입 추론 방식으로도 가능해 보인다.

    이처럼 선언과 동시에 초기화 하는 방법이 있고, 좀더 위에 쓴 예시에서처럼 init() 을 사용해

    class Calculator {
        var addOperation: AddOperation
        var subOperation: SubtractOperation
        var mulOperation: MultiplyOperation
        var divOperation: DivideOperation
        var remOperation: remainderOperation
        
        init() {
            self.addOperation = AddOperation()
            self.subOperation = SubtractOperation()
            self.mulOperation = MultiplyOperation()
            self.divOperation = DivideOperation()
            self.remOperation = remainderOperation()
        }
    }

    이렇게 초기화 하는 방법이 있다. 뭐가 다른 걸까?

    전자의 경우에는 선언과 동시에 인스턴스를 생성하기 때문에 초기화 시점에서 값을 고정적으로 설정해야 한다. 따라서 외부에서 입력값을 받아 동적으로 속성들을 설정 할 수 없는 반면에, 후자의 경우 init()을 통해 외부에서 매개변수를 통해 값을 넘겨 받을 수 있어 속성 값을 유동적으로 설정할 수 있게 된다.

    이 과제처럼 사용법이 정해져 있고 로직도 단순한 경우엔 전자의 경우처럼 하는게 간결해 보이고 단순해서 편할 순 있지만 상속이 될 경우 각 속성의 초기값을 다르게 지정하려 할 때 재정의가 어려워지고 동적으로 활용하기 어려워 애써 늘린 코드 유연성의 장점이 희미해지므로 후자의 경우를 애용해야 하겠다. 

    Lv3 코드 완성

    이로써 코드는 다음과 같이 완성하였다.

    이제 Lv2와 비교했을 때, 사칙연산 기능이 각각의 클래스로 분리되어 하나의 기능만을 담당하게 되었으므로 책임이 분리되었다고 할 수 있다. 따라서 어디에서 어떤 기능을 수행하는지 알아보기 좀 더 편해졌으므로 가독성이 좋아졌다고 할 수 있고, 어딘가 코드의 변경이 생겼을 때 그 파급 효과가 비교적 약할 것이므로 안정성이 향상되고 유지보수가 용이해졌다고 할 수 있다.

    또한 새로운 코드를 추가하려고 할 때, 해당 연산 기능들이 필요하면 Calculator처럼 구성을 통해 관계를 맺어 사용 할 수 있으므로 코드의 유연성과 확장성이 늘었다고 할 수 있으며, 코드를 다시 써야할 필요가 없으므로 코드 중복이 줄고 재사용성이 늘어났다고도 할 수 있다. 

Designed by Tistory.