대상 : 이 글은 자바에 익숙하며, 스칼라로 당장 개발해야 하는 자바 개발자를 위해 쓰여졌습니다.
목표
Scala의 기본적인 문법과 서비스에서 주로 쓰일 라이브러리, 주의사항 등을 다룹니다.
또한, 자바를 기준으로 스칼라의 구현체계 및 방식에 관해 설명합니다.
String str = "Hello"
Integer num1 = 1;
var str = "Hello"
var num1 = 1
장점
Compile time에 오류를 발견할 수 있다.
IDE의 도움을 받을수 있다.
val num1: Int = 1 // 타입을 기술
val num2 = 2 // 타입을 생략
var : mutable variable (var)
val : immutable value (val)
var num1: Int = 1
val num2: Int = 2
num1 = 2
num2 = 1 // ERROR!
-> 둘 다 사용 가능하나, 함수형 프로그래밍(functional programming)의 특성을 살려 immutable하게 val을 사용하자
val n: Int = 1
String str = null;
if (num > 10) str = "bigger";
else str = "smaller";
var str = "";
if (num > 10) str = "bigger";
else str = "smaller";
val str = if (num > 10) "bigger" else "smaller"
statement : 컴퓨터가 수행하는 명령 단위
expression : 값을 생성하거나 리턴.
-> 모든 expression은 항상 같은 값을 리턴하므로, 값으로 치환하거나 합성 가능하다. 이는 대수방정식을 풀때처럼 함수적으로 프로그래밍이 가능하다는 것을 의미한다.
Scala에서는 definition을 제외한 모든 식이 expression이다 (statement가 존재하지 않는다)
for (int item : list) {
System.out.println(item);
}
for (item <- list) {
print(item)
}
val list = List(1,2,3)
val increased = for (item <- list) yield (item + 1)
// increased = List(2,3,4)
num match {
case 1 => "one"
case 2 => "two"
case _ => "many" // switch-case의 default와 같은 역할을 한다
}
def myFunc1(n: Int): Int = {
n + 1
}
def myFunc2(n:Int, s: String): String = {
s + n
}
return을 명시해도 되지만 따로 하지 않아도 마지막 값이 리턴된다.
Java의 method 선언과 달라서 헛갈릴 수 있지만,
Scala의 선언문은 변수 선언이나 함수 선언 둘 다 구조가 동일하다는 점을 기억하면 쉽다.
변수 선언과 마찬가지로 리턴 타입을 생략할 수 있다.
val a = 10 // 리턴 타입 Int 생략함
def b(n: Int) = n + 1 // 리턴 타입 Int 생략함
def func() = {
def innerFunc(n: Int): String = n.toString
innerFunc _ // 함수를 리턴
}
func()
의 리턴 값이 생략되어 있는데, 리턴 값은 무엇일까?def func(): Int => String = { ... }
Int => String
란 Int를 인자로 받아서 String을 반환하는 함수 타입이란 의미이다.
아래의 function type에 대해 생각해보자.
String => Int
Int => Unit
(Int, String, Double) => Int
Int => (Int, String)
def b(n: Int) = n + 1
무명 함수로 정의해보자
(num: Int) => num + 1
=>
기호를 살펴보자. 함수를 표현하는데 왜 =>
기호를 사용할까.def func(): Int = throw Exception
a = func() // ERROR 발생.
b => func() // 이 시점에서는 ERROR가 발생하지 않는다.
=>
는 =
와 같은 할당 연산자에서 연산전략만이 다르다는 것을 표현한다.=
를 사용하는 a의 경우 바로 연산하고,=>
를 사용하는 b의 경우 나중에 연산한다. 연산전략이 다르다는 얘기는 사실이지만 기호에 대한 내용은 추측이므로, 이해를 돕기위한 참고로만 사용하자;
def func(a: Int): Int => Int =
(b: Int) => a + b
Int => Int
도 Java로 실행가능한 bytecode로 변환해야 할텐데,Int => Int
은 객체로 어떻게 표현될까.public Function<Integer, Integer> func(int a) {
return (b) -> a + b;
}
interface Function<T, R>
이다.def func(a: Int) = new Function1[Int, Int] {
def apply(b : Int) : Int = a + b
}
Int => Int
는 사실 Function1[Int, Int]
의 축약형이다.
def func(a: Int) = {
(b: Int, c: Int, d: Int, e: Int) => a + b + c + d + e
}
def func(a: Int) = new Function4[Int, Int, Int, Int, Int] {
def apply(b: Int, c: Int, d: Int, e: Int) : Int = {
a + b + c + d + e
}
}
String => Int
-> Function1[String, Int]
(Int, String, Double) => Int
-> Function3[Int, String, Double, Int]
-> compile time에 FunctionN으로 변환되며, 우리가 직접적으로 FunctionN을 쓸 일은 없다. 참고만 해두자.
(num: Int) => num + 1
val a = (num: Int) => num + 1
def a = (num: Int) => num + 1
<-
=>
=>
=>
이 기호는 할당 연산자 =
와 관계가 깊어서 =
기호에서 변형되었음을 설명했다.<=
이런 기호를 사용하지 않는지도 이해될 것이다. =>
를 case문에 동일하게 사용하나? num match {
case 1 => "one"
case 2 => "two"
case _ => "many"
}
val case1: PartialFunction[Int, String] = {
case n : Int if n == 1 => "one"
}
val case2: PartialFunction[Int, String] = {
case n : Int if n == 2 => "two"
}
val matchFunc = case1 orElse case2
items.map(item => item.id) // 이렇게도 표현하지만
items.map { case Item(id, name) => id } // 이렇게 사용하는 경우가 많다.
def sum(a: Int, b: Int) = a + b
def inc(a: Int) = sum(a, 1)
val inc = sum(1, _: Int)
(Int, Int) => Int
Int => Int
은 Function1의 새로운 함수로 리턴해준다.inc(10) // 11
def func(a: Int, b: Int) = a + b
public static Function<Integer, Function<Integer, Integer>> add() {
return new Function<Integer, Function<Integer, Integer>>() {
@Override
public Function<Integer, Integer> apply(final Integer x) {
return new Function<Integer, Integer>() {
@Override
public Integer apply(Integer y) {
return x + y;
}
};
}
};
}
public static Function<Integer, Function<Integer, Integer>> add() {
return x -> y -> x + y;
}
def add(a: Int): Int => Int = (b: Int) => a + b
def add(a:Int) =
(b: Int) =>
(c: Int) =>
(str: String) => (a + b + c).toString + str
-> Int => Int => Int => String => String
def add(a: Int)(b: Int)(c: Int)(str: String): String = (a + b + c).toString + str
def sum(a: Int, b: Int) = a + b
val inc = sum(1, _: Int)
def sum(a: Int)(b: Int) = a + b
val inc = sum(1)
def func(n: Int): String = {
n.toString
}
(n: Int) => n.toString
Int => Int
String => Unit
(Int, String) => Int
Int => Int => Double => String
def sum1(a: Int, b: Int) = a + b
def sum2(a: Int)(b: Int) = a + b
sum1(1, _)
sum2(1)
class User {
private Integer id;
private String name;
public User(Integer id, String name) {
this.id = id;
this.name = name;
}
public void sayHello() { System.out.println("hello"); }
}
class User(id: Int, name: String) {
def sayHello(): Unit = println("hello")
}
val user = new User(1, "name")
user.id // ERROR
val
을 추가한다. class User(val id: Int, val name: String)
case class User(id: Int, name: String)
val user = User(1, "name")
user.id // OK
new User(1, "name") == new User(1, "name") // true
val copied = user.copy()
copied == user // true
class Item {
public Integer id;
public Brand brand;
public Item(Integer id, Brand brand) {
this.id = id;
this.brand = brand;
}
}
class Brand {
public Integer id;
public BrandCategory brandCategory;
public Brand(Integer id, BrandCategory brandCategory) {
this.id = id;
this.brandCategory = brandCategory;
}
}
class BrandCategory {
public Integer id;
public String name;
public BrandCategory(Integer id, String name) {
this.id = id;
this.name = name;
}
}
new Item(1, new Brand(1, new BrandCategory(1, "a")))
case class User(id: Int, name: String)
val user1 = User(1, "kim")
val user2 = User(2, "lee")
user1 match {
case User(1, _) => "find user 1"
case User(id, "lee") => "find user with name 'lee'" + id
case _ => "other"
}
val item1 = Some(Item(1, Brand(1, BrandCategory(3, "item1 category name"))))
val item2 = Some(Item(2, Brand(2, BrandCategory(4, "item2 category name"))))
def printOnlyBrand1Category(item: Option[Item]) = ???
printOnlyBrand1Category(item1) // item1 category name
printOnlyBrand1Category(item2) // empty
printOnlyBrand1Category(None) // empty
object Item {
val ERROR_CODE = "500"
}
Item.ERROR_CODE
object Item {
val ERROR_CODE = "500"
private val TYPE_1 = "TYPE_1"
private val TYPE_2 = "TYPE_2"
}
case class Item(id: Int, tpe: String) {
def getType(): Int = {
tpe match {
case Item.TYPE_1 => 1
case Item.TYPE_2 => 2
}
}
}
Item.ERROR_CODE
//Item.TYPE_1 // ERROR
trait Shape {
def w: Int
def h: Int
def show(): String = s"w=${w}, h=${h}" // 일부 구현 가능
}
왜 interface에서는 구현이 불가능하게 되어 있었는지 생각해보자.
Diamond problem이라 불리는 문제 때문이었는데
이와 같은 경우 trait에서는 하나의 trait만 main trait만을 상속하고, 다른 trait는 mix-in함으로서 이 문제를 해결한다.
mix-in은 정확히는 상속하는 것이 아니라, 다른 코드를 포함한다는 의미에 가깝다.
결과적으로 하나의 trait만 상속하게 되어 이러한 문제를 해결하게 되는데,
따라서 여러 extends A, B, C 와 같이 표현하는 것이 아니라,
extend A with B with C 와 같이 표현하게 된다.
trait A {
def func: String
}
trait A1 extends A {
override def func: String = "A1"
}
trait A2 extends A {
override def func: String = "A2"
}
class A1A2 extends A1 with A2
class A2A1 extends A2 with A1
val a1a2 = new A1A2
val a2a1 = new A2A1
a1a2.func // A2
a2a1.func // A1
class User(id: Int, name: String)
case class는 내용 비교가 가능하며, pattern matching에 유용하다.
case class User(id: Int, name: String)
new User(1, "name") == new User(1, "name") // true
user match {
case User(id, "lee") => "find user with name 'lee'" + id
case _ => "other"
}
Object는 싱글톤 객체이며, 동일한 이름의 class를 companion으로 가질 수 있다.
Trait은 다중 상속을 가능하게 해주며, 자바의 interface와 달리 내부 구현이 가능하다. (정확히는 다중 상속처럼 보이게 해준다)
val num: Int = null // 잘못된 코드!
val num1: Option[Int] = None
val num2: Option[Int] = Some(1)
val num: Option[Int] = None
// 1. get
val a: Int = num.get // ERROR!!
// 2. if를 사용한다
if (num.isDefined)
// 오류는 아니지만, 가능한 get을 사용하지 않는 코드로 작성하자
num.get.toString
else
"No Value"
// 3. match-case를 사용한다.
num match {
case Some(n) => n.toString
case None => "No Value"
}
// 4. fold를 사용한다.
num.fold("No Value")(n => n.toString)
def process(maybeId: Option[Int]): Option[Item] =
maybeId match {
case Some(id) => Some(itemService.get(id))
case None => None
}
def process(maybeId: Option[Int]): Option[Item] =
maybeId.map(id => itemService.get(id))
val nums: List[Int] = List(1,2,3,4)
val emptyList = Nil
// iteration
// 특별한 경우가 아니라면 foreach를 사용하지 말자
nums.foreach(n => print(n)) // 1234
nums.map(n => n * 2) // List(2,4,6,8)
// filtering
nums.filter(n => n % 2 == 0) // List(2, 4)
// 정렬한다.
nums.sorted // List(1,2,3,4)
// 다음과 같이 정렬 방법을 지정해줄 수도 있다.
nums.sortBy(n => n * -1) // List(4,3,2,1)
// 사이즈를 구한다
nums.size // 4
// 조합해서 사용가능하다
nums.filter(n => n % 2 == 0).map(n => n * 2).sum // 12
// nums가 빈 리스트라면 Error가 발생한다. 아래처럼 사용하지 말자.
nums.reduceLeft((sum, n) => sum + n)
// 대신 초기값을 줄수 있는 fold를 사용하자.
nums.foldLeft(0)((sum, n) => sum + n) // nums.sum과 동일하다
val added = 0 :: nums // List(0,1,2,3,4)
val concat = nums ::: List(5,6,7) // List(1,2,3,4,5,6,7)
// nums가 빈 리스트라면 Error가 발생한다. 아래처럼 사용하지 말자.
nums.head // 1
// 대신 headOption을 사용한다.
nums.headOption // Some(1)
// nums가 빈 리스트라면 Error가 발생한다. 아래처럼 사용하지 말자.
nums(2) // 3
// 대신 drop을 사용하자.
nums.drop(2).headOption // Some(3)
// nums가 빈 리스트라면 Error가 발생한다. 아래처럼 사용하지 말자.
nums.last // 4
nums.lastOption // Some(4)
nums.tail // List(2,3,4)
case class Item(id: Int, name: String, price: Int, tpe: String)
val items = List(
Item(1, "아메리카노", 5000, "COUPON"),
Item(2, "아이스크림 케이크", 20000, "COUPON"),
Item(3, "교촌치킨", 15000, "COUPON"),
Item(4, "맥 립스틱", 20000, "DELIVERY"),
Item(5, "라이언인형", 10000, "DELIVERY"),
)
// 1. 모든 상품의 가격을 가져와서 0.1을 곱한 목록을 만들어보자
// 2. 쿠폰인 상품만 가져와 보자
// 3. 가격 순으로 정렬해보자
// 4. 모든 상품을 다음 Data 타입으로 바꿔보자
case class Data(id: Int, name: String, commission: Int)
// 5. 모든 쿠폰 상품들을 Data 타입으로 변환하고, 가격순으로 정렬해보자.
val values = (1, "str", true, 5)
val list: Seq[Any] = List(1, "str", true, 5)
val first: Option[Any] = list.headOption
val tuple: (Int, String, Boolean, Int) = (1, "str", true, 5)
val num: Int = tuple._1
val str: String = tuple._2
val sorted: Seq[(Item, Int)] = items
.sortBy(_.price)
.zipWithIndex
sorted.map {
data => {
val item = data._1
val index = data._2
(index, item.name)
}
}
sorted.map { case (item, index) => (index, item.name) }
class Tuple<A, B, C> {
A a;
B b;
C c;
public Tuple(A a, B b, C c) { ... }
}
new Tuple<Int, String, Boolean>(1, "str", true)
case class Tuple3[+T1, +T2, +T3](_1: T1, _2: T2, _3: T3)
val map = Map((1, "a"), (2, "b"), (3, "c"))
val map = Map((1 -> "a"), (2 -> "b"), (3 -> "c"))
val map = Map((1, "a"), (2, "b"), (3, "c"))
map.get(1) // Some("a")
map.getOrElse(4, "not found") // "not found"
map.contains(4) // false
map.map { case (key, value) =>
s"키=${key}/값=${value}"
}
// List(키=1/값=a, 키=2/값=b, 키=3/값=c)
abstract class GenMapFactory(...) {
def apply[A, B](elems : scala.Tuple2[A, B]*) = { ... }
}
->
표 기호 또한 실제로는 Tuple의 alias이다.def ->[B](y : B) : scala.Tuple2[A, B] = { ... }
val num: Option[Int] = Some(1)
if (num.isDefined) { // 이렇게도 할수 있지만
Some(num.get * 2)
} else {
None
}
이 값이 있는지 없는지 확인하는 일은 우리의 관심사가 아니다.
우리에 관심사에만 주목해보자.
val num: Option[Int] = Some(1)
num.map(n => n * 2)
val num: List[Int] = List(1,2,3)
num.map(n => n * 2)
val num: Future[Int] = Future.successful(1)
num.map(n => n * 2)
val num: F[Int] = ???
num.map(n => n * 2)
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
val f: Future[List[Int]] = ???
case class Item(id: Int, name: String, categoryId: Int)
case class Category(id: Int, name: String)
def getItem(id: Int): Future[Item]= ???
def getCategory(id: Int): Future[Category] = ???
// 상품의 카테고리를 어떻게 가져올수 있을까?
def getItemCategory(id: Int) = ???
def getItemCategory(id: Int): Future[Category] = {
getItem(id).map(
item => getCategory(item.categoryId)
)
// Future[Future[Category]] 타입이 달라 에러 발생!
}
def getItemCategory(id: Int): Future[Category] = {
getItem(id).map(
item => getCategory(item.categoryId)
).flatten
}
def getItemCategory(id: Int): Future[Category] = {
getItem(id).flatMap(
item => getCategory(item.categoryId)
)
}
case class User(id: Int)
case class Order(id: Int, itemId: Int)
case class Item(id: Int)
def getUser(userId: Int): Future[User] = ???
def getOrders(user: User): Future[Order] = ???
def getItem(itemId: Int): Future[Item] = ???
def getUserItems(userId: Int): Future[Item] = ???
def getItemCategory(id: Int): Future[Category] = {
for {
item <- getItem(id)
category <- getCategory(item.categoryId)
} yield category
}
많은 도움이 되었습니다 :)