[java] 폴리글랏 프로그래밍

폴리글랏 프로그래밍

저자: 임백준

1.자바

람다와 클로저

클로저

클로저

바깥에 있는 큰 원은 작은 원을 둘러싸고 있는 외부의 ‘코드’를 나타내고, 내부에 있는 원은 큰 원이라는 코드에게 둘러싸여 있는 내부의 ‘코드’를 의미한다. 이 때 내부의 코드가 바깥에서 선언된 n과 같은 변수에 접근할 수 있으면, 이 작은 원 내부의 코드를 클로저라고 부른다. 이때 n처럼 클로저 바깥에서 선언되었으면서 클로저 안에서 접근되는 변수는 자유변수(free variable) 또는 사로잡힌 변수(captured variable)라고 불린다.

자바에도 클로저 ‘비슷한’ 것이 오래전부터 존재해왔다.

public void foo(){
    final int n = 1; 
    JButton button = new JButton("Click me");
    button.addActionListener(new ActionListener(){
        public void actionPerformed(ActionEvent e ){
            System.out.println("Clicked! n = " + n);
        }
      }
    );
}

=> 사용자가 마우스로 “Click me”라고 적혀있는 버튼을 누르면 콘솔 화면에 “Clicked! n = 1”이라는 내용이 출력된다. => 익명 클래스의 내용 혹은 actionPerformed 메서드 내부 코드가 foo()가 실행될 때 곧바로 실행되는 것이 아니라 미래의 어떤 시점에, 즉 사용자가 버튼을 누를 때 실행된다는 사실이다.

람다

람다가 없는 경우

public void foo(){
    final int n = 1; 
    JButton button = new JButton("Click me");
    button.addActionListener(new ActionListener(){
        public void actionPerformed(ActionEvent e ){
            System.out.println("Clicked! n = " + n);
        }
      }
    );
}

람다가 있는 경우


