Backend

๐Ÿš€ 2025๋…„ ์ฝ”ํ”„๋ง ํ•„์ˆ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ 5์ข… ์™„๋ฒฝ ๊ฐ€์ด๋“œ

๊ด€๋ฆฌ์ž

2์ผ ์ „

22900
#Kotlin#SpringBoot#JDSL#Komapper#Kotest#MockK#QueryDSL๋Œ€์ฒด#์ฝ”ํ”„๋ง

๐Ÿš€ 2025๋…„ ์ฝ”ํ”„๋ง ํ•„์ˆ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ 5์ข… ์™„๋ฒฝ ๊ฐ€์ด๋“œ

๐ŸŽฏ ํ•œ ์ค„ ์š”์•ฝ

QueryDSL ๋Œ€์‹  ๋ญ˜ ์จ์•ผ ํ• ๊นŒ? ์นด์นด์˜ค, ๋„ค์ด๋ฒ„, ํ† ์Šค๊ฐ€ ์„ ํƒํ•œ Kotlin + Spring Boot ํ•„์ˆ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‹ค์ „ ์ฝ”๋“œ์™€ ํ•จ๊ป˜ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค!

Kotlin ๋ฉ”์ธ ์ด๋ฏธ์ง€

๐Ÿค” ์ž๋ฐ” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ทธ๋Œ€๋กœ ์“ฐ๋ฉด ์•ˆ ๋˜๋‚˜?

์†”์งํžˆ ๋ง์”€๋“œ๋ฆฌ๋ฉด, ์ฝ”ํ‹€๋ฆฐ์˜ ์ง„์งœ ๋งค๋ ฅ์„ 50%๋„ ๋ชป ๋А๋ผ๊ณ  ์žˆ๋Š” ๊ฒ๋‹ˆ๋‹ค!

  • "QueryDSL ๋ฉ”ํƒ€๋ชจ๋ธ ์ƒ์„ฑ์ด ๋„ˆ๋ฌด ๋ฒˆ๊ฑฐ๋กœ์›Œ..."
  • "Mockito๊ฐ€ ์ฝ”ํ‹€๋ฆฐ suspend ํ•จ์ˆ˜๋ฅผ ๋ชป ๋ชจํ‚นํ•ด..."
  • "JUnit์œผ๋กœ ์ฝ”๋ฃจํ‹ด ํ…Œ์ŠคํŠธํ•˜๊ธฐ ๋„ˆ๋ฌด ๋ณต์žกํ•ด..."

์ด์ œ ์ฝ”ํ‹€๋ฆฐ ๋„ค์ดํ‹ฐ๋ธŒ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ๊ฐˆ์•„ํƒˆ ์‹œ๊ฐ„์ž…๋‹ˆ๋‹ค! ๐Ÿ”ฅ

1๏ธโƒฃ Kotlin JDSL - QueryDSL์˜ ์™„๋ฒฝํ•œ ๋Œ€์ฒด์ œ

Kotlin JDSL

๐ŸŽฏ ์™œ JDSL์ธ๊ฐ€?

LINE์—์„œ ๋งŒ๋“  Kotlin JDSL์€ QueryDSL์˜ ๋ชจ๋“  ์žฅ์ ์„ ๊ฐ€์ง€๋ฉด์„œ๋„ ๋” ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค!

QueryDSL vs JDSL ๋น„๊ต:

ํŠน์ง• QueryDSL Kotlin JDSL
๋ฉ”ํƒ€๋ชจ๋ธ Qํด๋ž˜์Šค ์ƒ์„ฑ ํ•„์š” KProperty ์ง์ ‘ ์‚ฌ์šฉ
์„ค์ • ๋ณต์žกํ•œ Gradle ์„ค์ • Spring Boot Starter
์œ ์ง€๋ณด์ˆ˜ ์ปค๋ฎค๋‹ˆํ‹ฐ ์˜์กด LINE ๊ณต์‹ ์ง€์›
์ฝ”ํ‹€๋ฆฐ ์นœํ™”์„ฑ Java ์šฐ์„  ์„ค๊ณ„ Kotlin Native ์„ค๊ณ„

๐Ÿ’ป ์‹ค์ œ ์‚ฌ์šฉ๋ฒ•

