[우테코/5기/안드로이드] 1주차 프리코스 톺아보기

Falco·2022년 11월 17일
1

우테코 프리코스

목록 보기
1/5
post-thumbnail

1주차 피드백 🍏

요구사항을 정확히 준수한다

과제 제출 전에 기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항의 항목을 모두 잘 지켰는지 다시 한 번 점검한다.

커밋 메시지를 의미 있게 작성한다

커밋 메시지에 해당 커밋에서 작업한 내용에 대한 이해가 가능하도록 작성한다.
깃 컨벤션

  • 완벽하지는 않지만 각각의 커밋이 무엇을 의미하는지 정확하게 알 수 있다.

git을 통해 관리할 자원에 대해서도 고려한다

.class 파일은 java 코드가 있으면 생성할 수 있다. 따라서 .class 파일은 굳이 git을 통해 관리하지 않아도 된다.
IntelliJ IDEA의 .idea 폴더, Eclipse의 .metadata 폴더 또한 개발 도구가 자동으로 생성하는 폴더이기 때문에 굳이 git으로 관리하지 않아도 된다.
앞으로 git에 코드를 추가할 때는 git을 통해 관리할 필요가 있는지를 고려해볼 것을 추천한다.

  • .gitignore을 활용해 깃에 특정 파일, 폴더 등이 푸쉬되지 않게 막을 수 있다.

Pull Request를 보내기 전 브랜치를 확인한다

기능 구현 작업을 fork된 Repository의 main branch가 아닌, 기능 구현을 위해 새로 만든 브랜치에서 작업한 후 PR을 보낸다.
PR을 한 번 작성했다면 닫지 말고 추가 커밋을 한다

PR을 이미 한 번 보냈다면, 새로운 PR을 생성할 필요가 없다.

수정이 필요하다면 추가 커밋을 하면 자동으로 반영된다. 단, 미션 제출 기간 이후에는 추가 커밋을 하지 않는다.

이름을 통해 의도를 드러낸다

나 자신, 다른 개발자와의 소통을 위해 가장 중요한 활동 중의 하나가 좋은 이름 짓기이다. 변수 이름, 함수(메서드) 이름, 클래스 이름을 짓는데 시간을 투자하라.

  • 이름을 통해 변수의 역할, 함수의 역할, 클래스의 역할에 대한 의도를 드러내기 위해 노력하라. 연속된 숫자를 덧붙이거나(a1, a2, ..., aN),
  • 불용어(Info, Data, a, an, the)를 추가하는 방식은 적절하지 못하다.

축약하지 않는다

의도를 드러낼 수 있다면 이름이 길어져도 괜찮다.

누구나 실은 클래스, 메서드, 또는 변수의 이름을 줄이려는 유혹에 곧잘 빠지곤 한다. 그런 유혹을 뿌리쳐라. 축약은 혼란을 야기하며, 더 큰 문제를 숨기는 경향이 있다. 클래스와 메서드 이름을 한 두 단어로 유지하려고 노력하고 문맥을 중복하는 이름을 자제하자. 클래스 이름이 Order라면 shipOrder라고 메서드 이름을 지을 필요가 없다. 짧게 ship()이라고 하면 클라이언트에서는 order.ship()라고 호출하며, 간결한 호출의 표현이 된다.

  • 객체 지향 생활 체조 원칙 5: 줄여쓰지 않는다 (축약 금지)

공백도 코딩 컨벤션이다

if, for, while문 사이의 공백도 코딩 컨벤션이다.

공백 라인을 의미 있게 사용한다

공백 라인을 의미 있게 사용하는 것이 좋아 보이며, 문맥을 분리하는 부분에 사용하는 것이 좋다. 과도한 공백은 다른 개발자에게 의문을 줄 수 있다.

space와 tab을 혼용하지 않는다

들여쓰기에 space와 tab을 혼용하지 않는다. 둘 중에 하나만 사용한다. 확신이 서지 않으면 pull request를 보낸 후 들여쓰기가 잘 되어 있는지 확인하는 습관을 들인다.

