Scala

Java의 Builder Pattern과 Scala의 Default Parameter

partner_jun 2017. 5. 28. 14:47

1. Java에서의 Builder Pattern

자바에서 빌더 패턴(Builder Pattern)은 두 가지 경우에 주로 사용된다. 


1) 생성자 파라미터가 많거나 선택적인 생성자 파라미터가 필요할 때

생성자 파라미터가 많은 경우 빌더 패턴을 적용하면 훨씬 사용하기 편해진다. 특히 생성자에서 선택적으로 사용되는 파라미터가 필요한 경우에는 선언해야 하는 생성자의 수가 많아지게 되는데, 최악의 경우 파라미터 조합의 수만큼 생성자가 필요해진다. 이런 상황에 빌더 패턴을 이용하면 선언이 필요한 생성자의 수를 크게 줄일 수 있다.


2) 파라미터에 기본 값이 필요할 때

위 경우와 개념적으로는 같다. 자바는 파라미터의 기본 값이 지원되지 않기 때문에 기본 값을 사용한다고 가정하면 오버로드된 메소드들(혹은 생성자)이 필요해진다. 그런 경우 빌더 패턴을 사용함으로써 오버로드된 메소드를 선언하지 않을 수 있다.



빌더 패턴을 적용한 간단한 예제는 아래와 같다.

public class Fruit {

private String name;
private String color;
private int price;

// 생성자를 private로 선언해 객체를 직접 생성하지 못하게 한다.
private Fruit(String name, String color, int price) {
this.name = name;
this.color = color;
this.price = price;
}

/**
* Fruit Builder를 만들어 리턴하는 메소드
*
* @return Fruit Builder
*/
public static FruitBuilder builder(){
return new FruitBuilder();
}

@Override
public String toString() {
return name + " / " + color + " / " + price;
}

/**
* Fruit builder 클래스.
*/
private static class FruitBuilder {

// 생성자에 사용될 기본 값
private String name = "Banana";
private String color = "Yellow";
private int price = 2000;

/**
* 이름 설정 메소드.
*
* @param newName 새로운 이름
* @return FruitBuilder 객체
*/
public FruitBuilder name(String newName) {
this.name = newName;
return this;
}

/**
* 색상 설정 메소드.
*
* @param newColor 새로운 색상
* @return FruitBuilder 객체
*/
public FruitBuilder color(String newColor) {
this.color = newColor;
return this;
}

/**
* 가격 설정 메소드.
*
* @param newPrice 새로운 가격
* @return FruitBuilder 객체
*/
public FruitBuilder price(int newPrice) {
this.price = newPrice;
return this;
}

/**
* Fruit 객체를 생성하는 메소드.
*
* @return 만들어진 Fruit 객체
*/
public Fruit build() {
return new Fruit(name, color, price);
}
}

}


public static void main(String[] args) {

Fruit fruit = Fruit.builder()
.name("Apple")
.color("Red")
.price(1500)
.build();

Fruit fruit2 = Fruit.builder()
.price(5000)
.build();

System.out.println(fruit); // Apple / Red / 1500
System.out.println(fruit2); // Banana / Yellow / 5000

}



위와 같은 형태의 빌더 패턴의 적용을 간단한 순서로 나타내자면


 1) 객체의 생성자를 private로 선언함해 객체를 직접 생성하지 못하게 한다. 

 2) static nested class로 빌더 클래스를 선언한다.

 3) 빌더 클래스에 각 파라미터를 입력받고 빌더 클래스 객체를 리턴하는 메소드를 작성한다.

 4) 빌더 클래스에 객체를 생성하고 리턴하는 build() 메소드를 작성한다.

 5) 객체 클래스에 빌더 객체를 만들어 리턴하는 builder() 메소드를 작성한다. 


 ※ 객체 클래스에서 builder() 메소드를 호출해 빌더 클래스 객체를 얻어낸 후, 각 메소드로 값을 설정하고 마지막에 build() 메소드를 호출해 객체를 얻어낸다.





보일러플레이트가 굉장히 부담스럽다. 하지만 lombok을 사용하면 @Builder 어노테이션을 통해 엄청나게 간단히 빌더 패턴을 적용할 수 있다.

@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@ToString(includeFieldNames = false)
public class Fruit {

// 기본 값 설정
@Builder.Default private String name = "Banana";
@Builder.Default private String color = "Yellow";
@Builder.Default private int price = 2000;

}


public static void main(String[] args) {

Fruit fruit = Fruit.builder()
.name("Apple")
.color("Red")
.price(1500)
.build();

Fruit fruit2 = Fruit.builder()
.price(5000)
.build();

System.out.println(fruit); // Fruit(Apple, Red, 1500)
System.out.println(fruit2); // Fruit(Banana, Yellow, 5000)

}

lombok은 선택이 아닌 필수다.


코드가 굉장히 짧아졌다. 자바9에는 이런 어노테이션들이 기본으로 추가된다니 기대해보자.




