반응형

실무에서 아직 jdk1.6을 사용하지만 최신 자바에 대해 공부하는 중 가장 컸던 변화라 생각한 Stream에 대해 정리하고자 한다.

 

Stream

Stream은 자바8부터 추가된 기능으로 컬렉션, 배열과 같은 자료구조를 함수형 인터페이스(람다)를 적용하며 처리하는 기능입니다. 즉, 데이터를 담고 있는 자료구조(컬렉션)이 아닙니다.

스트림은 '데이터의 흐름'이며 적절한 operation을 통해 데이터를 가공해 원하는 타입으로 제공합니다.

 

자바8 이전에는 배열이나 컬렉션을 다루기 위해 for 또는 foreach를 통해 하나씩 꺼내서 다뤘습니다. 간단한 경우는 큰 문제가 없었지만 로직이 복잡해지면 코드의 양은 많아지고 향후 유지보수에도 어려움이 발생합니다.

 

아래 코드는 기존 for문을 사용한 방법과 stream을 사용한 방법의 예시입니다.

import java.util.*;
public class StreamExample{

     public static void main(String []args){
        List<String> names = new ArrayList<>();
        names.add("seungwoo");
        names.add("sw");
        names.add("jsw");
        names.add("java8");
        
        // 자바 8 이전 for문을 사용한 방법
        for (int i=0; i<names.size(); i++) {
            if (names.get(i).startsWith("s")) {
                System.out.println(names.get(i).toUpperCase());
            }
        }

        // stream을 사용한 방법
        names.stream().filter(name -> name.startsWith("s"))
                .map(name -> name.toUpperCase())
                .forEach(System.out::println);
     }
}

위 코드에서 stream을 이용한 방법을보면 메서드 체이닝을 통해 연산을 합니다. 이러한 오퍼레이션은 중개 오퍼레이션, 종료 오퍼레이션 2가지가 존재합니다.

 

중개 오퍼레이션

 

  • Stream을 리턴함
  • Lazy하다
  • filter, map, limit, skip, sorted, flatmap...

여기서 lazy하다는 말은 종료 오퍼레이션이 나오기 전까지는 실행하지 않는 것을 뜻합니다. 즉 결과가 필요하기 전까지 연산의 시점을 최대한 늦추는것을 뜻합니다.

List<String> names = new ArrayList<>();
        names.add("seungwoo");
        names.add("sw");
        names.add("jsw");
        names.add("java8");
        
// list의 내용을 바꾸지 않고 단지 stream으로 가지고 있음
// 중계 operation, 종료 operation이 있음
// 중계 operation는 기본적으로 lazy한데 중계 operation은 stream을 리턴하며 종료 operation은 stream이 아닌 다른타입을 리턴함.
Stream<String> stream = names.stream()
	.map(String::toUpperCase);

이처럼 종료 오퍼레이션이 없는경우 연산을 하지 않습니다.

 

종료 오퍼레이션

  • Stream을 리턴하지 않음
  • collect, allMatch, count, forEach, min, max...
List<String> names = new ArrayList<>();
        names.add("seungwoo");
        names.add("sw");
        names.add("jsw");
        names.add("java8");

long count = names.stream()
		.filter(name -> name.startsWith("s"))
		.count();
System.out.println(count);

종료 오퍼레이션이 있는경우 연산이 실행됩니다.

 

 

반응형
반응형

 최근 코딩테스트를 진행하면서 정규표현식과 관련된 문제가 나와 다시 한번 정리해 보기로 결심했다. 

정규표현식이란 특정 규칙을 가진 문자열을 표현하기 위해 쓰이는 식입니다. 개발을 진행하면 보통 주민등록번호, 휴대폰 번호, 이메일주소와 같이 정해져 있는 규칙이 있고 사용자가 형식에 맞게 입력했는지 검증할때 주로 사용합니다.

사실 기존에 정규표현식을 사용할 일이 있으면 구글링을 통해 정규표현식을 확인하고 사용해서 규칙을 정확히 숙지하고 있지 않았습니다. 근데 코딩테스트에서 검색없이 진행하려니 많은 어려움이 있었습니다.

 

