Skip to content

qjatjr29/Beom-story

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

278 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐Ÿš€ Beomstory

๐ŸŽฅ ์งง์€ ์‹œ์—ฐ ์˜์ƒ (์˜ˆ์ •)

๐Ÿ“Œ ๊ธฐ๋Šฅ๋ณ„ ์‹œ์—ฐ ์˜์ƒ 1
๐Ÿ“Œ ๊ธฐ๋Šฅ๋ณ„ ์‹œ์—ฐ ์˜์ƒ 2


โœ”๏ธ ํ”„๋กœ์ ํŠธ ๊ฐœ์š”

ํ‰์†Œ์˜ ์ผ์ƒ์ด๋‚˜ ์—ฌํ–‰ ์ถ”์–ต๋“ค์„ ๊ธฐ๋กํ•ด ์ถ”์–ตํ•˜๊ธฐ ์œ„ํ•จ

๐Ÿ”— ๋งํฌ

๐Ÿš€ ์„œ๋น„์Šค
๐Ÿ”— ํ”„๋กœ์ ํŠธ ๋…ธ์…˜ ๋ณด๊ธฐ
โ†–๏ธ Front ์ฝ”๋“œ ๋ณด๋Ÿฌ๊ฐ€๊ธฐ (Github)

โŒ›๏ธ ํ”„๋กœ์ ํŠธ ๊ธฐ๊ฐ„

2025.02 ~ ing


๐Ÿ›  ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

์‚ฌ์šฉ ๊ธฐ์ˆ 

My Skills

๐ŸŒ  ๊ฐœ๋ฐœ ์–ธ์–ด / ํ”„๋ ˆ์ž„์›Œํฌ

Java Java SpringBoot SpringCloud JUnit5

๐Ÿ“ฆ Database

MySQL Redis

๐Ÿงฑ ์ธํ”„๋ผ

Gradle AWS S3 AWS S3 AWS S3 AWS S3 Docker Apache Kafka NaverCloudPlatform

๐Ÿ“  ํ˜‘์—…ํˆด

GitHub

๐Ÿงฑ ERD

ERD

โญ๏ธ ์•„ํ‚คํ…์ฒ˜

์ „์ฒด

๐Ÿ‘ค ์œ ์ € Flow

graph TD
    A[ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ] -->|๋กœ๊ทธ์ธ ์„ฑ๊ณต| B[ํ™ˆ ํ™”๋ฉด]
    B --> C[์ผ์ƒ ๊ธฐ๋ก ๋ณด๊ธฐ]
    B --> D[์ผ์ƒ ๊ธฐ๋ก ํ•˜๊ธฐ]
    B --> E[ํ”„๋กœํ•„ ๋ณด๊ธฐ]
    B --> H[์ผ์ƒ ๊ฒ€์ƒ‰]
    C --> F[๋Œ“๊ธ€ ์ž‘์„ฑ - todo]
    C --> G[๊ฒŒ์‹œ๋ฌผ ์ข‹์•„์š” - todo]
    E --> I[ํ”„๋กœํ•„ ์ˆ˜์ •]
    D --> J[์ผ์ƒ์„ ๋ณด๋‚ธ ์žฅ์†Œ ์ถ”๊ฐ€]
    J --> N[์‚ฌ์ง„ ์—…๋กœ๋“œ]
    F --> K[๋Œ€๋Œ“๊ธ€ ์ž‘์„ฑ - todo]
    H --> L[ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰]
    H --> M[์žฅ์†Œ ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰ - todo]
    H --> O[์นดํ…Œ๊ณ ๋ฆฌ ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰ - todo]
Loading

๐Ÿƒํ”„๋กœ์ ํŠธ ์ง„ํ–‰

โœ… ๊ตฌํ˜„ ํ•„์ˆ˜ ๊ธฐ๋Šฅ

  • ํšŒ์›๊ฐ€์ž… / ๋กœ๊ทธ์ธ
  • ๊ตฌ๊ธ€, ์นด์นด์˜ค ๋กœ๊ทธ์ธ
  • ํ”„๋กœํ•„ ์กฐํšŒ / ์ˆ˜์ •
  • ์ผ์ƒ ๊ธฐ๋ก / ์ˆ˜์ • / ์‚ญ์ œ
  • ์ผ์ƒ ๊ธฐ๋ก ์ƒ์„ธ ์กฐํšŒ (๋ชจ๋“  ์žฅ์†Œ ์กฐํšŒ)
  • ์žฅ์†Œ ์ƒ์„ฑ / ์ˆ˜์ •
  • ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ / ์‚ญ์ œ
  • ๊ธฐ๋ก ์‚ญ์ œ์‹œ ๊ธฐ๋ก์— ํฌํ•จ๋œ ๋ชจ๋“  ์žฅ์†Œ์‚ญ์ œ(์ด๋ฏธ์ง€ ํฌํ•จ)
  • ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰

