-
팀원1 part(3.1 코틀린에서 컬렉션 만들기 ~ 3.2 함수를 호출하기 쉽게 만들기)
-
코틀린에서 컬렉션 구현하기
-
코틀린에서 구현한 객체는 어떤 클래스일까?
-
코틀린이 자체 컬렉션을 제공하지 않는 이유
-
3.2 함수를 호출하기 쉽게 만들기
-
3.2.1 이름 붙인 인자
-
3.2.2 디폴트 파라미터 값
-
3.2.3 정적인 유틸리티 클래스 없애기 : 최상위 함수와 프로퍼티
-
팀원2 part (3.3 메소드를 다른 클래스에 추가: 확장 함수와 확장 프로퍼티)
-
3.3.1 임포트와 확장 함수
-
3.3.2 자바에서 확장 함수 호출
-
3.3.3 확장 함수로 유틸리티 함수 정의
-
3.3.4 확장 함수는 오버라이드할 수 없다.
-
3.3.5 확장 프로퍼티
-
팀원3 part (3.4 컬렉션 처리: 가변 길이 인자, 중위 함수 호출, 라이브러리 지원)
-
3.4.1 자바 컬렉션 API 확장
-
3.4.2 가변 인자 함수: 인자의 개수가 달라질 수 있는 함수 정의
-
3.4.3 값의 쌍 다루기: 중위 호출과 구조 분해 선언
-
팀원4 part (3.5 문자열와 정규식 다루기 ~ 3.6 코드 다듬기)
-
1. 문자열 나누기
-
2. 정규식과 3중 따옴표 문자열
-
3. 여러 줄 3중 따옴표 문자열
-
4. 마무리
-
코드 다듬기: 로컬 함수와 확장
-
1. DRY 원칙이란?
-
2. 코드 중복 문제와 해결 방법
-
3. 로컬 함수 활용하여 코드 개선
-
4. 확장 함수로 변환하여 더 개선하기
-
도서 링크 바로가기
Kotlin in Action을 공부하며 정리한 내용입니다.
저작권에 문제가 될 시, 글을 모두 내리겠습니다.
제가 공부한 내용이 더 많은 분들에게도 도움이 되었으면 좋겠습니다. 부족한 부분은 댓글을 통해서 피드백을 주신다면 언제나 반영하겠습니다. 감사합니다.
책에 대한 링크는 맨 아래에 있습니다.
https://github.com/Kotlin-Android-Study-with-SSAFY/Kotlin_In_Action_1
GitHub - Kotlin-Android-Study-with-SSAFY/Kotlin_In_Action_1: SSAFY 13기 모바일 트랙 구미 5반 "코틀린 인 액션" 스
SSAFY 13기 모바일 트랙 구미 5반 "코틀린 인 액션" 스터디(A). Contribute to Kotlin-Android-Study-with-SSAFY/Kotlin_In_Action_1 development by creating an account on GitHub.
github.com
팀원1 part(3.1 코틀린에서 컬렉션 만들기 ~ 3.2 함수를 호출하기 쉽게 만들기)
코틀린에서 컬렉션 구현하기
- 집합 구현
val set = hashSetof (1, 7, 53)
- 리스트 구현
val list = arrayListOf(1, 7, 53)
- 맵 구현
- 여기서 to 의 경우 → 키워드(X), 일반 함수(O)
val map = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-three"
코틀린에서 구현한 객체는 어떤 클래스일까?
>>> println(set.javaClass)
class java.util.HashSet
>>> println(list.javaClass)
class java.util.ArrayList
>>> println(map.javaClass)
class java.util.HashMap
코틀린에서 javaClass → 자바 getClass()에 해당
- 즉, 코틀린은 자신만의 컬렉션 기능 제공 X
코틀린이 자체 컬렉션을 제공하지 않는 이유
- 표준 자바 컬렉션을 활용하면 자바코드와 상호작용하기 훨씬 쉽게 때문
- 자바에서 코틀린 함수를 호출하거나 코틀린에서 자바 함수를 호출할 때 서로 변환 불필요
- But 코틀린에서는 자바보다 더 많은 기능 사용 가능
- ex) 리스트의 마지막 원소 가져오기, 수로 이뤄진 컬렉션에서 최대값 찾기
>>> val strings = listOf("first", "second", "fourteenth")
>>> printlin(strings.last())
fourteenth
>>> val numbers = setOf(1, 14, 2)
>>> println(numbers.max())
14
3.2 함수를 호출하기 쉽게 만들기
컬렉션에 담긴 원소 출력하기
- 자바 컬렉션에는 디폴트로 toString() 구현이 포함
- 코틀린에서 원소 출력하는 방법
>>> val list = listOf(1, 2, 3)
>>> println(list)
[1, 2, 3]
joinToString() 함수의 초기 구현
- 디폴트 구현과 달리 (1; 2; 3) 처럼 원소 사이를 세미콜론으로 구분하고 괄호로 리스트를 둘러싸고 싶다면?
>>> val list = listOf(1, 2, 3)
>>> println(joinToString(list, ";", "(", ")")
(1; 2; 3)
- joinToString(list, separator, prefix, postfix, limit)
- separator : 원소간 연결 시 사이의 들어갈 문자열 ( ""을 주면 공백없이 연결된다.)
- prefix : 만들 문자열에서 시작할 문자열
- postfix : 만들 문자열에서 종료할 문자열
- limit : 개수 제한 → limit개 이후 ‘…’ 으로 표현
Kotlin joinToString - List 문자열 변환 List to String
1. joinToString - List to String List를 print를 사용하여 출력하거나하면 toString이 호출되며 [ ... ] 와 같은 형태로 변환된다. 하지만 리스트의 원소들을 모두 연결한 하나의 문자열로 만들고 싶은 경우가
notepad96.tistory.com
3.2.1 이름 붙인 인자
- joinToString(list, ";", "(", ")"
- 가독성이 좋지 않음 → 함수의 시그니처를 보지 않고는 위의 코드 상에서 전달인자만으로 정확한 역할을 알기 어려움
- 따라서 다음과 같이 전달인자로 일부(또는 전부)를 이름으로 명시 가능 → 호출 시 인자 중 어느 하나라도 이름을 명시하고 나면 혼동을 막기 위해 그 뒤에 오는 모든 인자는 이름을 꼭 명시
>>> joinToString(collection, sperator=" ", prefix=" ", postfix=".")
불행하게도 자바로 작성한 코드를 호출할 때는 이름 붙인 인자 사용 불가. 따라서 안드로이드 프레임워크나 JDK가 제공하는 함수를 호출할 때도 이름 붙인 인자 사용 불가.
3.2.2 디폴트 파라미터 값
- 자바에서는 일부 클래스에서 오버로딩(Overloading)한 메소드가 너무 많아진다는 문제가 있음 → 인자 중 일부가 생략된 오버로드 함수를 호출할 때 어떤 함수가 불릴지 모호한 경우 발생
- 코틀린에서는 함수 선언 시 파라미터의 디폴트 값을 지정 가능 → 오버로드 중 모호한 경우 회피 가능
- 디폴트 값을 사용한 joinToString 함수
>>> joinToString(list, ",", "", "")
1, 2, 3
>>> joinToString(list) // separtor, prefix, postfix 생략
1, 2, 3
>>> joinToString(list, ";") // separator를 ";"로 지정, prefix와 postfix 생략
이름 붙은 인자를 사용하는 경우 → 인자 목록의 중간에 있는 인자를 생략하고, 지정하고 싶은 인자는 이름을 붙여서 순서 상관없이 지정 가능
>>> joinToString(list, postfix=";", prefix="#")
# 1, 2, 3;
- 함수의 디폴트 파라미터 값 지정 → 함수를 호출하는 부분(X), 함수를 선언하는 부분(O)
- 어떤 클래스 안에 정의된 함수의 디폴트 값을 바꾸고 그 클래스가 포함된 파일을 재컴파일하면 → 그 함수를 호출하는 코드 중에 값을 지정하지 않은 모든 인자는 자동으로 바뀐 디폴트 값을 적용
- 자바에서는 디폴트 파라미터 값이라는 개념이 없음
- 따라서 코틀린 함수를 자바에서 호출하는 경우에는 해당 코틀린 함수가 디폴트 값을 제공하더라도 모든 인자를 명시해야함
- <자바에서 코틀린 함수를 자주 호출할 경우> @JvmOverloads 을 함수에 추가 가능 → 코틀린 컴파일러가 자동으로 맨 마지막 파라미터로부터 파라미터를 하나씩 생략한 오버로딩한 자바 메소드를 추가 해줌
- ex) joinToString에 @JvmOverloads를 붙인 경우 다음과 같이 오버로딩 함수 생성해줌
String joinToString(Collection<T> collection, String separtor, String prefix, String postfix);
String joinToString(Collection<T> collection, String separtor, String prefix);
String joinToString(Collection<T> collection, String separtor);
String joinToString(Collection<T> collection;
3.2.3 정적인 유틸리티 클래스 없애기 : 최상위 함수와 프로퍼티
- 자바에서는 모든 코드를 클래스의 메소드로 작성해야 해서, 특정 클래스에 속하기 애매한 코드들을 모아놓은 정적 메소드 전용 유틸리티 클래스(예: Collections)가 자주 등장
- But 코틀린에서는 이런 유틸리티 클래스가 필요 없음
- 그냥 최상위 함수로 만들면 되고, 패키지만 임포트하면 된다.
- 즉, 클래스 없이도 독립적인 함수들을 간편하게 관리할 수 있음
- JoinToString() 함수를 최상위 함수로 선언하기 → 코틀린에서는 클래스 없이 최상위 수준에 함수를 정의 가능
package strings
fun joinToString(...): String { ... }
- 하지만 JVM은 클래스 내부 코드만 실행 가능하므로, 코틀린 컴파일러가 자동으로 해당 파일 이름과 같은 클래스를 생성
- 예를 들어 join.kt 파일을 컴파일하면 JoinKt 클래스가 생성되며, 이 클래스의 정적 메소드로 함수가 포함
- 자바에서 호출할 때는 이렇게 사용
import strings.JoinKt;
JoinKt.joinToString(list, ", ", "");
- 파일의 클래스 이름을 바꾸고 싶다면 @file:JvmName("새이름") 애노테이션을 추가
@file:JvmName("StringFunctions")
package strings
fun joinToString(...): String { ... }
- 이제 자바에서는 아래처럼 사용 가능
import strings.StringFunctions;
StringFunctions.joinToString(list, ", ", "");
코틀린에서는 클래스 바깥에 변수도 선언 가능
var opCount = 0 // 연산 횟수 저장
fun performOperation() {
opCount++ // 값 변경 가능
}
fun reportOperationCount() {
println("Operation performed $opCount times")
}
이런 변수들은 JVM에서 정적 필드로 저장됨
4. 상수 선언 (const)
- 일반적인 val 프로퍼티는 접근자 메소드(getter)를 통해 접근해야 하지만, 상수처럼 사용하려면 const를 붙이면 됨
const val UNIX_LINE_SEPARATOR = "\n"
- 이렇게 하면 아래와 같은 자바 코드의 public static final 필드로 변환
public static final String UNIX_LINE_SEPARATOR = "\n";
팀원2 part (3.3 메소드를 다른 클래스에 추가: 확장 함수와 확장 프로퍼티)
확장 함수는 어떤 클래스의 멤버 메소드 인 것처럼 호출할 수 있지만 그 클래스 밖에 선언된 함수다.
기존 클래스의 소스를 수정하지 않고도 새로운 기능을 추가할 수 있는 강력한 기능
- 자바에서는 기존 클래스를 확장하려면 상속하거나 데코레이터 패턴을 사용해야 했지만, 코틀린에서는 확장 함수를 활용하면 훨씬 간편하게 기능을 추가할 수 있다.
클래스명.함수명 형태로 선언하며, this 키워드를 사용하며 해당 클래스의 멤버처럼 접근할 수 있다.
클래스의 멤버 함수 : 클래스 내부에 정의된 함수
// String 클래스를 확장해 lastChar()라는 함수를 추가
fun String.lastChar() : Char {
//this는 호출하는 주체(수신 객체)를 가리킴. 여기서는 "hello" 문자열이 됨.
return this.get(this.length-1)
}
fun main () {
println("Kotlin".lastChar()) // 'o' 출력
}
- 추가하려는 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙인다.
- 수신 객체 타입(receiver type) : 클래스 이름 ex) String
- 수신 객체(receiver object) : 확장 함수가 호출되는 대상이 되는 값(객체) ex) this, “Kotlin”
- this를 사용하면, 현재 함수를 호출한 “수신 객체”를 가리킨다.
- 실제 함수를 호출할 때 쓰이는 “실제 인스턴스”를 의미
수식 객체 타입은 확장이 정의될 클래스의 타입이며, 수신 객체는 그 클래스에 속한 인스턴스 객체이다.
어떤 언어로 작성됐는지 중요하지 않다. 다른 JVM 언어로 작성된 클래스도 확장할 수 있다. 즉, 자바 클래스로 컴파일한 클래스 파일이 있는 한 그 클래스에 원하는 대로 확장을 추가할 수 있다❕
→ JVM에 돌아가는 언어로 작성된 코드는 결국 .class 파일로 컴파일 되기 때문에 코틀린은 그 .class 파일만 있으면, 거기에 정의된 클래스를 대상으로 확장 함수를 추가할 수 있다.
일반 메소드의 본문에서 this를 사용할 때와 마찬가지로 확장 함수 본문에서도 this를 쓸 수 있다. 그리고 일반 메소드와 마찬가지로 확장 함수 본문에서도 this를 생략할 수 있다.
package strings
fun String.lastChar() : Char = get(length - 1)
- 확장 함수 내부에서는 일반적인 인스턴스 메소드의 내부에서와 마찬가지로 수신 객체의 메소드나 프로퍼티를 바로 사용할 수 있다.
- 하지만 확장 함수가 캡슐화를 깨지는 않는다!!!
- 캡슐화❔ 객체의 내부 데이터와 구현을 외부로부터 숨기고, 오직 공개된 인터페이스를 통해서만 상호작용하도록 하는 개념
- 클래스 안에서 정의한 메소드와 달리 확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 private 멤버나 protected 멤버를 사용할 수 없다.
- 캡슐화 특징 객체 내부의 변수(데이터)는 보통 private 또는 protected로 선언되어 외부에서 직접 접근하거나 변경할 수 없다. 이를 통해 의도치 않은 데이터 변경이나 오작동 방지
- 클래스의 멤버 메소드와 확장 함수를 모두 메소드라고 부른다.
3.3.1 임포트와 확장 함수
확장 함수를 정의했다고 해서 자동으로 프로젝트 안의 모든 소스코드에서 함수를 사용할 수 있지는 않다. 확장함수를 사용하기 위해서는 그 함수를 다른 클래스나 함수와 마찬가지로 import해야 한다.
- 코틀린에서는 클래스를 import할 때와 동일한 구문을 사용해 개별 함수를 import할 수 있다.
// 1. import
import strings.lastChar
// 2. *를 사용한 임포트도 잘 작동함
import strings.*.
// 3. as 키워드를 사용하여 import한 클래스나 함수를 다른 이름으로 부를 수 있음
import strings.lastChar as last
val c = "Kotlin".lastChar()
다른 여러 패키지에 이름이 같은 함수가 많은데 한 파일 안에서 그런 함수들을 함께 써야 하는 경우 이름을 바꿔서 import하면 편리하다.
import com.example.utils.printMessage as utilsPrint
import com.example.tools.printMessage as toolsPrint
fun main() {
utilsPrint() // Utils Package
toolsPrint() // Tools Package
}
일반적인 클래스나 함수를 부를 때 전체 이름을 쓰면 된다.
fun main() {
val text = "Hello"
println(com.example.utils.formatText(text)) // Utils: Hello
println(com.example.tools.formatText(text)) // Tools: Hello
}
패키지가 같을 경우 이름 충돌 해결은?
확장 함수 대신 클래스 내부의 멤버 함수로 정의하면 객체를 통해 호출해야 하므로 충돌을 피할 수 있다. 이렇게 하면 확장 함수가 아니라 멤버 함수가 되므로 충돌을 방지할 수 있다
package com.example.utils
class Formatter {
fun formatText(text: String) = "Utils: $text"
}
package com.example.utils
class Formatter {
fun formatText(text: String) = "Tools: $text"
}
import com.example.utils.Formatter as UtilsFormatter
import com.example.utils.Formatter as ToolsFormatter
fun main() {
val utils = UtilsFormatter()
val tools = ToolsFormatter()
println(utils.formatText("Hello")) // Utils: Hello
println(tools.formatText("Hello")) // Tools: Hello
}
하지만, 확장 함수는 코틀린 문법상 반드시 짧은 이름을 써야 한다. 따라서 import시 이름을 바꾸는 것이 확장 함수의 이름 충돌을 해결하는 유일한 방법이다!!
3.3.2 자바에서 확장 함수 호출
내부적으로 확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메소드이다. 그래서 확장 함수를 호출해도 다른 어댑터 객체나 실행 시점 부가 비용이 들지 않는다.
자바의 정적 메소드
- MathUtils.add(5, 3); → 클래스 이름으로 바로 호출 가능.
- 객체를 생성할 필요가 없음.
class MathUtils {
public static int add(int a, int b) {
return a + b;
}
}
public class Main {
public static void main(String[] args) {
int result = MathUtils.add(5, 3); // 객체 생성 없이 호출
System.out.println(result); // 8
}
}
코틀린의 정적 메소드
- 코틀린에서는 static 키워드가 없으므로, 파일 최상위 함수 또는 companion object를 사용해야 함.
- MathUtils.add(5, 3) → 객체 생성 없이 바로 호출 가능.
object MathUtils {
fun add(a: Int, b: Int): Int {
return a + b
}
}
fun main() {
val result = MathUtils.add(5, 3) // 객체 생성 없이 호출
println(result) // 8
}
인스턴스 메소드
- 정적 메소드가 아니라 일반 인스턴스 메소드(멤버 함수)는 반드시 객체를 생성해야 호출할 수 있음
- MathUtils() 객체를 먼저 만들어야 add() 메소드를 사용할 수 있음.
class MathUtils {
fun add(a: Int, b: Int): Int {
return a + b
}
}
fun main() {
val math = MathUtils() // 객체 생성
val result = math.add(5, 3) // 객체를 통해 호출
println(result) // 8
}
코틀린에서 확장함수는 클래스 내부에 있는 것이 아니라, 외부에서 정의된 함수이므로, 일반 멤버 함수처럼 객체가 필요하지 않다. 그래서 컴파일 시에 정적 메소드(static method)로 변환된다.
3.3.3 확장 함수로 유틸리티 함수 정의
유틸리티 함수는 일반적으로 특정 클래스의 상태와 무관하게 독립적으로 동작하며, 여러 곳에서 재사용 가능한 기능을 제공한다. 자바에서는 종종 static 메소드로 정의되는 경우가 많다.
- 클래스 외부에 정의되어 특정 기능(리스트를 문자열로 변환)을 독립적으로 수행
- 컴파일 시 정적 메소드로 변환되어, 인스턴스 상태와 무관하게 재사용 가능한 기능을 제공
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 수신 객체를 가리킴
if (index > 0) result.append(separator) //T 타입의 원소로 이뤄진 컬렉션
result.append(element)
}
result.append(postfix)
return result.toString()
}
val list = listOf(1, 2, 3)
println(list.joinToString(separator = "; ", prefix = "(", postfix = ")"))
//출력 결과
(1; 2; 3)
확장 함수는 클래스 외부에 정의되어 정적 메소드로 컴파일되므로, 인스턴스에 종속되지 않고 독립적으로 기능을 수행하는 유틸리티 함수라고 할 수 있습니다.
문자열의 컬렉션에 대해서만 호출할 수 있는 join함수를 정의하고 싶다면 다음과 같이 하면 된다.
- join() 확장 함수는 Collection에만 적용되기 때문이다.
- 확장 함수가 정적 메소드와 같은 특징을 가지므로, 확장 함수를 하위 클래스에서 오버라이드할 수 없다.
fun Collection<String>.join {
separator : String = " , " ,
prefix : String = "",
postfix : String = ""
) = joinToString(separator, prefix, postfix)
val words = listOf("one", "two", "eight")
println(words.join(" "))
//출력 결과
"one two eight"
val numbers = listOf(1, 2, 8)
println(numbers.join(" "))
//출력 결과
Error: Type mismatch:
inferred type is List<Int>but Collection<String> was expected
3.3.4 확장 함수는 오버라이드할 수 없다.
코틀린 메소드 오버라이드도 일반적인 객체지향의 메소드 오버라이드와 마찬가지이다. 하지만, 확장 함수는 오버라이드할 수 없다.
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
- open class View : 기본 클래스, open fun click() 메소드를 가지고 있음
- val view: View = Button() : 변수 view는 정적 타입은 View지만, 실제 할당되는 인스턴스는 Button입니다.
- 메소드 호출 시 실제 객체 타입에 따라 실행되는 메소드가 결정되는 서브타입(다형성)의 특징으로, Button이 View의 하위 타입이기 때문이다.
- 위의 예제는 변수의 정적 타입(View)과 실제 저장된 객체의 동적 타입(Button)이 다르다.
View와 Button 클래스에 대해 선언된 두 showOff 확장 함수
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의 정적 타입은 View이고, 실제 저장된 객체(동적 타입)는 Button이다.
- 확장 함수는 변수의 정적 타입을 기준으로 선택되므로, view.showOff() 호출 시 View에 대해 정의된 확장 함수가 호출된다.
- 확장 함수를 첫 번째 인자가 수신 객체인 정적 자바 메소드로 컴파일한다는 사실을 기억한다면 이런 동작을 쉽게 이해할 수 있다.
- 결과적으로 "I'm a view!"가 출력
어떤 클래스를 확장한 함수와 그 클래스의 멤버 함수 이름과 시그니처가 같다면 확장 함수가 아니라 멤버 함수가 호출된다 → 멤버 함수의 우선순위가 높다
3.3.5 확장 프로퍼티
객체가 가지는 데이터를 표현하는 요소 클래스 내부에서 객체의 상태(데이터)를 저장하고 관리하기 위한 변수와 비슷한 개념 단순한 변수와 달리 접근자(getter,setter)라는 기능이 결합되어 있어 데이터를 읽거나 쓸 때 자동으로 호출되는 메서드를 제공한다.
- val과 var
- val: 읽기 전용 프로퍼티. 초기화 후 값이 변경되지 않습니다. (즉, getter만 존재)
- var: 변경 가능한 프로퍼티. 값을 읽을 때는 getter, 값을 수정할 때는 setter가 호출됩니다.
class Person {
val name: String = "John" // 읽기 전용 프로퍼티
var age: Int = 30 // 읽기와 쓰기가 가능한 프로퍼티
}
fun main() {
val person = Person()
println(person.name) // "John" 출력
println(person.age) // 30 출력
person.age = 35 // setter가 호출되어 age 값이 변경됨
println(person.age) // 35 출력
}
- 위 예시에서 name은 val로 선언되었기 때문에 값을 변경할 수 없고, age는 var로 선언되어 값을 변경할 수 있습니다.
- person.name이나 person.age를 호출하면 내부적으로 자동 생성된 getter가 사용됩니다.
- person.age = 35 처럼 값을 할당하면 자동 생성된 setter가 호출됩니다.
확장 프로퍼티도 일반적인 프로퍼티와 같은데, 수신 객체 클래스가 추가됐을 뿐이다. 뒷받침하는 필드(field)가 없어서 기본 getter 구현을 제공할 수 없으므로 최소한 getter는 꼭 정의해야 한다
- 초기화 코드에서 계산한 값을 담을 장소가 전혀 없으므로, 초기화 코드도 쓸 수 없다.
var String.lastChar : Char
get () = get(length-1)
- StringBuilder에 같은 프로퍼티를 정의한다면 StringBuilder의 맨 마지막 문자는 변경 가능하므로 프로퍼티를 var로 만들 수 있다.
- 자바에서 확장 프로퍼티를 사용하고 싶다면 항상 게터나 세터를 명시적으로 호출해야 한다.
var StringBuilder.lastChar : Char
get() = get(length-1)
set(value : Char){
this.setCharAt(length-1, value)
}
>>> println("Kotlin".lastChar)
n
>>> val sb = StringBuilder("Kotlin?")
>>> sb.lastChar = '!'
>>>println(sb)
Kotlin!
일반 프로퍼티 : 클래스 내부에 선언되어 실제 값을 저장하는 백킹 필드가 존재할 수 있다. 확장 프로퍼티 : 클래스 외부에서 추가하는 프로퍼티인데, 백킹 필드가 없어서 값을 저장할 수 없고, 반드시 계산된 결과나 다른 방법으로 값을 제공해야 한다.
백킹 필드?
프로퍼티의 값을 저장하는 실제 메모리 공간으로, getter와 setter에서 field 키워드를 사용하면, 이 백킹 필드에 접근하여 값을 읽거나 수정할 수 있음
- 프로퍼티에 커스텀 getter나 setter를 정의할 때, field라는 특별한 키워드를 사용하여 백킹 필드에 접근할 수 있습니다.
- 예를 들어, 아래와 같이 커스텀 getter와 setter를 정의할 때 field를 사용하면, 실제 저장된 값을 읽거나 수정할 수 있습니다.
var nickname: String = "Guest"
get() {
println("getter 호출")
return field // 여기서 field는 백킹 필드를 의미
}
set(value) {
println("setter 호출")
field = value // 백킹 필드에 새로운 값을 저장
}
확장 프로퍼티는 원본 클래스에 새로운 프로퍼티를 추가하는 방법인데, 이 경우에는 백킹 필드(field)를 가질 수 없기 때문에 항상 계산된 결과만 반환해야 한다.
var String.lastChar: Char
get() = this.get(this.length - 1)
// 확장 프로퍼티는 백킹 필드가 없으므로 setter 없이 읽기 전용으로 만드는 경우가 많습니다.
팀원3 part (3.4 컬렉션 처리: 가변 길이 인자, 중위 함수 호출, 라이브러리 지원)
- varang 키워드를 사용하면 호출 시 인자 개수가 달라질 수 있는 함수를 정의할 수 있음
- 중위(infix) 함수 호출 구문을 사용하면 인자가 하나뿐인 메소드를 간편하게 호출할 수 있음
- 구조 분해 선언(destructuring declaration)을 사용하면 복합적인 값을 분해해서 여러 변수를 나눠 담을 수 있음
3.4.1 자바 컬렉션 API 확장
- 코틀린 컬렉션은 자바와 같은 클래스를 사용하지만 더 확장된 API를 제공
>>> val strings: List<String> = listOf("first", "second", "fourteenth")
>>> strings.last()
fourteenth
>>> val numbers: Collection<Int> = setOf(1, 14, 2)
>>> numbers.max()
14
- 자바 라이브러리 클래스의 인스턴스인 켈렉션에 대해 코틀린이 새로운 기능을 추가할 수 있는 이유? → last()와 max()는 모두 확장 함수이기 때문(3.3 절)
fun <T> List<T>.last() : T {/* 마지막 원소를 반환함 */}
fun Collection<Int>.max() : Int {/* 컬렉션의 최댓값을 찾음 */}
// last와 max는 모두 코틀린 표준 라이브러리 함수이자 자바 라이브러리인 컬렉션의 확장 함수
- 코틀린 표준 라이브러리 또한 자바처럼 IDE의 코드 완성 기능을 통해 메소드나 함수를 살펴볼 수 있음
3.4.2 가변 인자 함수: 인자의 개수가 달라질 수 있는 함수 정의
- 리스트를 생성하는 함수를 호출할 때 원하는 만큼 많이 원소를 전달할 수 있음
val list = listOf(2, 3, 5, 7, 11)
// listOf 함수 정의
fun listOf<T>(vararg values : T): List<T> { ... }
- 가변 길이 인자(vararg): 메소드를 호출할 때 원하는 개수만큼 값을 인자로 넘기면 자바 컴파일러가 배열에 그 값들을 넣어주는 기능
- 코틀린도 비슷하지만 문법이 조금 다름 → 타입 뒤에 … 를 붙이는 대신 파라미터 앞에 vararg 변경자를 붙임
- 이미 배열에 들어있는 원소를 가변 길이 인자로 넘길 경우
- 자바에서는 배열을 그냥 넘기면 되지만 코틀린에서는 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 해야 함
- 기술적으로는 스프레드(spread) 연산자가 그런 작업을 해줌 → 실제로는 전달하려는 배열 앞에 *****를 붙이면 됨
fun main(args: Array<String>) {
val list = listOf("args: ", *args) // 스프레드 연산자가 배열의 내용을 펼쳐줌
println(list)
}
- 스프레드 연산자를 통해 배열에 들어있는 값과 다른 여러 값을 함께 써서 함수를 호출할 수 있음 → 자바에는 없는 기능
// kotlin
fun printElements(vararg elements: String) {
println(elements.joinToString(", "))
}
fun main() {
val array = arrayOf("B", "C")
// 스프레드 연산자 *를 사용하여 배열의 요소를 개별 인자로 전달
printElements("A", *array, "D")
// 출력 결과: A, B, C, D
}
/* ---------------------------------------------------------*/
// java
// varargs를 지원하지만, 배열을 인자로 넘길 때 별도의 배열을 만들어 병합하는 등 추가 작업이 필요함.
// 스프레드 연산자 같은 문법이 없어 직접 요소들을 배열에 담아 전달해야 함.
public class Main {
public static void printElements(String... elements) {
System.out.println(String.join(", ", elements));
}
public static void main(String[] args) {
String[] array = {"B", "C"};
// Java에서는 배열을 그대로 전달해야 함.
// 추가적인 원소와 함께 전달하려면 별도의 배열을 만들어야 합니다.
String[] combined = new String[3];
combined[0] = "A";
System.arraycopy(array, 0, combined, 1, array.length);
// combined 배열에 "A", "B", "C"가 들어가게 됩니다.
printElements(combined);
// 만약 "D"도 추가하고 싶다면, 또 다른 배열을 만들어야 합니다.
String[] combined2 = new String[combined.length + 1];
System.arraycopy(combined, 0, combined2, 0, combined.length);
combined2[combined2.length - 1] = "D";
printElements(combined2);
// 출력 결과: A, B, C, D
}
}
3.4.3 값의 쌍 다루기: 중위 호출과 구조 분해 선언
- 코틀린에서 맵을 만들려면 mapOf 함수 사용
val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
- 여기서 to는 코틀린 키워드가 아님 → **중위 호출(infix call)**이라는 특별한 방식으로 to라는 일반 메소드를 호출
- 중위 호출 시에는 수신 객체와 유일한 메소드 인자 사이에 메소드 이름을 넣음 → 이 때 객체, 메소드 이름, 유일한 인자 사이에는 공백이 들어가야 함
1.to("one") // to 메소드를 일반적인 방식으로 호출
1 to "one" // to 메소드를 중위 호출 방식으로 호출
// 두 호출은 동일함
- 인자가 하나뿐인 일반 메소드나 확장 함수에 중위 호출을 사용할 수 있음
- 중위 호출을 사용하려면 infix 변경자를 메소드 선언 앞에 추가
// to 함수 정의를 간략하게 줄인 코드
infix fun Any.to(other:Any) = Pair(this, other)
// Pair 인스턴스를 반환
// 실제로 to는 제네릭 함수지만 여기서는 설명을 위해 생략
- Pair는 코틀린 표준 라이브러리 클래스 → 두 원소로 이뤄진 순서쌍 표현
- Pair의 내용으로 두 변수를 즉시 초기화 할 수 있음
val (number, name) = 1 to "one"
- 이런 기능을 구조 분해 선언(destructuring declaration)이라고 부름

