Kotlin

[Kotlin] ์ฝ”ํ‹€๋ฆฐ ์‚ฌ์šฉํ•˜๋ฉด์„œ ํ™œ์šฉํ•œ ์ฝ”๋“œ ์ •๋ฆฌ + WebFlux + MongoDB

soogoori 2024. 7. 12. 17:07

์‚ฌ์šฉ๋˜๋Š” ์ฝ”๋“œ๋ฅผ ์ •๋ฆฌํ•ด๋ด…๋‹ˆ๋‹น

๊ทธ์ € ์žŠ์ง€ ์•Š๊ธฐ ์œ„ํ•œ ๊ธฐ๋ก์šฉ์ด๊ณ ...๐Ÿ“ + WebFlux์™€ MongoDB๋„ ์กฐ๊ธˆ ์ •๋ฆฌ

์ˆ˜์‹œ๋กœ ์—…๋ฐ์ดํŠธ ์˜ˆ์ • .. ๐Ÿค“

 

 

Document (MongoDB)

  • data class
    • ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด๋Š” ๋ชฉ์ ์˜ ํด๋ž˜์Šค ์„ ์–ธ
    • ๋ฐ์ดํ„ฐ ์ €์žฅ, ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ ๊ธฐ๋ณธ์ ์ธ ๋ฉ”์„œ๋“œ ์ž๋™ ์ƒ์„ฑ 
    • equals(), hashCode(), toString(), componentN(), copy() ๋“ฑ ์ƒ์„ฑ ๐Ÿ‘‰ ํ•„์š”์— ๋”ฐ๋ผ ์ง์ ‘ ๊ตฌํ˜„ ๊ฐ€๋Šฅ
    • ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์˜ ์ฃผ ์ƒ์„ฑ์ž์— ์„ ์–ธ๋œ ํ”„๋กœํผํ‹ฐ ๐Ÿ‘‰ val๋กœ ์„ ์–ธ
data class User(val name: String, val age: Int)
fun main() {
    val user1 = User("Alice", 30)
    val user2 = User("Bob", 25)

    println(user1.toString())  // ์ถœ๋ ฅ: User(name=Alice, age=30)
    println(user1.hashCode())  // ์ถœ๋ ฅ: ํ•ด์‹œ ์ฝ”๋“œ ๊ฐ’
    println(user1 == user2)    // ์ถœ๋ ฅ: false, ๊ฐ ํ•„๋“œ๋ฅผ ๋น„๊ตํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

    val olderUser = user1.copy(age = 35)
    println(olderUser)         // ์ถœ๋ ฅ: User(name=Alice, age=35)
}

  • companion object
    • ํด๋ž˜์Šค ์•ˆ์— ์ •์˜๋˜๋Š” ํŠน๋ณ„ํ•œ ๊ฐ์ฒด
    • ํด๋ž˜์Šค์˜ ์ธ์Šคํ„ด์Šค ์—†์ด๋„ ํด๋ž˜์Šค์™€ ์—ฐ๊ด€๋œ ์ •์  ๋ฉค๋ฒ„๋‚˜ ๋ฉ”์„œ๋“œ ์„ ์–ธ ๊ฐ€๋Šฅ
    • ์ž๋ฐ”์˜ static์™€ ์œ ์‚ฌํ•œ ์—ญํ• 
    • ๊ฐ ํด๋ž˜์Šค๋Š” ๋‹จ ํ•˜๋‚˜์˜ companion object๋งŒ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์Œ
    • ๊ธฐ๋ณธ ์ด๋ฆ„ ์ƒ๋žต ๊ฐ€๋Šฅ ๐Ÿ‘‰ companion
class User(val name: String, val age: Int) {
    companion object {
        fun create(name: String): User {
            return User(name, 0)
        }
    }
}

fun main() {
    val user = User.create("Alice")
    println("User: ${user.name}, Age: ${user.age}")  // "User: Alice, Age: 0" ์ถœ๋ ฅ
}

