Notice
Recent Posts
Recent Comments
Link
250x250
반응형
«   2025/10   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

백고등어 개발 블로그

자바 개발자가 3일만에 코틀린 마스터하는 비법 (구글이 안드로이드 공식언어로 선택한 이유) 본문

카테고리 없음

자바 개발자가 3일만에 코틀린 마스터하는 비법 (구글이 안드로이드 공식언어로 선택한 이유)

백고등어 2025. 9. 26. 17:48
728x90
반응형

자바로 10년 개발했는데 코틀린을 배워야 하나요? 답은 무조건 YES입니다.

 

구글이 안드로이드 공식 언어로 코틀린을 선택한 건 우연이 아니에요.

 

JetBrains가 만든 이 언어는 자바의 모든 문제점을 해결하면서도 100% 호환되는 마법 같은 언어입니다.

왜 지금 코틀린을 배워야 하나?

1. 취업 시장에서의 현실

  • 안드로이드 개발: 이제 코틀린 필수, 자바는 레거시
  • 서버 개발: Spring Boot + Kotlin 조합이 트렌드
  • 대기업 채용: 삼성, LG, 카카오, 네이버 모두 코틀린 우대
  • 연봉: 코틀린 개발자가 자바 개발자보다 평균 20% 높음

2. 개발 생산성의 혁명

// 자바에서는 이렇게 길게...
public class User {
    private String name;
    private int age;
    
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return age == user.age && Objects.equals(name, user.name);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}
// 코틀린에서는 이렇게 간단하게!
data class User(var name: String, var age: Int)

Day 1: 코틀린의 첫인상, "이게 진짜 자바와 호환돼?"

변수 선언의 혁신

// 자바 방식 (여전히 가능)
String name = "홍길동";
int age = 25;

// 코틀린 방식 - 타입 추론의 마법
val name = "홍길동"        // 불변 (final과 같음)
var age = 25             // 가변

// null 안전성 - NPE 이제 안녕!
var nullableName: String? = null  // null 가능
var nonNullName: String = "홍길동"  // null 불가능

함수 선언이 이렇게 간단하다고?

// 자바 방식
public int add(int a, int b) {
    return a + b;
}

// 코틀린 방식 1
fun add(a: Int, b: Int): Int {
    return a + b
}

// 코틀린 방식 2 - 한 줄로!
fun add(a: Int, b: Int) = a + b

// 기본 매개변수 - 오버로딩 지옥 탈출
fun greet(name: String, prefix: String = "안녕하세요") = "$prefix, $name님!"

greet("홍길동")                    // "안녕하세요, 홍길동님!"
greet("홍길동", "반갑습니다")        // "반갑습니다, 홍길동님!"

문자열 템플릿 - printf 안녕

// 자바에서는...
String message = String.format("안녕하세요, %s님! 나이는 %d세이군요.", name, age);
// 코틀린에서는
val message = "안녕하세요, $name님! 나이는 $age세이군요."
val complexMessage = "계산 결과: ${add(10, 20)}"

Day 2: 객체지향의 새로운 경험

클래스와 프로퍼티

class Person {
    var name: String = ""
        get() = field.uppercase()    // 커스텀 getter
        set(value) {                // 커스텀 setter
            field = value.trim()
        }
    
    var age: Int = 0
        set(value) {
            if (value >= 0) field = value
        }
}

// 사용법
val person = Person()
person.name = "  홍길동  "
println(person.name)  // "홍길동" (trim + uppercase)

생성자의 혁신

// 주 생성자
class User(val name: String, var age: Int) {
    // 초기화 블록
    init {
        println("새로운 사용자: $name")
    }
    
    // 보조 생성자
    constructor(name: String) : this(name, 0) {
        println("나이 정보 없는 사용자")
    }
}

상속과 오버라이드

// 기본적으로 모든 클래스는 final! 상속하려면 open 키워드
open class Animal(val name: String) {
    open fun makeSound() = "어떤 소리"
}

class Dog(name: String) : Animal(name) {
    override fun makeSound() = "멍멍!"
}

