각 항목마다 이 프로젝트의 실제 코드를 예시로 사용합니다.
val vs varfun?, ?:, !!$map, italsorequire() — 전제 조건 검사mapOf() & to 중위 함수.. & in""" & trimIndent()1_000_000interface*::class.javaval vs var| 키워드 | 의미 | Java 대응 |
|---|---|---|
val | 한 번만 할당 가능 (불변) | final |
var | 재할당 가능 (가변) | 일반 변수 |
// PostService.kt
val offset = (page - 1) * size // 이후 바꿀 일 없음 → val
val posts = postMapper.findAll(offset, size)
// Post.kt (domain)
// MyBatis 가 리플렉션으로 값을 채워넣어야 하므로 var 사용
var id: Long = 0
var title: String = ""
// PostService.kt — updatePost()
// 조회 후 값을 바꿔야 하므로 var 이어야 가능
post.title = request.title // Post.title 이 var 이기 때문에 가능
post.content = request.content
원칙: 바꿀 필요가 없으면
val, 바꿔야 한다면var.
DTO(요청/응답 객체)는 한 번 만들고 끝이니val, 도메인 객체는 DB에서 값을 주입받아야 하니var.
Kotlin 컴파일러가 오른쪽 값을 보고 타입을 자동으로 알아냅니다.
val offset = (page - 1) * size // Int 로 추론
val start = System.currentTimeMillis() // Long 으로 추론
val posts = postMapper.findAll(offset, size) // List<Post> 로 추론
타입을 명시할 수도 있습니다 (선택 사항).
val offset: Int = (page - 1) * size // 명시적으로 써도 됨
val start: Long = System.currentTimeMillis()
fun// 기본 형태
fun 함수이름(파라미터: 타입): 반환타입 {
// 본문
}
// PostService.kt
fun getPosts(page: Int, size: Int): PostListResponse {
val offset = (page - 1) * size
val posts = postMapper.findAll(offset, size)
// ...
return PostListResponse(...)
}
반환값이 없으면 반환 타입을 생략하거나 Unit 을 씁니다.
fun deletePost(id: Long) { // 반환 타입 생략 = Unit
postMapper.delete(id)
}
함수 본문이 return 표현식 딱 한 줄이면, = 으로 줄여 쓸 수 있습니다.
// PostController.kt — 일반 형태
fun getPost(@PathVariable id: Long): ResponseEntity<PostResponse> {
return ResponseEntity.ok(postService.getPost(id))
}
// ↓ 표현식 함수로 줄이면
fun getPost(@PathVariable id: Long): ResponseEntity<PostResponse> =
ResponseEntity.ok(postService.getPost(id))
이 프로젝트의 Controller 메서드 대부분이 이 형태입니다.
Java 의 메서드 오버로딩 없이, 파라미터에 기본값을 지정할 수 있습니다.
// PostController.kt
fun getPosts(
@RequestParam(defaultValue = "1") page: Int = 1,
@RequestParam(defaultValue = "20") size: Int = 20,
): ResponseEntity<PostListResponse>
// Post.kt (domain) — 모든 파라미터에 기본값 → "인수 없는 생성자" 효과
data class Post(
var id: Long = 0,
var title: String = "",
var content: String = "",
// ...
)
함수를 호출할 때 파라미터 이름을 명시해서 가독성을 높입니다.
// PostService.kt — createPost()
val post = Post(
title = request.title, // 이름을 붙여서 호출
content = request.content,
author = request.author,
)
이름을 붙이면 순서를 바꿔도 되고, 어떤 값이 어떤 파라미터인지 한눈에 보입니다.
Kotlin 클래스는 선언과 동시에 생성자를 정의합니다.
// 클래스 이름 뒤 괄호가 "주 생성자(Primary Constructor)"
class PostController(private val postService: PostService) {
// postService 는 클래스 전체에서 사용 가능한 필드가 됨
}
private val postService 처럼 생성자 파라미터 앞에 접근 제어자를 붙이면,
생성자 파라미터이자 클래스 필드 가 됩니다.
Java 로 표현하면:
// Java
public class PostController {
private final PostService postService; // 필드 선언
public PostController(PostService postService) { // 생성자
this.postService = postService;
}
}
Spring 은 이 생성자를 보고 PostService 빈을 자동 주입(DI)합니다.
data 키워드를 붙이면 컴파일러가 아래 메서드를 자동 생성합니다.
| 자동 생성 메서드 | 역할 |
|---|---|
toString() | Post(id=1, title=안녕, ...) 형태의 문자열 반환 |
equals() | 모든 필드값이 같으면 true |
hashCode() | equals() 와 일관된 해시값 |
copy() | 일부 필드만 바꾼 새 객체 생성 |
componentN() | 구조 분해 선언 지원 |
// Post.kt
data class Post(
var id: Long = 0,
var title: String = "",
var content: String = "",
var author: String = "",
var viewCount: Int = 0,
var createdAt: LocalDateTime = LocalDateTime.now(),
var updatedAt: LocalDateTime = LocalDateTime.now(),
)
copy() 활용 예시val original = Post(title = "제목", content = "내용", author = "홍길동")
// title 만 바꾼 새 Post 객체 생성 (original 은 그대로)
val updated = original.copy(title = "새 제목")
data class 는 언제 쓰나?
데이터를 담는 것이 주 목적인 클래스 (DTO, 도메인 객체 등).
비즈니스 로직이 많은 클래스는 일반class가 낫습니다.
Java 의 static 에 해당하는 개념입니다.
Kotlin 에는 static 키워드가 없고, 대신 클래스 안에 companion object 블록을 만듭니다.
// PostDto.kt
data class PostResponse(
val id: Long,
val title: String,
// ...
) {
companion object { // ← 여기
fun from(post: Post) = PostResponse( // 정적 팩토리 메서드
id = post.id,
title = post.title,
// ...
)
}
}
// Java 의 PostResponse.from(post) 와 동일하게 호출
val response = PostResponse.from(post)
// Java
public class PostResponse {
// ...
public static PostResponse from(Post post) { // static 메서드
return new PostResponse(post.getId(), post.getTitle(), ...);
}
}
왜 팩토리 메서드 패턴을 쓰나?
도메인 객체(Post)를 응답 DTO(PostResponse)로 변환하는 로직을 한 곳에 모아두면,
변환 방식이 바뀔 때from()만 수정하면 됩니다.
?, ?:, !!Kotlin 은 null 이 될 수 있는 타입과 없는 타입을 컴파일 시점에 구분합니다.
| 표기 | 의미 |
|---|---|
String | 절대 null 이 될 수 없음 |
String? | null 이 될 수도 있음 (nullable) |
? — nullable 타입// PostMapper.kt
fun findById(@Param("id") id: Long): Post?
// ↑ DB에 없으면 null 을 반환할 수 있음
?: — 엘비스 연산자 (Elvis Operator)"null 이면 오른쪽을 실행해라" 라는 뜻입니다.
// PostService.kt
val post = postMapper.findById(id)
?: throw NoSuchElementException("게시글을 찾을 수 없습니다. id=$id")
// ↑ findById 가 null 을 반환하면 예외를 던짐
// DataSeederController.kt
val totalCount = jdbc.queryForObject("SELECT COUNT(*) FROM posts", Long::class.java) ?: 0
// ↑ null 이면 0 으로 대체
!! — non-null 단언 (강제 언박싱)"나는 이 값이 null 이 아님을 확신한다" 는 선언입니다.
null 이면 NullPointerException 이 발생하므로, 사용에 주의해야 합니다.
val name: String? = "홍길동"
val length = name!!.length // null 이 아님을 개발자가 보장
이 프로젝트에서는
!!대신?:로 안전하게 처리하고 있습니다.
$문자열 안에 변수나 표현식을 직접 삽입할 수 있습니다.
// PostService.kt
throw NoSuchElementException("게시글을 찾을 수 없습니다. id=$id")
// ↑ $변수명
중괄호로 감싸면 더 복잡한 표현식도 넣을 수 있습니다.
// DataSeederController.kt
"rate" to "${count * 1000 / elapsed.coerceAtLeast(1)} 건/초"
// ↑ ${ 표현식 }
Java 와 비교하면:
// Java
throw new NoSuchElementException("게시글을 찾을 수 없습니다. id=" + id);
// Kotlin
throw NoSuchElementException("게시글을 찾을 수 없습니다. id=$id")
map, it중괄호 { } 로 감싼 코드 블록입니다. 함수의 인수로 전달할 수 있습니다.
// PostService.kt
posts.map { PostResponse.from(it) }
// ↑ map 에 람다를 전달
it — 암묵적 파라미터람다 파라미터가 하나뿐일 때, 이름을 직접 지어주는 대신 it 으로 참조합니다.
posts.map { it -> PostResponse.from(it) } // it 명시적으로 쓴 것
posts.map { PostResponse.from(it) } // it 생략한 것 (같은 의미)
map컬렉션의 각 요소를 변환해서 새 리스트를 만드는 함수입니다.
// [Post, Post, Post] → [PostResponse, PostResponse, PostResponse]
val responses: List<PostResponse> = posts.map { PostResponse.from(it) }
Java 의 Stream 으로 표현하면:
// Java
List<PostResponse> responses = posts.stream()
.map(PostResponse::from)
.collect(Collectors.toList());
::)람다 대신 메서드 참조를 쓸 수도 있습니다.
posts.map { PostResponse.from(it) } // 람다
posts.map(PostResponse::from) // 함수 참조 (동일한 의미)
also스코프 함수는 객체에 대해 코드 블록을 실행하는 함수들입니다.
이 프로젝트에서는 also 를 사용합니다.
also"이것도 해라" 라는 의미입니다.
객체 자신을 it 으로 받아 부수 작업을 한 뒤, 원래 객체를 그대로 반환합니다.
// PostService.kt
return PostResponse.from(post.also { it.viewCount++ })
// ↑ post.viewCount 를 증가시키고, post 자체를 반환
// ↑ 증가된 post 를 from() 에 전달
흐름을 풀어 쓰면:
// also 없이 쓴다면
post.viewCount++
return PostResponse.from(post)
// also 를 쓰면 한 줄로
return PostResponse.from(post.also { it.viewCount++ })
| 함수 | 참조 방식 | 반환값 | 주 용도 |
|---|---|---|---|
let | it | 람다 결과 | null 체크 후 변환 |
run | this | 람다 결과 | 초기화 + 계산 |
apply | this | 객체 자신 | 객체 설정(빌더 패턴) |
also | it | 객체 자신 | 부수 작업 (로깅, 증가 등) |
with | this | 람다 결과 | 특정 객체에 여러 작업 |
require() — 전제 조건 검사파라미터의 유효성을 검사할 때 씁니다.
조건이 false 이면 IllegalArgumentException 을 자동으로 던집니다.
// DataSeederController.kt
require(count in 1..10_000_000) { "count 는 1 ~ 10,000,000 사이여야 합니다." }
// ↑ 조건 ↑ 실패 시 메시지를 반환하는 람다
Java 로 표현하면:
// Java
if (!(count >= 1 && count <= 10_000_000)) {
throw new IllegalArgumentException("count 는 1 ~ 10,000,000 사이여야 합니다.");
}
비슷한 함수:
check()는 상태 검사 (IllegalStateException),error()는 무조건 예외.
mapOf() & to 중위 함수mapOf()불변 Map 을 만드는 표준 함수입니다.
// DataSeederController.kt
return mapOf(
"inserted" to count,
"totalPosts" to totalCount,
"elapsedMs" to elapsed,
)
to — 중위 함수 (Infix Function)A to B 는 Pair(A, B) 와 같습니다.
mapOf() 는 Pair 들을 받아 Map 을 만듭니다.
"inserted" to count // Pair<String, Int>
// = Pair("inserted", count) // 동일한 의미
Java 로 표현하면:
// Java
Map<String, Object> result = new HashMap<>();
result.put("inserted", count);
result.put("totalPosts", totalCount);
.. & in.. — 범위 생성1..10 // 1 이상 10 이하 (IntRange)
1..10_000_000 // 1 이상 천만 이하
in — 범위 포함 여부 검사// DataSeederController.kt
require(count in 1..10_000_000) { "..." }
// ↑ count 가 1~10,000,000 범위 안에 있는지 확인
Java 로 표현하면:
// Java
count >= 1 && count <= 10_000_000
컬렉션에도 사용할 수 있습니다.
val list = listOf("a", "b", "c")
"b" in list // true
""" & trimIndent()""" — 삼중 따옴표 문자열줄바꿈과 들여쓰기를 그대로 포함한 문자열입니다.
// DataSeederController.kt
jdbc.execute("""
INSERT INTO posts (title, content, author, ...)
SELECT
CASE (i % 10)
WHEN 0 THEN '공지사항: ...' || i
...
END
FROM generate_series(1, $count) AS t(i)
""".trimIndent())
trimIndent()삼중 따옴표 문자열에서 공통 들여쓰기를 제거합니다.
붙이지 않으면 SQL 앞에 공백이 그대로 포함됩니다.
val sql = """
SELECT *
FROM posts
""".trimIndent()
// trimIndent() 적용 결과:
// "SELECT *\nFROM posts\n"
// (앞의 공백 4칸이 사라짐)
1_000_000긴 숫자를 읽기 쉽게 구분하는 문법입니다. 값 자체에는 영향이 없습니다.
// DataSeederController.kt
require(count in 1..10_000_000) { "..." }
// ↑ 10,000,000 과 완전히 같은 값
val million = 1_000_000 // 1000000 과 동일
val billion = 1_000_000_000
interfaceJava 와 거의 같습니다. 구현 없이 메서드 시그니처만 선언합니다.
// PostMapper.kt
@Mapper
interface PostMapper {
fun findAll(
@Param("offset") offset: Int,
@Param("limit") limit: Int,
): List<Post>
fun findById(@Param("id") id: Long): Post? // null 반환 가능
fun insert(post: Post) // 반환값 없음 (Unit)
}
MyBatis 의 @Mapper 어노테이션을 붙이면,
Spring 이 시작할 때 이 인터페이스의 구현체를 자동으로 만들어줍니다.
*패키지 안의 모든 것을 한 번에 가져옵니다.
// PostController.kt
import me.study.index.dto.* // dto 패키지의 모든 클래스를 가져옴
// 덕분에 아래를 따로 import 하지 않아도 됨:
// import me.study.index.dto.PostCreateRequest
// import me.study.index.dto.PostUpdateRequest
// import me.study.index.dto.PostResponse
// import me.study.index.dto.PostListResponse
::class.javaKotlin 의 타입 시스템과 Java 의 타입 시스템을 연결할 때 씁니다.
// DataSeederController.kt
jdbc.queryForObject("SELECT COUNT(*) FROM posts", Long::class.java)
// ↑ Java 의 Long.class 에 해당
// javaClass 프로퍼티: 현재 인스턴스의 Java 클래스를 반환
private val log = LoggerFactory.getLogger(javaClass)
// ↑ this.getClass() 와 동일
| Kotlin 표현 | Java 표현 |
|---|---|
String::class | — (KClass) |
String::class.java | String.class |
javaClass | this.getClass() |
| Kotlin | Java | |
|---|---|---|
| 반환값 없는 함수 | Unit (생략 가능) | void |
| 제네릭에서 "없음" | Unit | Void |
// 함수 반환값이 없으면 Unit (보통 생략)
fun deletePost(id: Long): Unit { ... }
fun deletePost(id: Long) { ... } // 위와 동일
// ResponseEntity 에서 반환 바디가 없을 때 Java 의 Void 를 써야 하는 경우
// PostController.kt
fun deletePost(@PathVariable id: Long): ResponseEntity<Void> {
postService.deletePost(id)
return ResponseEntity.noContent().build()
}
Spring 의
ResponseEntity는 Java 클래스라Void를 사용합니다.
순수 Kotlin 코드에서 "반환값 없음"은Unit입니다.
마지막 파라미터/요소 뒤에 쉼표를 붙여도 됩니다.
// PostDto.kt
data class PostCreateRequest(
val title: String,
val content: String,
val author: String, // ← 마지막에도 쉼표 OK
)
Java 에서는 문법 오류지만 Kotlin 에서는 허용됩니다.
나중에 줄을 추가하거나 순서를 바꿀 때 git diff 가 깔끔해지는 장점이 있습니다.
HTTP 요청
↓
[Controller] — 표현식 함수(=), @PathVariable, @RequestParam, 기본 파라미터값
↓
[Service] — val/var, 엘비스 연산자(?:), also, map, require
↓
[Mapper] — interface, nullable 반환(Post?), @Param
↓
[Domain/DTO] — data class, companion object, val/var, 기본값
↓
DB (PostgreSQL + MyBatis)