ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [스칼라 문법편] CH04 스칼라의 OOP
    24년 11월 이전/레거시-누구나 쉽게 스칼라+플레이 2019. 1. 29. 22:08
    반응형

    * 이 포스팅은 책 "누구나 쉽게 스칼라 + 플레이 - 고락윤 한빛미디어" 를 읽고 정리한 것입니다.

    CH04 스칼라의 OOP


    스칼라에서는 모든 것이 객체입니다. 이는 Java보다 훨씬 객체지향적인 특성을 지닌 말과도 일맥상통합니다. 사실 이전에 말씀드렸던 Akka 라이브러리의 경우 FP보다는 액터 모델을 기반으로 완전한 OOP로써 동시성을 해결하는 라이브러리입니다. 이제부터 우리는 스칼라의 객체 지향 프로그래밍을 지원하는 개념, 키워드들을 알아볼 것입니다. 키워드부터 말씀드리자면, class, object, trait 입니다.

    01. class


    기존, C++, Java, C# 등 OOP를 지원하는 프로그래밍 언어의 경험이 있다면 이미 이해하고 있을 개념입니다. 책에서는 OOP를 다음과 같이 한 줄로 소개하고 있습니다.

    "프로그래밍에서 쓰이는 요소들의 공통적인 부분을 묶어놓고 하나의 정체성을 부여하는 프로그래밍 방식"


    OOP에서 가장 많이 쓰이는 키워드 중 하나인 class는 일종의 붕어빵 틀이라고 생각하시면 됩니다. 붕어빵 틀을 제작해 붕어빵을 계속해서 찍어내는 것이죠. 여기서 붕어빵은 '객체'라고 말할 수 있습니다. 스칼라는 기존 자바와 다르게 필드 선언이 훨씬 효율적입니다. 이전 Person 클래스를 기억하시나요? 스칼라에서는 필드를 () 안에 정의하는 것이 가능합니다. 이렇게 말이죠.

    class Person(name: String, age: Int)


    Person 클래스의 객체는 이렇게 생성할 수 있습니다.

    val p = new Person("Jang", 28)


    다만, 이 때 필드에 대한 게터/세터 toString 등의 메소드를 구현하지 않기 때문에 쓸 수가 없습니다. 여기서 잠깐, 우리는 이전에 썼던 Person 클래스는 뭔가 다르지 않았었나요? 맞습니다 case란 키워드를 붙이면 언어 자체에서 게터/세터 toString() 등, 기본적인 것들이 만들어서 제공됩니다. 이렇게 말이죠.

    case class Person(name: String, age: Int)
    
    val p1 = Person("Jang", 28)
    println(s"${p1.name} ${p1.age}")


    case class의 장점은 개발자가 게터/세터, toString 메소드를 오버라이딩을 따로 하지 않아도 된다는 장점이 있습니다. 이외에도 위의 코드에서 확인할 수 있듯 new 키워드를 제거할 수 있습니다. 또한 패턴 매칭에 case 키워드가 이용됩니다. 이에 대한 설명은 추후에 하도록 하겠습니다. 또한 스칼라는 class 생성시 default 파라미터를 넘겨줄 수 있습니다. 무슨 뜻이냐면 다음 코드를 보시죠.

    case class Person(name: String="Gurumee", age: Int = 28)
    
    val p2 = Person()
    println(s"${p2.name} ${p2.age}")


    이 코드를 실행하면, 우리가 Person 객체를 생성할 때 아무것도 넘겨주지 않았음에도 불구하고 객체 p2에는 name, age 필드들이 초기화가 되어 있는 것을 볼 수 있습니다. 이는 클래스 키워드 외에도 함수 선언시에도 디폴트 파라미터 인자를 선언할 수 있다는 것을 알아주시면 좋겠습니다. 또한, 클래스는 부모 클래스로부터 특성을 상속받을 수 있습니다. 예를 들어보겠습니다.

    case class Person(name: String, age: Int)
    
    case class Rich(name: String, age:Int, money: Int) extends Person(name, age)


    이 경우 Person 클래스를 Rich 클래스가 상속하고 있는 것입니다. Rich는 사람의 name, age 특성 외에 돈이라는 money 라는 특성을 더 가지고 있습니다. case class의 경우 부모의 필드/메소드 등을 잘 상속 받았기 때문에, 잘 동작하지만, 커스토마이징을 원할 경우, oveerride 키워드를 이용해서 메소드를 재정의하면 됩니다. 또한 스칼라는 Java의 OOP의 특성을 잘 이어 받았기 때문에 추상 클래스 abstract 키워드를 지원합니다. 이는 3절 trait와 같이 보도록 하겠습니다.

    02. object


    object 키워드는 프로그램에서 관리하는 전역적인 객체라고 생각하시면 편합니다. 유식한 말로는 싱글톤 패턴이 적용된 클래스라고 보시면 됩니다. object 키워드로 클래스를 만들어두면 main이 실행될 때, 이 object 이름으로 단 하나의 객체가 만들어집니다. 덕분에 이렇게 만들어진 오브젝트 객체는 프로그램 내 모든 곳에서 접근이 가능합니다. 따라서, 기존 Java에서 static 필드/메소드 들이 여기에 정의해두고 사용한다고 생각하시면 됩니다. 하나의 예는 우리가 자주 작성했던 Main 오브젝트가 있습니다.

    object Main{
        def main(args: Array[String]) = {
    
        }
    }


    여기서 Main 오브젝트는 프로그램이 실행될 때 이미 메모리에 올라간 후 main 함수를 찾아서 실행하는 것입니다. 만약 Main에 필드 혹은 메소드를 추가한다면, 다른 코드에서 Main.Filed, Main.Method() 등으로 접근할 수 있습니다.

    동반 객체(Companion Object)

    object 키워드에서 중요한 것은 한 가지가 더 있습니다. 바로 동반 객체라는 개념인데, class, object를 같은 이름으로 설정할 수 있습니다. 이 때, 같은 이름의 오브젝트와 클래스는 서로의 private 필드를 접근할 수가 있습니다. 이것이 무슨 소리냐, 코드를 살펴보도록 하겠습니다.

    case class Person(private val name: String, 
                      private val age: Int)
    
    object Person{
        def getPersonInfo(p: Person) = (p.name, p.age) 
    }
    
    //main
    val p = Person("Gurumee", 28)
    
    //println(p.name, p.age) 컴파일 에러남 왜? private 필드에 접근하니까
    println(Person.getPersonInfo(p)) //에러 안남 동반객체의 힘!


    코드를 보시면 case class의 필드들이 모두 private으로 바뀌었습니다. 실제로 이렇게 될 경우 게터/세터가 막힙니다. 근데, object Person의 경우는 class Person의 private 필드, 메소드의 경우에도 자유롭게 접근이 가능합니다. 이것이 바로 동반 객체입니다.

    03. trait


    trait는 기존 Java 유저가 잘 이해할 수 있도록 정의하자면, "추상 클래스와 인터페이스의 중간인 녀석" 이라고 할 수 있겠습니다. 먼저 추상 클래스부터 살펴보도록 하죠.

    abstract class Car(tires: Int){
        abstract def open
        abstract def close
      
        def move() = println("부릉 부릉")
    }


    추상클래스는 이처럼 필드/메소드를 가질 수 있고, 메소드의 구현을 자신을 상속하는 하위 클래스에게 위임할 수 있습니다. 다만, abstract 클래스는 절대 자기 자신 자체로 생성할 수는 없습니다. 즉 이런 코드는 불가능한 것이죠.

    val c = new Car(4) //컴파일 에러


    이제 인터페이스에 대해서 알아봅시다. 인터페이스는 일종의 명세인데, 함수 원형만 가지고 있는 녀석입니다. 스칼라에는 없고 자바에만 있는 개념이죠. 자바 8 이전에는 인터페이스 내 에서 메소드 구현이 불가능하였습니다.(자바 8 이후에는 default 키워드를 사용하여 인터페이스 내에서 메소드 구현이 가능합니다) 또한, 필드 역시 static 필드 밖에 가지지 못하죠. 

    trait는 추상 클래스와 인터페이스의 중간 지점인. 스칼라의 새로운 개념입니다. trait 역시, 추상 클래스나 인터페이스와 마찬가지로 자기 자체로써는 인스턴스 생성이 불가능합니다. 다만, 필드/메소드를 가질 수 있고 메소드 구현 여부도 개발자가 원하는대로 할 수 있습니다. 한 가지 예를 살펴보죠.

    trait Flying {
        def fly = println("푸드덕 푸드덕")
    }
    
    trait Running {
        def run = println("다다다다다다다다다다다닷!")
    }
    
    trait Swimming {
        def swim = println("헤엄~ 헤엄~")
    }
    
    trait Eating {
        def eat
    }
    
    class FlyingWhale extends Flying with Swimming with Eating{
        override def eat: Unit = println("와구와구 쩝쩝")
    }
    
    //main
    val fw = new FlyingWhale
    fw swim;
    fw fly;
    fw eat


    코드를 보시면 날으는 고래는 3개의 트레이트를 상속받습니다. Swimming, Flying, Eating 이죠. 근데 Eating 은 현재 메소드 구현을 위임한 상태입니다. 따라서, FlyingWhale 은 eat 매소드를 override해야 합니다. 이런 특성만 보면, 아 인터페이스에서 조금 더 진화된 형태네라고 하실 수 있겠습니다. 그러나 트레이트는 믹스인과 트레이트 쌓기라는 기능을 제공하여 보다 유연하고 좋은 코드를 만들어낼 수 있습니다.

    믹스인

    믹스인은 추상 클래스와 트레이트를 섞어서 쓰는 것을 뜻합니다. 위의 코드에서 추상 클래스 Animal을 추가합시다.

    //trait는 동일
    abstract class Animal {
      def shout
    }
    
    class FlyingWhale extends Animal with Flying with Swimming with Eating{
      override def eat: Unit = println("와구와구 쩝쩝")
      override def shout: Unit = println("우아아아아아아아아~~~~~~~~앙")
    }
    
    //main
    
    fw swim;
    fw fly;
    fw eat;
    fw shout


    간단한 코드라서, 장점은 보이진 않지만, 코드가 복잡하면 복잡해질 수록 abstract class 와 trait 가 수도 없이 많이 생성될 것입니다. 이 둘이 만약 경쟁 상대라면, 굉장히 힘든 코딩이 되겠지만, 스칼라에서 이들은 보완관계입니다. 믹스인은 무엇보다 잠시 후에 알아볼 트레이트 쌓기의 기본이 되는 기능입니다.

    트레이트 쌓기

    이번에는 트레이트 쌓기라는 기능을 알아봅시다. 보통 프로그래밍 언어에서 다중 상속은 위험한 코드라고 지칭합니다. 왜냐 같은 특성의 이름이 있을 경우, 여러 개를 상속하는 하위 클래스에서 어떤 것을 써야 할지 몰라 에러가 나기 때문입니다. 스칼라에서는 이를 트레이트 쌓기로 훌륭하게 해결합니다. 코드를 살펴보도록 하겠습니다.

    abstract class Weapon {
        def shoot = println("Kong!")
    }
    
    trait Missile extends Weapon {
        override def shoot = println("피융~~~~ 쾅")
    }
    
    trait MachineGun extends Weapon {
        override def shoot = println("두두두두두두두두두두")
    }
    
    trait Blaster extends Weapon {
        override def shoot = println("콰과과과광!")
    }
    
    class Gundam extends Missile with MachineGun with Blaster
    
    //main
    val g = new Gundam
    g shoot


    이 때 출력 값은 어떻게 될까요? 기본적으로 트레이트가 쌓이는 것은 가장 나중에 쌓인 것으로 그 메소드의 구현부를 덮어씌웁니다. 그래서 결론적으로 Blaster의 shoot 메소드가 호출됩니다. 이번에는 코드를 약간 바꿔보겠습니다.

    abstract class Weapon {
      def shoot = println("콩콩")
    }
    
    trait Missile extends Weapon{
      override def shoot = {
        super.shoot
        println("피유우~~~~~웅!")
      }
    }
    
    trait MachineGun extends Weapon {
      override def shoot = {
        super.shoot
        println("두두두두두두두두두!")
      }
    }
    
    trait Blaster extends Weapon {
      override def shoot = {
        super.shoot
        println("콰과과과과과광!!!!")
      }
    }
    
    class Gundam extends Missile with MachineGun with Blaster


    이렇게 하면 어떻게 될까요? 이렇게 하면 쌓인 순서대로 Weapon -> Missile -> MachineGun -> Blaster 순으로 출력됩니다. 만약 3개의 트레이트는 순서가 변경되면, 출력 형식도 그에 맞춰 변경됩니다. 이것이 바로 트레이트 쌓기의 묘미이지요. 한 가지 주의할 점은 trait를 쌓을 때 가장 꼭대기에 추상 클래스가 존재해야 한다는 것입니다. 그렇지 않으면 이 코드는 에러가 뜨게 됩니다.

    마지막으로, 정리하는 개념으로 추상 클래스, trait, interface(Java8 이전)를 비교해보도록 하겠습니다.

    -추상 클래스트레이트인터페이스
    메소드 구현 가능OOX
    다중 상속 가능XOO
    필드 선언 가능OOX
    인스턴스 생성 가능XXX


    이렇게해서 스칼라의 OOP적 특성에 대해서 알아보았습니다. 고생하셨습니다! 어떠신가요? ㅎㅎ 스칼라에 대해 저와 함께 더 공부하고 싶으시다면 다음 장 함수에서 만나도록 합시다~!

    728x90
Designed by Tistory.