๐Ÿ‘‰ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑํ•˜์ง€ ์•Š๊ณ ๋„ companion object ๋ฉค๋ฒ„ ์ ‘๊ทผ ๊ฐ€๋Šฅ 


  • operator fun invoke
    • operator fun์€ ์—ฐ์‚ฐ์ž ์˜ค๋ฒ„๋กœ๋”ฉ ํ—ˆ์šฉํ•˜๋Š” ํŠน๋ณ„ํ•œ ํ•จ์ˆ˜ ์„ ์–ธ ๋ฐฉ์‹
    • ์—ฐ์‚ฐ์ž ์˜ค๋ฒ„๋กœ๋”ฉ ๐Ÿ‘‰ ํด๋ž˜์Šค๋‚˜ ๋ฐ์ดํ„ฐ ํƒ€์ž…์— ๋Œ€ํ•ด ํŠน์ • ์—ฐ์‚ฐ์ž์˜ ๋™์ž‘์„ ์ •์˜ํ•˜๊ฑฐ๋‚˜ ์žฌ์ •์˜
    • invoke ์—ฐ์‚ฐ์ž ๐Ÿ‘‰ ๊ฐ์ฒด๋ฅผ ํ•จ์ˆ˜์ฒ˜๋Ÿผ ํ˜ธ์ถœ ๊ฐ€๋Šฅ
class Greeter(val greeting: String) {
    operator fun invoke(name: String) {
        println("$greeting, $name!")
    }
}

fun main() {
    val greeter = Greeter("Hello")
    greeter("World")  // ์ถœ๋ ฅ: Hello, World!
}
greeter ๊ฐ์ฒด๋ฅผ ํ•จ์ˆ˜์ฒ˜๋Ÿผ ํ˜ธ์ถœ
greeter("World")์™€ greeter.invoke("World")๋Š” ๋™์ผ

  • with(๊ฐ์ฒด) ๐Ÿ‘‰ with๊ด€๋ จ ์ •๋ฆฌ
    • ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ ์—†์ด ๋‚ด๋ถ€์—์„œ ์ˆ˜์‹ ๊ฐ์ฒด๋ฅผ ์ด์šฉํ•ด ๋‹ค๋ฅธ ํ•จ์ˆ˜ ํ˜ธ์ถœ
    • ์ฃผ์–ด์ง„ ๊ฐ์ฒด์— ๋Œ€ํ•ด ๋ธ”๋ก์„ ์‹คํ–‰ํ•˜๊ณ  ๋ธ”๋ก ๋‚ด์—์„œ ํ•ด๋‹น ๊ฐ์ฒด์˜ ๋ฉค๋ฒ„์— ์ง์ ‘ ์ ‘๊ทผ
@Document("cat")
data class CatDocument(
	
    @Id
    val catId: String? = null,
    
    val color: String?,
    
    val catType: List<String>?, 
){
	companion object{
        operator fun invoke(cat: Cat) = with(cat){
            CatDocument(
                catId = catId, // catId = this.catId -> this ์ƒ๋žต
                color = color,
                catType = catType,
            ) // CatDocument ์ƒ์„ฑ์ž ํ˜ธ์ถœํ•˜์—ฌ ์ƒˆ๋กœ์šด CatDocument ๊ฐ์ฒด ์ƒ์„ฑ
        }
	}
}

๐Ÿ‘‰ Cat ๊ฐ์ฒด๋ฅผ CatDocument ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ธฐ๋Šฅ

๐Ÿ‘‰ cat์€ with ๋ธ”๋ก ์•ˆ์—์„œ Cat ๊ฐ์ฒด์˜ ํ•„๋“œ์— ์ ‘๊ทผํ•ด์„œ cat์„ ๊ธฐ๋ฐ˜์œผ๋กœ CatDocument ๊ฐ์ฒด ์ƒ์„ฑ

๐Ÿ‘‰ with ๋ธ”๋ก ๋‚ด์˜ this๋Š” cat์„ ๊ฐ€๋ฆฌํ‚ด !

 

 

 