class Cat(name: String) : Animal(name) {
    override fun makeSound() = "야옹!"
}

인터페이스의 진화

interface Drawable {
    fun draw()
    
    // 인터페이스에서도 기본 구현 가능!
    fun getInfo() = "그릴 수 있는 객체"
}

interface Clickable {
    fun click()
    fun showTooltip() = "클릭해주세요"  // 기본 구현
}

class Button : Drawable, Clickable {
    override fun draw() = println("버튼을 그립니다")
    override fun click() = println("버튼이 클릭되었습니다")
}

Day 3: 함수형 프로그래밍과 고급 기능들

컬렉션의 마법

val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

// 자바 8 스트림과 비슷하지만 더 직관적
val evenSquares = numbers
    .filter { it % 2 == 0 }      // 짝수 필터링
    .map { it * it }             // 제곱
    .toList()

println(evenSquares)  // [4, 16, 36, 64, 100]

// 더 복잡한 예제
val users = listOf(
    User("홍길동", 25),
    User("김영희", 30),
    User("박철수", 22)
)

val adultNames = users
    .filter { it.age >= 25 }
    .map { it.name }
    .joinToString(", ")

println(adultNames)  // "홍길동, 김영희"

고차 함수와 람다

// 함수를 매개변수로 받기
fun processNumbers(numbers: List<Int>, operation: (Int) -> Int): List<Int> {
    return numbers.map(operation)
}

// 사용법
val doubled = processNumbers(listOf(1, 2, 3)) { it * 2 }
val squared = processNumbers(listOf(1, 2, 3)) { it * it }

// 함수 참조
fun double(x: Int) = x * 2
val doubled2 = processNumbers(listOf(1, 2, 3), ::double)

when문 - switch의 진화형

fun getGrade(score: Int) = when (score) {
    100 -> "A+"
    in 90..99 -> "A"
    in 80..89 -> "B"
    in 70..79 -> "C"
    in 60..69 -> "D"
    else -> "F"
}

// 타입으로도 분기 가능
fun processValue(value: Any) = when (value) {
    is String -> "문자열: $value"
    is Int -> "숫자: $value"
    is Boolean -> "불린: $value"
    else -> "알 수 없는 타입"
}

확장 함수 - 기존 클래스에 새로운 기능 추가

// String 클래스에 새로운 기능 추가
fun String.isEmailValid(): Boolean {
    return this.contains("@") && this.contains(".")
}

fun String.toCamelCase(): String {
    return this.split(" ")
        .joinToString("") { word ->
            word.lowercase().replaceFirstChar { it.uppercase() }
        }
}

// 사용법
println("test@example.com".isEmailValid())  // true
println("hello world".toCamelCase())         // "HelloWorld"

실전 예제: 간단한 TODO 앱

data class Todo(
    val id: Int,
    var title: String,
    var isCompleted: Boolean = false
)

class TodoManager {
    private val todos = mutableListOf<Todo>()
    private var nextId = 1
    
    fun addTodo(title: String): Todo {
        val todo = Todo(nextId++, title)
        todos.add(todo)
        return todo
    }
    
    fun completeTodo(id: Int) {
        todos.find { it.id == id }?.isCompleted = true
    }
    
    fun getCompletedTodos() = todos.filter { it.isCompleted }
    
    fun getPendingTodos() = todos.filter { !it.isCompleted }
    
    fun getAllTodos() = todos.toList()  // 불변 리스트 반환
}

// 사용 예제
fun main() {
    val todoManager = TodoManager()
    
    todoManager.addTodo("코틀린 공부하기")
    todoManager.addTodo("블로그 글 쓰기")
    todoManager.addTodo("운동하기")
    
    todoManager.completeTodo(1)
    
    println("=== 할일 목록 ===")
    todoManager.getPendingTodos().forEach { todo ->
        println("[ ] ${todo.title}")
    }
    
    println("\n=== 완료된 일 ===")
    todoManager.getCompletedTodos().forEach { todo ->
        println("[x] ${todo.title}")
    }
}

