본문으로 건너뛰기
cccskills
무료GitHub에 공개

spring-boot-kotlin

Provides expert knowledge of Spring Boot conventions, Kotlin idioms, and MVC patterns for backend API development.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

설치 방법 보기

포함된 파일(1)

  • SKILL.md10.2 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

Spring Boot + Kotlin

Project Structure

Use feature-oriented MVC. One package per feature, not one package per layer.

src/main/kotlin/{base-package}/{feature}/
  controller/       # HTTP layer only — no business logic
  service/          # Business logic
  repository/       # Data access — extends JpaRepository or CrudRepository
  domain/           # JPA entities
  dto/              # Request/response objects — no @Entity here

src/test/kotlin/{base-package}/{feature}/
  controller/       # MockMvc tests
  service/          # Unit tests with MockK
  repository/       # @DataJpaTest tests

src/main/resources/
  application.yml
  application-prod.yml    # NOT in repo — use env vars
  db/migration/           # Flyway scripts

Never use src/main/kotlin/controllers/, src/main/kotlin/services/, etc. That is layer-first structure and it does not scale.

Kotlin Idioms for Spring

Data classes for DTOs. No setters, no mutable state.

data class CreateUserRequest(
    @field:NotBlank val email: String,
    @field:Size(min = 2, max = 100) val name: String
)

data class UserResponse(
    val id: UUID,
    val email: String,
    val name: String
)

Sealed classes for domain results and errors.

sealed class UserResult {
    data class Found(val user: User) : UserResult()
    data class NotFound(val id: UUID) : UserResult()
    data class Conflict(val email: String) : UserResult()
}

Null safety. Never use !! unless you have already verified the value is non-null and can document why. Prefer:

// bad
val name = user!!.name

// good
val name = user?.name ?: throw UserNotFoundException(id)

// good — scoped null check
user?.let { sendWelcomeEmail(it.email) }

Extension functions instead of utility classes.

// bad — Java style utility class
object UserMapper {
    fun toResponse(user: User): UserResponse { ... }
}

// good — extension function
fun User.toResponse(): UserResponse = UserResponse(
    id = id,
    email = email,
    name = name
)

Coroutines: do not introduce coroutines unless the project already uses them. Adding coroutines requires a Task Card with explicit scope.

Spring Boot Patterns

Controller

@RestController
@RequestMapping("/api/v1/users")
class UserController(private val userService: UserService) {

    @GetMapping("/{id}")
    fun getUser(@PathVariable id: UUID): ResponseEntity<UserResponse> {
        return when (val result = userService.getById(id)) {
            is UserResult.Found -> ResponseEntity.ok(result.user.toResponse())
            is UserResult.NotFound -> ResponseEntity.notFound().build()
        }
    }

    @PostMapping
    fun createUser(
        @Valid @RequestBody request: CreateUserRequest
    ): ResponseEntity<UserResponse> {
        val result = userService.create(request)
        return ResponseEntity.status(HttpStatus.CREATED).body(result.toResponse())
    }
}

Rules:

  • One controller per feature
  • No business logic in controllers — delegate to service
  • Use @Valid on request body params
  • Return ResponseEntity<T> for explicit HTTP status control

Service

@Service
@Transactional
class UserService(private val userRepository: UserRepository) {

    @Transactional(readOnly = true)
    fun getById(id: UUID): UserResult {
        val user = userRepository.findById(id).orElse(null)
            ?: return UserResult.NotFound(id)
        return UserResult.Found(user)
    }

    fun create(request: CreateUserRequest): User {
        if (userRepository.existsByEmail(request.email)) {
            throw UserConflictException(request.email)
        }
        return userRepository.save(User(email = request.email, name = request.name))
    }
}

Rules:

  • All business logic lives here
  • @Transactional at class level, readOnly = true for queries
  • Never @Transactional on private methods — Spring proxy does not intercept them
  • Never inject repositories into controllers

Repository

@Repository
interface UserRepository : JpaRepository<User, UUID> {
    fun findByEmail(email: String): User?
    fun existsByEmail(email: String): Boolean

    @Query("SELECT u FROM User u WHERE u.role = :role AND u.active = true")
    fun findActiveByRole(@Param("role") role: UserRole): List<User>
}

Rules:

  • Extend JpaRepository<Entity, IdType> or CrudRepository
  • Derived query methods for simple lookups
  • @Query with JPQL for complex queries
  • Native SQL only when JPQL cannot express it

Entity

@Entity
@Table(
    name = "users",
    indexes = [Index(columnList = "email", unique = true)]
)
class User(
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    val id: UUID = UUID.randomUUID(),

    @Column(name = "email", nullable = false, unique = true)
    var email: String,

    @Column(name = "name", nullable = false)
    var name: String,

    @Column(name = "created_at", nullable = false, updatable = false)
    @CreatedDate
    val createdAt: Instant = Instant.now()
)

Rules:

  • @Table with explicit name — never rely on inferred names
  • @Column(nullable = false) always explicit
  • UUIDs as PKs with GenerationType.UUID
  • Kotlin data class with JPA requires the Kotlin JPA plugin (adds no-arg constructor)
  • equals/hashCode based only on id, not all fields — or omit and use reference equality

