본문 바로가기

우아한테크코스

[우아한테크코스] kotlin 프리코스 - 1주차 회고록

 

우아한 테크코스를 시작한지 일주일이 지났습니다.

kotlin을 한번도 쓰지 못한 주제에, 극한의 환경이 성장시킨다는 생각으로 바로 헤딩을 해버렸네요.

걱정이 돼서 우테코 시작하기 전날부터 kotlin 문법 공부도 어느정도 미래 해놓고 시작하니 덕분에 생각보단 덜 힘들었지만, 이게 몸풀기라 하니 더더욱 분발해야겠네요.

잡설을 마치고! 문제가 7개나 있으니, 그 중에서 기억에 남는걸 뽑아서 어떻게 풀었는지 확인해봅시다!

문제1

🚀 기능 요구 사항

포비와 크롱이 페이지 번호가 1부터 시작되는 400 페이지의 책을 주웠다. 책을 살펴보니 왼쪽 페이지는 홀수, 오른쪽 페이지는 짝수 번호이고 모든 페이지에는 번호가 적혀있었다. 책이 마음에 든 포비와 크롱은 페이지 번호 게임을 통해 게임에서 이긴 사람이 책을 갖기로 한다. 페이지 번호 게임의 규칙은 아래와 같다.

  1. 책을 임의로 펼친다.
  2. 왼쪽 페이지 번호의 각 자리 숫자를 모두 더하거나, 모두 곱해 가장 큰 수를 구한다.
  3. 오른쪽 페이지 번호의 각 자리 숫자를 모두 더하거나, 모두 곱해 가장 큰 수를 구한다.
  4. 2~3 과정에서 가장 큰 수를 본인의 점수로 한다.
  5. 점수를 비교해 가장 높은 사람이 게임의 승자가 된다.
  6. 시작 면이나 마지막 면이 나오도록 책을 펼치지 않는다.

포비와 크롱이 펼친 페이지가 들어있는 리스트/배열 pobi와 crong이 주어질 때, 포비가 이긴다면 1, 크롱이 이긴다면 2, 무승부는 0, 예외사항은 -1로 return 하도록 solution 함수를 완성하라.

제한사항

  • pobi와 crong의 길이는 2이다.
  • pobi와 crong에는 [왼쪽 페이지 번호, 오른쪽 페이지 번호]가 순서대로 들어있다.

실행 결과 예시

pobi crong result
[97, 98] [197, 198] 0
[131, 132] [211, 212] 1
[99, 102] [211, 212] -1

어떻게 접근했는가?

이런 유형의 알고리즘은 많이 접해봤다. 하지만 kotlin 첫 코딩이기에 조금 엉성하게 코딩을 했던 부분들이 있었던거 같다.

우선 전체적인 알고리즘은

1. 입력이 옳바른지 확인한다.

- 책의 페이지는 랜덤으로 정해진다.
- 책의 페이지는 1~400페이지 이다.
- 책의 왼쪽페이지는 홀수, 책의 오른쪽 페이즈는 짝수이며, 연속되는 숫자여야한다.
- 시작면과 마지막 페이지는 나오지 않는다.

이 조건을 만족하지 않는 경우에, -1을 리턴하고 종료한다.

fun checkException(pageNum: List<Int>): Boolean {
    return when ((pageNum[0] % 2 == 1) &&		//왼쪽 페이지가 홀수인지 확인
            (pageNum[1] == pageNum[0] + 1) &&		//왼쪽이랑 오른쪽이 연속인지 확인
            (pageNum[0] >= 3) &&			//첫 페이지 방지
            (pageNum[0] <= 397)) {			//마지막 페이지 방지
        true -> true
        false -> false
    }
}

위 처럼 기능 구현을 했다. 하지만, 지금 뒤돌아 보니 아쉬운 점이 보인다.

바로 가독성을 더 좋게 할 수 있었단 것이다.

fun checkException(pageNum: List<Int>): Boolean {
	if(pageNum[%]2 == 0){			//왼쪽 페이지가 홀수인지 확인
    	return false
    }
    if(pageNum[1] != pageNum[0] + 1){		//왼쪽이랑 오른쪽이 연속인지 확인
    	return false
    }
    if(pageNum[0] < 3){				//첫 페이지 방지
    	return false
    }
    if(pageNum[0] > 397){			//마지막 페이지 방지
    	return false
    }
    return true
}