코틀린 vs 자바: 실제 코드 비교

1. Null 안전성

// 자바 - NPE 지옥
String name = getName();
if (name != null) {
    if (name.length() > 0) {
        System.out.println(name.toUpperCase());
    }
}
// 코틀린 - 안전한 호출과 엘비스 연산자
val name = getName()
name?.takeIf { it.isNotEmpty() }?.uppercase()?.let { println(it) }

// 또는 더 간단하게
println(getName()?.takeIf { it.isNotEmpty() }?.uppercase() ?: "이름 없음")

2. 데이터 클래스 비교

// 자바 - 보일러플레이트 코드 50줄
public class Person {
    private String name;
    private int age;
    
    // 생성자, getter, setter, equals, hashCode, toString 등등...
}
// 코틀린 - 1줄로 끝
data class Person(val name: String, val age: Int)

3. 리스트 처리

// 자바
List<String> names = Arrays.asList("홍길동", "김영희", "박철수");
List<String> upperNames = names.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toList());
// 코틀린
val names = listOf("홍길동", "김영희", "박철수")
val upperNames = names.map { it.uppercase() }

실무에서 자주 쓰는 코틀린 패턴들

1. 스코프 함수들

// let - null이 아닐 때만 실행
val result = nullableString?.let { str ->
    str.uppercase().trim()
}

// apply - 객체 초기화
val person = Person().apply {
    name = "홍길동"
    age = 25
}

// also - 추가 작업 수행
val numbers = mutableListOf<Int>().also { 
    it.addAll(listOf(1, 2, 3))
    println("리스트 크기: ${it.size}")
}

// with - 객체의 여러 메서드 호출
with(person) {
    println(name)
    println(age)
}

2. 지연 초기화

class DatabaseManager {
    // 처음 접근할 때만 초기화
    val database by lazy {
        println("데이터베이스 연결 중...")
        connectToDatabase()
    }
    
    // lateinit - 나중에 초기화할 것을 보장
    lateinit var config: Config
    
    fun initialize() {
        config = loadConfig()
    }
}

3. 델리게이트 패턴

interface Printer {
    fun print()
}

class FilePrinter : Printer {
    override fun print() = println("파일에 출력")
}

class Document(printer: Printer) : Printer by printer {
    fun createDocument() {
        println("문서 생성")
        print()  // FilePrinter의 print() 호출
    }
}

안드로이드 개발에서의 코틀린

class MainActivity : AppCompatActivity() {
    
    // 뷰 바인딩
    private lateinit var binding: ActivityMainBinding
    
    // 지연 초기화
    private val viewModel by viewModels<MainViewModel>()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        setupUI()
        observeViewModel()
    }
    
    private fun setupUI() {
        binding.apply {
            button.setOnClickListener { 
                viewModel.onButtonClick()
            }
            
            // 여러 뷰 설정을 깔끔하게
            editText.apply {
                hint = "입력하세요"
                inputType = InputType.TYPE_CLASS_TEXT
            }
        }
    }
    
    private fun observeViewModel() {
        viewModel.uiState.observe(this) { state ->
            when (state) {
                is UiState.Loading -> showLoading()
                is UiState.Success -> {
                    hideLoading()
                    updateUI(state.data)
                }
                is UiState.Error -> {
                    hideLoading()
                    showError(state.message)
                }
            }
        }
    }
}

서버 개발에서의 코틀린 (Spring Boot)

@RestController
@RequestMapping("/api/users")
class UserController(
    private val userService: UserService
) {
    
    @GetMapping
    fun getAllUsers(): List<UserDto> = userService.findAll()
    
    @GetMapping("/{id}")
    fun getUser(@PathVariable id: Long): UserDto? = 
        userService.findById(id) ?: throw UserNotFoundException("User not found: $id")
    
    @PostMapping
    fun createUser(@RequestBody @Valid request: CreateUserRequest): UserDto =
        userService.create(request)
    
    @PutMapping("/{id}")
    fun updateUser(
        @PathVariable id: Long,
        @RequestBody @Valid request: UpdateUserRequest
    ): UserDto = userService.update(id, request)
    
    @DeleteMapping("/{id}")
    fun deleteUser(@PathVariable id: Long) = userService.delete(id)
}

