devthewild

Coursera - EPiS 후기

Scala 3(a.k.a Dotty)의 업데이트와 함께 새로운 스칼라 입문 코스, Effective Programming in Scala가 코세라에 올라왔다. 소개 영상에 의하면 전제조건은 다른 프로그래밍 언어의 경험이 어느 정도 있을 것, 목표는 스칼라로 업무가 가능한 정도까지이다. 스칼라 입문이지 프로그래밍 입문이 아닌만큼 기본 개념에 대한 설명은 생략하고 다른 언어들에서 쓰이는 개념들은 스칼라에서 어떻게 쓰는지, 함수형으로는 어떻게 같은 논리를 구현하는지에 대해 초점이 맞춰있고 스칼라2에서는 어떻게 썼는지에 대해 차이점도 소개한다. 수업을 들으면서 정리를 좀 남기긴 했지만 스칼라 문법에 대한 이야기를 굳이 요약하기보다 수업을 따라 좋은 설명을 듣기를 추천하고 스칼라 2사용자들에게 유용한 내용들만 추려보겠다.

변경점

indent-based syntax - 1주차

일단 가장 큰 변화는 들여쓰기 문법을 도입하면서 중괄호({})를 쓸 필요가 없어졌다는 것이다. 1주차 수업부터 조건문을 설명하면서

1
2
3
4
if condition then
expression
else
expression

처럼 여러 줄의 표현식이 있을 때 중괄호없이 표기하는 예시를 보여준다.

imperative-loop - 2주차

지금까지 for문(for comprehension)을 쓸 때, flatMap, map, withFilter 등으로 변환된다고 알고 있었는데 여기에 foreach로 변환되는 문법이 하나 추가되었다. for … do인데

1
2
3
4
5
for
x <- exp1
do f(x)
// is equivalent to
exp1.foreach(x => f(x))

yield처럼 값을 반환하지 않고 실행만 하는 명령식 반복문(imperative loop)이 생겼다.

package-object - 3주차

탑레벨(top-level) 변수들이 허용되어서 굳이 패키지 객체가 필요없긴 하지만 기능도 사라졌다.

imports - 3주차

import 문에서 몇가지 변화가 생겼다. 일단은 패키지 내의 멤버 전부를 가져오는게 import root.from.to._였는데 이제는 import root.from.to.**을 사용하게 되었다. 아직 하위호환으로 _도 사용할 수 있다.

그리고 이름을 변경하여 가져올 때 import from.to.{Pkg => P}였다면 이제는 as라는 키워드를 사용해 import from.to.{Pkg as P}처럼 쓰면 된다.

새로운 given 키워드와 새로운 문법상 맥락이 생기며 given 변수들은 .*로 가져올 수 없으니 given을 한번에 가져오려면 import from.to.given을 쓰거나 given을 포함한 다른 멤버들도 한번에 가져오려면 import from.to{given, *}처럼 사용하면 된다.

Program Entry Point - 3주차

예전에는 Java처럼 main(args: Array[String]) 메소드가 있는 Object들을 진입점들로 찾았다면 이제는 @main이 붙은 메소드들이 모두 진입점이 될 수 있다. 그리고 인자로 @main def run(name: String, n: Int)같은 식의 타입을 받으면 받은 인자들을 순서대로 저 타입 변환을 하는데 맞지않으면 실행이 되지 않는다.

Opaque Types - 3주차

예전에도 데이터의 일관성을 위해 타입을 다른 이름으로 바꾸거나 다른 타입의 인자에 넣거나 trait를 붙여서 구분하는 등의 방식들이 존재했는데, 그 중에서 실행 시점에서 추가적인 리소스를 소모하지 않는 방법은 type alias가 있었지만 원래 타입으로 변환이 가능해서 type UserID = Long, type GroupID = Long이면 두 타입을 혼용하거나 원래 타입과 구분할 수 없다는 단점이 있었고 이걸 해결하기 위해 opaque type이라는 기능이 도입되었다.

1
2
3
4
5
6
7
8
object UserID:
opaque type UserID = Long
def parse(string: String) = string.toLongOption
extension (id: UserID)
def value(id: UserId): Long = id

def find(id: UserID): Option[User] =
... (id.value)

