코틀린

코틀린 멀티 모듈 게시판 만들기 - 자바와의 차이점

rotomoo 2026. 1. 30. 18:39

들어가며

실무에서 사용하는 코틀린을 경험해 보기 위해 이 프로젝트를 진행했다.

코틀린 멀티 모듈 구성 방법을 다루기보다는 자바와의 차이점만 다룬다.

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