백고등어 개발 블로그
코틀린 강의 10강: 실전 프로젝트와 모범 사례 본문
728x90
반응형
10강: 실전 프로젝트와 모범 사례
코틀린 코딩 규칙
명명 규칙
// 클래스와 인터페이스: PascalCase
class UserRepository
interface DataSource
// 함수와 변수: camelCase
fun calculateTotal()
val userName = "김코틀린"
// 상수: UPPER_SNAKE_CASE
const val MAX_COUNT = 100
const val API_KEY = "your-api-key"
// 패키지: 소문자, 점으로 구분
package com.example.myapp.domain
코드 구조
// 1. 패키지 선언
package com.example.myapp
// 2. Import 문
import kotlinx.coroutines.*
import java.time.LocalDate
// 3. 상수
private const val DEFAULT_TIMEOUT = 5000L
// 4. 클래스 선언
class MyClass {
// 4.1. 동반 객체
companion object {
const val TAG = "MyClass"
}
// 4.2. 프로퍼티
private var count = 0
// 4.3. 초기화 블록
init {
println("초기화")
}
// 4.4. 메서드
fun doSomething() {
// 구현
}
}
실전 프로젝트: 도서 관리 시스템
도메인 모델
// Book.kt
data class Book(
val id: Int,
val title: String,
val author: String,
val isbn: String,
val publishedYear: Int,
var isAvailable: Boolean = true
) {
fun borrow(): Result<Unit> {
return if (isAvailable) {
isAvailable = false
Result.success(Unit)
} else {
Result.failure(BookNotAvailableException("책이 이미 대출 중입니다"))
}
}
fun returnBook() {
isAvailable = true
}
}
// User.kt
data class User(
val id: Int,
val name: String,
val email: String
) {
private val borrowedBooks = mutableListOf<Book>()
fun getBorrowedBooks(): List<Book> = borrowedBooks.toList()
fun canBorrowMore(): Boolean = borrowedBooks.size < MAX_BORROWED_BOOKS
fun borrowBook(book: Book): Result<Unit> {
return when {
!canBorrowMore() -> Result.failure(
LimitExceededException("대출 한도를 초과했습니다")
)
book in borrowedBooks -> Result.failure(
DuplicateBorrowException("이미 대출한 책입니다")
)
else -> {
borrowedBooks.add(book)
Result.success(Unit)
}
}
}
fun returnBook(book: Book): Result<Unit> {
return if (borrowedBooks.remove(book)) {
Result.success(Unit)
} else {
Result.failure(BookNotFoundException("대출 기록이 없습니다"))
}
}
companion object {
private const val MAX_BORROWED_BOOKS = 5
}
}
// Exceptions.kt
class BookNotAvailableException(message: String) : Exception(message)
class LimitExceededException(message: String) : Exception(message)
class DuplicateBorrowException(message: String) : Exception(message)
class BookNotFoundException(message: String) : Exception(message)
저장소 패턴
// Repository.kt
interface Repository<T> {
fun findAll(): List<T>
fun findById(id: Int): T?
fun save(item: T): T
fun delete(id: Int): Boolean
}
// BookRepository.kt
class BookRepository : Repository<Book> {
private val books = mutableMapOf<Int, Book>()
private var nextId = 1
override fun findAll(): List<Book> = books.values.toList()
override fun findById(id: Int): Book? = books[id]
override fun save(item: Book): Book {
val book = if (item.id == 0) {
item.copy(id = nextId++)
} else {
item
}
books[book.id] = book
return book
}
override fun delete(id: Int): Boolean {
return books.remove(id) != null
}
fun findByTitle(title: String): List<Book> {
return books.values.filter {
it.title.contains(title, ignoreCase = true)
}
}
fun findByAuthor(author: String): List<Book> {
return books.values.filter {
it.author.contains(author, ignoreCase = true)
}
}
fun findAvailableBooks(): List<Book> {
return books.values.filter { it.isAvailable }
}
}
// UserRepository.kt
class UserRepository : Repository<User> {
private val users = mutableMapOf<Int, User>()
private var nextId = 1
override fun findAll(): List<User> = users.values.toList()
override fun findById(id: Int): User? = users[id]
override fun save(item: User): User {
val user = if (item.id == 0) {
item.copy(id = nextId++)
} else {
item
}
users[user.id] = user
return user
}
override fun delete(id: Int): Boolean {
return users.remove(id) != null
}
fun findByEmail(email: String): User? {
return users.values.find { it.email == email }
}
}
서비스 레이어
// LibraryService.kt
class LibraryService(
private val bookRepository: BookRepository,
private val userRepository: UserRepository
) {
fun borrowBook(userId: Int, bookId: Int): Result<String> {
val user = userRepository.findById(userId)
?: return Result.failure(IllegalArgumentException("사용자를 찾을 수 없습니다"))
val book = bookRepository.findById(bookId)
?: return Result.failure(IllegalArgumentException("책을 찾을 수 없습니다"))
return book.borrow()
.mapCatching { user.borrowBook(book).getOrThrow() }
.map { "${user.name}님이 '${book.title}'을(를) 대출했습니다" }
}
fun returnBook(userId: Int, bookId: Int): Result<String> {
val user = userRepository.findById(userId)
?: return Result.failure(IllegalArgumentException("사용자를 찾을 수 없습니다"))
val book = bookRepository.findById(bookId)
?: return Result.failure(IllegalArgumentException("책을 찾을 수 없습니다"))
return user.returnBook(book)
.map {
book.returnBook()
"${user.name}님이 '${book.title}'을(를) 반납했습니다"
}
}
fun searchBooks(query: String): List<Book> {
val byTitle = bookRepository.findByTitle(query)
val byAuthor = bookRepository.findByAuthor(query)
return (byTitle + byAuthor).distinct()
}
fun getUserBorrowHistory(userId: Int): Result<List<Book>> {
val user = userRepository.findById(userId)
?: return Result.failure(IllegalArgumentException("사용자를 찾을 수 없습니다"))
return Result.success(user.getBorrowedBooks())
}
fun getLibraryStatistics(): LibraryStatistics {
val allBooks = bookRepository.findAll()
val allUsers = userRepository.findAll()
return LibraryStatistics(
totalBooks = allBooks.size,
availableBooks = allBooks.count { it.isAvailable },
borrowedBooks = allBooks.count { !it.isAvailable },
totalUsers = allUsers.size,
activeUsers = allUsers.count { it.getBorrowedBooks().isNotEmpty() }
)
}
}
data class LibraryStatistics(
val totalBooks: Int,
val availableBooks: Int,
val borrowedBooks: Int,
val totalUsers: Int,
val activeUsers: Int
) {
override fun toString(): String {
return """
|=== 도서관 통계 ===
|총 도서 수: $totalBooks권
|대출 가능: $availableBooks권
|대출 중: $borrowedBooks권
|총 회원 수: $totalUsers명
|활성 회원: $activeUsers명
""".trimMargin()
}
}
메인 애플리케이션
// Main.kt
fun main() {
val bookRepo = BookRepository()
val userRepo = UserRepository()
val libraryService = LibraryService(bookRepo, userRepo)
// 초기 데이터 설정
setupInitialData(bookRepo, userRepo)
// 테스트 시나리오
runLibraryScenarios(libraryService)
}
fun setupInitialData(bookRepo: BookRepository, userRepo: UserRepository) {
// 책 추가
val books = listOf(
Book(0, "코틀린 인 액션", "드미트리 제메로프", "9788966261758", 2017),
Book(0, "이펙티브 자바", "조슈아 블로크", "9788966262281", 2018),
Book(0, "클린 코드", "로버트 C. 마틴", "9788966260959", 2013),
Book(0, "리팩토링", "마틴 파울러", "9791162242742", 2020),
Book(0, "데이터 중심 애플리케이션 설계", "마틴 클레프만", "9791158391409", 2018)
)
books.forEach { bookRepo.save(it) }
// 사용자 추가
val users = listOf(
User(0, "김코틀린", "kotlin@example.com"),
User(0, "이자바", "java@example.com"),
User(0, "박스프링", "spring@example.com")
)
users.forEach { userRepo.save(it) }
}
fun runLibraryScenarios(service: LibraryService) {
println("=== 도서 관리 시스템 시작 ===\n")
// 시나리오 1: 책 대출
println("--- 시나리오 1: 책 대출 ---")
service.borrowBook(1, 1).onSuccess { println(it) }.onFailure { println("오류: ${it.message}") }
service.borrowBook(1, 2).onSuccess { println(it) }.onFailure { println("오류: ${it.message}") }
service.borrowBook(2, 1).onSuccess { println(it) }.onFailure { println("오류: ${it.message}") }
println()
// 시나리오 2: 대출 이력 조회
println("--- 시나리오 2: 대출 이력 조회 ---")
service.getUserBorrowHistory(1).onSuccess { books ->
println("사용자 1의 대출 도서:")
books.forEach { println(" - ${it.title}") }
}
println()
// 시나리오 3: 책 검색
println("--- 시나리오 3: 책 검색 ---")
val searchResults = service.searchBooks("코틀린")
println("'코틀린' 검색 결과:")
searchResults.forEach { println(" - ${it.title} (${it.author})") }
println()
// 시나리오 4: 책 반납
println("--- 시나리오 4: 책 반납 ---")
service.returnBook(1, 1).onSuccess { println(it) }.onFailure { println("오류: ${it.message}") }
println()
// 시나리오 5: 통계 조회
println("--- 시나리오 5: 도서관 통계 ---")
val stats = service.getLibraryStatistics()
println(stats)
}
모범 사례
1. 불변성 선호
// 나쁜 예
var user = User("김코틀린", 25)
user.age = 26
// 좋은 예
val user = User("김코틀린", 25)
val updatedUser = user.copy(age = 26)
2. null 안전성 활용
// 나쁜 예
fun getLength(str: String?): Int {
if (str != null) {
return str.length
}
return 0
}
// 좋은 예
fun getLength(str: String?): Int = str?.length ?: 0
3. 확장 함수 활용
// List 확장 함수
fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null
// String 확장 함수
fun String.isValidEmail(): Boolean = contains("@") && contains(".")
fun main() {
val numbers = listOf(1, 2, 3)
println(numbers.secondOrNull()) // 2
val email = "test@example.com"
println(email.isValidEmail()) // true
}
4. 스코프 함수 활용
// let: null 체크와 변환
val length = nullableString?.let { it.length }
// apply: 객체 초기화
val person = Person().apply {
name = "김코틀린"
age = 25
}
// also: 추가 작업
val numbers = mutableListOf(1, 2, 3).also {
println("리스트 생성: $it")
}
// run: 블록 실행 후 결과 반환
val result = service.run {
val data = fetchData()
processData(data)
}
// with: 수신 객체로 여러 작업
with(person) {
println(name)
println(age)
}
5. 위임 패턴
// 프로퍼티 위임
class Settings {
var theme: String by lazy {
loadThemeFromFile()
}
private fun loadThemeFromFile(): String {
println("테마 로딩...")
return "Dark"
}
}
// 인터페이스 위임
interface Logger {
fun log(message: String)
}
class ConsoleLogger : Logger {
override fun log(message: String) {
println("LOG: $message")
}
}
class Service(logger: Logger) : Logger by logger {
fun doWork() {
log("작업 시작")
// 실제 작업
log("작업 완료")
}
}
성능 최적화 팁
- 시퀀스 사용: 대량의 데이터 처리 시 지연 평가
- 인라인 함수: 람다를 사용하는 고차 함수에 inline 사용
- 적절한 컬렉션 선택: List vs Set vs Map
- 코루틴 활용: 비동기 작업에 스레드 대신 코루틴 사용
마치며
이번 강의에서는 실전 프로젝트를 통해 코틀린의 여러 기능을 종합적으로 활용하는 방법을 알아보았습니다. 좋은 코드를 작성하기 위해서는:
- 명확한 구조: 레이어를 분리하고 각 레이어의 책임을 명확히 합니다
- 타입 안전성: 코틀린의 타입 시스템을 적극 활용합니다
- 함수형 접근: 불변성을 선호하고 부수 효과를 최소화합니다
- 간결성: 코틀린의 간결한 문법을 활용하되, 가독성을 해치지 않도록 합니다
코틀린은 현대적이고 실용적인 언어로, 자바의 장점을 유지하면서 생산성과 안전성을 크게 향상시킵니다. 지속적인 학습과 실전 경험을 통해 더 나은 코틀린 개발자가 되시기 바랍니다!
추가 학습 자료
- 공식 문서: https://kotlinlang.org/docs/home.html
- Kotlin Koans: 대화형 연습 문제
- Android 개발: Jetpack Compose와 함께하는 현대적 UI 개발
- 서버 개발: Ktor, Spring Boot with Kotlin
- 멀티플랫폼: Kotlin Multiplatform Mobile (KMM)
728x90
반응형
'코틀린 강의' 카테고리의 다른 글
| 코틀린 강의 9강: 코루틴 기초 (0) | 2025.10.28 |
|---|---|
| 코틀린 강의 8강: 제네릭 (0) | 2025.10.28 |
| 코틀린 강의 7강: 예외 처리 (0) | 2025.10.28 |
| 코틀린 강의 6강: 컬렉션 (0) | 2025.10.28 |
| 코틀린 강의 5강: 클래스와 객체 (0) | 2025.10.28 |