지난 편에서는 val/var로 변수를 선언하는 방법과 문자열 템플릿을 살펴봤습니다.
https://dmoritle.tistory.com/281
[Kotlin] 코틀린 온보딩 2편 - 변수: val/var와 문자열 템플릿으로 이해하기
지난 편에서는 코틀린의 기본 단위인 함수를, 문과 식의 구분을 통해 살펴봤습니다. https://dmoritle.tistory.com/280 [Kotlin] 코틀린 온보딩 1편 - 함수: 문과 식부터 이해하기이번 글의 핵심은 코틀린의
dmoritle.tistory.com
이번 편의 핵심은 그 변수들을 담는 그릇인 클래스와, 클래스 안으로 들어간 변수가 프로퍼티로 승격되는 방식입니다. 프로퍼티를 원하는 대로 다시 정의하는 커스텀 접근자, 그리고 코틀린 파일이 자바와 달리 디렉터리 구조에 얽매이지 않는다는 점까지 이어서 다뤄보겠습니다.
클래스 선언의 기본 골격
class Person {
var name = "모리"
var age = 20
}
fun main() {
val person = Person()
println(person.name) // 모리
person.age = 21
}
class 키워드 뒤에 클래스 이름, 중괄호 안에 본문이 오는 구조입니다. 접근 제어자를 따로 쓰지 않으면 자바와 코틀린 모두 기본값이 있는데, 그 기본값이 서로 다릅니다.
// 자바: 접근 제어자를 생략하면 default(package-private)
class Person {
String name = "모리";
}
// 코틀린: 접근 제어자를 생략하면 public
class Person {
var name = "모리"
}
자바에서는 아무것도 안 쓰면 같은 패키지 안에서만 보이는 default 접근 제어이지만, 코틀린에서는 아무것도 안 쓰면 public입니다. 자바의 public class, public String name에 해당하는 걸 코틀린에서는 class, var name만으로 씁니다.
클래스를 생성자와 함께 선언하는 방법(주 생성자,
init블록)은 이번 글의 범위를 넘어서므로 별도 포스트에서 다루겠습니다. 여기서는 본문에 프로퍼티를 바로 선언하는 형태로만 진행합니다.
프로퍼티: 필드가 아니라 접근자 쌍
자바 클래스는 보통 필드를 private로 감추고, 그 필드에 접근할 getter/setter를 따로 작성합니다.
// 자바
public class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
코틀린은 이 "필드 + getter + setter" 세트를 프로퍼티라는 하나의 문법으로 묶었습니다. 클래스 본문에 val/var를 선언하면, 컴파일러가 그에 맞는 접근자를 자동으로 만들어 줍니다.
class Person {
var name = "모리" // getter, setter 자동 생성
val age = 20 // getter만 자동 생성 (val이므로 setter 없음)
}

person.name처럼 필드에 직접 접근하는 것처럼 보이는 코드도, 실제로는 자동 생성된 getName()을 호출하는 것입니다. person.name = "예진"처럼 대입하는 코드도 setName("예진") 호출로 컴파일됩니다. 자바에서 코틀린 클래스를 가져다 쓸 때는 이 자동 생성된 getName()/setName()을 그대로 호출하게 됩니다.
커스텀 접근자
프로퍼티의 값을 그냥 저장하고 꺼내는 것이 아니라, 접근할 때마다 원하는 로직을 태우고 싶다면 get()/set()을 직접 정의할 수 있습니다.
class Person {
var firstName = "모리"
var lastName = "김"
val fullName: String
get() = "$lastName$firstName"
}
fun main() {
val person = Person()
println(person.fullName) // 김모리
}
fullName은 대입할 초기값이 없고, 호출될 때마다 firstName과 lastName을 조합해서 값을 계산합니다. 이렇게 커스텀 get()만 있고 실제로 값을 저장하는 공간이 없는 프로퍼티도 가능합니다.
backing field: field 키워드
값을 검증하거나 가공해서 저장하고 싶을 때는, set() 안에서 실제 저장 공간인 backing field를 field라는 이름으로 참조합니다.
class Person {
var age: Int = 0
set(value) {
if (value < 0) {
field = 0
} else {
field = value
}
}
}
fun main() {
val person = Person()
person.age = -5
println(person.age) // 0
}
컴파일러는 이 참조 여부만 보고 backing field 생성을 결정합니다. set(value) { field = ... }처럼 field를 한 번이라도 참조하면 실제 저장 공간(backing field)이 만들어지고, 앞서 본 fullName처럼 get()/set() 안에서 field를 전혀 참조하지 않으면 backing field는 생기지 않습니다.