아래는 정규표현식의 기본 작성법입니다.

자바에서는 이러한 정규표현식을 사용하기 위해 Pattern과 Matcher클래스를 사용합니다. 간단한 예제를 통해 살펴보겠습니다.

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {
    public static void main(String[] args) {

        // test를 위한 임시 String 배열
        String[] names = {"seungwoo", "sw", "jsw"};

        // 소문자 sw로 시작하는 단어
        Pattern pattern = Pattern.compile("sw[a-z]*");

        for (String name : names) {

            // 정규식으로 비교할 대상을 매개변수로 Pattern 클래스의 matcher 메서드를 호출해 인스턴스를 얻음
            Matcher matcher = pattern.matcher(name);

            // 정규식에 부합한지 체크
            if (matcher.matches()) {
                System.out.println(name);
            }
        }
    }
}

 

결과

 

Pattern클래스의 compile메서드를 통해 정규표현식으로부터 패턴을 만듭니다. 이후 패턴에 맞는 Matcher 객체를 pattern.matcher 메서드를 통해 반환하고 정규식에 부합한지 체크하는 과정입니다.

 

Matcher클래스의 matches 메서드는 정규식에 부합하면 true, 부합하지 않으면 false를 반환합니다.

Pattern과 Matcher 클래스의 더 상세한 메서드는 API를 통해 쉽게 확인할 수 있습니다.

 

정규표현식의 사용법은 간단하나 정규표현식 기본규칙을 암기하고 있지 않으면 이러한 문제를 만났을때 당황할 것 같습니다.

사실 모든걸 외울 수는 없으나 기본적인 정규표현식 규칙은 암기하고 있어야 할 것 같습니다.

 

반응형
반응형

Jsoup을 이용한 웹 크롤링 간단 예제 입니다.

Jsoup을 사용하기 위해서는 의존성을 받아야 하는데 maven repository에서 받으면 됩니다.

 

https://mvnrepository.com/

 

Maven Repository: Search/Browse/Explore

Infinispan Hot Rod Client Last Release on Jun 15, 2020

mvnrepository.com

저는 네이버 증권 페이지에서 진행하겠습니다. 네이버 증권 페이지를 들어가고 개발자 도구에 들어가서 코스피 지수를 확인합니다.

 

코스피 지수의 태그를 선택하고 개발자 도구에서 ...을 클릭하면 그림과 같이 css selector를 클립보드로 복사 할 수 있습니다.

여기서 저는 코스피 지수와 변동지수만 가져오도록 하겠습니다.

 

KospiCrawer

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;

public class KospiCrawer {
    private static final String CONNECT_URL ="https://finance.naver.com/";
    
    // 현재 코스피지수
    public String getKospi() throws Exception {
        Document document = Jsoup.connect(CONNECT_URL).get();
        Elements kospi = document.select("#content > div.article > div.section2 > div.section_stock_market > div.section_stock > div.kospi_area.group_quot.quot_opn > div.heading_area > a > span > span.num");
        return kospi.text();
    }

	// 코스피 변동지수
    public String getKospiChangeRate() throws Exception {
        Document document = Jsoup.connect(CONNECT_URL).get();
        Elements kospiChangedRate = document.select("#content > div.article > div.section2 > div.section_stock_market > div.section_stock > div.kospi_area.group_quot.quot_opn > div.heading_area > a > span > span.num2");
        return kospiChangedRate.text();
    }
}

 

copy selector로 복사한 것을 select메서드를 활용해 가져왔습니다.

 

Main

public class CrawlingApplication {

    public static void main(String[] args) throws Exception {
        KospiCrawer kospiCrawer = new KospiCrawer();
        String currentKospi = kospiCrawer.getKospi();
        String changeKospiRate = kospiCrawer.getKospiChangeRate();
        System.out.println("현재 코스피 : " + currentKospi + " 변동율 : " + changeKospiRate);
    }

}

 

결과

이처럼 Jsoup을 사용하면 간단하게 크롤링을 할 수 있다. 하지만 크롤링을 이용해 상업적으로 이용한다면 문제가 있을 수 있으니 주의하도록 하자.

