Kotlin + Spring Boot로 웹 크롤러 만들기
Jsoup을 활용한 웹 크롤링 API 구현 및 코루틴/가상 스레드 비동기 처리 비교
Kotlin과 Spring Boot를 사용하여 API 요청을 받고, 웹 사이트를 크롤링하여 데이터를 가져오는 애플리케이션을 만드는 방법을 단계별로 설명해 드리겠습니다.
- 프로젝트 설정
먼저, 필요한 의존성을 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
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"
}
}
- 컨트롤러 구현
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
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
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
2
3
4
data class CrawledData(
val title: String,
val body: String
)
서비스와 컨트롤러를 이 데이터 클래스를 사용하도록 수정합니다.
- 실행 및 테스트
애플리케이션을 실행하고 다음과 같이 API를 테스트할 수 있습니다:
1
GET http://localhost:8080/api/crawl?url=https://example.com
이 예제는 기본적인 크롤링 기능을 구현합니다. 실제 사용 시 다음 사항을 고려해야 합니다:
- 로봇 배제 표준(robots.txt) 준수
- 웹사이트의 이용 약관 확인
- 크롤링 속도 제한 및 예의 지키기
- 더 복잡한 파싱 로직 구현 (필요에 따라)
- 보안 고려 (사용자 입력 검증 등)
이 구조를 기반으로 필요에 따라 기능을 확장하고 최적화할 수 있습니다.
코루틴과 가상 스레드 모두 비동기 처리에 적합한 기술입니다. 각각의 장단점과 예제를 자세히 설명해 드리겠습니다.
- 코루틴 사용 예제
코루틴은 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)
}
}
- 가상 스레드 사용 예제
가상 스레드는 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)
}
}
비교:
- 코루틴 장점:
- Kotlin 네이티브 지원으로 가독성이 높음
- 구조화된 동시성 제공
- 취소 및 예외 처리가 용이
- 다양한 코루틴 스코프와 컨텍스트 제공
단점:
- Java 코드와의 통합이 상대적으로 복잡할 수 있음
- 학습 곡선이 있을 수 있음
- 가상 스레드 장점:
- 기존 Java 코드와 완벽히 호환
- 스레드 풀 관리가 필요 없음
- 대규모 동시성 처리에 효율적
- Java의 기존 동시성 API와 통합이 쉬움
단점:
- Java 21 이상 필요
- 코루틴에 비해 구조화된 동시성 기능이 부족
- Kotlin의 특화된 기능을 활용하기 어려울 수 있음
선택 기준:
- 프로젝트 요구사항: Kotlin 중심 프로젝트라면 코루틴, Java 호환성이 중요하다면 가상 스레드
- 팀 경험: Kotlin/코루틴에 익숙하다면 코루틴, Java에 익숙하다면 가상 스레드
- 성능 요구사항: 둘 다 높은 성능을 제공하지만, 대규모 동시성에서는 가상 스레드가 약간 유리할 수 있음
- 기존 코드베이스: 기존 Java 프로젝트라면 가상 스레드가 통합하기 쉬움
두 기술 모두 효율적인 비동기 처리를 제공하며, 프로젝트의 특성과 팀의 선호도에 따라 선택할 수 있습니다. 코루틴은 Kotlin의 강점을 최대한 활용할 수 있고, 가상 스레드는 Java 생태계와의 완벽한 호환성을 제공합니다.