@Service
@Transactional
class UserService(
    private val userRepository: UserRepository
) {
    
    fun findAll(): List<UserDto> = 
        userRepository.findAll().map { it.toDto() }
    
    fun findById(id: Long): UserDto? = 
        userRepository.findById(id).orElse(null)?.toDto()
    
    fun create(request: CreateUserRequest): UserDto {
        val user = User(
            name = request.name,
            email = request.email,
            age = request.age
        )
        return userRepository.save(user).toDto()
    }
    
    fun update(id: Long, request: UpdateUserRequest): UserDto {
        val user = userRepository.findById(id)
            .orElseThrow { UserNotFoundException("User not found: $id") }
        
        user.apply {
            name = request.name ?: name
            email = request.email ?: email
            age = request.age ?: age
        }
        
        return userRepository.save(user).toDto()
    }
}

// 확장 함수로 변환 로직 분리
fun User.toDto() = UserDto(
    id = this.id,
    name = this.name,
    email = this.email,
    age = this.age
)

코틀린만의 강력한 기능들

1. 인라인 함수와 리플렉션

// 인라인 함수 - 런타임 오버헤드 없음
inline fun <T> measureTime(block: () -> T): Pair<T, Long> {
    val start = System.currentTimeMillis()
    val result = block()
    val time = System.currentTimeMillis() - start
    return result to time
}

// 사용 예
val (result, time) = measureTime {
    // 시간을 측정하고 싶은 코드
    heavyComputation()
}
println("실행 시간: ${time}ms")

// 리플렉션 - 타입 안전한 방식
inline fun <reified T> isInstance(obj: Any): Boolean {
    return obj is T
}

println(isInstance<String>("Hello"))  // true
println(isInstance<Int>("Hello"))     // false

2. 코루틴 - 비동기 프로그래밍의 혁명

class ApiService {
    
    // 비동기 함수
    suspend fun fetchUser(id: Long): User {
        delay(1000)  // 네트워크 지연 시뮬레이션
        return userRepository.findById(id)
    }
    
    suspend fun fetchUserPosts(userId: Long): List<Post> {
        delay(500)
        return postRepository.findByUserId(userId)
    }
    
    // 병렬 처리
    suspend fun getUserData(userId: Long): UserData {
        val userDeferred = async { fetchUser(userId) }
        val postsDeferred = async { fetchUserPosts(userId) }
        
        val user = userDeferred.await()
        val posts = postsDeferred.await()
        
        return UserData(user, posts)
    }
}

// 사용법
class UserController {
    
    @GetMapping("/users/{id}/data")
    suspend fun getUserData(@PathVariable id: Long): UserData {
        return apiService.getUserData(id)  // 자동으로 비동기 처리
    }
}

3. DSL(Domain Specific Language) 만들기

// HTML DSL 예제
class HtmlBuilder {
    private val elements = mutableListOf<String>()
    
    fun html(block: HtmlBuilder.() -> Unit): String {
        this.block()
        return "<html>${elements.joinToString("")}</html>"
    }
    
    fun head(block: HtmlBuilder.() -> Unit) {
        val builder = HtmlBuilder()
        builder.block()
        elements.add("<head>${builder.elements.joinToString("")}</head>")
    }
    
    fun body(block: HtmlBuilder.() -> Unit) {
        val builder = HtmlBuilder()
        builder.block()
        elements.add("<body>${builder.elements.joinToString("")}</body>")
    }
    
    fun title(text: String) {
        elements.add("<title>$text</title>")
    }
    
    fun h1(text: String) {
        elements.add("<h1>$text</h1>")
    }
    
    fun p(text: String) {
        elements.add("<p>$text</p>")
    }
}

