반응형

Workfolio 미리보기 https://www.workfolio.kr

1.  왜 Workfolio를 만들게 되었나

이직을 준비할 때마다 같은 고통이 반복됐다.

이력서를 업데이트하려고 앉으면, 기억나는 건 최근 2~3개월뿐이다. 6개월 전에 끝낸 프로젝트에서 내가 무슨 역할이었는지, 어떤 문제를 해결했는지, 성과가 뭐였는지 — 전부 흐릿하다. Notion에 적어둔 것도 있고, Slack 메시지에 묻힌 것도 있고, 아예 기록하지 않은 것도 많았다.

결국 이력서에는 "~프로젝트 참여"같은 추상적인 한 줄만 남고, 면접에서 "구체적으로 어떤 기여를 하셨나요?"라는 질문에 버벅이게 된다.

 

문제를 정리하면 세 가지였다:

    문제 1. 기록이 흩어져 있다

    프로젝트마다 다른 도구를 쓰고, 회고는 팀 Confluence에 있고, 개인 성과는 메모장에 있다. 시간이 지나면 어디에 뭘 적었는지조차 기억이 안 난다.

 

    문제 2. 기록에서 이력서까지의 간극이 너무 크다

    매일의 업무 로그가 있다 해도, 이걸 이력서 형태로 정리하려면 매번 큰 노력이 필요하다. "어떤 프로젝트에서 어떤 역할로 무슨 성과를 냈는지"를 이력서 문장으로 바꾸는 건 별도의 작업이다.

 

    문제 3. 이직 과정 자체를 관리할 곳이 없다

    지원한 회사, 면접 진행 상황, 합격/불합격 여부, 연봉 협상 — 이 모든 걸 엑셀이나 메모장에서 관리하고 있었다. 이전 이직에서 어떤 회사에 지원했고 어디서 떨어졌는지 돌아볼 수도 없었다.

 

Workfolio는 이 세 가지 문제를 하나의 흐름으로 해결하기 위해 만들었다.

매일의 업무를 기록하면 → 그 기록이 이력서가 되고 → 이직 과정까지 한 곳에서 관리할 수 있는 플랫폼.

2.  문제를 해결하기 위해 만든 기능들

2-1. 업무 기록 시스템 — "기억에 의존하지 않는 커리어 관리"

가장 먼저 만든 건 구조화된 업무 기록 기능이다.

자유롭게 적는 메모가 아니라, 나중에 이력서로 변환할 수 있도록 템플릿 기반으로 기록하게 했다. 총 7개의 기록 템플릿을 만들었다.

 

일반 기록용:

  • 주간 회고 — Done(한 일), Problem(문제), Solution(해결), Achievement(성과), Lesson(교훈)
  • 프로젝트 회고 — Goal(목표), Role(역할), Problem(문제), Solution(해결), Achievement(성과)
  • 성과 기록 — Problem(문제), Action(행동), Result(결과), Metric(지표 변화)
  • 일일 업무 기록 — 오늘 한 일, 내일 할 일, 이슈
  • 자유 기록 — 형식 없이 자유롭게

프로젝트 기록용 (이력서 변환의 핵심):

  • 프로젝트 개요 — 서비스 설명, 역할, 팀 구성, 기술 스택
  • 업무(Task) 기록 — 업무 내용, 과정, 결과

왜 프로젝트용 템플릿을 따로 만들었냐면, 이력서로 변환할 때 프로젝트 개요에서 큰 틀을 잡고, 개별 Task 기록에서 구체적인 성과를 뽑아내는 구조가 필요했기 때문이다.

 

기록은 기록 그룹으로 묶인다. 예를 들어 "결제 시스템 리뉴얼" 그룹을 만들고, 그 안에 프로젝트 개요 1개 + Task 기록 여러 개 + 프로젝트 회고 1개를 쌓아가는 식이다. 그룹마다 색상을 지정할 수 있어서 캘린더에서 한눈에 구분된다.

2-2.  기록을 이력서로 변환하기 — "쌓은 기록이 이력서가 되는 순간"

이게 Workfolio의 핵심 기능이다.