반응형
반응형

Comparable, Comparator 모두 자바에서 정렬을 할 때 사용하고 있습니다. 하지만 이 2가지는 명확한 차이가 존재합니다.

2가지 모두 정렬할 때 비교해 객체의 순서를 결정하는 역할을 합니다. 이 2가지 인터페이스를 사용하면 Collections.binary.Search, Collections.binarySearch, Collections.max, Collections.min, Collections.sort, Arrays.binarySearch, Arrays.sort같은 순서를 결정하는 메소드를 사용할 수 있습니다.

결론적으로 Comparable과 Comparator의 차이는 다음과 같습니다.

 

* Comparable - 기준을 설정하고 정렬. 즉, 특정한 기준 1가지를 가지고 정렬 Ex) 학점을 기준으로 오름차순 or 내림차순

* Comparator - Comparable과 다르게 요구사항에서 주어진 특정 기준을 가지고 정렬 Ex) 학점이 같다면 이름순으로 정렬

 

그럼 Comparable과 Comparator의 2가지 차이점을 알아보겠습니다.

 

 

- Comparable

Comparable은 compareTo 메서드를 오버라이드 합니다. 

Comparable을 구현한 클래스는 기본적으로 오름차순을 기준으로 정렬을 수행합니다. 또한 1가지 기준점을 가지고 그에 대해 정렬을 하고 싶을 때 사용합니다.

 

- Comparator

Comparator는 compare 메서드를 오버라이드 합니다.

Comparator를 구현한 클래스는 일반적인 정렬기준을 넘어 사용자 정의 조건을 규칙으로 정렬을 하고 싶을 때 사용합니다.

 

 

정리하면 Comparable은 기본적인 정렬위주일 때 사용하고 특정한 규칙을 사용해 정렬을 하고 싶을 때는 Comparator를 사용하면 됩니다.

반응형
반응형

두 메소드 모두 Object의 값을 String으로 변환하지만 변경하고자 하는Object가 null인 경우 다르다.
toString()과 같은 경우 Null PointerException(NPE)을 발생시키지만 valueOf는 "null"이라는 문자열로 처리한다.

즉 비교해서 정리하자면
  • String.valueOf() - 파라미터가 null이면 문자열 "null"을 만들어서 반환한다.
  • toString() - 대상 값이 null이면 NPE를 발생시키고 Object에 담긴 값이 String이 아니여도 출력한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
destItemMap.get("LOWER_VAL") 이 null 일 경우
String lowerCoatingVal1 = String.valueOf(destItemMap.get("LOWER_VAL"));
String lowerCoatingVal2 = destItemMap.get("LOWER_VAL").toString();
 
lowerCoatingVal1 = "null"
lowerCoatingVal2 = NullPointerException 발생
 
String.valueOf()의 null 체크
String lowerCoatingVal1 = String.valueOf(destItemMap.get("LOWER_VAL"));
if("null".equals(lowerCoatingVal1)) {
    // To do Somting....
}
 
// equals함수를 사용할때 왼쪽에 있는 것을 기준으로 비교하기 때문에 변수보다는 문자열을 왼쪽에 두는 것을 추천한다.
// 즉 strTestVal이 null인 경우 ret = "1"인 if문은 NPE를 발생시킨다.
String strTestVal = null;
String ret = "";
 
/* Exception 발생 */
if!(strTestVal .equals("")) ) ret = "1";
 
/* 정상 */
if!("".equals(strTestVal)) ) ret = "2";
 
cs

다시 말해 두가지 메서드의 차이점은 null값에 따른 NPE의 발생 유무이다.

이런 차이점 때문에 valueOf의 null체크 방법은 "null".equals(string) 형태로 체크를 해야한다.

null로 인해 발생된 에러는 시간이 지나고, 타인의 소스인경우 디버깅하기 어렵고 어떤의미를 내포하고 있는지 판단하기 어렵다. 때문에 NPE를 방지하기 위해 toString보다는 valueOf를 사용하는 것을 추천한다.


반응형
반응형

자바 8부터 지원하는 람다식에 대해서 알아보도록 하겠습니다.



