본문 바로가기
Scala

Scala Trait

by dev여름 2025. 2. 22.
반응형

목차

1. trait의 개념 및 주요 특징

2. trait 기본사용법

    2-1. trait 정의하기

    2-2. trait 기본 사용

3. Java Interface와 비교

4. trait의 선형화

5. 추상클래스와 trait 

 

trait의 개념 및 주요 특징

객체지향 프로그래밍에서 Interface와 유사한 역할을 한다.

인스턴스화 할 수 없다.

인터페이스와 다르게 메소드와 필드를 포함할 수 있다.

다중상속은 불가능하지만 여러 trait을 섞어서 사용하는 mixin이라는 개념이 존재한다.

생성자를 가질 수 없다(Scala2 버전만 해당. 3버전은 생성자를 가질 수 있으나, 주의해야함 참고문서)

 

trait 기본 사용법

trait 정의하기 

trait Example {
    //추상메소드
    def exampleMethod1(): Int
    
    //구현된 메소드
    def exampleMethod2(): Unit = {
        println("initialized")
    }
    
    //추상 필드
    val immutableField1: String
    var mutableVField1: String
    
    //값이 정해진 변수
    val immutableField2: String = "immutableField2"
    var muntableField2: String = "mutableField2"
    
    //접근제한자
    private def exampleMethod3(): Unit = {
        println("private method can use only in the tarit")
    }
    
    protected def exampleMethod4(): Unit = {
        println("protected method")
    }
}

trait 사용하기 (매우 기본)

trait을 상속받을때는 extends 키워드를 사용한다.

그 다음 사용하는 trait들 부터는 with이라는 키워드를 사용하는데, mixin이 된다고 보면된다.

trait Sound {
    //추상메소드
    def makeSound(): Unit
}
 
trait Fly {
    //구현된 메소드
    def fly(): Unit = println("flying!")
}
 
trait Eat {
    def eat(): Unit = println("eating")
}

//Sound, Fly, Eat의 기능을 모두 갖고 있는 Bird
class Bird extends Sound with Fly with Eat
 
val bird = new Bird
bird.makeSound()  // Chirp Chirp
bird.fly()        // flying!
bird.eat()       // eating

 

Java와 차이점

1. 구현된 함수 정의

Java는 8버전 이상부터 default 키워드를 이용하면 인터페이스에 구현된 함수를 정의할 수 있다.

interface Animal {
    defualt public void makeSound(String x) {
        System.out.println("Sound : " + x);
    }
}

Scala는 특별한 키워드 없이 정의 가능하다

trait Animal {
    def makeSound(x: String): Unit = {
        println("Sound : " + x)
    }
}

2. protected 접근제한자 사용

Java의 인터페이스에서는 protected 접근제한자를 사용할 수 없지만, 스칼라는 가능하다.

//불가능
interface Animal {
    protected String getName();
}
//가능
trait Animal {
    protected getName(): String
}

3. 필드 지정

Java의 인터페이스에서는 필드를 정의할 수 없지만, 스칼라는 가능하다.

//불가능
interface Animal {
    String name;
}
//가능
trait Animal {
    var name: String
}

4. 키워드 사용

자바는 인터페이스 구현 시 Implements 키워드를 사용하지만

interface A
interface B
interface C
 
class D implements A, B, C

스칼라는 extends와 with을 사용한다.

trait A
trait B
trait C
 
class D extends A with B with C

자바에서는 클래스를 상속할때 extends키워드를 사용하지만 스칼라는 extends 키워드는 클래스와 trait을 구분하지 않는다.

 

trait의 선형화

C++등 다중상속이 가능한 언어에서는 죽음의 다이아몬드(the Deadly Diamond of Death)문제가 발생할 수 있다.

     A
    / \
   B   C
    \ /
     D

클래스 B,C가 A를 상속받고, 클래스 D가 B,C를 상속받은 상황에서

D가 super.message()를 출력하는 경우 어떤 클래스의 message()를 실행해야 할까?

일반적인 상황이라면 이런 경우 에러가 나며 실행이 불가능하다.

 

Scala에서는 클래스 다중상속을 막고 있지만 trait의 선형화를 통해 죽음의 다이아몬드 문제를 자연스럽게 해결하고 있다.

trait A {
  def message(): String = "A"
}

trait B extends A {
  override def message(): String = "B -> " + super.message
}

trait C extends A {
  override def message(): String = "C -> " + super.message
}

class D extends B with C {
  override def message(): String = "D -> " + super.message
}

object Main extends App {
  val d = new D
  println(d.message) // 출력 결과: "D -> C -> B -> A"
}

trait에서는 가장 오른쪽에 선언된 trait순서대로 실행되도록 한다.

이 코드의 선형화 순서는

1. D -> 출력, C가 가장 오른쪽에 선언되었으므로 C의 message 메소드가 실행된다.

2. C -> 출력, B가 오른쪽에서 두번째에 선언되었으므로 B의 message 메소드가 실행된다.

3. B -> 출력, A의 message 메소드가 실행된다. A는 B와 C클래스에서 두번 상속받았지만 선형화 과정에서 한번만 호출된다.

 

추상클래스와 trait

Scala에도 abstract class도 존재하는데, trait과 매우 유사하다는 생각이 들었다.

그럼 둘 중 어떤걸 써야 하는거지? 라는 생각이 들때 좀 찾아보니 이런 답변들이 있었다.

abstract class를 사용할 때

1. Java 코드에서 상속받아야 할때 (많이 혼용하기 때문)

2. Java 컴파일러를 사용해야 할 때

3. 생성자가 필요할때 (Scala 2버전 사용하는 경우)

trait을 사용할 때

1. 한 클래스에서 독립된 여러 행동들을 하는 경우

2. abstract class를 써야 할지 trait을 써야할지 모르겠는 경우, trait을 먼저 써보고 안되면 abstract class로 바꾸면 된다.

반응형