240424 Kotlin 강의 1주차 - 과제 계산기 완성하기, 코드리뷰 2일차

노재원·2024년 4월 24일
0

내일배움캠프

목록 보기
25/90

이번 주차의 과제인 강의에서 배우는 기본 Kotlin 문법을 활용한 계산기 만들기를 최대한 활용하는 거였는데 얼추 추상화로 연산을 추상화해서 Calculator 클래스의 연산 매개변수로 받아서 처리가 가능한지가 최종 과제의 목표 같아 오늘 얼추 작성을 했다.

아마 과제로 내기엔 문제 없을 정도고 어제자 TIL에 적은 추가적인 기능들을 적용하는게 이번 주의 개인 최종 목표가 될 것 같다.

여담으로 같은 조원 분들의 전체적인 공부는 아직 순조롭진 않으신 것 같다.
일단 노베이스에서 사전캠프-1주차 동안 써먹던 Html, CSS, Javascript를 머리에 집어 넣기도 어려우셨을 텐데 2주차에서 이제 겨우 3일째 되는 날 동안 긴 강의를 수강중이시고 오늘 특강도 연달아 있고 심지어 그 중 한 개는 알고리즘이라 너무 어렵다고 느끼신 것 같다. 나도 뉴비 시절의 고통이 아직도 머릿 속에 생생하게 남아 있어서 꺾이지 말라고 열심히 말씀드렸다.


2일차 코드리뷰

어제보다 다들 코드의 형태가 본격적으로 잡히신게 눈에 띄었다. 특히 어제는 조건문, 변수의 타입도 어색하셨었는데 껍데기, 오늘은 설계도라고 설명하기 시작한 클래스를 강의에 맞춘 형태로 써보셔서 실제로 작동이 안될 수는 있지만 작성하신 내용에서 응용하는 법을 다 같이 화면 띄워놓고 공유하는 시간을 가졌다.

다들 진도가 비슷하신 점은 다행히 행운으로 작용하는 것 같고 여기저기서 레퍼런스를 읽고 배운 지금의 클래스 = 설계도 비슷한 무언가, 인스턴스 = 설계도로 지은 건물 이라는 설명이 와닿으실진 모르지만 이걸로 응용하는 질문을 드렸을때 힌트만 있어도 치실 수 있으니 내일 당장 조금 까먹더라도 또 쳐보면서 머릿 속 실행 결과와 비교해보며 익숙해지실 수 있는 기반이 생기신 것 같다.

내 코드도 어지러운데 화면 공유로 보는 남의 코드가 어지럽다고 느껴지실 때도 있겠지만 아까 A님의 코드, 아까 B님의 코드 라고 표현을 해드려도 기억하고 이해해주셔서 내 부족한 말솜씨가 많이 커버가 되고 있다.

새삼 일주일만에 Html, Css, Javascript 보다가 Kotlin 으로 넘어와서 수 시간에 달하는 강의를 보시면서 어지럽고 피곤하실 수 밖에 없는데 최대한 시도하시는 팀원 분들의 열정이 엄청 대단하게 느껴졌다.

생성자에 (굳이) 욱여넣기

class Calculator {
        var num1: Double
        var num2: Double

        init {
            println("계산기에 저장할 첫번째 숫자 입력")
            num1 = readln().toDouble()
            println("계산기에 저장할 두번째 숫자 입력")
            num2 = readln().toDouble()
        }
}

Calculator 클래스에 예시에는 굳이 숫자를 인스턴스에 저장해두지 않는 것 같았는데 클래스의 이해를 설명할 때 멤버 변수가 없으면 Function 모음집으로밖에 보이지 않을 것 같아 나는 멤버 변수로 받는 걸로 설계했다.

그렇게 되면 따라오는 생성자 관련 이슈도 설명하기 위해 init을 채용해서 인스턴스의 생성 과정 설명에 살을 덧붙이려고 했는데 사실 입출력이 별 이유없이 메인이 아니라 클래스에서 이루어지는건 아무래도 지양해야하는 구조라고 생각하지만 생성자 관련 내용도 써먹긴 해야하니 굳이 써먹었다.

Interface 써서 매개변수로 써먹기

interface Operation {
	fun operate(num1: Double, num2: Double): Double
}
        
class AddOperation: Operation {
	override fun operate(num1: Double, num2: Double): Double = num1 + num2
}

// 그 외 Subtract, Multply 등등...

fun operation(operation: Operation): Double { /* */ }

Calculator 인스턴스를 만들어놓고 굳이 .add(AddOperation()) 같은 방식은 용납할 수가 없을 때 써먹기 좋은 추상화도 계산기에 간단하게 적용했다.
(사실 과제에서 써보라고 제시한 방법은 Abstract 클래스로 만드는 방식이지만 지금 당장은 Operation에 정의해둘 멤버도 없기 때문에 그냥 Interface로 작성했다.)

다만 사용을 한다면 calculator.operate(~Operation()) 의 형태일텐데 사실 이 것도 마음에 드는 건 아니다. Operation이 굳이 클래스로 관리될 필요도 없을 정도라 인스턴스로 매개변수를 넘기는 것도 별로고 과제 내용은 아니지만 iOS 개발하던 때였으면 enum 으로 처리했을 것이다.