์˜์กด์„ฑ ์ถ”๊ฐ€

dependencies {
    implementation("com.linecorp.kotlin-jdsl:spring-data-kotlin-jdsl-starter:3.4.1")
}

Repository ๊ตฌํ˜„

@Repository
interface UserRepository : JpaRepository<User, Long>, KotlinJdslJpqlExecutor {
    
    // QueryDSL ๋ฐฉ์‹
    // fun findActiveUsers(): List<User> {
    //     return queryFactory
    //         .selectFrom(QUser.user)
    //         .where(QUser.user.status.eq("ACTIVE"))
    //         .fetch()
    // }
    
    // JDSL ๋ฐฉ์‹ - ๋ฉ”ํƒ€๋ชจ๋ธ ๋ถˆํ•„์š”
    fun findActiveUsers() = findAll {
        select(entity(User::class))
        from(entity(User::class))
        where(col(User::status).eq("ACTIVE"))
    }
    
    // ๋ณต์žกํ•œ ์กฐ์ธ๋„ ๊ฐ„๋‹จํ•˜๊ฒŒ
    fun findUsersWithOrders() = findAll {
        selectNew<UserWithOrderDto>(
            col(User::id),
            col(User::name),
            count(Order::id)
        )
        from(entity(User::class))
        leftJoin(User::orders)
        groupBy(col(User::id))
        having(count(Order::id).gt(5))
    }
}

๐Ÿข ์‹ค์ œ ๋„์ž… ์‚ฌ๋ก€

  • Spoqa: QueryDSL โ†’ JDSL ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (2024๋…„ ๊ณต์‹ ๋ฐœํ‘œ)
  • LINE ๋‚ด๋ถ€: ๋‹ค์ˆ˜ ์„œ๋น„์Šค์—์„œ ํ™œ์šฉ
  • ์˜คํ”ˆ์†Œ์Šค ์ปค๋ฎค๋‹ˆํ‹ฐ: ์ ์ง„์  ๋„์ž… ํ™•์‚ฐ

2๏ธโƒฃ Komapper - ์ปดํŒŒ์ผ ํƒ€์ž„ SQL ๊ฒ€์ฆ์˜ ํ˜๋ช…

Komapper

๐ŸŽฏ ์™œ Komapper์ธ๊ฐ€?

๋Ÿฐํƒ€์ž„ SQL ์—๋Ÿฌ๋Š” ์ด์ œ ๊ทธ๋งŒ! Komapper๋Š” ์ปดํŒŒ์ผ ์‹œ์ ์— SQL์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.

์ฃผ์š” ํŠน์ง•:

  • ์ปดํŒŒ์ผ ํƒ€์ž„ SQL ํƒ€์ž… ์ฒดํฌ โœ…
  • ๋ฐฐ์น˜ ์ž‘์—… ์ตœ์ ํ™” (Hibernate๋ณด๋‹ค 20% ๋น ๋ฆ„)
  • R2DBC ๋„ค์ดํ‹ฐ๋ธŒ ์ง€์›
  • Spring Boot 3 ์™„๋ฒฝ ํ†ตํ•ฉ

๐Ÿ’ป ์‹ค์ œ ์‚ฌ์šฉ๋ฒ•

Entity ์ •์˜

@KomapperEntity
@Table("users")
data class User(
    @KomapperId
    @Column("user_id")
    val id: Long = 0,
    
    @Column("username")
    val username: String,
    
    @Column("created_at")
    val createdAt: LocalDateTime = LocalDateTime.now()
)

Repository ๊ตฌํ˜„

@Repository
class UserRepository(private val db: Database) {
    
    // ์ปดํŒŒ์ผ ํƒ€์ž„์— SQL ๊ฒ€์ฆ!
    fun findByUsername(username: String): User? {
        val query = QueryDsl.from(u)
            .where { u.username eq username }
            .singleOrNull()
        
        return db.runQuery(query)
    }
    
    // ๋ฐฐ์น˜ INSERT - ๋งค์šฐ ๋น ๋ฆ„
    suspend fun batchInsert(users: List<User>) {
        val query = QueryDsl.insert(u).batch(users)
        db.runQuery(query)
    }
    
