요깨비's LAB

[Modern Java] Chapter 1. 왜 자바 8이 필요한 것일까? 본문

Java/모던 자바

[Modern Java] Chapter 1. 왜 자바 8이 필요한 것일까?

요깨비 2019. 12. 19. 18:19

본 기록은 케빈님의 강의와 "한빛미디어 - 모던 자바 인 액션"을 정리한 내용들입니다.

1. 문제 상황

배열에서 값들을 꺼내서 "element1 : element2 : element3..."의 모양의 문자열로 변환하여 출력하는 코드를 짭니다.

 

2. Java8 이전 방식의 코드

final List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// 기존
final StringBuilder stringBuilder = new StringBuilder();

for (Integer number : numbers) {
	stringBuilder.append(number).append(" : ");
}

// 1 : 2 : 3 : 4 : 5 : 6 : 7 : 8 : 9 : 10 :
System.out.println(stringBuilder.toString());

마지막에 " : "가 하나 더 붙는군요... 이걸 어떻게 없앨까요

final int size = numbers.size();
for (int i = 0; i < size; i++) {
	stringBuilder.append(numbers.get(i));
	if (i != size - 1)
		stringBuilder.append(" : ");
}

// 1 : 2 : 3 : 4 : 5 : 6 : 7 : 8 : 9 : 10
System.out.println(stringBuilder.toString());

제대로 잘 나오네요 하지만 꼭 이렇게 짜야만 할까요? foreach를 결국 for문으로 바꿨고 for문이나 if문에서 실수 여지도 있고
다시 foreach로 가능하도록 짜보겠습니다.

for (Integer number : numbers) {
	stringBuilder.append(number).append(" : ");
}
		
if(stringBuilder.length() > 0) {
	// start가 어디부터 지우기를 시작하면 될까...
	// end는 stringBuilder.length()-1인가 아니면 그냥 stringBuilder.length()일까? API 참조해볼까 귀찮네...
	stringBuilder.delete(stringBuilder.length() - 3, stringBuilder.length());
}

if문 부분을 보시면 알겠지만 구현하기에 실수할 여지가 크고 다른 개발자가 보기에도 뭔가 더 보기 않좋은 코드 같군요...

2. Java8 방식으로!
그러면 Java8에서 제공해주는 기능을 이용해 보겠습니다

final String result = numbers.stream()
			// String의 valueOf를 호출해서 numbers의 값을 인자로 넣어 변환 -> String.valueOf(number)
			.map(String::valueOf) 
			.collect(Collectors.joining(" : "));

// 1 : 2 : 3 : 4 : 5 : 6 : 7 : 8 : 9 : 10
System.out.println(result);

따로 delete를 통해 length의 어느 부분부터 어디까지 삭제해야 하는지 고민할 필요도 없고, 한눈에 어떤 의미인지 파악하기 쉽고(물론 map, collect, joining 이런 의미를 공부하면서 마치 println, push, pop이 어떤 의미인지 우리가 익힌 것처럼 익숙해져야합니다 저도 지금 처음이라 이제 공부하면서 익혀야겠지만...)헷갈릴만한 부분이 없습니다.

이렇게 이전에 저희가 "어떠한 방식으로 결과를 만들어라"라는 HOW를 짜는 방식의 명령형 프로그래밍이 아닌
"How는 내부적으로 우리가 구현하였으니 너는 무엇을 해야하는지를 우리에게 알려주어라"라는 What을 짜는 방식을
함수형 프로그래밍이라고 합니다. 이러한 방식을 통해 더 직관적이고 실수의 여지를 줄이는 개발 방식을 택할 수 있습니다.

객체지향의 본산인 자바에서 함수형 프로그래밍이 가능하다니.. 그걸 이제야 알고 공부를 시작하는 저도 참 
부족한 것 같습니다. 앞으로 꾸준히 체득화하겠습니다.

3. 기존 OOP 프로그래밍

자.. 기존의 객체지향 방식의 코드입니다. 케빈님의 강의 고대로 배꼈습니다

public class OopAnotherExample {
	public static void main(String[] args) {
		final CalculatorService calculatorService = new CalculatorService();
		final int result = calculatorService.calculate(1, 1);
		
        // 2
		System.out.println(result);
	}
}

class CalculatorService {
	public int calculate(int num1, int num2) {
		return num1 + num2;
	}
}

잘 돌아가는 코드에서 고객으로부터 사칙연산 기능들이 추가되었으면 좋겠다는 요구사항을 전달 받았습니다.
메서드를 새로운 기능마다 만들어야 하니 귀찮으니까 어차피 계산 기능만 생각했을때는 기존 calculate 메서드 하나로 요구사항을 처리할 수 있게 해보자는 나름의 아이디어로 짰습니다. (실제로는 이렇게 짜면 안됩니다. 단일 책임 원칙 위배! =>
CalculatorService는 한가지 이유로 수정돼야 하는데 덧셈, 뺄셈, 곱셈, 나눗셈 등 여러가지 이유로 수정이 되기 때문입니다.
자세한 것은 아래에서 수정하면서 설명하겠습니다. 극 초보 개발자의 한계.. 바로 설명 못해버리네..)

