과제 제출 전에 기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항의 항목을 모두 잘 지켰는지 다시 한 번 점검한다.
커밋 메시지에 해당 커밋에서 작업한 내용에 대한 이해가 가능하도록 작성한다.
깃 컨벤션
.class 파일은 java 코드가 있으면 생성할 수 있다. 따라서 .class 파일은 굳이 git을 통해 관리하지 않아도 된다.
IntelliJ IDEA의 .idea 폴더, Eclipse의 .metadata 폴더 또한 개발 도구가 자동으로 생성하는 폴더이기 때문에 굳이 git으로 관리하지 않아도 된다.
앞으로 git에 코드를 추가할 때는 git을 통해 관리할 필요가 있는지를 고려해볼 것을 추천한다.
기능 구현 작업을 fork된 Repository의 main branch가 아닌, 기능 구현을 위해 새로 만든 브랜치에서 작업한 후 PR을 보낸다.
PR을 한 번 작성했다면 닫지 말고 추가 커밋을 한다
수정이 필요하다면 추가 커밋을 하면 자동으로 반영된다. 단, 미션 제출 기간 이후에는 추가 커밋을 하지 않는다.
나 자신, 다른 개발자와의 소통을 위해 가장 중요한 활동 중의 하나가 좋은 이름 짓기이다. 변수 이름, 함수(메서드) 이름, 클래스 이름을 짓는데 시간을 투자하라.
의도를 드러낼 수 있다면 이름이 길어져도 괜찮다.
누구나 실은 클래스, 메서드, 또는 변수의 이름을 줄이려는 유혹에 곧잘 빠지곤 한다. 그런 유혹을 뿌리쳐라. 축약은 혼란을 야기하며, 더 큰 문제를 숨기는 경향이 있다. 클래스와 메서드 이름을 한 두 단어로 유지하려고 노력하고 문맥을 중복하는 이름을 자제하자. 클래스 이름이 Order라면 shipOrder라고 메서드 이름을 지을 필요가 없다. 짧게 ship()이라고 하면 클라이언트에서는 order.ship()라고 호출하며, 간결한 호출의 표현이 된다.
if, for, while문 사이의 공백도 코딩 컨벤션이다.
공백 라인을 의미 있게 사용하는 것이 좋아 보이며, 문맥을 분리하는 부분에 사용하는 것이 좋다. 과도한 공백은 다른 개발자에게 의문을 줄 수 있다.
들여쓰기에 space와 tab을 혼용하지 않는다. 둘 중에 하나만 사용한다. 확신이 서지 않으면 pull request를 보낸 후 들여쓰기가 잘 되어 있는지 확인하는 습관을 들인다.
변수 이름, 함수(메서드) 이름을 통해 어떤 의도인지가 드러난다면 굳이 주석을 달지 않는다.
모든 변수와 함수에 주석을 달기보다 가능하면 이름을 통해 의도를 드러내고, 의도를 드러내기 힘든 경우 주석을 다는 연습을 한다.
IDE의 코드 자동 정렬 기능을 사용하면 더 깔끔한 코드를 볼 수 있다.
IntelliJ IDEA: ⌥⌘L, Ctrl+Alt+L
Eclipse: ⇧⌘F, Ctrl+Shift+F
함수(메서드)를 직접 구현하기 전에 Kotlin API에서 제공하는 기능인지 검색을 먼저 해본다.
Kotlin API에서 제공하지 않을 경우 직접 구현한다.
예를 들어 사용자를 출력할 때 사용자가 2명 이상이면 쉼표(,) 기준으로 출력을 위한 문자열은 다음과 같이 구현 가능하다.
val members = listOf("pobi", "jason")
val result = members.joinToString(",") // "pobi,jason"
Kotlin Collection 자료구조(List, Set, Map 등)를 사용하면 데이터를 조작할 때 다양한 API를 사용할 수 있다.
예를 들어 List<String>
에 "pobi"라는 값이 포함되어 있는지는 다음과 같이 확인할 수 있다.
val members = listOf("pobi", "jason")
val result = members.contains("pobi") // true
private const val EXCEPTION = -1
private const val POBIWIN = 1
private const val CRONGWIN = 2
private const val DRAW = 0
for (i in 1 until cryptogram.length) // X
for (idx in 1 until cryptogram.length) // O
// 변경 전
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() }
청개구리 문자로 바꾸어주는 함수
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
}
// 변경 전
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()
}
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
예외 체크를 위해
수많은 예외처리를 손수 한땀한땀 해준 모습
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를 활용하여 두줄로 줄여버렸다. 👍
// 변경 전
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
는 너무하다 생각했다.
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만큼 잘 사용하면 편한게 없는 것 같다.
public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}
인자로 받은 transform: (T) -> R
를 실행해 기존의 리스트를 변형한 리스트를 반환한다.
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
는 요소들을 왼쪽부터 오른쪽으로 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이 초기값으로 사용되어 정상 작동한다.
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
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=태경)]}
@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개의 형식에서 지원하고 있다.
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()
를 활용해 연산을 수행할 수 있다.
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)]
코틀린에서 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;
}
메모리 절약 가능
지금까지 경험해온 언어 중 코틀린이 가독성이 가장 좋은 언어라고 생각한다. 내가 작성한 소스와 타인의 언어를 비교해 보았을 때 가독성의 차이와 읽을 때의 속도 차이가 엄청난걸 느꼈다.
또한 깃 컨벤션
및 코드 코틀린의 코딩컨벤션에 대해서도 다시한번 생각해 볼 수 있는 기회였다. 앞으로는 좀 더 가독성 좋은 코드를 작성하기 위해,
를 항상 생각하며 코드를 작성하자.