Repository (MongoDB)

  • ReactiveMongoRepository
    • ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ MongoDB์—์„œ ์ œ๊ณตํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค
    • ๋น„๋™๊ธฐ์ ์œผ๋กœ MongoDB ์กฐ์ž‘ ๊ธฐ๋Šฅ ์ œ๊ณต ๐Ÿ‘‰ Webflux
    • CRUD ๋ฉ”์„œ๋“œ ์ œ๊ณต 
    • @Document ์–ด๋…ธํ…Œ์ด์…˜ ์‚ฌ์šฉํ•˜์—ฌ MongoDB์˜ ์ปฌ๋ ‰์…˜์— ๋งคํ•‘๋จ
  • @EnableReactiveMongoRepository
    • Spring Data MongoDB์˜ reactive repository๋ฅผ ํ™œ์„ฑํ™”ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ
    • basePackages : ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ฒ€์ƒ‰ํ•  ํŒจํ‚ค์ง€ ์ง€์ •
    • reactiveMongoTemplateRef : ํŠน์ • ReactiveMongoTemplate ๋นˆ ์‚ฌ์šฉ ์ง€์ •
@SpringBootApplication
@EnableReactiveMongoRepositories(
    basePackages = ["com.example.repositories"],
    reactiveMongoTemplateRef = "reactiveMongoTemplate"
)

 

  • MongoDB์—์„œ ๊ฐ์ฒด๋ฅผ ์ฟผ๋ฆฌํ•˜์—ฌ ํŠน์ • ์กฐ๊ฑด์— ๋งž๋Š” ๊ฒฐ๊ณผ ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ
dbConfiguration.MongoTemplate().find(query, Document::class.java)
	.skip(skipOffset)
    .take(size)

 

๐Ÿ”น query๋Š” MongoDB์—์„œ ๊ฒ€์ƒ‰ํ•  ์กฐ๊ฑด ์ •์˜
๐Ÿ”น Document::class.java๋Š” ๊ฒ€์ƒ‰ํ•  ๋ฌธ์„œ์˜ ํด๋ž˜์Šค ํƒ€์ž… ์ง€์ • ๋ฐ ๋ฐ˜ํ™˜ํ•  ๊ฐ์ฒด 
๐Ÿ”น.skip(skipOffset) ๋Š” ๊ฒฐ๊ณผ ์ง‘ํ•ฉ์—์„œ ์ง€์ •๋œ ์ˆ˜์˜ ๋ฌธ์„œ ๊ฑด๋„ˆ๋›ฐ๋„๋ก ์ง€์‹œ
๐Ÿ”น.take(size) ๋Š” ๋ฐ˜ํ™˜ํ•  ์ตœ๋Œ€ ๋ฌธ์„œ ์ˆ˜ = ํ•œ ํŽ˜์ด์ง€์— ํ‘œ์‹œํ•  ๋ฌธ์„œ ์ˆ˜ 

 

  • Criteria
    • ์ฟผ๋ฆฌ ์ž‘์„ฑ ์‹œ ์กฐ๊ฑด ์ •์˜ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋Š” ๊ฐ์ฒด ๐Ÿ‘‰ ํ•„ํ„ฐ๋ง
    • addCriteria() ๐Ÿ‘‰ Query ๊ฐ์ฒด์— ์ƒˆ๋กœ์šด ๊ฒ€์ƒ‰ ์กฐ๊ฑด ์ถ”๊ฐ€ ์—ญํ• 
@Service
class PersonService @Autowired constructor(private val mongoTemplate: MongoTemplate) {

    fun findByLastNameAndAge(lastName: String, age: Int): List<Person> {
        val criteria = Criteria().andOperator(
            Criteria("lastName").`is`(lastName),
            Criteria("age").gte(age)
        )
        val query = Query(criteria)
        return mongoTemplate.find(query, Person::class.java)
    }
}
@Service
public class UserService {

    @Autowired
    private MongoTemplate mongoTemplate;

    public List<User> findByAgeGreaterThan(int age) {
        Query query = new Query();
        query.addCriteria(Criteria.where("age").gt(age));
        return mongoTemplate.find(query, User.class);
    }
}

 

  • query.with()
    • ๋‹ค์–‘ํ•œ ์˜ต์…˜ ์ ์šฉ ๐Ÿ‘‰ ํŽ˜์ด์ง•, ์ •๋ ฌ, ํŠน์ • ์˜ต์…˜ ๋“ฑ ์„ค์ •
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;