이력서 편집 화면에서 "기록에서 가져오기" 버튼을 누르면, 그동안 쌓아둔 프로젝트 기록 그룹 목록이 나온다. 원하는 그룹을 선택하면 그 안의 기록들이 체크박스와 함께 표시되고, 가져올 기록을 선택하면 자동으로 이력서 프로젝트 항목으로 변환된다.

 

변환 과정은 이렇다:

  1. 프로젝트 개요 기록에서 → 서비스 설명, 역할, 팀 구성, 기술 스택을 추출
  2. Task 기록들에서 → 각각의 업무 내용, 과정, 결과를 bullet point로 정리
  3. 프로젝트 회고 기록에서 → 최종 성과/성취를 추출
  4. 모든 기록의 날짜를 계산해서 → 프로젝트 기간을 자동 산출

결과적으로 "결제 시스템 리뉴얼" 그룹의 기록 10개가 → 이력서의 프로젝트 한 섹션으로 깔끔하게 변환된다. 소속, 역할, 기술 스택, 상세 업무, 성과가 이력서 형식에 맞게 자동 구조화된다.

이 기능을 만들면서 가장 고민했던 건 "어느 수준까지 자동화할 것인가"였다. 처음에는 기록을 넣으면 완전히 자동으로 이력서 문장을 만들어주려 했지만, 사용자마다 강조하고 싶은 포인트가 다르다는 걸 깨달았다. 그래서 자동 변환 + 수동 편집의 중간 지점을 택했다. 틀은 자동으로 잡아주되, 세부 내용은 사용자가 다듬을 수 있게 했다.

2-3.  이력서 완성도 추적 — "뭘 빠뜨렸는지 알려주기"

이력서를 작성하다 보면 어떤 섹션을 안 채웠는지 놓치기 쉽다. 그래서 대시보드에 이력서 완성도를 퍼센트로 보여주는 기능을 넣었다.

8개 섹션(기본 정보, 포지션, 경력, 학력, 프로젝트, 활동/자격증, 어학, 자기소개)을 체크해서 몇 퍼센트가 채워졌는지, 어떤 섹션이 비어 있는지를 안내한다. "프로젝트 섹션이 비어 있습니다" 같은 가이드가 있으니 자연스럽게 빈 곳을 채우게 된다.

2-4.  PDF 다운로드 — "이력서를 파일로 뽑아내기"

완성된 이력서를 PDF 파일로 다운로드할 수 있다.

웹에서 보는 이력서와 동일한 디자인을 PDF로 그대로 옮기는 게 관건이었다. 웹 렌더링용 컴포넌트와 PDF 렌더링용 컴포넌트를 따로 만들었는데, 데이터 준비 과정은 동일하게 공유했다. 이력서 데이터를 하나의 포맷팅 함수에서 정리한 뒤, 웹 템플릿과 PDF 템플릿이 각각 자기 방식으로 렌더링하는 구조다.

PDF에서는 프로필 이미지, 경력 사항의 연봉 이력, 프로젝트 상세, 학력, 활동, 어학 — 모든 섹션이 A4 사이즈에 맞게 페이지 분할까지 자동으로 처리된다. 다운로드 시 파일명은 {이력서이름}_이력서.pdf로 저장된다.

시행착오가 있었던 건 한글 폰트 처리였다. PDF 렌더링 라이브러리가 기본적으로 한글을 지원하지 않아서, Pretendard 폰트를 별도로 등록하는 과정이 필요했다.

2-5.  URL로 이력서 공유하기 — "링크 하나로 이력서 전달"

채용 담당자에게 이력서를 보낼 때 PDF를 첨부하는 것도 방법이지만, 링크 하나로 항상 최신 이력서를 보여주는 것이 더 편리하다.

이력서의 "공개" 토글을 켜면 고유한 공개 URL이 생성된다. workfolio.kr/resumes/{publicId} 형태의 링크를 복사해서 전달하면, 로그인 없이 누구나 이력서를 볼 수 있다.

공개/비공개 전환이 자유롭다. 면접 기간에만 공개하고 끝나면 비공개로 돌리면 된다. 비공개로 바꾸면 해당 URL은 더 이상 접근할 수 없다.