public class OopAnotherExample {
	public static void main(String[] args) {
		final CalculatorService calculatorService = new CalculatorService();
		
		final int additionResult = calculatorService.calculate('+',5, 5);
		// 10
		System.out.println(additionResult);
		
		final int subtractionResult = calculatorService.calculate('-',5, 5);
		// 0
		System.out.println(subtractionResult);
		
		final int multipleResult = calculatorService.calculate('*', 5, 5);
		// 25
		System.out.println(multipleResult);
		
		final int divisionResult = calculatorService.calculate('/', 5, 5);
		// 1
		System.out.println(divisionResult);
	}
}

class CalculatorService {
	public int calculate(char calculation, int num1, int num2) {
		if(calculation == '+') {
			return num1 + num2;
		}else if(calculation == '-') {
			return num1 - num2;
		}else if(calculation == '*') {
			return num1 * num2;
		}else if(calculation == '/') {
			return num1 / num2;
		}else {
			throw new IllegalArgumentException("Unknown calculation: " + calculation);
		}
	}
}

해당 코드는 짧기 때문에 어떠한 문제가 발생했을때 빠르게 찾을 수 있지만, 해당 기능이 사칙연산뿐만 아니라 수백, 수천가지의 기능들을 가지고 있는 수백줄짜리 코드라고 한다면 유지보수를 위해 상당이 눈이 뻐근하고 피곤해질 수 밖에 없습니다.
이를 해결하기 위해 OOP 방식 중 디자인 패턴인 Strategy Pattern을 이용하여 리펙토링을 해보겠습니다.

public class OopAnotherExample {
	public static void main(String[] args) {
		final CalculatorService calculatorService = new CalculatorService();

		calculatorService.setCalculation(new Addition());
		final int additionResult = calculatorService.calculate(5, 5);
		System.out.println(additionResult);

		calculatorService.setCalculation(new Subtraction());
		final int subtractionResult = calculatorService.calculate(5, 5);
		System.out.println(subtractionResult);
		
		calculatorService.setCalculation(new Multiplication());
		final int multiplicationResult = calculatorService.calculate(5, 5);
		System.out.println(multiplicationResult);
		
		calculatorService.setCalculation(new Division());
		final int divisionResult = calculatorService.calculate(5, 5);
		System.out.println(divisionResult);
	}
}

interface Calculation {
	int calculate(int num1, int num2);
}

class Addition implements Calculation {

	@Override
	public int calculate(int num1, int num2) {
		// TODO Auto-generated method stub
		return num1 + num2;
	}

}

class Subtraction implements Calculation {

	@Override
	public int calculate(int num1, int num2) {
		// TODO Auto-generated method stub
		return num1 - num2;
	}

}

class Multiplication implements Calculation {

	@Override
	public int calculate(int num1, int num2) {
		// TODO Auto-generated method stub
		return num1 * num2;
	}

}

class Division implements Calculation {

	@Override
	public int calculate(int num1, int num2) {
		// TODO Auto-generated method stub
		return num1 / num2;
	}

}

class CalculatorService {
	private Calculation calculation; // composition 조립을 이용하여
									 // dependency injection으로 구현하였음.

	public CalculatorService() {
	}

	public CalculatorService(final Calculation calculation) {
		this.calculation = calculation;
	}
	
	public void setCalculation(Calculation calculation) {
		this.calculation = calculation;
	}

	public int calculate(int num1, int num2) {
		return calculation.calculate(num1, num2);
	}
}

이런식으로 작성하면 특정 부분에서 에러가 나면 해당 기능을 하는 클래스만 확인하면 되기 때문에 유지보수하기가 용이하죠
또한 필요한 기능을 조립했다 분리했다 자유자재로 기능을 이용할 수 있습니다.
다음은 CalculatorService가 calculate 연산만 하는 것이 아닌 compute1,2 연산도 가능하다고 가정해보겠습니다.

class CalculatorService {
	private Calculation calculation; // composition 조립을 이용하여 dependency
	private Calculation calculation2; // injection으로 구현하였음.
	private Calculation calculation3;
	
	public CalculatorService() {
	}

	public CalculatorService(final Calculation calculation) {
		this.calculation = calculation;
	}
	
	public void setCalculation(Calculation calculation) {
		this.calculation = calculation;
	}
	
	public void setCalculation2(Calculation calculation) {
		this.calculation2 = calculation;
	}
	
	public void setCalculation3(Calculation calculation) {
		this.calculation3 = calculation;
	}

	public int calculate(int num1, int num2) {
		return calculation.calculate(num1, num2);
	}
	
	public int compute1(int num1, int num2) {
		if(num1 > 10 && num2 < num1) {
			return calculation2.calculate(num1, num2);
		}else {
			throw new IllegalArgumentException("Invalid input num1: " + num1 + " num2: " + num2);
		}
	}
	
	public int compute2(int num1, int num2) {
		if(num1 < 00 && num2 < num1) {
			return calculation3.calculate(num1, num2);
		}else {
			throw new IllegalArgumentException("Invalid input num1: " + num1 + " num2: " + num2);
		}
	}
}