Query query = new Query();
Query query2 = new Query();

query.with(Sort.by(Sort.Direction.ASC, "fieldName"));
query2.with(PageRequest.of(pageNumber, pageSize));

 

 

 

 

 

Service

  •  flatMap
    • ๋น„๋™๊ธฐ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ ์ฒ˜๋ฆฌํ•˜๊ณ  ์—ฐ๊ฒฐ
  • let
    • null์ด ์•„๋‹Œ ๊ฒฝ์šฐ ์‚ฌ์šฉ๋  ๋กœ์ง ์ž‘์„ฑํ•˜๊ณ  ์ƒˆ๋กœ์šด ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜
  • Mono.just()
    • ๋น„๋™๊ธฐ์ ์œผ๋กœ ๋‹จ์ผ ๊ฐ’ ์ƒ์„ฑ
  • doOnSuccess
    • Mono ๋˜๋Š” Flux์—์„œ ๋น„๋™๊ธฐ ์ž‘์—…์ด ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋œ ํ›„ ๋ถ€๊ฐ€์ ์ธ ๋™์ž‘ ์ˆ˜ํ–‰
override fun update(taskId: String, task: Task): Mono<Task> {
    log.info { "[update] taskId: $taskId, task: $task" }

    return repository.findById(taskId)
        .flatMap { document ->
            repository.save(TaskDocument(taskId, document, task))
                .flatMap { taskDocument ->
                    task.taskSchedules?.let {
                        taskScheduleService.updateByTaskId(taskId, it, Task(taskDocument))
                            .collectList()
                            .map { scheduleList -> Task(taskDocument, scheduleList) }
                    } ?: Mono.just(Task(taskDocument))
                }
                .doOnSuccess { log.info { "[update] taskId: $taskId" } }
        }
}

 

๐Ÿ’ฅ ๋ณดํ†ต Spring + JPA๋ฅผ ์‚ฌ์šฉํ•ด์„œ UPDATE ๋กœ์ง์„ ๊ตฌํ˜„ํ•  ๋•Œ ์—”ํ‹ฐํ‹ฐ ๊ณ„์ธต์—์„œ ์ˆ˜์ • ๊ด€๋ จ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ถ”๊ฐ€ํ•ด์„œ ์ฒ˜๋ฆฌํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— repository.save()๋ฅผ ์‚ฌ์šฉํ•  ํ•„์š”๊ฐ€ ์—†์—ˆ๋‹ค ๐Ÿ‘‰ EntityManager๊ฐ€ ํŠธ๋žœ์žญ์…˜ ๋‚ด์—์„œ ์—”ํ‹ฐํ‹ฐ์˜ ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์ž๋™์œผ๋กœ ์ถ”์ ํ•˜๊ธฐ ๋•Œ๋ฌธ(dirty Checking)

โœจ ํ•˜์ง€๋งŒ ๋ฆฌ์•กํ‹ฐ๋ธŒ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ชจ๋ธ์—์„œ๋Š” ๋ชจ๋“  ์ž‘์—…์ด non-blocking ๋ฐ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ˆ˜ํ–‰๋˜๋ฏ€๋กœ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์—์„œ ๊ด€๋ฆฌํ•˜์ง€ ์•Š์•„ dirty checking์ด ์กด์žฌํ•˜์ง€ ์•Š๊ฒŒ ๋œ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ์—”ํ‹ฐํ‹ฐ์— ๋Œ€ํ•œ ๋ชจ๋“  ์ˆ˜์ • ์‚ฌํ•ญ์„ ๋ช…์‹œ์ ์œผ๋กœ DB์— ์ €์žฅ๋˜๊ฒŒ๋” ํ•ด์•ผํ•˜๋ฏ€๋กœ repository.save()๋ฅผ ํ˜ธ์ถœํ•ด ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์ €์žฅ !

 

 

  • thenEmpty()
