// build.sbt
name := "scala-learning-guide"
version := "1.0.0"
scalaVersion := "2.12.18"
// 의존성 설정
libraryDependencies ++= Seq(
"org.apache.spark" %% "spark-core" % "3.5.0",
"org.apache.spark" %% "spark-sql" % "3.5.0",
"org.scalatest" %% "scalatest" % "3.2.15" % Test,
"com.typesafe" % "config" % "1.4.2",
"ch.qos.logback" % "logback-classic" % "1.4.7"
)
// 컴파일러 옵션
scalacOptions ++= Seq(
"-deprecation",
"-feature",
"-unchecked",
"-Xlint",
"-Ywarn-unused"
)
// Java 버전
javacOptions ++= Seq("-source", "11", "-target", "11")
// build.sbt (루트)
lazy val root = (project in file("."))
.aggregate(core, api, worker)
lazy val core = (project in file("core"))
.settings(
name := "scala-guide-core",
libraryDependencies ++= commonDependencies
)
lazy val api = (project in file("api"))
.dependsOn(core)
.settings(
name := "scala-guide-api",
libraryDependencies ++= apiDependencies
)
lazy val worker = (project in file("worker"))
.dependsOn(core)
.settings(
name := "scala-guide-worker",
libraryDependencies ++= workerDependencies
)
// 공통 의존성
val commonDependencies = Seq(
"com.typesafe.scala-logging" %% "scala-logging" % "3.9.5",
"org.scalatest" %% "scalatest" % "3.2.15" % Test
)
val apiDependencies = Seq(
"com.typesafe.akka" %% "akka-http" % "10.5.0",
"com.typesafe.akka" %% "akka-stream" % "2.8.0"
)
val workerDependencies = Seq(
"org.apache.kafka" %% "kafka" % "3.4.0"
)
// project/plugins.sbt
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") // 코드 포매팅
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.7") // 코드 커버리지
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.1") // Fat JAR 생성
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.9.16") // 패키징
# 컴파일
sbt compile
# 테스트 실행
sbt test
# 특정 테스트만 실행
sbt "testOnly *UserServiceSpec"
# 지속적 컴파일 (파일 변경 감지)
sbt ~compile
# 의존성 트리 보기
sbt dependencyTree
# 패키지 생성
sbt package
# Fat JAR 생성
sbt assembly
# 프로젝트 클린
sbt clean
# REPL 시작
sbt console
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
// FlatSpec: BDD 스타일
class CalculatorSpec extends AnyFlatSpec with Matchers {
"A Calculator" should "add two numbers" in {
val result = Calculator.add(2, 3)
result should be(5)
}
it should "subtract two numbers" in {
val result = Calculator.subtract(5, 3)
result should be(2)
}
it should "multiply two numbers" in {
val result = Calculator.multiply(2, 3)
result should be(6)
}
it should "handle division by zero" in {
an[ArithmeticException] should be thrownBy {
Calculator.divide(1, 0)
}
}
}
object Calculator {
def add(a: Int, b: Int): Int = a + b
def subtract(a: Int, b: Int): Int = a - b
def multiply(a: Int, b: Int): Int = a * b
def divide(a: Int, b: Int): Int = a / b
}
import org.scalatest.matchers.should.Matchers._
// 동등성
result should be(expected)
result shouldBe expected
result should equal(expected)
// 부정
result should not be expected
result shouldNot be(expected)
// 컬렉션
list should contain("element")
list should have size 3
list should be(empty)
list should contain allOf ("a", "b", "c")
list should contain oneOf ("a", "b", "c")
// Option
option should be(defined)
option should be(empty)
option should contain(value)
// 예외
an[Exception] should be thrownBy { /* code */ }
the[Exception] thrownBy { /* code */ } should have message "error"
// 문자열
string should startWith("prefix")
string should endWith("suffix")
string should include("substring")
string should fullyMatch regex """[a-z]+""".r
import org.scalatest.concurrent.ScalaFutures
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
class AsyncServiceSpec extends AnyFlatSpec with Matchers with ScalaFutures {
"AsyncService" should "fetch user data" in {
val future = AsyncService.getUser(1)
whenReady(future) { user =>
user.id should be(1)
user.name should not be empty
}
}
it should "handle errors gracefully" in {
val future = AsyncService.getUser(-1)
whenReady(future.failed) { ex =>
ex shouldBe a[IllegalArgumentException]
}
}
}
object AsyncService {
def getUser(id: Int): Future[User] = Future {
if (id > 0) User(id, s"User$id")
else throw new IllegalArgumentException("Invalid ID")
}
}
case class User(id: Int, name: String)
import org.scalatest.propspec.AnyPropSpec
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
class StringUtilsSpec extends AnyPropSpec with ScalaCheckPropertyChecks {
property("reversing a string twice gives original string") {
forAll { (s: String) =>
val reversed = s.reverse.reverse
assert(reversed == s)
}
}
property("string length equals char count") {
forAll { (s: String) =>
assert(s.length == s.toList.size)
}
}
}
// Java 클래스
import java.util.{List => JList, ArrayList}
import java.util.HashMap
// Java 컬렉션을 Scala로 변환
import scala.jdk.CollectionConverters._
val javaList: JList[String] = new ArrayList[String]()
javaList.add("Java")
javaList.add("Scala")
// Java List → Scala List
val scalaList: List[String] = javaList.asScala.toList
println(scalaList) // List(Java, Scala)
// Scala List → Java List
val backToJava: JList[String] = scalaList.asJava
// Scala 클래스 (Java 친화적으로 작성)
import scala.beans.BeanProperty
class ScalaUser(@BeanProperty var name: String, @BeanProperty var age: Int) {
def this() = this("", 0) // 기본 생성자 (Java 호환)
def greet(): String = s"Hello, $name"
}
// @BeanProperty는 Java 스타일 getter/setter 생성
// getName(), setName(String), getAge(), setAge(int)
// Java에서 사용
public class JavaClient {
public static void main(String[] args) {
ScalaUser user = new ScalaUser("Alice", 25);
System.out.println(user.getName()); // Alice
user.setAge(26);
System.out.println(user.greet()); // Hello, Alice
}
}
// ❌ Java에서 사용 불가: 기본 파라미터
def greet(name: String = "World"): String = s"Hello, $name"
// ✅ Java에서 사용 가능: 오버로딩
def greet(name: String): String = s"Hello, $name"
def greet(): String = greet("World")
// ❌ Java에서 사용 불가: Option
def findUser(id: Int): Option[User] = ???
// ✅ Java에서 사용 가능: null 허용 또는 Optional
import java.util.Optional
def findUser(id: Int): Optional[User] = ???
// build.sbt
libraryDependencies += "org.typelevel" %% "cats-core" % "2.9.0"
import cats._
import cats.implicits._
// Semigroup
val result1 = 1 |+| 2 |+| 3 // 6
val result2 = "Hello" |+| " " |+| "World" // "Hello World"
// Functor
val option = Some(5)
val mapped = option.map(_ * 2) // Some(10)
// Monad
val result3 = for {
a <- Some(1)
b <- Some(2)
c <- Some(3)
} yield a + b + c // Some(6)
// build.sbt
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-actor-typed" % "2.8.0",
"com.typesafe.akka" %% "akka-stream" % "2.8.0"
)
import akka.actor.typed.ActorSystem
import akka.actor.typed.scaladsl.Behaviors
object HelloWorld {
def apply(): Behaviors.Receive[String] = Behaviors.receive { (context, message) =>
context.log.info(s"Received: $message")
Behaviors.same
}
}
// 사용
val system = ActorSystem(HelloWorld(), "hello-world")
system ! "Hello"
system ! "World"
// build.sbt
libraryDependencies += "com.typesafe.play" %% "play" % "2.9.0"
// app/controllers/HomeController.scala
package controllers
import javax.inject._
import play.api.mvc._
@Singleton
class HomeController @Inject()(val controllerComponents: ControllerComponents) extends BaseController {
def index() = Action { implicit request: Request[AnyContent] =>
Ok("Hello from Play Framework!")
}
def user(id: Int) = Action {
Ok(s"User ID: $id")
}
}
// build.sbt
libraryDependencies ++= Seq(
"com.typesafe.slick" %% "slick" % "3.4.1",
"org.postgresql" % "postgresql" % "42.5.4"
)
import slick.jdbc.PostgresProfile.api._
import scala.concurrent.ExecutionContext.Implicits.global
// 테이블 정의
class Users(tag: Tag) extends Table[(Int, String, Int)](tag, "users") {
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name")
def age = column[Int]("age")
def * = (id, name, age)
}
val users = TableQuery[Users]
// 쿼리
val db = Database.forConfig("mydb")
val query = users.filter(_.age > 18).result
val future = db.run(query)
// .scalafmt.conf
version = "3.7.3"
runner.dialect = scala212
maxColumn = 100
indent.main = 2
indent.defnSite = 2
rewrite.rules = [
RedundantBraces,
RedundantParens,
SortImports
]
# 포매팅 적용
sbt scalafmt
# 포매팅 검증
sbt scalafmtCheck
# 커버리지 리포트 생성
sbt clean coverage test coverageReport
// build.sbt
addCompilerPlugin("org.wartremover" %% "wartremover" % "3.0.9" cross CrossVersion.full)
wartremoverErrors ++= Warts.unsafe
// build.sbt
// sbt-assembly 플러그인 사용
assembly / assemblyJarName := "app.jar"
assembly / assemblyMergeStrategy := {
case PathList("META-INF", xs @ _*) => MergeStrategy.discard
case x => MergeStrategy.first
}
sbt assembly
java -jar target/scala-2.12/app.jar
// build.sbt
enablePlugins(JavaAppPackaging, DockerPlugin)
dockerBaseImage := "openjdk:11-jre-slim"
dockerExposedPorts := Seq(8080)
sbt docker:publishLocal
docker run -p 8080:8080 app:1.0.0
기능 | Maven | SBT |
---|---|---|
설정 파일 | XML (pom.xml) | Scala (build.sbt) |
의존성 | <dependency> |
libraryDependencies |
플러그인 | XML 설정 | Scala 코드 |
멀티 모듈 | <modules> |
aggregate , dependsOn |
빌드 | mvn package |
sbt package |
// JUnit
@Test
public void testAddition() {
assertEquals(5, Calculator.add(2, 3));
}
// ScalaTest (더 표현력 좋음)
"Calculator" should "add two numbers" in {
Calculator.add(2, 3) should be(5)
}
다음 구조의 프로젝트를 SBT로 구성하세요:
root
├── core (공통 모델과 유틸리티)
├── api (REST API)
└── worker (백그라운드 작업)
데이터베이스를 사용하는 통합 테스트 작성:
class UserRepositoryIntegrationSpec extends AnyFlatSpec with Matchers with BeforeAndAfterAll {
var db: Database = _
override def beforeAll(): Unit = {
// 테스트 DB 초기화
}
override def afterAll(): Unit = {
// 테스트 DB 정리
}
"UserRepository" should "save and retrieve users" in {
// 테스트 구현
}
}
Java 라이브러리를 Scala 친화적으로 래핑:
// Java의 java.util.concurrent.ExecutorService를 Scala Future로 래핑
이번 챕터에서 학습한 내용:
축하합니다! Scala 학습 가이드의 모든 챕터를 완료하셨습니다.
추천 자료: