포스트

(Day 39) Builder Pattern(Gson), 람다문법과 forEach, 메서드 레퍼런스

복습

JSON Library

GSON

jackson-databind

Builder Pattern (GoF)

Factory Method 보다 복잡한 옵션을 설정하여 인스턴스를 생성하려고 할 때 사용 만드려는 객체(레퍼런스 변수) = new 빌더().옵션설정(옵션).옵션설정(옵션).create();

어… 그러면 빌더().create()만 하면 팩토리 메서드랑 거의 똑같네요? = 네. 근데 그러면 옵션 설정을 생략한다는거나 다름없으니 빌더 패턴을 쓸 필요가 없을 것 같은데요?

Factory Method는 하나의 메서드에서 객체 생성과정을 전부 해결해야 한다.

왜?
객체 생성 과정이 복잡할 때 그것을 캡슐화하는 것이 OOP 정신 (유지보수) 이다. 빌더 패턴을 적용한 GSON을 한번 들여다보자. 쉬운 일(?)이 아니고 설정을 (옵션을) 복잡하게 하려면 팩토리메서드로는 불가능하다.

비교 createFromCsv() 라는 팩토리 메서드를 썼을 때는 도메인별로 (Variable Object) 팩토리 메서드를 작성해줘야 했다. 유사한 역할의 코드를 VO의 클래스별로 작성해주어야 했다. 지금생각하면 아주 불편한 일이다. VO가 얼마나 추가될지 모르는 상황이고, VO에서 어떤 변경이 일어날지 모르는 상황인데 말이다.

GSON에서는 그럴 필요가 없었다! 변수별로 설정해줄 수도 있고, 그냥 변수의 이름을 가져와서 (Reflection API) 자동으로 출력, 조회가 가능했다. jackson-databind 또한 게터/세터의 메서드명을 기준으로 자동으로 json 데이터를 생성해주었다.

물론 csv도 그렇게 동작하도록 만들 수는 있겠으나, csv는 첫 행에 메타데이터가 없는 경우라면, 그리고 계층 구조를 가진 데이터라면 아주 큰 문제가 발생할 것이다!

Json Deserialization

JSON으로 객체를 만들면 SVUID가 없다.

GSON에서는 toJSON으로 Serialization, fromJSON에서 Deserialization 한다. 이 때 용어를 인코딩, 디코딩으로 쓰는 경우도 있다. 잘못된 용어 사용이 아니다.

학습

기술과 형식의 2차원 그래프

형식의 중요성은 분야별로 다르다.

형식이 중요하지 않음————————————————- | 일상 대화 ▶ 소설 ▶ 드라마 ▶ 논문 ▶ 프로그래밍 ▶ 수학 | ———————————————————–형식이 중요함

람다함수

인터페이스가 추상메서드를 하나만 가지고 있을 때 람다함수로 더 간단하게 표현할 수 있다. 이를 알기 위해서는 인터페이스, 익명함수를 먼저 알아야 한다.

익명함수

익명함수의 가장 큰 오해는 생성자가 없고, 슈퍼클래스의 생성자를 호출한다는 것이다. 결과적으로 보면 그렇긴 한다. 그러나 바이트코드를 뜯어보면 생성자가 만들어지는 것을 확인해 볼 수 있다.

익명 함수도 생성자를 컴파일러가 작성해주며, 그 기본 생성자는 슈퍼클래스의 기본 생성자를 호출한다. 익명 클래스를 선언할 때, 생성자에 아규먼트를 주면 그것을 그대로 받는 익명 클래스의 생성자를 컴파일러가 만든다. 그리고 그 익명 클래스의 생성자는 받은 아규먼트를 그대로 슈퍼클래스의 생성자로 호출하면서 넘겨준다.

결론적으로는 익명클래스의 생성자는 슈퍼클래스의 생성자를 호출하는 일밖에 안하니까, 슈퍼클래스의 생성자를 호출한다고 줄여서 말하게 된 것이다…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 익명 클래스와 .class 파일
// => 자바의 nested class 는 모두 별도의 .class 파일을 갖는다.
// => 위의 main()에 정의된 로컬 익명 클래스는 다음과 같은 이름의 .class 파일로 컴파일 된다.
// Exam0110$1.class

// 람다와 .class 파일

// => 람다는 별도의 .class 파일을 생성하지 않는다.