๐Ÿง‘๐Ÿปโ€๐Ÿ’ป ์‚ฌ์šฉ ๊ธฐ์ˆ  ๋ฐ ๊ณ ๋ คํ•œ ๋‚ด์šฉ

โญ๏ธ Kotlin

โœ… ์‚ฌ์šฉ ์ด์œ 

Java์™€ Spring Boot๋กœ ๊ฐœ๋ฐœ์„ ํ•ด์™”์ง€๋งŒ ๊ฐœ์ธ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ ์ƒˆ๋กœ์šด ์–ธ์–ด์ธ Kotlin์„ ๊ฒฝํ—˜ํ•ด๋ณด๊ณ  ์‹ถ์—ˆ์Œ.

Kotlin์€ ๊ฐ€๋…์„ฑ๊ณผ ๋„ ์ฒ˜๋ฆฌ ๋ฐฉ์‹(Null Safety)์ด ๋›ฐ์–ด๋‚˜๊ณ  ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ์„ ๋†’์—ฌ์ค„ ์ˆ˜ ์žˆ๋‹ค๊ณ  ํŒ๋‹จํ•˜์—ฌ ๋„์ž…ํ•˜๊ฒŒ ๋จ.

๐Ÿ‘๐Ÿป ์žฅ์ 

Null Safety

  • Kotlin์˜ ํƒ€์ž… ์‹œ์Šคํ…œ์„ ํ†ตํ•ด NPE(NullPointerException) ๊ฐ€๋Šฅ์„ฑ์„ ์ปดํŒŒ์ผ ํƒ€์ž„์— ์˜ˆ๋ฐฉ.
  • ๋ช…์‹œ์ ์ธ ?, ?:, !! ์—ฐ์‚ฐ์ž ์‚ฌ์šฉ์œผ๋กœ ์ฝ”๋“œ ์•ˆ์ •์„ฑ์„ ๋†’์ž„.

ํ™•์žฅ ํ•จ์ˆ˜

  • ๊ธฐ์กด ํด๋ž˜์Šค์— ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋Š” Kotlin ํ™•์žฅ ํ•จ์ˆ˜๋ฅผ ์ ๊ทน ํ™œ์šฉํ•˜์—ฌ ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ์„ฑ๊ณผ ๊ฐ€๋…์„ฑ์„ ๋†’์ž„.

Data Class

  • ๊ฐ„๊ฒฐํ•˜๊ฒŒ DTO, Entity ์ •์˜๋ฅผ ์œ„ํ•ด data class๋ฅผ ์ ๊ทน ํ™œ์šฉ
  • equals/hashCode/toString ์ž๋™ ์ƒ์„ฑ์˜ ์ด์ ์„ ํ™œ์šฉ.

๐Ÿ“ฆ Spring data R2dbc

โœ… ์‚ฌ์šฉ ์ด์œ 
Kotlin + Coroutine ํ™˜๊ฒฝ์—์„œ ๋น„๋™๊ธฐ DB ํ†ต์‹ ์ด ํ•„์š”ํ•ด R2DBC๋ฅผ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค. Hibernate Reactive๋„ ๊ฒ€ํ† ํ–ˆ์œผ๋‚˜ ์ฝ”๋ฃจํ‹ด๊ณผ์˜ ํ˜ธํ™˜์„ฑ ์ธก๋ฉด์—์„œ R2DBC๊ฐ€ ๋” ์ ํ•ฉํ•˜๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ‘๐Ÿป ์žฅ์ 

  • Kotlin Coroutine ์ง€์›: suspend ํ•จ์ˆ˜ ๊ธฐ๋ฐ˜์œผ๋กœ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋น„๋™๊ธฐ ์ฝ”๋“œ ์ž‘์„ฑ ๊ฐ€๋Šฅ
  • Spring ๊ณต์‹ ์ง€์›: ํŠธ๋žœ์žญ์…˜ ๋“ฑ Spring ์ƒํƒœ๊ณ„์™€ ์—ฐ๋™์ด ์‰ฌ์›€

