메인 컨텐츠로 가기

developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관 보기.

developerWorks에 처음 로그인하면 developerWorks프로파일이 생성됩니다.귀하의 프로파일에서 동의하신 내용이 공개되지만 이 사항은 언제든지 변경 가능합니다. 귀하의 성명(숨김으로 체크되어 있어도 표시됩니다)과 디스플레이 이름은 게시한 컨텐츠나 사이트 엑세스시 표시됩니다.

모든 정보가 안전하게 전송되었습니다.

  • 닫기 [x]

처음 developerWorks에 로그인할 때 프로파일이 작성되므로, 이를 위해 디스플레이 이름을 선택해야 합니다. 선택하신 디스플레이 이름은 developerWorks에 게시한 컨텐츠에 표시됩니다.

3글자 이상 31글자 이하의 길이로 사용 가능합니다. dW커뮤니티 내에서는 보안상 이메일주소를 제외한 다른 이름을 지정하셔야 합니다.

developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관 보기.

모든 정보가 안전하게 전송되었습니다.

  • 닫기 [x]

언어 설계자가 꼭 알아야 할 점: 먼저, 해를 끼치지 말자

때로는 새로운 언어 기능으로 인해 야기된 잘못된 코드가 올바른 코드를 상회한다.

Brian Goetz, 자바 언어 기술자, Oracle
Brian Goetz
Brian Goetz는 Oracle의 Java 언어 아키텍트로서 developerWorks에 오랫동안 많은 글을 기고하고 있기도 하다. Brian의 저작으로는 2002년에서 2008년까지 이곳에 발표한 Java theory and practice 칼럼 시리즈와 Java 동시성에 관한 결정판이라 할 수 있는 Java Concurrency in Practice(Addison-Wesley, 2006년)가 포함된다.

요약:  몇 가지 제안된 언어 기능이 문제점을 찾는 데 해결책이 된다고 하더라도 대부분은 프로그래머가 기존 기능으로는 그들이 원하는 만큼 쉽거나 분명하게 또는 간결하거나 안전하게 표현하고 싶은 것을 표현할 수 없는 실제 상황에 기초를 두고 있습니다. "이 기능을 이용하면 작성하고 싶은 코드를 작성할 수 있다"는 점을 유스 케이스로 염두에 두는 것이 좋다고 해도 언어 설계자는 그들이 사용할 수도 있는 잘못된 코드를 고려하여 언어 기능을 평가해야 합니다.

이 연재 자세히 보기

기사 게재일:  2012 년 2 월 06 일
난이도: 중급 원문:  보기 PDF:  A4 and Letter (34KB | 8 pages)Get Adobe® Reader®
페이지뷰:  1801 회
의견:  


이 시리즈의 정보

아마도 모든 Java™ 개발자는 Java 언어가 어떻게 개선될 수 있는지에 대해 생각해 본 적이 있을 것이다. 이 시리즈에서는 Java 언어 아키텍트인 Brian Goetz가 Java SE 7, Java SE 8 및 그 이상에서 Java 언어를 혁신하는 데 필요한 과제로 제시된 이 언어의 몇 가지 설계상의 문제점을 탐구한다.

언어를 처음부터 설계할 때, 필자에게는 언어 기능을 그룹으로 평가하고 이 기능을 조정하여 서로 상승 효과를 내게 할 수 있는 기회나 부정적인 상호 작용이 일어나지 않게 할 수 있는 기회가 있었다. 그리고 언어 기능을 선택하는 과정을 통해 장려하고 싶은 프로그래밍 이디엄과 심성 모델을 선택할 수 있는 기회가 있었다. 기존 언어의 새로운 언어 기능을 고려해 보면, 선택할 수 있는 옵션은 거의 없다. 다른 기능을 조정하여 이 새로운 기능을 수용하게 할 수 있는 경우는 별로 없으며(적어도 쉽게는) 특정 프로그래밍 이디엄은 이미 이 언어에서만 사용되는 특별한 것이다. 이러한 경우에 할 수 있는 최상의 방법은 이 이디엄을 중심으로 설계하는 것이다.