이 backing field 개념을 이해하고 나면, 자바 개발자가 실제로 자주 걸려 넘어지는 지점이 하나 더 있습니다.
자바 개발자가 걸려 넘어지는 포인트:
person.age는 필드 접근처럼 보입니다. 하지만 실제로는 필드에 직접 접근하는 게 아닙니다.age에 커스텀get()이 있다면,person.age를 읽을 때마다 그 로직이 매번 실행됩니다. 반대의 경우도 마찬가지입니다. 별다른 로직 없는 단순한var age = 20이라도, 겉모습이 필드와 똑같다고 해서 "게터/세터가 없다"고 오해하면 안 됩니다. 눈에 보이지 않을 뿐, 자동 생성된getAge()/setAge()는 항상 존재합니다.
주의할 점:
set(value) { age = value }처럼field대신 프로퍼티 이름을 그대로 다시 대입하면 무한 재귀에 빠집니다.age = value는 다시setAge()를 호출하는 것과 같기 때문입니다. 저장 공간에 접근할 때는 반드시field를 써야 합니다.
패키지와 디렉터리 구조
자바에서는 package com.example.utils;로 선언한 클래스를 반드시 com/example/utils/ 디렉터리 아래에 두어야 합니다. 디렉터리 구조와 패키지가 어긋나면 빌드 도구가 클래스를 찾지 못합니다.
코틀린은 이 둘을 분리했습니다. 파일 상단의 package 선언과, 그 파일이 실제로 놓인 디렉터리는 서로 달라도 컴파일됩니다.
// 파일 경로: src/main/kotlin/Person.kt (디렉터리 없이 최상위)
package com.example.model // 디렉터리 구조와 무관하게 선언 가능
class Person {
var name = "모리"
}
Kotlin 공식 코딩 컨벤션도 패키지 구조와 같은 디렉터리 구조를 쓰는 걸 권장하지만, 순수 코틀린 프로젝트에서는 강제 사항은 아니라고 명시하고 있습니다.
다만 Kotlin Multiplatform 프로젝트에서는 얘기가 다릅니다. JVM은 top-level 함수·프로퍼티를 직접 담을 수 없어서, 컴파일러가 파일명 기반의 래퍼 클래스(예: PersonKt)를 대신 만듭니다. 같은 이름의 래퍼 클래스가 서로 다른 소스 세트에 중복되면 "Duplicate JVM classes" 오류가 나기 때문에, 이 경우는 관례를 따르는 게 실질적으로 강제나 다름없습니다.
한 파일에 여러 클래스를 선언하는 것도 자유롭습니다. 자바처럼 "public 클래스는 파일당 하나, 파일명과 클래스명이 같아야 한다"는 제약이 코틀린에는 없습니다.
// 파일 경로: Models.kt — 파일명과 클래스명이 달라도, 클래스가 여러 개여도 무방
package com.example.model
class Person {
var name = "모리"
}
class Address {
var city = "서울"
}
다른 패키지의 클래스를 쓰려면 import해야 합니다. 이 규칙은 클래스든 함수든 동일하고, 패키지 전체를 한 번에 가져오는 와일드카드 import(*)도 가능합니다.
import com.example.model.Person
// 또는
import com.example.model.*
import에 as로 별칭을 붙이는 방법 등 세부 규칙은 이후 편에서 필요할 때 다루겠습니다.
정리
| 구분 | 설명 |
| 프로퍼티 | val/var로 선언하면 필드 + getter(+setter)가 자동 생성됨 |
val |
getter만 생성, setter 없음 |
var |
getter/setter 모두 생성 |
| 커스텀 접근자 | get()/set()을 직접 정의해 값을 계산하거나 검증 |
| backing field | get()/set()에서 field를 참조할 때만 생성되는 실제 저장 공간 |
| 패키지·디렉터리 | 코틀린은 일치가 필수는 아님 (단, 예외 있음 - 본문 참고) |
| 파일당 클래스 수 | 제한 없음, 파일명과 클래스명이 같지 않아도 됨 |
마무리
클래스 안에서 변수가 프로퍼티로, 커스텀 접근자로 확장되는 과정과 패키지·디렉터리 구조의 자유로움까지 살펴봤습니다. 변수를 담던 그릇이었던 클래스가, 그 변수를 프로퍼티로 승격시키는 문법까지 갖추게 된 셈입니다. 다음 편에서는 조건 분기를 표현하는 when과, 정해진 값들의 집합을 다루는 enum을 이어서 다뤄보겠습니다.
https://dmoritle.tistory.com/288
[Kotlin] 코틀린 온보딩 4편 - enum과 when: 완전성 검사 다시 보기
지난 편에서는 변수를 프로퍼티로 승격시키는 클래스 선언과 커스텀 접근자, 그리고 패키지·디렉터리 구조의 자유로움까지 살펴봤습니다.https://dmoritle.tistory.com/287 [Kotlin] 코틀린 온보딩 3편 -
dmoritle.tistory.com
이 시리즈는 『코틀린 인 액션(Kotlin in Action)』을 참고하여 작성했습니다.
'Language > Kotlin' 카테고리의 다른 글
| [Kotlin] 코틀린 온보딩 6편 - 함수 호출: 이름 있는 인자부터 infix까지 이해하기 (0) | 2026.09.26 |
|---|---|
| [Kotlin] 코틀린 온보딩 5편 - 루프와 예외 처리: for/while과 try-catch (0) | 2026.09.26 |
| [Kotlin] 코틀린 온보딩 4편 - enum과 when: 완전성 검사 다시 보기 (0) | 2026.09.26 |
| [Kotlin] 코틀린 온보딩 2편 - 변수: val/var와 문자열 템플릿으로 이해하기 (1) | 2026.09.06 |
| [Kotlin] 코틀린 온보딩 1편 - 함수: 문과 식부터 이해하기 (0) | 2026.09.06 |