의미 없는 주석을 달지 않는다

변수 이름, 함수(메서드) 이름을 통해 어떤 의도인지가 드러난다면 굳이 주석을 달지 않는다.
모든 변수와 함수에 주석을 달기보다 가능하면 이름을 통해 의도를 드러내고, 의도를 드러내기 힘든 경우 주석을 다는 연습을 한다.

IDE의 코드 자동 정렬 기능을 활용한다

IDE의 코드 자동 정렬 기능을 사용하면 더 깔끔한 코드를 볼 수 있다.

IntelliJ IDEA: ⌥⌘L, Ctrl+Alt+L
Eclipse: ⇧⌘F, Ctrl+Shift+F

  • Save Action과 같은 플러그인도 사용할 수 있다.

Kotlin에서 제공하는 API를 적극 활용한다

함수(메서드)를 직접 구현하기 전에 Kotlin API에서 제공하는 기능인지 검색을 먼저 해본다.
Kotlin API에서 제공하지 않을 경우 직접 구현한다.

예를 들어 사용자를 출력할 때 사용자가 2명 이상이면 쉼표(,) 기준으로 출력을 위한 문자열은 다음과 같이 구현 가능하다.

val members = listOf("pobi", "jason")
val result = members.joinToString(",") // "pobi,jason"

배열 대신 Kotlin Collection을 사용한다

Kotlin Collection 자료구조(List, Set, Map 등)를 사용하면 데이터를 조작할 때 다양한 API를 사용할 수 있다.

예를 들어 List<String>에 "pobi"라는 값이 포함되어 있는지는 다음과 같이 확인할 수 있다.

val members = listOf("pobi", "jason")
val result = members.contains("pobi") // true

코드 리팩토링 🧑🏻‍💻

Problem1.Kt

  • 상수 추출하기
    kotlin에서 상수는 대문자 스네이크 케이스를 활용해 표현한다.
private const val EXCEPTION = -1
private const val POBIWIN = 1
private const val CRONGWIN = 2
private const val DRAW = 0

Problem2.kt

  • 의미 있는 iterator 사용하여 의도 나타내기
for (i in 1 until cryptogram.length)  // X
for (idx in 1 until cryptogram.length) // O

Problem3.kt

  • 코틀린 Collection API를 사용하여 가독성 증가시키기
// 변경 전
fun count369InNum(num: Int): Int {
    val numList = listOf(3, 6, 9)
    var number = num
    var cnt = 0
    while (number > 0) {
        if (number % 10 in numList) {
            cnt += 1
        }
        number /= 10
    }
    return cnt
}

// 변경 후
private fun Int.countOf369(): Int {
    val digits = setOf('3', '6', '9')
    return this.toString().count { it in digits }
}