2. Scala의 Default Parameter

위에서 본 빌더 패턴은 스칼라에서도 똑같이 적용되고 사용할 수 있다. 하지만 스칼라는 생성자나 함수의 파라미터에 기본 값을 지정할 수 있어 빌더 패턴이 자바에서만큼 유용하진 않다. 기본 값을 지정해두고 파라미터를 생략하거나 파라미터 이름을 입력해 파라미터의 순서도 마음대로 바꿀 수 있기 때문이다.


기본 값을 적용한 스칼라 코드는 아래와 같다.

case class Fruit(name: String = "Banana",
color: String = "Yellow",
price: Int = 2000)

val fruit = Fruit("Apple", "Red")
val fruit2 = Fruit(price = 3000)
val fruit3 = Fruit(color = "Gold", price = 5000)

println(fruit) // Fruit(Apple,Red,2000)
println(fruit2) // Fruit(Banana,Yellow,3000)
println(fruit3) // Fruit(Banana,Gold,5000)

case class 선언(생성자)에 기본 값을 설정한 예




이렇게 스칼라로 선언한 '기본 값'이 정해져 있는 클래스나 함수는 자바에서도 쓸 수 있다.

def printSomethingParam1(str1: String = "Hello",
str2: String = "World") = println(s"$str1 $str2")

스칼라에서 '기본 값'이 있는 함수 선언


public static void main(String[] args) {

DefaultValueScala.printSomethingParam1(
// 자바에서는 자동으로 생성된 $default$가 붙은 메소드를 파라미터로 사용하면 된다.
DefaultValueScala.printSomethingParam1$default$1(),
DefaultValueScala.printSomethingParam1$default$2()
);

}

스칼라로 선언한 '기본 값'이 정해져 있는 printSomething 함수를 자바에서 사용하는 코드


스칼라로 선언한 '기본 값'이 정해진 함수를 자바에서 호출하면 $default$가 붙은 메소드 몇 개가 추가된 것을 확인할 수 있다. 이 메소드는 스칼라에서 선언된 '기본 값'을 리턴하는 메소드다. 이 메소드의 반환 값을 인자로 넘김으로써 스칼라에서 선언해 둔 '기본 값'을 사용할 수 있다.


javap -c *.class 명령어를 이용해 자바 바이트코드를 보면 $default$가 붙은 메소드가 String을 반환하는 static 메소드임을 알 수 있다.




파라미터 목록이 두 개 이상인 경우는 조금 다르다. 두 번째 값에 해당하는 메소드가 추가적인 파라미터를 입력받는다. 

def printSomethingParam2(str1: String = "Hello")
(str2: String = "World") = println(s"$str1 $str2")

스칼라로 파라미터 목록이 두 개인 함수를 선언했다.


DefaultValueScala.printSomethingParam2(
DefaultValueScala.printSomethingParam2$default$1(),
DefaultValueScala.printSomethingParam2$default$2("???") // Hello World
// 파라미터 목록이 두개인 경우에는 파라미터를 입력받는다.
// 이 파라미터는 뭘까?
);



이 파라미터는 뭘까? 새로운 함수를 통해 확인해 보자.

def printSomethingParam3(str1: String = "Hello")
(str2: String = str1 + " World!") = println(s"$str2")

스칼라에서 새로운 함수를 선언. 첫 번째 파라미터 str1를 두 번째 파라미터 str2의 기본 값에서 사용한다.


DefaultValueScala.printSomethingParam3(
DefaultValueScala.printSomethingParam3$default$1(),
DefaultValueScala.printSomethingParam3$default$2("???") // ??? World!
);

DefaultValueScala.printSomethingParam3(
"Hello",
DefaultValueScala.printSomethingParam3$default$2("!!!") // !!! World!
);

새로 선언한 함수를 사용하는 자바 코드. 첫 번째 파라미터를 무시한다!



첫 번째 파라미터가 무시되고 두 번째로 호출된 메소드의 결과만 출력된다. 스칼라 코드를 직접 컴파일하면 왜 이렇게 되는지 알 수 있다.


scalac - print *.scala 명령어로 스칼라 코드를 컴파일하면서 출력하게 하면 이런 결과를 볼 수 있다.


printSomethingParam3(str1, str2) 함수는 스칼라 코드에서 파라미터 목록이 두개인 함수였지만 컴파일되며 파라미터 목록이 하나로 합쳐졌다. 그리고 스칼라 코드에서 두 번째 파라미터에 해당하는 $default$2 함수파라미터로 문자열을 입력받고 그 문자열과 "World!"를 합쳐 리턴하는 함수로 만들어졌다.


이를 통해 스칼라에서 선언한 '기본 값'이 있는 함수나 클래스는 자바에서 사용할 때 주의가 필요함을 알 수 있다. 스칼라 코드를 직접 확인하지 않는다면 이 함수에서 의미있는, 무시되지 않는 파라미터가 무엇인지 알 수 없기 때문이다.