/**
* Lambda
* 람다는 익명 함수라고 할 수 있는데 자바에서 익명 클래스가
* 이름없이 정의되어 사용될 수 있듯이 함수도 이름없이 사용되는 형태를 뜻합니다.
* 간단히 말해 메소드를 하나의 식으로 표현한 것입니다.
*
* int max(int a, int b) {
* return a>b ? a : b;
* }
*
* 와 같은 메소드를 람다식으로 다음과 같이 표현할 수 있습니다.
* (a,b) -> a>b ? a : b;
*
* 이러한 람다 표현식은 메소드의 매개변수로 전달될 수도 있으며, 메소드의 결과값으로 반환될 수도 있습니다.
* 즉, 람다식을 사용하면 기존의 불필요한 코드를 줄여주고, 가독성을 높여줄 수 있어
* 자바8부터 람다식을 사용하여 함수형 프로그래밍을 할 수 있게 되었습니다.
*
* 람다식의 기본 작성 방법
* (매개변수) -> {메소드 내용...}
* 람다식 작성에는 몇가지 주의해야 할 사항들이 있습니다.
* 1. 매개변수의 타입을 추론할 수 있다면 생략이 가능합니다.
* 2. 매개변수가 단 하나라면 괄호는 생략할 수 있습니다.
* 3. 메소드 몸체가 하나의 명령문이라면 중괄호를 생략할 수 있으며 이때는 세미콜론을 붙이지 않습니다.
* => 이때 몸체의 명령문이 return문이라면 생략할 수 없습니다.
* 4. return 대신 표현식을 사용할 수 있으며 이때 반환값은 표현식의 결과값이 됩니다.
*
* 아래 쓰레드 생성방식을 기존 람다식을 사용하지 않던방법과
* 람다식을 사용한 방법을 비교하면서 더 알아보도록 하겠습니다.
* 예제에서 보는 것과 같이 람다식을 사용하면 가독성이 훨씬 좋아지며,
* 코드를 줄일 수 있다는 장점을 알 수 있습니다.
*/
public class LambdaExam extends Thread{
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread 생성...(람다식 사용하지 않음)");
}
}).start();

new Thread(()-> {
System.out.println("Thread 생성... (람다식 사용)");
}).start();
}
}
/**
* 함수형 인터페이스
* 람다 표현식을 사용할 때는 람다 표현식을 저장하기 위한 참조 변수의 타입을 결정해야 합니다.
* 참조변수의 타입 참조변수 이름 = 람다 표현식
* 위와 이 사용하는데 람다 표현식을 하나의 변수에 대입할 때 사용하는 참조 변수의 타입을 함수형 인터페이스라고 칭합니다.
* 함수형 인터페이스는 추상 클래스와 달리 단 하나의 추상 메소드만을 가져야 하며
* @FunctionalInterface 어노테이션을 사용하여 함수형 인터페이스임을 명시할 수 있습니다.
*/
@FunctionalInterface
interface LambdaTest {
public int min(int a, int b);
}

public class LamdaExam2 {
public static void main(String[] args) {
// LambdaTest 함수형 인터페이스(추상 메소드)의 구현
LambdaTest minNum = (x, y)-> x < y ? x : y;
// 함수형 인터페이스의 사용
System.out.println(minNum.min(1,6));
}
}


/**
* 메소드 참조는 람다 표현식이 단 하나의 메소드만을 호출하는 경우에 해당 람다식에서 불필요한 매개변수를 제거하고 사용할 수 있도록
* 해줍니다. 메소드 참조를 사용하면 불필요한 매개변수를 제거하고 '::'기호를 사용하여 표현할 수 있습니다.
* ---------------------------------------- 문 법 ----------------------------------------
* 1. ClassName::MethodName
* 2. 참조변수이름::메소드이름
* ----------------------------------------------------------------------------------------
* 두가지 방법으로 사용가능 하며 예제를 통해 살펴보도록 하겠습니다.
*/
public class LamdaExam3 {
public static void main(String[] args) {
//사용 방법
DoubleUnaryOperator oper;
oper = (n) -> Math.abs(n);
System.out.println(oper.applyAsDouble(-5));

oper = Math::abs;
System.out.println(oper.applyAsDouble(-5));
}
}