일단 . 찍고 생각하게 만든 Swift식 enum 매개변수 설계를 굉장히 좋아하기 때문에 코틀린에서도 지원이 되는진 모르겠지만 나중에 과제가 조금 자유형식이 된다거나 프로젝트로 진행하게 되면 아마 코드에 내 스타일이 많이 녹아들 것 같다.


알고리즘 특강

Kotlin 트랙만을 위한 알고리즘 특강이 있었다. 보통 특강이 백엔드 트랙 전체를 대상으로 할 때가 많아서 코드를 보긴 힘들었는데 이번엔 Kotlin 코드를 기준으로 예시를 볼 수 있어서 좋았다.

사실 알고리즘을 전문적으로 판 적도 없고 앱 짜면서 코드 퀄리티를 고민할 때 퍼포먼스는 부적절한 함수 쳐내고 순회에 적절한 내장함수 채택하기 정도만 고민해도 딱히 구린 부분이 없어서 발전이 미약하다보니 머릿속에서 돌릴 수 있을 정도로 능력이 있진 않은데 같은 트랙 동기분들의 채팅을 보면 확실히 알고리즘에 강점이 있으신 분들이 꽤 많으신 것 같아 놀랬다. (나는 알고리즘 문제를 대하는 근성이 꽤 부족해서 포기가 빠르다.)

나도 계속 알고리즘 코드카타를 하고 있긴 하지만 다짜고짜 과제나 테스트로 별 예쁘게 찍기, 길 찾기, 이진 탐색같은 거 내주면 못짤거니 뉴비의 입장에서 들을 수 있는 재밌는 시간이 됐다.

그리고 특강중 처음 듣거나 내 지식이 부족한 키워드는 추가로 기록좀 해두기로 했다. 몇 개는 정보처리기사 문제로도 공부해서 아주 기초는 알지만 솔직히 평소에 써먹질 않아서 와닿진 않는다.

이번 코드카타를 끝까지 풀게 되면 프로그래머스 기준 100번은 넘길텐데 그제서야 배워서 써먹을 일이 생기지 않을까?

메모용

  • pseudo code (의사코드)
  • 이진 탐색
  • 다이나믹 프로그래밍
  • 깊이/너비 우선 탐색
  • 그리디 알고리즘
  • 세그먼트 트리
  • 다익스트라
  • N-Queen

코드카타 - 프로그래머스 시저 암호

어떤 문장의 각 알파벳을 일정한 거리만큼 밀어서 다른 알파벳으로 바꾸는 암호화 방식을 시저 암호라고 합니다. 예를 들어 "AB"는 1만큼 밀면 "BC"가 되고, 3만큼 밀면 "DE"가 됩니다. "z"는 1만큼 밀면 "a"가 됩니다. 문자열 s와 거리 n을 입력받아 s를 n만큼 민 암호문을 만드는 함수, solution을 완성해 보세요.

문제 링크

fun solution(s: String, n: Int): String = s.map { 
        if (it == ' ') it 
        else {
            (if (it.isUpperCase()) ('A'..'Z') else ('a'..'z'))
                .let { range ->
                    range.elementAt((range.indexOf(it) + n) % range.count())
                }
        }
    }.joinToString("")

ASCII 테이블처럼 'Z' -> 'a'로 이동한다던가 하는 건 없고 대문자, 소문자 안으로 범위를 제한해야 했기에 생각보다 골치가 아팠다. 사실 가장 먼저 하드한 방법으로 떠오른 건 계산용 index를 관리해서 'z' + 2를 하면 +2와 index를 가지고 지지고 볶고 해야하나? 싶었는데 이건 진짜 말도 안되는 소리라 금방 걸렀고

범위 밖을 벗어나도 처리 되는 범위 순회에 핵심을 두고 키워드를 찾고 나서야 Range와 나머지를 이용해서 적절한 순회를 처리했다.
(처음엔 Range를 List로 처리했는데 이후 Range 안에서도 충분히 순회와 탐색이 가능하길래 뺐다.)

약간 가독성은 떨어지지만 퍼포먼스도 괜찮은 거 아닌가? 싶기엔 이번에도 역시나 디테일이 살짝 딸렸다.

s.toList().joinToString(separator = "") {
            when (it) {
                in 'A'..'Z' -> ('A'.toInt() + (it.toInt() - 'A'.toInt() + n) % ('Z' - 'A' + 1)).toChar()
                in 'a'..'z' -> ('a'.toInt() + (it.toInt() - 'a'.toInt() + n) % ('z' - 'a' + 1)).toChar()
                else -> it
            }.toString()
        }
  1. 우선 joinToString 의 Transform 을 또 까먹고 안쓴 디테일도 조금 아쉽고
  2. when 채용도 진즉 생각했으나 Guard clause처럼 다룬다고 if-else로 작성한 것도 개인적으론 내 스타일 아니고 when이 여전히 가독성 좋은 데다가
  3. elementAt으로 어렵게 값을 찾을 게 아니라 생각해보면 그냥 Char에 toInt() 때려 박고 Range니 List니 이전에 Char의 활용을 극대화 했으면 훨씬 아름다웠을 것이다.

문제가 쓰라고 유도하는 ASCII 값을 냅두고 이렇게 지정된 범위 순회 에 매몰된 부분은 좀 아쉬움이 남는다.

그래도 내 코드가 가진 장점이라면 문제의 조건과 값 범위가 달라져도 (달라질 일 없지만) 대응이 쉽다는 장점이 있긴 하니까 나름 얻어가는 건 있다고 생각하기로 했다.

0개의 댓글