kotlin 개발전 문법관련 기초내용 정리 포스트입니다
인프런 - 코틀린 문법부터 실무까지 (자바 to 코틀린 실무) 강의를 보면서 예시 코드를 다르게 하여 포스팅합니다
typescript, java 개발 경험이 있습니다
다루는 내용
- 인터페이스
- open키워드와 override
- 가시성
- object
인터페이스
자바의 implement와 extends 를 ":"으로 대체합니다
인터페이스 메서드 구현은 override를 반드시 명시해야합니다
interface Clickable {
fun click(): Unit
}
class Button: Clickable {
override fun click() {
println("Button clicked")
}
}
open키워드와 override
kotlin은 클래스를 생성할때 기본적으로 final class로 생성됩니다
final class는 상속이 되지 않기 때문에 상속을 하고싶은 경우에는 open 키워드를 입력해줘야 합니다
class FinalButton {
fun click() = println("Button clicked")
}
open class RichButton : Clickable {
// 기본적으로 final method
fun disable() {}
// open을 붙여서 override 가능합니다
open fun animate() {}
// override도 override 가능합니다
override fun click() {
println("RichButton clicked")
}
}
가시성
kotlin은 기본적으로 public 입니다
새로 도입된 internal은 모듈 안에서만 볼 수 있는 가시성입니다
private은 클래스 내에서 사용하면 해당 클래스에서만 사용가능하며, 파일 내에서 사용하면 해당 파일안에서만 볼 수 있습니다
internal 예시
// 데이터베이스 관련 모듈 내부
internal object DatabaseUtils {
fun sanitizeInput(input: String): String {
// SQL 인젝션 방지를 위한 입력 정제 로직
}
}
이 유틸리티 클래스는 데이터베이스 모듈 내부에서만 사용되며, 다른 모듈에서는 직접 접근할 필요가 없습니다.
// 네트워킹 모듈
public class ApiClient {
internal class RequestBuilder {
// 요청 생성 로직
}
fun makeRequest() {
val builder = RequestBuilder()
// ...
}
}
RequestBuilder는 ApiClient의 내부 구현 세부사항이므로 모듈 외부에 노출될 필요가 없습니다.
// 게임 엔진 모듈
public class GameEngine {
internal data class GameState(
val score: Int,
val level: Int,
val playerPosition: Position
)
private var currentState: GameState = GameState(0, 1, Position(0, 0))
fun updateGame() {
// currentState를 사용하여 게임 상태 업데이트
}
}
GameState는 게임 엔진 모듈 내부에서만 사용되는 중간 상태 객체입니다.
// 앱의 코어 모듈
public class UserManager {
internal fun resetForTesting() {
// 테스트를 위한 상태 초기화 로직
}
}
resetForTesting 함수는 같은 모듈 내의 테스트 코드에서만 사용되어야 하며, 다른 모듈에서는 접근할 필요가 없습니다.
이러한 예시들에서 internal의 사용은 모듈의 내부 구현을 캡슐화하고, 불필요한 외부 노출을 방지하며, 모듈의 API를 깔끔하게 유지하는 데 도움을 줍니다. 이는 코드의 유지보수성과 모듈의 독립성을 높이는 데 기여합니다.
sealed
제한된 계층 구조:
sealed 클래스나 인터페이스는 자신의 하위 타입을 제한합니다. 모든 직접적인 하위 클래스나 구현체는 같은 파일 내에 선언되어야 합니다.
단, Kotlin 1.6부터는 sealed interface를 사용할 경우, 같은 모듈 내의 다른 파일에서도 구현체를 정의할 수 있게 되었습니다. 이는 sealed class보다 좀 더 유연한 사용을 가능하게 합니다.
컴파일 시점 검사:
컴파일러가 sealed 클래스나 인터페이스의 모든 하위 타입을 알 수 있게 합니다.
when 표현식과의 시너지:
when 표현식에서 사용할 때, 모든 케이스를 처리했는지 컴파일 시점에 확인할 수 있습니다.
타입 안정성:
런타임에 예상치 못한 타입이 발생하는 것을 방지합니다.
interface 예시
getCharacter에서 컴파일 에러발생
'when' expression must be exhaustive, add necessary 'else' branch
interface Error
class FileError(val message: String): Error
class DBError(val message: String): Error
enum class DBTYPE {
MYSQL, MARIADB, ORACLE, MSSQL
}
fun getCharacter(error: Error) = when (error) {
is FileError -> "FileError"
is DBError -> "DBError"
}
그래서 else를 추가해줘야합니다
fun getCharacter(error: Error) = when (error) {
is FileError -> "FileError"
is DBError -> "DBError"
else -> "Unknown"
}
sealed 적용
sealed interface Error
class FileError(val message: String): Error
class DBError(val message: String): Error
enum class DBTYPE {
MYSQL, MARIADB, ORACLE, MSSQL
}
fun getCharacter(error: Error) = when (error) {
is FileError -> "FileError"
is DBError -> "DBError"
}
클래스 예시
open class Shape
class Circle : Shape()
class Rectangle : Shape()
fun getArea(shape: Shape) = when (shape) {
is Circle -> // 원의 넓이 계산
is Rectangle -> // 직사각형의 넓이 계산
// else 분기가 없어도 컴파일러는 경고하지 않음
}
sealed class Shape
class Circle : Shape()
class Rectangle : Shape()
fun getArea(shape: Shape) = when (shape) {
is Circle -> // 원의 넓이 계산
is Rectangle -> // 직사각형의 넓이 계산
// 모든 경우를 처리했으므로 else 분기가 필요 없음
}
// Result.kt 파일
sealed class Result<out T> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
object Loading : Result<Nothing>()
}
// 네트워크 요청을 시뮬레이션하는 함수
fun fetchUserData(userId: String): Result<User> {
return try {
// 네트워크 요청 시뮬레이션
if (userId == "123") {
Result.Success(User("John Doe", "john@example.com"))
} else {
throw Exception("User not found")
}
} catch (e: Exception) {
Result.Error(e)
}
}
// User 데이터 클래스
data class User(val name: String, val email: String)
// Result를 처리하는 함수
fun handleResult(result: Result<User>) {
when (result) {
is Result.Success -> println("User fetched successfully: ${result.data}")
is Result.Error -> println("Error fetching user: ${result.exception.message}")
is Result.Loading -> println("Loading user data...")
}
}
// 사용 예시
fun main() {
println("Fetching user data...")
val result = fetchUserData("123")
handleResult(result)
println("\nFetching non-existent user...")
val errorResult = fetchUserData("456")
handleResult(errorResult)
}
object 키워드
클래스 선언과 객체 생성을 동시에 진행
활용 케이스
Comparator의 구현처럼 단 한번만 사용되는 경우에도 사용
싱글톤 패턴 적용할때도 사용
Mapper 클래스처럼 static method만 존재하고 별도의 인스턴스 생성하지 않는 경우에도 사용
메모리 할당:
object 내의 모든 멤버(메서드와 필드)는 클래스 로딩 시점에 한 번만 초기화되며, JVM의 메서드 영역(또는 클래스 영역)에 저장됩니다.
이 영역은 모든 스레드가 공유하는 영역입니다.
접근 방식:
object의 멤버들은 객체 인스턴스 없이 직접 접근할 수 있습니다.
Java로 변환될 때, 이들은 실제로 static 멤버로 컴파일됩니다.
공유 특성:
모든 스레드와 애플리케이션의 모든 부분에서 동일한 object 인스턴스와 그 멤버들을 공유합니다.
이는 전역 상태를 관리하거나 애플리케이션 전체에서 공통 기능을 제공할 때 유용합니다.
초기화:
object는 처음 접근될 때 초기화됩니다 (lazy initialization).
초기화는 스레드 안전하게 이루어집니다.
예시
싱글톤객체
object Counter {
var count = 0
fun increment() {
count++
}
}
fun main() {
println(Counter.count) // 0
Counter.increment()
println(Counter.count) // 1
// 다른 곳에서도 같은 Counter 인스턴스에 접근
anotherFunction()
}
fun anotherFunction() {
Counter.increment()
println(Counter.count) // 2
}
익명클래스
interface ClickListener {
fun onClick()
}
fun main() {
val button = object : ClickListener {
override fun onClick() {
println("Button clicked!")
}
}
button.onClick()
}
comparator
data class Person(val name: String, val age: Int)
object PersonComparator : Comparator<Person> {
override fun compare(p1: Person, p2: Person): Int {
return p1.age - p2.age
}
}
fun main() {
val people = listOf(
Person("Alice", 30),
Person("Bob", 25),
Person("Charlie", 35)
)
val sortedPeople = people.sortedWith(PersonComparator)
println(sortedPeople)
}
mapper
package com.example
data class AddressDto(val street: String, val city: String, val country: String)
data class AddressEntity(val id: Long, val street: String, val city: String, val country: String)
data class CustomerDto(val id: String, val name: String, val address: AddressDto)
data class CustomerEntity(val id: Long, val name: String, val addressId: Long)
object CustomerMapper {
fun toEntity(dto: CustomerDto, addressId: Long): CustomerEntity {
return CustomerEntity(
id = dto.id.toLong(),
name = dto.name,
addressId = addressId
)
}
fun toDto(entity: CustomerEntity, address: AddressEntity): CustomerDto {
return CustomerDto(
id = entity.id.toString(),
name = entity.name,
address = AddressDto(
street = address.street,
city = address.city,
country = address.country
)
)
}
}
fun main() {
val addressDto = AddressDto("123 Main St", "New York", "USA")
val customerDto = CustomerDto("1", "Alice", addressDto)
val customerEntity = CustomerMapper.toEntity(customerDto, 100L)
println(customerEntity)
}
companion object: 자바의 static method를 대체하는 용도
자바에서 static으로 상수 값을 저장하거나 factory 생성자를 만들던 방식을 동일하게 활용 가능
팩토리 메서드
class User private constructor(val name: String, val email: String) {
companion object {
fun of(name: String, email: String): User {
// 이메일 유효성 검사 등의 로직을 수행할 수 있습니다.
require(email.contains("@")) { "Invalid email format" }
return User(name, email)
}
}
}
fun main() {
val user = User.of("John Doe", "john@example.com")
println("user.name, user.email: ${user.name}, ${user.email}")
}
상수
class Config {
companion object {
const val API_BASE_URL = "https://api.example.com"
const val TIMEOUT_SECONDS = 30
const val MAX_RETRY_ATTEMPTS = 3
}
}
fun main() {
println(Config.API_BASE_URL)
println(Config.TIMEOUT_SECONDS)
}
'kotlin > 기초' 카테고리의 다른 글
kotlin 코루틴 정리 - 1 (0) | 2024.08.25 |
---|---|
kotlin 문법 기초 - 4 (0) | 2024.07.23 |
kotlin 문법 기초 - 2 (1) | 2024.07.14 |
kotlin 문법 기초-1 (0) | 2024.07.10 |