Reactor ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ์ œ๊ณตํ•˜๋Š” ๋ฉ”์„œ๋“œ๋กœ, ํŠน๋ณ„ํžˆ ๋ฐ˜ํ™˜ํ•  ๊ฐ’์ด ์—†์„ ๋•Œ ์‚ฌ์šฉ
๐Ÿ‘‰ ๋‹จ์ˆœํžˆ ๋น„๋™๊ธฐ์ ์ธ ์ž‘์—…์ด ์™„๋ฃŒ๋˜์—ˆ์Œ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์šฉ๋„
public final Mono<Void> thenEmpty(Publisher<Void> other) {
        return this.then(fromDirect(other));
}

 

  • .when()
Reactor ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ์ œ๊ณตํ•˜๋Š” ๋ฉ”์„œ๋“œ๋กœ, ์—ฌ๋Ÿฌ ๊ฐœ์˜ Mono๋ฅผ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰ํ•˜๊ณ  ๋ชจ๋“  Mono๊ฐ€ ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฐ ํ›„ ๊ฒฐ๊ณผ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ ์ œ๊ณต 
Aggregate given publishers into a new Mono that will be fulfilled when all of the given sources have completed.
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

public class MonoWhenExample {
    public static void main(String[] args) {
        // ๋‘ ๊ฐœ์˜ ๋น„๋™๊ธฐ ์ž‘์—… ์ •์˜
        Mono<String> task1 = Mono.fromCallable(() -> {
            Thread.sleep(1000); // 1์ดˆ ๋™์•ˆ ๋Œ€๊ธฐ
            return "Task 1 completed";
        }).subscribeOn(Schedulers.boundedElastic());

        Mono<String> task2 = Mono.fromCallable(() -> {
            Thread.sleep(2000); // 2์ดˆ ๋™์•ˆ ๋Œ€๊ธฐ
            return "Task 2 completed";
        }).subscribeOn(Schedulers.boundedElastic());

        // Mono.when()์„ ์‚ฌ์šฉํ•˜์—ฌ ๋‘ ์ž‘์—…์„ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰ํ•˜๊ณ , ๋ชจ๋“  ์ž‘์—…์ด ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆผ
        Mono.when(task1, task2)
            .doOnTerminate(() -> System.out.println("All tasks completed"))
            .block(); // ๋ธ”๋กœํ‚น ํ˜ธ์ถœ๋กœ ๊ฒฐ๊ณผ ๋Œ€๊ธฐ (์˜ˆ์ œ์šฉ, ์‹ค์ œ ์‚ฌ์šฉ ์‹œ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ฒ˜๋ฆฌ)

        System.out.println("Program finished");
    }
}

 

โœจ ์‹ค์ œ ์ฝ”๋“œ์—์„œ๋Š” .thenEmpty(Mono.`when`( ~~~) ) 
์ด๋ ‡๊ฒŒ when ๋ฉ”์„œ๋“œ์— ๋ฐฑํ‹ฑ์„ ์ถ”๊ฐ€ํ–ˆ๋‹ค. ๊ทธ ์ด์œ ๋Š” ์ฝ”ํ‹€๋ฆฐ์—์„œ when์ด ํ‚ค์›Œ๋“œ์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
์ถฉ๋Œ์„ ํ”ผํ•˜๊ณ  ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ์„ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๋ฐฑํ‹ฑ์„ ์‚ฌ์šฉํ•œ๋‹ค !

 

  • filterNot
์ปฌ๋ ‰์…˜์˜ ์š”์†Œ๋ฅผ ์กฐ๊ฑด์— ๋”ฐ๋ผ ํ•„ํ„ฐ๋งํ•˜์—ฌ ํŠน์ • ์กฐ๊ฑด์— ๋ถ€ํ•ฉํ•˜์ง€ ์•Š๋Š” ์š”์†Œ๋“ค๋กœ ์ƒˆ๋กœ์šด ์ปฌ๋ ‰์…˜ ๋ฐ˜ํ™˜

 

 

 

์ฐธ๊ณ ์ž๋ฃŒ 

https://mangkyu.tistory.com/358

https://www.geeksforgeeks.org/spring-webflux-reactive-crud-rest-api-example/

https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Mono.html

https://www.baeldung.com/kotlin/backticks