몇 가지 제안된 기능이 비현실적인 생각에 따른 것이라고 해도 대부분은 구체적인 유스 케이스에 그 근거를 두고 있다. 이러한 기능은 특정 이디엄을 표현하는 데 필요한 코드가 현재의 언어에서는 얼마나 투박하거나 장황하거나 취약한지에 대한 불만으로 인해 주로 생겨나며 이러한 기능에는 "그저 코드를 작성할 수 있기만 해도 좋지 않을까..."라고 하는 생각이 수반된다. 그러나 이 멋진 코드좋은 기능이 되려면 많은 시간이 필요하다. 분명한 점은 언어 기능이 바람직해지려면 이전에는 표현할 수 없었던 일부 "우수한" 새 프로그램을 표현할 수 있어야 하지만 새로운 언어 기능을 이용하면 "잘못된" 새 프로그램을 일부 나타날 수도 있다. 그리고 새 기능을 사용해도 "잘못된" 새 프로그램이 나타나지 않는다고 해도 이 기능으로 인해 기존 언어의 변종이나 사용자 기대 또는 성능 모델 특성이 손상될 수 있다. 기존 언어를 발전시키려면 표현성의 증가로 인한 이득과 안전이나 기능 상호 작용 또는 사용자 혼란으로 인한 손해 간에 균형을 유지해야 한다.

간단한 예: 오브젝트 전환

Java SE 7에 도입된 언어 기능 중 하나는 switch문에서 문자열 유형 변수뿐만 아니라 프리미티브 유형과 열거형 변수를 조작하는 기능이다. switch문이 미치는 범위를 문자열뿐만 아니라 다른 유형으로까지 확장하는 것은 수년 간 반복해서 요청돼왔던 주제였다. (예를 들면, RFE 5012262에서는 문자열뿐만 아니라 모든 오브젝트를 전환하고 equals() 메소드[참고자료 참조]를 통해 비교할 것을 요구한다.) 자주 요구되는 또 다른 비슷한 사항은 비상수형 표현식이 switch문의 case 레이블로 표시될 수 있게 하는 것이다.

얼핏 보면 switch문은 if ... else문의 중첩에 상당하는 구문상의 부가적 수단에 불과한 것처럼 보인다. 사실상 개발자는 코드에서 어떤 것이 더 좋아 보이느냐에 따라 주로 switch문과 if...else문의 중첩 간에서 선택을 하게 된다. 그러나 이 둘은 동일하지 않다. switch문 고유의 제한사항(case 레이블은 상수 값이어야 하며, switch 피연산자는 상수와 같이 작동하는 유형으로 제한됨)은 성능과 안전을 위한 것이다. 상수 레이블로 제한하면 분기 연산이 O(n) 연산 대신 O(1) 연산이 된다. if...else문의 시맨틱에서는 순차적인 실행을 요구하기 때문에 if...else문의 중첩에서 else 블록에 도달하려면 모든 비교를 수행해야 한다. case 레이블을 상수형 값(프리미티브, 열거형 및 문자열)으로 제한하면 비교 연산에서 부작용이 없어지고 이렇게 제한하지 않으면 가능하지 않을 특정 최적화가 가능해진다. 임의의 레이블을 case 레이블로 허용했으면 equals() 메소드를 호출할 때 예측할 수 없는 부작용이 발생했을 것이다.

언어를 처음부터 설계하고 있었으면 프로그래머의 편의가 예측 가능성보다 더 중요한지 여부를 결정하고 switch문의 시맨틱과 제한사항을 적절하게 정의하는 데 더 융통성이 있었을 것이다. 그러나 Java 언어의 경우에는 이러한 기회가 이미 사라졌다. switch를 상수형 값 이외의 것으로 확장하면 수년 간 Java 개발자가 빌드해 온 성능 모델이 손상되므로 switch에서 임의의 오브젝트를 허용하는 표현을 추가하면 커다란 대가를 치러야 한다. String 클래스는 변경되지 않고 정교하게 지정되고 제어되므로 이 클래스를 switch 안으로 가져오는 것이 실용적이었지만 거기서 그치는 것이 현명했다.


논쟁의 여지가 많은 예