    // ๋™์  ์ฟผ๋ฆฌ๋„ ํƒ€์ž… ์„ธ์ดํ”„
    fun search(criteria: SearchCriteria): List<User> {
        val query = QueryDsl.from(u).where {
            criteria.username?.let { u.username contains it }
            criteria.minAge?.let { u.age gte it }
        }
        return db.runQuery(query)
    }
}

๐ŸŽฏ Komapper ์ฃผ์š” ํŠน์ง•

  • ์ปดํŒŒ์ผ ํƒ€์ž„ ๊ฒ€์ฆ: ๋Ÿฐํƒ€์ž„ ์—๋Ÿฌ ๋ฐฉ์ง€
  • R2DBC ์ง€์›: ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ
  • ๋ฐฐ์น˜ ์ตœ์ ํ™”: ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ์— ์œ ๋ฆฌ
  • Spring Boot 3: ์™„๋ฒฝํ•œ ํ†ตํ•ฉ ์ง€์›

3๏ธโƒฃ Kotest - JUnit์€ ์ด์ œ ์•ˆ๋…•

Kotest

๐ŸŽฏ ์™œ Kotest์ธ๊ฐ€?

JUnit์˜ ํ•œ๊ณ„๋ฅผ ๋›ฐ์–ด๋„˜๋Š” ์ฝ”ํ‹€๋ฆฐ ๋„ค์ดํ‹ฐ๋ธŒ ํ…Œ์ŠคํŒ…!

JUnit vs Kotest ๋น„๊ต:

๊ธฐ๋Šฅ JUnit 5 Kotest
์ฝ”๋ฃจํ‹ด ๋ณ„๋„ ์„ค์ • ํ•„์š” ๋„ค์ดํ‹ฐ๋ธŒ ์ง€์›
์ŠคํŽ™ ์Šคํƒ€์ผ 1๊ฐ€์ง€ (@Test) 10๊ฐ€์ง€ ์Šคํƒ€์ผ
ํ”„๋กœํผํ‹ฐ ํ…Œ์ŠคํŒ… ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋‚ด์žฅ ์ง€์›
DSL ์–ด๋…ธํ…Œ์ด์…˜ ๊ธฐ๋ฐ˜ ํ•จ์ˆ˜ํ˜• DSL

๐Ÿ’ป ์‹ค์ œ ์‚ฌ์šฉ๋ฒ•

๋‹ค์–‘ํ•œ ์ŠคํŽ™ ์Šคํƒ€์ผ

// 1. FunSpec (๊ฐ„๋‹จํ•œ ํ…Œ์ŠคํŠธ)
class UserServiceTest : FunSpec({
    
    test("์‚ฌ์šฉ์ž ์ƒ์„ฑ ์„ฑ๊ณต") {
        val service = UserService()
        val user = service.create("๊น€์ฝ”ํ‹€๋ฆฐ")
        
        user.name shouldBe "๊น€์ฝ”ํ‹€๋ฆฐ"
        user.id shouldBeGreaterThan 0
    }
    
    test("์ค‘๋ณต ์‚ฌ์šฉ์ž๋ช… ์˜ˆ์™ธ") {
        val service = UserService()
        service.create("๊น€์ฝ”ํ‹€๋ฆฐ")
        
        shouldThrow<DuplicateUsernameException> {
            service.create("๊น€์ฝ”ํ‹€๋ฆฐ")
        }
    }
})

// 2. BehaviorSpec (BDD ์Šคํƒ€์ผ)
class OrderServiceTest : BehaviorSpec({
    
    Given("์ฃผ๋ฌธ์ด ์žˆ์„ ๋•Œ") {
        val order = Order(id = 1, status = "PENDING")
        
        When("๊ฒฐ์ œ๋ฅผ ์™„๋ฃŒํ•˜๋ฉด") {
            order.complete()
            
            Then("์ƒํƒœ๊ฐ€ COMPLETED๋กœ ๋ณ€๊ฒฝ๋œ๋‹ค") {
                order.status shouldBe "COMPLETED"
            }
        }
        
        When("์ฃผ๋ฌธ์„ ์ทจ์†Œํ•˜๋ฉด") {
            order.cancel()
            
            Then("์ƒํƒœ๊ฐ€ CANCELLED๋กœ ๋ณ€๊ฒฝ๋œ๋‹ค") {
                order.status shouldBe "CANCELLED"
            }
        }
    }
})

