Post

Kotlin + Spring Boot로 웹 크롤러 만들기

Jsoup을 활용한 웹 크롤링 API 구현 및 코루틴/가상 스레드 비동기 처리 비교

Kotlin과 Spring Boot를 사용하여 API 요청을 받고, 웹 사이트를 크롤링하여 데이터를 가져오는 애플리케이션을 만드는 방법을 단계별로 설명해 드리겠습니다.

  1. 프로젝트 설정

먼저, 필요한 의존성을 build.gradle.kts 파일에 추가합니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
plugins {
    id("org.springframework.boot") version "3.3.2"
    id("io.spring.dependency-management") version "1.1.0"
    kotlin("jvm") version "1.9.22"
    kotlin("plugin.spring") version "1.9.22"
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jsoup:jsoup:1.16.1") // 웹 크롤링을 위한 Jsoup 라이브러리
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}
  1. 크롤링 서비스 구현

웹 사이트를 크롤링하는 서비스를 만듭니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.jsoup.Jsoup
import org.springframework.stereotype.Service

@Service
class CrawlerService {

    fun crawlWebsite(url: String): String {
        val doc = Jsoup.connect(url).get()
        val title = doc.title()
        val body = doc.body().text()
        return "Title: $title\nBody: $body"
    }
}
  1. 컨트롤러 구현

API 요청을 처리할 컨트롤러를 만듭니다:

1
2
3
4
5
6
7
8
9
10
11
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/api")
class CrawlerController(private val crawlerService: CrawlerService) {

    @GetMapping("/crawl")
    fun crawlWebsite(@RequestParam url: String): String {
        return crawlerService.crawlWebsite(url)
    }
}
  1. 예외 처리

크롤링 중 발생할 수 있는 예외를 처리하기 위한 예외 처리기를 추가합니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestControllerAdvice

@RestControllerAdvice
class GlobalExceptionHandler {

    @ExceptionHandler(Exception::class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    fun handleException(e: Exception): String {
        return "An error occurred: ${e.message}"
    }
}
  1. 비동기 처리 (선택사항)

크롤링 작업이 오래 걸릴 수 있으므로, 비동기 처리를 추가할 수 있습니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.springframework.stereotype.Service

@Service
class CrawlerService {

    suspend fun crawlWebsiteAsync(url: String): String = withContext(Dispatchers.IO) {
        val doc = Jsoup.connect(url).get()
        val title = doc.title()
        val body = doc.body().text()
        "Title: $title\nBody: $body"
    }
}

컨트롤러도 수정합니다:

1
2
3
4
5
6
7
8
9
10
11
12
import kotlinx.coroutines.runBlocking
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/api")
class CrawlerController(private val crawlerService: CrawlerService) {

    @GetMapping("/crawl")
    fun crawlWebsite(@RequestParam url: String): String = runBlocking {
        crawlerService.crawlWebsiteAsync(url)
    }
}
  1. 데이터 모델 (선택사항)

크롤링한 데이터를 구조화하려면 데이터 클래스를 만들 수 있습니다:

1
2
3
4
data class CrawledData(
    val title: String,
    val body: String
)

서비스와 컨트롤러를 이 데이터 클래스를 사용하도록 수정합니다.

  1. 실행 및 테스트

애플리케이션을 실행하고 다음과 같이 API를 테스트할 수 있습니다:

1
GET http://localhost:8080/api/crawl?url=https://example.com

이 예제는 기본적인 크롤링 기능을 구현합니다. 실제 사용 시 다음 사항을 고려해야 합니다:

  • 로봇 배제 표준(robots.txt) 준수
  • 웹사이트의 이용 약관 확인
  • 크롤링 속도 제한 및 예의 지키기
  • 더 복잡한 파싱 로직 구현 (필요에 따라)
  • 보안 고려 (사용자 입력 검증 등)

이 구조를 기반으로 필요에 따라 기능을 확장하고 최적화할 수 있습니다.

코루틴과 가상 스레드 모두 비동기 처리에 적합한 기술입니다. 각각의 장단점과 예제를 자세히 설명해 드리겠습니다.

  1. 코루틴 사용 예제

코루틴은 Kotlin의 기본 비동기 프로그래밍 모델로, 가독성이 높고 구조화된 동시성을 제공합니다.

build.gradle.kts에 의존성 추가:

1
2
3
4
dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.7.3")
}