// => 컴파일러는 람다 코드를 해당 클래스의 스태틱 메서드로 정의한다.

// => 그리고 리플렉션 API를 사용하여 이 메서드를 호출하도록 변경한다.

// => 람다 문법이 초기에 등장했을 때는 익명 클래스로 변환되었다.

// => 그러나 최근에는 메서드로 변환한 후 호출하는 방식으로 바뀌었다.

// => 예)

// 원래의 자바코드:

// Player p2 = () -> {

// System.out.println("람다");

// };

//

// 컴파일러가 변환한 코드:

// private static synthetic void lambda$0();

// 0 getstatic java.lang.System.out : java.io.PrintStream [33]

// 3 ldc <String "람다"> [39]

// 5 invokevirtual java.io.PrintStream.println(java.lang.String) : void [41]

// 8 return

// Line numbers:

// [pc: 0, line: 27]

// [pc: 8, line: 28]

//

// => 람다를 호출하는 코드는 자동 생성된 메서드를 호출하는 코드로 변환된다.

//

요약 : 람다문법은 원래 익명클래스에서 시작되어서, 컴파일러가 익명 클래스로 변환하였으나 지금은 스태틱 메서드로 변환한다. 람다문법은 메서드가 하나뿐인 인터페이스를 구현하는 방법을 좀 더 간결하게 작성한 것이다. (인터페이스는 생성자를 갖지 못해서 Object의 생성자를 호출하게 된다.)

타입 refVar = (args) -> 구현;

추상메서드가 하나면 된다. 스태틱 메서드나 default 메서드는 몇개 있든지 상관 없다. 반대로 말하면 Functional Interface가 되려면 추상 메서드가 하나만 있는 인터페이스여야 한다. 추상 클래스는 안된다.

(args) -> 구현 으로 functional interface를 구현하여

인클로징 메서드 안에서 사용하는 경우

로컬클래스나 익명클래스, 람다 문법을 인클로징 메서드 안에서 사용하는 경우 인클로징 메서드에서 사용하는 로컬변수들을 사용할 수 있다. 그런데 인클로징 메서드의 로컬 변수들을 사용한다는 것이 직접 접근한다는 것이 아니다! (자주하는 오해)

바이트코드를 뜯어보면, 인클로징 메서드의 로컬 변수들을 복사해오는 부분이 확인된다.

Static Method Reference

스태틱 메서드 레퍼런스. 인터페이스에 메서드를 마치 조립하는 것 같다. 구현은 스테틱 메서드에서 되어 있고, 그것을 인터페이스에 조립하는 것 같다.

이름은 달라도 된다. 인터페이스에서 요구하는 리턴타입과 파라미터는 맞아야 한다. 마치 정사각형의 네 꼭지점들에 점을 찍어 놓은 것 같은 :: 라는 기호를 사용하는 (콜론 두개) 문장을 본 적 있는가? 이것이 Static Method Reference 문법이다.

사용은 이렇게 한다. 인터페이스명::스태틱메서드명 그러면 인터페이스의 Abstract Method의 구현을 스태틱메서드로 대신한다. 저 문장은 결국 객체 주소를 반환하며, 레퍼런스 변수로 받아서 그 객체를 쓰면 된다. (혹은 객체 자체를 받고자 하는 경우도 가능하다. 람다 문법과 사용 방법이 유사하다.)

구현된 추상 메서드를 호출할 때는, 인터페이스에서 정의된 이름으로 호출할 수 있다.

▼ 확인 필요 :: 으로 조립한 스태틱 메서드의 이름은 인터페이스의 메서드 이름을 통해 호출된다.

컴파일러는..

이런 문법들은 컴파일러가 문법을 해석하여 코드를 변환해주기에 가능하다.

메서드 레퍼런스를 지정할 때 리턴 타입의 규칙은?

// => 메서드 레퍼런스를 지정할 때 리턴 타입의 규칙: // 1) 같은 리턴 타입 // 2) 암시적 형변환 가능한 타입 // 3) auto-boxing 가능한 타입 (I/F 에서 Object로 리턴값 정의한 우 등에는 오토박싱 될 수 있다. int -> Integer ) // 4) void // 결론, // 메서드 레퍼런스가 가리키는 실제 메서드를 호출한 후 // 그 메서드가 리턴한 값이 // 인터페이스에 정의된 메서드의 리턴 값으로 사용할 수 있다면 // 문제가 없다.