compute1과 2를 보시면 구조가 반복적이고 calculation2,calculation3가 새로 생기면서 반복적인 코드가 생기고 있습니다.

4. Functional 프로그래밍

위 OOP 코드를 함수형 코드로 바꿔보겠습니다.

class FpCalculatorService {
	public int calculate(Calculation calculation, int num1, int num2) {
		if(num1 > 10 && num2 < num1) {
			return calculation.calculate(num1, num2);
		}else {
			throw new IllegalArgumentException("Invalid input num1: " + num1 + ", num2: " + num2);
		}
	}
}

calculate(Calculation calculation => 이 부분이 이전 char '+','-'를 넣는 것과 비슷해 보이지만 그거랑은 전혀 다릅니다.
calculation을 받아와서 비교를 하는 것이 아닌 calculation의 기능을 실행만 했습니다.

public class OopAndFpExample {
	public static void main(String[] args) {
		// OOP 방식!!!
		final CalculatorService calculatorService = new CalculatorService();

		calculatorService.setCalculation(new Addition());
		final int additionResult = calculatorService.calculate(5, 5);
		System.out.println("addition: " +additionResult);

		calculatorService.setCalculation(new Subtraction());
		final int subtractionResult = calculatorService.calculate(5, 5);
		System.out.println("subtraction: " +subtractionResult);
		
		calculatorService.setCalculation(new Multiplication());
		final int multiplicationResult = calculatorService.calculate(5, 5);
		System.out.println("multiplication: " +multiplicationResult);
		
		calculatorService.setCalculation(new Division());
		final int divisionResult = calculatorService.calculate(5, 5);
		System.out.println("division: " +divisionResult);
		
		
		// FP 방식!!!!
		final FpCalculatorService fpCalculatorService = new FpCalculatorService();
		System.out.println("addition: " + fpCalculatorService.calculate(new Addition(), 4, 2));
		System.out.println("subtraction: " + fpCalculatorService.calculate(new Subtraction(), 4, 2));
		System.out.println("multiplication: " + fpCalculatorService.calculate(new Multiplication(), 4, 2));
		System.out.println("division: " + fpCalculatorService.calculate(new Division(), 4, 2));
	}
}

딱 봐도 FP 방식의 코드가 훨씬 간결하고 직관적으로 보이죠? 와 지금 저도 정리하면서 이 간결함에 놀라고 있습니다...

이제 FP 부분의 코드를 보시면 calculate부분에 항상 new Addtion, new Subtraction의 객체를 생성해서 인자로 넣고 있죠?
이게 자바는 함수가 일급객체가 아니다 보니까 이거를 객체로 감싸서 전달해 주고 있습니다.

* 일급 객체(First Class Citizen):
1. Element가 Function의 파라미터로 넘길 수 있어야하고
2.반환값으로 받을 수 있어야 하고,
3.자료구조에 저장이
될 수 있어야 합니다. 자바 스크립트(ES6)를 예로 보면 쉬울 것 같습니다.

function double (arr) {
  return arr.map((item) => item * 2)
}

function add (arr) {
  return arr.reduce((prev, current) => prev + current, 0)
}

자바에서는 Object를 일급 객체로 취급하죠 자바에서는 위 같은 코드가 불가능했지만 Java8부터는 이것을 지원합니다.
이것을 이용해서 fp부분을 수정해보겠습니다.

// FP 방식!!!!
final FpCalculatorService fpCalculatorService = new FpCalculatorService();
// System.out.println("addition: " + fpCalculatorService.calculate(new Addition(), 4, 2));
// System.out.println("subtraction: " + fpCalculatorService.calculate(new Subtraction(), 4, 2));
// System.out.println("multiplication: " + fpCalculatorService.calculate(new Multiplication(), 4, 2));
// System.out.println("division: " + fpCalculatorService.calculate(new Division(), 4, 2));
System.out.println("addition: " + fpCalculatorService.calculate((num1, num2 )-> num1+num2, 4, 2));
System.out.println("subtraction: " + fpCalculatorService.calculate((num1, num2 )-> num1-num2, 4, 2));
System.out.println("multiplication: " + fpCalculatorService.calculate((num1, num2 )-> num1*num2, 4, 2));
System.out.println("division: " + fpCalculatorService.calculate((num1, num2 )-> num1/num2, 4, 2));


자바스크립트 Fat Arrow같이 되었네요! 객체를 따로 생성할 필요 없이 함수를 인자값으로 넘기다니.. 대단한 것 같습니다.

"명령형 프로그래밍은 어떻게 할 것인가(How)를 표현하고, 선언형 프로그래밍은 무엇을 할 것인가(What) 표현한다."

가장 중요한 정리 글인것 같습니다.

* 참고: 자바8의 기능을 통해 병렬 프로그래밍을 수월하게 진행하도록 도와준다고 하네요!

* 구글링 및 책, 강의를 통해 해당 내용들을 계속해서 보완할 계획입니다. 지금은 고등어 뼈 같지만 앞으로 이 내용에서
제가 이해한 내용들을 붙여서 통통하게 만들어 나가겠습니다.

Comments