[clearfix]
1. 개요
프로그래밍 언어 Java의 평가에 관해 정리한 문서이다.2. 긍정적 평가
2.1. 수많은 개발자와 레퍼런스
나온 지도 오래되었고, 다른 최신 언어에 비해서 여러 가지로 욕을 먹는 Java가 그래도 항상 상위권을 유지하는 이유는 바로 수많은 개발자와 레퍼런스를 보유하고 있다는 점이다. 타 언어를 전문적으로 사용하는 개발자들도 Java 정도는 할 줄 안다고 할 정도로 배우기 쉽고 대중적이다. 대중적인 언어라서 참고 자료나 오픈 소스가 많고, 그러한 자료들을 바탕으로 수많은 대형 프로젝트들이 진행되어 왔기 때문에 많은 부분에서 안정성이 입증되었다. 즉, 안정적인 인력 풀을 유지하면서, 알려진 위협을 제거하고 운영 노하우가 많은 검증된 언어라는 것.하지만 검증되었다는 건 반대로 말하자면 오래되었다는 뜻이기도 하다. 최근에는 Java에 대한 개발자들의 불만이 누적된 탓인지 JavaScript나 Python 같은 다른 언어를 선호하는 경향이 강하다. 국내에서는 전자정부표준프레임워크의 존재 때문인지 아직도 신규 프로젝트의 주 언어로 Java를 선호하는 경향이 강하지만, 세계적으로는 신규 프로젝트에서 Java를 선호하는 비중은 높지 않은 편이다.[1]
국내에서의 언어 외적인 장점은 바로 개발자 구인의 용이성이다. 국내에서 Java 개발자의 인력 풀이 타 언어보다 더 큰 이유는 앞서 말했던 전자정부표준프레임워크의 존재 때문이기도 하고, 그 때문에 Java 개발자를 정부에서 국비 지원으로 대거 양성했기 때문이기도 하다. 상당수의 정부 하청 프로젝트가 Java와 전자정부표준프레임워크로 개발되었다. 이런 이유 때문에 굳이 정부 프로젝트가 아니더라도 Java 개발자를 구인하기 쉬웠고, 더 많은 프로젝트가 Java로 개발되었다. 그리고 그렇게 만들어진 프로젝트를 유지 보수 하기 위해서 더 많은 Java 개발자가 필요하므로 더 많은 인력이 유입되는 일종의 선순환 효과가 있다. 취업 사이트를 확인해 보면 다른 분야보다 Java 개발자를 구인하는 경우가 많은 건 이 때문이다. 사람을 구하기도 쉽고, 직장을 구하기도 쉽다. 영어권 국가에 비해 공유되는 자료가 비교적 부족한 국내에서도 JSP, Spring에 관한 자료만큼은 높은 퀄리티를 보여주는 경우가 많다. 그러나 반대로 할 줄 아는 사람들이 너무 많아 경쟁력이 떨어지는 면도 있다. 몇몇 회사에서는 Java 개발자에 대한 보수나 기타 대우가 좋지 않은 경우도 많다. 개발자가 많다는 것은 취업이나 구인 면에서는 장점이지만, 다른 측면에서는 단점이 될 수도 있다.
기존의 Java로 만들어진 프로젝트를 재사용하기 위해 Java가 쓰인 대표적인 사례는 카카오뱅크가 있다. 한정된 시간 때문에 Java 코드를 재사용해야 했던 경우다. #
2.2. 기기 호환성
장점으로는, 해당 운영 체제에 Java Virtual Machine(JVM)을 설치하면 Java로 만든 프로그램은 어떤 컴퓨터에서도 완벽히 똑같이 동작한다. 가상 머신이 각각의 운영 체제에 맞춰서 결과적으로 완벽히 똑같이 돌아갈 수 있도록 제작되는 덕. 가상 머신 없는 운영 체제라면 아예 Java 프로그램을 사용하지 못하겠지만, 썬 마이크로시스템즈는 주요 OS용의 가상 머신을 발표하고 있고, IBM, 휴렛팩커드 등의 회사는 직접 자사 운영 체제용 JDK/JVM을 제작하여 발표하며, 이들과 상관없이 독립적으로 특화된 성능 향상 기능을 가진 JVM을 만들어서 발표하는 회사도 존재한다. 그래서 이 부분은 보통 단점으로 꼽히지 않는다. 오히려 여러 운영 체제에 발 벌리는 업체라면 윈도우용, 맥용 등을 따로 제작할 필요 따위가 없이 '그냥 하나 만들면 끝!'이라고 Java 초창기에 홍보되었다.
그러나 다른 크로스 플랫폼 언어들과 마찬가지로 각 플랫폼마다 미묘하게 기능이나 작동에 차이가 있는 부분이 결국은 존재하기 때문에, 이러한 부분을 고려하지 않고 작성된 프로그램을 그대로 다른 데에서 돌릴 때에 문제가 발생할 가능성이 존재한다. JVM의 장점은 그나마 이런 부분들이 다른 언어에 비해서 매우 적은 편이라는 점이다. 이런 경우, 대부분 크로스 플랫폼으로 작성된 코드가 그러하듯 타겟 플랫폼을 인지하여 특정 플랫폼에서는 다르게 동작하게 하는 식으로 코딩을 하게 된다. 주로 java.nio 패키지에 속한 API에서 이러한 경우를 발견할 수 있으며, OS X에서만 일부 특이하게 동작하는 MIDI 관련 API 또한 이러한 경우에 속한다. Java의 모토는 Write once, run everywhere(한 번 짜서, 어디서나 실행하라)인데 프로그래머들은 이를 비꼬아 Write once, test everywhere(한 번 짜서, 모든 플랫폼에서 테스트하라)라고 말하곤 한다.
C나 C++에서도 크로스 플랫폼 형태로 소스 코드를 작성하는 것이 가능하기는 했지만, 개발자가 타겟 플랫폼과 해당 플랫폼용 바이너리 코드를 생성하는 컴파일러에 대해서 잘 이해하고 나서 #define, #if 등 전처리기를 써서 각 타겟 플랫폼에 맞게 동작하도록 코드를 직접 작성해 줘야 했다. 이와 달리, Java는 단일 소스 코드를 컴파일하여 생성된 바이트코드 클래스 파일을 JVM이 존재하는 환경이라면 어디서나 (대부분은) 추가 컴파일이나 수정 작업 없이 그대로 똑같이 실행할 수 있다는 점이 차이점이다. 기업 입장에서 컴파일된 바이너리가 크로스 플랫폼을 보장해 주는 Java는 매력적인 언어였다.
JVM 위에서 구동 가능한 언어는 Java 외에도 존재하는데, 이 언어들을 사용하면 Java와 동일한 수준의 호환성을 구현할 수 있다.[2] 구글에서 안드로이드 개발의 차세대 언어로 밀어주는 Kotlin이 대표적인 케이스다. Java의 개발 주체인 Oracle에서 개발하는 GraalVM이라는
요즘은 하드웨어와 밀접하게 연동되어야 하는 프로그램이 아니라면, 또는 메인 로직은 서버에서 돌아가고 클라이언트에서는 인터페이스만 제공해 주면 되는 경우라면 JVM보다는 웹 기술을 이용한 방식이 더 주목받고 있다. 아예 어디에서나 동일한 동작을 보증하는 웹사이트의 형식으로 서비스를 하거나, 네이티브 앱의 UI가 필요한 경우에는 React Native 또는 Flutter 등의 프레임워크를 이용하는 경우가 많다. 전자는 웹 개발에서 주로 쓰이는 JavaScript를 이용하고, 후자는 구글에서 웹 프로그래밍의 용도로 만든 Dart라는 언어를 이용한다. 이 외에 게임이나 3D 렌더링이 필요한 앱의 경우 게임 엔진을 이용한다.
2.3. 안정성
다른 언어에 비해 높은 안정성을 꼽고 있다. 우선 C나 C++에 안정성 문제가 제기되는 포인터 연산자[4] 및 메모리 직접 접근 함수들을 지원하지 않는다. 여기에 C++과는 다르게 다중 상속을 허용하지 않는다. 이는 객체 지향의 특성 중 하나인 '상속'의 자유도를 확 떨어트리는 것이기에 언뜻 보기에는 객체 지향적 관점에 위배되는 것처럼 보일 수 있으나, 반대로 오히려 이게 더 객체 지향적이라고 볼 수도 있다. 객체 지향의 목적 자체가 재사용을 통한 생산성의 향상과 관리상의 이점인데, 다중 상속은 잘못 사용할 시 극도로 복잡하게 꼬인 프로그램을 만들 위험성을 갖고 있다. 물론 코드 관리의 측면에서도 다중 상속에 의해 발생하는 문제는 좋지 않다. 수준 높은 프로그래머라면 이 문제도 잘 해결할 수 있지만, Java는 아예 미연에 방지하기 위해 다중 상속을 언어 스펙에서 제거하는 방법을 택했다.[5]메모리를 대용량으로 사용하는 프로그램에서 상대적으로 C/C++보다 안정적인 모습을 보일 때도 있는데, 이런 경우에 JVM이 시작될 때 필요한 메모리를 먼저 통으로 잡아버리기 때문이다. 메모리를 자주 할당하거나 해제하는 C/C++ 프로그램은 오히려 Java보다 성능이 느릴 수 있다. 다만 이는 메모리 할당자 없이 매번 힙 영역 메모리를 운영 체제로부터 할당받는 경우에 해당하는 말이고[6], 실제 C/C++ 프로젝트에서는 jemalloc 등 메모리 할당자 라이브러리를 사용하거나 메모리 할당자를 직접 구현하여 이런 문제를 해결하는 것이 일반적이다.
3. 부정적 평가
3.1. JVM 로딩 속도 문제
Java의 심각한 단점 중 하나는, 실행하는 과정에서 Java Virtual Machine이 반드시 완벽하게 로딩되어야 하기 때문에 프로그램의 초기 시작 시간이 완전한 이진 코드로 컴파일된 프로그램을 실행하는 것에 비해 오래 걸리는 것이다. 단적인 예로, 아무것도 안 하고 콘솔 화면에 달랑 "Hello, World!"라고 찍기만 하는 프로그램이 실행되는 데에도 thread가 10개쯤 뜬다. 특히 그 프로그램에 AWT, Swing, SQL같이 불필요한 기능을 끌어들이는 것은 매우 심각한 문제이다. 이 문제는 런타임 자체가 아직 모듈화되지 않았다는 점에서 기인한다. 안드로이드는 이 문제 때문에 JVM을 안 올리고 달빅 JIT, 안드로이드 런타임으로 아예 컴파일을 하는 식으로 대응한다.하지만 요즘 같은 고사양 컴퓨터에서는 아주 많은 라이브러리를 끌어오는 것이 아니라면 체감상 차이는 크게 나지 않는다. 또한 Java 9부터는 드디어 런타임 라이브러리를 모듈화하고 있으므로, 필요한 모듈만 끌어서 프로그램을 짤 수 있다.
3.2. 가상 머신 바이트코드 실행 속도 문제
C/C++, Pascal, Fortran과 같은 언어와 달리, Java는 바이트코드로 된 프로그램을 실행하기 위해 운영 체제와 프로그램 사이에 JVM이라는 두꺼운 계층이 하나 더 자리 잡게 된다. 그리고 바이트코드는 실시간으로 각 타겟 플랫폼용 기계어로 번역되어 실행된다. 덕분에 JVM만 설치되어 있다면 어느 운영 체제나 CPU이든 간에 자바 프로그램이 실행될 수 있지만 네이티브 바이너리 코드를 출력하는 언어와 비교하여 실행 속도와 성능에 일정 부분 손실이 발생한다. AWT, Swing 같은 GUI 라이브러리를 사용할 때도 심각하게 느린 것을 체감할 수 있다. 이런 문제점을 썬 마이크로시스템즈도 곧 깨달았고, 최초 발표에서 2년 후인 1998년부터 JIT 컴파일러를 JVM에 내장하여 성능이 상당 부분 개선되었다. 하지만, 그만큼 메모리가 뒷받침해 줘야 한다. 현재는 보통 같은 기능/알고리즘을 실행하는 데 C++보다 2~3배 정도의 시간이 더 필요하다고 알려져 있다. 이 부분은 꽤 초기부터 지속적으로 개선되어 왔기 때문에 현재 실행 속도 자체에 대한 이슈는 예전에 비해 많이 줄어든 편이다.이 문제는 Java 9에서 '선행 컴파일'이라는 이름으로 개선될 예정이다. JIT 컴파일로 실행과 동시에 컴파일을 하는 게 아니라 기존의 정적 컴파일처럼 바이트코드를 미리 기계어로 번역하면, 컴파일 속도는 다소 느려지지만 실행 속도는 빨라지게 된다. 물론 컴파일 한 번으로 여러 플랫폼에서 동일하게 실행시키는 건 불가능해진다.
3.3. 가비지 컬렉션에 의한 실행 지연 문제
가비지 컬렉션에 의한 메모리 프리징 현상[7]이 초반부터 지속적으로 Java를 괴롭혔다. 멀쩡하게 동작해야 할 프로그램이 순간적으로 뚝뚝 끊기는 듯한 현상이 발생하는 것. 오늘날 Java의 문제는 바이트코드 변환으로 인한 속도 저하보다 이 가비지 컬렉션의 영향이 더 크다고 할 수 있다. 이러한 문제점은 가비지 컬렉션을 지원하는 다른 프로그래밍 언어들도 마찬가지이긴 하지만 실행 속도와 함께 Java 초기부터 꾸준히 문제로 꼽혀온 것으로, 버전이 올라갈 때마다 다양하게 개선되어 왔다.Java 8부터는 G1 가비지 컬렉터가 기본 설정으로 바뀌었다. G1 GC는 메모리 누수를 일으키던 메소드 영역의 PermGen Area를 제거하여 static 인스턴스와 리터럴 문자열도 GC의 대상이 되도록 바뀌었으며, 클래스, 메소드, 배열의 메타 정보는 동적 리사이징이 가능한 Metaspace로 이동시켜 시스템 힙 영역에 저장된다. 덕분에 JVM 힙 영역의 공간이 늘어나고 PermGen Area를 스캔/삭제할 필요가 없어져 GC의 성능이 대폭 향상되었다.
3.4. 불편한 예외 처리
다른 객체 지향 언어들처럼, Java 역시 try~catch문으로 대표되는 예외 처리를 할 수 있다. 대부분의 언어에서 차용하고 있는 좋은 기능이지만... 유독 Java는 다른 언어와는 달리 프로그래머의 검사가 필요한 예외(Exception을 직접 상속하는 예외 클래스)가 등장한다면 무조건 프로그래머가 선언을 해줘야 한다. 그렇지 않으면 컴파일조차 거부한다. 원래 의도는 철저한 예외 처리를 하니까 만약에 발생할 수 있는 모든 상황에 안정성을 확보할 수 있겠지...였으나, 결국 대부분의 경우엔 귀찮다는 이유로, 가장 일반적인 예외인 Exception대부분의 다른 언어에서는 원하는 에러만 try-catch문으로 뽑아내고 그렇지 않은 경우에는 그냥 아무 처리를 해주지 않아도 된다. 이러한 언어를 접하던 사람이 Java를 접하면 그 특유의 경직된 예외 처리에 불편해하기도 한다. 오히려 명시적으로 예외 처리를 할 수 없는 경우도 존재하는데, 인터페이스를 상속받을 때 인터페이스에 선언된 예외가 아니면 구현 클래스에서 그 예외를 던질 수 없다! 특히, Java에서 제공하는 Iterator 인터페이스에는 throws 선언 따위는 없기 때문에 Iterator를 구현받았을 때 명시적으로 예외를 던질 수 없다. 이 상황을 해결하려면 RuntimeException 계열을 쓸 수밖에 없는 상황이 펼쳐진다.
다만, 상기의 내용은 실무적 접근에 의한 내용이고, 실제로는 이는 장점으로도 취급되기도 한다. Assert문을 자유자재로 쓰면서 예외 처리를 하거나 코딩과 동시에 발생할 수 있는 각종 예외들을 인지하고 처리해 주는 걸 잊어먹는 경우에 대한 대처가 가능하다.[11]
3.5. 소스 코드 길이
Java는 소스 코드의 길이가 다른 언어에 비해 상당히 긴 편이다. 같은 기능을 하는 코드를 짠다고 했을 때 다른 언어에 비해 입력해야 할 양이 많다. 구체적으로 말하자면 일명 Boilerplate라고 부르는, 기본적인 구조를 짜기 위해서 무조건 의무적으로 작성해 주어야만 하는 서식과 코드의 분량이 많다.인터프리터 언어에서는 puts("Hello") 정도로 끝났을 일을 Java에서는
#!syntax java
class Main {
public static void main(String args[]) {
System.out.println("Hello");
}
}
이만큼을 써야 한다.[12]
같은 일을 하는 Kotlin 언어 코드는
#!syntax kotlin
fun main()=println("Hello")
C언어 코드는
#!syntax cpp
main() {
puts("Hello");
}
[13]C# 10.0의 경우[14][15]
#!syntax csharp
Console.WriteLine("Hello");
JavaScript, TypeScript의 경우
#!syntax javascript
console.log("Hello")
Python, Lua, Swift의 경우
#!syntax python
print("Hello")
Ruby의 경우
#!syntax ruby
puts "Hello"
위키책에 있는 Hello World 프로그램의 목록이나, 나무위키 내의 프로그래밍 언어/예제 문서를 보면 하이레벨 언어 중에서는 코드양이 긴 편인 걸 알 수 있다.
오죽하면 이런 포스트가 만들어질까. 물론 이건 Java의 문제가 아니고 마세라티 문제[16]라고 알려진 프로그래머의 과욕이 부른 참상이지만 코드에 유연성을 조금 추가하기 위해 써 넣어야 할 코드의 길이가 기하급수로 증가한다는 하나의 예시로 볼 수 있다. 참고로 저 포스트의 5년 차 코드는 Spring의 패러디다. Java 이후에 나온 차세대 언어들은 같은 수준의 유연성을 확보하기 위해 들여야 할 노력의 양이 훨씬 적다.
이렇게 의도적인 장황함(verbosity)을 추구하는 언어 설계와 커뮤니티의 문화가 아이러니하게도 위에서 언급한 장점이 무색하게 가독성을 저해하는 요인이 되기도 한다. 같은 기능을 하더라도 수십 줄의 보일러플레이트 코드를 가지는 Java 코드보다 다른 언어의 코드가 보통은 더 읽기 쉽기 때문.
게다가 다른 하이레벨 언어(C#, Python, Ruby 등)에 비해 문법적 설탕(Syntactic sugar)[17]이 적어 이쪽에서 넘어오면 꽤 불편해하는 편. 하지만 최근 Java 8로 넘어오면서 람다 표현식, 스트림[18] 등을 지원하는 식으로 문법적 편리함을 늘려가는 추세다. 이 흐름은 다음 Java 9에서 더욱 강화될 것으로 보는 추세.[19]
그러나 무조건 코드양이 많다고 해서 나쁜 것만 있는 것이 아니다. 일단 Java는 클래스 지향적이기 때문에 어쩔 수 없는 부분이고, 커다란 프로젝트 단위에서 봤을 땐 오히려 클래스와 메소드, 변수의 소속이 확실하기 때문에 코드를 금방 파악할 수 있다. 축약어의 사용을 최대한 자제하는 방향으로 만들었기 때문에 그렇다.[20] Python이나 JavaScript 같은 동적 타입 언어들은 소규모 프로젝트에는 좋겠지만 대형 프로젝트에서는 불편할 수도 있다. 또한 IntelliJ IDEA라는 혁신적인 IDE가 등장한 이후 코드 타이핑의 불편함은 대폭 감소했다고 봐도 된다. 오히려 이렇게 명확한 타이핑을 통해 디버깅에서 있어 오류를 줄여주는 점을 좋아하는 개발자도 상당히 많아서 코드 양에 대한 부분은 자바를 통해 입문하는 개발자에 한해 문제가 되는 경우가 많고 실제 사용에서는 거의 영향이 없다. 다만 C#에서도 그렇지만 프린트문 자체에는 불만을 표하는 경우가 꽤 있기도 하다.
JetBrains에서 개발한 Kotlin은 바로 이 Java의 언어적 불편함을 최소화하려고 나온 새 프로그래밍 언어이며, 카카오에서도 카카오톡 메시징 서버에 Kotlin을 도입하는 등( #) Java를 Kotlin으로 대체하려는 움직임이 조금씩 나타나다가, 이후에는 아예 안드로이드 스튜디오에서 앱 개발 기본 언어로 지정되기까지 했다.
3.6. 언어적 불편함
3.6.1. 명사형 사고를 강제
Java는 모든 동작이 객체 상위에서 이루어지게 함으로써 명사형으로 생각하는 것을 강제한다. 그 결과로 Java에는 전역 함수가 없고 모든 함수는 어떤 클래스에 종속되어 있다. 이 때문에 기능적인 부분을 작성하는 데 자잘한 클래스들을 작성해야 한다는 불편함이 있다.이런 명사 중심적 생각은 확실히 많은 경우 편리하나 동사 중심으로 생각해야 하는 상황도 생각보다 흔하다는 게 문제. 예를 들어서, 퀵소트를 Java에서 엄격하게 의도된 대로 짜려면 quickSort(array)라는 함수 대신 QuickSorter라는 객체의 생성자에 배열을 넣고, 그 생성자를 참조하는 참조 변수를 이용해 run()을 호출하여 동작시켜야 하는 것이다.[21]
디자인 패턴을 사용해서 어느 정도 동사형 사고방식으로 코드를 작성할 수 있긴 하다. 디자인 패턴에서 핵심적 지위를 차지하는 인터페이스라는 놈을 사용하면 상당히 동사적인 관점으로 객체를 다룰 수 있다. Java의 리플렉션 API를 사용하는 방법도 있고. 하지만 애초에 언어가 생겨먹은 것 자체가 명사 기준으로 생각하게 디자인된 건 사실이다. Java의 근간을 이루는 표준 java.lang 클래스와 java.util 클래스를 동사형 사고방식으로 재작성하기 전에는 힘들다. 이 재작성 삽질은 이미 Scala에서 해 놨으므로 Java의 이러한 특징이 싫다면 Scala를 쓰면 된다.
3.6.2. 클로저 미지원
명사형 생각을 강제한다는 것의 연장선. 버전 7 이하의 Java는 함수를 일급 객체로 취급하지 않는다. 어떤 '동작'을 넘겨야 할 때는 그 동작을 추상화한 인터페이스를 만들고, 그것을 임시 클래스로 구현한 뒤, 그 객체를 파라미터로 넘겨야 한다. 반면에 클로저를 지원하는 언어는 그냥 함수를 파라미터로 넘기면 된다.Java의 수많은 디자인 패턴들은 이 클로저 미지원 문제 때문에 만들어졌다. 유명한 GoF의 디자인 패턴 중 전략 패턴이나 옵저버 패턴의 용도만 생각해 봐도, 이들은 애초부터 클로저가 지원되었다면 패턴이라는 거창한 이름을 붙일 가치조차 없는 몇 줄의 예제 코드에 지나지 않았을 것이다.
Java라는 언어가 설계될 당시에는 클로저라는 개념 자체가 LISP, Haskell, ML 등의 언어를 사용하거나 프로그래밍 언어 자체를 연구하는 사람들만 아는 몹시 마이너한 개념이었기 때문에 동시기에 만들어진 다른 많은 프로그래밍 언어들도 클로저를 지원하고 있지는 않았다. 따라서 설계 결함이라고 부를 수는 없고 현대에 들어서 단점으로 부각되기 시작했다는 편이 더 적절하다.
Java 8에서는 람다 표현식을 지원함과 더불어 메소드 참조라는 방식(this::add)을 통해 함수를 다른 함수의 파라미터로 넘길 수 있게 되었다. 이러한 함수 파라미터는 Functional Interface를 이용하여 선언하는데, Functional Interface는 수십여 종이 있으며 인자가 복수 개인 것도 당연히 있고, 하나의 추상 메소드를 가진 인터페이스를 새로 만들어서 써도 된다. 다만 대부분의 언어들과 달리 자바의 람다식은 바깥에 있는 변수를 참조하려면 그 변수가 final이거나 final을 붙여도 문제가 없는 변수들뿐이다. 따라서 람다식이 주위에 있는 변수를 '저장'할 수는 있으나 그것의 값을 바꿀 수는 없다. 변수에 값을 할당하는 것만 불가능할 뿐 변수에 접근해서 조작하는 것 자체는 가능하기 때문에, 변수를 배열로 선언해서 배열 인덱스에 값을 할당하거나 별도의 wrapper 클래스를 만들어 조작하는 등의 트릭을 활용할 수는 있다. 물론 그다지 권장할 만한 기법이 아님에는 주의할 필요가 있다.
java.util.function에 있는 대표적인 함수형 인터페이스 (Functional Interface)와 그에 대응하는 추상 메소드 몇 개를 나열하자면 다음과 같다.
- Predicate<T> → boolean test(T)
- Consumer<T> → void accept(T)
- Supplier<T> → T get()
- Function<T, U> → U apply(T)
- BiFunction<T, U, R> → R apply(T, U)
물론 위와 같은 몇몇 상용 인터페이스들이 지원되어도 언어의 구조적 한계[22]로 인해 최신 언어들에 비해 그때그때 필요한 구조의 람다식을 선언해 써먹기가 매우 피곤하다는 문제점은 여전하다.
3.6.3. 문자열 비교가 상대적으로 불편함
- {{{#!syntax java
String b = "Java";
System.out.println(a == b); // true
}}}
- {{{#!syntax java
String b = new String("Java");
System.out.println(a == b); // false
}}}
- {{{#!syntax java
String b = new String("Java").intern();
System.out.println(a == b); // true
}}}
이는 원시(primitive) 타입 외엔 전부
Object
로 처리하는 자바의 특징 때문이다. 원시 타입에선 ==
연산자는 두 요소가 같은 값을 가지면 true
를 반환한다. 그러나 Object
타입에선 두 인스턴스의 주소가 같으면 true
를 반환한다. 즉, 메모리 상의 위치가 같다고 인식되는 절대적으로 동등한 인스턴스인 경우에만 같다고 처리하게 된다.String
역시 예외 없이 Object
로 취급하기 때문에 a == b
는 문자열값 비교가 아닌 주소 비교를 하게 되는 것이다. 이 때문에 문자열이 같은지 비교할 때 a.equals(b)
같은 식으로 여느 C-Like 언어와는 따로 노는 방법을 사용하게 된다.여기서 특이한 점이 하나가 더 있는데,
String
을 Object
로 취급하면서도 문자열 자체는 문자열 풀에 캐싱을 해둔다는 점이다. 첫 번째 예시에서 a
에 "Java"
를 할당하는 순간 "Java"
가 문자열 풀에 캐싱되고 b
에 할당하는 "Java"
는 이전에 캐싱해 둔 "Java"
를 불러온다. 그래서 첫 번째 예시에서는 true
를 반환한다.두 번째 예시에서는
b
에 new String("Java")
를 할당하는데, 이는 기존 캐싱되어 있는 값을 활용하지 않고 새로운 String
인스턴스를 할당하겠다는 의미가 담겨 있다. 그리하여 두 번째 예시에서는 false
가 반환된다.그러나 세 번째 예시에서는 두 번째 예시에서와 다르게
true
가 반환되는데, String
인스턴스에서 intern()
을 호출하면 해당 인스턴스가 가진 문자열을 문자열 풀에 캐시를 시도하고, 이미 캐시되어 있다면 그 참조를 반환하게 되어 true
가 반환되게 되는 것이다.이 단점이 가장 두드러지는 곳이 외부에서 XML, JSON 등을 불러와 해석해서 그 안에 있는 문자열을 검증할 때인데, 이로 인해 디버깅이 골치 아파지기도 한다.
3.6.4. 언어 설계의 일관성 부족
자바는 초기 "Write Once, Run Anywhere"라는 철학과 단순성을 강조했으나, 시간이 지남에 따라 다른 언어들의 특징을 도입하면서 본래의 설계 원칙에서 벗어나는 모습을 보였다.예를 들어 인터페이스에 디폴트 메서드를 추가하여 순수 추상화 개념을 훼손시켰고, 다중 상속의 문제점을 부분적으로 도입하여 복잡성이 증가되었다.
Java 8에서 다른 현대 언어들처럼 함수형 프로그래밍을 지원하기 위해 람다와 스트림을 도입하였으나, 이는 다른 언어에 비하여 제한적인 지원에 그쳤으며 그마저도 기존 패러다임에서 벗어나 코드 리딩을 어렵게 한다는 이유로 일부의 기존 개발자들에게 비판을 받았다.
자바는 철저한 객체 지향 언어를 표방하지만 타 객체 지향 언어들과 달리 원시 타입이 존재하여 이들을 관리하기 위해 IntFunction, LongFunction 등의 별도의 클래스/인터페이스를 필요로 한다. 이는 언어 차원에서의 복잡성을 증가시킨다.
결과적으로 자바는 다른 언어들의 장점을 수용하려는 노력 속에서 초기의 단순성과 일관성을 일부 상실하고, 학습 곡선이 가파른 이도 저도 아닌 언어가 되어버렸다는 비판을 받고 있다.
3.7. 서버리스에 적합하지 않음
서버리스 아키텍처의 경우 상시 가동 상태인 기존의 서버와 달리 리퀘스트가 있을 시에만 애플리케이션이 기동되는 형태이기 때문에, JVM이 완벽하게 기동이 된 후 비로소 애플리케이션이 동작하는 Java의 경우 오히려 Python과 같은 인터프리터 언어로 작성된 애플리케이션보다 반응이 느리다(물론 연산의 부하가 높다면 Java 쪽이 빠르다).이런 문제 때문에 GraalVM 및 이를 활용해 네이티브로 컴파일되는 Quarkus 같은 프로젝트가 개발되고 있기도 한데, 네이티브 컴파일은 Kotlin과 같은 다른 JVM 언어로도 가능하기 때문에 굳이 Java를 고집할 이유가 없다.
[1]
특히 웹 개발 분야에서 해외의 경우 Java는 도태되는 추세고,
Node.js 덕분에 서버와 프런트엔드 양쪽에서 쓸 수 있는
JavaScript와
Django를 통하여 쉽고 빠르게 백엔드 구축이 가능한
Python의 사용률이 늘어나고 있다.
[2]
JVM 위에서 구동 가능한 언어에 대해서는 영문 위키백과의
List of JVM languages 참고.
[3]
...고는 하지만 혹자는
쥐라알VM이라고 읽기도 한다. 당연하지만 옳은 발음은 그랄VM이다.
[4]
정말로 포인터를 써야 할 경우 Unsafe 클래스를 쓰면 된다. 다만 팩토리 메소드가 막혀있어 리플렉션을 사용해야만 이용할 수 있다.
[5]
어차피 Java에서는 다중 상속을 포기함으로써 생기는 문제를 interface를 다중 구현 할 수 있도록 하여 어느 정도 해결하기도 했다.
[6]
쉽게 생각하면 배열 1000개를 할당받기 위해 운영 체제에 1번 호출하여 한꺼번에 할당받지 않고, 1000번 호출하여 각각 할당받는 것이다. 기본적으로, 동적 메모리 할당은 운영 체제에서 처리하기 때문에 속도가 느리다. 어떤 언어에서라도, 운영 체제의 메모리 할당 횟수를 줄이는 것이 성능에 도움이 된다.
[7]
메모리를 훑으면서 순간적으로 프로그램이 얼어붙어 멈추는 현상.
[8]
이를
gotta catch 'em all 패턴이라고 한다.
[9]
물론 이렇게 짜면 안 되지만 구조적으로 강요받는 형편.
[10]
이쪽은 RuntimeException 계열의 예외로, 이것을 던지는 메소드가 throws에 명시적으로 던진다고 선언하지 않았을 경우 검사를 하지 않아도 된다.
[11]
대한민국에 국한되는 경향이 강하긴 하지만 현재 한국에서 이러한 예외 처리를 코딩하는 순간에 인지하는 개발자는 드물다. 국비 지원 학원을 막 졸업한 신입들은 말할 것도 없으며 현업 몇 년 한 초중급 경력자들 역시 이를 인지하지 못하는 경우가 허다하기 때문에, 어찌 보면 대한민국 IT계의 버그 처리와 예외 처리에 있어서만큼은 장점으로 볼 수도 있다.
[12]
그래서 IntelliJ 같은 IDE는 'psvm', 'sout' 같은 약어를 통해 위 문장을 빠르게 입력할 수 있게 해 주는 매크로가 들어있다.
[13]
사실 C(C++이 아니다)는 C99기준 처음에 '#include <stdio.h>'를 포함하지 않아도 puts, printf 등을 사용할 수 있다. C, C++ 공통으로는(C++ 11 기준) main에 int를 붙이지 않아도 컴파일이 된다. 만일 구버전용으로 제대로 쓴다면
#include <stdio.h>
int main() {
puts("Hello");
}
이 된다. [14] .NET 5.0부터 9.0 버전을 사용한다. 이 버전부터 최상문을 지원해서 아래와 같이 한 줄만 쓰면 된다. [15] .NET 6.0부터 10.0 버전을 사용한다. 이 버전부터 implicit global using을 지원해서 System 네임스페이스도 없어도 된다. [16] 그 마세라티가 맞다. 자신이 탈 수나 있을지도 모르는 마세라티를 타면 어떻게 해야 될까...라는 쓸데없는 고민을 하는 것을 비유하는 문제로, 당장에 쓸데없는 기능을 위해 과도하게 투자하는 것을 의미한다. [17] 프로그래밍 언어를 좀 더 쉽게 표현할 수 있도록 하는 보조 문법. C에서 구조체 포인터를 쓸 때 (*ptr).num은 ptr->num으로도 표현 가능하므로, ->는 문법적 설탕이라고 할 수 있다. [18] 컬렉션(Collection)의 이터레이터를 확장해서 처리할 수 있는 개념으로, 이터레이터의 원소를 필터링해서 원하는 원소만 뽑거나, 원소 개수를 줄여버리고, 이렇게 다시 뽑힌 원소들로 갖가지 처리를 만드는 등 '함수적인(functional)' 기능을 제공한다. [19] 정작 람다 표현식의 추가는 Java 진영 내에서 논란이 있다. 람다식이 뒤늦게 추가된 것도 코드 리딩이 어려워진다고 싫어하는 개발자가 많아서였다. [20] 멀리 갈 것 없이, C 언어의 stdio.h와 자바의 System.out을 비교해 보자. Java는 직관적으로 '시스템'에서 뭔가가 '나온다'는 것을 쉽게 인식할 수 있지만, stdio.h를 봤을 때 직관적으로 뭘 떠올릴까? 저걸 처음 보고 STanDard Input and Output.Header를 떠올릴 사람이 얼마나 있을까?물론 C를 하다 보면 언어 기능 자체가 적어 파악하기가 쉬운 건 함정이다
[21]
실제로 Java에서 기본 제공하는 정렬 기능은 Collections 클래스의 정적 메소드인 sort(array)를 호출하도록 작성되어 있다.
[22]
2010년대부터 개나 소나 지원하기 시작한 변수 타입에 대한 컴파일러 추론을 지원하지 않는 문제 등등.
#include <stdio.h>
int main() {
puts("Hello");
}
이 된다. [14] .NET 5.0부터 9.0 버전을 사용한다. 이 버전부터 최상문을 지원해서 아래와 같이 한 줄만 쓰면 된다. [15] .NET 6.0부터 10.0 버전을 사용한다. 이 버전부터 implicit global using을 지원해서 System 네임스페이스도 없어도 된다. [16] 그 마세라티가 맞다. 자신이 탈 수나 있을지도 모르는 마세라티를 타면 어떻게 해야 될까...라는 쓸데없는 고민을 하는 것을 비유하는 문제로, 당장에 쓸데없는 기능을 위해 과도하게 투자하는 것을 의미한다. [17] 프로그래밍 언어를 좀 더 쉽게 표현할 수 있도록 하는 보조 문법. C에서 구조체 포인터를 쓸 때 (*ptr).num은 ptr->num으로도 표현 가능하므로, ->는 문법적 설탕이라고 할 수 있다. [18] 컬렉션(Collection)의 이터레이터를 확장해서 처리할 수 있는 개념으로, 이터레이터의 원소를 필터링해서 원하는 원소만 뽑거나, 원소 개수를 줄여버리고, 이렇게 다시 뽑힌 원소들로 갖가지 처리를 만드는 등 '함수적인(functional)' 기능을 제공한다. [19] 정작 람다 표현식의 추가는 Java 진영 내에서 논란이 있다. 람다식이 뒤늦게 추가된 것도 코드 리딩이 어려워진다고 싫어하는 개발자가 많아서였다. [20] 멀리 갈 것 없이, C 언어의 stdio.h와 자바의 System.out을 비교해 보자. Java는 직관적으로 '시스템'에서 뭔가가 '나온다'는 것을 쉽게 인식할 수 있지만, stdio.h를 봤을 때 직관적으로 뭘 떠올릴까? 저걸 처음 보고 STanDard Input and Output.Header를 떠올릴 사람이 얼마나 있을까?