이 기능에서 중요했던 건 공개 이력서는 인증 없이 접근 가능해야 한다는 점이었다. 일반 API는 로그인이 필요하지만, 공개 이력서 조회는 별도의 익명 API를 통해 비인증 상태에서도 열람할 수 있게 분리했다.

또한 공개 이력서에는 이력서 템플릿을 적용할 수 있다. URL에 템플릿 slug를 포함시켜(/resumes/{publicId}/{templateSlug}) 같은 이력서를 다른 디자인으로 보여줄 수 있게 했다.

2-6.  이직 관리 — "지원부터 합격까지 한 눈에"

이건 단순한 이력서 도구를 넘어서, 이직이라는 여정 자체를 관리하는 기능이다.

하나의 이직 활동을 3단계로 나누어 관리한다:

 

1단계: 준비 (Goal)

  • 이직 사유 — 왜 옮기려 하는지
  • 이직 목표 — 목표 포지션, 연봉, 회사 유형
  • 자기소개서 준비 — 질문별 답변 미리 작성 (강점/약점, 지원 동기 등)
  • 예상 면접 질문 — 질문과 모범 답변 정리
  • 체크리스트 — 이력서 업데이트, 포트폴리오 정리 등 할 일 목록
  • 메모 및 첨부파일 — 참고 자료, 포트폴리오 URL 등

2단계: 진행 (Challenge)

여기가 핵심이다. 지원한 회사별로 전형 진행 상황을 추적한다.

 

각 지원 건마다:

  • 회사명, 포지션, 채용 공고 URL
  • 지원 경로 (원티드, 링크드인, 직접 지원 등)
  • 전형 단계별 진행 상황 — 서류 전형 → 코딩 테스트 → 1차 면접 → 2차 면접 → 최종 합격까지 각 단계의 통과/탈락 여부를 기록
  • 단계별 메모 (면접 후기, 과제 내용 등)

여러 회사에 동시 지원할 때 "A사는 2차 면접 대기 중, B사는 코딩테스트 통과, C사는 서류 탈락" 같은 상황을 한 화면에서 파악할 수 있다.

 

3단계: 회고 (Retrospective)

이직이 완료되면 돌아보는 단계다.

  • 입사한 회사명, 연봉, 포지션, 입사일
  • 근무 형태 (출근/하이브리드/리모트)
  • 만족도 (1~5점)
  • 회고 요약 — 좋았던 점, 아쉬운 점, 성장한 점

이 회고가 쌓이면 "나는 이직할 때 어떤 패턴이 있는지"를 객관적으로 볼 수 있다. 몇 개 회사에 지원해서 몇 개 붙었는지, 주로 어느 단계에서 떨어지는지, 연봉은 어떻게 변해왔는지 — 다음 이직 때 전략을 세우는 데 실질적인 데이터가 된다.

대시보드에서는 이직 활동의 완성도도 추적한다. 이직 사유, 목표, 체크리스트, 지원 현황, 회고, 만족도, 연봉 협상 등 8개 항목 중 몇 개를 채웠는지 퍼센트로 보여준다.

 

2-7. 대시보드 — "내 커리어를 한눈에"

모든 기능의 허브 역할을 하는 대시보드에는 다음이 표시된다:

  • 활동 히트맵 — GitHub의 잔디밭처럼, 1년간 어떤 날에 기록을 남겼는지 색상 강도로 보여준다. 기록을 꾸준히 쌓는 습관을 시각적으로 동기부여한다.
  • 연속 기록일 (Streak) — 며칠 연속으로 기록했는지 추적한다.
  • 기록 통계 — 기록 그룹별 추이를 차트로 보여준다.
  • 최근 기록 — 가장 최근에 작성한 기록 3개를 바로 확인할 수 있다.
  • 이력서 완성도 — 이력서의 어떤 섹션이 비어 있는지 안내한다.
  • 이직 활동 완성도 — 현재 진행 중인 이직 활동의 준비 상태를 보여준다.

2-8. 비로그인 체험 모드 — "가입 전에 먼저 써보기"

서비스를 처음 방문한 사용자가 회원가입 없이도 모든 기능을 체험할 수 있게 했다.