โš ๏ธ ํ•œ๊ณ„

  • ORM ๊ธฐ๋Šฅ ๋ฏธ์ง€์›: JPA ์–ด๋…ธํ…Œ์ด์…˜ ์‚ฌ์šฉ ๋ถˆ๊ฐ€, ์—”ํ‹ฐํ‹ฐ ๋งคํ•‘ ์ˆ˜๋™ ์ฒ˜๋ฆฌ ํ•„์š”
  • ์Šคํ‚ค๋งˆ ์ž๋™ ์ƒ์„ฑ ์—†์Œ: ์ง์ ‘ SQL ์Šคํฌ๋ฆฝํŠธ๋‚˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋„๊ตฌ ์‚ฌ์šฉ

๐Ÿ“ ์˜ˆ์‹œ

interface UserRepository : CoroutineCrudRepository<User, Long>
suspend fun findUserById(id: Long): User? = userRepository.findById(id)

blog ์ •๋ฆฌ


๐Ÿ—ณ๏ธ ํŠธ๋žœ์žญ์…˜ ์•„์›ƒ๋ฐ•์Šค ํŒจํ„ด + kafka

โœ… ์‚ฌ์šฉ ์ด์œ 

  • ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ ๋ณด์žฅ: ์ผ์ƒ(story) ์‚ญ์ œ ์‹œ ์—ฐ๊ฒฐ๋œ ์žฅ์†Œ(place) ์‚ญ์ œ๋ฅผ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋ฉด์„œ๋„ ํŠธ๋žœ์žญ์…˜ ์•ˆ์ •์„ฑ ํ™•๋ณด

  • ์‹ ๋ขฐ์„ฑ: DB ํŠธ๋žœ์žญ์…˜๊ณผ Kafka ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰์„ ์›์ž์ (atomic) ์œผ๋กœ ์ฒ˜๋ฆฌํ•ด ๋ฉ”์‹œ์ง€ ์œ ์‹ค ๋ฐฉ์ง€

  • ์‹œ์Šคํ…œ ๋ถ„๋ฆฌ: ์žฅ์†Œ ์‚ญ์ œ ์ฒ˜๋ฆฌ๋ฅผ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ถ„๋ฆฌํ•ด ์„œ๋น„์Šค ๊ฐ„ ๊ฒฐํ•ฉ๋„ ๊ฐ์†Œ

๐Ÿ› ๏ธ ๊ตฌํ˜„ ๋ฐฉ๋ฒ•

  1. ์ด๋ฒคํŠธ ์ €์žฅ (story service)
@Transactional
suspend fun delete(userId: Long, storyId: Long) {
    storyDeletePort.deleteStory(userId, storyId)  // 1. ์ผ์ƒ ์‚ญ์ œ
    storyOutboxPort.saveStoryDeleteMessage(storyId) // 2. ์•„์›ƒ๋ฐ•์Šค ํ…Œ์ด๋ธ” ์ €์žฅ
}
  1. message ๋ฐœํ–‰
@Scheduled(cron = "0 */10 * * * *")  // 10๋ถ„ ์ฃผ๊ธฐ ์‹คํ–‰
@Transactional
suspend fun publishPendingMessages() {
val pendingMessages = outboxRepository.findAllByOrderByCreatedAtAsc(batchSize)

    runBlocking {
        val successfulIds = publishPendingMessagesAsync(pendingMessages) // 3. Kafka ๋ฐœํ–‰
        outboxRepository.deleteAllById(successfulIds)  // 4. ์„ฑ๊ณตํ•œ ๋ฉ”์‹œ์ง€ ์‚ญ์ œ
    }
}
  1. consume ํ›„ ์‚ญ์ œ ์ฒ˜๋ฆฌ(place service)
@KafkaListener(topics = ["\${kafka.topic.story-outbox}"])
fun handleStoryOutboxEvent(payload: String) {
    runBlocking {
        try {
            val storyOutboxPayload = Json.decodeFromString<StoryOutboxPayload>(payload)
            when (storyOutboxPayload.type) {
                StoryOutboxType.STORY_DELETED -> handleStoryDeletedEvent(storyOutboxPayload.storyId)
            }
        } catch (e: Exception) {
            logger.error("โ— Kafka ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.message}")
            throw e
        }
    }
}

