JDK & OpenJDK

System/Linux 2012. 4. 5. 13:13
























OpenJDK (Open Java Development Kit) is a free and open source implementation of the Java programming language.[2] It is the result of an effort Sun Microsystems began in 2006. The implementation is licensed under the GNU General Public License (GPL) with alinking exception, which exempts components of the Java class library from the GPLlicensing terms. OpenJDK is the official Java SE 7 reference implementation.[3][4]

Contents

  [hide

[edit]History

[edit]Sun's promise and initial release

Sun announced in JavaOne 2006 that Java would become open-source software,[5][6] and on October 25, 2006, at the Oracle OpenWorld conference,Jonathan Schwartz said that the company intended to announce the open-sourcing of the core Java Platform within 30 to 60 days.[7]

Sun released the Java HotSpot virtual machine and compiler as free software under the GNU General Public License on November 13, 2006, with a promise that the rest of the JDK (which includes the Java Runtime Environment) would be placed under the GPL by March 2007, "except for a few components that Sun does not have the right to publish in source form under the GPL".[8] According to free-software advocate Richard Stallman, this would end the "Java trap", the vendor lock-in that he argues applied to Java and programs written in Java.[9]

[edit]Release of the class library

Following their promise to release a Java Development Kit (JDK) based almost completely on free and open source code in the first half of 2007,[10] Sun released the complete source code of the Java Class Library under the GPL on May 8, 2007, except for some limited parts that some third parties licensed to Sun that rejected the terms of the GPL.[11] Included in the list of encumbered parts were several major components of the Java graphical user interface (GUI). Sun stated that it planned to replace the remaining proprietary components with alternative implementations and to make the class library completely free.[12]

[edit]Community improvements

On November 5, 2007, Red Hat announced an agreement with Sun, signing Sun's broad contributor agreement (which covers participation in all Sun-led free and open source software projects by all Red Hat engineers) and Sun's OpenJDK Community Technology Compatibility Kit (TCK) License Agreement (which gives the company access to the test suite that determines whether a project based on OpenJDK complies with the Java SE 6 specification).[13]

Also on November 2007, the Porters Group was created on OpenJDK[14] to aid in efforts to port OpenJDK to different processor architectures andoperating systems. The BSD porting projects,[15] led by Kurt Miller and Greg Lewis and the Mac OS X porting project (based on the BSD one)SoyLatte led by Landon Fuller[16] have expressed interest in joining OpenJDK via the Porters Group and as of January 2008 are part of the mailing list discussions. Another project pending formalization on the Porters Group is the Haiku Java Team, led by Bryan Varner.[17]

On December 2007, Sun moved the revision control of OpenJDK from TeamWare to Mercurial, as part of the process of releasing it to open sourcecommunities.[18][19]

OpenJDK has comparatively strict procedures of accepting code contributions: every proposed contribution must be reviewed by two of Sun's engineers and the contributor must have signed the Sun/Oracle Contributor Agreement.(SCA/OCA[20]) Preferably, there should also be a jtreg[21] test demonstrating that the bug has been fixed. Initially, the external patch submission process was slow[22] and commits to the codebase were only made by Sun engineers, until September 2008.[23] The process has improved and, as of 2010, simple patches and backports from OpenJDK 7 to OpenJDK 6 can take place within hours rather than days.[24]

[edit]Collaboration with IBM, Apple, and SAP

On October 11, 2010, IBM, by far the biggest participant in the Apache Harmony project, decided to join Oracle on the OpenJDK project, effectively shifting its efforts from Harmony to OpenJDK.[25][26] Bob Sutor, IBM's head of Linux and open source, blogged that "IBM will be shifting its development effort from the Apache Project Harmony to OpenJDK".[27]

On November 12, 2010, Apple Inc. (just three weeks after deprecating its own Java runtime port[28]) and Oracle Corporation announced the OpenJDK project for Mac OS X. Apple will contribute most of the key components, tools and technology required for a Java SE 7 implementation on Mac OS X, including a 32-bit and 64-bit HotSpot-based Java virtual machine, class libraries, a networking stack and the foundation for a new graphical client.[29]

On January 11, 2011, the Mac OS X Port Project was created on OpenJDK, and Apple made the first public contribution of code to the project. The initial Apple contribution built on the OpenJDK BSD port.[30]

In July 2011, SAP AG announced that SAP officially joined the OpenJDK project.[31]

[edit]Status

[edit]Supported JDK versions

OpenJDK was initially based only on the JDK 7 version of the Java platform.[32]

Since February 15, 2008, there are two separate OpenJDK projects:

  • The main OpenJDK project, which is based on JDK 7.
  • The OpenJDK 6 project, a cut-down version of OpenJDK 7,[33] which provides an open-source version of Java 6.[34]

[edit]Compiler and virtual machine

Sun's Java compiler, javac, and HotSpot (the virtual machine), are now under a GPL license.

[edit]Class library

As of the first May 2007 release, 4% of the OpenJDK class library remained proprietary.[35] By the appearance of OpenJDK 6 in May 2008, less than 1% (the SNMP implementation,[36] which is not part of the Java specification) remained,[37] making it possible to build OpenJDK without any binary plugs.[36]The binary plug requirement was later dropped from OpenJDK 7 as part of b53 in April 2009.[38]

This was made possible, over the course of the first year, by the work of Sun Microsystems and the OpenJDK community. Each encumbrance[39] was either released as free and open source software or replaced with an alternative. Beginning in December 2010, all the so called binary plugs were replaced by Open source replacements, making the whole JDK open sourced and the binary plugs not necessary anymore.[40]

Sun has made continued promises about releasing their web browser plugin and Web Start implementation as part of OpenJDK, but have so far failed to deliver.[41][citation needed] The only currently available free plugin and Web Start implementation are those provided by IcedTea.

[edit]IcedTea and inclusion in software distributions

To be able to bundle OpenJDK in Fedora and other free Linux distributions, OpenJDK needed to be buildable using only free software components. Due to the encumbered components in the class library and implicit assumptions within the build system that the JDK being used to build OpenJDK was a Sun JDK[why?], this was not possible. To achieve this goal, a project called IcedTea was started by Red Hat in June 2007.[42] It began life as an OpenJDK/GNU Classpath hybrid that could be used to bootstrap OpenJDK, replacing the encumbrances with code from GNU Classpath.[43][44]

On November 5, 2007, Red Hat signed both the Sun Contributor Agreement and the OpenJDK Community TCK License.[45] One of the first benefits of this agreement is tighter alignment with the IcedTea project, which brings together Fedora, the Linux distribution, and JBoss, the application server, technologies in a Linux environment. IcedTea is providing free software alternatives for the few remaining proprietary sections in the OpenJDK project.

In May 2008, the Fedora 9[37][46] and Ubuntu 8.04[47] distributions included IcedTea 6, based completely on free and open source code.[48] Fedora 9was the first version to ship with IcedTea6, based on the OpenJDK6 sources from Sun rather than OpenJDK7. It was also the first to use OpenJDK for the package name (via the OpenJDK trademark agreement) instead of IcedTea.[37]Ubuntu also first packaged IcedTea7[49] before later moving to IcedTea6. Packages for IcedTea6 were also created for Debian and included in lenny. On July 12, 2008, Debian accepted OpenJDK-6 in unstable,[50][51] and it is now in stable.[52] OpenJDK is also available on openSUSE,[53] Red Hat Enterprise Linux and RHEL derivatives such asCentOS.[54]

In June 2008, Red Hat announced that the packaged binaries for OpenJDK on Fedora 9, built using IcedTea 6, had passed the Technology Compatibility Kit tests and could claim to be a fully compatible Java 6 implementation.[55] In July 2009, an IcedTea 6 binary build for Ubuntu 9.04 passed all of the compatibility tests in the Java SE 6 JCK.[56]

Since August 2008, OpenJDK 7 is runnable on Mac OS X and other BSD variants.[57]

[edit]See also

[edit]References

  1. ^ http://download.java.net/openjdk/jdk6/promoted/b05/
  2. ^ "OpenJDK Legal Documents"Sun Microsystems.
  3. ^ Moving to OpenJDK as the official Java SE 7 Reference Implementation
  4. ^ Java Platform, Standard Edition 7 Reference Implementations
  5. ^ Schwartz, Jonathan (May 23, 2006). "Busy Week...". Sun Microsystems. Retrieved May 9, 2007.[dead link]
  6. ^ "Sun Opens Java" (OGG Theora). Sun Microsystems.[dead link]
  7. ^ "Sun CEO sets open source Java time frame - Announcement set for 30 to 60 days"InfoWorld. 2006-10-25. Retrieved 2011-12-22.
  8. ^ "Sun Opens Java". Sun Microsystems. November 13, 2006. Archived from the original on April 21, 2007. Retrieved May 9, 2007.
  9. ^ Stallman, Richard. "Free But Shackled—The Java Trap". Retrieved December 4, 2007.
  10. ^ http://www.sun.com/software/opensource/java/faq.jsp#b4
  11. ^ "Open JDK is here!". Sun Microsystems. May 8, 2007. Retrieved May 9, 2007.
  12. ^ Some encumbered code[clarification needed] remains in the JDK; Sun stated that it will continue to use such code in commercial releases until fully functional free and open source alternatives replace it.
  13. ^ Broad contributor agreement and TCK License pave way for a fully compatible, free and open source Java Development Kit for Red Hat Enterprise Linux
  14. ^ Porters Group
  15. ^ http://www.eyesbeyond.com/freebsddom/java/jdk16.html
  16. ^ http://landonf.bikemonkey.org/code/macosx/
  17. ^ New java for haiku team formed
  18. ^ James Gosling (October 2006). James Gosling on Open Sourcing Sun's Java Platform Implementations, Part 1. Interview with Robert Eckstein.
  19. ^ O'Hair, Kelly (December 12, 2007). "Mercurial OpenJDK Questions".
  20. ^ "Sun Microsystems Inc. Contributor Agreement".
  21. ^ "Regression Test Harness for the OpenJDK platform: jtreg". Retrieved August 26, 2008.
  22. ^ Tripp, Andy (July 16, 2007). "Classpath hackers frustrated with slow OpenJDK process". Retrieved April 20, 2008.
  23. ^ Kennke, Roman (September 29, 2008). "A small step for me". Retrieved October 19, 2008.[dead link]
  24. ^ Darcy, Joe (June 10, 2010). "Backporting changeset from 7 to 6 for bugfix".
  25. ^ "Oracle and IBM Collaborate to Accelerate Java Innovation Through OpenJDK"Oracle Corporation. Retrieved October 22, 2010.
  26. ^ Ryan Paul. "Java wars: IBM joins OpenJDK as Oracle shuns Apache Harmony"Ars Technica. Retrieved October 22, 2010.
  27. ^ Bob Sutor. "IBM joins the OpenJDK community, will help unify open source Java efforts". Retrieved October 22, 2010. "IBM will be shifting its development effort from the Apache Project Harmony to OpenJDK. For others who wish to do the same, we’ll work together to make the transition as easy as possible. IBM will still be vigorously involved in other Apache projects."
  28. ^ "Java for Mac OS X 10.6 Update 3 and 10.5 Update 8 Release Notes". October 20, 2010.
  29. ^ "Oracle and Apple Announce OpenJDK Project for Mac OS X"Business Wire. 2010-11-12. Retrieved 2010-11-12. "Oracle and Apple today announced the OpenJDK project for Mac OS X. Apple will contribute most of the key components, tools and technology required for a Java SE 7 implementation on Mac OS X, including a 32-bit and 64-bit HotSpot-based Java virtual machine, class libraries, a networking stack and the foundation for a new graphical client. OpenJDK will make Apple’s Java technology available to open source developers so they can access and contribute to the effort."
  30. ^ Mike Swingler (Apple) (2011-01-11). "Announcing: OpenJDK for Mac OS X source repository, mailing list, project home". OpenJDK. Retrieved 2010-11-12. "I'm very happy to let you know that today we made the first public contribution of code to the OpenJDK project for Mac OS X. This initial contribution builds on the hard work of the BSD port, and initially has the same functionality. Today's contribution simply modifies the build process to create universal binary, and produces a .jdk bundle which is recognized by Java Preferences and the JVM detection logic in Mac OS X."
  31. ^ Volker Simonis (SAP AG) (2011-07-14). "SAP joins the OpenJDK". OpenJDK. Retrieved 2010-11-12. "I'm really happy that as of today, SAP has signed the Oracle Contributor Agreement (OCA). This means that with immediate effect the SAP JVM developers can officially join the discussions on the various OpenJDK mailing lists and contribute patches and enhancements to the project."
  32. ^ "Didn't you promise to open source both JDK 6 and JDK 7 last November? What happened to JDK 6?". Sun Microsystems. Retrieved October 14, 2007. "Sun did make that promise, and we plan to keep it. But in the six months since the November 2006 announcement, it has become clear that doing this is far more complex than just changing the license and publishing the source code."
  33. ^http://weblogs.java.net/blog/robogeek/archive/2009/01/it_will_be_open.html
  34. ^ Darcy, Joe (February 11, 2008). "The code is coming! The code is coming!". Retrieved February 16, 2008. "At Sun we're making final preparations for the first source release for the OpenJDK 6 project. We plan to release a tarball of the source, along with matching binary plugs, by February 15, 2008."
  35. ^ Fitzsimmons, Thomas (May 18, 2007). "Plans for OpenJDK". Retrieved May 22, 2007.
  36. a b "OpenJDK 6 b10 source posted". May 30, 2008. Retrieved June 1, 2008.
  37. a b c Wade, Karsten (March 13, 2008). "OpenJDK in Fedora 9!". redhatmagazine.com. Retrieved April 5, 2008. "Thomas Fitzsimmons updated the Fedora 9 release notes source pages to reflect that Fedora 9 would ship with OpenJDK 6 instead of the IcedTea implementation of OpenJDK 7. Fedora 9 (Sulphur) is due to release in May 2008."
  38. ^ "Changes in OpenJDK7 b53". April 2, 2009. Retrieved September 5, 2009.
  39. ^ Herron, David (October 4, 2007). "Plans for OpenJDK". Retrieved October 9, 2007.
  40. ^ Kelly O'Hair (December 2010). "OpenJDK7 and OpenJDK6 Binary Plugs Logic Removed"Oracle Corporation. Retrieved 2011-11-25.
  41. ^ Darcy, Joe (June 8, 2009). "OpenJDK and the new plugin". Retrieved September 5, 2009.
  42. ^ Fitzsimmons, Thomas (June 8, 2007). "Credits". Retrieved June 8, 2007.
  43. ^ Andrew, Haley (June 7, 2007). "Experimental Build Repository at icedtea.classpath.org". Retrieved June 9, 2007.
  44. ^ Mark, Wielaard (June 7, 2007). "Experimental Build Repository at icedtea.classpath.org". Retrieved June 9, 2007.
  45. ^ "Red Hat and Sun Collaborate to Advance Open Source Java Technology"Red Hat. November 5, 2007. Retrieved November 6, 2007.
  46. ^ "Open Source Java Technology Debuts In GNU/Linux Distributions". Sun Microsystems. Retrieved May 2, 2008.
  47. ^ "openjdk-6 in Ubuntu". Retrieved April 19, 2008.
  48. ^ Reinhold, Mark (April 24, 2008). "There’s not a moment to lose!". Retrieved April 19, 2008.
  49. ^ "icedtea-java7 in Ubuntu". Retrieved April 19, 2008.
  50. ^ Topic, Dalibor (July 14, 2008). "QotD: Debian Overview of openjdk-6 source package". Retrieved July 15, 2008.
  51. ^ "Overview of openjdk-6 source package". debian.org. Retrieved July 15, 2008.
  52. ^ "Package: openjdk-6-jdk". debian.org. February 14, 2009. Retrieved February 16, 2009.
  53. ^ "Package: OpenJDK". opensuse.org. Retrieved June 1, 2009.[dead link]
  54. ^ "How to download and install prebuilt OpenJDK packages". Retrieved March 3, 2010.
  55. ^ Sharples, Rich (June 19, 2008). "Java is finally Free and Open".
  56. ^ Announcing OpenJDK 6 Certification for Ubuntu 9.04 (jaunty)
  57. ^ Fuller, Landon (August 19, 2008). "SoyLatte, Meet OpenJDK: OpenJDK 7 for Mac OS X". Retrieved August 22, 2008.

[edit]External links








JDK 7 The Platform 2011

  

NHN Business platform 웹플랫폼개발랩 문종호

2011년 7월 말 JDK 7이 정식으로 릴리스되었습니다. 비록 Lambda나 Jigsaw 같이 화제가 되었던 프로젝트는 JDK 8로 연기되어 김이 새긴 했지만, 무려 5년 만에 나오는 새 버전인 만큼 전세계 Java 개발자들의 이목이 집중되었습니다. 이 글에서는 JDK 7을 전체적으로 살펴보고, 주요 기능은 좀 더 심도 있게 알아보겠습니다.

  • 들어가며

  • OpenJDK

    Sun(현재 Oracle)이 JDK 7을 개발하기 시작할 때 이전과 다른 점이 하나 있었다. 그것은 바로 Sun이 JDK를 오픈소스화하기 위해 2007년 OpenJDK를 만들었다는 것이다. Sun은 저작권자가 오픈소스화를 거부한 일부 컴포넌트를 제외한 나머지 JDK 소스코드 전부를 OpenJDK에 제공했고, OpenJDK는 이를 기반으로 JDK 7 프로젝트를 시작했다. 즉, 누구나 OpenJDK의 소스코드 저장소에 접근하여 소스코드를 살펴볼 수 있으며, 의지와 능력만 있으면 개발에도 참여할 수 있다.

    그런데 한 가지 이상한 점이 있다. OpenJDK는 소스코드만 배포하고 바이너리는 배포하지 않는다. 앞에서 언급했듯이 일부 컴포넌트의 저작권자가 오픈소스화를 거부했기 때문이다. 그럼 JDK 7을 사용하려면 소스코드를 내려 받아 직접 빌드해야 하나? 다행히 그렇지는 않다.

    Java.net에는 또 하나의 JDK 7 프로젝트(이하, Oracle JDK 7)가 있다. Oracle JDK 7은 OpenJDK의 JDK 7 기반에 추가로 OpenJDK에 포함되지 않는 컴포넌트까지 모두 갖춘 프로젝트이다. 하지만 Oracle JDK 7은 오픈소스가 아니며, Oracle의 직원들만 개발에 참여할 수 있다. Oracle의 공식 JDK 7은 바로 이 프로젝트의 결과물이다.

  • Java SE 7, JDK 7, Java 7

    이야기가 나온 김에 한 가지 더 짚고 넘어가자. Java 7, Java SE 7, JDK 7 등의 용어가 혼용되는데 이들의 차이점은 무엇일까?

    Java SE는 Java Platform, Standard Edition의 약자로, Java Platform의 근간을 이루는 '명세(Specification)'를 가리킨다. JDK는 Java Development Kit의 약자로, 앞에서 말한 '명세'의 '구현체'를 가리킨다고 보면 된다. 즉, Java SE 7은 JSR-336로 규정된 Java SE의 7번째 명세를 가리키는 용어이고, JDK 7은 그 구현체, 이를테면 OpenJDK JDK 7, Oracle JDK 7과 같은 것을 가리키는 것이다.

    그런데 JDK 7은 Java SE 7이 확정되기 전에 개발되었다. OpenJDK에서 명세와 구현을 동시에 진행하고 그 결과가 JSR(Java Specification Request)에 반영되어 명세가 결정되는 순서로 진행되었던 것이다. 그래서인지 Java SE 7보다는 JDK 7이라는 용어가 훨씬 더 많이 사용되었다. 따라서 여기에서도 Java SE 7보다는 JDK 7이라는 용어를 사용하도록 하겠다.

    한편 Java는 다양한 의미로 사용되지만 엄밀히 말하면 버전을 붙여서 사용할 수 있는 용어는 아니다. 즉 Java 7은 정확한 표현이 아니다. 하지만 Java SE 7이나 JDK 7보다 발음하기 편하고, 그냥 Java 7이라고 부른다고 해서 무슨 의미인지 혼란스러워 할 사람도 없기 때문에 흔히 Java 7이라고 부르기도 하는 것이다.

  • Plan B

    그런데 JDK 7이 릴리스되기 전부터 JDK 8에 대한 이야기가 들려오기 시작했다. 어떻게 된 것일까?

    Java SE 6까지 Java SE는 약 2년을 주기로 메이저 버전이 릴리스되어 왔다. 그래서 Java SE 7도 Java SE 6이 릴리스된 지 약 2년 후, 그러니까 2008년 말이나 늦어도 2009년 중에는 릴리스될 것이라 생각되었다. 그러나 Oracle이 Sun을 인수하는 등 여러 일로 인해 JDK 7 개발은 자꾸 지연되었고, 이대로는 2012년에나 완성될 것이라고 예측하기도 했다. 결국 2010년 9월, Oracle의 Mark Reinhold는 마무리되어가고 있는 명세를 모아 2011년에 JDK 7으로 릴리스하고 나머지는 2012년 말에 릴리스할 JDK 8으로 미루자는 Plan B를 제안했다. 그리고 Oracle은 이 제안을 채택하여 2010년 JavaOne에서 공식적으로 Plan B 채택을 발표했다.

    JDK 8으로 미뤄진 프로젝트는 Project Coin의 일부와 Project Lambda, Project Jigsaw 등이다. Project Lambda는 Java 언어에 Closure를 도입하는 것으로 자바 언어의 표현력을 크게 높일 수 있는 반면 함수형 언어에 익숙하지 않은 개발자들을 혼란에 빠뜨릴 수 있다. Generics 이후 최대의, 어쩌면 Generics보다 더 큰 언어적 변화를 가져올 수 있는 프로젝트이다.

    Project Jigsaw는 JDK 클래스 라이브러리 자체를 모듈화하는 것을 목표로, Java 클래스들을 묶어 모듈을 구성할 수 있도록 하는 프로젝트이다. 여기서 모듈은 패키지나 JAR과는 다른 것으로, OSGi의 모듈과 비슷하다고 할 수 있다.

    이 두 프로젝트는 규모가 크고, 그만큼 많은 사람들의 관심을 끌고 있다. 이들에 비하면 JDK 7에 포함된 프로젝트는 미미해 보일 정도이다. 아쉽기는 하지만, 그래도 JDK 7에는 꽤 유용한 기능들이 남아 있다. 지금부터 JDK 7에서 만나볼 수 있는 기능들을 찬찬히 살펴보도록 하겠다.

  • JDK 7의 주요 기능

    JDK 7의 전체 기능은 JDK Features(http://openjdk.java.net/projects/jdk7/features/)에서 볼 수 있다. 여기에서는 중요한 기능들만 간단히 살펴보도록 하자.

  • JSR 292: Support for dynamically-typed languages (InvokeDynamic)

  • JVM(Java Virtual Machine)에서 동작하는 언어는 Java만이 아니다. 기존 언어가 JVM에서 동작하도록 구현한 JRuby, Jython 같은 언어도 있고, Groovy, Scala 같이 처음부터 JVM 기반으로 만들어진 언어도 있다. 그러나 JVM에는 아직 Java에 의존적인 부분이 있기 때문에 다른 언어, 특히 동적 타입 언어가 JVM에서 동작하는 데 걸림돌이 되는 부분이 있었다.

    InvokeDynamic은 이러 걸림돌을 제거하는 작업의 일환으로, JVM에서 동작하는 동적 타입 언어들의 효율성(바이너리 크기 등)과 성능 향상을 이끌어낼 것으로 보인다. 다만 동적 타입 언어의 제작자가 아닌 일반 개발자가 InvokeDynamic을 직접 사용할 일은 거의 없으므로 우리가 InvokeDynamic을 깊이 살펴볼 필요는 없을 것이다.

  • JSR 334: Small language enhancements (Project Coin)

  • Java 개발자들이 가장 직접적으로 큰 변화를 느낄 수 있는 부분이 바로 Project Coin이다. Project Coin은 Java 언어에 작은 변화를 주어서 개발자들의 가려운 곳을 긁어주자는 목적으로 시작됐으며, 구체적인 변화 내용에 대해서는 Oracle 내부 개발자뿐 아니라 누구라도 참여할 수 있도록 공개적으로 제안서를 모집했다. 그 결과 70여 개의 제안서를 받았고, 그 중 다음과 같은 내용을 JDK 7에 포함했다.

    • String in switch: switch 문에서 int와 Enum 외에 문자열도 사용할 수 있다.
    • Binary integral literals and underscores in numeric literals: 2진수 리터럴 표현이 가능해지며, 모든 숫자 리터럴에서 언더스코어(_)를 사용하여 가독성을 높일 수 있다.
    • Multi-catch and more precise rethrow: 하나의 catch 절에서 여러 타입의 예외를 처리할 수 있다.
    • Improved Type Inference for Generic Instance Creation: Generic 객체를 생성할 때 타입 파라미터를 일일이 기술하지 않아도 된다.
    • try-with-resources statement: try 절에서 사용되는 AutoClosable을 구현한 자원의 close() 메서드가 불리는 것을 보장하여 자원이 새는 것을 막는다.
    • Simplified Varargs Method Invocation: 가변 인자 메서드와 generic이 만나서 발생하는 경고(warning)를 해결할 수 없는 경우가 있다. 이 경고를 해결하는 방법을 제공한다.
  • JSR 203: More new I/O APIs for the Java platform (NIO.2)

  • java.io.File을 대체할 java.nio.Path가 추가되며, 파일시스템과 관련된 많은 유틸리티 API, 비동기 채널, 파일 속성 관련 API, 파일시스템 감시(watcher) 기능 관련 API 등 많은 API가 추가된다.

  • Concurrency and collections updates(jsr166y)

  • Fork/Join 프레임워크를 포함하여 Java 동시성(concurrency)과 관련된 클래스가 추가된다.

  • 기타

  • 그 밖에 한번 훑어볼 만한 기능은 다음과 같다.

    • Upgrade class-loader architecture: 클래스 로더의 의존 관계에 사이클이 존재하면 데드락(deadlock)이 발생하는 문제를 피하도록 수정되었다.
    • Locale enhancement: Java의 로캘(locale)은 현재 IETF BCP 47로 대체된 IETF RFC 1766 기반이다. 따라서 Java의 로캘을 IETF BCP 47 기반으로 변경했다.
    • Unicode 6.0: Unicode 6.0을 지원한다.
    • Enhanced JMX Agent and MBeans: JRockit에서 포팅된 기능으로, 방화벽 내의 MBean 서버에 접근할 수 있게 하고, VM에 대해 더 많은 정보를 제공하는 MBean을 제공한다.
    • JDBC 4.1: JDBC 4.1로 업그레이드된다.
    • Update the XML stack: JAXP, JAXB, JAX-WS의 최신 버전으로 업그레이드된다.

     

    이 글에서는 이 중 Project Coin과 NIO.2, Fork/Join 프레임워크에 대해서 한층 더 깊이 살펴보도록 한다.

  • Project Coin

    Project Coin은 JDK 7의 새 기능들 중 우리에게 가장 직접적으로 영향을 줄 기능이다. 그런데 갑자기 웬 동전일까? 아는 사람도 많겠지만, 영어 사전을 찾아보면 동사 coin은 다음과 같은 뜻이다.

  • (새로운 낱말, 어구를) 만들다
  • (주화를) 주조하다
  • Project Coin에서 coin은 첫 번째 의미로 사용된다. Java의 문법에 새로운 뭔가를 만들어 넣는다는 것이다. 그래도 의문은 남는다. Project Lamda나 Project Jigsaw 등 다른 프로젝트들은 이름이 그 프로젝트의 내용을 나타내는데, Project Coin은 단지 '새로 만든다'라는 의미뿐이다. 이렇게 된 이유는 Project Coin의 시작과 진행 과정에서 찾을 수 있다.

    Project Coin은 2008년 12월, 프로젝트 리더인 Joseph D. Darcy의 블로그 게시글에서 시작되었다. Sun(현재 Oracle)이 JDK 7에 '작은 언어 변화'를 포함시키기로 했는데, 구체적으로 어떤 변화를 줄 것인지는 공개적으로 제안서를 받아서 선정하겠다는 이야기였다. 그리고 여기에서 말하는 '작은 언어 변화'가 구체적으로 어떤 것인지, 제안서를 어떻게 써야 하는지 안내하는 게시글이 이어졌다. 그리고 2009년 2월 27일, Project Coin의 메일링리스트가 개설되었고 3월 30일까지 약 한 달 동안 70여 개의 제안서가 제출되었다. 같은 해 8월, 이 중 Project Coin에 포함될 제안서들이 확정되었다.

    이처럼 Project Coin은 구체적으로 어떤 변화를 줄 것인지 정해놓고 시작한 것이 아니라 대중으로부터 제안을 받아 취합하는 형태로 진행된 프로젝트였기 때문에 이렇게 약간은 막연한 이름을 갖게 된 것이다.

    이후 JDK 7의 진행이 지지부진해지면서 Project Coin도 함께 지지부진하게 진행되었다. 그리고 JDK 7과 JDK 8의 분리가 결정되면서 Project Coin도 JDK 7과 JDK 8에 포함될 것들로 구분했다. 다행히 JDK 8으로 연기된 기능은 Language support for collections 하나뿐이고, 나머지 기능들은 JSR 334에서 공식적인 절차에 따라 명세가 확정되었다.

    그럼 이제부터 Project Coin의 기능들을 간단한 것부터 순서대로 살펴보도록 하겠다.

  • Binary integrals and underscores in numeric literals

  • 이 기능은 두 가지 내용을 담고 있다. 하나는 16진수 리터럴처럼 2진수 리터럴도 사용할 수 있게 하는 것이다. 16진수가 0x 또는 0X로 시작하는 것처럼 2진수는 0b 또는 0B로 시작한다.

    1
    int x = 0b0110;

    다른 하나는 가독성을 높이기 위해 숫자 리터럴 중간에 언더스코어(_)를 넣을 수 있다는 것이다. 예를 들어 다음과 같은 상수를 보자.

    1
    2
    private static final long TIMEOUT = 3600000;
    private static final int BINARY_VALUE = 0b1001011011000011;

    한눈에 파악하기 쉽지 않다. JDK 7에서는 이를 다음과 같이 쓸 수 있다.

    1
    2
    private static final long TIMEOUT = 3600_000;
    private static final int BINARY_VALUE = 0b1001_0110_1100_0011;

    언더스코어는 숫자 사이 어디에든 넣을 수 있고, 언더스코어를 둘 이상 붙여서 쓸 수도 있다.

  • Strings in switch

  • 지금까지 switch 문에는 int와 Enum 타입만 사용할 수 있었지만 JDK 7부터는 String도 사용할 수 있다. 번잡한 if ... else ... 체인 대신 깔끔하게 switch 문을 사용할 수 있게 된 것이다. 다음은 String을 사용한 switch 문의 예이다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public void handle(String code, Something arg) {
    switch (code) {
    case "INSERT":
    handleInsert(arg);
    break;
    case "DELETE":
    handlerDelete(arg);
    break;
    default:
    throw new IllegalArgumentException();
    }
    }

    만약 switch 문에 null 값이 들어오면(위 예제에서 code의 값이 null이면) NullPointerException이 발생한다.

    문법적으로 더 설명할 만한 것은 없으나 Strings in switch가 어떻게 구현되는지 간단히 알아보자. Project Coin의 목표는 JVM 명세는 변경하지 않으면서 변화를 주는 것이다. 즉 Strings in switch는 자바 바이트코드 수준에서 지원되는 것이 아니라 컴파일러가 적절한 코드로 변환하는 것이다. 그런데 Project Coin은 이런 변환 과정이 어떻게 이루어지는지, 어떤 코드로 변환하는지는 언급하지 않고 있다. 컴파일러에 따라 최적의 방식으로 구현할 것이며, 우리는 구체적인 내용은 알 것 없이 그냥 쓰면 된다.

    String을 사용할 수 있다면 그냥 모든 객체를 다 사용할 수 있게 하면 안 되는지 궁금해하는 사람들도 있을 것이다. 하지만 그렇게 만만한 일은 아니다. String을 지원하는 것은 자바 명세에 한 줄을 추가하는 것으로 끝났지만, Object를 지원하려면 훨씬 많이 변경해야 한다. 게다가 switch 문에서 굳이 Object를 사용해야 하는 경우가 있을까?

  • Improved Type Inference for Generic Instance Creation (diamond)

  • 이 기능은 많은 사람들이 반길 만한 기능이다. 기존에 작성했던 다음과 같은 코드를 보자.

    1
    2
    List<map <string,="" object="">> mapList = new ArrayList<map <string,="" object="">>();
     </map></map>

    위 코드를 다음과 같이 작성할 수 있게 되었다.

    1
    2
    List<map <string,="" object="">> mapList = new ArrayList<>();
     </map>

    즉, generic 객체를 생성할 때, 타입 파라미터를 명시하지 않아도, 컴파일러가 추론해서 자동으로 채워주는 기능이다. 물론, 모든 상황에서 가능한 것은 아니지만 OpenJDK, Tomcat, NetBeans 소스를 대상으로 테스트한 결과 약 90%정도는 정확하게 추론해냈다고 한다.

    일반적으로 객체를 생성하는 경우는 다음과 같이 네 가지로 나눌 수 있으며, 네 경우 모두 diamond를 사용할 수 있다.

    • 변수 선언
    1
    2
    List<map <string,="" object="">> mapList = new ArrayList<>();
     </map>
    • 변수 대입
    1
    2
    3
    4
    List<map <string,="" object="">> mapList;
    ……
    mapList = new ArrayList<>();
     </map>
    • 메서드 호출
    1
    2
    3
    4
    5
    6
    public void doSomething(List<map <string,="" object="">> map) {
    ……
    }
      
    doSomething(new ArrayList<>());
     </map>
    • 반환
    1
    2
    3
    4
    5
    public List<map <string,="" object="">> doSomething() {
    ……
    return new ArrayList<>();
    }
     </map>

    그러나 항상 이 기능을 사용하는 것이 바람직하지는 않을 것 같다. 메서드를 호출할 때, 특히 타입 파라미터를 가진 메서드를 호출할 때에는 상황이 복잡해질 수 있고, 최악의 경우 컴파일러가 추론에 실패하여 엉뚱한 타입을 집어넣거나 아예 컴파일 에러가 발생할 수도 있다. 이때에는 뭐가 잘못된 것인지 상황을 분석하기도 쉽지 않다고 한다.

    하지만 변수 선언이나 대입의 경우에는 귀찮은 중복 코딩을 많이 줄여줄 유용한 기능이니 적극적으로 사용하길 권장한다.

  • Simplified Varargs Method Invocation

  • 이 기능은 매우 간단하지만 왜 필요한지 이해하기는 쉽지 않다. 바로 Generics가 얽힌 문제이기 때문이다. 한 문장으로 설명하자면 "비정형적인(non-reifiable) varargs 파라미터를 가지는 메서드의 varargs 파라미터에 비정형적인 타입의 값을 전달하여 호출하면 반드시 경고가 발생하는데, 이를 없앨 수 있는 방법을 제공하겠다"라는 것이다.

    1
    2
    3
    4
    Map<string ,="" object=""> foo = new HashMap<string ,="" object="">();
    Map<string ,="" object=""> bar = new HashMap<string ,="" object="">();
    List<map <string,="" object="">> result = Arrays.asList(foo, bar); // 경고 발생
     </map></string></string></string></string>

    세 번째 줄에서 'Type safety : A generic array of Map<String, Object> is created for a varargs parameter'라는 경고가 발생한다. 무엇이 문제인지 알아보기 위해 asList() 메서드를 살펴보자.

    1
    2
    3
    4
    public static <t> List<t> asList(T... a) {
    return new ArrayList<t>(a);
    }
     </t></t></t>

    아무리 봐도 잘못된 부분은 없다. 하지만 asList()를 호출하는 코드에 @SuppressWarnings("unchecked")를 삽입하는 것 외에는 저 경고를 제거할 수 있는 방법이 없다.

    이 문제는 컴파일러가 varargs 메서드 파라미터를 배열로 변환하기 때문에 발생한다. asList()는 내부적으로 다음과 같이 변환된다.

    1
    2
    List<map <string,="" object="">> result = Arrays.asList(new Map<string ,="" object=""> [] { foo, bar });
     </string></map>

    실제로 이 코드를 직접 작성하여 컴파일하면 new Map<String, Object>[]에서 에러가 발생하고 컴파일이 중지된다. Java Generics에서 사용하는 type eraser 때문에 형 안전(type safe)하지 않은 상황이 발생할 가능성이 있어서 generic 배열을 생성할 수 없도록 했기 때문이다. 그런데 varargs 메서드를 처리할 때에는 컴파일러가 직접 이렇게 변환하고는 경고를 한다. 그러면 다음과 같은 문제가 발생한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public static <t> ValueHolder<t>[] doBadThing(ValueHolder<t>... values) {
    Object[] objs = values;
    objs[0] = new ValueHolder<integer>(10);
    return values;
    }
      
    public static void main(String[] args) {
    ValueHolder<string>[] result = doBadThing(new ValueHolder<string>("foo"), new ValueHolder<string>("bar"));
      
    for (ValueHolder<string> holder : result) {
    String value = holder.getValue(); // ClassCastException 발생
    System.out.println(value);
    }
    }
      
    public static class ValueHolder<t> {
    private T value;
      
    public ValueHolder(T value) {
    this.value = value;
    }
      
    public T getValue() {
    return value;
    }
    }
     </t></string></string></string></string></integer></t></t></t>

    위의 코드를 컴파일하면 경고만 하나 발생하고 컴파일이 완료된다. 그런데 이를 실행시키면 ClassCastException이 발생한다. 이는 doBadThing()에서 values에 ValueHolder<Integer>를 넣었기 때문이다. 그리고 컴파일러가 이를 막지 못한 것은 type eraser에 의해 values의 타입 파라미터 정보가 지워졌기 때문이다. 즉, 런타임에 VM이 values가 ValueHolder의 배열이라는 것만 알 뿐 ValueHolder<String> 배열이라는 것은 알 수 없다. 마찬가지로 ValueHolder<Integer>를 생성하더라도 런타임 시에는 이것이 ValueHolder로만 취급된다. 따라서 JVM이 보기엔 아무런 문제가 없다.

    위 코드는 JVM에게 아래와 같은 코드로 보인다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public static ValueHolder[] doBadThing(ValueHolder[] values) {
    Object[] objs = values;
    objs[0] = new ValueHolder (10);
    return values;
    }
      
    public static void main(String[] args) {
    ValueHolder[] result = doBadThing(new ValueHolder[] { ValueHolder ("foo"), new ValueHolder ("bar")});
      
    for (ValueHolder holder : result) {
    String value = (String) holder.getValue(); // ClassCastException 발생
    System.out.println(value);
    }
    }
      
    public static class ValueHolder {
    private Object value;
      
    public ValueHolder(Object value) {
    this.value = value;
    }
      
    public Object getValue() {
    return value;
    }
    }

    이제 어떤 문제가 발생할 수 있는지 알았다. 그런데 저런 일이 얼마나 일어나겠는가? 일부러 문제를 일으키려고 작정하지 않은 이상 이런 코드를 작성할 일은 거의 없다. 처음에 예를 든 Arrays.asList()에서도 이런 문제가 발생할 일은 전혀 없다. 그럼에도 불구하고 우리는 계속 무의미한 경고를 보지 않을 수 없었다.

    JDK 7에서는 이러한 문제를 해결하기 위해 다음과 같은 조치를 취했다.

    • non-reifiable한 varargs를 가지는 메서드의 호출뿐만 아니라 선언에도 경고를 발생시킨다.
    • 해당 메서드에 아무런 문제가 없다면 메서드의 선언에 @SafeVarargs 어노테이션을 달 수 있다. 이 어노테이션을 달면 메서드의 선언과 호출에 경고가 발생하지 않는다.

    다음은 @SafeVarargs의 사용 예제이다. 이 메서드를 호출하는 코드에는 경고가 발생하지 않는다.

    1
    2
    3
    4
    5
    6
    7
    @SafeVarargs
    public static <t> void addAll(List<t> list, T... items) {
    for (T item : items) {
    list.add(item);
    }
    }
     </t></t>

    @SafeVarargs를 아무데나 붙일 수 있는 것은 아니다. 다음 조건을 만족시키지 않는 메서드에 @SafeVarargs를 사용하면 컴파일 에러가 발생한다.

    • varargs 메서드여야 한다
    • final 메서드나 static 메서드여야 한다.

    또한, 명세에서는 다음과 같은 메서드에 @SafeVarargs를 달면 컴파일러가 경고를 발생시키도록 권장하고 있다.

    • varargs 파라미터가 reifiable 타입인 경우
    • 위 예시 코드의 doBadThing()에서처럼 varargs 파라미터를 다른 변수에 대입하는 등 메서드에서 잠재적으로 문제의 가능성이 있는 연산을 수행하는 경우

    당연히 JDK 7의 라이브러리에는 @SafeVarargs가 적용되어 있다. 다음은 @SafeVarargs가 적용된 메서드이다.

    • public static <T> List<T> java.util.Arrays.asList(T... a)
    • public static <T> boolean java.util.Collections.addAll(Collection<? super T> c, T... elements)
    • public static <E extends Enum<E>> java.util.EnumSet<E> EnumSet.of(E first, E... rest)
    • protected final void javax.swing.SwingWorker.publish(V chunks)
  • Multi-catch and more precise rethrow

  • 이제 남은 두 가지 기능은 모두 try/catch 절과 관련된 것이다. 그 중에 Multi-catch and more precise rethrow는 두 가지 내용을 담고 있는데, 이는 어떤 한 가지 문제 상황을 해결하기 위한 것이다. 다음 예제를 통해 어떠한 문제인지 살펴보도록 하자.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public void someMethod() throws FooException, BarException {
    try {
    canThrowFooException();
    canThrowBarException();
    } catch (FooException e) {
    logger.log(e, e);
    throw e;
    } catch (BarException e) {
    logger.log(e, e);
    throw e;
    }
    }

    someMethod()는 canThrowFooException()과 canThrowBarException()을 호출하며, 이 메서드들은 각각 FooException과 BarException을 던진다. 이들이 예외를 던지면 someMethod()는 이를 잡아 로그를 남긴 후 다시 던진다.

    그런데 FooException과 BarException에 대한 catch 절이 완전히 동일하다. 중복을 줄이기 위해서 FooException과 BarException을 한꺼번에 처리할 수 있다면 좋을 것이다. 그래서 JDK 7에서는 다음과 같이 하나의 catch 절에서 여러 타입의 예외를 처리할 수 있는 multi catch 기능을 제공한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public void someMethod() throws FooException, BarException {
    try {
    canThrowFooException();
    canThrowBarException();
    } catch (FooException | BarException e) {
    logger.log(e, e);
    throw e;
    }
    }

    이때 e는 FooException과 BarException이 공통으로 구현한 모든 타입, 즉 공통의 조상 클래스를 상속받고 두 예외가 공통으로 구현한 인터페이스를 구현한 타입으로 취급받는다. 예를 들어 FooException과 BarException이 다음과 같이 선언되었다면 위 코드의 예외 파라미터 e는 ParentException를 상속받고 SomeInterface를 구현한 타입으로 취급된다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class ParentException {
    public void parentMethod() {
    //……
    }
    }
      
    public interface SomeInterface {
    void someMethod();
    }
      
    public interface AnotherInterface {
    void anotherMethod();
    }
      
    public FooException extends ParentException implements SomeInterface, AnotherInterface {
    // ……
    }
      
    public BarException extends ParentException implements SomeInterface {
    // ……
    }

    이렇게 multi catch에 사용된 파라미터 e는 final로 선언되지 않았어도 암묵적으로 final로 취급된다. 물론 명시적으로 final로 선언해도 되지만 스타일상의 이슈로 final을 쓰지 않을 것을 권장하고 있다. 사실 예외 파라미터에 다른 값을 대입할 일은 거의 없기 때문에 문제가 되지는 않을 것이다.

    multi catch와 관련한 또 하나의 제약 사항은 하나의 multi catch 절에서 처리하는 타입들 간에 부모 자식 관계가 존재하면 안 된다는 것이다. 예를 들어 다음과 같이 FooException과 그 부모 클래스인 ParentException을 함께 잡는 코드는 컴파일 에러가 발생한다. 하지만 이 역시, ParentException 하나만 잡으면 당연히 FooException도 같이 잡히기 때문에 굳이 둘을 같이 처리할 필요는 없으므로 별로 문제가 될 만한 제약은 아니다.

    1
    2
    3
    4
    5
    6
    7
    try {
    canThrowFooException();
    canThrowBarException();
    } catch (ParentException | FooException e) { // 컴파일 에러
    logger.log(e, e);
    throw e;
    }

    다시 처음에 제기했던 문제로 돌아가 보자. 사실 multi catch 없이 코드 중복을 막을 수 있는 방법도 있다. 다음 코드를 보자.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public void someMethod() throws Exception {
    try {
    canThrowFooException();
    canThrowBarException();
    } catch (Exception e) {
    logger.log(e, e);
    throw e;
    }
    }

    이렇게 하나의 catch 절에서 모든 예외를 잡으면 된다. 그런데 여기에서 새로운 문제가 발생한다. 이렇게 모든 예외를 잡아서 다시 던지려면 someMethod()도 예외를 던지도록 선언해야 한다. 문제는, 누가 봐도 FooException과 BarException 밖에 나올 수 없는데 컴파일러는 그냥 catch절만 보고 throws Exception을 선언해야 한다고 판단해 버린다는 것이다.

    JDK 7에서는 바로 more precise rethrow로 이 문제를 해결했다. 이제 Java 컴파일러는 catch 절에서 다시 예외를 던질 때 다음과 같이 그 예외의 타입을 결정한다. catch 절에서 사용된 예외 파라미터가 다음 조건을 만족하면 위의 코드에서 someMethod()는 FooException과 BarException을 던진다고 선언할 수 있다.

    • final이거나 effectively final이다. effectively final은 JDK 7에서 추가된 개념으로, final로 선언되지 않았지만 한 번 값이 할당된 후 변경되는 일이 없는 경우를 가리킨다.
    • try 절에서 발생할 수 있다.
    • 앞선 catch 절에서 잡은 것이 아니다.
    • 예외 파라미터에 대입할 수 있다.

    다음 예시에서 canThrowWhat()이 어떤 예외를 던진다고 선언해야 하는지, 즉 메서드 본문의 두 번째 catch 절에서 던지는 예외가 어떤 타입인지 결정되는 과정을 통해 이 규칙이 어떻게 적용되는지 살펴보겠다. 앞선 예시들에서 본 것처럼 canThrowFooException()과 canThrowBarException()은 각각 FooException과 BarException을 던지며, FooException과 BarException은 ParentException을 상속받는다. canThrowAnotherException()은 AnotherException을 던지며, AnotherException은 FooException, BarException, ParentException과는 아무런 관계가 없고 Exception을 직접 상속받는 예외이다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public void canThrowWhat() throws ??? {
    try {
    canThrowFooException();
    canThrowBarException();
    canThrowAnotherException();
    } catch (FooException e) {
    // ……
    } catch (ParentException e) {
    System.out.println(e);
    throw e; // e의 타입은?
    } catch (AnotherException e) {
    // ……
    }
    }
    • catch 절에서 예외 파라미터 e에 다른 값이 할당되지 않으므로 e는 effectively final이다.
    • try 절에서 던지는 예외는 FooException, BarException, AnotherException이다.
    • 앞선 catch 절에서 FooException을 잡으므로 남은 것은 BarException과 AnotherException이다.
    • ParentException에 대입 가능한 것은 BarException이다.

    따라서 canThrowWhat()은 BarException을 던진다고 선언하면 된다.

    more precise rethrow와 관련해서 마지막으로 언급할 것은 이 기능 때문에 소스 레벨에서 하위 호환성이 깨진다는 점이다. 즉, JDK 6까지 문제 없이 컴파일이 되던 코드가 JDK 7에서는 컴파일이 되지 않을 수 있다. 하지만 걱정할 필요는 없다. 다행히도 이런 코드는 실제로는 전혀 있을 수 없는 코드이고, 실제로 수많은 소스코드를 검사하여 이런 패턴의 코드는 존재하지 않음을 확인했다고 한다. 그러면 도대체 어떤 코드에 문제가 발생하는 것인지 알아보자.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    try {
    throw new FooException();
    } catch (ParentException e) {
    try {
    throw e;
    } catch (BarException e) {
    // 도달할 수 없는 코드
    }
    }

    JDK 6에서는 두 번째 throw가 ParentException을 던진다고 판단하고 BarException을 잡는 catch 절이 실행 가능하다고 판단한다. 하지만 JDK 7에서는 두 번째 throw가 던지는 예외가 FooException이라고 더 정확하게 판단한다. 그 결과 BarException을 잡는 catch 절이 사용되지 않는 코드(dead code)임을 알아차리고 컴파일 에러를 발생시킨다. 즉, 실제로 잘못된 코드였으므로 컴파일되지 않는다고 해서 문제가 되진 않는다.

  • try-with-resource statement

  • 이제 Project Coin의 마지막 기능이자 말도 많고 탈도 많았던 try with resource이다. 이 기능은 자원을 사용한 후 적절하게 해제하는 것이 너무 어렵기 때문에 언어적으로 이를 지원하여 개발자의 실수를 줄여주자는 의도에서 도입되었다. Joshua Bloch는 이 기능을 제안하면서 Sun의 공식 문서에도 틀린 예제가 올라와 있었고, 자신이 쓴 책 Java Puzzler에서도 틀린 답이 있었는데 아무도 알아채지 못했을 정도로 자원을 관리하는 것은 어렵다고 말했다.

    이 기능은 네 부분으로 이루어져 있으며, 첫 번째는 try with resource에 의해 자동으로 관리될 클래스가 구현해야 하는 AutoCloseable 인터페이스이다.

    1
    2
    3
    public interface AutoCloseable {
    void close() throws Exception;
    }

    두 번째는 새로운 try with resource 문법이다. 다음과 같은 코드를 작성하면 try 블록 안에서 예외가 발생하든 발생하지 않든 resource.close()가 반드시 호출됨을 보장한다. 단, 여기서 SomeResource는 AutoCloseable을 구현한 클래스여야 한다.

    1
    2
    3
    try (SomeResource resource = new SomeResource()) {
    // …
    }

    세 번째는 Throwable 클래스의 변경이다. try 절에서 예외가 발생하여 자원의 close() 메서드를 호출했는데 거기서 또 예외가 발생한 경우, 최종적으로 던져지는 예외는 try에서 발생한 예외이다. 하지만 close()를 호출할 때 발생한 예외를 그냥 무시할 수도 없다. 이를 해결하기 위해 suppressed exception이라는 개념이 도입됐고, Throwable에 이와 관련한 메서드들이 추가되었다.

    마지막으로, JDK의 플랫폼 라이브러리 중 각종 스트림과 채널 등 try with resource에 사용될 수 있는 모든 클래스는 AutoCloseable을 구현하도록 수정되었다. 특히 java.io.Closeable이 AutoCloseable을 상속받도록 수정됐기 때문에 Closeable을 구현한 모든 클래스는 자동으로 AutoCloseable을 구현하게 된다.

    그럼 이제부터 try with resource에 대해 자세히 살펴보도록 하자.

    1
    2
    3
    4
    5
    6
    7
    try (SomeResource resource = createSomeResource()) {
    // ……
    } catch (FooException e) {
    // ……
    } finally {
    // ……
    }

    try 뒤, SomeResource를 선언하고 있는 괄호 안을 자원 명세(Resource Specificaiton)라고 한다. 이 곳에서 AutoCloseable을 구현한 클래스들의 변수를 하나 이상 선언할 수 있다. 이 변수들은 암묵적으로 모두 final로 처리되기 때문에 반드시 이 곳에서 초기화되어야 한다. 또, 이 변수들의 scope는 자원 명세부와 try 블록으로 한정되기 때문에 catch와 finally 블록에서는 이 변수들이 보이지 않는다.

    앞에서 언급했듯이 Project Coin은 JVM 명세를 전혀 변경하지 않는다. 따라서 try with resource도 컴파일러가 코드를 변환하는 방식으로 동작한다. 따라서 컴파일러가 이 코드를 어떻게 변환하는지 살펴보면 try with resource가 어떻게 동작하는지 알 수 있을 것이다. 이 코드만 이해하면 try with resource에 대해 모든 것을 파악한 것이나 마찬가지이다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    try {
    final SomeResource resource = createSomeResource();
    Throwable primaryException = null;
      
    try {
    // 원래 try 블록의 코드
    } catch (Throwable t) {
    primaryException = t;
    throw t; // more precise rethrow를 활용
    } finally {
    if (resource != null) {
    if (primaryException != null) {
    try {
    resource.close();
    } catch (Throwable suppressedException) {
    primaryException.addSuppressed(suppressedException);
    }
    } else {
    resource.close();
    }
    }
    }
    } catch (FooException) {
    // 원래 catch 블록의 코드
    } finally {
    // 원래 finally 블록의 코드
    }

    변환된 코드에서 알 수 있는 사실은 다음과 같다.

    • 자원의 초기화 도중 예외 E가 발생하면 E가 전파된다. try 블록은 실행되지 않으며 자원의 close()도 호출되지 않는다.
    • 자원의 초기화 도중 예외가 발생하지 않았고, 자원이 null이 아니면 자원의 close() 메서드는 반드시 호출된다.
    • try 블록 안에서 예외 E가 발생하고 자원의 close() 메서드에서 예외가 발생하지 않으면 E가 전파된다.
    • try 블록 안에서 예외 E가 발생하고 자원의 close() 메서드에서 예외 F가 발생하면 E가 전파되며 F는 E의 suppressed exception으로 추가된다 .
    • try 블록 안에서 예외가 발생하지 않고 자원의 close() 메서드에서 예외 E가 발생하면 E가 전파된다.

    또한 왜 자원 명세에 선언된 변수가 try 블록 안에서만 보이는지 알 수 있으며, final로 선언된 것도 확인할 수 있다.

    앞에서 언급한 것과 같이, 자원 명세부에서 세미콜론(;)으로 구분하여 변수를 여러 개 선언할 수 있다. 이때 앞에서 선언한 변수를 뒤에서 사용할 수도 있다. 다음 코드에서 앞에서 선언한 some을 AnotherResource의 생성자에 전달하는 것이 그 예이다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    try {
    final SomeResource some = createSomeResource() {
    Throwable primaryException = null;
      
    try (AnotherResource another = new AnotherResource(some);) {
    // 원래 try 블록의 코드
    } catch (Throwable t) {
    primaryException = t;
    throw t;
    } finally {
    if (some != null) {
    if (primaryException != null) {
    try {
    some.close();
    } catch (Throwable suppressedException) {
    primaryException.addSuppressed(suppressedException);
    }
    } else {
    some.close();
    }
    }
    }
    } catch (FooException) {
    // 원래 catch 블록의 코드
    } finally {
    // 원래 finally 블록의 코드
    }

    이렇게 여러 개의 자원을 선언했을 때 어떻게 동작하는지도 컴파일러가 이 코드를 어떻게 변환하는지 살펴보면 명확하게 알 수 있다. 컴파일러는 여러 개의 자원이 선언되었을 때 이를 재귀적으로 처리한다. 우선은 첫 번째 자원인 some에 대해서 앞에서 본 것과 같은 방법으로 변환한다. 이때 차이점은 안쪽 try가 자원 명세에서 some을 제외한 나머지 자원, 즉 another에 대한 try with resource라는 것이다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    try {
    final SomeResource some = createSomeResource() {
    Throwable primaryException = null;
      
    try {
    final AnotherResource another = new AnotherResource(some) {
    Throwable primaryException2 = null;
      
    try {
    // 원래 try 블록의 코드
    } catch (Throwable t2) {
    primaryException2 = t2;
    throw t2;
    } finally {
    if (another != null) {
    if (primaryException2 != null) {
    try {
    another.close();
    } catch (Throwable suppressedException2) {
    primaryException2.addSuppressed(suppressedException2);
    }
    } else {
    another.close();
    }
    }
    }
    } catch (Throwable t) {
    primaryException = t;
    throw t;
    } finally {
    if (some != null) {
    if (primaryException != null) {
    try {
    some.close();
    } catch (Throwable suppressedException) {
    primaryException.addSuppressed(suppressedException);
    }
    } else {
    some.close();
    }
    }
    }
    } catch (FooException) {
    // 원래 catch 블록의 코드
    } finally {
    // 원래 finally 블록의 코드
    }

    자원이 세 개 선언되었다면 여기에서 변환이 한 번 더 일어난다. 이와 같은 방식으로 재귀적으로 처리한다.

    이렇게 두 개 이상의 자원이 사용되었을 때 변환되는 코드를 살펴보면 다음과 같은 사실을 확인할 수 있다. 여러 자원을 선언했을 때 자원 명세에서 선언된 자원을 초기화하는 중에 예외 E가 발생하면 그 이후에 선언된 자원들은 초기화되지 않으며, 그 이전에 선언된 자원들 중 null이 아닌 자원에 대해서는 close()가 호출된다. 이 때 close() 호출 도중 예외가 발생하면 그 예외들은 E의 suppressed exception으로 설정되며 최종적으로 전파되는 예외는 E이다.

     

    보통 이렇게 새로운 기능에 대한 설명을 듣고 나면 왠지 모든 문제를 해결해 줄 것이라는 기대를 갖게 된다. 하지만 이 세상에 완벽한 것은 없다. try with resource 명세 제정 과정에서 오고 간 논의의 한 토막을 살펴보자.

     

    A: 자원을 자동으로 관리해 줄 수 있도록 try with resource를 만들자.

    B: 그것으로는 lock을 처리할 수 없다.

    A: 자원마다 사용하는 패턴이 다르다. try with resource는 획득 -> 사용 -> 반환의 패턴으로 사용되는 자원만 지원한다.

    B: 일부에만 적용되는 복잡한 솔루션을 언어에 넣는 것은 바람직하지 않다. 게다가 JDK 8에 closure가 도입되면 언어에서 지원하지 않아도 라이브러리 수준에서 해결할 수 있는 문제이다.

    A: 일부가 아니라 대부분의 자원에 대해 적용할 수 있으며, 그것만으로도 충분한 가치가 있다. 그리고 JDK 8까지 기다리기만 할 수는 없다.

     

    간단하게 정리하니 별 내용 아닌 것 같지만 Project Coin 메일링리스트에서 가장 많은 메일이 오간 주제가 try with resource였다. 논란이 컸던 이유는 '자원'이라는 애매모호한 용어 때문이 아닐까 한다. 자원이라는 단어 하나가 가리키고 있는 것이 너무나도 많다. 스트림, 채널, 소켓, 메모리, 커넥션, lock 등이 모두 자원이고 그 특성이 제각각인데 그냥 하나로 묶어서 '자원'이라고 부르고 '자원을 자동으로 관리해 주겠다'라고 하니 듣는 사람들은 엄청난 걸 떠올릴 수 밖에 없다.

     

    결론은 try with resource가 모든 자원 관리의 해법이 될 수는 없다는 것이다. 따라서 이 기능을 사용하고 싶다면 정말 try with resource의 패턴에 맞는 자원인지, 즉 한 번 획득해서 사용한 후 곧바로 반환해야 하는 자원인지 확인하고 사용해야 한다. 이렇게 적절한 자원에 try with resource를 활용하면 쉽게 안전한 코드를 작성할 수 있을 것이다.

  • More New I/O APIs

    JDK 7에서 두 번째로 살펴볼 부분은 NIO.2이다. "NIO"는 New I/O의 약자이고 뒤에 붙은 ".2"는 물론 두 번째라는 의미이다. 즉, NIO.2는 JDK에 두 번째로 추가되는 새로운 I/O API들을 나타내며, 크게 파일시스템 API와 비동기 채널로 나눌 수 있다.

    Java의 파일시스템 API에는 부족함이 많았다. 파일의 속성이야 워낙 플랫폼에 의존적인 부분이라 플랫폼 독립성을 핵심 가치로 내세우던 Java가 제공하기 난감한 부분이었겠지만, 파일 복사와 같은 기본적인 API조차 제공하지 않아 직접 만들어 쓰게 하는 것은 어찌 보면 성의 부족이라고까지 할 수 있었다. 당연히 개선을 요구하는 목소리가 꾸준히 있었지만 사실 파일을 다루는 것 자체가 치명적으로 중요한 경우가 많지 않고 또 그렇게 근사해 보이는 분야도 아니어서 차일피일 미루어져 왔던 것 같다. 그러다가 JDK 7에 이르러서야 드디어 이를 보완한 것이다.

    NIO.2의 두 번째 축인 비동기 채널은 I/O 작업을 비동기로 처리할 수 있도록 해주는 채널이다. 지금까지는 Apache MINA와 같은 별도의 프레임워크를 사용해야만 가능했던 일이지만 이제는 JDK 자체에서 이를 제공하게 되었다.

    모든 기능을 세세하게 살펴보긴 어려우므로 NIO.2의 주요 기능들에 대해서만 전체적으로 훑어보기로 하자.

  • Improved Filesystem Interface

    NIO.2의 파일시스템 API는 기존의 java.io.File을 보완하는 것이 아니라, 완전히 대체하는 것이다. 기존의 File 클래스는 사실 이도 저도 아닌 애매한 모양새였기 때문에 이를 유지보수하는 것보다는 아예 새 판을 짤 필요가 있었다. JDK 7에서는 File 클래스가 deprecate되진 않지만 사용을 자제할 것을 권장하고 있다.

  • Path

  • 우선 첫 번째로 살펴볼 것은 File 클래스를 직접 대체할 java.nio.file.Path이다. 이름에서 알 수 있듯이 파일보다는 경로 자체에 초점을 맞추고 있다. 실제로 Path의 메서드들을 보면 순전히 경로를 다루는 메서드뿐이라는 것을 알 수 있다. 사실 기존의 File 객체가 가리키는 것은 어떤 경로가 가리키는 대상일 뿐, 그게 진짜 파일인지는 알 수 없는 것이었으니 Path가 좀 더 정확한 의미일 것이다.

    Path가 제공하는 메서드는 다음과 같이 분류할 수 있다.

    • Path의 이름 요소 처리: getName(), getNameCount(), iterator()
    • 관련된 Path 구하기: getFileName(), getParent(), getRoot(), subpath()
    • 상대 경로 처리: relativize(), resolve(), resolveSibling()
    • 경로 비교: compareTo() , startsWith(), endsWith()
    • 경로 변환: normalize(), toAbsolutePath(),toRealPath()
    • 타입 변환: toFile(), toString(), toUri()
    • 기타: getFileSystem(), isAbsolute(), register()

    "이름 요소"는 경로를 구성하고 있는 디렉터리 또는 파일명을 뜻한다. 즉 "foo/bar/sample.txt"라는 경로에서 이름 요소는 "foo", "bar", "sample.txt"이다. getNameCount()는 이름 요소의 개수를, getName(int)은 주어진 인덱스에 해당하는 이름 요소를 반환하는 메서드이다.

    iterator()는 Iterable로부터 상속받은 메서드이며, 이 메서드를 호출하여 얻은 이터레이터는 경로의 이름 요소들을 순회하는 데 사용된다. 즉, "foo/bar/sample.txt"를 나타내는 Path의 이터레이터는 "foo", "bar", "sample.txt"를 차례로 반환한다.

    Path는 또한 File을 사용하는 기존 코드와의 호환성을 위해 toFile()을 제공한다. 반대로 File에도 toPath()가 추가되었으니 File이 Path로 대체된다고 해서 호환성 문제가 생기지는 않을 것이다.

    그런데 Path는 File과는 달리 클래스가 아니라 인터페이스이다. 그렇다면 Path는 어떻게 만들어 사용해야 할까?

  • FileSystem

  • 파일시스템은 플랫폼에 따라 매우 다른 모습을 가지고 있다. 경로를 가리키는 문자열, 즉 Path를 생성하기 위해 사용되는 경로 표기법조차도 제각각이다. 따라서 플랫폼마다 적절하게 경로를 해석하여 Path를 생성하는 방법을 제공해야 한다. NIO.2는 FileSystem이라는 팩토리로 이 문제를 해결했다.

    java.nio.file 패키지에는 FileSystem이라는 추상 클래스가 있다. 이 클래스의 getPath()가 바로 Path를 생성해주는 팩토리 메서드이다. JDK 7부터 JVM은 자신이 동작하는 플랫폼에 맞는 FileSystem을 기본 FileSystem으로 가지며, Path 등 파일시스템 관련 객체들은 기본 FileSystem을 통해 생성된다. 다음은 Path를 생성하는 예이다.

    1
    2
    FileSystem fs = FileSystems.getDefault();
    Path path = fs.getPath("foo/bar/sample.txt");

    위 예에서 볼 수 있듯이 FileSystems.getDefault()를 호출하면 현재 플랫폼에 맞는 기본 FileSystem이 반환된다. 이 FileSystem을 사용하여 Path 등 파일시스템 관련 객체들을 생성해 사용하면 된다. 이렇게 매번 FileSystems.getDefault()를 호출하는 것이 번거로운 사람들을 위해서 Paths라는 유틸리티 클래스도 제공한다. 다음은 Paths를 사용하는 예이다.

    1
    Path path = Paths.get("foo/bar/sample.txt");

    기본 FileSystem이 있다는 이야기는 기본이 아닌 FileSystem도 있다는 뜻이다. 하나의 JVM에는 여러 개의 FileSystem을 등록하고 이를 사용할 수 있다. 하지만 대부분의 개발자들은 기본 FileSystem만으로도 충분할 것이다.

    FileSystem은 Path 외에도 아직 설명하지 않은 NIO.2의 주요 타입들에 대한 팩토리 메서드들을 가지고 있다. Path 이외에 FileSystem으로 생성할 수 있는 객체는 다음과 같다.

    표 1 FileSystem으로 생성할 수 있는 객체

    객체

    메서드

    설명

    PathMatcher

    getPathMatcher()

    Path가 glob 또는 정규표현식으로 표현된 패턴과 부합하는지 확인하는 데 사용한다.

    FileStore

    getFileSotres()

    파티션, 볼륨 등과 같은 저장소를 나타내며 저장소에 대한 정보를 획득할 수 있다.

    UserPrincipalLookupService

    getUserPrincipalLookupService()

    사용자 또는 사용자 그룹의 principal을 구하기 위해 사용한다.

    WatchService

    newWatchService()

    WatchService를 생성한다.

  • Files

  • Path에는 파일 관련 메서드는 없고 오직 경로와 관련된 메서드뿐이다. NIO.2에서 파일을 다루는 메서드는 모두 java.nio.file.Files에 있다. Files에는 많은 메서드들이 정의되어 있어서 이를 하나하나 살펴보기는 어렵다. 대신, 대부분의 유틸리티 클래스들이 그렇듯이 어떤 메서드가 있는지 알아두는 것만으로도 충분히 유용하기 때문에, 여기서는 간단하게 Files의 메서드들을 종류에 따라 분류하고 나열해 보도록 하겠다.

    • 파일 연산
      • 파일을 이동/복사/삭제하는 move(), copy(), delete(), deleteIfExists()
      • 파일의 존재 여부를 체크하는 exists(), notExists()
      • 각종 파일시스템 구성 요소를 생성하는 createFile(), createLink(), createSymbolicLink(), createDirectory(), createDirectories()
      • 임시 파일과 디렉터리를 생성하는 createTempFile(), createTempDirectory()
    • I/O
      • 파일 I/O를 위한 객체들의 팩토리 메서드 newInputStream(),newOutputStream(),newByteChannel(), newBufferedReader(), newBufferedWriter()
      • 파일을 한꺼번에 바이트 배열로 읽고 쓰는 readAllBytes(), write()
      • 파일의 모든 줄을 한꺼번에 읽고 쓰는 readAllLines(), write()
    • 파일 속성
      • 파일의 특성을 체크하는 isDirectory(), isExecutable(), isHidden(), isReadable(), isRegularFile(), isSymbolicLink(), isWritable()
      • 파일 변경 시각을 조회, 설정하는 getLastModifiedTime(), setLastModifiedTime()
      • 파일의 크기를 조회하는 size()
      • 파일의 소유자를 조회하고 설정하는 getOwner().setOwner()
      • POSIX 파일 권한을 조회하고 설정하는 getPosixFilePermissions(), setPosixFilePermissions()
      • 위에 언급한 기본 속성뿐 아니라 플랫폼 종속적인 파일의 속성들까지 조회하고 설정하는 getAttribute(), getFileAttributeView(), readAttributes(), setAttribute()
    • 디렉터리 탐색
      • Visitor 패턴으로 디렉터리를 탐색하는 walkFileTree()
      • 디렉터리의 엔트리들을 순회하는 데 사용하는 DirectoryStream의 팩토리 메서드 newDirectoryStream()
    • 기타
      • 파일이 속한 FileStore를 반환하는 getFileStore()
      • 두 Path가 같은 파일을 가리키는지 확인하는 isSameFile()
      • 파일의 MIME 타입을 결정하는 probeContentType()
      • 심볼릭 링크가 가리키는 대상 Path를 조회하는 readSymbolicLink()
  • 파일 속성

  • 파일의 일부 속성은 거의 모든 플랫폼에서 동일하기 때문에 명시적인 API를 추가해도 큰 문제가 없다. 앞에서 살펴본 파일 속성 관련 메서드 중 대부분이 이러한 메서드이다. 하지만 이렇게 일반화하기 어려운 속성도 많다. 지금까지는 Java에서 이러한 속성에 접근할 수 있는 방법이 없었지만, NIO.2에서는 가능해졌다.

    FileAttributeView

    Java.nio.file.attribute 패키지에는 FileAttributeView와 그 하위 인터페이스들이 포함되어 있다. 각 인터페이스는 특정 파일 속성에 접근할 수 있는 메서드를 제공한다. 이름만 봐도 각 FileAttributeView가 어떤 속성을 다루는지 대략 짐작할 수 있을 것이다. 자세히 알고 싶다면 각 FileAttributeView의 javadoc을 살펴보기 바란다.

    • FileAttributeView
      • BasicFileAttributeView
        • PosixFileAttributeView
        • DosFileAttributeView
    • FileOwnerAttributeView
      • AclFileAttrivuteView
    • UserDefinedFileAttributeView

    FileAttributeView는 Files의 getFileAttributeView() 메서드를 호출해서 얻을 수 있다. 이때 원하는 FileAttributeView의 타입을 명시해야 한다.

    1
    2
    3
    4
    BasicFileAttributeView attrs = Files.getFileAttributeView(path, BasicFileAttributeView.class);
    if (attrs != null) {
    // 속성 처리
    }

    플랫폼마다 지원하는 FileAttributeView가 다르므로 지원하지 않는 FileAttributeView를 요청하면 null이 반환된다. BasicFileAttributeView는 모든 플랫폼에서 제공된다. 어떤 FileAttributeView가 지원되는지 미리 확인하려면 다음과 같이 파일이 속한 FileStore를 통해 조회한다.

    1
    2
    3
    4
    if (Files.getFileStore(path).supportsFileAttributeView(AclFileAttributeView.class)) {
    AclFileAttributeView view = Files.getFileAttributeView(path, AclFileAttributeView.class);
    // 속성 처리
    }

    FileAttributeView를 직접 사용하지 않고 파일 속성들에 접근할 수 있는 방법도 있다. BasicFileAttributeView와 그 하위 인터페이스들이 제공하는 속성은 다음과 같이 Files의 readAttributes()를 호출하면 바로 가져올 수 있다.

    1
    DosFileAttributes attrs = Files.readAttributes(path, DosFileAttributes.class);

    Dynamic Access

    모든 파일 속성에는 문자열로 표현되는 이름이 있다. 이 이름은 "{뷰 이름}:{속성 이름}"의 형식이며, 이렇게 이름으로 파일의 속성에 접근하는 것을 동적 접근이라 한다. 동적 접근을 위한 API도 Files에 있다. 다음은 DosFileAttribute의 hidden 속성을 가져오는 예이다.

    <
    1
    Boolean value = (Boolean) Files.getAttribute(path, "dos:hidden");

    setAttribute()을 사용하여 동적 접근으로 속성을 쓸 수도 있으며, readAttributes()로 여러 속성을 한꺼번에 읽어올 수도 있다.

  • 디렉터리 탐색

  • NIO.2 이전에는 어떤 디렉터리의 엔트리들을 탐색하려면 File의 list() 또는 listFiles()를 호출하여 엔트리 목록을 한꺼번에 읽어 들여야 했다. 그래서 엔트리의 개수가 아주 많은 경우에는 메모리 관련 이슈가 발생하기도 했다. NIO.2에서는 이런 문제를 해결하고 좀 더 편리하게 디렉터리를 탐색할 수 있는 방법을 제공한다.

    DirectoryStream

    첫 번째 방법은 디렉터리의 엔트리들에 대한 이터레이터를 제공하는 DirectoryStream을 사용하는 것이다. 다음은 디렉터리 내의 모든 엔트리를 출력하는 예이다.

    1
    2
    3
    4
    5
    6
    DirectoryStream<path> ds = Files.newDirectoryStream(path);
    for (Path entry : ds) {
    System.out.println(entry);
    }
    ds.close();
     </path>

    DirectoryStream를 사용하고 나면 위 예제처럼 반드시 close()를 호출해야 한다. 그렇지 않으면 자원의 누수로 시스템에 문제를 일으킬 수 있다. 그런데 자원을 반환하는 메서드를 제대로 호출하는 일은 생각보다 매우 어렵다. 그래서 앞에서 설명했듯이 JDK 7에는 try with resource라는 새로운 문법이 추가되었다. 이를 사용하면 try 또는 catch 절에서 예외가 발생하든 발생하지 않든, 사용한 자원의 close() 메서드가 반드시 호출되는 것을 보장한다. 다음은 JDK 7의 새로운 문법을 사용하도록 수정한 예이다.

    1
    2
    3
    4
    5
    6
    7
    8
    try (DirectoryStream<path> ds = Files.newDirectoryStream(path)) {
    for (Path entry : ds) {
    System.out.println(entry);
    }
    } catch (IOException | DirectoryIteratorException e) {
    System.err.println(e);
    }
     </path>

    DirectoryStream을 생성할 때, DirectoryStream.Filter 객체를 함께 전달하여 조건에 부합하는 엔트리만을 걸러내도록 할 수도 있다. 단순히 경로의 패턴을 가지고 엔트리를 걸러낼 경우 glob 패턴 문자열로 걸러낼 수도 있다. 다음은 DirectoryStream.Filter를 사용하여 디렉터리의 목록만을 출력하는 예이다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    DirectoryStream.Filter<path> filter = new DirectoryStream.Filter<path>() {
    @Override
    public boolean accept(Path entry) throws IOException {
    return Files.isDirectory(entry);
    }
    };
    try (DirectoryStream<path> ds = Files.newDirectoryStream(path, filter)) {
    for (Path entry : ds) {
    System.out.println(entry);
    }
    } catch (IOException | DirectoryIteratorException e) {
    System.err.println(e);
    }
     </path></path></path>

    FileVisitor

    DirectoryStream은 대상 디렉터리에 직접 포함된 엔트리만을 순회하며, 하위 디렉터리의 내용은 포함되지 않는다. 만약 하위 디렉터리들까지 모두 탐색하려면 FileVisitor를 사용한다. 이름에서 알 수 있듯이 visitor 패턴을 사용하여 파일 트리를 탐색한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    FileVisitor<path> visitor = new FileVisitor<path>() {
      
    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
    System.out.println("entering: " + dir);
    return FileVisitResult.CONTINUE;
    }
      
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
    System.out.println("visit: " + file);
    return FileVisitResult.CONTINUE;
    }
      
    @Override
    public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
    System.out.println("visit failed: " + file);
    return FileVisitResult.CONTINUE;
    }
      
    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
    System.out.println("leaving: " + dir);
    return FileVisitResult.CONTINUE;
    }
    };
      
    Files.walkFileTree(path, visitor);
     </path></path>

    walkFileTree()는 주어진 디렉터리를 루트로 깊이 우선 탐색을 수행하며 FileVisitor의 메서드를 호출한다. 메서드 이름에서 알 수 있듯이 디렉터리에 들어가기 전에 preVisitDirectory(), 파일을 방문할 때 visitFile(), 디렉터리에서 나올 때 postVisitDirectory(), 어떤 이유에서든 파일 방문에 실패했을 때 visitFileFailed()가 호출된다. 방문 순서에 대해서는 깊이 우선 탐색이라는 점 이외에 정해진 것이 없다. 즉, 한 디렉터리 안의 엔트리들이 어떤 순서로 방문될 지 알 수 없으며, 심지어 파일과 디렉터리 중 어느 것이 먼저 방문될지도 알 수 없다.

    FileVisitor의 각 메서드는 FileVisitResult라는 enum 타입을 반환하도록 선언되어 있으며, 이 반환값에 따라 파일 트리 탐색을 계속할지 결정한다. 위의 예처럼 CONTINUE를 반환하면 탐색을 계속하고, TERMINATE를 반환하면 탐색을 즉시 중단한다. SKIP_SUBTREE는 preVisitDiretory()에서만 사용할 수 있는 값으로 이 디렉터리 아래에 있는 것들은 탐색하지 말라는 의미이다. 마지막으로 SKIP_SIBLING은 현재 엔트리와 부모가 같은 엔트리들은 더 이상 탐색하지 않겠다는 의미이다.

    WatchService

    설정 파일이 변경되면 자동으로 감지하고 읽어 들여 반영시키라는 요구 사항이 있다면 어떻게 구현해야 할까? 이제까지는 파일을 읽어 들인 후 수정 시각을 기록하고, 타이머를 돌려 주기적으로 파일의 수정 시각을 체크하고, 수정 시각이 바뀌면 파일이 변경된 것으로 보고 파일을 읽어 들여야만 했다.

    NIO.2는 이런 상황에 활용할 수 있는 WatchService를 제공한다. 다음은 D:\temp 디렉터리의 변경 사항을 모니터링하는 예이다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    // 1. WatchService 획득
    FileSystem fs = FileSystems.getDefault();
    WatchService watcher = fs.newWatchService();
      
    Path target = fs.getPath("D:\\temp");
      
    // 2. 감시 대상에 WatchService 등록
    target.register(watcher, StandardWatchEventKind.ENTRY_CREATE,
    StandardWatchEventKind.ENTRY_DELETE,
    StandardWatchEventKind.ENTRY_MODIFY);
      
    for (;;) {
    // 3. 이벤트 대기
    WatchKey key = watcher.take();
      
     // 4. 이벤트 처리
    for (WatchEvent<!--?--> event: key.pollEvents()) {
    Path path = (Path) event.context();
      
    if (event.kind() == StandardWatchEventKind.ENTRY_CREATE) {
    System.out.println("created: " + path);
    } else if (event.kind() == StandardWatchEventKind.ENTRY_DELETE) {
    System.out.println("deleted: " + path);
    } else if (event.kind() == StandardWatchEventKind.ENTRY_MODIFY) {
    System.out.println("modified: " + path);
    } else if (event.kind() == StandardWatchEventKind.OVERFLOW) {
    System.out.println("overflow");
    } else {
    throw new RuntimeException("Unknown event kind: " + event);
    }
    }
      
    // 5. 이벤트 후처리
    boolean valid = key.reset();
      
    if (!valid) {
    break;
    }
    }

    다음은 위 코드의 각 단계에 대한 설명이다.

  • WatchService를 사용하기 위해 우선 WatchService 객체를 얻어 온다. 앞에서 설명한 것처럼 FileSystem의 newWatchService()를 호출하여 WatchService를 얻는다.
  • 감시할 대상, 여기서는 Path 타입의 변수인 target의 register() 메서드를 호출하여 WatchService를 등록한다. register()를 호출할 때 어떤 이벤트를 감시할 것인지도 함께 명시하는데, JDK 7에서 모니터링할 수 있는 이벤트는 StandardWatchEventKind에 정의된 EVENT_CREATE, EVENT_DELETE, EVENT_MODIFY로, 각각 디렉터리 내에 어떤 엔트리가 생성, 삭제, 수정되는 이벤트를 나타낸다.
  • WatchService의 take() 메서드를 호출하여 이벤트가 발생한 WatchKey를 가져온다. take() 메서드는 이벤트가 발생할 때까지 블록되는 메서드이다.
  • WatchKey는 WatchService와 Watchable 사이의 등록을 나타내는 객체이며, 이 객체의 pollEvents()를 호출하면 이벤트를 나타내는 WatchEvent의 List가 반환된다.

    WatchKey의 kind() 메서드를 호출하면 발생한 이벤트의 종류가 반환되고, context()를 호출하면 이벤트와 관련된 부가 정보가 반환된다. context()는 디렉터리에 대한 엔트리 생성, 삭제, 수정 등의 이벤트가 발생한 엔트리를 나타내는 Path 객체를 반환하며, 이 Path는 감시 대상으로부터의 상대 경로이다.

  • 이벤트를 모두 처리한 후에는 반드시 WatchKey의 reset() 메서드를 호출해야 하며, reset()을 호출하지 않으면 더 이상 이벤트를 통보받을 수 없다. reset() 메서드는 이 WatchKey가 유효한지를 나타내는 boolean을 반환하며, false가 반환되면 이 WatchKey는 더 이상 사용할 수 없다.
  • WatchService를 위의 예처럼 무한루프를 도는 전용 스레드에서만 사용해야 하는 것은 아니다. WatchService의 poll() 메서드를 사용하여 원하는 때에만 이벤트를 확인하는 방식으로 사용할 수도 있다. 또한, 감시할 대상이 하나뿐이라면 WatchService의 take()나 poll()을 호출할 필요 없이, register()를 호출할 때 반환된 WatchKey로 직접 pollEvents()를 호출해도 된다.

    이처럼 WatchService를 사용하면 디렉터리 내에서 발생하는 이벤트들을 쉽게 통보받을 수 있어 상당히 유용하다. 하지만 WatchService는 디렉터리에 대해서만 사용할 수 있으므로 특정 파일만을 감시하고 싶을 때에는 약간 번거롭다. 감시할 파일의 부모 디렉터리에 ENTRY_MODIFY 이벤트에 대해 WatchService를 등록한 후, 이벤트가 발생하면 WatchEvent의 context()를 호출하여 이벤트가 발생한 파일이 내가 감시하고자 하는 파일이 맞는지 확인하는 과정을 거쳐야만 한다.

  • Channel API

    이제 NIO.2의 다른 한 축인 비동기 I/O에 대해 살펴보도록 하겠다. 사실, NIO.2의 한 축이라고 했지만 파일시스템 API에 비하면 분량이 매우 적다.

  • Asynchronous I/O

  • NIO.2는 세 가지 비동기 채널을 제공한다. AsynchronousFileChannel, AsynchronousServerSocketChannel, AsynchronousSocketChannel이며, 각각 FileChannel, ServerSocketChannel, SocketChannel의 비동기 버전이다. 즉, 다른 점은 모두 똑같고 I/O를 요청하는 메서드가 비동기로 동작하는 차이점만 있다고 보면 된다.

    비동기 채널은 두 가지 패턴으로 사용할 수 있는데, Future를 사용한 방법과 CompletionHandler를 사용하는 방법이 있다. 모든 비동기 채널은 이 두 가지 패턴의 I/O 메서드를 지원하는데, I/O 메서드의 구체적인 시그니처는 각 채널의 특성에 따라 조금씩 다르다. 여기서는 혼자서도 간단하게 테스트해 볼 수 있는 AsynchronousFileChannel을 위주로 설명한다.

  • Future

  • 먼저 Future를 사용한 비동기 I/O 예를 살펴보자.

    1
    2
    3
    4
    5
    6
    7
    8
    AsynchronousFileChannel ch = AsynchronousFileChannel.open(path);
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    Future<integer> future = ch.read(buffer, 0);
      
    // 다른 작업 처리
      
    Integer readBytes = future.get();
     </integer>

    이처럼 비동기 채널은 Future를 반환하는 I/O 메서드를 가지고 있으며, 이를 호출하면 I/O 작업이 끝나지 않았더라도 즉시 Future를 반환한다. 그리고 다른 작업을 처리하다가 나중에 Future로부터 I/O 작업의 결과를 확인할 수 있다. Future를 사용하는 기존의 다른 API들과 다를 바 없다.

  • CompletionHandler

  • 비동기 채널을 사용하는 또 다른 방법은 CompletionHandler를 사용하는 것이다. 이는 I/O 작업이 끝났을 때 호출할 콜백 함수를 등록하는 것이라 생각하면 된다. 예시 코드부터 살펴보자.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    AsynchronousFileChannel ch = AsynchronousFileChannel.open(path);
    ByteBuffer buffer = ByteBuffer.allocate(1024);
      
    CompletionHandler<integer ,="" bytebuffer=""> handler = new CompletionHandler<integer ,="" bytebuffer="">() {
      
    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
    System.out.println(exc);
    }
      
    @Override
    public void completed(Integer result, ByteBuffer attachment) {
    byte[] bytes = new byte[result];
    attachment.flip();
    attachment.get(bytes);
      
    System.out.println("read " + result + " bytes");
    System.out.println(new String(bytes));
    }
    };
      
    ch.read(buffer, 0, buffer, handler);
      
    // 다른 작업 처리
     </integer></integer>

    CompletionHandler를 사용하는 I/O 메서드를 호출하면, I/O 작업의 완료와 상관 없이 즉시 반환된다. I/O를 요청한 스레드는 이제 I/O를 신경쓰지 않고 다른 작업을 수행해도 된다. I/O가 완료됐을 때 다른 스레드에서 CompletionHandler의 메서드를 호출하여 뒤처리를 할 것이기 때문이다.

    I/O가 성공적으로 완료되면 CompletionHandler의 completed()가 호출되고, 실패하면 failed()가 호출된다. completed()는 I/O 작업의 결과인 result와 I/O작업을 요청할 때 전달한 attachment를 파라미터로 받고, failed는 I/O 실패를 나타내는 예외와 함께 attachment를 파라미터로 받는다.

    attachment는 I/O 요청과 완료 처리 간의 문맥을 유지시키기 위한 것으로, 어떤 값이든 전달할 수 있다. 위 예에서는 파일 읽기 작업을 요청하면서 읽기 버퍼를 attachment로 전달하므로 버퍼로 읽어 들인 값을 completed()에서 출력할 수 있다.

  • Groups

  • 바로 앞에서 CompletionHandler는 다른 스레드에서 호출된다고 했는데, 이 스레드의 정체에 대해서 간단히 이야기해보자.

    네트워크와 관련된 비동기 채널들은 모두 AsynchronousChannelGroup에 속한다. 같은 그룹에 속한 채널들은 스레드 풀 등의 자원을 공유하며, 바로 이 스레드 풀이 CompletionHandler를 호출한다.

    채널을 생성할 때, 즉 AsynchronousSocketChannel이나 AsynchronousServerSocketChannel의 open()을 호출할 때, 새로 생성될 채널이 속할 AsynchronousChannelGroup을 지정할 수 있다. 채널이 속할 AsynchronousChannelGroup을 지정하지 않으면 시스템의 기본 그룹에 포함된다.

    AsynchronousFileChannel도 마찬가지로 CompletionHandler 호출 등에 사용할 스레드 풀이 필요하지만 AsynchronousChannelGroup에 속하지 않는다. 대신 채널을 생성할 때, 사용할 ExecuterService를 지정할 수 있으며, 따로 지정하지 않으면 시스템 기본 스레드 풀을 사용한다.

  • Fork/Join Framework

    Fork/Join 프레임워크는 재귀 알고리즘을 병렬적으로 수행하여 멀티프로세서 환경을 충분히 활용할 수 있도록 해주는 프레임워크이다. 이 프레임워크는 Doug Lea의 주도하에 작성됐으며, 그의 논문 "A Java Fork/Join Framework"에 기반을 두고 있다.

  • 병합 정렬

  • 재귀 알고리즘을 설명할 때 자주 사용되는 예인 병합 정렬을 예로 들어 Fork/Join이 어떤 것인지 감을 잡아보도록 하자. 두 개의 워커 스레드가 크기 4인 배열에 들어있는 데이터에 대해 병합 정렬(Merge Sort)을 수행하는 시나리오이며, M(a, b)는 배열의 a번째 값부터 b번째 값까지의 병합 정렬 작업을 나타낸다.

  • 각 워커 스레드에는 자신만의 작업 큐가 있다. 처음 Fork/Join 프레임워크에 M(0, 3) 작업을 전달하면, 놀고 있던 스레드 중 하나가 이 작업을 가져가 처리한다. 여기서는 1번 스레드가 먼저 작업을 가져간다.

    122111_0530_JDK71.png

  • M(0, 3)은 M(0, 1)과 M(2, 3) 두 하위 작업을 파생시킨다. 1번 스레드는 이 두 작업을 자신의 작업 큐에 넣는다. 2번 스레드는 아직 일거리가 없어 놀고 있다.

    122111_0530_JDK72.png

  • M(0, 3)은 파생된 두 하위 작업이 끝나야만 일을 마무리할 수 있다. 따라서 1번 스레드는 M(0, 3)를 잠시 한 쪽에 치워두고, 작업 큐에서 다음 작업을 꺼내서 처리한다.

    122111_0530_JDK73.png

  • M(2, 3)은 또다시 M(2, 2), M(3, 3)으로 나누어진다. 이번에도 1번 스레드는 이 두 작업을 자신의 작업 큐에 넣는다. 눈치 빠른 사람들은 이쯤에서 1번 스레드가 자신의 작업 큐의 한쪽(그림에서는 아래쪽)에서만 작업을 넣고 꺼내고 있다는 것을 알아챘을 것이다. Fork/Join에서 워커 스레드는 자신의 작업 큐를 LIFO(Last In, First Out)로 관리한다.

    122111_0530_JDK74.png

  • 이때 2번 스레드가 일거리를 찾는다. 자신의 작업 큐는 여전히 비어있기 때문에, 1번 스레드의 작업 큐에서 M(0, 1)을 훔친다.

    122111_0530_JDK75.png

  • 이것이 바로 work stealing이라는 정책으로, Cilk에서 처음 사용된 개념이다. 여러 스레드에 작업을 분배할 때, 어떤 작업이 얼마나 걸릴지 미리 예측하기 어렵기 때문에, 처음부터 각 스레드에 공평하게 작업을 분배해 준다는 것은 거의 불가능하다. 따라서 어떤 스레드는 일이 없어서 놀고 있는데, 어떤 스레드는 쉬지 않고 일해도 할 일이 산더미처럼 쌓여 있게 되는 상황이 벌어질 수 있다. 이런 경우에 놀고 있는 스레드가 바쁜 스레드의 작업을 가져다 처리하는 것이 work stealing이다. 따라서 각 프로세서에 골고루 작업이 분배되어 멀티프로세서 환경을 충분히 활용할 수 있다.

    여기서 하나 눈여겨볼 점은, 다른 스레드의 작업 큐에서 작업을 훔쳐올 때는 큐의 반대쪽 방향에서 꺼내온다는 것이다. 여기에는 두 가지 이유가 있다. 하나는 작업 큐의 주인인 스레드와 작업을 훔쳐가려는 스레드 간의 경합을 피하기 위한 것이다. 다른 하나의 이유는 work stealing의 횟수를 줄이기 위한 것이다. Fork/Join 프레임워크의 특성상, 먼저 큐에 들어간 작업이 더 큰 작업일 가능성이 높다. 위의 그림에서는 큐의 가장 안쪽에 M(0, 1)이 있는데, 이 작업이 큐에 있는 작업들 중 가장 크기가 크다는 것을 알 수 있다. 자잘한 일을 훔쳐오면 금방 일이 끝나서 또 할 일이 없어져 또 다시 일을 훔쳐와야 하니, 되도록이면 큼직한 일을 가져와 이런 수고를 덜게 하는 것이다.

  • 이제 두 스레드가 모두 열심히 일을 하고 있다. 1번 스레드는 M(2, 3)를 잠시 접어두고 M(3, 3)를 꺼낸다. 2번 스레드는 M(0, 1)을 처리하기 시작했고, 그 결과 파생된 M(0, 0)과 M(1, 1)을 자신의 작업 큐에 넣는다.

    122111_0530_JDK76.png

  • 1번 스레드가 처리하던 M(3, 3)는 더 이상 하위 작업을 파생시키지 않고 끝난다. 2번 스레드는 M(0, 1)을 한쪽으로 치워두고 M(1, 1)을 처리한다.

    122111_0530_JDK77.png

  • 1번 스레드의 M(2, 2) 역시 하위 작업을 추가시키지 않고 끝났다. M(2, 2)과 M(3, 3)이 모두 끝났으니 기다리던 M(2, 3)을 마무리할 수 있게 되었다. 2번 스레드도 M(1, 1)을 끝내고 M(0, 0)을 처리한다.

    122111_0530_JDK78.png

  • 1번 스레드는 M(2, 3)의 처리를 끝냈다. 하지만 아직 M(0, 1)의 처리가 끝나지 않았기 때문에 M(0, 3)을 시작할 수 없다. 훔쳐올 일이 있는지 2번 스레드의 작업 큐를 살펴보지만, 그쪽도 남은 일이 없다. 2번 스레드는 이제 M(0, 1)의 처리를 시작한다.

    122111_0530_JDK79.png

  • 2번 스레드가 M(0, 1)을 마쳤다. 1번 스레드는 M(0, 3)를 마무리한다. 이로써 모든 작업이 완료된다.

    122111_0530_JDK710.png

  • 이제 Fork/Join 프레임워크의 실체를 살펴보기로 하자. 다음은 위에서 설명한 병합 정렬을 처리하는 코드이다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    public class Sample {
    public static void main(String[] args) {
    ForkJoinPool threadPool = new ForkJoinPool();
      
    int[] values = new int[] { 10, 3, 4, 1, 2, 5, 7, 9 };
      
    threadPool.invoke(new SortTask(values, new int[8], 0, 7));
      
    System.out.println(Arrays.toString(values));
    }
      
    private static class SortTask extends RecursiveAction {
    private int[] a;
    private int[] tmp;
    private int lo, hi;
      
    public SortTask(int[] a, int[] tmp, int lo, int hi) {
    this.a = a;
    this.lo = lo;
    this.hi = hi;
    this.tmp = tmp;
    }
      
    @Override
    protected void compute() {
    if (hi == lo) {
    return;
    }
      
    int m = (lo + hi) / 2;
      
    invokeAll(new SortTask(a, tmp, lo, m), new SortTask(a, tmp, m+1, hi));
    merge(a, tmp, lo, m, hi);
    }
    }
      
    private static void merge(int[] a, int[] b, int lo, int m, int hi) {
    if (a[m] <= a[m+1])
    return;
      
    System.arraycopy(a, lo, b, lo, m-lo+1);
      
    int i = lo;
    int j = m+1;
    int k = lo;
      
    while (k < j && j <= hi) {
    if (b[i] <= a[j]) {
    a[k++] = b[i++];
    } else {
    a[k++] = a[j++];
    }
    }
      
    System.arraycopy(b, i, a, k, j-k);
    }
    }

    우선 눈여겨봐야 할 코드는 main() 메서드에서 ForkJoinPool을 생성하고 invoke()를 호출하여 SortTask를 전달하는 부분이다. 이것이 앞에서 본 그림 중 첫 번째에 해당하는 부분, 즉 모든 것이 시작되는 부분이다.

    그리고 SortTask의 compute() 메서드에서 하위 작업을 나타내는 SortTask 객체들을 생성하고 invokeAll()을 통해 이들을 호출하는 부분도 잘 살펴보기 바란다. invokeAll()은 인자로 전달된 모든 작업을 작업 큐에 넣고, 그들의 실행이 완료될 때까지 현재 작업을 멈추는 메서드이다. 앞의 그림 설명에서 '하위 작업들을 작업 큐에 넣고 현재 작업을 잠시 한쪽으로 치워두는' 것이 invokeAll()에서 이루어진다.

  • ForkJoinPool, ForkJoinTask

  • Fork/Join 프레임워크를 사용하기 위해 알아야 하는 것은 ForkJoinPool과 ForkJoinTask 둘뿐이다.

    ForkJoinPool은 워커 스레드들을 관리하며, 외부에서 이 스레드 풀에 작업을 요청할 때 사용할 수 있는 API를 제공한다. 간단하게 말하자면, ForkJoinPool은 work stealing이 가능한 ExecutorService라고 할 수 있다. 실제로 ExecutorService를 구현하고 있기 때문에 Runnable이나 Callable 객체를 ForkJoinPool에서 실행시킬 수도 있다. 하지만 이 경우, ForkJoinPool의 혜택을 모두 누릴 수는 없다.

    ForkJoinPool이 제공하는 효율적인 병렬 처리 효과를 제대로 보려면 ForkJoinTask를 사용해야 한다. ForkJoinTaks는 하위 작업을 작업 큐에 넣는 fork(), invoke(), invokeAll() 등의 메서드와 작업이 종료되길 기다릴 때 사용하는 join() 등의 메서드를 제공한다. 이를 활용하여 추상 메서드인 execute()를 구현하면 된다.

    Fork/join 프레임워크는 ForkJoinTask를 상속한 RecursiveAction과 RecursiveTask도 제공한다. 예시 코드에서 사용된 RecursiveAction은 리턴값이 없는 재귀 작업을 좀 더 간단하게 구현하게 해주며 RecursiveTask는 리턴값이 있는 재귀 작업을 위한 것이다.

    지면 관계상 각 클래스와 메서드들에 대해 깊이 살펴볼 수는 없으므로 관심 있으신 사람들은 JDK 7의 Javadoc을 살펴보기 바란다. Fork/Join 프레임워크 제작자인 Doug Lea가 Javadoc에 굉장히 상세한 설명을 달아놓았기 때문에 다른 레퍼런스가 거의 필요 없을 정도이다.

  • 그 밖에 추가된 것들

  • JDK 7에서 java.util.concurrent 패키지에 추가된 클래스는 위에 언급한 것들 외에 몇 가지가 더 있다. 이들은 대부분 Fork/Join 프레임워크의 구현에 필요하여 작성된 것이지만, 다른 곳에서도 유용하게 사용될 수도 있기에 포함된 것들이다. 이들에 대해 간단히 살펴보도록 하자.

    • TransferQueue: BlockingQueue를 상속받은 인터페이스로, transfer() 메서드를 호출하여 큐에 값을 넣으면, 누군가가 그 값을 꺼내갈 때까지 블록되는 큐이다.
    • LinkedTransferQueue: TransferQueue의 구현체이다.
    • Phaser: CyclicBarrier와 유사하지만, 좀 더 유연하고 다양한 기능을 제공한다.
    • ThreadLocalRandom: java.util.Random은 여러 스레드에서 동시에 사용할 경우 경합이 벌어져 성능에 영향을 줄 수 있다. ThreadLocalRandom은 이런 상황을 피할 수 있게 해준다.
  • 맺음말

    JDK 7은 분명 오래 기다린 것에 비해서는 아쉽다. 게다가 JDK 7가 나오자마자, 버그가 있으니 JDK 7을 사용하지 말라는 소리가 릴리스 소식보다 더 많이 들려온 것을 보면 앞날이 평탄치 않을 것 같기도 하다. 어쨌든 우리는 좋으면 쓰고 안 좋으면 안 쓰면 되는 것이고, JDK 7에는 써서 득 볼 수 있는 부분이 분명 있다고 본다.

    짧은 글로 JDK 7의 모든 면을 샅샅이 살펴보기는 불가능하다. 여기에서 미처 설명하지 못한 기능들이 많으며, 설명했다 하더라도 세세한 옵션이나 커스터마이징할 수 있는 부분에 대해 거의 언급하지 못 했다. 그러니 이 글을 JDK 7의 전부가 아니라 JDK 7을 알아가는 출발점으로 여기기 바란다.



  • helloworld_%EA%B4%80%EB%A6%AC%EC%9E%90%EA%B3%84%EC%A0%95%EC%82%AC%EC%A7%84%EB%B0%B0%EA%B2%BD.gif
    NBP 웹플랫폼개발랩 문종호
    세상만사 부질없으니 흘러가는 대로 흐르렵니다.
    어쩌면 무력함에 대한 변명일 지도 모르겠습니다만, 그렇다면 그런 거지 뭐 있겠습니까.



    출처 - http://helloworld.naver.com/helloworld/1219







    Sun(현재 Oracle)이 JDK 7을 개발하기 시작할 때 이전과 다른 점이 하나 있었는데, Sun이 JDK를 오픈소스화 하기 위해 2007년 OpenJDK를 만들었다는 것이다. (OpenJDK는 완전한 Free의 오픈소스 코드를 기반으로 Fully buildable한 Java Development Kit을 배포하기 위한 Sun의 노력이다.)

    Sun이 3rd-Party 라이브러리의 저작권자에게 오픈소스로 공개할 수 있도록 설득하고자 했으나 잘되지 않았고, 저작권자가 오픈소스화를 거부한 일부 컴포넌트를 제외한 나머지 JDK 소스코드 전부를 OpenJDK에 제공했고, OpenJDK는 이를 기반으로 이외의 컴포넌트들의 대안 코드를 마련하면서 JDK7 프로젝트를 시작했다.

    Oracle’s Plan for OpenJDK

    아래는 OpenJDK FAQ (http://openjdk.java.net/faq/) 의 주요 내용을 요약한 내용 이다.

    Oracle은 OpenJDK에 참여하는가?

    Oracle은 OpenJDK 프로젝트를 주도하는 주체이며, 오픈소스모델은 기술적인 발전을 위한 가장 좋은 방법이기 때문에 OpenJDK를 향상시키기 위한 노력을 계속 할 것이다.

    OpenJDK의 License 모델을 변경될수 있는가?

    OpenJDK Community는 지속적으로 Oracle에 의해 운영되며, 뿐만 아니라 이외의 기업, 연구원 또는 개인에 의해 GPL-based 라이센스를 가지며, 변경될 계획은 없다.

    OpenJDK Users & Contributors

    Ubuntu / Fedora / Red Hat Enterprise 와 같은 메이저 리눅스 제공자는 배포시에 OpenJDK를 기본 Java SE 구현체로 제공하고 있다. 추가적으로 Eclipse Community의 2010 설문에서는 개발자의 21%가 OpenJDK를 사용하고 있다고 응답하였다.

    OpenJDK 와 OracleJDK 스펙

    Oracle JDK는 OpenJDK를 기본으로 하는가?

    그렇다. Oracle JDK는 OpenJDK의 JDK7 기반에 추가로 OpenJDK에 포함되지 않은 Component까지 모두 갖춘 프로젝트이다.

    아래와 같이 Vendor에 의한 분리된 Version이 존재하는데,

    • Oracle’s JDK (Commertial support from oracle)
    • OpenJDK, the open source java

    JDK7 이전에는 두 Version간 큰 차이가 존재해 OpenJDK는 Oracle JDK에 비해 누락된 기능 및 성능이슈가 존재 했으나 현재는 java-web-plugin(http://en.wikipedia.org/wiki/IcedTea - 저작권이 있는 라이브러리의 대안으로 작성된)을 제외하고는 정확하게 같다고 볼 수 있다. 몇몇 사람들은 아직도 OpenJDK가 Oracle JDK에 비해 성능이 떨어진다고 하지만, 이것은 근거없는 말이다.

    • 두 Version은 모두 Java SE 7 JSR(JSR 336) 스펙을 동일하게 구현하였다.

    Dodgy Version History


    Open JDK7를 사용 한다면 Oracle JDK7와 동일하게 안전하다고 볼 수 있지만, 그에 비해 OpenJDK6은 안정적이지 않는 History가 있는데, 진행 중인 프로젝트인 OpenJDK7를 기초로 JDK7 스펙을 제거하는 방식으로 JDK6과 Compatible 하도록 진행 됐기 때문이다.

    Open JDK6에서는 파일 처리와 같은 기본적인 OS-Integration 관련 기능과 네트워크 처리 및 Swing 에서 몇몇 문제점이 Report 되고 있다고 한다. Open JDK를 사용시에는 꼭 Open JDK7를 사용하도록 한다.

    JDK6을 사용하고 있을 때, OpenJDK로 이관한다면 JDK6 -> JDK7로의 변경시의 이슈가 더욱 중요해 보인다.

    All new JDK7 features - http://openjdk.java.net/projects/jdk7/features/

    JVM

    OpenJDK 프로젝트는 아래와 같은 몇몇 Component로 구성되어 있는데,

    • HotSpot VM
    • The Java Class Library
    • Java Compiler

    VM 역시 Oracle에 의해 제공되는 HotSpot VM Spec과 동일하다.

    OpenJDK는 오픈소스이기 때문에 RedHat과 같은 Vendors에 의해서 Customized 되어 배포된다면 VM에 차이가 있을 수 있다. 하지만 물론 Vendor’s VM은 배포시에 Java Trademark를 사용하기를 원한다면 Java TCK에 일치하는 것을 증명해야 한다.
    http://openjdk.java.net/groups/conformance/JckAccess/jck-access.html

    Source 빌드 및 Binary 배포 이슈

    OpenJDK는 소스코드만 배포하고 있어, 직접 빌드해야하나라는 의문이 생길수 있지만 아래와 같이 리눅스에서 rpm 패키지로 다운로드 및 설치가 가능하다.

    Debian, Ubuntu

    $ sudo apt-get install openjdk-7-jre

    Fedora, Oracle Linux, Red Hat Enterprise Linux

    su -c "yum install java-1.7.0-openjdk"

    OpenJDK Quality Metrics

    OpenJDK의 사용 범위

    OpenJDK를 통해 JVM 기반의 오픈 소스를 이용하여 서비스 및 플랫폼 운영시에 성능 이슈가 있는지 확인한다.


    JVM 기반의 오픈소스 플랫폼 및 Spring과 같은 Java 기반의 오픈소스

    JVM기반의 오픈소스 사용시에는 Requirements 스펙에 JDK Version이 명시되어 있는지 확인한다.

    • Apache Kafka / Netty / MongoDB / Cassandra / Etc
    • https://github.com/apache/cassandra 페이지를 예를 들면 Requirements 항목에 Java >= 1.7 (OpenJDK and Oracle JVMS have been tested) 라고 명시되어 있는 것을 볼 수 있다.

    CI / Build

    • Java 기반의 Ant/Maven/Gradle 과 같은 빌드 도구를 사용하는 빌드서버는 OpenJDK를 이용한다.

    WAS / Application

    • Tomcat 및 Application 레벨에서 OpenJDK를 이용한다.

    OpenJDK 설치

    JDK Download

    JDK6 : http://download.java.net/openjdk/jdk6
    JDK7 : http://download.java.net/openjdk/jdk7
    JDK8 : http://download.java.net/openjdk/jdk8

    JDK 설치 및 Tomcat 설치 Script

    su -c "yum install java-1.7.0-openjdk"

    결론 & OpenJDK 이관시 Risk

    위의 내용을 바탕으로 OpenJDK 는 Java 플랫폼의 Next Version인 JDK7의 근간이 되는 프로젝트로서, JDK7를 기준으로 Oracle 에서 Binary로 배포되는 JDK와 OpenJDK는 차이가 없다고 봐도 무방하다.
    하지만 실제 서비스에 적용하기 위해서는 사내에 OpenJDK 적용 사례가 있는지 확인 할 필요가 있어보이고, 서비스에 직접적으로 영향을 미치지 않는 관리자도구 등에 시범적으로 적용하여 레퍼런스를 쌓아가는 것도 바람직해보인다.

    만약 JDK6 기반에서 운영하는 서비스가 있다면 JDK7으로의 변경으로 인한 이슈를 살펴 볼 필요가 있다.
    추가적으로 어플리케이션/빌드 및 배포/플랫폼 전반에 JVM기반의 오픈소스를 사용시에는 OpenJDK의 지원여부 및 성능 및 안정성에 대한 이슈를 자세히 체크 해야 한다.

    참고

    References

    http://openjdk.java.net/
    http://openjdk.java.net/projects/jdk6/
    http://openjdk.java.net/projects/jdk7/
    http://openjdk.java.net/projects/jdk8/
    http://helloworld.naver.com/helloworld/1219
    http://www.slideshare.net/PrincipledTechnologies/comparing-java-performance-red-hat-enterprise-linux-6-and-openjdk-vs-microsoft-windows-server-2012-and-oracle-java-hotspot

    FAQ

    http://stackoverflow.com/questions/22358071/differences-between-oracle-jdk-and-open-jdk-and-garbage-collection
    http://stackoverflow.com/questions/11547458/what-is-the-difference-between-jvm-jdk-jre-openjdk
    https://blogs.oracle.com/henrik/entry/moving_to_openjdk_as_the
    https://blogs.oracle.com/henrik/entry/java_7_questions_answers
    https://blogs.oracle.com/jtc/entry/comparing_jvms_on_arm_linux
    https://blogs.oracle.com/jtc/entry/comparing_arm_linux_jvms_revisited
    http://superuser.com/questions/593954/performance-oraclejdk-or-openjdk
    http://www.reddit.com/r/Clojure/comments/1v9a86/openjdk_vs_oracle_jdk/
    http://www.coderanch.com/t/611388/java/java/OpenJDK-OracleJDK-Performance
    http://askubuntu.com/questions/437752/openjdk-oracle-is-better




    출처 - http://www.holaxprogramming.com/2014/09/24/java-open-jdk/








    Posted by linuxism
    ,