로그인하지 않은 상태에서도 대시보드의 히트맵, 캘린더의 기록, 이직 관리 화면 등이 샘플 데이터로 채워져서 동작한다. 실제 서비스와 동일한 인터랙션을 경험한 뒤 마음에 들면 가입하는 흐름이다.

샘플 데이터는 현실적인 시나리오로 구성했다. 예를 들어 이직 관리 샘플에는 네이버, 카카오, 토스, 쿠팡, 배민에 지원하고 각각 다른 단계까지 진행되는 5개의 이직 활동이 담겨 있다. "이런 식으로 쓰는 거구나"를 바로 이해할 수 있게 하기 위해서다.

2-9. 이력서 템플릿 — "같은 내용, 다른 디자인"

같은 이력서 데이터를 다른 디자인 템플릿으로 보여줄 수 있다. 템플릿을 바꿔도 내용은 동일하고, 레이아웃과 스타일만 달라진다.

하나의 템플릿은 웹 버전(URL 공유용)PDF 버전(다운로드용) 두 가지 렌더링을 모두 갖고 있다. 데이터 준비는 공통으로 처리하고, 렌더링만 분리했기 때문에 새로운 디자인 템플릿을 추가하기가 수월하다.

템플릿은 크레딧으로 구매하는 유료 기능으로 설계했다. 기본 템플릿은 무료로 제공하고, 추가 디자인은 크레딧을 충전해서 구매하는 구조다.

정리하며

Workfolio가 풀고자 한 문제는 결국 하나다.

"오늘 한 일을 기록하면, 내일의 이력서가 되고, 이직까지 한 곳에서 관리된다."

기록 → 이력서 변환 → PDF 다운로드/URL 공유 → 이직 관리까지. 흩어져 있던 커리어 관리의 모든 과정을 하나의 흐름으로 연결하는 것이 Workfolio의 핵심이다.

가장 중요하게 생각한 건 진입장벽을 낮추는 것이었다. 비로그인 체험 모드를 통해 가입 없이 먼저 써볼 수 있게 하고, 템플릿 기반 기록으로 "뭘 적어야 하지?" 고민을 줄이고, 기록이 쌓이면 버튼 하나로 이력서가 되는 경험을 만들었다.

 

Workfolio 체험해보기 https://www.workfolio.kr

반응형
반응형
자료 구조와 개발 언어는 알고 있다는 가정하에 코딩 테스트만 이야기하려 한다.

 

예전에는 코딩 테스트를 공부하기 위해 한 문제를 두고 직접 생각해보며 생각하는 힘을 기르려고 노력하였다.

 

그래서 하루 한 문제를 푸는데 3시간을 사용하곤 했다.

 

돌이켜보면 멍청했던 것 같다.

 

사실 코딩 테스트의 알고리즘은 생각한다고 되는 게 아닌 거 같다. 천재들은 될 수도..

 

알고리즘이 수학 공식과 비슷하다고 생각했다. 

 

결국 문제를 풀어내는 기준은 공식 아느냐 모르느냐 차이인 거 같다.

 

이제는 모르는 문제를 마주하더라도 5분 이상 시간을 사용하지 않으려고 한다.

 

사실 실전에서 문제를 만나면 문제를 읽는 도중 혹은 문제를 다 읽고 나면 어떤 알고리즘을 적용해야 하는지 머릿속에 떠오른다.

 

그걸 오래 본다고 모르는 게 갑자기 머릿속에 주입되지는 않을 것이다.

 

5분 동안 생각해보고 안 되면 모르는 문제라고 생각하고 어떤 알고리즘을 적용하는지 보는 게 더 효율적인 공부 방법이라고 생각한다.

 

왜 이 알고리즘을 적용하는지?, 최적화는 어떻게 하는지? 등 기술적인 고민을 더 하는 게 개발자로서 공부가될 거 같다.

 

결론은 모른다고 기죽지 말자 다른 사람들도 처음은 있었을 것이다.

 

모름에서 아는 것으로 넘어가는 시간을 최소한으로 줄여 효율적인 학습을 하자.