fun main() {
// 중위 호출을 이용하여 Pair 생성
val pair = "Hello" to "World"
// 구조 분해 선언을 통해 Pair의 값들을 개별 변수로 추출
val (greeting, target) = pair
println("$greeting, $target!") // 출력: Hello, World!
}
- Pair 인스턴스 외 다른 객체에도 구조 분해를 적용할 수 있음(ex. key, value 같은 맵의 원소)
- 루프에서도 구조 분해 선언을 활용할 수 있음 → joinToString에서 본 withIndex를 구조 분해 선언과 조합하면 컬렉션 원소의 인덱스와 값을 따로 변수에 담을 수 있음
for ((index, element) in collection.withIndex()) {
println("$index : $element")
}
- to 함수는 확장 함수 → 타입과 관계없이 임의의 순서쌍을 만들 수 있음 → to의 수신 객체가 제네릭하다는 뜻
// mapOf 함수 선언
fun <K, V> mapOf(vararg values: Pair<K, V>) : Map<K, V>
- mapOf 또한 listOf처럼 vararg이므로 원하는 개수만큼 인자를 전달할 수 있음 → mapOf의 경우에는 각 인자가 key와 value로 이뤄진 순서쌍
- 새로운 맵을 만드는 구문은 코틀린에서는 특별한 문법인 것처럼 느껴지지만 실제로는 일반적인 함수를 더 간결한 구문으로 호출하는 것
팀원4 part (3.5 문자열와 정규식 다루기 ~ 3.6 코드 다듬기)
1. 문자열 나누기
자바 개발자라면 split 메서드를 자주 사용했을 것이다. 하지만 자바의 split 메서드는 빈 문자열을 무시하는 특징이 있다.
val result = "12-345-6-A".split("-")
println(result) // [12, 345, 6, A]
위의 예제에서 "-"을 기준으로 문자열을 나누었다.
자바 split과 코틀린 split의 차이
자바의 split 메서드는 인자로 전달된 문자열을 정규식으로 해석하여 동작한다. 하지만 이로 인해 점(.)을 기준으로 문자열을 나누려고 할 때 문제가 발생할 수 있다. 점(.)은 정규식에서 모든 문자를 의미하는 특수 문자이므로, 단순한 문자로 해석되지 않는다.
String input = "www.example.com";
String[] result = input.split(".");
System.out.println(Arrays.toString(result));
위 코드를 실행하면 예상했던 "www", "example", "com"으로 나뉘는 것이 아니라, 빈 배열이 반환되거나 split이 제대로 동작하지 않는 현상이 발생할 수 있다. 이는 .이 정규식에서 특별한 의미를 가지기 때문이다. 이를 해결하려면 백슬래시()를 추가하여 이스케이프 처리해야 한다.
String input = "www.example.com";
String[] result = input.split("\\.");
System.out.println(Arrays.toString(result));
// 출력: [www, example, com]
하지만 코틀린에서는 split 함수에서 문자열을 직접 구분자로 사용하거나, toRegex()를 사용하여 정규식으로 처리할 수 있기 때문에 더 직관적이다.
정규식을 이용한 문자열 분할
코틀린에서는 split 함수가 정규식을 사용할 수 있도록 toRegex() 확장 함수를 제공한다.
val result = "12-345-6.A".split("-".toRegex())
println(result) // [12, 345, 6, A]
또한 여러 개의 구분자를 지정할 수도 있다.
val result = "12.345-6.A".split(".", "-")
println(result) // [12, 345, 6, A]
이를 통해 복잡한 패턴을 쉽게 나누고 정규식을 활용하여 다양한 구분자를 처리할 수 있다. 또한 점(.)을 기준으로 문자열을 분리할 때도 추가적인 이스케이프 처리가 필요하지 않다.
2. 정규식과 3중 따옴표 문자열
파일 경로나 URL 등에서 특정 부분을 추출할 때 유용한 확장 함수를 사용할 수 있다.
fun parsePath(path: String) {
val directory = path.substringBeforeLast("/")
val fullName = path.substringAfterLast("/")
val fileName = fullName.substringBeforeLast(".")
val extension = fullName.substringAfterLast(".")
println("Dir: $directory, name: $fileName, ext: $extension")
}
parsePath("/Users/yole/kotlin-book/chapter.adoc")
// Dir: /Users/yole/kotlin-book, name: chapter, ext: adoc
정규식을 사용하면 위의 코드를 더 간결하게 만들 수 있다.
fun parsePath(path: String) {
val regex = """(.+)/(.+)\.(.+)""".toRegex()
val matchResult = regex.matchEntire(path)
if (matchResult != null) {
val (directory, filename, extension) = matchResult.destructured
println("Dir: $directory, name: $filename, ext: $extension")
}
}
parsePath("/Users/yole/kotlin-book/chapter.adoc")
// Dir: /Users/yole/kotlin-book, name: chapter, ext: adoc
이처럼 정규식을 활용하면 문자열을 더욱 유연하게 처리할 수 있으며, 복잡한 문자열 패턴도 쉽게 추출할 수 있다.
3. 여러 줄 3중 따옴표 문자열
코틀린에서는 여러 줄 문자열을 작성할 때 3중 따옴표(""") 를 사용할 수 있다. 이를 활용하면 이스케이프 문자 없이도 여러 줄 문자열을 쉽게 다룰 수 있다.
val kotlinLogo = """
| //
| //
|/ \""".trimMargin()
println(kotlinLogo)
// 출력 결과:
// //
// //
// / \
이처럼 3중 따옴표 문자열을 사용하면 코드의 가독성이 높아지고, 여러 줄 텍스트를 쉽게 다룰 수 있다.
- 줄 바꿈이 포함된 문자열을 쉽게 만들 수 있다.
- 하지만 \n 같은 특수 문자는 직접 입력해야 한다.
- 문자열 내부에서 템플릿 표현식을 사용할 수 없다.
4. 마무리
코틀린에서는 문자열을 보다 쉽게 다룰 수 있도록 다양한 확장 함수를 제공한다. 특히 split, substringBeforeLast(), toRegex() 등을 활용하면 코드가 더 간결하고 직관적이 된다. 또한, 3중 따옴표 문자열을 사용하면 긴 문자열을 손쉽게 관리할 수 있다. 이를 활용하면 문자열을 더욱 강력하게 다룰 수 있으며, 실용적인 코드 작성이 가능해진다.
코드 다듬기: 로컬 함수와 확장
1. DRY 원칙이란?
DRY(Don't Repeat Yourself) 원칙은 코드 중복을 최소화하여 유지보수성과 가독성을 높이는 소프트웨어 개발 원칙이다.
"한 가지 개념이나 기능을 단 한 번만 구현하라."
반복되는 코드를 줄이면 수정할 때 여러 곳을 고칠 필요가 없어 실수를 방지할 수 있고, 코드가 간결해지며 이해하기 쉬워진다.
하지만 현실에서는 같은 기능이 여러 곳에서 필요할 수 있다. 이런 경우 메서드 추출(Extract Method) 리팩토링을 사용하여 공통된 부분을 함수로 분리할 수 있다. 그러나 너무 많은 작은 메소드가 생기면 코드가 오히려 복잡해질 수 있다. 이를 해결하기 위해 코틀린에서는 로컬 함수와 확장 함수를 활용할 수 있다.
2. 코드 중복 문제와 해결 방법
사용자 정보를 데이터베이스에 저장하기 전에 각 필드를 검증하는 코드가 중복되어 있다.
class User(val id: Int, val name: String, val address: String)
fun saveUser(user: User) {
if (user.name.isEmpty()) {
throw IllegalArgumentException("Can't save user ${user.id}: empty Name")
}
if (user.address.isEmpty()) {
throw IllegalArgumentException("Can't save user ${user.id}: empty Address")
}
// user를 데이터베이스에 저장한다.
}
문제점:
- 같은 검증 로직이 반복되며 코드 중복이 발생한다.
- 새로운 필드가 추가되면 유사한 코드가 여러 곳에서 늘어나 수정이 어려워질 수 있다.
3. 로컬 함수 활용하여 코드 개선
코틀린에서는 로컬 함수를 활용하여 중복된 코드를 함수 내부에서만 사용할 수 있도록 캡슐화할 수 있다.
로컬 함수 적용
fun saveUser(user: User) {
fun validate(user: User, value: String, fieldName: String) {
if (value.isEmpty()) {
throw IllegalArgumentException("Can't save user ${user.id}: empty $fieldName")
}
}
validate(user, user.name, "Name")
validate(user, user.address, "Address")
// user를 데이터베이스에 저장한다.
}
개선된 점:
- 검증 로직을 validate() 함수로 추출하여 코드 중복 제거.
- 새로운 필드가 추가될 경우 손쉽게 검증 로직을 확장할 수 있음.
- validate() 함수는 saveUser() 내부에서만 사용되므로 외부 코드의 복잡성이 줄어듦.
로컬 함수는 자신을 감싸는 함수의 모든 변수와 파라미터에 접근할 수 있다. 이를 이용하면 user 객체를 함수에 전달할 필요 없이, user.id를 직접 참조할 수 있다.
fun saveUser(user: User) {
fun validate(value: String, fieldName: String) {
if (value.isEmpty()) {
throw IllegalArgumentException("Can't save user ${user.id}: empty $fieldName")
}
}
validate(user.name, "Name")
validate(user.address, "Address")
// user를 데이터베이스에 저장한다.
}
최적화된 점:
- validate() 함수에서 user.id에 직접 접근 가능하여 user를 인자로 전달할 필요가 없음.
- 코드가 더욱 간결해지고 유지보수가 쉬워짐.
4. 확장 함수로 변환하여 더 개선하기
로컬 함수도 유용하지만, 검증 로직을 User 객체 자체의 기능으로 만들면 재사용성이 더욱 증가한다. 이럴 때 확장 함수를 사용할 수 있다.
확장 함수 적용
class User(val id: Int, val name: String, val address: String)
fun User.validateBeforeSave() {
fun validate(value: String, fieldName: String) {
if (value.isEmpty()) {
throw IllegalArgumentException("Can't save user $id: empty $fieldName")
}
}
validate(name, "Name")
validate(address, "Address")
}
fun saveUser(user: User) {
user.validateBeforeSave()
// user를 데이터베이스에 저장한다.
}
개선된 점:
- validateBeforeSave() 확장 함수를 통해 User 객체 내부에서 직접 검증 수행 가능.
- saveUser() 함수에서 검증 로직이 사라지고, validateBeforeSave()만 호출하여 코드가 깔끔해짐.
- 다른 곳에서도 validateBeforeSave()를 호출하여 동일한 검증 로직을 재사용 가능.
도서 링크 바로가기
https://product.kyobobook.co.kr/detail/S000001804588
Kotlin in Action | 드미트리 제메로프 - 교보문고
Kotlin in Action | 코틀린이 안드로이드 공식 언어가 되면서 관심이 커졌다. 이 책은 코틀린 언어를 개발한 젯브레인의 코틀린 컴파일러 개발자들이 직접 쓴 일종의 공식 서적이라 할 수 있다. 코틀
product.kyobobook.co.kr
'Programming Language > Kotlin' 카테고리의 다른 글
[Kotlin In Action] 5장. 람다로 프로그래밍 정리 (1) | 2025.03.21 |
---|---|
[Kotlin In Action] 4장. 클래스 , 객체 , 인터페이스 정리 (0) | 2025.03.21 |
[Kotlin In Action] 2장. 코틀린 기초 스터디 정리 (0) | 2025.03.01 |
[Kotlin In Action] 1장. 코틀린이란 무엇이며, 왜 필요한가? 정리 (0) | 2025.02.23 |
[Kotlin] BigDecimal 이란 (0) | 2024.05.31 |
Kotlin in Action을 공부하며 정리한 내용입니다.
저작권에 문제가 될 시, 글을 모두 내리겠습니다.
제가 공부한 내용이 더 많은 분들에게도 도움이 되었으면 좋겠습니다. 부족한 부분은 댓글을 통해서 피드백을 주신다면 언제나 반영하겠습니다. 감사합니다.
책에 대한 링크는 맨 아래에 있습니다.
https://github.com/Kotlin-Android-Study-with-SSAFY/Kotlin_In_Action_1
GitHub - Kotlin-Android-Study-with-SSAFY/Kotlin_In_Action_1: SSAFY 13기 모바일 트랙 구미 5반 "코틀린 인 액션" 스
SSAFY 13기 모바일 트랙 구미 5반 "코틀린 인 액션" 스터디(A). Contribute to Kotlin-Android-Study-with-SSAFY/Kotlin_In_Action_1 development by creating an account on GitHub.
github.com
팀원1 part(3.1 코틀린에서 컬렉션 만들기 ~ 3.2 함수를 호출하기 쉽게 만들기)
코틀린에서 컬렉션 구현하기
- 집합 구현
val set = hashSetof (1, 7, 53)
- 리스트 구현
val list = arrayListOf(1, 7, 53)
- 맵 구현
- 여기서 to 의 경우 → 키워드(X), 일반 함수(O)
val map = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-three"
코틀린에서 구현한 객체는 어떤 클래스일까?
>>> println(set.javaClass)
class java.util.HashSet
>>> println(list.javaClass)
class java.util.ArrayList
>>> println(map.javaClass)
class java.util.HashMap
코틀린에서 javaClass → 자바 getClass()에 해당
- 즉, 코틀린은 자신만의 컬렉션 기능 제공 X
코틀린이 자체 컬렉션을 제공하지 않는 이유
- 표준 자바 컬렉션을 활용하면 자바코드와 상호작용하기 훨씬 쉽게 때문
- 자바에서 코틀린 함수를 호출하거나 코틀린에서 자바 함수를 호출할 때 서로 변환 불필요
- But 코틀린에서는 자바보다 더 많은 기능 사용 가능
- ex) 리스트의 마지막 원소 가져오기, 수로 이뤄진 컬렉션에서 최대값 찾기
>>> val strings = listOf("first", "second", "fourteenth")
>>> printlin(strings.last())
fourteenth
>>> val numbers = setOf(1, 14, 2)
>>> println(numbers.max())
14
3.2 함수를 호출하기 쉽게 만들기
컬렉션에 담긴 원소 출력하기
- 자바 컬렉션에는 디폴트로 toString() 구현이 포함
- 코틀린에서 원소 출력하는 방법
>>> val list = listOf(1, 2, 3)
>>> println(list)
[1, 2, 3]
joinToString() 함수의 초기 구현
- 디폴트 구현과 달리 (1; 2; 3) 처럼 원소 사이를 세미콜론으로 구분하고 괄호로 리스트를 둘러싸고 싶다면?
>>> val list = listOf(1, 2, 3)
>>> println(joinToString(list, ";", "(", ")")
(1; 2; 3)
- joinToString(list, separator, prefix, postfix, limit)
- separator : 원소간 연결 시 사이의 들어갈 문자열 ( ""을 주면 공백없이 연결된다.)
- prefix : 만들 문자열에서 시작할 문자열
- postfix : 만들 문자열에서 종료할 문자열
- limit : 개수 제한 → limit개 이후 ‘…’ 으로 표현
Kotlin joinToString - List 문자열 변환 List to String
1. joinToString - List to String List를 print를 사용하여 출력하거나하면 toString이 호출되며 [ ... ] 와 같은 형태로 변환된다. 하지만 리스트의 원소들을 모두 연결한 하나의 문자열로 만들고 싶은 경우가
notepad96.tistory.com
3.2.1 이름 붙인 인자
- joinToString(list, ";", "(", ")"
- 가독성이 좋지 않음 → 함수의 시그니처를 보지 않고는 위의 코드 상에서 전달인자만으로 정확한 역할을 알기 어려움
- 따라서 다음과 같이 전달인자로 일부(또는 전부)를 이름으로 명시 가능 → 호출 시 인자 중 어느 하나라도 이름을 명시하고 나면 혼동을 막기 위해 그 뒤에 오는 모든 인자는 이름을 꼭 명시
>>> joinToString(collection, sperator=" ", prefix=" ", postfix=".")
불행하게도 자바로 작성한 코드를 호출할 때는 이름 붙인 인자 사용 불가. 따라서 안드로이드 프레임워크나 JDK가 제공하는 함수를 호출할 때도 이름 붙인 인자 사용 불가.
3.2.2 디폴트 파라미터 값
- 자바에서는 일부 클래스에서 오버로딩(Overloading)한 메소드가 너무 많아진다는 문제가 있음 → 인자 중 일부가 생략된 오버로드 함수를 호출할 때 어떤 함수가 불릴지 모호한 경우 발생
- 코틀린에서는 함수 선언 시 파라미터의 디폴트 값을 지정 가능 → 오버로드 중 모호한 경우 회피 가능
- 디폴트 값을 사용한 joinToString 함수
>>> joinToString(list, ",", "", "")
1, 2, 3
>>> joinToString(list) // separtor, prefix, postfix 생략
1, 2, 3
>>> joinToString(list, ";") // separator를 ";"로 지정, prefix와 postfix 생략
이름 붙은 인자를 사용하는 경우 → 인자 목록의 중간에 있는 인자를 생략하고, 지정하고 싶은 인자는 이름을 붙여서 순서 상관없이 지정 가능
>>> joinToString(list, postfix=";", prefix="#")
# 1, 2, 3;
- 함수의 디폴트 파라미터 값 지정 → 함수를 호출하는 부분(X), 함수를 선언하는 부분(O)
- 어떤 클래스 안에 정의된 함수의 디폴트 값을 바꾸고 그 클래스가 포함된 파일을 재컴파일하면 → 그 함수를 호출하는 코드 중에 값을 지정하지 않은 모든 인자는 자동으로 바뀐 디폴트 값을 적용
- 자바에서는 디폴트 파라미터 값이라는 개념이 없음
- 따라서 코틀린 함수를 자바에서 호출하는 경우에는 해당 코틀린 함수가 디폴트 값을 제공하더라도 모든 인자를 명시해야함
- <자바에서 코틀린 함수를 자주 호출할 경우> @JvmOverloads 을 함수에 추가 가능 → 코틀린 컴파일러가 자동으로 맨 마지막 파라미터로부터 파라미터를 하나씩 생략한 오버로딩한 자바 메소드를 추가 해줌
- ex) joinToString에 @JvmOverloads를 붙인 경우 다음과 같이 오버로딩 함수 생성해줌
String joinToString(Collection<T> collection, String separtor, String prefix, String postfix);
String joinToString(Collection<T> collection, String separtor, String prefix);
String joinToString(Collection<T> collection, String separtor);
String joinToString(Collection<T> collection;
3.2.3 정적인 유틸리티 클래스 없애기 : 최상위 함수와 프로퍼티
- 자바에서는 모든 코드를 클래스의 메소드로 작성해야 해서, 특정 클래스에 속하기 애매한 코드들을 모아놓은 정적 메소드 전용 유틸리티 클래스(예: Collections)가 자주 등장
- But 코틀린에서는 이런 유틸리티 클래스가 필요 없음
- 그냥 최상위 함수로 만들면 되고, 패키지만 임포트하면 된다.
- 즉, 클래스 없이도 독립적인 함수들을 간편하게 관리할 수 있음
- JoinToString() 함수를 최상위 함수로 선언하기 → 코틀린에서는 클래스 없이 최상위 수준에 함수를 정의 가능
package strings
fun joinToString(...): String { ... }
- 하지만 JVM은 클래스 내부 코드만 실행 가능하므로, 코틀린 컴파일러가 자동으로 해당 파일 이름과 같은 클래스를 생성
- 예를 들어 join.kt 파일을 컴파일하면 JoinKt 클래스가 생성되며, 이 클래스의 정적 메소드로 함수가 포함
- 자바에서 호출할 때는 이렇게 사용
import strings.JoinKt;
JoinKt.joinToString(list, ", ", "");
- 파일의 클래스 이름을 바꾸고 싶다면 @file:JvmName("새이름") 애노테이션을 추가
@file:JvmName("StringFunctions")
package strings
fun joinToString(...): String { ... }
- 이제 자바에서는 아래처럼 사용 가능
import strings.StringFunctions;
StringFunctions.joinToString(list, ", ", "");
코틀린에서는 클래스 바깥에 변수도 선언 가능
var opCount = 0 // 연산 횟수 저장
fun performOperation() {
opCount++ // 값 변경 가능
}
fun reportOperationCount() {
println("Operation performed $opCount times")
}
이런 변수들은 JVM에서 정적 필드로 저장됨
4. 상수 선언 (const)
- 일반적인 val 프로퍼티는 접근자 메소드(getter)를 통해 접근해야 하지만, 상수처럼 사용하려면 const를 붙이면 됨
const val UNIX_LINE_SEPARATOR = "\n"
- 이렇게 하면 아래와 같은 자바 코드의 public static final 필드로 변환
public static final String UNIX_LINE_SEPARATOR = "\n";
팀원2 part (3.3 메소드를 다른 클래스에 추가: 확장 함수와 확장 프로퍼티)
확장 함수는 어떤 클래스의 멤버 메소드 인 것처럼 호출할 수 있지만 그 클래스 밖에 선언된 함수다.
기존 클래스의 소스를 수정하지 않고도 새로운 기능을 추가할 수 있는 강력한 기능
- 자바에서는 기존 클래스를 확장하려면 상속하거나 데코레이터 패턴을 사용해야 했지만, 코틀린에서는 확장 함수를 활용하면 훨씬 간편하게 기능을 추가할 수 있다.
클래스명.함수명 형태로 선언하며, this 키워드를 사용하며 해당 클래스의 멤버처럼 접근할 수 있다.
클래스의 멤버 함수 : 클래스 내부에 정의된 함수
// String 클래스를 확장해 lastChar()라는 함수를 추가
fun String.lastChar() : Char {
//this는 호출하는 주체(수신 객체)를 가리킴. 여기서는 "hello" 문자열이 됨.
return this.get(this.length-1)
}
fun main () {
println("Kotlin".lastChar()) // 'o' 출력
}
- 추가하려는 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙인다.
- 수신 객체 타입(receiver type) : 클래스 이름 ex) String
- 수신 객체(receiver object) : 확장 함수가 호출되는 대상이 되는 값(객체) ex) this, “Kotlin”
- this를 사용하면, 현재 함수를 호출한 “수신 객체”를 가리킨다.
- 실제 함수를 호출할 때 쓰이는 “실제 인스턴스”를 의미
수식 객체 타입은 확장이 정의될 클래스의 타입이며, 수신 객체는 그 클래스에 속한 인스턴스 객체이다.
어떤 언어로 작성됐는지 중요하지 않다. 다른 JVM 언어로 작성된 클래스도 확장할 수 있다. 즉, 자바 클래스로 컴파일한 클래스 파일이 있는 한 그 클래스에 원하는 대로 확장을 추가할 수 있다❕
→ JVM에 돌아가는 언어로 작성된 코드는 결국 .class 파일로 컴파일 되기 때문에 코틀린은 그 .class 파일만 있으면, 거기에 정의된 클래스를 대상으로 확장 함수를 추가할 수 있다.
일반 메소드의 본문에서 this를 사용할 때와 마찬가지로 확장 함수 본문에서도 this를 쓸 수 있다. 그리고 일반 메소드와 마찬가지로 확장 함수 본문에서도 this를 생략할 수 있다.
package strings
fun String.lastChar() : Char = get(length - 1)
- 확장 함수 내부에서는 일반적인 인스턴스 메소드의 내부에서와 마찬가지로 수신 객체의 메소드나 프로퍼티를 바로 사용할 수 있다.
- 하지만 확장 함수가 캡슐화를 깨지는 않는다!!!
- 캡슐화❔ 객체의 내부 데이터와 구현을 외부로부터 숨기고, 오직 공개된 인터페이스를 통해서만 상호작용하도록 하는 개념
- 클래스 안에서 정의한 메소드와 달리 확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 private 멤버나 protected 멤버를 사용할 수 없다.
- 캡슐화 특징 객체 내부의 변수(데이터)는 보통 private 또는 protected로 선언되어 외부에서 직접 접근하거나 변경할 수 없다. 이를 통해 의도치 않은 데이터 변경이나 오작동 방지
- 클래스의 멤버 메소드와 확장 함수를 모두 메소드라고 부른다.
3.3.1 임포트와 확장 함수
확장 함수를 정의했다고 해서 자동으로 프로젝트 안의 모든 소스코드에서 함수를 사용할 수 있지는 않다. 확장함수를 사용하기 위해서는 그 함수를 다른 클래스나 함수와 마찬가지로 import해야 한다.
- 코틀린에서는 클래스를 import할 때와 동일한 구문을 사용해 개별 함수를 import할 수 있다.
// 1. import
import strings.lastChar
// 2. *를 사용한 임포트도 잘 작동함
import strings.*.
// 3. as 키워드를 사용하여 import한 클래스나 함수를 다른 이름으로 부를 수 있음
import strings.lastChar as last
val c = "Kotlin".lastChar()
다른 여러 패키지에 이름이 같은 함수가 많은데 한 파일 안에서 그런 함수들을 함께 써야 하는 경우 이름을 바꿔서 import하면 편리하다.
import com.example.utils.printMessage as utilsPrint
import com.example.tools.printMessage as toolsPrint
fun main() {
utilsPrint() // Utils Package
toolsPrint() // Tools Package
}
일반적인 클래스나 함수를 부를 때 전체 이름을 쓰면 된다.
fun main() {
val text = "Hello"
println(com.example.utils.formatText(text)) // Utils: Hello
println(com.example.tools.formatText(text)) // Tools: Hello
}
패키지가 같을 경우 이름 충돌 해결은?
확장 함수 대신 클래스 내부의 멤버 함수로 정의하면 객체를 통해 호출해야 하므로 충돌을 피할 수 있다. 이렇게 하면 확장 함수가 아니라 멤버 함수가 되므로 충돌을 방지할 수 있다
package com.example.utils
class Formatter {
fun formatText(text: String) = "Utils: $text"
}
package com.example.utils
class Formatter {
fun formatText(text: String) = "Tools: $text"
}
import com.example.utils.Formatter as UtilsFormatter
import com.example.utils.Formatter as ToolsFormatter
fun main() {
val utils = UtilsFormatter()
val tools = ToolsFormatter()
println(utils.formatText("Hello")) // Utils: Hello
println(tools.formatText("Hello")) // Tools: Hello
}
하지만, 확장 함수는 코틀린 문법상 반드시 짧은 이름을 써야 한다. 따라서 import시 이름을 바꾸는 것이 확장 함수의 이름 충돌을 해결하는 유일한 방법이다!!
3.3.2 자바에서 확장 함수 호출
내부적으로 확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메소드이다. 그래서 확장 함수를 호출해도 다른 어댑터 객체나 실행 시점 부가 비용이 들지 않는다.
자바의 정적 메소드
- MathUtils.add(5, 3); → 클래스 이름으로 바로 호출 가능.
- 객체를 생성할 필요가 없음.
class MathUtils {
public static int add(int a, int b) {
return a + b;
}
}
public class Main {
public static void main(String[] args) {
int result = MathUtils.add(5, 3); // 객체 생성 없이 호출
System.out.println(result); // 8
}
}
코틀린의 정적 메소드
- 코틀린에서는 static 키워드가 없으므로, 파일 최상위 함수 또는 companion object를 사용해야 함.
- MathUtils.add(5, 3) → 객체 생성 없이 바로 호출 가능.
object MathUtils {
fun add(a: Int, b: Int): Int {
return a + b
}
}
fun main() {
val result = MathUtils.add(5, 3) // 객체 생성 없이 호출
println(result) // 8
}
인스턴스 메소드
- 정적 메소드가 아니라 일반 인스턴스 메소드(멤버 함수)는 반드시 객체를 생성해야 호출할 수 있음
- MathUtils() 객체를 먼저 만들어야 add() 메소드를 사용할 수 있음.
class MathUtils {
fun add(a: Int, b: Int): Int {
return a + b
}
}
fun main() {
val math = MathUtils() // 객체 생성
val result = math.add(5, 3) // 객체를 통해 호출
println(result) // 8
}
코틀린에서 확장함수는 클래스 내부에 있는 것이 아니라, 외부에서 정의된 함수이므로, 일반 멤버 함수처럼 객체가 필요하지 않다. 그래서 컴파일 시에 정적 메소드(static method)로 변환된다.
3.3.3 확장 함수로 유틸리티 함수 정의
유틸리티 함수는 일반적으로 특정 클래스의 상태와 무관하게 독립적으로 동작하며, 여러 곳에서 재사용 가능한 기능을 제공한다. 자바에서는 종종 static 메소드로 정의되는 경우가 많다.
- 클래스 외부에 정의되어 특정 기능(리스트를 문자열로 변환)을 독립적으로 수행
- 컴파일 시 정적 메소드로 변환되어, 인스턴스 상태와 무관하게 재사용 가능한 기능을 제공
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 수신 객체를 가리킴
if (index > 0) result.append(separator) //T 타입의 원소로 이뤄진 컬렉션
result.append(element)
}
result.append(postfix)
return result.toString()
}
val list = listOf(1, 2, 3)
println(list.joinToString(separator = "; ", prefix = "(", postfix = ")"))
//출력 결과
(1; 2; 3)
확장 함수는 클래스 외부에 정의되어 정적 메소드로 컴파일되므로, 인스턴스에 종속되지 않고 독립적으로 기능을 수행하는 유틸리티 함수라고 할 수 있습니다.
문자열의 컬렉션에 대해서만 호출할 수 있는 join함수를 정의하고 싶다면 다음과 같이 하면 된다.
- join() 확장 함수는 Collection에만 적용되기 때문이다.
- 확장 함수가 정적 메소드와 같은 특징을 가지므로, 확장 함수를 하위 클래스에서 오버라이드할 수 없다.
fun Collection<String>.join {
separator : String = " , " ,
prefix : String = "",
postfix : String = ""
) = joinToString(separator, prefix, postfix)
val words = listOf("one", "two", "eight")
println(words.join(" "))
//출력 결과
"one two eight"
val numbers = listOf(1, 2, 8)
println(numbers.join(" "))
//출력 결과
Error: Type mismatch:
inferred type is List<Int>but Collection<String> was expected
3.3.4 확장 함수는 오버라이드할 수 없다.
코틀린 메소드 오버라이드도 일반적인 객체지향의 메소드 오버라이드와 마찬가지이다. 하지만, 확장 함수는 오버라이드할 수 없다.
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
- open class View : 기본 클래스, open fun click() 메소드를 가지고 있음
- val view: View = Button() : 변수 view는 정적 타입은 View지만, 실제 할당되는 인스턴스는 Button입니다.
- 메소드 호출 시 실제 객체 타입에 따라 실행되는 메소드가 결정되는 서브타입(다형성)의 특징으로, Button이 View의 하위 타입이기 때문이다.
- 위의 예제는 변수의 정적 타입(View)과 실제 저장된 객체의 동적 타입(Button)이 다르다.
View와 Button 클래스에 대해 선언된 두 showOff 확장 함수
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의 정적 타입은 View이고, 실제 저장된 객체(동적 타입)는 Button이다.
- 확장 함수는 변수의 정적 타입을 기준으로 선택되므로, view.showOff() 호출 시 View에 대해 정의된 확장 함수가 호출된다.
- 확장 함수를 첫 번째 인자가 수신 객체인 정적 자바 메소드로 컴파일한다는 사실을 기억한다면 이런 동작을 쉽게 이해할 수 있다.
- 결과적으로 "I'm a view!"가 출력
어떤 클래스를 확장한 함수와 그 클래스의 멤버 함수 이름과 시그니처가 같다면 확장 함수가 아니라 멤버 함수가 호출된다 → 멤버 함수의 우선순위가 높다
3.3.5 확장 프로퍼티
객체가 가지는 데이터를 표현하는 요소 클래스 내부에서 객체의 상태(데이터)를 저장하고 관리하기 위한 변수와 비슷한 개념 단순한 변수와 달리 접근자(getter,setter)라는 기능이 결합되어 있어 데이터를 읽거나 쓸 때 자동으로 호출되는 메서드를 제공한다.
- val과 var
- val: 읽기 전용 프로퍼티. 초기화 후 값이 변경되지 않습니다. (즉, getter만 존재)
- var: 변경 가능한 프로퍼티. 값을 읽을 때는 getter, 값을 수정할 때는 setter가 호출됩니다.
class Person {
val name: String = "John" // 읽기 전용 프로퍼티
var age: Int = 30 // 읽기와 쓰기가 가능한 프로퍼티
}
fun main() {
val person = Person()
println(person.name) // "John" 출력
println(person.age) // 30 출력
person.age = 35 // setter가 호출되어 age 값이 변경됨
println(person.age) // 35 출력
}
- 위 예시에서 name은 val로 선언되었기 때문에 값을 변경할 수 없고, age는 var로 선언되어 값을 변경할 수 있습니다.
- person.name이나 person.age를 호출하면 내부적으로 자동 생성된 getter가 사용됩니다.
- person.age = 35 처럼 값을 할당하면 자동 생성된 setter가 호출됩니다.
확장 프로퍼티도 일반적인 프로퍼티와 같은데, 수신 객체 클래스가 추가됐을 뿐이다. 뒷받침하는 필드(field)가 없어서 기본 getter 구현을 제공할 수 없으므로 최소한 getter는 꼭 정의해야 한다
- 초기화 코드에서 계산한 값을 담을 장소가 전혀 없으므로, 초기화 코드도 쓸 수 없다.
var String.lastChar : Char
get () = get(length-1)
- StringBuilder에 같은 프로퍼티를 정의한다면 StringBuilder의 맨 마지막 문자는 변경 가능하므로 프로퍼티를 var로 만들 수 있다.
- 자바에서 확장 프로퍼티를 사용하고 싶다면 항상 게터나 세터를 명시적으로 호출해야 한다.
var StringBuilder.lastChar : Char
get() = get(length-1)
set(value : Char){
this.setCharAt(length-1, value)
}
>>> println("Kotlin".lastChar)
n
>>> val sb = StringBuilder("Kotlin?")
>>> sb.lastChar = '!'
>>>println(sb)
Kotlin!
일반 프로퍼티 : 클래스 내부에 선언되어 실제 값을 저장하는 백킹 필드가 존재할 수 있다. 확장 프로퍼티 : 클래스 외부에서 추가하는 프로퍼티인데, 백킹 필드가 없어서 값을 저장할 수 없고, 반드시 계산된 결과나 다른 방법으로 값을 제공해야 한다.
백킹 필드?
프로퍼티의 값을 저장하는 실제 메모리 공간으로, getter와 setter에서 field 키워드를 사용하면, 이 백킹 필드에 접근하여 값을 읽거나 수정할 수 있음
- 프로퍼티에 커스텀 getter나 setter를 정의할 때, field라는 특별한 키워드를 사용하여 백킹 필드에 접근할 수 있습니다.
- 예를 들어, 아래와 같이 커스텀 getter와 setter를 정의할 때 field를 사용하면, 실제 저장된 값을 읽거나 수정할 수 있습니다.
var nickname: String = "Guest"
get() {
println("getter 호출")
return field // 여기서 field는 백킹 필드를 의미
}
set(value) {
println("setter 호출")
field = value // 백킹 필드에 새로운 값을 저장
}
확장 프로퍼티는 원본 클래스에 새로운 프로퍼티를 추가하는 방법인데, 이 경우에는 백킹 필드(field)를 가질 수 없기 때문에 항상 계산된 결과만 반환해야 한다.
var String.lastChar: Char
get() = this.get(this.length - 1)
// 확장 프로퍼티는 백킹 필드가 없으므로 setter 없이 읽기 전용으로 만드는 경우가 많습니다.
팀원3 part (3.4 컬렉션 처리: 가변 길이 인자, 중위 함수 호출, 라이브러리 지원)
- varang 키워드를 사용하면 호출 시 인자 개수가 달라질 수 있는 함수를 정의할 수 있음
- 중위(infix) 함수 호출 구문을 사용하면 인자가 하나뿐인 메소드를 간편하게 호출할 수 있음
- 구조 분해 선언(destructuring declaration)을 사용하면 복합적인 값을 분해해서 여러 변수를 나눠 담을 수 있음
3.4.1 자바 컬렉션 API 확장
- 코틀린 컬렉션은 자바와 같은 클래스를 사용하지만 더 확장된 API를 제공
>>> val strings: List<String> = listOf("first", "second", "fourteenth")
>>> strings.last()
fourteenth
>>> val numbers: Collection<Int> = setOf(1, 14, 2)
>>> numbers.max()
14
- 자바 라이브러리 클래스의 인스턴스인 켈렉션에 대해 코틀린이 새로운 기능을 추가할 수 있는 이유? → last()와 max()는 모두 확장 함수이기 때문(3.3 절)
fun <T> List<T>.last() : T {/* 마지막 원소를 반환함 */}
fun Collection<Int>.max() : Int {/* 컬렉션의 최댓값을 찾음 */}
// last와 max는 모두 코틀린 표준 라이브러리 함수이자 자바 라이브러리인 컬렉션의 확장 함수
- 코틀린 표준 라이브러리 또한 자바처럼 IDE의 코드 완성 기능을 통해 메소드나 함수를 살펴볼 수 있음
3.4.2 가변 인자 함수: 인자의 개수가 달라질 수 있는 함수 정의
- 리스트를 생성하는 함수를 호출할 때 원하는 만큼 많이 원소를 전달할 수 있음
val list = listOf(2, 3, 5, 7, 11)
// listOf 함수 정의
fun listOf<T>(vararg values : T): List<T> { ... }
- 가변 길이 인자(vararg): 메소드를 호출할 때 원하는 개수만큼 값을 인자로 넘기면 자바 컴파일러가 배열에 그 값들을 넣어주는 기능
- 코틀린도 비슷하지만 문법이 조금 다름 → 타입 뒤에 … 를 붙이는 대신 파라미터 앞에 vararg 변경자를 붙임
- 이미 배열에 들어있는 원소를 가변 길이 인자로 넘길 경우
- 자바에서는 배열을 그냥 넘기면 되지만 코틀린에서는 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 해야 함
- 기술적으로는 스프레드(spread) 연산자가 그런 작업을 해줌 → 실제로는 전달하려는 배열 앞에 *****를 붙이면 됨
fun main(args: Array<String>) {
val list = listOf("args: ", *args) // 스프레드 연산자가 배열의 내용을 펼쳐줌
println(list)
}
- 스프레드 연산자를 통해 배열에 들어있는 값과 다른 여러 값을 함께 써서 함수를 호출할 수 있음 → 자바에는 없는 기능
// kotlin
fun printElements(vararg elements: String) {
println(elements.joinToString(", "))
}
fun main() {
val array = arrayOf("B", "C")
// 스프레드 연산자 *를 사용하여 배열의 요소를 개별 인자로 전달
printElements("A", *array, "D")
// 출력 결과: A, B, C, D
}
/* ---------------------------------------------------------*/
// java
// varargs를 지원하지만, 배열을 인자로 넘길 때 별도의 배열을 만들어 병합하는 등 추가 작업이 필요함.
// 스프레드 연산자 같은 문법이 없어 직접 요소들을 배열에 담아 전달해야 함.
public class Main {
public static void printElements(String... elements) {
System.out.println(String.join(", ", elements));
}
public static void main(String[] args) {
String[] array = {"B", "C"};
// Java에서는 배열을 그대로 전달해야 함.
// 추가적인 원소와 함께 전달하려면 별도의 배열을 만들어야 합니다.
String[] combined = new String[3];
combined[0] = "A";
System.arraycopy(array, 0, combined, 1, array.length);
// combined 배열에 "A", "B", "C"가 들어가게 됩니다.
printElements(combined);
// 만약 "D"도 추가하고 싶다면, 또 다른 배열을 만들어야 합니다.
String[] combined2 = new String[combined.length + 1];
System.arraycopy(combined, 0, combined2, 0, combined.length);
combined2[combined2.length - 1] = "D";
printElements(combined2);
// 출력 결과: A, B, C, D
}
}
3.4.3 값의 쌍 다루기: 중위 호출과 구조 분해 선언
- 코틀린에서 맵을 만들려면 mapOf 함수 사용
val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
- 여기서 to는 코틀린 키워드가 아님 → **중위 호출(infix call)**이라는 특별한 방식으로 to라는 일반 메소드를 호출
- 중위 호출 시에는 수신 객체와 유일한 메소드 인자 사이에 메소드 이름을 넣음 → 이 때 객체, 메소드 이름, 유일한 인자 사이에는 공백이 들어가야 함
1.to("one") // to 메소드를 일반적인 방식으로 호출
1 to "one" // to 메소드를 중위 호출 방식으로 호출
// 두 호출은 동일함
- 인자가 하나뿐인 일반 메소드나 확장 함수에 중위 호출을 사용할 수 있음
- 중위 호출을 사용하려면 infix 변경자를 메소드 선언 앞에 추가
// to 함수 정의를 간략하게 줄인 코드
infix fun Any.to(other:Any) = Pair(this, other)
// Pair 인스턴스를 반환
// 실제로 to는 제네릭 함수지만 여기서는 설명을 위해 생략
- Pair는 코틀린 표준 라이브러리 클래스 → 두 원소로 이뤄진 순서쌍 표현
- Pair의 내용으로 두 변수를 즉시 초기화 할 수 있음
val (number, name) = 1 to "one"
- 이런 기능을 구조 분해 선언(destructuring declaration)이라고 부름

fun main() {
// 중위 호출을 이용하여 Pair 생성
val pair = "Hello" to "World"
// 구조 분해 선언을 통해 Pair의 값들을 개별 변수로 추출
val (greeting, target) = pair
println("$greeting, $target!") // 출력: Hello, World!
}
- Pair 인스턴스 외 다른 객체에도 구조 분해를 적용할 수 있음(ex. key, value 같은 맵의 원소)
- 루프에서도 구조 분해 선언을 활용할 수 있음 → joinToString에서 본 withIndex를 구조 분해 선언과 조합하면 컬렉션 원소의 인덱스와 값을 따로 변수에 담을 수 있음
for ((index, element) in collection.withIndex()) {
println("$index : $element")
}
- to 함수는 확장 함수 → 타입과 관계없이 임의의 순서쌍을 만들 수 있음 → to의 수신 객체가 제네릭하다는 뜻
// mapOf 함수 선언
fun <K, V> mapOf(vararg values: Pair<K, V>) : Map<K, V>
- mapOf 또한 listOf처럼 vararg이므로 원하는 개수만큼 인자를 전달할 수 있음 → mapOf의 경우에는 각 인자가 key와 value로 이뤄진 순서쌍
- 새로운 맵을 만드는 구문은 코틀린에서는 특별한 문법인 것처럼 느껴지지만 실제로는 일반적인 함수를 더 간결한 구문으로 호출하는 것
팀원4 part (3.5 문자열와 정규식 다루기 ~ 3.6 코드 다듬기)
1. 문자열 나누기
자바 개발자라면 split 메서드를 자주 사용했을 것이다. 하지만 자바의 split 메서드는 빈 문자열을 무시하는 특징이 있다.
val result = "12-345-6-A".split("-")
println(result) // [12, 345, 6, A]
위의 예제에서 "-"을 기준으로 문자열을 나누었다.
자바 split과 코틀린 split의 차이
자바의 split 메서드는 인자로 전달된 문자열을 정규식으로 해석하여 동작한다. 하지만 이로 인해 점(.)을 기준으로 문자열을 나누려고 할 때 문제가 발생할 수 있다. 점(.)은 정규식에서 모든 문자를 의미하는 특수 문자이므로, 단순한 문자로 해석되지 않는다.
String input = "www.example.com";
String[] result = input.split(".");
System.out.println(Arrays.toString(result));
위 코드를 실행하면 예상했던 "www", "example", "com"으로 나뉘는 것이 아니라, 빈 배열이 반환되거나 split이 제대로 동작하지 않는 현상이 발생할 수 있다. 이는 .이 정규식에서 특별한 의미를 가지기 때문이다. 이를 해결하려면 백슬래시()를 추가하여 이스케이프 처리해야 한다.
String input = "www.example.com";
String[] result = input.split("\\.");
System.out.println(Arrays.toString(result));
// 출력: [www, example, com]
하지만 코틀린에서는 split 함수에서 문자열을 직접 구분자로 사용하거나, toRegex()를 사용하여 정규식으로 처리할 수 있기 때문에 더 직관적이다.
정규식을 이용한 문자열 분할
코틀린에서는 split 함수가 정규식을 사용할 수 있도록 toRegex() 확장 함수를 제공한다.
val result = "12-345-6.A".split("-".toRegex())
println(result) // [12, 345, 6, A]
또한 여러 개의 구분자를 지정할 수도 있다.
val result = "12.345-6.A".split(".", "-")
println(result) // [12, 345, 6, A]
이를 통해 복잡한 패턴을 쉽게 나누고 정규식을 활용하여 다양한 구분자를 처리할 수 있다. 또한 점(.)을 기준으로 문자열을 분리할 때도 추가적인 이스케이프 처리가 필요하지 않다.
2. 정규식과 3중 따옴표 문자열
파일 경로나 URL 등에서 특정 부분을 추출할 때 유용한 확장 함수를 사용할 수 있다.
fun parsePath(path: String) {
val directory = path.substringBeforeLast("/")
val fullName = path.substringAfterLast("/")
val fileName = fullName.substringBeforeLast(".")
val extension = fullName.substringAfterLast(".")
println("Dir: $directory, name: $fileName, ext: $extension")
}
parsePath("/Users/yole/kotlin-book/chapter.adoc")
// Dir: /Users/yole/kotlin-book, name: chapter, ext: adoc
정규식을 사용하면 위의 코드를 더 간결하게 만들 수 있다.
fun parsePath(path: String) {
val regex = """(.+)/(.+)\.(.+)""".toRegex()
val matchResult = regex.matchEntire(path)
if (matchResult != null) {
val (directory, filename, extension) = matchResult.destructured
println("Dir: $directory, name: $filename, ext: $extension")
}
}
parsePath("/Users/yole/kotlin-book/chapter.adoc")
// Dir: /Users/yole/kotlin-book, name: chapter, ext: adoc
이처럼 정규식을 활용하면 문자열을 더욱 유연하게 처리할 수 있으며, 복잡한 문자열 패턴도 쉽게 추출할 수 있다.
3. 여러 줄 3중 따옴표 문자열
코틀린에서는 여러 줄 문자열을 작성할 때 3중 따옴표(""") 를 사용할 수 있다. 이를 활용하면 이스케이프 문자 없이도 여러 줄 문자열을 쉽게 다룰 수 있다.
val kotlinLogo = """
| //
| //
|/ \""".trimMargin()
println(kotlinLogo)
// 출력 결과:
// //
// //
// / \
이처럼 3중 따옴표 문자열을 사용하면 코드의 가독성이 높아지고, 여러 줄 텍스트를 쉽게 다룰 수 있다.
- 줄 바꿈이 포함된 문자열을 쉽게 만들 수 있다.
- 하지만 \n 같은 특수 문자는 직접 입력해야 한다.
- 문자열 내부에서 템플릿 표현식을 사용할 수 없다.
4. 마무리
코틀린에서는 문자열을 보다 쉽게 다룰 수 있도록 다양한 확장 함수를 제공한다. 특히 split, substringBeforeLast(), toRegex() 등을 활용하면 코드가 더 간결하고 직관적이 된다. 또한, 3중 따옴표 문자열을 사용하면 긴 문자열을 손쉽게 관리할 수 있다. 이를 활용하면 문자열을 더욱 강력하게 다룰 수 있으며, 실용적인 코드 작성이 가능해진다.
코드 다듬기: 로컬 함수와 확장
1. DRY 원칙이란?
DRY(Don't Repeat Yourself) 원칙은 코드 중복을 최소화하여 유지보수성과 가독성을 높이는 소프트웨어 개발 원칙이다.
"한 가지 개념이나 기능을 단 한 번만 구현하라."
반복되는 코드를 줄이면 수정할 때 여러 곳을 고칠 필요가 없어 실수를 방지할 수 있고, 코드가 간결해지며 이해하기 쉬워진다.
하지만 현실에서는 같은 기능이 여러 곳에서 필요할 수 있다. 이런 경우 메서드 추출(Extract Method) 리팩토링을 사용하여 공통된 부분을 함수로 분리할 수 있다. 그러나 너무 많은 작은 메소드가 생기면 코드가 오히려 복잡해질 수 있다. 이를 해결하기 위해 코틀린에서는 로컬 함수와 확장 함수를 활용할 수 있다.
2. 코드 중복 문제와 해결 방법
사용자 정보를 데이터베이스에 저장하기 전에 각 필드를 검증하는 코드가 중복되어 있다.
class User(val id: Int, val name: String, val address: String)
fun saveUser(user: User) {
if (user.name.isEmpty()) {
throw IllegalArgumentException("Can't save user ${user.id}: empty Name")
}
if (user.address.isEmpty()) {
throw IllegalArgumentException("Can't save user ${user.id}: empty Address")
}
// user를 데이터베이스에 저장한다.
}
문제점:
- 같은 검증 로직이 반복되며 코드 중복이 발생한다.
- 새로운 필드가 추가되면 유사한 코드가 여러 곳에서 늘어나 수정이 어려워질 수 있다.
3. 로컬 함수 활용하여 코드 개선
코틀린에서는 로컬 함수를 활용하여 중복된 코드를 함수 내부에서만 사용할 수 있도록 캡슐화할 수 있다.
로컬 함수 적용
fun saveUser(user: User) {
fun validate(user: User, value: String, fieldName: String) {
if (value.isEmpty()) {
throw IllegalArgumentException("Can't save user ${user.id}: empty $fieldName")
}
}
validate(user, user.name, "Name")
validate(user, user.address, "Address")
// user를 데이터베이스에 저장한다.
}
개선된 점:
- 검증 로직을 validate() 함수로 추출하여 코드 중복 제거.
- 새로운 필드가 추가될 경우 손쉽게 검증 로직을 확장할 수 있음.
- validate() 함수는 saveUser() 내부에서만 사용되므로 외부 코드의 복잡성이 줄어듦.
로컬 함수는 자신을 감싸는 함수의 모든 변수와 파라미터에 접근할 수 있다. 이를 이용하면 user 객체를 함수에 전달할 필요 없이, user.id를 직접 참조할 수 있다.
fun saveUser(user: User) {
fun validate(value: String, fieldName: String) {
if (value.isEmpty()) {
throw IllegalArgumentException("Can't save user ${user.id}: empty $fieldName")
}
}
validate(user.name, "Name")
validate(user.address, "Address")
// user를 데이터베이스에 저장한다.
}
최적화된 점:
- validate() 함수에서 user.id에 직접 접근 가능하여 user를 인자로 전달할 필요가 없음.
- 코드가 더욱 간결해지고 유지보수가 쉬워짐.
4. 확장 함수로 변환하여 더 개선하기
로컬 함수도 유용하지만, 검증 로직을 User 객체 자체의 기능으로 만들면 재사용성이 더욱 증가한다. 이럴 때 확장 함수를 사용할 수 있다.
확장 함수 적용
class User(val id: Int, val name: String, val address: String)
fun User.validateBeforeSave() {
fun validate(value: String, fieldName: String) {
if (value.isEmpty()) {
throw IllegalArgumentException("Can't save user $id: empty $fieldName")
}
}
validate(name, "Name")
validate(address, "Address")
}
fun saveUser(user: User) {
user.validateBeforeSave()
// user를 데이터베이스에 저장한다.
}
개선된 점:
- validateBeforeSave() 확장 함수를 통해 User 객체 내부에서 직접 검증 수행 가능.
- saveUser() 함수에서 검증 로직이 사라지고, validateBeforeSave()만 호출하여 코드가 깔끔해짐.
- 다른 곳에서도 validateBeforeSave()를 호출하여 동일한 검증 로직을 재사용 가능.
도서 링크 바로가기
https://product.kyobobook.co.kr/detail/S000001804588
Kotlin in Action | 드미트리 제메로프 - 교보문고
Kotlin in Action | 코틀린이 안드로이드 공식 언어가 되면서 관심이 커졌다. 이 책은 코틀린 언어를 개발한 젯브레인의 코틀린 컴파일러 개발자들이 직접 쓴 일종의 공식 서적이라 할 수 있다. 코틀
product.kyobobook.co.kr
'Programming Language > Kotlin' 카테고리의 다른 글
[Kotlin In Action] 5장. 람다로 프로그래밍 정리 (1) | 2025.03.21 |
---|---|
[Kotlin In Action] 4장. 클래스 , 객체 , 인터페이스 정리 (0) | 2025.03.21 |
[Kotlin In Action] 2장. 코틀린 기초 스터디 정리 (0) | 2025.03.01 |
[Kotlin In Action] 1장. 코틀린이란 무엇이며, 왜 필요한가? 정리 (0) | 2025.02.23 |
[Kotlin] BigDecimal 이란 (0) | 2024.05.31 |