// 사용법 - HTML을 코틀린 코드로!
val htmlContent = HtmlBuilder().html {
    head {
        title("코틀린 DSL 예제")
    }
    body {
        h1("안녕하세요!")
        p("이것은 코틀린 DSL로 만든 HTML입니다.")
    }
}

println(htmlContent)

실무에서 알아두면 좋은 팁들

1. 성능 최적화

// 컬렉션 생성 시 크기 지정
val largeList = ArrayList<Int>(10000)  // 미리 크기 지정

// 시퀀스 사용으로 지연 계산
val result = (1..1000000)
    .asSequence()
    .filter { it % 2 == 0 }
    .map { it * 2 }
    .take(10)
    .toList()

// 인라인 클래스로 타입 안전성 + 성능
@JvmInline
value class UserId(val value: Long)

@JvmInline
value class OrderId(val value: Long)

// 이제 실수로 섞어 쓸 일이 없음
fun processOrder(userId: UserId, orderId: OrderId) { }

2. 테스트 코드 작성

class UserServiceTest {
    
    private val userRepository = mockk<UserRepository>()
    private val userService = UserService(userRepository)
    
    @Test
    fun `사용자 생성 테스트`() {
        // Given
        val request = CreateUserRequest("홍길동", "hong@example.com", 25)
        val savedUser = User(1L, "홍길동", "hong@example.com", 25)
        
        every { userRepository.save(any()) } returns savedUser
        
        // When
        val result = userService.create(request)
        
        // Then
        result shouldBe UserDto(1L, "홍길동", "hong@example.com", 25)
        verify { userRepository.save(any()) }
    }
    
    @Test
    fun `존재하지 않는 사용자 조회 시 예외 발생`() {
        // Given
        every { userRepository.findById(999L) } returns Optional.empty()
        
        // When & Then
        shouldThrow<UserNotFoundException> {
            userService.findById(999L)
        }
    }
}

3. 멀티플랫폼 개발

// 공통 코드 (commonMain)
expect class Platform {
    val name: String
}

expect fun getCurrentTime(): Long

class CommonGreeting {
    fun greeting(): String = "Hello, ${Platform().name}!"
}

// Android 구현 (androidMain)
actual class Platform {
    actual val name: String = "Android ${Build.VERSION.SDK_INT}"
}

actual fun getCurrentTime(): Long = System.currentTimeMillis()

// iOS 구현 (iosMain)
actual class Platform {
    actual val name: String = UIDevice.currentDevice.systemName()
}

actual fun getCurrentTime(): Long = NSDate().timeIntervalSince1970.toLong() * 1000

자바 개발자를 위한 마이그레이션 전략

1단계: 점진적 도입

// 기존 자바 코드와 100% 호환
// 새로운 클래스만 코틀린으로 작성 시작

2단계: 유틸리티 함수 변환

// 자주 사용하는 유틸리티를 코틀린 확장함수로
fun String.isValidEmail(): Boolean {
    return android.util.Patterns.EMAIL_ADDRESS.matcher(this).matches()
}

fun List<String>.joinWithComma(): String {
    return this.joinToString(", ")
}

3단계: 데이터 클래스 교체

// 기존 POJO를 data class로 교체
// 보일러플레이트 코드 대량 제거

4단계: 함수형 프로그래밍 적용

// 기존 for 루프를 map, filter 등으로 변환
// 코드 가독성과 안정성 향상

결론: 코틀린은 선택이 아닌 필수

코틀린은 단순히 자바의 대안이 아닙니다. 자바의 진화된 형태입니다.

3일 만에 마스터할 수 있는 이유:

  1. 자바 지식 재활용: 기존 자바 지식의 80%가 그대로 적용
  2. 점진적 학습: 자바 스타일로 시작해서 점점 코틀린답게
  3. 실무 적용: 배우는 즉시 프로젝트에 적용 가능

지금 바로 시작하세요:

  • IntelliJ IDEA에서 Kotlin 플러그인 활성화
  • 기존 자바 프로젝트에 .kt 파일 하나 추가
  • 간단한 유틸리티 클래스부터 시작

코틀린의 세계로 오신 것을 환영합니다!

728x90
반응형