// 3. StringSpec (๊ฐ€์žฅ ๊ฐ„๊ฒฐ)
class CalculatorTest : StringSpec({
    
    "1 + 1 = 2" {
        (1 + 1) shouldBe 2
    }
    
    "๋ฌธ์ž์—ด ์—ฐ๊ฒฐ" {
        "Hello, " + "World!" shouldBe "Hello, World!"
    }
})

์ฝ”๋ฃจํ‹ด ํ…Œ์ŠคํŠธ

class AsyncServiceTest : FunSpec({
    
    test("๋น„๋™๊ธฐ API ํ˜ธ์ถœ").config(timeout = 5.seconds) {
        val service = AsyncService()
        
        // suspend ํ•จ์ˆ˜ ์ง์ ‘ ํ…Œ์ŠคํŠธ!
        val result = service.fetchDataAsync()
        
        result shouldNotBe null
        result.size shouldBeGreaterThan 0
    }
    
    test("๋™์‹œ ์‹คํ–‰ ํ…Œ์ŠคํŠธ") {
        val service = AsyncService()
        
        // 100๊ฐœ ๋™์‹œ ์‹คํ–‰
        val results = (1..100).map { id ->
            async { service.process(id) }
        }.awaitAll()
        
        results.distinct().size shouldBe 100
    }
})

ํ”„๋กœํผํ‹ฐ ๊ธฐ๋ฐ˜ ํ…Œ์ŠคํŒ…

class PropertyTest : FunSpec({
    
    test("๋ชจ๋“  ์–‘์ˆ˜์— ๋Œ€ํ•ด ์„ฑ๋ฆฝ") {
        checkAll<Int> { num ->
            whenever(num > 0) {
                (num * 2) shouldBeGreaterThan num
            }
        }
    }
    
    test("๋ฌธ์ž์—ด ์—ญ์ˆœ ํ…Œ์ŠคํŠธ") {
        checkAll<String> { str ->
            str.reversed().reversed() shouldBe str
        }
    }
})

4๏ธโƒฃ MockK - Mockito๋Š” ์žŠ์–ด๋ผ

MockK

๐ŸŽฏ ์™œ MockK์ธ๊ฐ€?

์ฝ”ํ‹€๋ฆฐ์„ ์œ„ํ•ด ์ฒ˜์Œ๋ถ€ํ„ฐ ์„ค๊ณ„๋œ ๋ชจํ‚น ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ!

Mockito vs MockK ๋น„๊ต:

๊ธฐ๋Šฅ Mockito MockK
Final ํด๋ž˜์Šค inline mock ํ•„์š” ๊ธฐ๋ณธ ์ง€์›
Suspend ํ•จ์ˆ˜ ์ง€์› ๋ถˆ๊ฐ€ coEvery/coVerify
DSL Java ์Šคํƒ€์ผ Kotlin DSL
์–ธ์–ด ์นœํ™”์„ฑ Java ์šฐ์„  Kotlin ์ „์šฉ

๐Ÿ’ป ์‹ค์ œ ์‚ฌ์šฉ๋ฒ•

๊ธฐ๋ณธ ๋ชจํ‚น

@Test
fun `์‚ฌ์šฉ์ž ์กฐํšŒ ํ…Œ์ŠคํŠธ`() {
    // Given
    val repository = mockk<UserRepository>()
    val service = UserService(repository)
    
    every { repository.findById(1L) } returns User(1L, "๊น€์ฝ”ํ‹€๋ฆฐ")
    
    // When
    val user = service.getUser(1L)
    
    // Then
    user.name shouldBe "๊น€์ฝ”ํ‹€๋ฆฐ"
    verify(exactly = 1) { repository.findById(1L) }
}

Suspend ํ•จ์ˆ˜ ๋ชจํ‚น

@Test
fun `๋น„๋™๊ธฐ API ๋ชจํ‚น`() = runTest {
    // Mockito๋Š” ์ด๊ฒŒ ์•ˆ ๋จ!
    val client = mockk<ApiClient>()
    
    coEvery { client.fetchData() } returns listOf("A", "B", "C")
    
    val service = DataService(client)
    val result = service.processData()
    
    result shouldBe listOf("A", "B", "C")
    coVerify { client.fetchData() }
}