반응형
반응형

Stop the world

GC를 실행하기 위해 JVM이 어플리케이션 실행을 멈추는 것이다. stop-the-world가 발생하면 GC를 실행하는 쓰레드를 제외한 나머지 쓰레드는 모두 작업을 멈춘다. 그리고 GC작업을 완료한 이후에 작업을 다시 시작한다. 대개 GC튜닝이라 하면 stop-the-world시간을 줄이는 것이다.

즉, Stop the world란 GC가 더이상 필요 없는 객체를 찾아 지우는 작업을 하기위해 GC를 실행하는 쓰레드를 제외한 나머지 쓰레드가 멈추는 것을 의미한다.


Stop the world를 공부하면서 GC의 2가지 영역을 알게되었다. 이름과 마찬가지로 최근 생긴 객체는 Young, 오래된 객체는 Old영역에 위치하게 된다.


Young 영역(Yong Generation 영역)

 새롭게 생성한 객체의 대부분이 여기에 위치한다. 대부분의 객체가 금방 접근 불가능 상태가 되기 때문에 매우 많은 객체가 Young 영역에 생성되었다가 사라진다. 이 영역에서 객체가 사라질때 Minor GC가 발생한다고 말한다.


Old 영역(Old Generation 영역)

 접근 불가능 상태로 되지 않아 Young 영역에서 살아남은 객체가 여기로 복사된다. 대부분 Young 영역보다 크게 할당하며, 크기가 큰 만큼 Young 영역보다 GC는 적게 발생한다. 이 영역에서 객체가 사라질 때 Major GC(혹은 Full GC)가 발생한다고 말한다.


반응형
반응형

[ 이글은 Effective Java를 참고하여 작성하였습니다 ]



생성자를 따로 만들지 않으면 컴파일러는 자동으로 인자가 없는 public 기본 생성자를 만든다. 사용자는 이 생성자를 일반 생성자와 구별할 수 없으므로 원래 의도와는 달리 객체 생성이 가능한 클래스로 될 위험이 존재한다. 또한 객체를 만들 수 없게 하기 위해 abstract(추상) 클래스로 선언해 봤자 하위 클래스를 정의하는 순간 객체 생성이 가능해 지기 때문에 무용지물이다. 


이런 경우에는 private 생성자를 클래스에 넣어서 객체 생성을 방지할 수 있다. 명시적으로 정의된 생성자가 private이므로 클래스 외부에서 사용하는 것은 불가능 하기 때문이다.

public class UtilityClass {

     // 기본 생성자가 자동 생성되지 못하도록 하여 객체 생성 방지

     private UtilityClass() {

          throw new AssertionError();

     }

}


이와 같이 구현한다. 이때 AssertionError는 꼭 필요하지는 않지만 클래스 안에서 실수로라도 생성자를 호출했을 때 알기 위해 한 것이며, 생성자를 명시적으로 구현했음에도 불구하고 호출할 수 없다는 것이 직관적이지 않으므로 위에 보인 것처럼 주석을 달아두는 것이 좋다.



반응형
반응형

[ 이 글은 Effective Java를 참조하여 작성되었습니다. ]


 싱글턴 패턴에 대해서 모르시는 분은 먼저 싱글턴 패턴에 대해 공부하고 오시는 것이 좋습니다.

 싱글턴은 간단하게 객체를 하나만 만들 수 있는 클래스 입니다. 때문에 보통 유일할 수 밖에 없는 컴포넌트를 나타내곤 하죠. 하지만 클래스를 싱글턴으로 만들면 테스트하기가 어려울수도 있습니다. 싱글턴 패턴을 구현하는 방법은 크게 2가지가 있습니다. 두 방법 모두 생성자는 private이고 객체는 static멤버를 이용합니다.

 첫번째 방법은 정적 멤버를 final로 선언하는 것입니다. 

public class Sw{

     public static final Sw INSTANCE = new Sw();

     private Sw(){ ... }