Java SE 8에서 가장 중요한 새 언어 기능은 람다 표현식, 즉 클로저이다. 클로저는 함수 리터럴, 즉 값으로 처리하여 나중에 호출할 수 있는 연기된 연산을 구현하는 표현식이다. 그리고 이러한 표현식은 어휘적 범위가 지정되어 있기 때문에 클로저 안에 있는 기호의 의미는 클로저 밖에 있는 기호의 의미와 동일하다. (클로저 안에 있는 모듈로 로컬 변수 선언은 어휘적 범위 안에 있는 변수를 나타낸다.) Java SE 1.1 이후부터는 Java 언어에 취약한 클로저(내부 클래스)가 포함되었지만, 이러한 클래스의 한계와 성가신 구문은 코드를 데이터처럼 다룰 수 있는 메커니즘이 허용되는 추상 기능을 진정으로 활용하는 API를 개발하는 데 방해가 되었다.

언어에 클로저가 있으면 클라이언트가 연산을 약간 제공할 수 있게 되어 API로 더 협력적이고 더 풍부한 연산을 표현할 수 있다. 콜렉션 API는 이러한 작동을 제한된 형태로 지원한다. 다시 말해서 비교기Collections.sort()에 전달하지만 이러한 조작은 정렬과 같은 비교적 무거운 조작에만 한정된다. "크기가 10을 초과하는 요소로 구성된 목록을 작성하는 것"과 같은 단순한 조작의 경우에는 이렇게 하는 대신 다음 예제에서와 같이 클라이언트가 수동으로 해당 조작을 전개하도록 한다.

Collection<Element> bigOnes = new ArrayList<Element>();
for (Element e : collection)
    if (e.size() > 10)
        bigOnes.add(e);

이 코드가 매우 간결하고 읽기에 쉽다고 하더라도 Collection API는 많은 도움이 되지 않았으며, for 루프의 시맨틱은 직렬이고 이것이 콜렉션의 요소를 반복할 수 있는 유일한 수단이기 때문에 필자는 기본적으로 직렬 실행을 시행하기로 했다. 콜렉션에서 원하는 요소 서브세트를 추출하는 조작은 일반적인 조작이다. 모든 제어 논리(직렬 또는 병렬)를 원하는 요소의 조건부에 의해서만 매개변수화되는 라이브러리 루틴으로 이동할 수 있으면 좋다. 그러면 코드가 다음과 같이 줄어든다.