이런 식이면 예외 조건을 확실히 더 보기좋게 할 수 있었다.

`when`을 처음 사용한게 신기하다보니 막 써보고 싶었나보다..

 

2. 포비/크롱이 각각 가지는 최대값을 가진다.

1. 왼쪽 페이지의 각 자릿수의 합 혹은 곱 중 큰 것을 구한다.
2. 오른쪽도 똑같이 구한다.
3. 비교해서 더 큰 값을 최대값으로 정한다.
fun compareLeftRight(pageNum: List<Int>): Int {
    val left = calculateMax(pageNum[0])	//왼쪽의 최대값 받아오기
    val right = calculateMax(pageNum[1])	// 오른쪽의 최대값 받아오기
    return when (left > right) {	//왼쪽과 오른쪽 크기 비교
        true -> left
        false -> right
    }
}

fun calculateMax(page: Int): Int {
    var num = page
    val sumAndProduct = mutableListOf(0, 1)	//각 자릿수의 합과 곱을 저장하는 리스트
    do {
        sumAndProduct[0] += num % 10	//뒷자리 숫자부터 하나씩 더하기
        sumAndProduct[1] *= num % 10	//뒷자리 숫자부터 하나씩 곱하기
        num /= 10			//앞으로 한칸 옮기기
    } while (num > 0)
    return when (sumAndProduct[0] > sumAndProduct[1]) {	//합과 곱 크기 비교
        true -> sumAndProduct[0]
        false -> sumAndProduct[1]
    }
}

 

3. 포빙와 크롱의 최대값을 비교하여 옳바른 출력을 구한다.

- 포비가 이기면 '1'
- 크롱이 이기면 '2'
- 무승부는 '0'

여기서 중요한 것은 if로 단순 비교를 하면 무승부를 확인하는 코드는 조금 가독성이 떨어지는 것이었다.

그래서 다음과 같이 해결했다.

fun match(pobi_num: Int, crong_num: Int): Int {
    return when (pobi_num - crong_num) {
        in 1..300 -> 1  	// 포비 > 크롱
        0 -> 0			// 크롱 == 포비
        else -> 2		// 크롱 > 포비
    }
}

이런 식으로 'when'을 이용하여 가독성을 증가시키고, 숫자의 차를 이용하여 패, 승, 무승부를 가린다.

 


문제7

🚀 기능 요구 사항

레벨 2의 팀 프로젝트 미션으로 SNS(Social Networking Service)를 만들고자 하는 팀이 있다. 팀에 속한 크루 중 평소 알고리즘에 관심이 많은 미스터코는 친구 추천 알고리즘을 구현하고자 아래와 같은 규칙을 세웠다.

  • 사용자와 함께 아는 친구의 수 = 10점
  • 사용자의 타임 라인에 방문한 횟수 = 1점

사용자 아이디 user와 친구 관계 정보 friends, 사용자 타임 라인 방문 기록 visitors가 매개변수로 주어질 때, 미스터코의 친구 추천 규칙에 따라 점수가 가장 높은 순으로 정렬하여 최대 5명을 return 하도록 solution 함수를 완성하라. 이때 추천 점수가 0점인 경우 추천하지 않으며, 추천 점수가 같은 경우는 이름순으로 정렬한다.

제한사항

  • user는 길이가 1 이상 30 이하인 문자열이다.
  • friends는 길이가 1 이상 10,000 이하인 리스트/배열이다.
  • friends의 각 원소는 길이가 2인 리스트/배열로 [아이디 A, 아이디 B] 순으로 들어있다.
    • A와 B는 친구라는 의미이다.
    • 아이디는 길이가 1 이상 30 이하인 문자열이다.
  • visitors는 길이가 0 이상 10,000 이하인 리스트/배열이다.
  • 사용자 아이디는 알파벳 소문자로만 이루어져 있다.
  • 동일한 친구 관계가 중복해서 주어지지 않는다.
  • 추천할 친구가 없는 경우는 주어지지 않는다.

실행 결과 예시