서비스 클래스:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import org.jsoup.Jsoup
import org.springframework.stereotype.Service

@Service
class CrawlerService {

    suspend fun crawlWebsites(urls: List<String>): List<CrawledData> = withContext(Dispatchers.IO) {
        urls.map { url ->
            async {
                try {
                    val doc = Jsoup.connect(url).get()
                    CrawledData(url, doc.title(), doc.body().text())
                } catch (e: Exception) {
                    CrawledData(url, "Error", e.message ?: "Unknown error")
                }
            }
        }.map { it.await() }
    }
}

data class CrawledData(val url: String, val title: String, val content: String)

컨트롤러:

1
2
3
4
5
6
7
8
9
10
11
12
import kotlinx.coroutines.runBlocking
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/api")
class CrawlerController(private val crawlerService: CrawlerService) {

    @PostMapping("/crawl")
    fun crawlWebsites(@RequestBody urls: List<String>): List<CrawledData> = runBlocking {
        crawlerService.crawlWebsites(urls)
    }
}
  1. 가상 스레드 사용 예제

가상 스레드는 Java 21부터 제공되는 기능으로, 기존 스레드 API와 호환되면서도 경량 스레드의 이점을 제공합니다.

build.gradle.kts에 Java 버전 설정:

1
2
3
java {
    sourceCompatibility = JavaVersion.VERSION_21
}

서비스 클래스:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.jsoup.Jsoup
import org.springframework.stereotype.Service
import java.util.concurrent.Executors

@Service
class CrawlerService {

    fun crawlWebsites(urls: List<String>): List<CrawledData> {
        return Executors.newVirtualThreadPerTaskExecutor().use { executor ->
            urls.map { url ->
                executor.submit<CrawledData> {
                    try {
                        val doc = Jsoup.connect(url).get()
                        CrawledData(url, doc.title(), doc.body().text())
                    } catch (e: Exception) {
                        CrawledData(url, "Error", e.message ?: "Unknown error")
                    }
                }
            }.map { it.get() }
        }
    }
}

data class CrawledData(val url: String, val title: String, val content: String)

컨트롤러:

1
2
3
4
5
6
7
8
9
10
11
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/api")
class CrawlerController(private val crawlerService: CrawlerService) {

    @PostMapping("/crawl")
    fun crawlWebsites(@RequestBody urls: List<String>): List<CrawledData> {
        return crawlerService.crawlWebsites(urls)
    }
}

비교:

  1. 코루틴 장점:
    • Kotlin 네이티브 지원으로 가독성이 높음
    • 구조화된 동시성 제공
    • 취소 및 예외 처리가 용이
    • 다양한 코루틴 스코프와 컨텍스트 제공

단점:

  • Java 코드와의 통합이 상대적으로 복잡할 수 있음
  • 학습 곡선이 있을 수 있음
  1. 가상 스레드 장점:
    • 기존 Java 코드와 완벽히 호환
    • 스레드 풀 관리가 필요 없음
    • 대규모 동시성 처리에 효율적
    • Java의 기존 동시성 API와 통합이 쉬움

단점:

  • Java 21 이상 필요
  • 코루틴에 비해 구조화된 동시성 기능이 부족
  • Kotlin의 특화된 기능을 활용하기 어려울 수 있음

선택 기준:

  1. 프로젝트 요구사항: Kotlin 중심 프로젝트라면 코루틴, Java 호환성이 중요하다면 가상 스레드
  2. 팀 경험: Kotlin/코루틴에 익숙하다면 코루틴, Java에 익숙하다면 가상 스레드
  3. 성능 요구사항: 둘 다 높은 성능을 제공하지만, 대규모 동시성에서는 가상 스레드가 약간 유리할 수 있음
  4. 기존 코드베이스: 기존 Java 프로젝트라면 가상 스레드가 통합하기 쉬움

두 기술 모두 효율적인 비동기 처리를 제공하며, 프로젝트의 특성과 팀의 선호도에 따라 선택할 수 있습니다. 코루틴은 Kotlin의 강점을 최대한 활용할 수 있고, 가상 스레드는 Java 생태계와의 완벽한 호환성을 제공합니다.

This post is licensed under CC BY 4.0 by the author.