๊ณ ๊ธ‰ ๊ธฐ๋Šฅ

// 1. Spy (๋ถ€๋ถ„ ๋ชจํ‚น)
@Test
fun `์‹ค์ œ ๊ฐ์ฒด ๋ถ€๋ถ„ ๋ชจํ‚น`() {
    val service = spyk(UserService())
    
    every { service.validate(any()) } returns true
    // ๋‚˜๋จธ์ง€ ๋ฉ”์„œ๋“œ๋Š” ์‹ค์ œ ๊ตฌํ˜„ ์‚ฌ์šฉ
    
    service.create("ํ…Œ์ŠคํŠธ") shouldNotBe null
}

// 2. Capturing (์ธ์ž ์บก์ฒ˜)
@Test
fun `๋ฉ”์„œ๋“œ ์ธ์ž ์บก์ฒ˜`() {
    val repository = mockk<UserRepository>()
    val slot = slot<User>()
    
    every { repository.save(capture(slot)) } returns Unit
    
    val service = UserService(repository)
    service.create("๊น€์ฝ”ํ‹€๋ฆฐ")
    
    slot.captured.name shouldBe "๊น€์ฝ”ํ‹€๋ฆฐ"
}

// 3. Relaxed Mock (์ž๋™ ๊ธฐ๋ณธ๊ฐ’)
@Test
fun `Relaxed ๋ชจ๋“œ`() {
    val repository = mockk<UserRepository>(relaxed = true)
    // ๋ชจ๋“  ๋ฉ”์„œ๋“œ๊ฐ€ ๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜ (์„ค์ • ๋ถˆํ•„์š”)
    
    repository.findAll() // ๋นˆ ๋ฆฌ์ŠคํŠธ ๋ฐ˜ํ™˜
    repository.count() // 0 ๋ฐ˜ํ™˜
}

5๏ธโƒฃ Exposed - JetBrains์˜ SQL DSL

๐ŸŽฏ ์™œ Exposed์ธ๊ฐ€?

JetBrains๊ฐ€ ๋งŒ๋“  ์ฝ”ํ‹€๋ฆฐ SQL ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ!

  • JPA ์—†์ด ํƒ€์ž… ์„ธ์ดํ”„ SQL
  • DSL๊ณผ DAO ํŒจํ„ด ๋ชจ๋‘ ์ง€์›
  • ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ ๊ฐ„ํŽธ

๐Ÿ’ป ์‹ค์ œ ์‚ฌ์šฉ๋ฒ•

// ํ…Œ์ด๋ธ” ์ •์˜
object Users : IntIdTable() {
    val name = varchar("name", 50)
    val email = varchar("email", 100).uniqueIndex()
    val age = integer("age")
}

// ์‚ฌ์šฉ
class UserService {
    fun createUser(name: String, email: String, age: Int) = transaction {
        Users.insert {
            it[Users.name] = name
            it[Users.email] = email
            it[Users.age] = age
        }
    }
    
    fun findAdults() = transaction {
        Users.select { Users.age greaterEq 18 }
            .map { 
                User(
                    it[Users.id].value,
                    it[Users.name],
                    it[Users.email],
                    it[Users.age]
                )
            }
    }
}

๐Ÿข ์‹ค์ œ ๋„์ž… ํ˜„ํ™ฉ

๊ฒ€์ฆ๋œ ๋„์ž… ์‚ฌ๋ก€

  • Spoqa: Kotlin JDSL ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์„ฑ๊ณต ์‚ฌ๋ก€
  • LINE: ์ž์ฒด ๊ฐœ๋ฐœ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋‚ด๋ถ€ ํ™œ์šฉ
  • JetBrains: Exposed๋ฅผ ๋‹ค์ˆ˜ ํ”„๋กœ์ ํŠธ์— ์‚ฌ์šฉ