๐Ÿ—‚๏ธ MySQL Full-Text Search

๐Ÿ” ๋ฐฐ๊ฒฝ ๋ฐ ๋ฌธ์ œ ์ƒํ™ฉ
๋‹จ์ˆœํžˆ ์‚ฌ์šฉ์ž๋“ค์ด "์ œ๋ชฉ" ๋˜๋Š” "์„ค๋ช…"์— ํฌํ•จ๋œ ํ‚ค์›Œ๋“œ๋กœ LIKE '%keyword%' ๊ฒ€์ƒ‰์„ ์ˆ˜ํ–‰ํ•˜๋„๋ก ์„ค๊ณ„ํ–ˆ์ง€๋งŒ ์ด๋Š” ํ…Œ์ด๋ธ” ํ’€ ์Šค์บ” ๋ฐฉ์‹์œผ๋กœ ๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ์•„์งˆ์ˆ˜๋ก ์„ฑ๋Šฅ์ด ์ข‹์ง€ ์•Š์„ ๊ฒƒ์ด๋ผ ์ƒ๊ฐ

๐Ÿ› ๏ธ ํ•ด๊ฒฐ์ฑ…: Full-Text Search + n-gram

ALTER TABLE table_A ADD FULLTEXT INDEX idx_fulltext_title_desc (title, description);
EXPLAIN 
SELECT * FROM table_A WHERE MATCH(title, description) AGAINST('์—ฌํ–‰' IN BOOLEAN MODE);

blog ์ •๋ฆฌ


๐Ÿ’ฅ ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

๐Ÿ“จ ์ฝ”๋ฃจํ‹ด ๊ธฐ๋ฐ˜์˜ ๋น„๋™๊ธฐ ๋ฉ”์‹œ์ง€ ์ „์†ก ๋ฐฉ์‹

ํŠธ๋žœ์žญ์…˜ ์•„์›ƒ๋ฐ•์Šค ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜์—ฌ outbox ํ…Œ์ด๋ธ”์—์„œ ๋ฐฐ์น˜ ๋‹จ์œ„๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€ Kafka๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐœํ–‰ํ•˜์—ฌ ๋ฐ์ดํ„ฐ์˜ ์ •ํ•ฉ์„ฑ์„ ๋งž์ถ”๋Š” ๊ณผ์ •์—์„œ
๋ฉ”์‹œ์ง€๊ฐ€ ์‹ค์ œ๋กœ Kafka์— ์ •์ƒ์ ์œผ๋กœ ์ „๋‹ฌ๋˜์—ˆ๋Š”์ง€๋ฅผ ํŒŒ์•…ํ•˜๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ดˆ๊ธฐ์—๋Š” KafkaTemplate.send().get() ๋ฅผ ํ†ตํ•ด ๋™๊ธฐ์ ์œผ๋กœ ๋ฉ”์‹œ์ง€๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ „๋‹ฌ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ’ฃ๋ฌธ์ œ์ 

  • ๋™๊ธฐ์  ์ฒ˜๋ฆฌ๋กœ ๋ฉ”์‹œ์ง€๋‹น ์‘๋‹ต์„ ๊ธฐ๋‹ค๋ ค์•ผ ํ•จ
  • ๋Œ€๋Ÿ‰ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์‹œ ์„ฑ๋Šฅ ์ €ํ•˜
  • Kafka์˜ ๋น„๋™๊ธฐ์  ํŠน์„ฑ์„ ํ™œ์šฉํ•˜์ง€ ๋ชปํ•จ
  • ์ˆœ์ฐจ์  ์ฒ˜๋ฆฌ๋กœ ์ „์ฒด ์†Œ์š” ์‹œ๊ฐ„ ์ฆ๊ฐ€

์ฝ”๋ฃจํ‹ด ๊ธฐ๋ฐ˜์˜ ๋น„๋™๊ธฐ ๋ฉ”์‹œ์ง€ ์ „์†ก ๋ฐฉ์‹ ์œผ๋กœ ํ•ด๊ฒฐ
ํ•ด๊ฒฐ ๊ณผ์ • - blog