user friends visitors result
"mrko" [ ["donut", "andole"], ["donut", "jun"], ["donut", "mrko"], ["shakevan", "andole"], ["shakevan", "jun"], ["shakevan", "mrko"] ] ["bedi", "bedi", "donut", "bedi", "shakevan"] ["andole", "jun", "bedi"]

어떻게 접근했는가?

이 문제는 다른 문제들보다 세심함이 필요했다. `친구 추천`이기에 친구인 사람을 추천해서도 안되고 자기 자신이어도 안된다. 또한, 친구추천 점수도 계산을 하여 이중 정렬까지 해야한다. 그리고 객체를 이용하면 보다 더 편리하게 코딩할 수 있었다. 그러므로 다음과 같이 구현했다.

 

1. 내 친구 리스트를 만든다.

우선 나와 친구인 사람이 누구인지 알아야, 나와 친구가 아닌 사람을 구별할 수 있고, 또 친구의 친구가 누구인지 알 수 있으므로 필요한 과정이라 생각했다. 

여기서 주의할 것은 freinds리스트가 [나, 친구] 뿐만 아니라, [친구, 나] 이런식으로 저장될 수 있다.
그러므로, 내 이름의 위치의 반대쪽 사람을 추가해야 `내`가 아닌 `친구`가 정확히 추가된다. 
fun makeMyFriend(user : String ,friends : List<List<String>>): List<String>{
    val myFriend : MutableList<String> = mutableListOf()
    for(relation in friends){
        if(relation.contains(user)){			//나와 친구인 사람이라면
            myFriend.add(when(relation[0]==user){	//친구관계에서 친구를 골라내는 작업
                true -> relation[1]
                false -> relation[0]
            })
        }
    }
    return myFriend
}

 

2. 나와 친구가 아닌 친구의 친구를 추천 집계판에 넣는다.

1번에서 거친 친구리스트를 이용하여, friends에서 나와 친구가 아닌 친구의 친구를 점수와 함께 추천리스트에 넣는 것이다.

단, 여기서 그 친구가 이미 리스트에 있는지 없는지 확인해서, 없으면 새로 추가하고, 있으면 기존에 점수를 추가하는 식으로 진행해야한다.  

참고로, 나의 친구와 나의 친구의 친구관계도 걸러줘야 한다.

이 과정은 다음 차례에 확인한다.

fun addFriend(user: String,
              friends: List<List<String>>,
              myFriend :List<String>){
    for(i in friends){				//친구 관계 탐색
        if(!i.contains(user) &&		//나와 친구인 사람은 있으면 안된다.
            (myFriend.any{ it == i[0] } xor myFriend.any{it == i[1]} )){ //둘 다 친구인 경우는 제외
            when(myFriend.any{ it == i[0] }){		//나와 친구 사람의 인덱스를 찾아 반대 인덱스를 추가
                true -> ScoreCalculate.calculate(i[1],10)
                false ->ScoreCalculate.calculate(i[0],10)
            }
        }
    }
}

 

3.친구 추천 집계판에 이름과 점수를 기입한다.

위의 과정에서 친구 추가 리스트에 넣을 사람을 구했다면 다음과 같은 코드로 친구 추천 리스트를 만드면 된다.단, 여기서 이미 집계판에 있는지 없는지 확인해서 새로 추가하는지, 기존 점수에 더하는지 확인을 해야 동일 인물의 점수가 분리돼서 저장되는 참사를 막을 수 있다.

object ScoreCalculate{
    var Scores = mutableListOf<Score>()		// 친구 추천 집계판

    fun calculate(name : String, score : Int){
        when(Scores.any{it.name==name}){	//집계판에 이미 있는 사람인지 확인
            true -> addOld(name, score)
            false -> addNew(name, score)
        }
    }

    private fun addNew(name :String, score : Int){
        val new = Score(name, score)
        Scores.add(new)
    }

    private fun addOld(name : String, score : Int){
        for(i in Scores){
            if(i.name==name){
                i.score+=score
                break
            }
        }
    }
}

data class Score(val name:String, var score:Int)	//점수 데이타 클래스

개인적으로 가장 기억에 남는 코드 부분이다.

이유는 처음으로 kotlin에서 객체를 이용하기도 했지만, 더 큰 이유는 바로 데이타 클래스다.

 