์ปค๋ฎค๋‹ˆํ‹ฐ ๋™ํ–ฅ

  • ์˜คํ”ˆ์†Œ์Šค: GitHub์—์„œ 1,000+ ํ”„๋กœ์ ํŠธ ์‚ฌ์šฉ
  • ์Šคํƒ€ํŠธ์—…: Spring Boot + Kotlin ์กฐํ•ฉ์œผ๋กœ ํ™•์‚ฐ
  • ๊ฐœ๋ฐœ์ž: QueryDSL ๋Œ€์•ˆ์œผ๋กœ ๊ด€์‹ฌ ์ฆ๊ฐ€

๐Ÿ“Š ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํŠน์„ฑ ๋น„๊ต

๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ฃผ์š” ๊ฐ•์  ๋Ÿฌ๋‹ ์ปค๋ธŒ ์ƒํƒœ๊ณ„
JDSL ๋ฉ”ํƒ€๋ชจ๋ธ ๋ถˆํ•„์š” โญโญโญ LINE ์ง€์›
Komapper ์ปดํŒŒ์ผ ํƒ€์ž„ ๊ฒ€์ฆ โญโญโญโญ ํ™œ๋ฐœํ•œ ๊ฐœ๋ฐœ
Exposed JetBrains ํ’ˆ์งˆ โญโญ ์•ˆ์ •์ 
Kotest ๋‹ค์–‘ํ•œ ์ŠคํŽ™ โญโญโญ ํฐ ์ปค๋ฎค๋‹ˆํ‹ฐ
MockK ์ฝ”ํ‹€๋ฆฐ ๋„ค์ดํ‹ฐ๋ธŒ โญโญ ํ‘œ์ค€ ๋„๊ตฌ

๐Ÿš€ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฐ€์ด๋“œ

QueryDSL โ†’ JDSL

// Before (QueryDSL)
queryFactory
    .selectFrom(QUser.user)
    .where(QUser.user.age.gt(18))
    .fetch()

// After (JDSL)
findAll {
    select(entity(User::class))
    from(entity(User::class))
    where(col(User::age).gt(18))
}

JUnit โ†’ Kotest

// Before (JUnit)
@Test
fun testSomething() {
    assertEquals(2, 1 + 1)
}

// After (Kotest)
test("1 + 1 = 2") {
    (1 + 1) shouldBe 2
}

Mockito โ†’ MockK

// Before (Mockito)
when(repository.findById(1L)).thenReturn(user)

// After (MockK)
every { repository.findById(1L) } returns user

๐Ÿ’ญ ๋งˆ๋ฌด๋ฆฌ

2025๋…„, ์•„์ง๋„ ์ž๋ฐ” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋งŒ ์“ฐ๊ณ  ๊ณ„์‹ ๊ฐ€์š”?

์ฝ”ํ‹€๋ฆฐ์˜ ์ง„์ •ํ•œ ๋งค๋ ฅ์€ ์ฝ”ํ‹€๋ฆฐ์„ ์œ„ํ•ด ๋งŒ๋“ค์–ด์ง„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์“ธ ๋•Œ ๋น›๋‚ฉ๋‹ˆ๋‹ค.

ํŠนํžˆ QueryDSL์ฒ˜๋Ÿผ ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์ค‘๋‹จ๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๊ณ„์† ์“ธ ์ด์œ ๋Š” ์—†์Šต๋‹ˆ๋‹ค.
JDSL์ด๋‚˜ Komapper๋กœ ๊ฐˆ์•„ํƒ€๋ฉด ๋” ๊ฐ„๊ฒฐํ•˜๊ณ  ๋น ๋ฅธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์—ฌ๋Ÿฌ๋ถ„์€ ์–ด๋–ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ ํƒํ•˜์‹ค ๊ฑด๊ฐ€์š”? ๋Œ“๊ธ€๋กœ ๊ฒฝํ—˜์„ ๊ณต์œ ํ•ด์ฃผ์„ธ์š”! ๐Ÿš€


์ด ๊ธ€์ด ๋„์›€์ด ๋˜์…จ๋‹ค๋ฉด ์ข‹์•„์š”์™€ ๊ณต์œ  ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค! โค๏ธ

๐Ÿ”— ์ฐธ๊ณ  ์ž๋ฃŒ

๋Œ“๊ธ€ 0๊ฐœ

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค

์ฒซ ๋ฒˆ์งธ ๋Œ“๊ธ€์„ ์ž‘์„ฑํ•ด๋ณด์„ธ์š”!