Collection<Element> bigOnes
    = collection.filter(#{ Element e -> e.size() > 10 });

이렇게 하기 위해 내부 클래스를 사용할 수도 있지만, 때로는 치료하는 것이 질병 자체보다 더 나쁠 수 있다는 점에서 내부 클래스는 사용하기에 너무 거추장스럽다. Collection 프레임워크가 개발되었을 당시에도 내부 클래스는 사용 가능했지만, 내부 클래스의 구문상의 오버헤드로 인해 내부 클래스의 사용을 조장하는 Collection API를 작성하는 것이 바람직하지 않았다. (Collection API에 대한 개선뿐만 아니라 람다 표현식의 구문은 일시적인 것이고 Java SE 8로 작성할 수도 있는 코드를 제시하는 데 불과하다.

이전 예제에 있는 람다 표현식은 특히 모범적인 유형의 람다 표현식, 즉 해당 어휘적 범위에서 아무 값도 캡처하지 않는 표현식이다. 그러나 다음 메소드에서 로컬 변수 n을 캡처하는 것과 같이 이미 범위에 있는 다른 값과 관련해서 연산을 표현하는 것이 가치가 있는 경우도 있다.

public static<T> Collection<Element> biggerThan(Collection<Element> coll, int n) {
    return coll.filter(#{ Element e -> e.size() > n });
}

내부 클래스와 람다 표현식의 한 가지 제한사항은 내부 클래스와 람다 표현식에서는 해당 어휘적 범위에 있는 final 로컬 변수만을 참조할 수 있다는 점이다. Java SE 8에 있는 람다 표현식에서는 final 변수(final로 선언되지 않았지만 초기에 할당된 후에 수정되지 않은 변수)를 효과적으로 캡처할 수 있게 함으로써 이러한 제한을 약간 더 완화한다. (inner-class 표현식이 인스턴스 컨텍스트에서 나타나는 경우에는 내부 클래스가 가변 인스턴스 필드를 참조할 수 있지만, 이것은 동일한 것이 아니다. 이것이 인클로징 클래스의 x 필드에 대한 참조(내부 클래스 안에 있는)이고 사실상 Outer.this.x를 단축한 것이라고 생각하는 것이 좋으며 여기에서 Outer.this는 묵시적으로 선언된 final 로컬 변수이다.) 이러한 제한사항이 있게 된 여러 가지 원인 중에는 로컬 변수 캡처를 final 필드로 제한하면 클로저가 참조를 복사할 수 있고 그로 인해 로컬 변수의 수명이 이 변수가 선언된 블록의 수명과 같아지는 작동을 유지할 수 있다는 점이 없었다.

어휘적 컨텍스트에서 불변 상태만을 캡처하도록 제한하는 것이 프로그래머를 매우 짜증나게 한다는 점은 의심할 여지가 없다. 결국에는 Java 언어에 클로저가 수용되겠지만, 표면적으로는 클로저의 이러한 특성이 때를 놓친 것으로 보인다는 점 때문에 프로그래머는 짜증이 많이 날 것이다.

가변 로컬 변수를 캡처할 코드의 일반적인 예제는 목록 1과 같다.


목록 1. 클로저로 가변 로컬 변수 캡처(Java SE 8에서는 금지됨)

int sum = 0;
collection.forEach(#{ Element e -> sum += e.size() });
System.out.printf("The sum is %d%n", sum);

이렇게 하는 것은 확실히 정상적이고 분명하기까지 하며 클로저를 지원하는 기타 언어에서는 이렇게 하는 것이 분명히 일반적인 이디엄이다. Java에서는 이 코드를 왜 허용하고 싶지 않을까?

첫째, 처음에는 그렇게 보이지 않지만, 이렇게 하면 로컬 변수의 시맨틱에 중대한 변화가 발생한다. 로컬 변수의 수명은 이 변수가 선언된 블록의 수명으로 제한된다. 그러나 람다 표현식은 값으로 처리되므로 변수에 저장될 수 있으며 캡처된 변수가 선언된 블록이 범위를 벗어난 후에도 실행될 수 있다. 가변 로컬 변수를 캡처하는 것이 허용되면, 해당 플랫폼에서 로컬 변수의 수명이 이 변수를 캡처하는 모든 람다 표현식의 동적 수명만큼 길게 확장되어야 한다. 이점은 프로그래머가 기대하는 로컬 변수의 시맨틱에 중대한 변화가 일어나는 것이며, 특히 이 변수가 수명이 긴 이상하고 새로운 로컬 변수 중 하나라는 점을 특별히 선언하지 않은 상태에서는 더욱 그러하다.

이러한 문제점은 forEach() 메소드가 다른 스레드에서 람다를 호출하여 해당 함수가 콜렉션의 다양한 요소에 동시에 적용될 수 있도록 한다는 점을 고려하면 더욱 심각해진다. 그리고 다중 스레드가 로컬 변수를 동시에 업데이트하려고 하기 때문에 로컬 변수 sum을 대상으로 데이터 경쟁이 발생한다. 현재는 데이터 경쟁이 없는 로컬 변수 액세스에 언제나 의존할 수 있기 때문에 로컬 변수를 대상으로 하는 데이터 경쟁은 새로운 형태의 위험이 된다. 목록 1에 있는 코드를 스레드로부터 안전하게 하고 이 이디엄이 반드시 발생되도록 할 수 있는 간단한 방법은 없다.

이 시점에서는 신중하다는 것이 물러선다는 것을 의미한다. 동시성 Java 프로그램에서 데이터 경쟁을 피하는 것은 바라는 것보다 훨씬 더 어렵다. 로컬 변수는 단일 스레드에서만 액세스할 수 있으므로 이러한 위험에서 벗어날 수 있는 한 가지 해결책은 로컬 변수가 데이터 경쟁에 영향을 받지 않게 하는 것이다. 그러나 람다 표현식으로 가변 로컬 변수를 캡처할 수 있게 하면 가변 로컬 변수를 로컬 변수가 아니라 필드와 같이 비가시적으로 작동하도록 하여 가변 로컬 변수가 데이터 경쟁의 위험에 노출되지 않게 향상 시킬 수 있다. 2011년에 동시 및 병렬 조작을 훨씬 더 위험하게 하는 방식으로 Java 언어를 발전시키려고 하는 것은 어리석은 일이다.

캡처할 수 있는 가변 로컬 변수를 캡처하는 람다의 시맨틱을 변수가 정의되어 있는 어휘적 범위 및 해당 스레드로 강제로 제한하는 가변 로컬 변수에 수정자를 정의함으로써(가변 로컬 변수를 원래의 로컬 변수와 명시적으로 차별화함) 이 이디엄을 구제할 수도 있다. 이러한 기능은 상충관계가 있기 때문에 특정 프로그래밍 이디엄의 실행 가능성을 유지하려면 언어가 복잡해지고 그나마도 본래 특성이 직렬인 언어가 구식이 되어버리고 만다.

더 나은 해결책

현재 이 이디엄을 지원하기 위해 추가적으로 복잡해지는 문제를 기꺼이 받아들이지 않는 이유는 동일한 결과를 얻을 수 있는 더 나은 방법이 있기 때문이다. 이 이디엄은 reduction 또는 folding 조작이 결합된 mapping 조작의 한 예이기 때문에 sum이나 max와 같은 연관 연산자가 값의 시퀀스에 쌍으로 적용된다. 연관성 덕택에 이러한 reduction 조작이 병렬화에 적합한 것이다. 다음과 같이 콜렉션에서 직접 mapReduce() 메소드를 노출할 수 있다.

int sum = collection.mapReduce(0, #{ Element e -> e.size() },
                               #{ int left, int right -> left + right });

여기에서 첫 번째 람다 표현식은 각 요소를 해당 크기에 맵핑하는 맵퍼이며 두 번째 람다 표현식은 크기를 두 가지 취하여 이 두 값을 더하는 리듀서이다. 병렬에 친화적인 방법을 사용한다는 점을 제외하면 이 코드의 결과는 목록 1에 있는 예제의 결과와 동일하다. (병렬 처리를 하려면 라이브러리가 병렬화 기능을 제공해야 하지만, 적어도 이디엄이 이런 방식으로 표현되는 경우에는 라이브러리로 이러한 조작을 병렬로 구현할 수 있다.) 맵핑과 리덕션이 병렬화를 따를 뿐만 아니라 맵과 리듀스 조작이 단일 병렬 패스로 결합될 수 있다는 점이 훨씬 더 효과적이다. (그리고 이 모든 조작은 클라이언트 코드에서 가변 상태 없이 수행될 수 있다.)

사실상, 이 코드를 맵퍼용 size() 메소드에 대한 메소드 참조와 정수 덧셈을 위한 사전 정의된 리듀서를 사용하여 더 간결하게 표현하게 된다.

int sum = collection.mapReduce(0, #Element.size, Reducers.INT_SUM);

이러한 방식으로 연산을 지정하는 개념에 익숙해지면, 이 코드가 문제 기술서처럼 여겨진다. 다시 말해서 이 코드는 콜렉션의 각 요소에 size() 메소드를 적용한 결과에 정수 덧셈을 적용한다.

논쟁하지 말자

대부분의 개발자들이 가변 로컬 변수의 캡처 제한에 대한 "차선책"(즉, 목록 2와 같이 가변 로컬 변수를 단일 요소 배열에 대한 final 참조로 대체하는 것)이 있다는 것을 파악하는 데는 시간이 많이 걸리지 않을 것이다.


목록 2. 단일 요소 배열에 대한 final 참조로 해당 컴파일러 속이기. 이렇게 하지 말자!

int[] sumH = new int[1];
collection.forEach(#{ Element e -> sumH[0] += e.size() });
System.out.printf("The sum is %d%n", sumH[0]);

이 코드는 컴파일러를 전달하기 때문에 "시스템을 속인다"는 만족감을 잠시 제공하지만, 이로 인해 데이터 경쟁이 일어날 가능성이 다시 발생한다. 이는 좋은 생각이 아니므로 이러한 유혹을 떨쳐버려야 한다. 이렇게 하면 테이블 톱의 톱날 덮개를 제거하는 것과 마찬가지로 사고 위험이 증가하게 된다. 그러나 테이블 톱을 사용할 때와는 달리 피해를 입는 것은 자신이 아니라 타인이 될 가능성이 높다. 이러한 조작(map-reduce)에 적합한 더 안전하고 더 빠른 이디엄이 있다는 점을 감안하면 "이 경우에" 이 코드가 안전하게 보인다고 하더라도 이처럼 안전하지 못한 코드를 작성할 만한 이유가 없다.


결론

새로운 언어 기능을 살펴보고 이 기능에서 사용하게 될 우수한 코드만을 파악하기는 쉽다. 필자는 이러한 기능을 수행할 더 나은 방법을 언제나 찾고 있지만, 새로운 언어 기능은 정말로 나쁜 결과를 일부 초래할 수도 있다. 잘못된 언어 기능을 도입할 때 발생하는 위험은 매우 심각하기 때문에 언어 설계자는 손익분석을 통해 장점이 단점을 상회하는지 분석할 때 보수적인 관점을 유지해야 한다. 의심이 드는 경우에는 Primum non nocere(먼저, 해를 끼치지 말라)라는 라틴 격언을 되새겨야 한다.


참고자료

교육

제품 및 기술

토론

필자소개

Brian Goetz

Brian Goetz는 Oracle의 Java 언어 아키텍트로서 developerWorks에 오랫동안 많은 글을 기고하고 있기도 하다. Brian의 저작으로는 2002년에서 2008년까지 이곳에 발표한 Java theory and practice 칼럼 시리즈와 Java 동시성에 관한 결정판이라 할 수 있는 Java Concurrency in Practice(Addison-Wesley, 2006년)가 포함된다.

잘못된 도움말 신고

부정사용 신고

감사합니다. 이 항목은 운영자가 관심을 표시했습니다.


잘못된 도움말 신고

부정사용 신고

제출실패 신고. 나중에 다시 실행해주세요.


디벨로퍼웍스 로그인


IBM ID가 필요하세요?
IBM ID를 잊으셨습니까?


비밀번호를 잊으셨습니까?
비밀번호 변경

developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관.

 


developerWorks에 처음 로그인하면 developerWorks프로파일이 생성됩니다.귀하의 프로파일에서 동의하신 내용이 공개되지만 이 사항은 언제든지 변경 가능합니다. 귀하의 성명(숨김으로 체크되어 있어도 표시됩니다)과 디스플레이 이름은 게시한 컨텐츠나 사이트 엑세스시 표시됩니다.

화면상에 보여지는 닉네임을 정하세요.

처음 developerWorks에 로그인할 때 프로파일이 작성되므로, 이를 위해 디스플레이 이름을 선택해야 합니다. 선택하신 디스플레이 이름은 developerWorks에 게시한 컨텐츠에 표시됩니다.

3글자 이상 31글자 이하의 길이로 사용 가능합니다. dW커뮤니티 내에서는 보안상 이메일주소를 제외한 다른 이름을 지정하셔야 합니다.

3개의 &이나 대쉬를 포함해주시고 31글자내로 제한해주세요.


developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관.

 


아티클 순위

의견

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=20
Zone=자바
ArticleID=791626
ArticleTitle=언어 설계자가 꼭 알아야 할 점: 먼저, 해를 끼치지 말자
publish-date=02062012

태그

Help
검색 필드를 사용하여 My developerWorks 내에서 해당 태그가 사용된 모든 종류의 컨텐츠를 검색하십시오.

태그를 더 많이 보거나 적게 보기 위해 슬라이더 막대를 사용하십시오.

인기 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 최고 인기 태그를 보여줍니다.

내 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 귀하의 태그를 보여줍니다.

검색 필드를 사용하여 My developerWorks 내에서 해당 태그가 사용된 모든 종류의 컨텐츠를 검색하십시오. 인기 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 최고 인기 태그를 보여줍니다. 내 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 귀하의 태그를 보여줍니다.