들어가며
실무에서 사용하는 코틀린을 경험해 보기 위해 이 프로젝트를 진행했다.
코틀린 멀티 모듈 구성 방법을 다루기보다는 자바와의 차이점만 다룬다.
https://github.com/rotomoo/kotlin-multi-module-board
GitHub - rotomoo/kotlin-multi-module-board: 2026.02 코틀린 멀티 모듈 게시판
2026.02 코틀린 멀티 모듈 게시판. Contribute to rotomoo/kotlin-multi-module-board development by creating an account on GitHub.
github.com
프로젝트 구조
kotlin-multi-module-board/
├── api/ # API 모듈 (컨트롤러, Facade, DTO)
├── domain/ # DOMAIN 모듈 (엔티티, Repository, Service)
├── gradle/
│ └── libs.versions.toml # 공통 버전 관리
├── build.gradle.kts # 루트
└── settings.gradle.kts # 모듈 정의
1. .kotlin/sessions 디렉토리는 무엇인가?
프로젝트 루트에 .kotlin/sessions 디렉토리가 생성되는 것을 발견했다.
이는 Kotlin 컴파일러 데몬의 세션 정보를 저장하는 디렉토리이다.
코틀린 컴파일러는 빌드 성능을 최적화하기 위해 데몬 프로세스를 사용한다.
이 데몬은 JVM을 계속 실행 상태로 유지하여 매번 새로운 JVM을 시작하는 오버헤드를 줄인다.
sessions 디렉토리는 이 데몬과 Gradle 간의 통신 세션 정보를 관리한다.
자바에서는 이런 디렉토리가 없는데, 코틀린 컴파일러만의 특징이다.
배포할 필요는 없으니 .gitignore에 .kotlin/을 추가해두자.
2. build.gradle.kts - Kotlin DSL 사용
코틀린을 사용하는 이유 에서도 소개했지만 Groovy 코드와는 다른 장점들이 있다.
- 타입 안전성: 컴파일 시점에 오류를 잡을 수 있다

- IDE 자동완성: IntelliJ에서 코드 완성 기능이 완벽하게 동작한다

