모던 자바 인 액션 18장

시스템 구현과 유지보수

쉽게 유지보수할 수 있는 프로그램이란… 자바개발자들은 귀에 딱지가 앉게 들어보는 것이 있다.

  • 시스템의 구조를 이해하기 쉽게, ‘클래스 계층으로 반영’
  • 낮은 결합성(coupling), 높은 응집성(cohension)

하지만 현실은 그렇지 않다. 대부분 의도한대로 코드가 동작하지 않는다.

그리고, 그 의도하지 않은대로 코드가 동작하지 않는 경우를 통계를 낸다면,

아마 ‘코드 크래시 디버깅’ 문제가 1위를 차지할 것이고ㅋㅋ, 그 코드 크래시 중에서도 ‘예상하지 못한 변수 값(NPE!!NPE!!)’이 대부분의 케이스를 차지할 것이다 ㅋㅋ

변수값이 의도치 않게 바뀌는 것을 막기 위해서는

개발잘알인 저자는 여기서 ‘함수형 프로그래밍이 제공하는 부작용 없음(no side effect)과 불변셩(immutability) 라는 개념이 문제를 해결하는데 도움을 준다’ 고 한다.

개발알못인 나는 ‘부작용 없음’ 의 의미를 잘 알진 못하겠고, 객체 혹은 변수에 ‘불변성’이 부여되는 것은 무슨 의미를 갖는지 알고 있다.(사실 ‘불변성’을 잘 지키지 않아서 side effect -개발자가 예상치 못한 문제- 가 나는거니, 둘은 다른 개념이 아니라 전후관계에 있는거 아닐까..)

가변 데이터가 이리저리 공유된다

그림만 봐도 화가 남.

굳이 다른 설명이 필요 없다. 그림 하나로 다 설명된다. 이런 경우의 저 ‘리스트’ 변수는, 가변 데이터이면서 이리저리 공유된다.

심지어 저런 경우, ‘리스트’에 문제가 생기면 끔찍할만큼 디버깅이 힘들다. 클래스 C에서 Exception이 났다고 했을 때, 문제의 원인은 클래스 C뿐만 아니라 클래스 B에서 발생했을수도 있다.

심지어…. 클래스 A는 읽기만 하니까 제외…? 아니다. 소스를 보기 전까지 읽기만 하는지 쓰기만 하는지 어떻게 알아!?

이건 클래스 A,B,C의 코드가 문제(가 없진 않지만)라기보단, 애초에 하나의 변수가 이곳저곳 ‘공유’가 되는 상황 자체가 문제이다.(몇몇 생각없는 개발자는 여기서 A,B,C가 문제니 여기에 체킹 로직을 ‘견고하게(ㅋㅋ)’ 넣으라는 이야기를 한다…그게 나였었던 적이 있다.. 이러지 말자…)

그럼 어떻게 해야 함?

해결하기 제일 쉬운 방법은 ‘불변 객체를 이용’ 하는 것이다. 별도로 이 개념을 익혀야 할 러닝커브도 존재하지 않고, 공부가 필요할 추가적인 개념이 없으며(‘상수’, ‘불변한다’의 의미를 모르는 개발자가 있을까..?) 심지어 코드 작성조차도 어렵지 않다.

실제로 내가 속한 회사에서 이 방법을 이용해서 가변테이터의 공유를 줄여서 효과를 봤다. 이 방법을 사용한 이후부터 그 똥X레기같은 NPE가 진짜 눈에 띌 만큼 확연하게 줄어있음을 체감했다. 뭐, 어찌저찌 가뭄에 콩나듯 NPE가 난다 해도 추적과 디버깅이 쉽다. write하는 곳은 선언한 그 순간, only 한군데거든..

추가로, write하는 곳이 생성-선언- 시점 한군데이니까, 객체(변수)의 상태를 바꿀 수 없음 –> thread safe한 변수(?상수?) –> 딱히 큰 고민 없이 멀티코어 병렬성을 사용할 수 있다는 장점 또한 가져갈 수 있다.(읽는 데이터가 값이 일관적이니까)

선언형(declarative) 프로그래밍

(책 중간에 ‘무엇을’ 과 ‘어떻게’ 가 반대로 번역된 부분이 있는 것 같지만 기분탓이겠죠..?)

함수형 프로그래밍을 알기에 앞서, 함수형 프로그래밍의 기반을 이루는 ‘선언형’ 프로그래밍을 알아보자.

그리고 그에 앞서, 고전적인 프로그래밍 방식인 ‘명령형’ 프로그래밍에 대해 짧게 알아보자.

‘어떻게(how)’ 에 집중하는 프로그래밍…이 명령형 프로그래밍이다

아 뭐 이런거 있잖아…..
  • 리스트 사이즈만큼 반복을 돌려 (반복)
    • 리스트 안에서는 각각의 인덱스를 더해 (할당)
    • 혹은 반복되는 리스트 엘리먼트가 너보다 커? 작아? (조건분기)

별로다….. 뭐 별로가 아닐수도 있다고 생각 들지만, 그럼 ‘무엇을’에 집중한 코드 샘플도 보자.

위에거랑 완전 같은 동작이라고 아 ㅋㅋㅋ

‘어떻게’는 반복을 통해 분기를 쳐서, mostExpensive라는 변수에 값 할당을 한다는 것에 집중을 하였다.

명령형 프로그래밍은 결국 ‘최대값을 구하는 것이군!’ 이라는 결론 도출을 ‘코드’가 아닌 ‘코드를 읽은 개발자가 유추’ 해야 한다.