     메소드...

}


와 같은 방식이죠. private 생성자는 INSTANCE를 초기화 할 때 단 한번만 호출됩니다. public 또는 protected로 선언된 생성자가 없기 때문에 오로자 하나만 존재하게 되는거죠. 


 두번째 방법은 정적 팩토리 메서드를 사용하는 것입니다. 

public class Sw{

     private static final Sw INSTANCE = new Sw();

     private Sw() { ... }

   

     public static Sw getInstance() { return INSTANCE; }

}


와 같은 방식입니다. 여기서 getInstance는 항상 같은 객체만을 return 해줍니다. 이 두가지 방법은 JDK 1.5이전에 주로 사용하던 방법이기 때문에 최적의 방법은 아닐 수도 있습니다. 왜냐하면 최신 Java Virtual Machine은 정적 팩토리 메서드를 항상 인라인 처리하기 때문입니다. 하지만 이 정적 팩토리 메서드를 사용하는 방식은 2가지의 장점이 존재합니다.


(1) API를 변경하지 않고도 싱글턴을 포기할 수 있다는 것입니다.


싱글턴 객체를 참조하는 유일한 필드가 private이므로 이를 지우고 getInstance의 구현을 변경해도 API는 영향을 받지 않겠죠.


(2) 제네릭 타입을 수용하기 쉽다.


간단히 설명하자면 자료형 유추가 가능하고, 형 안정성을 제공할 수 있다는 것이다. 하지만 이 장점이 필요 없는 경우도 많습니다. 

이럴 때는 public 필드를 사용하는 쪽이 더 간단합니다.



그렇다면 JDK 1.5 이후에는 어떤 방법을 사용하는지 알아봅시다.

바로 원소가 단 하나뿐인 enum 자료형을 정의하는 방법입니다.

public enum Sw{

     INSTANCE;