๐Ÿ“‚ Spring Data R2DBC - Coroutine ๊ณผ Pagination

๐Ÿ” ๋ฌธ์ œ ์ƒํ™ฉ
Spring Data R2DBC๋Š” ๋ฆฌ์•กํ‹ฐ๋ธŒ ํŠน์„ฑ์ƒ Page/Slice ๋ฐ˜ํ™˜์„ ๊ณต์‹ ์ง€์› โŒ ๊ธฐ์กด JPA์˜ PagingAndSortingRepository์ฒ˜๋Ÿผ ์ž๋™ํ™”๋œ ํŽ˜์ด์ง•์ด ๋ถˆ๊ฐ€๋Šฅํ•ด ์กฐํšŒ + ์ด ๊ฐœ์ˆ˜ ์ฟผ๋ฆฌ๋ฅผ ์ˆ˜๋™์œผ๋กœ ๊ฒฐํ•ฉ ๐Ÿ› ๏ธ ํ•ด๊ฒฐ ์ „๋žต

์ปค์Šคํ…€ ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๊ตฌํ˜„

  • DatabaseClient๋ฅผ ์ด์šฉํ•ด LIMIT/OFFSET ๊ธฐ๋ฐ˜ SQL ์ง์ ‘ ์ž‘์„ฑ
(์˜ˆ์‹œ)
val contentSql = """
    SELECT * FROM story
    WHERE title = :title
    LIMIT :limit OFFSET :offset
"""
val countSql = "SELECT COUNT(*) FROM story WHERE title = :title"

๋ณ‘๋ ฌ ์ฟผ๋ฆฌ ์‹คํ–‰

  • ์ปจํ…์ธ  ์กฐํšŒ์™€ ์ด ๊ฐœ์ˆ˜ ์กฐํšŒ๋ฅผ ์ฝ”๋ฃจํ‹ด ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ
suspend fun paginate(...): Page<StoryEntity> {
    val content = contentQuery()
    val total = countQuery()
    return PageImpl(content, pageable, total)
}

โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ

  • SQL ์ค‘๋ณต: ์ปจํ…์ธ /๊ฐœ์ˆ˜ ์ฟผ๋ฆฌ์˜ ์กฐ๊ฑด๋ฌธ์ด ๋™๊ธฐํ™”๋˜์ง€ ์•Š์œผ๋ฉด ๋ฐ์ดํ„ฐ ๋ถˆ์ผ์น˜ ๋ฐœ์ƒ
  • ์„ฑ๋Šฅ ์ €ํ•˜: OFFSET ๊ฐ’์ด ํด์ˆ˜๋ก ์ฟผ๋ฆฌ ์„ฑ๋Šฅ์ด ์„ ํ˜•์ ์œผ๋กœ ๊ฐ์†Œ
  • ํŠธ๋žœ์žญ์…˜ ๋ถ„๋ฆฌ: ๊ฐœ์ˆ˜ ์ฟผ๋ฆฌ์™€ ์ปจํ…์ธ  ์ฟผ๋ฆฌ๊ฐ€ ๋‹ค๋ฅธ ์Šค๋ƒ…์ƒท์„ ๋ณผ ์ˆ˜ ์žˆ์Œ

๐Ÿš€ ์ตœ์ ํ™” ๋ฐฉ์•ˆ(todo)

  1. ์ปค์„œ ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง• (๊ณ ๋ ค)
WHERE id > :lastId ORDER BY id LIMIT :size
  • OFFSET ๋Œ€์‹  ID ๋ฒ”์œ„ ์กฐ๊ฑด ์‚ฌ์šฉ
  1. R2dbcEntityTemplate ํ™œ์šฉ
template.select(StoryEntity::class)
    .matching(Query.query(where("title").`is`(title)).with(pageable))
    .all()
  1. ์บ์‹œ ์ „๋žต
@Cacheable("storyCount")
suspend fun getTotalCount(): Long
  • ๊ฐœ์ˆ˜ ์ •๋ณด์— ๋Œ€ํ•ด ์บ์‹ฑ์œผ๋กœ ์กฐํšŒ ์„ฑ๋Šฅ์ด ํ–ฅ์ƒ๋  ๊ฒƒ์ด๋ผ ์˜ˆ์ƒ

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

โšก