이렇게 타입에 이름을 붙이고, 원 타입과의 변환은 선언된 스코프 내에서만 가능해서 type alias와 다르게 안전하게 사용할 수 있다.

Extension Method - 3주차

위의 예제에서 id.valuevalue 멤버가 없는 opaque type에 접근한 것처럼 타입을 확장할 수 있는 기능이다. 예전에는 암묵적 변환(implicit conversion)을 통해 다른 클래스로 변환하고 그 클래스의 메소드를 실행하는 방식이었는데, import를 통해 extension을 가져올 수 있고 특수한 경우로 UserID처럼 opaque type에 연결되어있으면 그 opaque type만 import하면 가져올 수 있다.

Given - 5주차

예전에도 Context Bound라는 타입 연산자(:)가 있었고, 풀어서 쓰자면

1
2
3
def g[A : B](a: A)
// is equivelent to
def g[A](a: A)(implicit ev: B[A])

처럼 맥락에 해당하는 묵시적 변수를 implicit으로 표기했는데 이제는 모든 묵시적 행동에 쓰이던 implicit이라는 키워드가 사라지고 이런 용도로는 using으로 쓴다. 그리고 using에서 자동으로 가져오기 위한 변수를 선언하는 키워드는 given이 되었다.

1
2
3
4
5
object Ordering:
given IntOrd: Ordering[Int] with
def compare(x: Int, y: Int) = ...
given Ordering[Double] with
def compare(x: Double, y: Double) = ...

이렇게 해당 given에 이름을 붙일 수도 생략할 수도 있고, given intOrdering: Ordering[Int] = IntOdering처럼 given이 아닌 변수지만 메소드를 제공한다면 given 변수에 할당해서 사용할 수도 있다.

또한 implicitly라고 문맥상 스코프에 존재하는 묵시적인 변수를 가져오는 함수는 이제 summon으로 쓴다. summon[Ordering[Int]]처럼 부르는데, 이것도 implicitly처럼 미리 선언된 함수이다.

given의 경우 다음과 같은 방식으로 가져올 수 있다

  • 이름: import Ordering.Int
  • 타입: import Ordering.{given Ordering[Int]}
  • 타입*: import Ordering.{given Ordering[?]}
  • 전부: import Ordering.given

T 타입의 given은 다음과 같은 순서로 찾는다

  1. 접근할 수 있는 given 인스턴스들
  • 상속받았거나 import했거나 스코프 안에서 정의된 변수들
  1. T와 관련된 컴패니언 객체를 통해서
  • ‘관련된’의 의미는
    • T 자체의 컴패니언 객체
    • T가 상속하는 타입들의 컴패니언 객체
    • T에 있는 타입 인자들의 컴패니언 객체
    • T가 inner class라면 바깥쪽 스코프의 객체

given a: Agiven b: B보다 더 구체적이다, 라는 말은

  • a가 b보다 가까운 스코프에 있다
  • b가 정의된 클래스의 스버클래스 안에. a가 있다
  • a가 b의 서브타입이다
  • A 타입이 B 타입보다 더 “고정된” 부분이 있다.
    • Ordering[Int]Ordering[?]보다 더 고정되어 있다.

유용한 내용

sbt에 대한 설명은 아주 기본적인 것이나 아주 깊은 내용 아니면 찾기 어려워서 기본적으로 내부에서 사용하는 개념에 대해 정리된 자료를 찾기 힘든데, 3주차의 “sbt, Keys, and Scopes” 챕터에서 sbt 내에서 쓰이는 중요한 개념인 KeyTask, 그리고 Scope에 대해 잘 설명해준다.

Coursera - FPPiS 후기, 그리고 테스트

coursera.org의 functional programming principle in scala의 진행이 끝났다. functional programming이라는 생소한 개념을 배우면서 재미있기도 했고, 강의를 시작하기 직전에 봤던 '자바 개발자를 위한 함수형 프로그래밍'를 읽으면서 제대로 알지 못했던 개념을 좀 더 확실하게 배웠다. 물론 강의와 서적 모두 '입문'에 대한 것이라 의미에 대해 알려면 더 많은 경험이 필요하겠고, 서적과 강의가 지향하는 바가 조금 달라서 내용도 다르지만 서적을 보면서 제대로 이해하지 못했던 개념을 강의를 보면서 이해하는데 도움이 되었고 기초가 있는 상태에서 다시 서적을 읽으면 더 이해가 잘 될 것 같다.