메인 솔루션.kt

	// 변경 전
    var answer = 0
    var num = number
    while (num > 2) {
        answer += count369InNum(num)
        num -= 1
    return answer
    
    // 변경 후
    return (2..number)
        .sumOf { it.countOf369() }

Problem4.kt

청개구리 문자로 바꾸어주는 함수

fun translateFroglang(s: Char): Char {
    if (s == ' ') {
        return ' '
    }
    if (!isAlphabet(s)) {
        return s
    }
    val frogLang = listOf(
        'A' to 'Z',
        'B' to 'Y',
        'C' to 'X',
        'D' to 'W',
        'E' to 'V',
        'F' to 'U',
        'G' to 'T',
        'H' to 'S',
        'I' to 'R',
        'J' to 'Q',
        'K' to 'P',
        'L' to 'O',
        'M' to 'N',
        'N' to 'M',
        'Z' to 'A',
        'Y' to 'B',
        'X' to 'C',
        'W' to 'D',
        'V' to 'E',
        'U' to 'F',
        'T' to 'G',
        'S' to 'H',
        'R' to 'I',
        'Q' to 'J',
        'P' to 'K',
        'O' to 'L',
    )
    if (s.code >= 'a'.code) {
        val answer = frogLang.find { it.first.toLowerCase() == s }?.second
            ?: throw IllegalArgumentException("$s 가 잘못된 입력값입니다.")
        return answer.toLowerCase()
    }
    return frogLang.find { it.first == s }?.second ?: throw IllegalArgumentException("$s 가 잘못된 입력값입니다.")
}

지금 보니 너무 어지럽다..

Char자료형에서 지원하는 code를 사용하여 변경

private fun Char.translateForgLang(): Char {
    if (isUpperCase()) {
        return ('Z'.code + 'A'.code - code).toChar()
    } else if (isLowerCase()) {
        return ('z'.code + 'a'.code - code).toChar()
    }
    return this
}
  • StringBuilder를 사용하여 String를 다루기
// 변경 전
fun solution4(word: String): String {
    var answer = ""
    for (i in word) {
        answer += translateFroglang(i)
    }
    return answer
}

// 변경 후 
fun solution4(word: String): String {
    val answer = StringBuilder()
    for (char in word) answer.append(char.translateForgLang())
    return answer.toString()
}

Problem6.kt

  • 예외 체크 과정 간략화
fun divideEmail(email: String): Pair<String, String> {
    var idx = 0
    for (i in email) {
        if (i == '@') break
        idx += 1
    }
    return email.subSequence(0, idx).toString() to email.subSequence(idx + 1, email.length).toString()
}

private fun checkNicknameIsKr(nickname: String): Boolean {
    val regex = Regex("^[가-힣]{1,20}$")
    return regex.matches(nickname)
}
    
private fun String.isDomainEmail(): Boolean = this == "email.com"

if (nickname.length <= 1) continue

예외 체크를 위해

  1. 이메일을 @기준으로 나누고
  2. 닉네임 체크하고
  3. 이메일이 정확한지 체크하고...
  4. 닉네임이 1글자 이하이면 continue하고

수많은 예외처리를 손수 한땀한땀 해준 모습

    for (i in forms.indices) {
        val email = forms[i][0]
        val nickname = forms[i][1]

        check(divideEmail(email = email).second.isDomainEmail()) {
            "$email email.com 도메인 형식을 지켜주세요."
        }
        check(email.length in 11..19) {
            "$email 이메일의 길이를 지켜주세요."
        }
        check(checkNicknameIsKr(nickname = nickname)) {
            "$nickname 이름의 형식을 지켜주세요."
        }
        
        // ...

심지어 해당 함수를 for문을 돌려서 일일히 check해주고 있었다.

지금 보니 이러한 소스는 전혀 코틀린스럽지 않았고, 다른분들의 소스를 참고하니 다음과 같이 해결했다.

private val EMAIL_FORMAT = "^[A-Za-z0-9._-]{1,9}@email.com\$".toRegex()
private val NICKNAME_FORMAT = "^[ㄱ-힣]{1,19}\$".toRegex()

forms.filter { form ->
	isValidEmail(form[0]) && isValidNickname(form[1])
}

이 모든 예외처리가 다음과 같은 정규식 두개면 충분했다.
또한 for문을 돌리는 것이 아니라 코틀린의 API를 활용하여 두줄로 줄여버렸다. 👍

Problem7.kt

  • 네이밍 변경
	// 변경 전
    val userList = getAllUserList(friends, visitors)
    val gradeTable = MutableList(userList.size) { 0 }
    val friendsList = getFriendsList(friends, user)
    val friendsOfFriendsList = getFriendsListOfFriends(friends, friendsList, user)
    
    // 변경 후
    val totalUser = getTotalUsers(friends, visitors)
    val gradeTable = MutableList(totalUser.size) { UserGrade(name = totalUser[it], grade = 0) }
    val direct = directFriends(friends, user)
    val inDirect = inDirectFriends(friends, direct, user)

friendsOfFriendsList는 너무하다 생각했다.

  • Pair, Map, HashMap보다 data Class를 활용하여 의미를 더 정확하게 전달하기
private data class UserGrade(
    val name: String,
    var grade: Int
)

val gradeTable = MutableList(totalUser.size) { UserGrade(name = totalUser[it], grade = 0) }

해당 객체에서 이름있는 아규먼트를 사용가능하기 때문에 보는이가 어떤 파라미터를 전달하는지 빠르게 알 수 있다.

  • 범위지정함수를 사용해 가독성 늘리기
inDirect.forEach { name ->
        gradeTable.find { it.name == name }?.apply {
            grade += 10
        }
}

apply는 람다 식을 수행한 결과 객체를 반환한다.

  • asSequence() 및 코틀린 API 활용하기
    return gradeTable
        .asSequence()
        .filter { it.grade != 0 }
        .sortedWith(compareBy({ -it.grade }, { it.name }))
        .map { it.name }
        .take(5)
        .toList()

코틀린 API만큼 잘 사용하면 편한게 없는 것 같다.

What I Learn 🤔

코틀린 콜렉션 API에 대해

  • map
public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
    return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}

인자로 받은 transform: (T) -> R를 실행해 기존의 리스트를 변형한 리스트를 반환한다.

  • flatmap
public inline fun <T, R> Iterable<T>.flatMap(transform: (T) -> Iterable<R>): List<R> {
    return flatMapTo(ArrayList<R>(), transform)
}
public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.flatMapTo(destination: C, transform: (T) -> Iterable<R>): C {
    for (element in this) {
        val list = transform(element)
        destination.addAll(list)
    }
    return destination
}

중첩 콜렉션에 대한 map함수를 실행 가능하다. take를 사용해 인자 중 특정 부분만 가져와 연산할 수 있다.

val nestedList = listOf(listOf(1,2),listOf(3,4),listOf(4,6))
print(nestedList.flatMap { it }) // [1, 2, 3, 4, 4, 6]
print(nestedList.flatMap { it.take(1) }) // [1, 3, 4]


val nestedList = listOf(listOf(listOf(1,2),listOf(3,4),listOf(5,6)),listOf(7,8),listOf(9,10))
println(nestedList.flatMap { it }) // [[1, 2], [3, 4], [5, 6], 7, 8, 9, 10]
println(nestedList.flatMap { it.take(1) }) // [[1, 2], 7, 9]
  • fold, reduce

foldreduce는 요소들을 왼쪽부터 오른쪽으로 Accumulate(누적)하는 작업을 수행한다.
하지만 fold는 지정해 둔 초기값으로 시작한다.

val numbers = listOf(7, 4, 8, 1, 9)

println(numbers.reduce { total, num -> total + num }) // 29 
println(numbers.fold(5) { total, num -> total + num }) // 34

이에따라 reduce는 빈 컬렉션에 대한 작업이 불가능하다.

val numbers = emptyList<Int>()

println(numbers.reduce { total, num -> total + num }) // UnsupportedOperationException Error!!
println(numbers.fold(5) { total, num -> total + num }) // 5

또한 첫번째 요소에 대한 작업수행에 대한 차이가 있다.

val numbers = listOf(5, 2, 10, 4)

println(numbers.reduce { total, num -> total + num * 2 }) // 29
println(numbers.fold(0) { total, num -> total + num * 2 }) // 34

reduce는 첫 번째 요소에 대한 *2작업을 수행하지 않는다. -> total이 첫번 째 element로 사용된다.
fold는 첫 번째 element에 total이 초기값으로 사용되어 정상 작동한다.

  • all, any, none
    filter 함수가 조건에 만족하는 원소를 리스트로 반환한다면,
    any : 조건을 만족하는 원소가 1개 이상 존재
    all : 모든 원소가 조건을 만족
    * none : 조건을 만족하는 원소가 없음
    를 확인한다.
val numbers = listOf(1,2,3,4,5)

println(numbers.all { it > 4 }) // false
println(numbers.none { it > 4 }) // false
println(numbers.any { it > 4 }) // true
  • groupby
public inline fun <T, K> Iterable<T>.groupBy(keySelector: (T) -> K): Map<K, List<T>> {
    return groupByTo(LinkedHashMap<K, MutableList<T>>(), keySelector)
}

groupBy는 keySelector 연산 (T) -> K를 수행한 후 이를 Map<K, List<T>>타입으로 반환해 준다. 여기서 Key는 group을 묶어줄 조건이며, Value는 Key 조건에 만족하는 원소들 리스트이다.

val numbers = listOf(
    FullName("김","승현"),
    FullName("이","해찬"),
    FullName("김","찬희"),
    FullName("이","지성"),
    FullName("신","성준"),
    FullName("강","태경"),
)

println(numbers.groupBy { it.firstName }) 
// { 김=[FullName(firstName=김, lastName=승현), FullName(firstName=김, lastName=찬희)],
// 이=[FullName(firstName=이, lastName=해찬), FullName(firstName=이, lastName=지성)],
// 신=[FullName(firstName=신, lastName=성준)], 강=[FullName(firstName=강, lastName=태경)]}

filter와 비슷하게 조건문을 넣어 true, false인 두 Map으로 나눌 수 있다.

val numbers = listOf(
    FullName("김","승현"),
    FullName("이","해찬"),
    FullName("김","찬희"),
    FullName("이","지성"),
    FullName("신","성준"),
    FullName("강","태경"),
)

println(numbers.groupBy { it.firstName == "김" }) 
// { true=[FullName(firstName=김, lastName=승현), FullName(firstName=김, lastName=찬희)], 
// false=[FullName(firstName=이, lastName=해찬), FullName(firstName=이, lastName=지성), FullName(firstName=신, lastName=성준), FullName(firstName=강, lastName=태경)]}
  • groupingBy, eachCount
    groupingBy는 콜렉션에서 keySelector 오퍼레이션을 수행해 해당 람다식에 따른 Grouping객체를 반환한다.
@SinceKotlin("1.1")
public inline fun <T, K> Iterable<T>.groupingBy(crossinline keySelector: (T) -> K): Grouping<T, K> {
    return object : Grouping<T, K> {
        override fun sourceIterator(): Iterator<T> = this@groupingBy.iterator()
        override fun keyOf(element: T): K = keySelector(element)
    }
}

해당 함수는 4개의 형식에서 지원하고 있다.

  • Iterable.groupingBy()
  • Array.groupingBy()
  • Sequence.groupingBy()
  • CharSequence.groupingBy()
val numbers = listOf(
    FullName("김","승현"),
    FullName("이","해찬"),
    FullName("김","찬희"),
    FullName("이","지성"),
    FullName("신","성준"),
    FullName("강","태경"),
)

val grouping = numbers.groupingBy { it.firstName }
println(grouping.javaClass.name) // AppKt$main$$inlined$groupingBy$1
println(grouping.eachCount()) // {김=2, 이=2, 신=1, 강=1}

Grouping객체에서는 eachCount(), reduce(), fold(), aggregate()와 같은 확장함수를 지원하고 있으며, sourceIterator()를 활용해 연산을 수행할 수 있다.

  • zip
    zip을 활용하여 두 List를 묶어 줄 수 있다.
    묵을 때 동일한 인덱스끼리 묶이므로 순서가 동일한 서로 연관된 리스트를 묶을 때 사용한다.
public inline fun <T, R, V> Iterable<T>.zip(other: Iterable<R>, transform: (a: T, b: R) -> V): List<V> {
    val first = iterator()
    val second = other.iterator()
    val list = ArrayList<V>(minOf(collectionSizeOrDefault(10), other.collectionSizeOrDefault(10)))
    while (first.hasNext() && second.hasNext()) { // first, second 리스트가 값이 있을때까지 while문을 지속
        list.add(transform(first.next(), second.next()))
    }
    return list
}

다음 예는 이름과 학번 리스트를 통합하는 예제이다.

val numbers = listOf(
    FullName("김","승현"),
    FullName("이","해찬"),
    FullName("김","찬희"),
)
val studentNumbers = listOf(
    2018215421,
    2018214231,
    2019245122
)

println(numbers.zip(studentNumbers))
// [(FullName(firstName=김, lastName=승현), 2018215421), (FullName(firstName=이, lastName=해찬), 2018214231), (FullName(firstName=김, lastName=찬희), 2019245122)]
  • asSequence는()

코틀린에서 Sequence는 Lazy evaluation를 제공하며 이는 지금 하지 않아도 되는 연산은 최대한 뒤로 미루고, 어쩔 수 없이 연산이 필요한 순간에 연산을 수행하는 방식이다.

예를 들어 한 선생님이 이름이 김으로 시작하며 이름순으로 정렬했을 때 3번 째 사람에게 발표를 시키고 싶다고 하자.

val students = listOf(
    FullName("김","승현"),
    FullName("이","해찬"),
    FullName("김","찬희"),
    FullName("이","지성"),
    FullName("신","성준"),
    FullName("강","태경"),
    // ...
)

학생 리스트는 다음과 같이 계속 이어지며 총 500명이 넘는다고 한다면, 3번 째 사람을 뽑기 위해 이 500명의 학생리스트에 대해 다음과 같은 작업을 수행해야한다.

val announcement = students
    .filter { it.firstName =="김" }
    .sortedWith(compareBy({it.firstName},{it.lastName}))
    .getOrNull(2)

이는 3번 째 사람을 뽑기위해 모든 학생을 필터링하고 정렬하는 것은 매우 비효율적이다. 이럴때 사용하는 것이 Sequence의 Lazy evaluation이다. 이는 take(2)와 같이 조건에 맞는 요소 개수가 정해져있을 때 불필요한 연산을 줄일 수 있기 때문이다.

val announcement = students
    .asSequence()
    .filter { it.firstName =="김" }
    .sortedWith(compareBy({it.firstName},{it.lastName}))
    .take(2)
    .last()
println(announcement)

StringBuilder란?

fun addString(word: String): String {
    var answer = ""
    for (i in word) {
        answer += i
    }
    return answer
}

위 소스에서 solution("FOUR")를 실행한다면 answer의 값은

"F"
"F" + "O"
"FO" + "U"
"FOU" + "R"
"FOUR"

순으로 변화될 것이다.
String으로 문자열을 합치면 이 계산과정 중 F, O, FO, U, FOU, R, FOUR의 7개의 메모리 공간이 사용되게 된다.

StringBuilder을 사용하면

fun solution4(word: String): String {
    val answer = StringBuilder()
    for (char in word) answer.append(char)
    return answer.toString()
}

내부적으로 Array를 활용하여 어레이 뒤에 Capacity를 확보하고 Char을 더하는 모습을 볼 수 있다.

    @Override
    public AbstractStringBuilder append(char c) {
        ensureCapacityInternal(count + 1); // Capacity 1 증가
        if (isLatin1() && StringLatin1.canEncode(c)) {
            value[count++] = (byte)c; // 늘어난 저장공간에 C 저장
        } else {
            if (isLatin1()) {
                inflate();
            }
            StringUTF16.putCharSB(value, count++, c);
        }
        return this;
    }

메모리 절약 가능

느낀점

지금까지 경험해온 언어 중 코틀린이 가독성이 가장 좋은 언어라고 생각한다. 내가 작성한 소스와 타인의 언어를 비교해 보았을 때 가독성의 차이와 읽을 때의 속도 차이가 엄청난걸 느꼈다.
또한 깃 컨벤션 및 코드 코틀린의 코딩컨벤션에 대해서도 다시한번 생각해 볼 수 있는 기회였다. 앞으로는 좀 더 가독성 좋은 코드를 작성하기 위해,

  • 좋은 변수명을 어떻게 지을지 좀 더 고민하기
  • 코틀린 API를 최대한 활용하기

를 항상 생각하며 코드를 작성하자.

Kotlin-onboading

profile
강단있는 개발자가 되기위하여

0개의 댓글