반응형
반응형
이 글은 새로 배우면서 익히는 정보들을 기억하기 위한 글이며, 아래 내용들은 "kotlin IN ACTION" 책에서 발췌해온 내용과 개인적인 생각을 넣은 내용입니다.

문자열 나누기

자바 split 메서드로는 점(.)을 이용하여 문자열을 분리할 수 없다. 그 이유는 split의 구분 문자열은 실제로 정규식이기 때문이다.
정규식에서는 마침표(.)가 모든 문자를 나타낸다.

코틀린에서는 자바의 split 대신에 여러 가지 다른 조합의 파라미터를 받는 split 확장 함수를 제공함으로써 혼동을 야기하는 메서드를 감춘다. 정규식을 파라미터로 받는 함수는 String이 아닌 Regex 타입의 값을 받는다.

>>> println("12.345-6.A".split("\\. |-".toRegex()) // 정규식을 명시적으로 만든다.
[12, 345, 6, A]

코틀린의 정규식 문법은 자바와 똑같다. 뿐만 아니라 꼭 정규식을 사용할 필요 없이 split 확장 함수를 이용하여 하나 이상의 인자를 넣을 수 있다.

>> println(12.345-6.A".split(".", "-")) // println(12.345-6.A".split('.', '-'))
[12, 345, 6, A]

문자열이나 문자를 넣어도 마찬가지 결과가 나온다.

- kotlin IN ACTION 129 ~ 130p -

정규식과 3중 따옴표로 묶은 문자열

val path = "/Users/yole/kotlin-book/chapter.adoc"

위의 전체 경로 명을 String 확장 함수를 통해 구분 할 수 있다.

val directory = path.substringBeforeLast("/") // /Users/yole/kotlin-book
val fullName = path.substringAfterLast("/") // chapter.adoc
val fileName = fullName.substringBeforeLast(".") // chapter
val extension = fullName.substringAfterLast(".) // adoc

3중 따옴표 문자열을 사용하여 정규식을 만들수 있다.

val regex = """(.+)/(.+)\.(.+)""".toRegex()

3중 따옴표 문자열은 역슬래시(\)를 포함한 어떤 문자도 이스케이프할 필요가 없다. 마침표 기호를 이스케이프 하려면 \\. 라고 써야 하지만, 3중 따옴표 문자열에서는 \.라고 쓰면 된다.

- kotlin IN ACTION 130 ~ 133p -
반응형

'개발 언어 > Kotlin' 카테고리의 다른 글

Kotlin 함수 정의와 호출  (0) 2022.01.11
Kotlin 기초  (0) 2021.12.31
반응형
이 글은 새로 배우면서 익히는 정보들을 기억하기 위한 글이며, 아래 내용들은 "kotlin IN ACTION" 책에서 발췌해온 내용과 개인적인 생각을 넣은 내용입니다.

이름 붙인 인자

코틀린에서는 함수를 호출할 때 파라미터 명을 지정할 수 있다.

fun <T> joinToString(collection: Collection<T>, separator: String, prefix: String, postfix: String): String {
    val result = StringBuilder(prefix)

    for((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }

    result.append(postfix)
    return result.toString()
}

>>> println(joinToString(collection = collection, separator = ",", prefix = "(", postfix=")"))

위와 같이 파라미터 명을 지정할 수 있으며 파라미터 명을 하나라도 지정했을 경우 혼동을 막기 위해 모든 인자 값의 파라미터 명을 지정해줘야 한다.

- kotlin IN ACTION 107 ~ 108p -

디폴트 파라미터 값

자바에서는 일부 클래스에서 오버로딩한 메서드가 너무 많아진다는 문제가 있다. java.lang.Thread에 있는 8가지 생성자를 살펴보라.

코틀린에서는 함수 선언에서 파라미터의 디폴트 값을 지정할 수 있으므로 이런 오버로드 중 상당수를 피할 수 있다.

fun <T> joinToString(
collection: Collection<T>, 
separator: String = ", ", 
prefix: String = "", 
postfix: String = ""
): String

>>> println(joinToString(collection = collection))

위의 호출 예시처럼 컬렉션만 인자 값으로 넣어도 나머지 파라미터는 디폴트 값을 사용할 수 있다.

함수의 디폴트 파라미터 값은 함수를 호출하는 쪽이 아니라 함수 선언 쪽에서 지정된다는 사실을 기억하라.

자바에는 디폴트 파라미터 값이라는 개념이 없어서 코틀린 함수를 자바에서 호출하는 경우에는 그 코틀린 함수가 디폴트 파라미터 값을 제공하더라도 모든 인자를 명시해야 한다.
그럴 때 @JvmOverloads 애노테이션을 함수에 추가할 수 있다. @JvmOverloads를 함수에 추가하면 코틀린 컴파일러가 자동으로 맨 마지막 파라미터로부터 하나씩 생략한 오버로딩한 자바 메서드를 추가해준다.

- kotlin IN ACTION 109 ~ 110p -

정적인 유틸리티 클래스 없애기: 최상위 함수

자바를 아는 사람은 객체지향 언어인 자바에서는 모든 코드를 클래스의 메서드로 작성해야 한다는 사살을 알고 있다. 보통 그런 구조는 잘 작동한다. 하지만 실전에서는 어느 한 클래스에 포함시키기 어려운 코드가 많이 생긴다. 일부 연산에는 비슷하게 중요한 역할을 하는 클래스가 둘 이상 있을 수도 있다. 중요한 객체는 하나뿐이지만 그 연산을 객체의 인스턴스 API에 추가해서 API를 너무 크게 만들고 싶지는 않은 경우도 있다.

코틀린에서는 이런 무의미한 클래스가 필요 없다. 대신 함수를 직접 소스 파일 최상위 수준, 모든 다른 클래스의 밖에 위치시키면 된다.

아래 예시는 join.kt에 최상위 메서드를 만든 예시이다.

package strings

fun joinToString(...) : String { ... }

자바로 변환하면 아래와 같다.

package strings;

public clas JoinKt {
    public static String joinToString(...) { ... }
}

코틀린 join.kt파일을 컴파일한 결과와 같은 클래스를 자바 코드로 보여준다. 파일 명으로 클래스가 생성되는 것을 볼 수 있다.

최상위 함수가 포함된 클래스의 이름을 바꾸고 싶다면 @JvmName 애노테이션을 사용하면 파일 명을 변경할 수 있다.

@file:JvmName("StringFunctions")

- kotlin IN ACTION 111 ~ 113p -

최상위 프로퍼티

최상위 함수와 마찬가지로 클래스 밖에 프로퍼티를 추가할 수 있다.

상수를 추가하고 싶다면 const val str = ""과 같이 const를 추가하면 상수로 사용할 수 있다.

자바로 변환하면 public static final과 동일하다

- kotlin IN ACTION 113 ~ 114p -

메서드를 다른 클래스에 추가: 확장 함수와 확장 프로퍼티

확장 함수는 소유하고 있는 API가 아니더라도 함수를 추가할 수 있는 기능이다.
그러면 기존 API를 재작성하지 않고도 편리한 기능을 사용할 수 있다.

fun String.lastChar() : Char = this.get(this.length - 1)

위와 같이 String 클래스에 lastChar라는 함수를 추가하여 마지막 자리의 문자를 얻는 기능을 추가하였다.
여기서 String수신 객체 타입이라고 하고 this수신 객체라고 한다.

이런 게 추가된 함수를 사용하면 아래와 같다.

>>> println("Kotlin".lastChar()) // n

String 클래스에 추가된 lastChar 함수를 바로 사용할 수 있다.
뿐만 아니라 수신 객체 멤버에 this 없이 접근할 수도 있다.

fun String.lastChar() : Char = get(length - 1)

단, 함장 함수는 클래스 내부에서만 사용할 수 있는 private, protected와 같은 멤버를 사용할 수 없다.

확장 함수를 사용하기 위해서는 다른 클래스나 함수와 마찬가지로 임포트 해야만 한다. 확장 함수를 정의하자마자 어디서든 그 함수를 쓸 수 있다면 한 클래스에 같은 이름의 확장 함수가 둘 이상 있어서 이름이 충돌하는 경우가 자주 생길 수 있다.

import strings.lastChar
// import strings.* // * 를 사용해도 잘 된다.
val c = "Kotlin".lastChar()

한 파일 안에서 다른 여러 패키지에 속해있는 이름이 같은 함수를 가져와 사용해야 하는 경우 이름을 바꿔서 임포트 하면 이름 충돌을 막을 수 있다.

import strings.lastChar as last
val c = "Kotlin".last()

따라서 임포트 할 때 이름을 바꾸는 것이 확장 함수 이름 충돌을 해결할 수 있는 유일한 방법이다.

- kotlin IN ACTION 115 ~ 117p -

확장 함수로 유틸리티 함수 정의

이전에 만들었던 joinToString을 최종 버전으로 만들어보자.

fun <T> Collection<T>.joinToString( // Collection<T>에 대한 확장 함수
    separator: String, // 디폴트 값 지정
    prefix: String, // 디폴트 값 지정
    postfix: String): String {  // 디폴트 값 지정
   
    val result = StringBuilder(prefix)

    for((index, element) in this.withIndex()) { // this는 수신 객체를 가리킨다. 여기서 T의 원소로 이뤄진 컬렉션이다.
        if (index > 0) result.append(separator)
        result.append(element)
    }

    result.append(postfix)
    return result.toString()
}

확장 함수를 이용해서 Collection에서 바로 사용할 수 있게 하였다.
또한 구체적인 원소 타입을 지정할 수 있다.

fun Collection<String>.join(
    separator: String,
    prefix: String,
    postfix: String) = joinToString(separator, prefix,postfix)

위의 두 예제 모두 다 아래와 같이 사용할 수 있다.

listOf(1, 2, 3).joinToString(separator = "; ", prefix = "(", postfix = ")") // (1; 2; 3)
listOf("one", "two", "three").joint(" ") // one two three

- kotlin IN ACTION 118 ~ 119p -

동적 디스패치와 정적 디스패치

실행 시점에 객체 타입에 따라 동적으로 호출될 대상 메서드를 결정하는 방식을 동적 디스패치(dynamic dispatch)라고 한다. 반면 컴파일 시점에 알려진 변수 타입에 따라 정해진 메소드를 호출하는 방식은 정적 디스패치(static dispatch)라고 부른다. 참고로 프로그래밍 언어 용어에서 '정적'이라는 말은 컴파일 시점을 의미하고, '동적'이라는 말은 실행 시점을 의미한다.

- kotlin IN ACTION 120p -

확장 함수는 오버라이드 할 수 없다.

일반적인 오버라이드부터 확인해보자

open class View {
    open fun click() = println("view clicked"
}

class Button : View {
    override fun click() = println("button clicked")
}

val view: View = Button()
view.click()

button clicked

위의 예제를 보면 알 수 있듯이 View 타입 변수에 대해 click과 같은 일반 메서드를 호출했는데 click을 Button 클래스가 오버라이드 했다면 실제로 Button이 오버라이드 한 click이 호출된다.

하지만 확장 함수는 클래스 외부에 있는 것이기 때문에 오버라이드가 되지 않는다.

fun View.showOff() = println("I'm a view!")
fun Button.showOff() = println("I'm a button!")

val view: View = Button()
view.showOff()

I'm a view!

view가 가리키는 객체의 실제 타입은 Button이지만, 이 경우 view의 타입이 View이기 때문에 무조건 View의 확장 함수가 호출된다.

일반 함수는 동적으로 결정되지만, 확장 함수는 정적으로 결정된다.

/* 노트 */
어떤 클래스를 확장한 함수와 그 클래스의 멤버 ㅂ함수의 이름과 시그니처가 같다면 확장 함수가 아니라 멤버 함수가 호출된다(멤버 함수의 우선순위가 더 높다). 클래스의 API를 변경할 경우 항상 이를 염두에 둬야 한다. 여러분이 코드 소유권을 가진 클래스에 대한 확장 함수를 정의해서 사용하는 외부 클라이언트 프로젝트가 있다고 하자. 그 확장 함수와 이름과 시그니처가 같은 멤버 함수를 여러분의 클래스 내부에 추가하면 클라이언트 프로젝트를 재 컴파일하는 순간부터 그 클라이언트는 확장 함수가 아닌 새로 추가된 멤버 함수를 허용하게 된다.

- kotlin IN ACTION 120 ~ 122p -

메서드를 다른 클래스에 추가: 확장 프로퍼티

확장 함수와 동일하게 수신 객체를 추가해서 프로퍼티를 만들 수 있다.

val String.lastChar
    get() = get(length - 1)

확장 프로퍼티는 뒷받침하는 필드가 없어서 기본 게터 구현을 제공할 수 없으므로 최소한 게터는 꼭 정의해야 한다. 마찬가지로 초기화 코드에서 계산한 값을 담을 장소가 전혀 없으므로 초기화 코드도 쓸 수 없다.

변경 가능한 프로퍼티는 var로 만들 수 있다.

var StringBuilder.lastChar
    get() = get(length - 1)
    set(value: Char) {
         this.setCharAt(length - 1, value)
    }

자바에서 확장 프로퍼티를 사용하려면 게터나 세터를 명시적으로 호출해야 한다.

- kotlin IN ACTION 122 ~ 124p -

자바 컬렉션 API 확장

코틀린이 자바 라이브러리를 사용하여 더 많은 기능을 제공할 수 있었던 것은 바로 위에서 배운 확장 함수를 이용한 것이다.

- kotlin IN ACTION 124 ~ 125p -

가변 인자 함수: 인자의 개수가 달라질 수 있는 함수 정의

자바에서는 가변 인자를 받기 위해 타입 뒤에 ... 을 붙여서 배열 형태로 담아 준다. 코틀린도 같은 방식이지만 문법적으로 다르다.
코틀린에서는 파라미터 앞에 vararg 변경자를 붙인다.

fun <T> test(vararg values: T) {
    val list = listOf(*values)
    println(list)
}

test("1", "2", "3")

>>> [1, 2, 3]

이미 배열에 들어있는 원소를 가변 길이 인자로 넘길 때도 코틀린과 자바 구문이 다르다. 자바에서는 배열을 그냥 넘기면 되지만 코틀린에서는 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 해야 한다. 기술적으로는 스프레드 연산자(*)가 그런 작업을 해준다.

이 예제는 스프레드 연산자를 통하면 배열에 들어있는 값과 다른 여러 값을 함께 써서 함수를 호출할 수 있음을 보여준다. 이런 기능은 자바에서는 사용할 수 없다.

- kotlin IN ACTION 126p -

값의 쌍 다루기: 중위 호출과 구조 분해 선언

val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")

이 코드는 중위 호출이라는 특별한 방식으로 to라는 일반 메서드를 호출한 것이다. 중위 호출 시에는 수신 객체와 유일한 메소드 인자 사이에 메소드 이름을 넣는다.(이때 객체, 메소드 이름, 유일한 인자 사이에는 공백이 들어가야 한다.)

1.to("one")
1 to "one"

위의 두 호출은 동일하다.
인자가 하나뿐인 일반 메서드나 인자가 하나뿐인 확장 함수에 중위 호출을 사용할 수 있다. 함수를 중위 호출에 사용하게 허용하고 싶으면 infix 변경자를 함수 선언 앞에 추가해야 한다.

infix fun Any.to(other: Any) = Pair(this, other)

Pair는 두 변수를 즉시 초기화할 수 있다. 이런 기능을 구조 분해 선언(destructuring declaration)이라고 부른다.

val (number, name) = 1 to "one"

-> 1 to "one"
-> Pair(1, "one")
-> val (number, name) = 1 to "one"

위와 같이 Pair가 동작한다.

to는 확장 함수이다. to를 사용하려면 타입과 관계없이 임의의 순서쌍을 만들 수 있다. 이는 to의 수신 객체가 제네릭하다는 뜻이다.
1 to "one", "one" to 1, list to list.size() 등의 호출이 모두 잘 동작한다.

fun <K, V> mapOf(vararg values: Pair<K, V>): Map<K, V>

- kotlin IN ACTION 127 ~ 129p -
반응형

'개발 언어 > Kotlin' 카테고리의 다른 글

Kotlin 문자열과 정규식 다루기  (0) 2022.01.19
Kotlin 기초  (0) 2021.12.31

+ Recent posts