핵심은? 컴파일러가 변환하는 방식을 알고 있으면, 거기서 오토박싱/언박싱/타입캐스팅 지원 여부를 통해 위 1~4가지를 외울 필요 없이 판단 가능하다.

메서드 레퍼런스를 지정할 때 파라미터 타입 규칙

인터페이스 규칙에 따라 받은 값을 실제 메서드에 그대로 전달할 수 있으면 메서드 레퍼런스로 지정하여 사용 가능하다.

정적 영역에 있는 클래스가 아니면 호출해서 부르는 방식인데, 없는 걸 부를 수 없겠지.. 라고 생각했는데 인스턴스도 가능하다.

Predicate

인터페이스 중 자주 쓸 것이라고 생각하여, 자바에서 미리 만들어둔 인터페이스이다. Predicate 가 대표적이다.

아직 소화시키기 어려운 내용

Predicate 클래스에서 람다 표현식으로 변환할 때, 타입 파라미터 클래스 이름과 인스턴스 메서드의 클래스 이름이 일치하는 특이점에 대해 설명하겠습니다.

Predicatetest 메서드는 단일 매개변수를 가지며, 보통 타입 파라미터로 표시됩니다. 람다 표현식에서는 매개변수의 타입을 명시적으로 쓰지 않고 컴파일러가 추론하도록 하기 때문에 이러한 특이점이 발생합니다.

1
Predicate<String> predicate1 = (String s) -> s.length() > 5;

위의 예제에서는 Predicate의 타입 파라미터가 String으로 표시되어 있습니다. 그리고 람다 표현식에서 매개변수 s의 타입이 명시적으로 지정되어 있습니다.

이 특이점을 살펴보기 위해, 일치하는 클래스 이름을 갖는 정적 메서드를 갖는 클래스를 만들어보겠습니다:

1
2
3
4
5
class MyStringOperations {
    static boolean checkLength(String s, int length) {
        return s.length() > length;
    }
}

이제 이를 사용하여 Predicate를 만들어 보겠습니다:

1
Predicate<String> predicate2 = MyStringOperations::checkLength;

여기서 람다 표현식의 매개변수는 MyStringOperations 클래스의 정적 메서드 checkLength의 첫 번째 매개변수와 일치합니다. 그리고 첫 번째 매개변수가 String 타입이므로, Predicate의 타입 파라미터가 String으로 자동으로 추론됩니다.

즉, 이 특이점은 람다 표현식에서 메서드 참조를 사용할 때, 일치하는 클래스의 메서드와 타입 파라미터의 추론이 자연스럽게 이루어진다는 것입니다.

forEach

Consumer<T> 인터페이스를 구현한 클래스는 forEach 메서드에 아규먼트로 줄 수 있다. Consumer 인터페이스는 추상 메서드가 하나뿐이므로 추상메서드나 람다식으로 작성할 수 있다. (그리고 그렇게 많이 쓴다)

forEach()는 아래와 같은 형태이다.

1
2
3
4
5
public void forEach(Consumer<? super E> action) {
	public void for (E value : this) {
		action.accept(value);
	}
}

enhanced for loop처럼 이터러블한 객체에 반복문을 도는데, 그러면서 각각에 Cosumer 객체의 accept()를 수행한다.

functional interface이므로 아래와 같은 간략화가 가능하다.

1
iterable.forEach(item -> 작업);

여기에 메서드 레퍼런스 까지 적용한다면,

1
iterable.forEach(System.out::println);

왜? 나중에 실무 가서도 모르던게 나오면 그 때 파보고 공부해라! 두려워하지 말라! (자바 랭귀지에 대한 2000페이지 책들에도 안나오는 문법이 있으며 계속해서 문법이 추가, 변경된다. 어쩔 수 없다.)

그 외

포트폴리오 / 이력서 : 4월 중반까지 초반 5/23 이후 이력서 / 포폴 첨삭함. 그 떄 잘 만들어져 있어야 함.

SI의 한계

3년차 정도 까지는, 구현 위주인 SI 업계에서 (관리시스템을) 구현을 반복하는 경험이 성장이 더 빠르다.

서비스를 운영하기 위한 엔진을 만드는 쪽이 처음에는 (3년차 정도는) 성장 속도가 느린 것 같아도 나중에는 훨씬 더 빠르다.

엔진(프레임워크나 라이브러리)을 개발할 수 있는 사람들을 원한다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.