- 일관된 언어: 프로젝트 코드와 빌드 스크립트가 같은 언어를 사용한다.
3. Kotlin 컴파일러 옵션 설정
kotlin {
compilerOptions {
freeCompilerArgs.addAll("-Xjsr305=strict")
}
}
먼저 코틀린의 타입 표기를 보자.
코틀린 타입 표기
- Entity : non-null 타입. null 불가하다.
- Entity? : nullable 타입. null 가능하다.
- Entity! : 플랫폼 타입 (자바에서 온 것). null인지 아닌지 모른다.
Entity!는 자바 코드에서 온 타입이다.
이 경우 컴파일러가 null 체크를 강제하지 않아서 런타임에 NPE가 발생할 수 있다는걸 인지해두자.
-Xjsr305=strict 표기
JSR-305는 @Nullable, @Nonnull 같은 nullability 어노테이션의 표준이다. 이 옵션을 strict로 설정하면 Spring의 null-safety 어노테이션을 코틀린 타입 시스템에 반영한다. 즉, Entity! 대신 Entity? 또는 Entity로 정확하게 인식한다.
옵션 없을 때:
val entity = repository.findByIdOrNull(1L)
// entity 타입: Entity! (null인지 아닌지 모름)
// 컴파일러가 null 체크 강제 안함
entity.name // 컴파일 OK
런타임에 NPE 발생
옵션 있을 때:
val entity = repository.findByIdOrNull(1L)
// entity 타입: Entity? (nullable로 인식)
entity.name // 컴파일 에러로 null 체크 필요
entity?.name // OK
이 경우엔 컴파일 에러가 발생할테니 런타임 NPE를 방지할 수 있다.
이 옵션은 start.spring.io에서 Kotlin 프로젝트 생성 시 기본으로 포함된다.
4. 테스트의 Mockito vs MockK
코틀린에서는 두 가지 Mocking 라이브러리를 사용한다.
MockK (PostFacadeTest.kt)
@ExtendWith(MockKExtension::class)
class PostFacadeTest {
@MockK
private lateinit var postService: PostService
@InjectMockKs
private lateinit var postFacade: PostFacade
@Test
fun `should create post and return response`() {
// given
every { postService.create(categoryId, memberId, title, content) } returns savedPost
// when
val response = postFacade.create(categoryId, request)
// then
verify(exactly = 1) { postService.create(categoryId, memberId, title, content) }
}
}
Mockito-Kotlin (PostControllerTest.kt)
@WebMvcTest(PostController::class)
class PostControllerTest {
@MockitoBean
private lateinit var postFacade: PostFacade
@Test
fun `should return 201 when post created successfully`() {
// given
whenever(postFacade.create(eq(categoryId), any())).thenReturn(response)
// when & then
mockMvc.perform(post("/api/v1/category/$categoryId/posts"))
.andExpect(status().isCreated)
verify(postFacade).create(eq(categoryId), any())
}
}
차이점 비교
| 항목 | MockK | Mockito-Kotlin |
|---|---|---|
| 언어 | 코틀린 네이티브 | 자바 라이브러리의 코틀린 래퍼 |
| 문법 | every { }, verify { } | whenever(), verify() |
| final class mocking | 기본 지원 | 별도 설정 필요 |
| 코루틴 지원 | 네이티브 지원 | 제한적 |
| Spring 통합 | SpringMockK 필요 | @MockitoBean 기본 제공 |
둘중 어떤걸 사용하는가?
MockK는 코틀린 DSL 전용이라 자바에서는 사용할 수 없다.
만약 자바 → 코틀린 전환 프로젝트라면 기존 Mockito를 유지하고, 새 코드에만 MockK를 쓰는 경우도 있다.
하지만 처음부터 코틀린 프로젝트라면 MockK만 사용해도 충분하다.
5. 엔티티에서 = 0, ? = null 사용 이유와 kotlin-jpa 플러그인
@Entity
class Post(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
@ManyToOne(fetch = FetchType.LAZY)
var parent: Category? = null,
@Column(nullable = false)
var viewCount: Int = 0,
var description: String? = null
)
JPA 기본 생성자 문제와 kotlin-jpa 플러그인
JPA 스펙에서는 엔티티에 기본 생성자가 반드시 필요하다.
Hibernate가 리플렉션으로 엔티티 인스턴스를 생성하기 때문이다.
자바
@Entity
public class Post {
@Id
private Long id;
private String title;
// 기본 생성자 - 자바는 명시적으로 생성하거나(@NoArgsConstructor), 다른 생성자가 없으면 자동 생성됨
public Post() {}
public Post(String title) {
this.title = title;
}
}
자바에서는 다른 생성자가 없으면 기본 생성자가 자동으로 생성된다.
있어도 직접 추가하면 된다.
코틀린
@Entity
class Post(
@Id
val id: Long,
val title: String
// 기본 생성자 없음
)
코틀린에서는 기본 생성자가 자동 생성되지 않는다.
위 코드는 Post(id, title) 생성자만 존재한다.
그러므로 JPA가 기본 생성자를 호출하려고 하면 에러가 발생한다.
해결 방법 1: 모든 파라미터에 기본값 부여
@Entity
class Post(
@Id
val id: Long = 0,
val title: String = "" // 기본값을 주면 기본 생성자 생성됨
)
모든 파라미터에 기본값을 주면 코틀린 컴파일러가 기본 생성자를 자동 생성한다.
하지만 이 방법은 의미 없는 기본값을 강제해야 하는 단점이 있다.
해결 방법 2: kotlin-jpa 플러그인 사용
// domain/build.gradle.kts
plugins {
alias(libs.plugins.kotlin.jpa)
}
kotlin-jpa 플러그인은 컴파일 시점에 @Entity, @Embeddable, @MappedSuperclass 어노테이션이 붙은 클래스에 기본 생성자를 자동으로 추가한다.
이 플러그인 덕분에 의미 없는 기본값 없이도 엔티티를 정의할 수 있다.
플러그인 적용은 했다. 그런데 왜 = 0, = null을 사용하는것일까?
@Entity
class Post(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long,
@Column(nullable = false)
var viewCount: Int
)
val post = Post(
viewCount = 0,
)
// Post 객체를 생성할 때 기본값이 없어 컴파일 오류가 발생한다. id에 뭘 기본값으로 넣어야될지 모르기때문에
@Entity
class Post(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
@Column(nullable = false)
var viewCount: Int = 0,
)
val post = Post(
viewCount = 0,
)
// Post 객체를 생성할 때 id에 기본값이 있어 컴파일 오류가 발생하지 않는다.
즉, 플러그인이 있는데도 기본값을 사용하는 이유는 JPA를 위한 기본 생성자가 아니라 개발 편의성 때문이다.
이때 id를 0으로 했기 때문에 새로운 엔티티로 식별한다.
@Override
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) { // 1. 새 객체인지 확인
em.persist(entity); // 2. 새 객체면 persist (Insert)
return entity;
} else {
return em.merge(entity); // 3. 기존 객체면 merge (Update)
}
}
public boolean isNew(T entity) {
ID id = getId(entity);
...
if (id instanceof Number) {
return ((Number) id).longValue() == 0L;
}
return StringUtils.hasText(id.toString());
}
'코틀린' 카테고리의 다른 글
| 코틀린을 사용하는 이유 (1) | 2025.12.19 |
|---|