오늘은 계산기 과제의 제출일이었다. 과제에 대한 얘기를 두서없이 그냥 주저리 주저리 적어보려 한다.
Lv1: Calculator 클래스에 가감승제 연산을 수행하도록 하고, 이를 이용하여 연산 진행 및 출력
Lv2: 계속해서 입력받고, 출력값에 추가 연산을 가능하도록 하기
Lv3: 가감승제 각각을 연산 클래스로 만들고 Calculator 클래스와 관계 맺기.
Lv4(선택): 가감승제 클래스들의 부모로 추상 연산 클래스를 만들기.
Lv1은 금방이었지만 Lv2가 난관이었다. 계산 기능 자체 및 클래스화, 함수화는 문제가 아니었다. Lv1에서는 코드에서 그냥 호출하고 출력하니 문제가 없었는데, 계속해서 입력받아 명령을 수행하는 형태로 하려니 입력 받는 문자열을 처리하는 게 진짜 문제였다.
일단 명령을 어떤 포맷으로 받을 것인지. a+b로 받을 것인지, a + b로 받을 것인지, 혹은 "연산자를 선택하세요. 1: +, 2: - ..." 형태로 연산자를 선택하고 두 수를 차례로 받을지, 숫자 받고 연산자 받고 숫자 받을지, 연산자를 입력값 그대로 받을지... 그래도 역시 a+b나 a + b 형태로 받아야 계산기답다. 모두 붙여서 받는 것보다 띄어쓰기로 구분하여 받아서 나누는 게 쉬워서 a + b 형태로 받기로 했다.
그러나 또 문제가, 출력값에 추가계산을 하는 경우 앞 숫자 없이 + 3 형태로 받아야 한다. 그러면 첫 토큰(문자열 한 덩어리)이 숫자인지 연산자인지도 구분해야 한다. 이에 따라서 뒤에 뭘 받을지도 결정이 되고.
뭣보다 예외처리가 너무 귀찮다... 아예 입력을 안할 경우, 숫자로 변환이 안되는 입력인 경우, 연산자 다음 숫자가 아닌 경우, 지원하지 않는 연산자인 경우... 역시 문자열 다루는 건 지옥이다.
그리고 또! 문제가 있다. 정수와 정수를 계산하면 정수로 나오지만, 둘 중 하나라도 소수라면 계산 결과도 소수로 내보내야 했다. 문제는 하나의 함수는 하나의 반환형을 갖는다는 거다. 거기다 만약 경우에 따라 다른 반환형을 내보낼 수 있다고 쳐도 이 함수를 호출해 받는 쪽에서는 받으려는 변수에 맞는 자료형이어야 하기에 애초에 불가능하다. 이 얘기는 뭐냐? 함수를 따로 만들어야 한다는 거다. 그것도 입력값이 정수 정수, 정수 소수, 소수 정수, 소수 소수인 4개의 경우에 대해서 말이다. 여기서 끝이 아니다! 이전 결과값에 추가 계산을 하는 경우, 입력 매개변수는 한 개가 되어 다른 함수가 되고, 그것도 정수와 소수의 경우로 나뉜다. 이렇게 6개의 경우로 나뉘는데, 여기서 끝이 아니다! 연산자가 4종류이므로 총 24개의 함수가 생기는 것이다. 대충 힘들다는 말이다.
아무튼 열심히 짜기 시작했다. 문자열을 나눠서 접근하는 것으로 StreamTokenizer가 생각났다. 백준에서 빠른 입력을 위해 유용하게 사용하던 터라 아주 좋을 것 같았다. 수 시간을 코딩하고 돌려보는데... 큰 문제가 있었다. 연산자를 안먹는다. 토큰이 숫자값이면 -1, 문자값이면 -2 이런 식으로 종류가 나오는데, 엉뚱하게 43이 나온다. 소스 파일을 살펴봐도 43이란 숫자는 안보이고, StreamTokenizer로 연산자를 받는 방법을 찾아봐도 안나온다. 한참 알아봤지만, 결국 포기하고 그냥 split(" ")으로 갈아엎어야 했다. 그렇게 또 수 시간을 코딩하고... 결국 Lv2를 구현했다.
Lv3를 구현하는건 어렵지 않았다. 연산을 클래스로 모아주고 연동만 해주면 되니까. 하지만 이 때 약간 딜레마가 생겼다. 이전 계산 결과값을 저장하는 역할은 연산 클래스에 넣을 수 없고 계산기 클래스가 갖고 있을 수밖에 없다. 그렇다면 추가 계산의 경우는 연산자로 전부 보내버릴 수 없고, 계산기 클래스에서 약간 들고있는 형태가 되어버린다. 이런 구조가 썩 맘에 들진 않았지만, 다른 방도가 딱히 없어보여 그냥 진행했다.
연산 클래스를 만들 때는 굳이 인스턴스를 가질 필요가 없어서 object로 생성했다. 연산 클래스에 들어있는 거래봤자 fun op(a: Double, b: Double): Double = a + b 이런거뿐이니 말이다. 아무튼 이리 하여 Lv3까지 완성했다.
그리고 Lv4 추상화가 남았는데... 도무지 추상화해서 뭘 해야 할지 감이 안왔다. 그냥 무지성 추상화를 해봐야 코드만 복잡해질 뿐이라서.. 추상화는 놔두고 중간제출을 했다. 중간제출 이후 해설영상이 올라온다고 했으니 이걸 보고 추가할 생각이었다.
그리고 올라온 해설 영상. 튜터님의 코드는 내 구조와는 사뭇 달랐다. 나는 계산기 객체가 각 연산자 오브젝트의 연산 함수를 호출하는 형태로 만들었고, 튜터님은 각각의 연산자가 따로 따로 계산기 객체의 연산자 객체로 들어가서, 4개의 계산기 객체에 1개씩의 연산자 객체가 존재하는 형태였다. 그래서 계산기 클래스가 서로 다른 연산자 클래스를 업캐스팅해서 부모클래스 매개변수로 받을 수 있어야 했고, 부모클래스 메서드로 연산을 호출하면 연산클래스에서 오버라이딩한 함수가 호출되어야 하므로 추상화가 필요한 형태였던 것이다.
그래서 해설 영상을 보고 난 뒤 내 구조에서 추상화를 어떻게 적용할까 고민해봤는데... 적용할만한 부분이 없었다. 나는 연산클래스는 object로 만들고 계산기는 함수를 직접 호출하니까 딱히 매개변수로 받지도 않고, 호출도 따로 하고 있어서 추상화를 한다고 이득이 되는 부분이 없었다. 결국 그냥 Lv4 추상화는 적용하지 않기로 했다. (못한게 아니라 안한겁니다!)
Lv4를 하는 대신 main에서 수행하던 각종 문자열 예외처리와 함수 호출 등을 계산기 클래스에 넣어주었고 main은 무한루프와 리셋, 종료 정도만 수행하게 만들었다. 그리고 4개의 연산클래스를 모두 다른 파일에 만들었는데, 개별 파일로 작성하기에는 코드가 너무나도 적어서 오히려 파편화가 되어버렸기에 한 파일로 모아줬다.
(추가)
내가 작성한 코드에 대해 글로 작성하다보니, 내 프로그램 구조에 대해 문득 깨달음을 얻었다. 내 계산기는 입력 처리를 수행하는 implement와 계산을 수행하는 operate로 나뉘어 있는데, operate가 implement에서 할만한 일을 일부 수행하고 있어서 각기 다른 함수가 불필요하게 많이 존재하고 있었던 것이다. 그래서 둘 사이의 관계를 분명히 하고 역할을 나눠줬더니.. 200줄이 넘었던 코드가 90여줄로 줄어들었다...