사실 강의를 읽으면서 도움이 된 것은 functional programming에 대한 이해보다 다른 것이다. 물론 functional programming에 대한 기초를 쌓는데도 무척 도움이 되었지만, 그것보다 과제를 진행하는데 있어서 '테스트'의 중요성에 대한 인식이 생겼다.

과제를 진행하는 방식은 이렇다.

  1. 테스트 케이스가 주어진다.
  2. 작성해야하는 로직이 제외된 코드가 주어진다.
  3. 단계별로 로직을 구현하면서 테스트를 통해 작성된 로직이 유효한지 확인한다.
  4. 이전 단계에서 구현한 것을 기반으로 다음 단계의 로직을 작성한다.

물론 주어진 테스트 케이스들 중에서 좋은 테스트 케이스만 있는 것이 아니다. 스펙에서 요구되었지만 테스트에서 확인하지 않은 케이스(정렬된 배열을 가져와야하는데 그 종류와 갯수만 확인하는 경우)는 다음 단계의 테스트를 통과하지 못한다. 그럴 경우에는 테스트에 성공한 코드로 돌아가서 수정을 해야하므로 좋은 테스트라고 볼 수 없다. 이런 식으로 좋은 테스트가 적성되어있는 경우에는 그 테스트를 통과하면 다시 그 코드를 수정할 필요가 없고, 성능상의 문제가 있는 경우에는 그 코드를 수정하고 다시 테스트를 통과하면 다른 코드들을 건들이지 않아도 된다.

여기에 대한 전제가 중요하긴 하다. 앞서 이야기했듯이 '좋은(명확한) 테스트가 작성되어 있을 것', 그리고 '확실히 단계별로 구분된 설계가 있을 것'이다. 이 전제들이 참 어렵지만, 반대로 생각하면 이 전제들이 확실한 경우가 있다. 바로 내가 겪었던 것과 같이 '과제'를 작성하는 경우다. 커다란 과제가 주어졌을 경우에 어디서부터 어떻게 접근해야할지 명세서를 봐도 막막한 경우가 있는데 그런 경우에 bottom-up으로 제대로 된 설계에 따라 테스트를 하나씩 거치면서 확실히 눈에 보이는 진척을 확인할 수 있다. 물론 실무에 적용하려면 저 전제들을 만족하기 참 힘들겠지만 말이다. 혹은 퍼포먼스의 이슈가 없거나 종속성이 적은 코드들의 경우에 신입사원들에게 연습시키는 용도로는 확실할 수 있다.

예전에 일하던 회사에서 '시켜서' 억지로 작성한 테스트들이 떠오르며 (사실 잘 떠오르지도 않지만) 부끄러워졌다. 사실 TDD라고 하지만 아직도 TDD가 뭔지 잘 모르겠고, 이해하지 못한 상태에서 작성한 테스트들은 당연히 좋은 테스트일리가 없었다. 이렇게 실무에서 어떻게 테스트를 더 잘 적용시킬 수 있는지에 대해 듣고 싶어서 저번달에 모 컨퍼런스에 다녀왔는데 그냥 '테스트는 의지를 가지고 해야 한다.'라는 말만 반복해서 하더라. 하긴 누군가가 말해줘도 그걸 체득하기는 어렵다. 계기와 경험이 중요한데 나는 일단 이 강의라는 계기를 얻었으니 앞으로 경험을 쌓도록 노력해야지.

참고로 80점 만점에서 60%(48점)만 얻으면 인증을 얻는데 과제를 너무 늦게 시작하는 바람에 1주차에 80% 감점을 받고 시작한 것을 합쳐서 69점으로 통과했다. 만점을 목표로 한 것에 비하면 부끄럽지만 인증을 받은 것만으로도 일단은 만족. 그리고 스칼라 스터디에 한번 참여해보고 싶었는데 영어 실력이 부족하여 참여할만큼 이해한 강의가 없어서 한번도 참여를 못한 것 두가지가 아쉽다.