DTO Separation

Never expose @Entity directly in API responses. Always map to a DTO.

// bad
@GetMapping("/{id}")
fun getUser(@PathVariable id: UUID): User  // exposes entity internals

// good
@GetMapping("/{id}")
fun getUser(@PathVariable id: UUID): UserResponse  // controlled surface

Validation

Validate at the DTO layer, not the service layer.

data class CreateUserRequest(
    @field:NotBlank(message = "Email is required")
    @field:Email(message = "Email must be valid")
    val email: String,

    @field:NotBlank(message = "Name is required")
    @field:Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters")
    val name: String
)

Use @field: prefix on annotations — Kotlin applies annotations to the property by default, not the backing field.

Error Handling

@ControllerAdvice
class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException::class)
    fun handleUserNotFound(ex: UserNotFoundException): ResponseEntity<ErrorResponse> {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
            .body(ErrorResponse(error = ex.message ?: "User not found", code = "USER_NOT_FOUND"))
    }

    @ExceptionHandler(MethodArgumentNotValidException::class)
    fun handleValidation(ex: MethodArgumentNotValidException): ResponseEntity<ErrorResponse> {
        val message = ex.bindingResult.fieldErrors
            .joinToString(", ") { "${it.field}: ${it.defaultMessage}" }
        return ResponseEntity.badRequest()
            .body(ErrorResponse(error = message, code = "VALIDATION_ERROR"))
    }
}

data class ErrorResponse(val error: String, val code: String)

Rules:

  • One @ControllerAdvice — do not scatter @ExceptionHandler across controllers
  • Typed domain exceptions — never throw RuntimeException directly
  • Consistent error body: { "error": "...", "code": "..." }
  • Never expose stack traces in responses

Testing

Unit tests with MockK.

@ExtendWith(MockKExtension::class)
class UserServiceTest {

    @MockK
    private lateinit var userRepository: UserRepository

    private lateinit var userService: UserService

    @BeforeEach
    fun setUp() {
        userService = UserService(userRepository)
    }

    @Test
    fun `should return NotFound when user does not exist`() {
        // Arrange
        val id = UUID.randomUUID()
        every { userRepository.findById(id) } returns Optional.empty()

        // Act
        val result = userService.getById(id)

        // Assert
        assertThat(result).isInstanceOf(UserResult.NotFound::class.java)
        verify(exactly = 1) { userRepository.findById(id) }
    }
}

Integration tests with MockMvc.

@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {

    @Autowired
    private lateinit var mockMvc: MockMvc

    @Test
    fun `should return 404 when user is not found`() {
        mockMvc.perform(get("/api/v1/users/${UUID.randomUUID()}"))
            .andExpect(status().isNotFound)
    }
}

Repository tests.

@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private lateinit var userRepository: UserRepository

    @Test
    fun `should find user by email`() {
        val user = userRepository.save(User(email = "test@example.com", name = "Test"))
        val found = userRepository.findByEmail("test@example.com")
        assertThat(found).isEqualTo(user)
    }
}

Rules:

  • Use MockK, not Mockito — the Kotlin API is cleaner and null-safe
  • Do not use @MockBean where mockk<T>() is sufficient
  • @DataJpaTest with H2 for repository tests (fast)
  • @SpringBootTest + @AutoConfigureMockMvc for controller tests

Security Rules

  • No stack traces in API responses — only @ControllerAdvice with controlled messages
  • Validate all input in DTO layer — service layer should receive already-validated data
  • application-prod.yml must not be committed — use environment variables or a secrets manager
  • Credentials, tokens, and keys are never hardcoded — use @Value with externalized config

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Use when adding monitoring, metrics, logging, or tracing to Spring AI or LLM integration code. Covers token tracking, latency measurement, cost estimation, and prompt/response logging. Use when user mentions AI monitoring, token costs, or LLM observability.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

lgzarturo/springboot-course32026년 10월 20일 업데이트

android

무료

Provides expert knowledge for building modern native Android apps using Kotlin, Jetpack Compose, Material Design 3, MVI architecture, Hilt dependency injection, and Media3/ExoPlayer.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

lgzarturo/springboot-course32026년 10월 20일 업데이트

Design, implement, review, and audit Android Jetpack Compose UI/UX for phone interfaces. Use when a request concerns concrete Compose layout, components, interaction, accessibility, or visual behavior. Do not activate for framework or dependency presence alone; non-UI Android work is excluded.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

lgzarturo/springboot-course32026년 10월 20일 업데이트

Provides expert knowledge for designing, implementing, and managing REST API versioning strategies with deprecation workflows.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

lgzarturo/springboot-course32026년 10월 20일 업데이트

astro

무료

Provides expert knowledge for building Astro 5+ sites with Islands Architecture, Content Collections, TypeScript, and performance-first rendering strategies.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

lgzarturo/springboot-course32026년 10월 20일 업데이트

Audits token storage mechanisms to prevent XSS-based JWT theft.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

lgzarturo/springboot-course32026년 10월 20일 업데이트

lgzarturo의 스킬 모두 보기

이 스킬의 문제 신고