[CHAPTER 9] 왜 "함수형프로그래밍" 인가? (정리글 모음) [과제 리뷰]
함수형 프로그래밍을 시작하는 첫 장에서는
- 함수란 무엇인가?
- 다형성(Polymorphism)의 필요성과 이를 이용하기 위한 OOP 의 한계
- 이를 극복하기 위한, 기존 Java8 이전의 방법과 편리해진 람다와 메소드 레퍼런스
정도의 내용을 다루었습니다. (위의 개념에 대해 생각이 안난다면, 꼭 복습을 권장합니다.)
이에 대한 포스팅 정보는 아래에서 확인 :-)
그리고 이를 실습해보기 위한 과제가 있었고, 이번 포스팅에서는 과제에 대한 리뷰를 해보고자 합니다.
1. SwitchAndSwitch
첫 번째 숙제는 람다에 대한 튜토리얼 진행을 해보고자 했던 내용이었습니다.
요구사항은 아래의 소스를 리팩토링하는 것이었습니다.
메소드 내부에 존재하는 코드들은 일련의 공통적인 작업들이 많이 보입니다.
보통은 이런 작업에 대해서 따로 메소드 추출 등의 리팩토링을 하겠지만, 이 로직이 오직 switchAndSwitch 메소드에서만 사용될 것 같았기 때문에 다른 방법을 찾아보자는 것이었습니다.
(추 후, 다른 로직에서도 사용이 된다면 그 때 캡슐화하는 것으로...)
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
53 |
public void switchAndSwitch() {
List<GenerateModel1> generateModel1List = Model1Dao.GetInstance().selectList(Arrays.asList(1,2,3));
// something work1
{
List<Integer> memberSnList = Arrays.asList(2,3);
for (GenerateModel1 model1 : generateModel1List) {
if (memberSnList.contains(model1.getMemberSubjectSn())) {
String name = model1.getName();
switch (name) {
case "강현지":
System.out.println("IF 란 사치임을 증명한 " + name);
break;
case "유덕형":
System.out.println("한 수에 버그를 말살하는 " + name);
break;
case "유형주":
System.out.println("한 메소드에 5줄 이면 충분한 " + name);
break;
}
}
}
}
// something work2
{
List<String> filterNameList = Arrays.asList("강현지", "유덕형");
for (GenerateModel1 model1 : generateModel1List) {
if (filterNameList.contains(name)) {
String name = model1.getName();
switch (name) {
case "강현지":
System.out.println("IF 란 사치임을 증명한 " + name);
break;
case "유덕형":
System.out.println("한 수에 버그를 말살하는 " + name);
break;
case "유형주":
System.out.println("한 메소드에 5줄 이면 충분한 " + name);
break;
}
}
}
}
} |
cs |
우리는 일련의 동작 즉 함수를 값으로 가질 수 있다는 개념을 알았고, 이를 이용해서 굳이 메소드 추출을 안하고 이 문제를 해결할 수 있었습니다.
네, 람다를 이용해보는 것이죠.
그 전에 프로그래밍 원칙 중 중요한 원칙이 한 개 있습니다.
"변하는 부분과 변하지 않는 부분을 분리하라!"
이 법칙에 근거했을 때, 저는 해당 로직에 대해 다음과 같이 정의를 했습니다.
- 변하는 부분 : GenerateModel1 에 대한 필터 로직
- 변하지 않는 부분 : Loop 를 돌며, switch 문을 수행하는 과정.
이에 따라 저는 다음과 같이 리팩토링 할 수 있었습니다.
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 |
public void switchAndSwitch() {
List<GenerateModel1> generateModel1List = Model1Dao.GetInstance().selectList(Arrays.asList(1,2,3));
/**
* Predicate 를 받아, 일련의 공통작업을 수행하는 소비자.
*/
Consumer<Predicate<GenerateModel1>> switchConsumer =
(predicate) -> {
for (GenerateModel1 model1 : generateModel1List) {
if(predicate.test(model1)) {
String name = model1.getName();
switch (name) {
case "강현지":
System.out.println("IF 란 사치임을 증명한 " + name);
break;
case "유덕형":
System.out.println("한 수에 버그를 말살하는 " + name);
break;
case "유형주":
System.out.println("한 메소드에 5줄 이면 충분한 " + name);
break;
}
}
}
};
// something work1
{
List<Integer> memberSnList = Arrays.asList(2,3);
// 프리디케이트가 정의되는 부분이 변하는 부분.
switchConsumer.accept(model1 -> memberSnList.contains(model1.getMemberSubjectSn()));
}
// something work2
{
List<String> filterNameList = Arrays.asList("강현지", "유덕형");
// 프리디케이트가 정의되는 부분이 변하는 부분.
switchConsumer.accept(model1 -> filterNameList.contains(model1.getName()));
}
} |
cs |
람다에 대한 첫 튜토리얼로 나름 나쁘지 않았다고 생각합니다. :-)
사실 개인적으로 람다표현식 보다도 위의 규칙이 더 중요하며, 저 규칙만 잘지켜도 좋은 프로그래머가 될 수 있을 것이란 생각이 드는군요.
2. Template-Method 의 극복? 이 방법은 정말로 좋은가?
동적 파라미터화를 이용하면, 굳이 클래스를 상속받아 구현하지 않아도 다형성을 사용할 수 있음을 알았습니다.
또한 JAVA8 에서는 기본 함수형 인터페이스까지 제공해주기 때문에 동적파라미터화를 하기 위해 따로 인터페이스를 제작하지 않아도 될 것 같아 보이는데요.
기본형 함수형 인터페이스에 대한 내용은 아래 포스팅에서 참고.
이 숙제는, 정말로 이 방법이 좋은 지에 대해 다뤄보는 내용이었습니다.
요구사항은 다음과 같았습니다.
ORM 으로 제작된 클래스들의 필드는 종종 비슷한 경우가 많지만, 아쉽게도 제너레이터에 의해 제작되기 때문에 이 곳을 수정하는 것은 문제가 있어보입니다.
(즉 상속 등 클래스 관계를 지어줄 수 없으며, 이는 꽤 골치아픈 문제가 될 수 있습니다.)
즉 이러한 이유로 다형성 등의 개념을 이용할 수 없는 것처럼 보이며, 이는 비슷한 로직도 재활용이 쉽지 않음을 의미합니다.
이를 극복하기 위한 여러가지 방법을 다뤘으며, 대표적인 방법 중 한 가지는 Template-Method 패턴을 이용해보는 것이었습니다.
하지만, 굳이 한 메소드 제작을 위해서 복잡한 클래스 구조를 가질 수 있어 보이는 Template-Method 를 사용하는 것은 부담이라 생각하였습니다.
(이런 생각은 귀차니즘에서 보통 비롯하곤 합니다. ㅡㅡ^)
그러던 중, 동적 파라미터화 및 기존 제공 함수형 인터페이스가 있는 것을 배웠고 이를 이용해 Template-Method 와 비슷한 효과를 낼 수 있을 것 같았습니다.
즉, 어떠한 인터페이스나 추상클래스를 만들지 않고 Template-Method 를 흉내내는 것이 이 과제의 목적이었습니다.
아래 소스는 과제로 제출해준 한 분의 소스입니다.
(제작해주셔서 감사합니다.^^)
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83 |
/**
* 제너레이트 된 모델에 대한 비지니스 로직 정의.
*
*
* @param <G>
* @param addTargetMemberSnList
* @param excludeTargetMemberSnList
*/
public <G> void createByGenerateModel(
List<Integer> addTargetMemberSnList
, List<Integer> excludeTargetMemberSnList
, Function<Integer, G> toGenerateModel
, Function<List<Integer>, List<G>> selectList
, Function<G, Integer> toMemberSubjectSn
, Consumer<G> insertData
, Consumer<List<Integer>> deleteByMemberSnList) {
final HashMap<Integer, G> groupByMemberSnMemberMap = new HashMap<>();
{
for (Integer memberSn : addTargetMemberSnList) {
// 일단은 MemberSn 만 넣는다고 가정.
G generateModel = toGenerateModel.apply(memberSn);
groupByMemberSnMemberMap.put(memberSn, generateModel);
}
}
// 이미 존재하는 구성원이거나 제외대상자는 입력 대상에서 제외.
{
// 이미 존재하는 구성원순번 또는 제외 타겟 순번 집합.
HashSet<Integer> excludeTargetMemberSnSet = new HashSet<>();
{
// 이미 존재하는 구성원 순번 목록 삽입.
List<G> existList = selectList.apply(groupByMemberSnMemberMap.keySet().stream().collect(Collectors.toList()));
for (G model : existList) {
excludeTargetMemberSnSet.add(toMemberSubjectSn.apply(model));
}
// 제외 대상 파라미터도 추가.
excludeTargetMemberSnSet.addAll(excludeTargetMemberSnList);
}
// 추가대상 그룹에서 제외 대상 집합을 삭제한다.
groupByMemberSnMemberMap.keySet().removeAll(excludeTargetMemberSnSet);
}
// 데이터 트랜잭션
{
// 데이터 삽입.
for (G model : groupByMemberSnMemberMap.values()) {
insertData.accept(model);
}
// 제외대상 삭제.
deleteByMemberSnList.accept(excludeTargetMemberSnList);
}
}
// 메소드 사용 예
// Model1 에 대한 데이터 처리.
ModelSampleService.GetInstance().createByGenerateModel(Arrays.asList(1, 2), Arrays.asList(3),
(Integer memberSn) -> {
GenerateModel1 generateModel1 = new GenerateModel1();
generateModel1.setMemberSubjectSn(memberSn);
return generateModel1;
},
(List<Integer> memberSnList) -> {
List<GenerateModel1> list = Model1Dao.GetInstance().selectList(memberSnList);
return list;
},
(GenerateModel1 generateModel1) -> {
Integer memberSn = generateModel1.getMemberSubjectSn();
return memberSn;
},
(GenerateModel1 generateModel1) -> {
Model1Dao.GetInstance().create(generateModel1);
return;
},
(List<Integer> targetMemberSnList) -> {
Model1Dao.GetInstance().deleteByMemberSnList(targetMemberSnList);
return;
}
); |
cs |
의도 했던 바와 같이 어떠한 [인터페이스, 추상메소드] 없이, OCP 를 지킨 코드는 나왔습니다.
추 후, GenerateModel 이 또 등장하였고, 비슷한 로직을 사용한다면 각 함수형 인터페이스를 구현해주면 됩니다.
하지만, 일단 메소드 사용성 면에서 많은 불편함을 느꼈을 것입니다.
특정 프로토콜로 묶인 추상메소드의 구현이 아니기 때문에, 각 람다를 구현할 때마다 무슨 기능을 했었는지 살펴봐야 합니다.
이는 가독성도 떨어진다고 볼 수 있을 것 같네요..
(인터페이스나 추상클래스로 구현 했다면, 보다 동적인 구현에 있어서 무슨 일을 하는지 명확했을 수 있습니다.)
이 과제의 의도는 새로운 지식을 맹신하지 말라는 일종의 견제를 해주고 싶었고(패턴병과 유사한 함수병), 요구사항과 현재 상황에 따라 적절한 대처가 필요함을 느낄 수 있도록 하는 것이 목적이었습니다.
(요구사항이 기껏 한 두개의 함수형 인터페이스만 사용할 정도라면, 깔끔할 수 있었습니다.)
3. 계속 존재했던 동적 파라미터화
마지막 과제는 명령패턴을 이용해, undo/redo 를 구현해보고자 하였습니다.
이와 관련된 내용은 아래 포스팅을 참고.
과제 자체는 사실 이 패턴에 대한 이해도 중요했지만, 꼭 동적 파라미터화 같은 방법이 JAVA8 에서 등장한 것은 아니었다는 것에 대한 실습이었습니다.
명령패턴은 일종의 요청을 캡슐화하고 컨트롤러가 이를 관리하게 함으로써, 실제 요청자와 실행자 사이의 관계를 느슨하게 하게 하는 것을 목적으로 합니다.
대부분 이 패턴의 예제에서는 명령을 캡슐화하기 위해 인터페이스를 구현하는 구현클래스를 제작하지만, 이 과제에서는 굳이 클래스를 제작하지 않고 동적 파라미터화를 이용하여 즉시 구현하는 것을 목적으로 하였습니다.
(다양한 요청에 대해서, 재활용 안 할 구현클래스를 만드는 것은 일종의 낭비이지 않을까요?)
일단, 이 패턴을 구현하기 위한 interface 는 다음과 같이 제작했습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 |
/**
* 커맨드 인터페이스 정의.
*
* Created by Doohyun on 2017. 7. 10..
*/
public interface ICommand {
/**
* 어떤기능이든 undo 구현.
*/
void undo();
/**
* 어떤기능이든 실행 구현.
*/
void execute();
} |
cs |
이를 실행하기 위한 실행자(Receiver)와 컨트롤러(Controller) 는 다음과 같습니다.
(대부분의 내용은 앞써 언급한 포스팅에서 복사했습니다. 이 패턴이 궁금하다면 해당 링크를 참고하세용.)
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79 |
/**
* 명령을 관리하는 컨트롤러
*/
public class RemoteController {
// 일반 명령을 위한 스택
private Stack<ICommand> commandStack = new Stack<>();
// UNDO 명령을 위한 스택
private Stack<ICommand> undoStack = new Stack<>();
// 명령을 추가
public void setCommand(ICommand commandWithUndoable) {
commandStack.push(commandWithUndoable);
}
/**
* 일반적인 실행. (REDO 포함)
*/
public void execute() {
if (!commandStack.isEmpty()) {
// [일반명령 스택]에서 가장 마지막에 담긴 명령객체를 추출 후 실행.
ICommand command = commandStack.pop();
command.execute();
// 해당 명령을 UNDO 스택에 삽입.
undoStack.push(command);
}
}
/**
* 작업 취소 (Undo)
*/
public void undo() {
if (!undoStack.isEmpty()) {
// [UNDO 명령 스택]에서 가장 마지막에 담긴 명령객체를 추출 후 실행.
ICommand command = undoStack.pop();
command.undo();
// 일반 실행 스택에 데이터 삽입.
commandStack.push(command);
}
}
}
/**
* 글씨를 입력하는 데모 클래스.
*
* Created by Doohyun on 2017. 7. 10..
*/
public class TextWatcherDemo {
private StringBuilder builder = new StringBuilder("");
/**
* 텍스트 입력.
*
* <pre>
* 텍스트를 입력하고, 현재 상태를 표시한다.
* </pre>
*
* @param text
*/
public void addText(String text) {
builder.append(text);
System.out.println(builder.toString());
}
/**
* 텍스트 삭제.
*
* <pre>
* 텍스트를 삭제하고, 현재 상태를 표시한다.
* </pre>
*/
public void deleteText() {
builder.deleteCharAt(builder.length() - 1);
System.out.println(builder.toString());
}
} |
cs |
재료는 다 만들었고, 이를 실제 실행하는 부분만 만들어보면 될 듯 합니다.
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 |
RemoteController remoteController = new RemoteController();
TextWatcherDemo textWatcherDemo = new TextWatcherDemo();
// 텍스트를 입력받아, 컨트롤러에 명령을 세팅하고 실행하는 소비자 정의.
Consumer<String> InputText = (text) -> {
// 명령 세팅.
// 동적으로 행위를 정의. 즉 동적파라미터화
remoteController.setCommand(
new ICommand(){
@Override
public void undo(){
textWatcherDemo.deleteText();
}
@Override
public void execute(){
textWatcherDemo.addText(text);
}
}
);
// 실행
remoteController.execute();
};
// 메소드 레퍼런스
// undo 실행을 위한 함수 정의.
Runnable undo = remoteController::undo;
// 람다 버전의 REDO 함수.
// redo 실행을 위한 함수 정의.
Runnable redo = () -> remoteController.execute();
InputText.accept("안");
InputText.accept("녕");
undo.run();
redo.run();
// CONSOLE LOG
// 안
// 안녕
// 안
// 안녕 |
cs |
이런 방식은 사실 이벤트 처리 기반 시스템을 만들다보면, 꽤 많이 해봤을 것입니다.
결국 [JAVA8 in Action] 에서 메소드를 파라미터로 넘긴다는 이야기는 이전부터 있던 개념이며, 람다나 메소드 레퍼런스는 이를 보다 쉽게 사용할 수 있는 개념이라 볼 수 있을 것 같네요.
(추 후 공부하게 될, Stream API 사용을 위해서는 이를 적극적으로 사용해야 합니다.)
이로써, 지난 주에 실습한 내용에 대한 리뷰가 끝났습니다.
첫 시간이지만, 꽤 많은 내용을 다뤘던 듯 합니다. 그에 따라 과제도 좀 많이 있었죠. ^^;
이 과제를 언급할 때, 최근에 시청한 한 프로그램의 출연자가 했던 대화 중 하나를 같이 말했습니다.
Knowing is nothing, Doing is the best.
(아는 것은 중요하지 않다. 하는 것이 가장 좋다.)
단순히 듣기만 하는 것이 아니라, 한번 해보는 것은 정말 중요한 듯 합니다.
(이런 행동들은 조금씩 우아한 방법을 찾아 보는 것에 도움이 되겠죠?.. 아마도...)
어쨌든 개인시간을 투자하며, 계속 지금과 같은 시간을 같이 보내준 여러분들께 감사합니다.
(맥주를 한 병 더 먹었다면 아마 더 감성글이 됐을지도...)
'스터디 > [STUDY] FP' 카테고리의 다른 글
[CAHPTER 15] Stream API 리뷰 (2) (3) | 2017.07.26 |
---|---|
[CAHPTER 14] Stream API 리뷰 (1) (0) | 2017.07.20 |
[CHAPTER 12] Lambda 심화 & 과제 리뷰 (0) | 2017.07.14 |
[CHAPTER 9] 왜 "함수형프로그래밍" 인가? (정리글 모음) (3) | 2017.07.05 |
[CHAPTER 8] Starting with JAVA8, Easy FP! (orientation) (0) | 2017.07.03 |