데이타 클래스는 기존 자바의 불편함을 한번에 해소시킨 기분이 들었다. 그냥 맨 밑줄의 코드처럼 쓰면 .toString() 과 같은 기본적인 기능 함수들이 자동적으로 내장돼서 생성된다.

자바였으면 생성자부터 메소드까지 다 했어야했는데... kotlin.. 왜 너가 곽광받는지 알겠다.

 

4. 친구가 아닌 방문자들도 추천리스트에 넣는다.

2번 과정인 친구관계에서 꺼내오는 것 보단 훨씬 간단하다.방문자들 중에서 친구인 사람을 싹 다 제외하고, 남은 사람들을 하나씩 `calculate`하면 됐다.

fun addVisitor(visitors : List<String>, myFriend: List<String>){
    val visitorList  = visitors.toMutableList()
    for(i in myFriend){			//친구인 사람들 제거
        visitorList.remove(i)
    }
    for(i in visitorList){		//남은 사람들 친구 추천 집계판에 추가
        ScoreCalculate.calculate(i,1)
    }
}

 

5. 추천 리스트를 이중정렬한다.

4번 과정으로 완성된 리스트를 점수 내림차순하고 동점인 경우에는 이름으로 오름차순 정렬을 한다.

fun sortAsc(scores : MutableList<Score>): MutableList<Score>{
    scores.sortWith(
        compareBy(
            {-it.score},	//점수 내림차순
            {it.name}		// 이름 오름차순
        )
    )
    return scores
}

여기서 포인트는 바로 `sortWtih()` 와 `compareBy()` 이다.

sortWith()로 정렬을 하는데, 그 기준을 compareBy()의 괄호안에 있는 순서에 맞춰 오름차순으로 하는 것이다.

물론 내림차순도 가능하다. `compareByDescending()`을 이용하거나 위의 코드처럼 ` - `를 이용하여 역순으로 지정해줄 수 있다.

 

6. 최대 5명까지 출력한다.

이제 마무리다. 정렬한 최종 리스트를 앞에서 5명까지만 출력하도록 코드를 짜는 간단한 부분이다.

물론 없는 5명까지이므로 5명을 못채우는 경우( ex_ 1명, 0명) 에도 출력이 된다.

fun makeRank(scores : MutableList<Score>) : List<String>{
    val rank = mutableListOf<String>()
    var count = 0
    for(i in scores){
        rank.add(i.name)//최종 랭크 리스트
        count++
        if(count ==5)	//5명 까지만
            break
    }
    return rank
}

근데 여기서 살짝 아쉬운 거는 `count`를 써서 5명까지를 잘랐는데, 생각해보니 굳이 count없이 rank.size()가 5가 되는 경우 break가 되도록 짜서 인스턴스를 줄이는 방법도 있었다는 것이다. 역시 이런건 되돌아봐야 보이네..


회고록을 마치며...


내 인생의 첫 kotlin은 생각보다 놀라웠다. 기존에 쓰던 자바보다 훨씬 문법이 직관적이며 간단하고 편리했다.

그리고 이번에 우테코에서 제시한 코딩 컨벤션을 지키면서 코딩을 해보니 많은 점을 느꼇다.

 

내가 하던 코딩 스타일은 '우선 구동이 되는게 중요하지!' 느낌으로 구동 자체에만 집중한 코딩, 즉 '막코딩' 스타일이었다.

이런 스타일은 그때 당장에는 빠르게 문제를 해결할 수 있어도, 나중에 협업을 하거나 유지보수를 할 때, 굉장히 치명적인 문제로 돌아올 수 있다는 것을 느꼈다.

 

그렇기에 우테코에서 제시한 코딩 컨벤션을 지켜 코딩을 해보니, '와... 이걸 내가 했다고? 엄청 깔끔하잖아?' 라는 느낌이 들었다. 전에 신경을 덜 쓰거나 신경쓰지 않았던 '인스턴스 변수 개수', '들여쓰기', 'else 예약문', 심지어는 '변수명' 까지 시간을 들여 신경썼더니  아주 흡족한 결과가 나왔다.

 

그래도 회고록을 쓰다보니 아직 고칠 점이 몇개 보이긴 하다... 아직 갈길은 멀지만 그 길은 보람찰 거 같다.

 

Github 주소 : https://github.com/kyounggudev/kotlin-onboarding/tree/kyounggudev