 이와 같은 방법은 기능적으로는 public 필드를 사용하는 방법과 동등하지만 좀 더 간결하다는 것과 직렬화가 자동으로 처리된다는 장점이 있다. 또한, enum은 인스턴스가 여러 개 생기지 않도록 확실하게 보장해주며 유일한 INSTANCE가 생성될 떄, 멀티 쓰레드 환경에서 안전하기도 하다.

Effective Java에서는 enum을 사용하는 것이 가장 좋은 방법이라고 언급하고 있다. 하지만 enum은 익숙하지 않은 방법이라 자주 사용할지는 모르겠지만 의식적으로라도 한 번씩 사용하도록 노력해야겠다.

반응형
반응형

본 게시글은 Effective Java 2판을 참고하여 작성하였습니다.


생성자 인자가 많다면 코드작성 뿐만아니라 가독성도 떨어지게 될 것입니다. 보통 선택적 인자를 받는 생성자가 존재할 때는 점층적 생성자 패턴을 적용합니다. 필수 인자만 받는 생성자를 하나 정의하고, 선택적 인자를 하나씩 추가해서 작성하곤 합니다. 만약 그 선택적 인자가 10개 이상이 넘어간다면 그 많은 인자가 무슨 값인지 알기 위해서는 주의 깊게 봐야합니다. 또한 자료형이 같은 인자들이 많아진다면 클라이언트가 인자의 순서를 실수로 뒤집게 되는 경우 문제가 생기겠죠


때문에 두번째 대안으로 자바빈 패턴을 이용하곤 합니다. 인자 없는 생성자를 호출하여 객체를 생성한 다음, 설정 메서드를 호출하여 필수 필드와 선택필드까지 초기화 하는 것입니다. 하지만 1회의 함수 호출로 객체 생성을 끝낼 수 없어, 객체 일관성이 일시적으로 깨질수 있다는 단점이 존재하죠. 일관성이 깨지면 디버깅 하기도 어렵고 자바빈 패턴으로는 변경 불가능한 클래스를 만들 수 없다는 것이 가장 critical하다고 할 수 있습니다. 


그렇다면 이러한 문제점을 해결할 수 있는 방법은 무엇일까요? 바로 Builder패턴입니다. 필요한 객체를 직접 생성하는 대신, 먼저 필수 인자들을 생성자 or 정적 팩토리 메서드(1강 참조)에 전부 전달하여 빌더 객체를 만듭니다. 이후 빌더 객체에 정의된 설정 메서드들을 호출하여 선택적 인자를 추가하는 방식입니다. 마지막으로는 아무런 인자없이 build메서드를 호출하여 변경 불가능한 객체를 만듭니다. 음... 일단 그 실제 코드를 볼까요?


public class NutritionFacts {

private final int servingSize;

private final int servings;

private final int calories;

private final int fat;

private final int sodium;

private final int carbohydrate;


public static class Builder {

// 필수 인자

private final int servingSize;

private final int servings;


// 선택적 인자는 기본값으로 초기화

private int calories = 0;

private int fat = 0;

private int carbohydrate = 0;

private int sodium = 0;


public Builder(int servingSize, int servings) {

this.servingSize = servingSize;

this.servings = servings;

}


public Builder calories(int value) {

calories = value;

return this;

}


public Builder fat(int value) {

fat = value;

return this;

}


public Builder carbohydrate(int value) {

carbohydrate = value;

return this;

}


public Builder sodium(int value) {

sodium = value;

return this;

}

public NutritionFacts build() {

return new NutritionFacts(this);

}

}

private NutritionFacts(Builder builder) {

servingSize = builder.servingSize;

servings = builder.servings;

calories = builder.calories;

fat = builder.fat;

sodium = builder.sodium;

carbohydrate = builder.carbohydrate;

}

}


NutritionFacts 객체가 변경 불가능하다는 것과 모든 인자가 한곳에 모여 있다는 것을 다시 보자. 빌더에 정의된 설정 메서드는 빌더 객체 자신을 반환하기 때문에 코드는 이어서 쓸 수 있다. 사용하는 예를 보자.


NutritionFacts coke = new NutritionFacts.Builder(240,8). caloreis(100).sodium(35).carbohydrate(27).build();


어떤가?? 필자는 이 코드를 보고 아니 이 패턴을 보고 감동했다. 마치 Python과 같이 선택적 인자에 이름을 붙일 수 있는 방법과 비슷하지 않은가? 아니 실례로 현업에서 지저분한 소스 코드를 봤던 필자에게는 마치 감동일 수 밖에 없었다. 알아보기 쉽고 쓸데없는 주석도 필요없다. 마지막 build 메서드를 통해 해당 불변식이 위반되었는지 검사 할수 있으며 실제 객체의 필드를 두고 검사할 수 있다는 것도 매우 중요하다.

그리고 빌더 패턴은 유연하다. 하나의 빌더 객체로 여러 객체를 만들 수 있기 때문이다. 다른 객체를 생성해야 할 때 마다 빌더 객체의 설정 메서드를 호출하면 다음 생성 객체를 바꿀수 있으며, 어떤 필드의 값은 자동으로 채울 수도 있다!


하지만 이 역시 장점만 존재할 수는 없을 것이다.(참 아쉽다) 객체를 생성하려면 우선 빌더 객체를 생성해야 한다. 성능에서는 분명 차이가 있겠지만 실무에서는 큰 문제가 될 소지는 아니라고 판단된다. 두번째는 이 빌더 패턴을 구현하는 소스는 점층적 생성자 패턴보다 많은 코드를 요구하기 때문에 인자가 충분히 많은 상황에서 이용해야 한다.(통제하기 힘들 정도 필자는 10개 이상이라고 생각한다) 


앞 장에서 작성한 정적 팩토리 메서드도 빌더 패턴도 상황에 맞게 써야 할 것이다. 이번장에서 작성한 빌더 패턴은 인자가 많은 생성자(특히 선택적 인자)나 정적 팩토리가 필요한 클래스를 설계할 때 매우 유리할 것이다. 가독성은 편해지고 자바빈 패턴보다 안전한 것은 물론이다. 모든 상황에 맞는 패턴은 존재하지 않는다. 다양한 패턴을 익혀두고 상황에 맞는 패턴으로 구현하는 자세를 가져야 겠다.

반응형

+ Recent posts