public void foo(){
    final int n = 1; 
    JButton button = new JButton("Click me");
    button.addActionListener( () -> System.out.println("Clicked! n = " + n );
    
}

=> 간결하고 아름답다.

람다

() -> System.out.println("Clicked!");

람다이면서 클로저

() -> System.out.println("Clicked! n = " + n );

조슈아 블로크

  1. 객체의 상태를 변경시키는 메서드를 제공하지 마라.
  2. 클래스가 상속되지 못하도록 (final로) 만들어라.
  3. 모든 필드를 final로 선언하라.
  4. 모든 필드를 private으로 선언하라.
  5. 변경불가능성을 만족시키지 못하는 컴포넌트에 대한 접근을 통제하라.

닐 게프터

프로그래밍 언어에 새로운 기능을 더하는 일은, 특히 그것이 언어의 타입 시스템 깊숙한 곳을 건드리는 기능일 경우에는 따져보아야 하는 부분이 너무나 많기 때문에 결코 쉬운 일이 아니다.

프레드 브룩스가 말하길,
“상위수준의 언어가 성취하는 것은 무엇인가? 그것은 비본질적인 복잡성으로부터 프로그램을 자유롭게 만드는 것이다. 추상적인 프로그램은 개념적인 구조물로 이루어진다. 그러한 구조물은 연산자, 데이터 타입, 열, 그리고 통신 등이다. 구체적인 기계어 프로그램은 비트, 레지스터, 조건, 분기문, 채널, 디스크와 같은 대상을 염두에 두어야 한다. 상위수준의 프로그래밍 언어는 이와 같은 구조물들을 추상적인 프로그램 안에서 완전히 사라지도록 만들어 주는 것이다.”

수많은 강연과 글과 책과 코드를 통해서 자바의 발전에 많은 공을 세워온 자바 진영의 맹장 닐 게프터가 갑자기 말머리를 돌려서 ‘적군’인 마이크로소프트의 C# 진영으로 투항을 했다는 소식이 들려온 것이다.

연산자 오버로딩

“어떤 의미에서 언어를 설계하는 과거의 방식은 그 자체로 프로그램에 대한 패턴에 해당했다. 하지만 지금 우리는 보다 메타적 접근이 필요하다. 이제 우리는 언어를 설계하는 것을 (특정한 프로그램이 아니라) 언어의 설계 자체에 대한 패턴으로 인식할 필요할 필요가 있다. 즉 동일한 종류의 도구를 만들어 내는 도구로서의 언어다. 내가 이야기하고자 하는 핵심이 바로 이것이다. 언어의 설계는 더 이상 ‘무엇’이 아니다. 그것은 패턴이다. 성장을 위한 패턴. 프로그래머들이 자신의 진짜 업무와 핵심 목표를 달성하기 위해서 사용할 수 있는 패턴(코드)를 정의하기 위한 패턴(특정 언어, 프로그램)를 성장시키는 패턴을 의미하는 것이다.
“그렇기 때문에 적어도 언어의 한 부분은 언어 자체의 성장을 도울 수 있는 방식으로 설계되어야 한다. 새로운 타입의 집합, 사용자가 정의한 새로운 타입, 새로운 어휘와 규칙을 언어에 더하고, 온갖 종류의 패턴을 활용하는 것이 가능해야 한다.”
“우리는 언어를 성장시키기 위한 도구 그 자체를 사용자의 손에 쥐어주어야 한다.”
“스티일은 이와 같은 연산자 오버로딩을 자바 프로그래머들이 다른 타입에 대해서도 자유롭게 수행할 수 있어야 한다고 생각했다. 이러한 연산자 오버로딩과 제네릭이 자바를 ‘성장이 가능한’ 언어로 만들 것이라고 믿었기 때문이다.”

john == smith
john.equals(smith);

=> 초급수준을 뛰어넘는 자바 프로그래머라면 두 문장의 차이를 잘 알고 있을 것이다. ‘==’ 연산자는 두 객체가 메모리상에서 동일한 주소를 가리키고 있는지 (즉 문자 그대로 동일한 ‘존재’인지) 여부를 비교하는 것이고, ‘equals’ 메서드는 두 객체가 Person 클래스를 작성한 프로그래머가 메서드 내부에 정의해놓은(Overriding) 알고리즘의 조건을 만족하는지 (즉 ‘이름’이 같은지, 혹은 ‘나이’가 같은지, 혹은 ‘피부색’이 같은지 등등) 여부를 검사하는 것이다. 이러한 미묘한 차이는 종종 프로그램에서 버그를 양산한다. 경험이 풍부한 프로그래머조차 내용이 같지만 메모리상에서는 별도의 위치를 점하는 두 개의 객체를 ‘==’연산자를 이용해서 비교하는 실수를 저지르는 것이다. 단순히 ‘이름’이 같은지 여부를 검사하기 위해서 그들의 ‘존재’를 비교하는 실수인 것이다. 혹은 반대로 두 객체가 완전히 동일한 ‘존재’인지 검사하기 위해서 그들의 ‘이름’을 비교하기도 한다. 이것은 프로그래머의 잘못이라기보다는 언어 자체의 문제다. 연산자 오버로딩이 가능하다면 이런 식의 미묘한 함정을 원천적으로 예방하기 위해서 Person 클래스의 작성자가 ‘==’연산자를 오버로딩할 수 있다. ‘==’연산이 메모리상의 주소를 비교하는 것이 아니라 객체의 내용을 비교하도록 정의하는 것이다.

: 예를 들어 ‘+’ 연산자를 오버로딩해서 다음과 같이 표현하는 경우를 생각해보라.

bradPitt + angelinaJolie

=> 여기에서 ‘+’연산자가 어떤 기능을 수행하는지 이해하는 것은 별로 어렵지 않다. 구체적으로 어떤 일을 수행하는지는 이러한 연산이 반환하는 타입을 살펴봐야 알 수 있겠지만, 그것이 아마도 결혼이나 연애처럼 남녀가 만나서 커플을 이루는 내용일 거라는 정도는 직관적으로 이해할 수 있다.


하지만 80년대에 C++가 메인스트림 언어로서는 처음으로 연산자 오버로딩을 지원하기 시작했을 때 당시의 프로그래머들은 ‘직관’과 ‘신중함’이 갖는 덕목을 알지 못했다. C++ 프로그래머들으 온갖 종류의 연산자를 가리지 않고 ‘오버로드’함으로써 자기가 짠 코드를 읽는 후임들을 무참한 곤경에 빠뜨렸다.
당시의 C++ 프로그래머들은 연산자를 오버로딩하기 위해서 회사를 출근했고, 다음날 연산자를 더 많이 오버로딩하기 위해서 일찍 잠자리에 들었다. 연산자 오버로딩을 말 그래로 ‘오버’로딩 했다.

직관적이지 않은 연산자 오버로딩의 예

a << 1

=> 여기에서 a가 정수면 ‘«’ 연산자는 a의 값을 1비트 왼쪽으로 이동시킨다. 따라서 a에 2를 곱하는 것과 동일한 연산을 수행한다. 하지만 만약 a가 출력스트림이면 ‘«‘연산자는 스트림에 1이라는 값을 적어 넣는 일을 수행한다. 보다시피 두 연산 사이에은 아무런 연관이 없다.(즉 전혀 직관적이지 않다.) 이 정도는 오버로딩된 연산자로 인해서 C++ 코드가 풍기는 악취의 시작에 불과하다.

결국 널리 사용되고 있는 메인스트림 언어 중에서 자바는 연산자 오버로딩 기능을 지원하지 않는 유일한 언어로 남게 되었다. 연산자 오버로딩이 포함된다고 해도 과연 그것이 스티일이 말한 것처럼 자바를 ‘성장이 가능한’ 언어로 만들어 줄지는 모르겠지만, 자바의 외연이 지금보다 확장되는 기회는 제공될 것이라고 생각된다는 점에서 오라클의 머뭇거림은 다소 아쉬운 일이다.