선언형 프로그래밍 샘플을 보자. 3번째줄에 떡하니 있는 ‘max’ 라는 메소드 명만 봐도 ‘아, 최대값을 뽑나보네’ 를 시작으로 코드 분석을 할 수 있다. 코드분석의 접근성 또한 차이가 생긴다. (‘반복’ 만 보고 ‘최대값’을 유추할 순 없잖아)

그래서 함수형 프로그래밍이란?

(사실 같은 개념이지만)Computer Science 에서의 함수 개념이 아니라, 수학에서의 함수 개념으로 받아들이면 이 ‘함수형 프로그래밍’을 이해하기 쉽다. 0개 이상의 입력-인수- 를 받고, 최소 한개 이상의 결과를 반환하는, 그리고 외부에 영향을 주는 부작용이 없는 바로 그 개념…

위의 ‘선언형’ 코드 샘플에서,

  • 우린 max가 오작동을 하지 않는 것에 대해 의심을 하지 않았다
    • max는 입력을 여러개의 숫자만으로 받을거야
    • max가 무조건 최대값을 내뱉어 줄거야

그렇다. 부작용이 있을 것이라고(실제 있더라도 사용하는 놈이 눈치를 못채는) 생각하지 않았다. 이게 함수형 프로그래밍의 핵심 개념이다.

함수형 자바

수학에서의 함수 개념은

  • 0개 이상의 입력
  • 1개 이상의 출력
  • 부작용 없음

이라고 했다.

case 1.

하지만 자바에선 Exception이라는 것이 있다.

요거 어떻게 해

Exception을 저 함수가 걍 먹을수도 없고…. 그렇다고 caller한테 예외상황을 숨겨서도 안되는 상황 (나눗셈 연산을 하는데 분모가 0인 경우)도 존재한다.

뭐 이런 경우는 Optional을 이용하면 문제를 해결할 수 있다고 가이드를 주었다.

case 2.

비함수형 동작을 감출 수 있는 상황에서만 부작용을 포함하는 라이브러리를 사용해야 한다.

함수가 동작을 수행하기 전에 미리 인수로 받은 자료구조를 따로 복사해 둔다던가 하는 방법으로….

참조 투명성

부작용을 감춘다는 함수형 프로그래밍의 핵심 개념은, ‘참조 투명성’의 개념과 귀결된다고 한다. (같은 말이지만)이때의 ‘투명’을 투명한 물 개념이라기보다는, 이 기부금을 ‘투명’하게 사용하였다는 개념으로 이해하면 ‘참조 투명성’에 대해 조금 이해가 빠르게 될 것 같다.

  • 특정 입력이 주어졌을 때… 언제 어디서나 같은 결과값을 뱉뱉해야 한다
    • Random.nextInt는 함수형 아님
      • 결과값이 투명하지 않다.
    • Math.max는 함수형임
      • 입력값이 동일하다면 언제 어디서나 호출하던 결과값이 투명하기 때문.

실전!

{1,4,9} 이렇게 숫자 3개로 이루어진 집합의 모든 부분집합을 내뱉는 함수형 프로그램을 하나 만들어 보자.

재귀!

이 코드에서 함수형이 들어간 개념이 insertAll과 concat일 것이다.

insertAll을 함수형 프로그래밍의 개념에 부합한 채 작성한다고 해 보자.

copyList라는 리스트 변수를 새로 만듦으로써 기존 인수인 lists에 아무런 영향도(부작용) 없이 메소드를 잘~ 짰다.ㅇㅇ

concat도 마찬가지

나쁜 예
좋은 예

나쁜 예의 a변수는 함수형 프로그래밍이라고 생각했던 concat메소드한테 배신 당했다. 인수를 집어넣었는데 인수가 변하는 마법을 시전당했기 떄문…..

좋은 예의 a변수는 코드를 보면 알겠지만 무사하다…

재귀와 반복

함수형 프로그래밍을 지향하는 코드를 작성하면, 코드에 반복문은(있더라도 매우매우 제한적으로) 존재하지 않는다는 것을 알 수가 있다.

  • 루프 조건문이 갱신되면 문제가 복잡해 진다…
    • 무한루프, 루프가 안돌거나..
요런 코드는 괜찮음 ㅇㅇ
얘는 안괜찮음

그래서 보통 루프를 이용한 코드는 왠만하면 재귀를 통해서 문제를 해결하기도 한다.

이론의 원천은 모르지만 모든 반복은 ‘이론적으로는’ 재귀가 가능하다고 한다…참고하자..

아 어려워
아 쉬워
어렵던 쉽던 걍 스트림 쓰자

근데 재귀는 남용할 경우 method call stack이 느무느무 많이 쌓여 의도치 않게 StackOverflowError를 뱉기도 한다.

이건 공간복잡도의 개념이고, 시간복잡도에서의 문제 또한 만만치 않다. 실제 이론적 시간복잡도는 동일하더라도, method call 이 한번씩 일어날떄마다 그 시간낭비가 계속 발생하기 때문이다..

그럼 어떻게 할까? method call stack을 쓸데없이 쌓지 않는 꼬리 호출 최적화 기법이라는 것이 있다.(알아만 두자)

call stack이 쌓이지 않게끔, 이미 사용한 연산을 재활용하는 기법이라고 하는데…. 현재 자바는 이 최적화를 제공하지 않는다고 한다.

  • (자바 한정)걍 반복문을 써도 되지 않을까 ㅇㅇ 나중에 생각하자

Leave a Comment