자바 17 (3) - 자바 언어, 첫 발 내딛기!!!
자바의 기본 구성 요소 시작하기
이 장에서는 자바 프로그래밍 언어의 필수 구성 요소와 용어에 대해 자세히 다룹니다. 비록 이 장이 입문 장으로 여겨질 수 있지만, 그 중요성을 과소평가할 수 없습니다. 이전 장에서는 자바 코드를 작성하기 위한 완전히 구성된 개발 환경을 준비했습니다. 이제 그것을 활용할 때입니다. 이 장에서 다루는 주제는 다음과 같습니다:
- 핵심 구문 요소
- JShell 사용하기
- 자바 기본: 패키지, 모듈 및 클래스
- IntelliJ IDEA로 자바 프로젝트 생성하기
- 자바 코드 컴파일 및 실행
- 자바 애플리케이션을 실행 가능한 JAR로 패키징하기
- Maven 사용하기
핵심 구문 요소
자바 코드를 작성하는 것은 간단하지만, 시작하기 전에 몇 가지 기본 구문 규칙을 이해하는 것이 중요합니다. 다음은 Listing 3-1에 나와 있는 이 책의 예제 코드를 살펴보겠습니다:
package com.apress.ch.one.hw;
import java.util.List;
public class Example01 {
public static void main(String[] args) {
List<String> items = List.of("1", "a", "2", "a", "3", "a");
items.forEach(item -> {
if (item.equals("a")) {
System.out.println("A");
} else {
System.out.println("Not A");
}
});
}
}
Listing 3-1: 스마트 초보자를 위한 자바 초보자 코드 샘플
다음 설명은 같거나 유사한 기능의 라인 또는 코드 그룹을 명확히 합니다:
- ; (세미콜론): 문장 또는 선언의 끝을 표시합니다.
- package com.apress.ch.one.hw;: 패키지를 선언합니다. 이를 클래스가 위치한 장소라고 생각하세요.
- import java.util.List;: 자바 표준 라이브러리의 List 클래스를 사용하고 있음을 나타내는 import 문입니다. 자바는 이러한 클래스를 패키지로 정리하고 있으며, 다른 패키지에 동일한 이름의 클래스가 있을 수 있으므로 사용할 클래스를 명시해야 합니다.
- public class Example01: 클래스 선언입니다. 접근 지정자(public), 타입(class), 클래스 이름(Example01)으로 구성됩니다. 클래스 본문은 중괄호 {}로 둘러싸여 있습니다.
- { ... } (중괄호): 클래스, 메서드 또는 기타 논리적 그룹화와 같은 코드 블록으로 문장을 그룹화할 때 사용됩니다. 블록은 세미콜론으로 끝나지 않습니다.
- public static void main(String[] args): 자바 애플리케이션의 시작점인 main 메서드를 선언합니다. 접근 지정자(public), 키워드(static), 메서드 이름(main), 매개변수(String[] args)를 포함합니다.
- List<String> items = List.of("1", "a", "2", "a", "3", "a");: String 타입의 리스트를 선언하고 초기화합니다. 리스트는 "1", "a", "2", "a", "3", "a" 요소로 채워집니다.
- items.forEach(...): items 리스트의 각 항목에 대해 람다 표현식에서 정의된 작업을 수행하며 반복합니다.
- item -> { ... }: 리스트의 각 항목에 대해 실행할 코드 블록을 지정하는 람다 표현식입니다.
- if (<조건>) { ... } else { ... }: 조건에 따라 서로 다른 코드 블록을 실행하는 조건문입니다.
- System.out.println(<텍스트>);: 지정된 텍스트를 콘솔에 출력합니다.
자바 학습의 이 단계에서는 이러한 개념을 깊이 파고들 필요는 없지만, 패키지 선언 및 import 문을 제외한 모든 코드는 반드시 블록 내에 있어야 한다는 점이 중요합니다. 단일 라인의 문은 세미콜론 " ; "으로 끝나야 제대로 컴파일됩니다.
더 복잡한 자바 클래스를 작성하기 전에 간단한 자바 문을 작성하여 구문에 익숙해지세요. 자바 9부터 도입된 JShell 덕분에 전체 클래스를 생성하여 컴파일하고 바이트코드를 실행하지 않고도 자바 코드를 신속히 실행할 수 있습니다. JShell은 자바 코드를 쉽게 학습하고 프로토타이핑할 수 있는 강력한 대화형 도구입니다.
JShell 활용하기
JShell은 비교적 늦게 등장한 편입니다. Python과 Node 같은 스크립트 언어들은 수년 전에 유사한 도구를 소개했으며, JVM 언어들인 Scala, Clojure, 그리고 Groovy가 그 뒤를 이었습니다. 하지만 늦게라도 도입된 것이 좋은 일입니다.
JShell은 Read-Eval-Print Loop (REPL)로, 선언문, 문장, 표현식을 입력 즉시 평가하고 결과를 곧바로 보여줍니다. 이를 통해 전체 개발 환경이나 코드가 실행될 전체 컨텍스트 없이도 새로운 아이디어와 기법을 신속하게 시험해 볼 수 있습니다.
JShell은 JDK의 표준 구성 요소로, 실행 파일은 JDK 설치 디렉토리의 bin 디렉토리에 있습니다. 설치가 완료되면 터미널(Windows에서는 명령 프롬프트)을 열고 jshell을 입력하기만 하면 됩니다. 시스템 경로에 bin 디렉토리의 내용이 추가되어 있다면, JDK 버전이 포함된 환영 메시지를 보고 jshell을 사용 중이라는 것을 알리기 위해 터미널의 시작 부분이 jshell>로 바뀌는 것을 확인할 수 있습니다.
Listing 3-2에서는 jshell -v를 호출해 JShell을 상세 모드로 시작했습니다. 이 모드는 세션이 끝날 때까지 실행되는 모든 문에 대해 자세한 피드백을 제공합니다.
$ jshell -v
| Welcome to JShell -- Version 17-ea
| For an introduction type: /help intro
jshell>
Listing 3-2: jshell -v 명령의 출력
책을 읽으며 명령을 실행해 보고 계신다면, /help를 입력하여 사용할 수 있는 모든 작업과 명령의 목록을 볼 수 있습니다. 그렇지 않다면, Listing 3-3에서 예상 출력을 확인하세요.
jshell> /help
| Type a Java language expression, statement, or declaration.
| Or type one of the following commands:
| /list [<name or id>|-all|-start]
| list the source you have typed
| /edit <name or id>
| edit a source entry
| /drop <name or id>
| delete a source entry
| /save [-all|-history|-start] <file>
| Save snippet source to a file
...
| /exit [<integer-expression-snippet>]
| exit the jshell tool
...
Listing 3-3: jshell에서 /help 명령의 출력
Java에서는 변수라는 이름의 문자 그룹에 값을 할당합니다. (이들을 고르고 사용하는 방법에 대해서는 4장에서 더 다룹니다.) JShell 사용을 시작하려면 이름이 six인 변수를 선언하고 6을 할당해 보겠습니다. 이 명령문과 jshell 로그는 Listing 3-4에 설명되어 있습니다.
jshell> int six = 6;
six ==> 6
| created variable six : int
Listing 3-4: jshell을 사용한 변수 선언
보시다시피 로그 메시지는 명령이 성공적으로 수행되어 int 타입의 six라는 변수가 생성되었다는 것을 명확히 알려줍니다. six ==> 6은 우리가 생성한 변수에 값 6이 할당되었음을 나타냅니다.
JShell 세션이 닫히지 않는 한 원하는 만큼 변수를 생성하고 수학 연산, 문자열 결합 등 빠르게 실행할 수 있는 모든 것을 수행할 수 있습니다. Listing 3-5에서는 JShell로 실행한 여러 명령문과 그 결과를 보여줍니다.
jshell> int six = 6
six ==> 6
| modified variable six : int
| update overwrote variable six : int
jshell> six = six + 1
six ==> 7
| assigned to six : int
jshell> six + 1
$14 ==> 8
| created scratch variable $14 : int
jshell> System.out.println("Current val: " + six)
Current val: 7
Listing 3-5: jshell에서 다양한 명령문과 출력
앞서 설명된 코드에서 $14 ==> 8은 $14라는 이름의 변수에 값 8이 할당되었음을 보여줍니다. 이 변수는 jshell에서 생성된 것입니다. 명령문의 결과가 개발자가 명명한 변수에 할당되지 않으면, jshell은 임시 변수를 생성하고 그 이름은 $(달러) 기호와 변수를 나타내는 내부 인덱스 번호로 구성됩니다. 공식 문서에 명시되어 있지는 않지만, jshell을 사용하면서 관찰한 결과, 인덱스 값은 그 변수를 생성하게 된 명령문의 번호인 것으로 보입니다.
자바 코드의 가장 중요한 구성 요소 중 하나는 클래스입니다. 클래스는 현실 세계의 객체와 사건을 모델링하는 코드 조각입니다. 클래스는 상태를 모델링하는 클래스 변수(필드 또는 속성으로도 불림)와 행동을 모델링하는 메서드라는 두 종류의 멤버를 포함합니다.
JDK는 대부분의 애플리케이션을 만드는데 필요한 기본 구성 요소를 모델링하는 여러 클래스를 제공합니다. 클래스에 대한 더 자세한 내용은 다음 장에서 다룹니다. 현재 일부 개념이 낯설게 느껴지더라도, 인내심을 갖고 계속 학습하다 보면 점차 이해할 수 있게 될 것입니다.
JDK의 가장 중요한 클래스 중 하나는 java.lang.String으로, 텍스트 객체를 표현하는 데 사용됩니다. 이 클래스는 String 변수의 값을 조작하는 풍부한 메서드 집합을 제공합니다. Listing 3-6은 String 타입으로 선언된 변수에서 호출되는 몇 가지 메서드를 보여줍니다.
jshell> String lyric = "twice as much ain't twice as good"
lyric ==> "twice as much ain't twice as good"
| created variable lyric : String
jshell> lyric.toUpperCase()
$18 ==> "TWICE AS MUCH AIN'T TWICE AS GOOD"
| created scratch variable $18 : String
jshell> lyric.length()
$20 ==> 33
| created scratch variable $20 : int
Listing 3-6: String 변수에 대해 jshell에서 호출한 메서드 예시
JDK 타입의 변수를 사용하여 JShell에서 Java 코드를 작성하는 작업이 복잡해 보일 수 있지만, jshell은 메서드가 존재하지 않는 경우 이를 알려주므로 유용합니다. 메서드를 호출할 때 키를 누르면 사용 가능한 메서드 목록이 표시됩니다. 이는 코드 자동 완성 기능이라고 하며, 스마트 Java 편집기에서도 제공합니다.
Listing 3-7은 존재하지 않는 메서드를 호출하려고 할 때 jshell에서 출력되는 오류 메시지와 특정 타입에 대해 사용 가능한 메서드를 표시하고 걸러내는 방법을 보여줍니다.
jshell> lyric.toupper()
| Error:
| cannot find symbol
| symbol: method toupper()
| lyric.toupper()
| ^-----------^
jshell> lyric.to # <Tab>
toCharArray() toLowerCase( toString() toUpperCase(
jshell> lyric. # <Tab>
charAt( chars() codePointAt(
codePointBefore( codePointCount( codePoints()
...
Listing 3-7: String 변수에 대해 jshell에서 호출한 추가 메서드 예시
jshell은 우리가 String 클래스에 대해 알려져 있지 않은 toupper() 메서드를 사용했다는 것을 분명히 지적합니다.
가능한 메서드를 나열할 때 (로 끝나는 메서드는 인수가 필요하지 않으며, (로 끝나는 메서드는 인수가 없거나 여러 형식을 가질 수 있습니다. 이러한 형식을 보려면 변수에 메서드를 작성하고 다시 키를 누릅니다. Listing 3-8은 indexOf 메서드의 여러 형식을 보여줍니다.
jshell> lyric.indexOf( # <Tab>
$1 $14 $18 $19 $2 $20 $5 $9 lyric six
Signatures:
int String.indexOf(int ch)
int String.indexOf(int ch, int fromIndex)
int String.indexOf(String str)
int String.indexOf(String str, int fromIndex)
<press tab again to see documentation>
Listing 3-8: String 클래스의 indexOf 메서드의 모든 형식 나열
lyric.indexOf( 줄 바로 다음에 jshell은 세션 중에 생성된 변수를 나열하며, 기존 인수를 쉽게 선택할 수 있도록 도와줍니다.
Java 프로젝트에서 작성할 수 있는 것은 jshell에서도 마찬가지로 작성할 수 있습니다. 장점은 프로그램을 여러 문으로 분할하여 즉시 실행해 결과를 확인하고 필요에 따라 조정할 수 있다는 점입니다. jshell이 제공하는 또 다른 기능들도 있으며, 이 책에서 가장 중요한 부분들을 다룹니다.
jshell 세션에서 선언한 모든 변수는 /vars 명령을 실행하여 나열할 수 있습니다. Listing 3-9는 이 장의 세션에서 선언된 변수를 보여줍니다.
jshell> /vars
| int $1 = 5
| int $2 = 42
| int $5 = 8
| int $9 = 8
| int six = 7
| int $14 = 8
| String lyric = "twice as much ain't twice as good"
| String $18 = "TWICE AS MUCH AIN'T TWICE AS GOOD"
| int $19 = 9
| int $20 = 33
Listing 3-9: 작은 코딩 세션의 jshell> /vars 출력 예시
JShell 세션에서의 모든 입력을 저장하려면 /save {filename}.java 명령을 실행하면 됩니다.
모든 문장이 유효한 Java 문장이라면, 생성된 파일의 문장들은 /open {filename}.java 명령으로 새로운 jshell 세션에서 실행될 수 있습니다.
모든 명령과 기능을 시도해 보고 싶다면, Oracle 공식 사이트에서 제공하는 JShell 사용자 가이드를 참조할 수 있습니다.
The Oracle JShell user guide can be found at Oracle, “Java Shell User’s Guide,” https://docs.oracle.com/javase/9/jshell/JSHEL.pdf, accessed October 15, 2021.
자바 기본 구성 블록
이는 자바 플랫폼에 대한 일관된 소개입니다. 코드를 자신 있게 작성하려면, 내부에서 어떤 일이 일어나는지, 구성 요소가 무엇인지, 그것들을 어떤 순서로 설정하고 작성해야 하는지에 대한 이해가 필요합니다. 물론 다음 섹션을 완전히 건너뛰어도 괜찮지만, 어떤 초보 운전자들은 운전대를 잡기 전에 엔진이 어떻게 작동하는지에 대해 약간의 지식이 필요하듯이, 프로그래밍할 때도 메커니즘을 조금 이해하면 더 자신감 있고 통제되는 느낌을 받을 수 있습니다. 그래서 이 책을 읽는 모든 독자들이 제대로 시작할 수 있도록 하고 싶었습니다.
자바 애플리케이션을 작성하려면, 개발자는 자바 프로그램의 기본 구성 요소를 잘 알아야 합니다. 이를 이렇게 생각해보세요: 자동차를 만들고자 한다면, 휠이 무엇인지 그리고 어디에 배치되어야 하는지를 배워야 하겠죠? 이 책에서 자바에 대해 달성하고자 하는 바가 바로 이것입니다. 모든 구성 요소와 그 목적을 설명하는 것입니다.
이 생태계의 핵심은 클래스입니다. 자바에는 다른 객체 타입도 있지만, 클래스는 애플리케이션을 구성하는 객체의 템플릿을 나타내기 때문에 가장 중요합니다. 클래스는 주로 필드와 메서드로 구성됩니다. 객체가 생성되면, 필드의 값이 객체의 상태를 정의하고, 메서드가 그 동작을 설명합니다.
자바 객체는 실제 세계의 객체를 모델링합니다. 따라서 자바에서 자동차를 모델링하려면, 자동차를 설명하는 필드를 정의해야 합니다: 제조사, 모델명, 생산연도, 색상, 속도 등이 그 예입니다. 자동차 클래스의 메서드는 자동차가 하는 일을 설명합니다. 자동차가 주로 하는 일은 가속과 제동이므로, 모든 메서드는 이 두 가지와 관련된 동작을 설명해야 합니다.
패키지
자바 코드를 작성할 때, 현실 세계의 항목들의 상태와 동작을 설명하는 코드를 작성하게 됩니다. 코드는 애플리케이션을 구축하기 위해 함께 사용되는 클래스 및 다른 타입으로 구성되어야 합니다. 모든 타입은 .java 확장자를 가진 파일에 기술됩니다. 객체 타입은 패키지로 조직화됩니다.
패키지는 타입의 논리적 집합입니다. 그중 일부는 패키지 외부에서 볼 수 있고, 일부는 그렇지 않을 수도 있는데, 이는 그들의 범위에 따라 달라집니다.
패키지가 작동하는 방식을 이해하기 위해, 다른 상자를 포함한 상자를 상상해보세요. 그 상자들은 다른 상자로 가득 차 있을 수도 있고, 상자가 아닌 어떤 항목들로 가득 차 있을 수도 있습니다. 이 예를 위해, 그 항목들이 레고 조각이라고 가정해 봅시다. 이 비유는 잘 맞아떨어지는 것이, 자바 타입도 레고 조각들이 조립되는 방식과 동일하게 구성될 수 있기 때문입니다.
패키지 이름은 고유해야 하며, 특정한 템플릿을 따라야 합니다. 이 템플릿은 보통 프로젝트를 진행하는 회사에 의해 정의됩니다. 좋은 관행에 따르면, 이름의 고유성과 의미를 보장하기 위해, 일반적으로 이름을 조직의 인터넷 도메인 이름의 역순으로 시작한 다음, 다양한 그룹화 기준을 추가합니다.
이 프로젝트에서는 패키지 이름이 여기에 묘사된 템플릿을 따릅니다: com.apress.bgn.[]+. 이 템플릿은 Apress 출판사(www.apress.com)의 역순 도메인 이름으로 시작하며, 책을 식별하는 용어가 추가됩니다(bgn은 초보자를 의미하는 약어입니다). 마지막으로, 는 소스와 (일반적으로) 일치하는 패키지 번호로 대체됩니다.
앞서 소개한 상자와 레고의 비유를 고려하면, com 패키지는 apress 상자를 포함하는 큰 상자입니다. 다른 레고들을 포함할 수도 있지만, 이 예에서 그렇지는 않습니다.
apress 상자는 com.apress 패키지를 나타내며, bgn 상자를 포함합니다.
bgn은 com.apress.bgn 패키지 상자를 나타내며, 각 챕터에 해당하는 상자들을 포함하고 있습니다. 이 상자들은 다른 상자 또는 레고(즉, 자바 파일로, 자바 코드를 포함한)를 포함할 수 있습니다. Figure 3-1은 이러한 상자와 레고, 그리고 그것들이 중첩되는 방식을 나타냅니다.
컴퓨터에서는 패키지가 디렉터리의 계층 구조입니다. 각 디렉터리는 다른 디렉터리 및/또는 자바 파일을 포함할 수 있습니다. 이것은 모두 조직력에 따라 다릅니다. 이 조직 체계는 중요합니다. 패키지 이름과 객체 타입의 이름을 사용하여 자바 객체 타입을 고유하게 식별할 수 있기 때문입니다.
만약 HelloWorld라는 클래스를 HelloWorld.java라는 파일에 작성하고 이 파일을 com.apress.bgn.one 패키지에 넣는다면, 자바 프로젝트에서 com.apress.bgn.one.HelloWorld라는 표현은 이 클래스의 고유 식별자로 작용하는 전체 클래스 명입니다. 패키지 이름을 클래스의 주소로 볼 수 있습니다.
자바 5부터 각 패키지 내에 package-info.java라는 파일을 생성할 수 있으며, 이 파일에는 패키지 선언, 패키지 주석, 패키지 주석, 그리고 Javadoc 주석을 포함할 수 있습니다. 이 주석들은 Javadoc으로 알려진 프로젝트의 개발 문서에 내보내집니다. Chapter 9에서는 Maven을 사용하여 프로젝트 Javadoc을 생성하는 방법을 다룹니다. package-info.java는 패키지의 마지막 디렉터리에 위치해야 합니다. 따라서 com.apress.bgn.one 패키지를 정의할 경우, 자바 프로젝트의 전체 구조와 내용은 그림 3-2와 같습니다.
Figure 3-2: 자바 패키지 내용
package-info.java의 내용은 다음 Listing 3-10과 유사할 수 있습니다.
/**
* Contains classes used for reading information from various sources.
* @author iuliana.cosmina
* @version 1.0-SNAPSHOT
*/
package com.apress.bgn.one;
Listing 3-10: package-info.java 내용
타입 정의를 포함하는 .java 확장자의 파일은 동일한 패키지 구조에 따라 구성되고 하나 이상의 JAR(Java Archives)로 패키징되는 .class 확장자의 파일로 컴파일됩니다. 이전 예제의 경우, 컴파일 및 링크 후 생성된 JAR을 풀면 그림 3-3에 보여진 내용을 볼 수 있습니다.
package-info.java 파일은 패키지에 대한 정보만을 담고 행동이나 타입을 포함하지 않더라도 컴파일됩니다.
package-info.java 파일은 필수는 아닙니다. 패키지들은 그것 없이도 정의될 수 있으며, 주로 문서화 목적에 유용합니다.
하나의 패키지 내용이 여러 JAR에 걸쳐 있을 수 있습니다. 즉, 프로젝트 내에 여러 하위 프로젝트가 있는 경우, 동일한 패키지 이름을 여러 곳에서 사용할 수 있으며, 이는 서로 다른 클래스를 포함할 수 있습니다. 이러한 상징적 표현은 그림 3-4에 설명되어 있습니다.
Figure 3-4: 여러 JAR에 걸친 패키지 내용 예시
라이브러리는 특정 기능을 구현하는 데 사용되는 클래스들을 포함한 JAR 모음입니다. 예를 들어, JUnit은 자바 단위 테스트를 작성하는 데 편리한 여러 클래스를 제공하는 매우 유명한 자바 프레임워크입니다.
적당히 복잡한 자바 애플리케이션은 하나 이상의 라이브러리를 참조합니다. 애플리케이션을 실행하려면 모든 종속성(모든 JAR)이 클래스패스에 있어야 합니다. 이는 무슨 의미일까요? 자바 애플리케이션을 실행하려면 JDK, 종속성(외부 JAR) 및 애플리케이션 JAR이 필요하다는 것을 의미합니다. 그림 3-5는 이를 명확히 보여줍니다.
Figure 3-5: 애플리케이션 클래스패스
여기서는 애플리케이션이 작성된 환경과 동일한 환경에서 실행된다고 가정하며, JDK가 애플리케이션을 실행하는 데 사용됩니다. JDK 11까지는 JRE로 자바 애플리케이션을 실행할 수 있었습니다. 그러나 11 버전부터 자바는 완전히 모듈화되었습니다. 이는 애플리케이션을 실행하기 위해 필요한 모듈로만 구성된 맞춤형 "JRE" 배포판을 생성할 수 있음을 의미합니다. 간접적으로, 생성된 JRE에는 최소한의 수의 JDK 컴파일 클래스가 포함될 것입니다.
애플리케이션 클래스패스를 구성하는 JAR들은 (당연히) 항상 서로 독립적인 것은 아닙니다. 21년 동안 이러한 조직 스타일은 충분했지만, 복잡한 애플리케이션에서는 다음과 같은 문제로 인한 많은 합병증이 발생했습니다.
- 여러 JAR에 흩어져 있는 패키지 (그림 3-4를 기억하나요?) 이것은 코드 중복과 순환 종속성을 초래할 수 있습니다.
- JAR 간의 전이적 종속성으로 인해 동일한 클래스의 서로 다른 버전이 클래스패스에 존재할 수 있습니다. 이는 예측할 수 없는 애플리케이션 동작을 초래할 수 있습니다.
- 누락된 전이적 종속성과 접근성 문제. 이는 애플리케이션 충돌을 초래할 수 있습니다.
이러한 모든 문제는 하나의 이름, 즉 "Jar Hell"로 통칭됩니다. 이 문제는 자바 9에서 패키지를 구성할 또 다른 수준인 모듈을 도입하여 해결되었습니다. 최소한 그것이 의도였습니다. 그러나 산업계는 자바 모듈 채택에 주저하고 있습니다. 이 장이 작성될 당시, 대부분의 자바 생산 애플리케이션은 여전히 자바 8에 묶여 있으며, 개발자들은 전염병을 피하듯 모듈을 피하고 있습니다.
그러나 모듈을 소개하기 전에 접근 제어자에 대해 언급해야 합니다. 자바 타입과 그 멤버들은 패키지 내에서 특정 접근 권한으로 선언되며, 이는 코딩에 들어가기 전에 이해해야 할 꽤 중요한 부분입니다.
접근 제어자
자바에서 타입을 선언할 때—여기서는 지금까지 언급된 유일한 유형이므로 클래스로 한정하겠습니다—그 범위를 접근 제어자를 사용하여 구성할 수 있습니다.
접근 제어자는 클래스에 대한 접근을 지정하는 데 사용할 수 있으며, 이런 경우 최상위 수준에서 사용된다고 합니다.
또한 클래스 멤버에 대한 접근을 지정하는 데에도 사용할 수 있으며, 이런 경우 멤버 수준에서 사용된다고 합니다.
최상위 수준에서는 두 가지 접근 제어자만 사용할 수 있습니다: public과 아무것도 명시하지 않은 상태입니다.
public으로 선언된 최상위 클래스는 동일한 이름을 가진 자바 파일에 정의되어야 합니다.
Listing 3-11은 com.apress.bgn.zero 패키지에 있는 Base.java 파일에 정의된 Base라는 이름의 클래스를 보여줍니다.
package com.apress.bgn.zero;
// 최상위 접근 제어자
public class Base {
// 코드 생략
}
Listing 3-11: Base 클래스
클래스의 내용은 집중력을 방해하지 않기 위해 생략되었습니다. public 클래스는 애플리케이션의 어디에서나 모든 클래스에서 볼 수 있습니다. 다른 패키지의 다른 클래스도 이 타입의 객체를 생성할 수 있으며, 이는 Listing 3-12에 나와 있습니다.
package com.apress.bgn.three;
import com.apress.bgn.zero.Base;
public class Main {
public static void main(String... args) {
// Base 타입의 객체 생성
Base base = new Base();
}
}
Listing 3-12: Base 클래스를 사용하여 객체 생성하기
Base base = new Base(); 이 줄에서 객체가 생성됩니다. new 키워드는 클래스의 인스턴스화를 나타내며, 이는 Base 클래스를 나타내는 코드에 의해 설명된 사양을 기반으로 객체가 생성됨을 의미합니다.
클래스는 템플릿입니다. 객체는 이 템플릿을 사용하여 생성되며, 이를 인스턴스라고 합니다.
일단은 이 사실을 이해하십시오: public 클래스는 어디서나 모든 클래스에서 볼 수 있습니다.
명시적인 접근 제어자가 언급되지 않으면, 그 클래스는 기본(default) 또는 패키지 전용(package-private)이라고 선언된 것으로 간주됩니다. 접근 제어자가 없는 경우에 대한 두 가지 설명 방식이 있다는 것이 혼란스럽게 느껴질 수 있지만, 다른 책이나 블로그에서 이 상황을 언급할 수 있으므로 여기서 모든 가능성을 정리해 두는 것이 좋습니다.
접근 제어자가 없는 클래스는 동일한 패키지에 정의된 클래스들에 의해서만 객체를 생성할 수 있습니다. 즉, 그 범위는 정의된 패키지 내로 제한됩니다. 접근 제어자가 없는 클래스는 동일한 이름의 Java 파일, 또는 파일에 이름을 부여하는 클래스 옆에 정의될 수 있습니다.
여러 클래스가 동일한 파일에 선언된 경우, public 클래스는 그 파일 내에 정의된 것과 같은 이름을 가져야 하며, 따라서 이 클래스가 파일에 이름을 부여합니다.
이 테스트를 위해, 이전에 소개한 Base.java 파일에 HiddenBase라는 클래스를 추가해 보겠습니다. 이는 Listing 3-13에 나와 있습니다.
package com.apress.bgn.zero;
public class Base {
// 코드 생략
}
class HiddenBase {
// 당신은 나를 볼 수 없어요
}
Listing 3-13: 접근 제어자가 없는 클래스
Base 클래스는 com.apress.bgn.zero 패키지에 선언되어 있습니다. 만약 com.apress.bgn.three 패키지 내부에 선언된 클래스에서 HiddenBase 타입의 객체를 생성하려고 하면, IDE는 경고를 표시하며 텍스트를 읽기 전용으로 만들고 코드 자동 완성을 제공하지 않을 것입니다. 더 나아가, 현재 파일의 문제를 나열하는 탭이 열리며 명백한 오류 메시지가 표시됩니다. 이는 그림 3-6에 나와 있습니다.
Figure 3-6: 접근자 수정자가 없는 자바 클래스 오류
여기에서 잠시 멈춰, 이 사실을 기억하십시오: 접근 제어자가 없는 클래스는 동일한 패키지 내의 모든 클래스(및 다른 타입)에서 볼 수 있습니다.
클래스 내부에는 클래스 멤버가 정의됩니다: 필드와 메서드입니다. 멤버 수준에서는 앞서 언급한 두 가지 외에도 더 두 가지 수정자가 적용될 수 있습니다: private과 protected. 멤버 수준의 접근 제어자는 다음과 같은 영향을 미칩니다:
- public: 최상위 수준과 동일하며, 멤버는 어디서든 접근할 수 있습니다.
- private: 멤버는 선언된 클래스 내에서만 접근할 수 있습니다.
- protected: 멤버는 이를 포함하는 클래스가 선언된 패키지 내에서, 또는 다른 패키지에 있는 해당 클래스의 하위 클래스에서만 접근할 수 있습니다.
- none(기본/패키지 전용): 멤버는 자신의 패키지 내에서만 접근할 수 있습니다.
복잡해 보일 수 있지만, 코드를 작성하기 시작하면 익숙해질 것입니다. 공식 Oracle 문서 페이지에도 멤버의 가시성을 보여주는 표가 있으며, 이는 표 3-1에 나와 있습니다.
표 3-1: 멤버 수준 접근자 범위
수정자클래스패키지하위 클래스전 세계
public | 예 | 예 | 예 | 예 |
protected | 예 | 예 | 예 | 아니요 |
none(기본/패키지 전용) | 예 | 예 | 아니요 | 아니요 |
private | 예 | 아니요 | 아니요 | 아니요 |
코드에서 해당 표가 어떻게 적용되는지를 전반적으로 이해하기 위해, Listing 3-14의 클래스가 아주 유용합니다.
package com.apress.bgn.three.same;
public class PropProvider {
public int publicProp;
protected int protectedProp;
/* default */ int defaultProp;
private int privateProp;
public PropProvider() {
privateProp = 0;
}
}
Listing 3-14: 다양한 접근자로 장식된 멤버를 가진 자바 클래스 PropProvider
PropProvider 클래스는 네 가지 필드/속성을 선언하며, 각각 다른 접근 제어자를 가집니다.
privateProp 필드는 이 클래스 내부에서만 수정될 수 있습니다. 이는 이 클래스의 다른 모든 멤버가 이 속성의 값을 읽고 변경할 수 있음을 의미합니다.
현재까지의 내용에서는 메서드만 다른 유형의 멤버로 언급되었습니다.
그러나 클래스는 다른 클래스의 본문 내에 선언될 수 있습니다. 이러한 클래스를 중첩 클래스(nested class)라고 하며, 중첩 클래스는 감싸고 있는 클래스의 모든 멤버(비공식 멤버 포함)에 접근할 수 있습니다. 그림 3-7은 printPrivate이라는 추가 메서드가 있는 수정된 PropProvider 클래스를 보여줍니다. 이 메서드는 private 필드의 값을 읽어 출력합니다.
또한 LocalPropRequester라는 중첩 클래스도 선언되어 있으며 이 클래스에서 private 필드가 수정되는 모습이 나타납니다(라인 56).
Figure 3-7은 IntelliJ IDEA에서 자바 코드가 어떻게 표시되는지를 보여주는 스크린샷입니다. 접근할 수 없는 필드는 빨간색으로 표시됩니다.
표 3-1의 두 번째 열, 패키지(Package) 열은 PropProvider 클래스와 동일한 패키지에 선언된 클래스가 접근할 수 있는 필드를 다룹니다. Figure 3-8은 PropProvider 클래스의 모든 필드를 수정하려고 시도하는 PropRequester라는 클래스의 예를 보여줍니다. 여기서 private 필드가 밝은 빨간색으로 표시되어 있는 것을 주목하세요. 이는 해당 필드에 접근할 수 없음을 나타내며, IntelliJ IDEA가 이를 명확히 하고 있습니다.
Figure 3-8: 자바 코드에서의 패키지 접근자
표 3-1의 세 번째 열, 하위 클래스(Subclass) 열은 PropProvider 클래스의 하위 클래스가 접근할 수 있는 필드를 다룹니다. 하위 클래스는 그 기반이 되는 클래스(슈퍼클래스)로부터 상태와 행동을 상속받습니다. 하위 클래스는 extends 키워드와 함께 슈퍼클래스의 이름을 사용하여 생성됩니다. Figure 3-9는 PropProvider로부터 상속받은 모든 필드를 수정하려고 시도하는 SubClassedProvider라는 클래스의 예를 보여줍니다. 이 클래스에서도 private 필드와 접근 제어자가 없는 필드가 밝은 빨간색으로 표시되어 있습니다. 이는 해당 필드에 접근할 수 없음을 의미하며, IntelliJ IDEA가 이를 명확히 하고 있습니다.
Figure 3-9: 자바 코드에서의 하위 클래스 접근자
접근 제어자가 없는 필드는 이전 예제에서 접근할 수 없었던 이유는 하위 클래스가 다른 패키지에 있었기 때문입니다. 만약 하위 클래스를 같은 패키지로 이동시킨다면, 표 3-1의 '패키지' 열에 명시된 규칙이 적용되게 됩니다.
표 3-1의 네 번째 열, 전 세계(World) 열은 PropProvider 클래스가 선언된 패키지 밖의 모든 클래스에 적용되며, 이 클래스의 하위 클래스가 아닙니다. Figure 3-10은 PropProvider에 선언된 모든 필드에 접근하려고 시도하는 AnotherPropRequester라는 클래스의 예를 보여줍니다. 예상대로 public 필드만 접근 가능하고 나머지는 빨간색으로 표시됩니다.
Figure 3-10: 자바 코드에서의 전 세계 접근자
스마트 편집기인 IntelliJ IDEA 외부에서 프로젝트를 빌드하려고 할 경우, 이러한 방법은 작동하지 않을 것입니다. 오류 메시지는 컴파일 오류의 원인과 위치를 알려줍니다. 예를 들어, AnotherPropRequester 클래스가 포함된 chapter03 하위 프로젝트를 Maven 빌드 도구를 사용하여 빌드하려고 하면 실패합니다. 터미널에서 다음과 같은 오류 메시지가 표시됩니다:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project chapter03: Compilation failure: Compilation failure:
[ERROR] /Users/iulianacosmina/apress/workspace/java-17-for-absolute-beginners/chapter03/src/main/java/com/apress/bgn/three/other/AnotherPropRequester.java:[42,17] protectedProp has protected access in com.apress.bgn.three.same.PropProvider
[ERROR] /Users/iulianacosmina/apress/workspace/java-17-for-absolute-beginners/chapter03/src/main/java/com/apress/bgn/three/other/AnotherPropRequester.java:[44,17] defaultProp is not public in com.apress.bgn.three.same.PropProvider; cannot be accessed from outside package
[ERROR] /Users/iulianacosmina/apress/workspace/java-17-for-absolute-beginners/chapter03/src/main/java/com/apress/bgn/three/other/AnotherPropRequester.java:[46,17] privateProp has private access in com.apress.bgn.three.same.PropProvider
빌드 도구와 편집기는 자바 코드에 문제가 있을 때 이를 알려주는 데 능숙합니다. 이를 잘 활용하고 신뢰하면 생산성을 높일 수 있습니다. 물론 작은 문제는 있을 수 있지만, 그리 많지는 않습니다.
자바 코드를 작성하기 시작하면 아마도 표 3-1에 한두 번 다시 돌아올 가능성이 높습니다. 앞서 언급한 모든 내용은 모듈이 도입된 후에도 여전히 유효합니다(단, 모듈 접근을 적절히 구성하는 경우에 한합니다).
자바 모듈
Java 9부터 새로운 개념인 모듈(Module)이 도입되었습니다. Java 모듈은 패키지를 조직하고 집합적으로 관리하는 보다 강력한 메커니즘을 나타냅니다. 이 새로운 개념의 구현은 10년 이상 걸렸습니다. 모듈에 대한 논의는 2005년에 시작되었으며, Java 7에서 구현될 것으로 기대되었습니다. Project Jigsaw라는 이름 아래, 2008년에 탐색 단계가 시작되었습니다. 자바 개발자들은 Java 8에서 모듈화된 JDK를 사용할 수 있을 것이라 기대했지만, 그것은 실현되지 않았습니다.
모듈은 3년간의 작업(그리고 거의 7년간의 분석) 끝에 마침내 Java 9에 도입되었습니다. 이를 지원하기 위해 Java 9의 공식 출시일이 2017년 9월로 연기되었습니다.
Java 모듈은 패키지를 그룹화하고 패키지 내용을 더 세분화된 접근 권한으로 구성하는 방법입니다. Java 모듈은 독특한 이름과 재사용 가능한 패키지 및 리소스의 그룹으로, 소스 디렉토리의 루트에 위치한 module-info.java라는 파일에 의해 설명됩니다. 이 파일에는 다음과 같은 정보가 포함됩니다:
- 모듈의 이름
- 모듈의 종속성(즉, 이 모듈이 의존하는 다른 모듈)
- 다른 모듈에 명시적으로 제공되는 패키지(모듈 내의 다른 모든 패키지는 기본적으로 다른 모듈에서 접근할 수 없음)
- 제공하는 서비스
- 소비하는 서비스
- 리플렉션을 허용하는 다른 모듈
- 네이티브 코드
- 리소스
- 구성 데이터
이론상으로 모듈 이름은 패키지 이름과 유사하며, 역 도메인 이름 규칙을 따릅니다. 실제로는 모듈 이름에 숫자가 포함되지 않도록 하고, 그 목적을 명확하게 드러내는 것이 중요합니다. module-info.java 파일은 모듈 설명자로 컴파일되며, 이는 클래스와 함께 평범한 JAR 파일로 포장된 module-info.class라는 파일입니다. 이 파일은 자바 소스 디렉토리의 루트에 있으며, 어떤 패키지에도 속하지 않습니다. 이전에 소개한 chapter03 프로젝트의 경우, module-info.java 파일은 src/main/java 디렉토리에 위치하며, com 디렉토리와 같은 수준에 있습니다.
Figure 3-11: module-info.java 파일의 위치
모든 *.java 확장자를 가진 파일과 마찬가지로 module-info.java는 *.class 파일로 컴파일됩니다. 모듈 선언은 자바 타입 선언의 일부가 아니므로, module은 자바 키워드가 아니며, 자바 타입의 변수 이름으로 그대로 사용할 수 있습니다. 패키지의 경우는 상황이 다릅니다. 자바 타입 선언은 반드시 패키지 선언으로 시작해야 합니다. Listing 3-15에 선언된 SimpleReader 클래스를 살펴보세요.
package com.apress.bgn.three;
public class SimpleReader {
private String source;
// 코드 생략
}
Listing 3-15: SimpleReader Class
패키지 선언을 볼 수 있지만, 모듈은 어디에 있습니까? 모듈은 module-info.java에 의해 설명되는 추상적인 개념입니다. 따라서 Java 9부터 애플리케이션에서 자바 모듈을 구성하는 경우, Figure 3-4는 Figure 3-12로 발전합니다.
Figure 3-12: 시각적으로 나타낸 자바 모듈
Java 모듈은 관련된 Java 패키지를 논리적으로 그룹화하는 방법입니다.
모듈의 도입은 JDK를 모듈로 나눌 수 있게 합니다. java --list-modules 명령은 로컬 JDK 설치에 있는 모든 모듈을 나열합니다. Listing 3-16은 현재 JDK 17-ea가 설치된 개인 컴퓨터에서 이 명령을 실행한 결과를 보여줍니다.
$ java --list-modules
java.base@17-ea
java.compiler@17-ea
java.datatransfer@17-ea
java.desktop@17-ea
# 출력 생략
Listing 3-16: JDK 17-ea 모듈
각 모듈 이름 뒤에는 @17-ea라는 버전 문자열이 따라옵니다. 이는 해당 모듈이 Java 버전 17-ea에 속한다는 것을 의미합니다.
따라서 자바 애플리케이션이 모든 모듈을 필요로 하지 않는 경우, 필요한 모듈만으로 구성된 런타임을 생성할 수 있으며, 이는 런타임의 크기를 줄이는 데 도움이 됩니다. 애플리케이션에 맞춤형으로 조정된 더 작은 런타임을 생성하는 도구는 jlink라고 하며, JDK 실행 파일의 일부입니다. 이는 더 높은 수준의 확장성과 성능 향상을 가능하게 합니다. jlink의 사용 방법은 이 책의 주제가 아닙니다. 이 책의 초점은 자바 프로그래밍 언어를 배우는 것이므로, 자바 플랫폼의 기술적 세부 사항은 최소한으로 유지될 것입니다. 코드를 자신 있게 작성하고 실행할 수 있도록 하는 데 충분한 정보만 포함됩니다.
모듈의 도입은 경험이 풍부한 개발자들이 수년간 기대해온 여러 가지 이점을 제공합니다. 그러나 큰 규모의 복잡한 프로젝트에 대한 모듈 구성은 결코 쉬운 일이 아니며, 대부분의 소프트웨어 회사들은 JDK 8에 머물거나 아예 모듈 구성을 피하는 경향이 있습니다.
module-info.java의 내용은 모듈의 이름과 본체를 포함하는 두 개의 괄호로 구성된 만큼 간단할 수 있습니다. 이는 Listing 3-17에 나와 있습니다.
module chapter.three {
}
Listing 3-17: 간단한 module-info.java 구성
고급 모듈 구성
자바 모듈 선언 본문에는 모듈에 포함된 패키지와 클래스의 접근 구성 및 종속성 요구 사항을 나타내는 지시문 하나 이상이 포함됩니다. 이 지시문들은 표 3-2에 있는 키워드를 사용하여 구성됩니다.
이 책의 부록에는 모든 지시문에 대한 예제가 포함되어 있습니다. 자바 언어를 배우기 위해 실제로 알아야 할 것은 requires와 exports입니다. 이번 판에서는 각 지시문을 충분히 이해할 수 있는 맥락이 마련되자마자 깊이 있게 설명하고, 실제 세계의 사건과 시나리오와의 비유를 추가하여 개념을 명확히 설명할 것입니다. 이 장에서는 두 가지 주요 지시문만 먼저 설명합니다.
모듈은 서로 의존할 수 있습니다. 이 책의 프로젝트는 13개의 모듈로 구성되어 있으며, 대부분이 chapter.zero 모듈에 의존합니다. 이 모듈은 다른 모듈에서 더 복잡한 구성 요소를 구축하는 데 사용되는 기본 구성 요소를 포함하고 있습니다. 예를 들어, chapter.three 모듈 내 클래스들은 chapter.zero 모듈 내 패키지와 클래스에 접근할 필요가 있습니다. 모듈 종속성을 선언하는 것은 requires 지시문을 사용하여 하며, 이는 Listing 3-18에 나와 있습니다.
module chapter.three {
requires chapter.zero;
}
Listing 3-18: 간단한 module-info.java 구성
앞서 언급한 종속성은 명시적인 것입니다. 그러나 암묵적인 종속성도 있습니다. 예를 들어, 개발자가 선언한 어떤 모듈이든 암묵적으로 JDK의 java.base 모듈을 요구합니다. 이 모듈에는 Java SE 플랫폼의 기본 API가 포함되어 있어서, 이 모듈 없이는 어떤 자바 애플리케이션도 작성할 수 없습니다. 이 암묵적인 지시문은 기본적인 자바 코드를 작성할 수 있도록 최소한의 자바 타입에 대한 접근을 보장합니다. Listing 3-18은 Listing 3-19와 동등합니다.
module chapter.three {
requires java.base;
requires chapter.zero;
}
Listing 3-19: requires java.base 명시적 지시문이 포함된 간단한 module-info.java 구성
모듈을 필요하다고 선언하는 것은 해당 모듈이 코드가 컴파일될 때—주로 컴파일 시간이라고 하며, 코드가 실행될 때—주로 런타임이라고 하는 때에 필요하다는 것을 의미합니다. 만약 모듈이 런타임에만 필요하다면, requires static 키워드를 사용하여 종속성을 선언합니다. 지금은 이 점만 기억해 두세요. 웹 애플리케이션에 대해 이야기할 때 명확해질 것입니다.
이제 chapter.three가 chapter.zero 모듈에 의존하게 되었습니다. 그러나 이것이 chapter.three가 chapter.zero 모듈의 모든 패키지에서 모든 public 타입(및 중첩된 public과 protected 타입)에 접근할 수 있다는 것을 의미할까요? 충분하지 않다고 생각했다면, 맞습니다. 모듈이 다른 모듈에 의존한다고 해서 실제로 필요한 패키지와 클래스에 접근할 수 있는 것은 아닙니다. 필요한 모듈은 내부를 공개하도록 구성되어야 합니다. 이것은 어떻게 할 수 있을까요? 우리의 경우, chapter.zero 모듈이 필요한 패키지에 접근할 수 있도록 해야 합니다. 이를 위해 이 모듈의 module-info.java를 사용자 정의하여 exports 지시문을 추가하고 필요한 패키지 이름을 지정합니다. Listing 3-20은 단일 패키지를 공개하는 chapter.zero 모듈의 module-info.java 파일을 보여줍니다.
module chapter.zero {
exports com.apress.bgn.zero;
}
Listing 3-20: chapter.zero 모듈의 module-info.java 구성 파일
이렇게 생각해 보세요: 당신은 방에서 크리스마스 장식을 자르고 있고, 장식을 위한 템플릿이 필요합니다. 당신의 룸메이트가 모든 템플릿을 가지고 있습니다. 하지만 필요하다고 해서 그것이 마법처럼 나타나는 것은 아닙니다. 룸메이트와 이야기해야 합니다. 룸메이트의 도움이 필요한 것은 requires room-mate 지시문으로 볼 수 있습니다. 룸메이트와 이야기한 후, 그는 아마 이렇게 말할 것입니다:
"물론이죠, 들어오세요, 책상 위에 있습니다! 필요한 만큼 가져가세요." 이는 exports all-templates-on-desk 지시문으로 간주될 수 있습니다. 책상은 아마 패키지에 대한 좋은 비유일 것입니다.
Listing 3-20의 구성을 사용함으로써 우리는 com.apress.bgn.zero 패키지에 접근할 수 있도록, requires module.zero; 지시문으로 구성된 모듈에 접근 권한을 부여했습니다. 만약 우리가 그걸 원하지 않는다면? (이전의 팁을 고려하면, 룸메이트가 그의 방의 문을 열어 두어서 누구든지 들어와서 템플릿을 가져갈 수 있습니다!)
만약 접근을 chapter.three 모듈로만 제한하고 싶다면? (그래서 룸메이트가 템플릿을 오직 당신에게만 줄 수 있도록) 이는 to 키워드를 추가하고 모듈 이름을 명시하여, 해당 모듈만이 구성 요소에 접근할 수 있도록 명확히 함으로써 이루어질 수 있습니다. 이는 표 3-2에서 언급한 exports 지시문의 한정된 버전입니다.
표 3-2: 자바 모듈 지시문
지시문목적
requires | 모듈이 다른 모듈에 의존함을 지정합니다. |
exports | 모듈의 패키지 중 공용 타입(및 중첩된 public 및 protected 타입)을 다른 모든 모듈의 코드가 접근할 수 있도록 합니다. |
exports ... to | exports 지시문의 한정된 버전입니다. 쉼표로 구분된 목록으로 지정된 모듈 또는 모듈 코드가 내보낸 패키지에 접근할 수 있도록 합니다. |
open | 모듈 수준 선언 (open module mm {})에서 사용되며, 모듈의 모든 패키지에 대한 리플렉션 접근을 허용합니다. Java 리플렉션은 클래스의 모든 기능을 런타임에 분석하고 수정하는 과정이며, private 타입과 멤버에도 작동합니다. 그러므로 Java 9 이전에는 아무것도 실질적으로 캡슐화되지 않았습니다. |
opens | 모듈 선언 본문 내에서 사용되어 특별히 구성된 패키지에 대한 리플렉션 접근을 선택적으로 허용합니다. |
opens ... to | opens 지시문의 한정된 버전입니다. 쉼표로 구분된 목록으로 지정된 모듈 또는 모듈 코드가 리플렉션을 통해 패키지에 접근할 수 있도록 합니다. |
uses | 이 모듈이 사용하는 서비스를 지정하며, 모듈을 서비스 소비자로 만듭니다. 여기서 서비스는 다른 모듈이 구현을 제공하는 인터페이스 또는 추상 클래스의 전체 이름을 나타냅니다. |
provides ... with | 모듈이 특정 구현을 제공하여 서비스를 제공함을 명시하며, 모듈을 서비스 제공자로 만듭니다. |
transitive | requires와 함께 사용되어 다른 모듈에 대한 종속성을 지정하고, 당신의 모듈을 읽는 다른 모듈이 그 종속성도 읽도록 보장합니다. 이를 암시적 가독성이라고 합니다. |
만약 권장된 Jar Hell 기사를 읽었다면, Jars로 패키지된 자바 소스를 작업할 때의 염려 중 하나가 보안이라는 것을 알게 되었을 것입니다. 이는 자바 소스에 접근 없이도, 애플리케이션의 종속성으로 jar를 추가함으로써 객체를 검사, 확장 및 인스턴스화할 수 있기 때문입니다. 신뢰할 수 있는 구성 제공, 플랫폼의 확장성 향상, 무결성 및 성능 향상 외에도 모듈 도입의 목표는 사실 더 나은 보안이었습니다.
Listing 3-21은 chapter.three 모듈에만 단일 패키지를 노출하는 chapter.zero 모듈의 module-info.java 파일을 보여줍니다.
module chapter.zero {
exports com.apress.bgn.zero to chapter.three;
}
Listing 3-21: chapter.zero 모듈의 고급 module-info.java 구성 파일
Listing 3-22와 같이 쉼표로 구분하여 원하는 모듈을 나열하여 여러 모듈에 접근 권한을 지정할 수 있습니다.
module chapter.zero {
exports com.apress.bgn.zero to chapter.two, chapter.three;
}
Listing 3-22: 여러 모듈이 포함된 chapter.zero 모듈의 고급 module-info.java 구성 파일
모듈의 순서는 중요하지 않으며, 모듈이 많을 경우 여러 줄에 걸쳐 나열할 수 있습니다. 선언을 세미콜론(;) 으로 끝내는 것을 잊지 마세요.
이 책의 이 단계에서 모듈에 대해 소개할 수 있는 것은 이것이 전부지만, 걱정하지 마세요. 다른 모든 지시문은 적절한 시기에 다룰 것입니다.
자바 프로젝트의 구조 결정 방법
자바 프로젝트의 구조는 여러 가지 방법으로 결정될 수 있으며, 이는 다음 요인에 따라 달라집니다:
- 프로젝트 범위
- 사용되는 빌드 도구
프로젝트 범위가 구조에 영향을 미치는 이유가 궁금할 수 있으며, 이와 관련하여 표준이 있어야 한다고 생각할 수 있습니다. 표준은 존재하지만, 여러 개가 있으며 이는 프로젝트 범위에 따라 달라집니다. 자바 프로젝트를 만드는 이유가 그 크기에 영향을 줍니다. 프로젝트가 작으면 소스를 하위 프로젝트로 나누지 않아도 될 수 있으며, 빌드 도구도 필요하지 않을 수 있습니다. 빌드 도구는 프로젝트를 조직하는 자체적인 표준 방식을 제공합니다. 이제 가장 작은 자바 프로젝트인 "Hello World!"를 콘솔에 출력하는 것을 시작해 보겠습니다.
IntelliJ IDEA에서 “HelloWorld!” 프로젝트
참고로, 프로젝트가 없어도 jshell을 사용하면 됩니다. 터미널(Windows의 경우 명령 프롬프트)을 열고 jshell을 실행한 다음, System.out.print("Hello World!") 문을 입력하세요. 이는 Listing 3-23에 나와 있습니다.
jshell>
| Welcome to JShell -- Version 17-ea
| For an introduction type: /help intro
jshell> System.out.print("Hello World!")
Hello World!
Listing 3-23: jshell로 Hello World!
IntelliJ IDEA를 설치했으니, 자바 프로젝트를 생성하여 편집기가 선택한 프로젝트 구조를 확인해 보겠습니다. 첫 번째 IntelliJ IDEA 대화 상자에서 "Create New Project" 옵션을 클릭합니다. 그러면 만들 수 있는 프로젝트 유형이 왼쪽에 나열된 두 번째 대화 상자가 나타납니다. 여기 언급된 두 개의 대화 창은 Figure 3-13에 나타나 있습니다.
Figure 3-13: IntelliJ IDEA 프로젝트 구성 시작 대화 창 생성
왼쪽에서 Java 프로젝트 유형을 선택하고, 오른쪽에 나열된 추가 라이브러리 및 프레임워크를 선택하지 않고 "Next"를 클릭합니다.
다음 대화 상자에서는 프로젝트에 대한 템플릿을 선택할 수 있습니다. 우리는 "Next"를 클릭하여 이를 건너뛰겠습니다.
다음 대화 상자에서는 프로젝트 이름과 위치를 선택할 수 있습니다. Java 17을 사용하고 있으므로, 하단에 자바 모듈을 구성하는 섹션이 있다는 것을 확인할 수 있습니다. 이 구성 창은 Figure 3-14에 나타나 있습니다.
Figure 3-14: IntelliJ IDEA 프로젝트 이름 및 위치 구성 대화 창
프로젝트 이름과 모듈 이름 모두에 "sandbox"를 사용하고 "Finish"를 클릭합니다. 다음 창은 코드 편집 창입니다. 여기서 코드를 작성하게 됩니다. 왼쪽의 sandbox 노드를 확장하면(그 섹션은 "Project view"라고 불립니다) 설치한 JDK(이 경우 17)를 사용하여 프로젝트가 구성되어 있는 것을 볼 수 있습니다. src 디렉터리가 생성되었습니다. 프로젝트는 Figure 3-15에 설명된 것과 매우 유사하게 보일 것입니다.
Figure 3-15: IntelliJ IDEA 프로젝트 보기
코드를 작성하기 전에, 다른 프로젝트 설정 사항들을 확인해 보겠습니다. IntelliJ IDEA는 File > Project Structure 메뉴 항목을 통해 프로젝트 속성을 보고 편집할 수 있는 접근 권한을 제공합니다. 이를 클릭하면 Figure 3-16에 나타난 것과 유사한 대화 상자가 열립니다.
Figure 3-16: IntelliJ IDEA 프로젝트 설정 탭
기본적으로 "Project settings" 탭이 열립니다. 이전 그림에서 두 개의 화살표가 "Project SDK:" 섹션에 주의를 끌고 있는데, 이는 자바 프로젝트의 JDK 버전과 "Project language level:" 섹션을 나타냅니다. 이 장을 작성할 당시 JDK 17 EA가 최신 버전입니다. IntelliJ IDEA의 최신 버전은 Java 17에 대한 구문 및 코드 완성을 지원하므로 여기서 설명되고 있는 것입니다. 이것이 프로젝트 언어 수준 설정의 의미입니다.
"Modules"라는 이름의 탭으로 전환하면 Figure 3-17에 나타난 정보를 볼 수 있습니다.
Figure 3-17: IntelliJ IDEA 프로젝트 모듈 탭
이전 이미지에 대한 설명이 필요합니다. 자바 모듈은 패키지를 함께 묶는 것 외에도, 프로젝트 내에서 공통의 목적을 가진 자바 소스 및 리소스 파일을 함께 묶는 방법입니다. Oracle이 자바 애플리케이션을 모듈화하는 방법으로 모듈 개념을 도입하기 이전에, 대형 프로젝트를 효율적으로 구조화해야 하는 개발자들에 의해 이러한 애플리케이션을 구성하는 코드는 이미 모듈화되었습니다.
모듈 탭에서는 프로젝트에 얼마나 많은 부분(모듈)이 있는지와 각 부분의 설정을 확인할 수 있습니다. sandbox 프로젝트에는 하나의 부분, sandbox라는 이름의 하나의 모듈이 있으며, 해당 모듈의 소스는 src 디렉토리에 포함되어 있습니다. 만약 "Hello World!"를 출력하는 클래스를 작성하려면, HelloWorld.java 파일이 그 아래에 위치해야 합니다. src 디렉토리를 마우스 오른쪽 버튼으로 클릭하면 Figure 3-18에 나와 있는 메뉴가 나타납니다.
Figure 3-18: src 디렉터리에 생성할 수 있는 자바 객체를 나열한 IntelliJ IDEA 메뉴
Java Class 옵션 외에도, 몇 가지 빨간색 화살표가 src 디렉토리에 포함될 수 있는 다른 구성 요소를 보여줍니다. 이제 클래스를 생성해보겠습니다. Java Class 메뉴 옵션을 클릭하고 클래스 이름을 입력한 후, 테스트 필드 아래의 목록에서 Class를 선택합니다. Figure 3-19에서는 생성할 수 있는 모든 자바 타입을 볼 수 있습니다.
Figure 3-19: 자바 타입을 생성하기 위한 IntelliJ IDEA 대화 창
이 장의 시작에서 자바 애플리케이션의 핵심 구성 요소는 클래스라고 언급했지만, 자바에는 이외에도 다른 타입들이 있습니다. 이전 그림의 목록에는 다섯 가지 자바 타입이 나열되어 있으며, 각각의 타입은 이후에 자세히 설명됩니다. 지금은 src 디렉토리 아래에 HelloWorld.java라는 파일이 생성되었고, 그 내용은 Listing 3-24와 같이 간단하다는 것을 알아두세요.
public class HelloWorld {
}
Listing 3-24: HelloWorld 클래스
당신은 방금 매우 단순한 첫 자바 프로젝트에서 첫 자바 클래스를 생성했습니다. 아직 아무것도 하지 않습니다. 이 클래스는 IntelliJ IDEA의 Build 메뉴에서 Build Project 옵션을 선택하거나 각 운영 체제마다 다른 키 조합을 눌러 컴파일됩니다. 클래스를 컴파일하면 바이트코드가 포함된 HelloWorld.class 파일이 생성됩니다. IntelliJ IDEA는 기본적으로 컴파일 결과를 out/production이라는 디렉토리에 저장합니다. 프로젝트를 컴파일하기 위한 메뉴 옵션과 결과는 Figure 3-20에 표시되어 있으며, 메뉴 옵션은 녹색으로 강조 표시되어 있습니다.
Figure 3-20: 자바 프로젝트를 컴파일하는 방법 (IntelliJ IDEA)
이제 클래스에 Hello World!를 출력하는 방법을 추가할 때입니다. 이를 위해 클래스에 특별한 메소드가 필요합니다. 모든 자바 데스크톱 애플리케이션에는 main이라는 특별한 메소드가 있어야 하며, 이는 최상위 클래스에서 선언되어야 합니다. 이 메소드는 JRE에 의해 호출되어 자바 프로그램/애플리케이션을 실행하게 하며, 이를 시작 지점(entry point)이라고 부릅니다.
이러한 메소드가 없으면 자바 프로젝트는 실행할 수 없는 클래스 모음에 불과하며, 특정 기능을 수행할 수 없습니다.
이것은 다음과 같이 상상해볼 수 있습니다: 차가 있지만, 점화 실린더가 없어 그것을 시작할 방법이 없는 것과 같습니다.
의도와 목적상 차이지만, 실제로 어딘가로 가져다 줄 수 있는 주요 목적을 수행할 수 없습니다. main 메소드를 점화 실린더로, JRE가 애플리케이션을 실행하기 위해 키를 삽입하는 것으로 상상할 수 있습니다. 이제 HelloWorld 클래스에 그 메소드를 추가해봅시다.
IntelliJ IDEA는 훌륭한 편집기이기 때문에, psvm을 입력하고 키를 눌러 main 메소드를 생성할 수 있습니다. 이 네 글자는 메소드 선언의 모든 구성 요소의 시작 글자를 나타냅니다: public, static, void, 그리고 main.
Hello World!라는 텍스트를 출력하는 main 메소드를 가진 HelloWorld 클래스는 Listing 3-25에 나와 있습니다.
public class HelloWorld {
public static void main(String... args) {
System.out.println();
}
}
Listing 3-25: main 메소드를 포함한 HelloWorld 클래스
이제 main 메소드가 생겼으니, 이 코드를 실행할 수 있습니다. IntelliJ IDEA에서는 다음 두 가지 옵션이 있습니다:
- Run 메뉴에서 "Run [ClassName]" 옵션 선택
- 클래스 본문을 마우스 오른쪽 버튼으로 클릭하고 나타나는 메뉴에서 "Run [ClassName].main()" 선택
Figure 3-21은 클래스를 실행하기 위해 사용할 수 있는 두 메뉴 항목과 실행 결과를 보여줍니다.
Figure 3-21: 자바 클래스를 실행하는 방법 (IntelliJ IDEA)
이것이 자바 프로젝트의 가장 기본적인 구조입니다. 이 프로젝트는 너무 단순해서 명령 줄에서 수동으로 컴파일할 수도 있습니다. 그러니 해보죠!
"HelloWorld!" 프로젝트의 명령 줄에서 컴파일 및 실행
IntelliJ IDEA에서 Terminal 버튼을 보셨을 것입니다. 해당 버튼을 클릭하면 편집기 내부에 터미널이 열립니다. Windows의 경우 명령 프롬프트 인스턴스가 열리고, Linux와 macOS의 경우 기본 셸이 열립니다. IntelliJ는 프로젝트 루트에 터미널을 열어줍니다. 다음으로 해야 할 일은 다음과 같습니다:
- 다음 명령을 실행하여 src 디렉토리로 이동합니다: cd src
- cd는 Windows 및 Unix 시스템 모두에서 작동하며 change directory의 약자입니다.
- 다음을 실행하여 HelloWorld.java 파일을 컴파일합니다: javac HelloWorld.java
- javac는 자바 파일을 컴파일하는 데 사용되는 JDK 실행 파일로, IntelliJ IDEA도 백그라운드에서 호출합니다.
- 다음을 실행하여 HelloWorld.class 파일에서 생성된 바이트코드를 실행합니다: java HelloWorld
Figure 3-22는 IntelliJ IDEA의 터미널에서 이러한 명령을 실행하는 과정을 보여줍니다.
Figure 3-22: IntelliJ IDEA 내의 터미널에서 HelloWorld 클래스를 수동으로 컴파일 및 실행
간단해 보이죠? 실제로도 그렇습니다. 이유는 패키지나 자바 모듈이 정의되지 않았기 때문입니다. 하지만 그게 가능한가요? 가능합니다. 패키지를 정의하지 않으면, 그 클래스는 여전히 JSE 플랫폼이 제공하는 이름 없는 기본 패키지의 일부입니다.
이는 여기서 작성 중인 것 같은 소규모의 임시 교육용 애플리케이션 개발을 위해 제공됩니다.
이제 프로젝트를 조금 더 복잡하게 만들어서 클래스가 속할 이름이 있는 패키지를 추가해 봅시다.
"HelloWorld" 클래스를 패키지에 넣기
Figure 3-18에서 나열된 메뉴에는 Package 옵션이 포함되어 있습니다. src 디렉토리를 마우스 오른쪽 버튼으로 클릭하고 해당 옵션을 선택하세요. 패키지 이름을 입력해야 하는 작은 대화 상자가 나타납니다. com.sandbox를 입력합니다. Figure 3-23에서 대화 상자가 나타나 있습니다. 만약 생성하려는 패키지가 이미 존재하면, 빨간색으로 오류 메시지가 표시됩니다.
Figure 3-23: IntelliJ IDEA에서 중복 패키지 생성
이제 패키지가 생겼지만, 클래스는 그 안에 있지 않습니다. 클래스를 패키지로 이동하려면, 클래스를 클릭하고 드래그하세요. 이 작업을 정말로 수행하고자 하는지 확인하는 또 다른 대화 상자가 나타납니다(Figure 3-24에 설명됨).
Figure 3-24: IntelliJ IDEA에서 클래스를 패키지로 이동
Refactor 버튼을 클릭하고 클래스에 어떤 변화가 생기는지 보세요. 이제 클래스가 package com.sandbox; 선언으로 시작해야 합니다. 프로젝트를 다시 빌드한 다음 production 디렉토리를 확인하면, Figure 3-25와 유사한 구조를 볼 수 있을 것입니다.
Figure 3-25: com.sandbox 패키지를 추가한 후 새 디렉토리 구조
당연히 클래스를 수동으로 컴파일하고 실행할 경우, 이제 패키지를 고려해야 하므로 명령어가 다음과 같이 변경됩니다:
~/sandbox/src/> javac com/sandbox/HelloWorld.java
~/sandbox/src/> java com/sandbox/HelloWorld
그렇다면 모듈이 설정되면 어떻게 될까요? 기본적으로 이름없는 모듈이 있으며, 모든 JAR(모듈식이든 아니든)과 클래스가 클래스패스 내에 포함됩니다. 이 이름 없는 기본 모듈은 모든 패키지를 내보내고 다른 모든 모듈을 읽습니다. 이름이 없기 때문에 명명된 애플리케이션 모듈에 의해 요구되거나 읽힐 수 없습니다. 따라서 작은 프로젝트가 JDK 9 이상 버전에서 작동하는 것처럼 보이더라도, 다른 모듈에 의해 접근될 수 없습니다. 하지만 다른 것에 접근할 수 있기 때문에 작동합니다. (이는 JDK의 오래된 버전과의 호환성을 보장합니다.) 이것을 감안하여, 이제 우리 프로젝트에 모듈을 추가해 봅시다.
"com.sandbox" 모듈 구성하기
모듈을 구성하는 것은 src 디렉토리 아래에 module-info.java 파일을 추가하는 것만큼 쉽습니다. Figure 3-18의 메뉴에서 module-info.java 옵션을 선택하면 IDE가 파일을 자동으로 생성해줍니다. 모든 것이 잘 준비되고, 생성된 모듈 이름이 마음에 들지 않으면 변경할 수 있습니다. 저는 Oracle 개발자들이 정한 모듈 명명 규칙을 따르기 위해 이름을 com.sandbox로 변경했습니다. 파일은 Listing 3-26에 나오는 것처럼 초기에는 비어 있습니다.
module com.sandbox {
}
Listing 3-26: com.sandbox 모듈 구성 파일
이제 모듈이 생겼으니 어떻게 될까요? IDE 관점에서는 별로 달라지는 것이 없습니다. 하지만 모듈을 수동으로 컴파일하려면 몇 가지를 알아야 합니다. 저는 Listing 3-27의 명령을 사용하여 모듈을 컴파일했습니다.
~/sandbox/src/> javac -d ../out/com.sandbox \
module-info.java \
com/sandbox/HelloWorld.java
Listing 3-27: 모듈에 포함된 패키지를 수동으로 컴파일하기
“\”는 macOS/Linux 구분자입니다. Windows에서는 명령을 하나의 줄에 쓰거나 “\”를 “^”로 바꾸세요.
이전 명령은 Listing 3-28의 템플릿에 따라 작성되었습니다.
javac -d [destination location]/[module name] \
[source location]/module-info.java \
[java files...]
Listing 3-28: 모듈에 포함된 패키지를 수동으로 컴파일하는 명령 템플릿
-d [destination]은 실행 결과가 저장될 위치를 결정합니다. Listing 3-27의 명령에서 출력 폴더를 /out/com.sandbox로 지정한 이유는 com.sandbox가 포함 모듈이라는 것을 명확히 하기 위함입니다. 이 디렉토리 아래에는 com.sandbox 패키지의 일반적인 구조가 있습니다. out 디렉토리의 내용은 Figure 3-26에 묘사되어 있습니다.
Figure 3-26: 수동으로 컴파일된 자바 모듈 com.sandbox
이 예제에서 보았듯이, 자바 모듈은 module-info.class 설명자에 의해 설명된 패키지를 캡슐화하는 논리적 모드일 뿐이므로 소스를 컴파일하기 전까지는 실제로 존재하지 않습니다. com.sandbox 디렉토리가 생성된 유일한 이유는 javac -d 명령에서 이를 인수로 지정했기 때문입니다.
이제 모듈을 컴파일했으니, Listing 3-29는 모듈 내에 포함된 HelloWorld 클래스를 실행하는 방법을 보여줍니다.
~/sandbox/> java --module-path out \
--module com.sandbox/com.sandbox.HelloWorld
Hello World! # 결과
Listing 3-29: 모듈에 포함되어 있는 클래스를 수동으로 실행하기
이전 명령은 Listing 3-30의 템플릿에 따라 작성되었습니다.
java --module-path [destination] \
--module [module name]/[package name].HelloWorld
Listing 3-30: 모듈에 포함된 클래스를 수동으로 실행하는 명령 템플릿
2017년 9월의 Oracle Magazine 판에서는 처음으로 예를 언급했으며, Oracle 개발자들은 모듈 이름이 패키지와 같은 규칙을 따라야 한다고 결정했지만, 복잡한 프로젝트에서는 패키지 이름이 매우 길어지는 경향이 있을 때 이 규칙은 다소 중복되어 보입니다. 모듈 이름도 그렇게 길게 해야 할까요?
사실 표준은 사람들이 만드는 것이고, 대부분의 경우 실용적인 것이 표준이 됩니다. 2007년 이후로 모듈을 수용할 수 있는 프로젝트들은 더 간단하고 실용적인 모듈 이름을 선택했습니다. 예를 들어, 스프링 프레임워크를 만든 팀은 그들의 모듈을 org.springframework.core 대신 spring.core로, org.springframework.beans 대신 spring.beans로 명명했습니다. 특수 문자와 숫자를 피하는 한 원하는 대로 모듈 이름을 정하십시오.
빌드 도구를 사용하는 자바 프로젝트, 주로 Maven
Apache Maven은 주로 자바 프로젝트를 위해 사용되는 빌드 자동화 도구입니다. Gradle이 점점 인기를 얻고 있지만, Maven은 여전히 가장 많이 사용되는 빌드 도구 중 하나입니다. Maven과 Gradle 같은 도구는 애플리케이션의 소스 코드를 상호 의존적인 프로젝트 모듈로 구성하고, 컴파일, 검증, 소스 생성, 테스트 및 아티팩트 생성을 자동으로 구성하는 방법을 제공합니다. 아티팩트(artifact)는 일반적으로 JAR 파일이며, Maven 저장소에 배포됩니다. Maven 저장소는 JAR 파일이 특수한 디렉토리 구조로 저장되는 HDD의 위치입니다.
빌드 도구에 대한 논의는 반드시 Maven으로부터 시작해야 합니다. 왜냐하면 이 빌드 도구가 오늘날 개발에서 사용하는 많은 용어를 표준화했기 때문입니다. 여러 하위 프로젝트로 나누어진 프로젝트는 GitHub에서 다운로드받아 커맨드 라인에서 빌드하거나 IntelliJ IDEA에 가져올 수 있습니다. 이 접근 방식은 한 번에 컴파일할 수 있는 품질 있는 소스를 확보할 수 있게 합니다. 또한 새로운 챕터를 읽을 때마다 IntelliJ IDEA에 새 프로젝트를 로드하고 싶지 않을 거라고 생각하니 실용적입니다. 또한 제게는 소스를 유지하고 새로운 JDK에 적응하는 걸 더 쉽게 만들어 주며, Oracle이 자주 릴리즈하게 되면서 저는 이를 빠르게 해야 할 필요가 있습니다.
이 책에서 작성한 코드를 테스트할 (그리고 자신이 원하는 경우 코드 작성도 가능한) 프로젝트는 java-17-for-absolute-beginners라고 합니다. 이는 다중 모듈 Maven 프로젝트입니다. 프로젝트의 첫 번째 레벨은 java-17-for-absolute-beginners 프로젝트이며, pom.xml이라는 구성 파일을 가지고 있습니다. 이 파일에는 모든 종속성과 그 버전이 나열되어 있습니다. 하위 프로젝트, 즉 두 번째 레벨의 프로젝트들은 이 프로젝트의 모듈입니다. 우리는 이들을 하위 프로젝트라고 부르는데, 이것들은 상위 프로젝트에서 이러한 종속성과 모듈을 상속받습니다. 구성 파일에서는 상위에 정의된 목록에서 필요한 종속성을 지정할 수 있습니다.
이 모듈들은 각 챕터를 위해 소스를 함께 묶는 방법이며, 그래서 이 모듈들은 chapter00, chapter01 등으로 이름이 지어졌습니다. 프로젝트가 크고 작성해야 할 코드가 많으면, 코드는 또 다른 수준의 모듈로 나뉘어집니다. chapter05 모듈은 그러한 경우이며, 그 아래 프로젝트의 상위로 구성되어 있습니다. Figure 3-27에서는 IntelliJ IDEA에 로드된 이 프로젝트의 모습과, chapter05 모듈이 확장되어 세 번째 수준의 모듈을 볼 수 있습니다. 각 레벨은 해당 번호로 표시됩니다.
Figure 3-27: Maven 다중 레벨 프로젝트 구조
2장에서 가르쳐드린 대로 이것을 IntelliJ IDEA에 로드하셨다면, 모든 것이 올바르게 작동하는지 빌드하여 확인해보세요. 방법은 다음과 같습니다: IntelliJ IDEA 편집기를 사용하여, 오른쪽 상단에 Maven이라는 탭이 있어야 합니다. 프로젝트가 Figure 3-28에 나와 있는 것처럼 로드되었다면, 제대로 로드된 것입니다. Maven 탭이 보이지 않는다면 (1)로 표시된 것과 같은 레이블을 찾아 클릭하세요. java-17-for-absolute-beginners (루트) 노드를 확장하여 (2)로 표시된 빌드 작업을 찾습니다. 더블 클릭하고 편집기 하단에서 오류가 보이지 않으면, 모든 프로젝트가 성공적으로 빌드된 것입니다. BUILD SUCCESS (3) 메시지를 볼 수 있어야 합니다.
Figure 3-28: Maven 프로젝트 보기
Maven 프로젝트가 예상대로 작동하는지 확인하는 두 번째 방법은 커맨드 라인에서 빌드하는 것입니다. IntelliJ IDEA 터미널을 열고, 2장에서 설명한 대로 시스템 경로에 Maven을 설치했다면 그저 mvn을 입력하고 를 누르세요.
프로젝트 루트에 있는 주요 pom.xml 파일은 다음 줄을 통해 기본 목표가 구성되어 있습니다:
<defaultGoal>clean install</defaultGoal>
이 프로젝트를 빌드하는 데 필요한 두 가지 Maven 실행 단계를 선언합니다. 구성에 이 요소가 없다면, 프로젝트를 빌드하기 위해 명령어에서 두 단계를 지정해야 합니다. 예를 들어: mvn clean install.
명령 줄에서 이 책을 받을 때 JDK 17이 여전히 불안정하다면 몇 가지 경고 메시지를 볼 수 있지만, 실행이 BUILD SUCCESSFUL로 끝난다면 모든 것이 제대로 된 것입니다.
sandbox 프로젝트 외에도, 이 섹션에서 언급된 모든 클래스, 모듈, 패키지는 이 프로젝트의 일부입니다. chapter00과 chapter01은 해당 챕터들에 특별히 속하는 클래스를 실제로 가지고 있는 것은 아니며, 단지 자바 모듈 예제를 구성할 수 있도록 만들 필요가 있었습니다. IntelliJ IDEA는 모듈을 알파벳순으로 정렬하므로, 챕터 모듈의 이름을 이와 같은 방식으로 정해서 작업해야 하는 순서대로 나열될 수 있게 했습니다.
지금까지 이 챕터는 Java 애플리케이션의 기본 구성 요소에 집중했고, 지침을 따라 Hello World!를 출력하는 클래스를 만들었습니다. 하지만 모든 세부 사항이 설명되진 않았습니다. 이제 그 작업을 수행하고, 클래스에 새 세부 정보를 추가해 보겠습니다.
"Hello World!" 클래스 설명 및 강화
이전에 우리는 sandbox 프로젝트에서 HelloWorld라는 클래스를 작성했습니다. 이 클래스는 chapter03 프로젝트의 com.apress.bgn.three.helloworld 패키지에 복사되었습니다. 이 장은 클래스의 주요 구성 요소 목록으로 시작합니다. HelloWorld 클래스는 이러한 요소 중 일부를 포함하고 있으며, 이에 대해 더 자세히 설명합니다. Figure 3-29에서는 IntelliJ IDEA 편집기에서의 HelloWorld 클래스가 묘사되어 있습니다.
Figure 3-29: java-17-for-absolute-beginners 프로젝트의 HelloWorld 클래스
다음 목록에서는 줄 수와 목록에 있는 번호가 일치하는 서로 다른 문장을 설명합니다.
- 패키지 선언: 클래스가 패키지의 일부일 때, 해당 패키지를 선언하는 이 줄로 코드가 시작해야 합니다. package는 자바에서 예약된 키워드로, 패키지 선언을 제외한 다른 용도로는 사용할 수 없습니다.
<비어 있음>: 그림을 더 깔끔하게 보여주기 위해 비워둠 - 클래스 선언: 타입을 선언하는 줄입니다:
- 공개(public)되어 있으므로 어디서든 볼 수 있습니다.
- 클래스입니다.
- HelloWord라는 이름을 가집니다.
- 중괄호로 둘러싸인 본문이 있으며 여는 괄호는 이 줄에 있습니다. 하지만 빈 공간은 무시되므로 다음 줄에 올 수도 있습니다.
- main() 메소드 선언: 자바에서 메소드 이름과 매개변수의 수, 타입, 순서를 메소드 시그니처라고 합니다. 메소드에는 반환 타입도 있으며, 결과로 반환하는 타입입니다. 그러나 메소드가 아무것도 반환하지 않는 것을 선언하는 데 사용할 수 있는 특별한 타입도 있습니다. 등장 순서에 따라, main() 메소드의 각 용어는 다음을 나타냅니다:
- public 메소드 접근자: main 메소드는 반드시 public 이어야 JRE가 접근하고 호출할 수 있습니다.
- static: 이 장의 시작 부분에서 클래스에 멤버(필드와 메소드)가 있다고 언급했었죠? 해당 클래스 타입의 객체가 생성되면, 클래스가 선언한 대로 필드와 메소드를 가집니다. 클래스는 객체를 만드는 템플릿입니다. 그러나 static 키워드 덕분에, main 메소드는 클래스 타입의 객체가 아닌 클래스 자체와 연결됩니다. 자세한 내용은 다음 장에서 설명합니다.
- void: main 메소드는 아무것도 반환하지 않음을 알려주기 위해 사용되며, 이는 타입이 없는 것과 마찬가지입니다. 아무것도 반환되지 않으므로 타입이 필요하지 않죠.
- String... args 또는 String[] args: 메소드는 때때로 입력 데이터를 받도록 선언됩니다. String[] args는 텍스트 값들의 배열을 나타냅니다. 세 점은 동일한 타입의 여러 인수가 있을 수 있음을 나타내는 다른 방법입니다. 세 점 표기법은 메소드 인수에서만 사용할 수 있으며, 이를 varargs라고 부릅니다. (varargs 인수는 반드시 메소드의 유일한 매개변수이거나 마지막 매개변수여야 합니다. 그렇지 않으면 인수를 해결하는 것이 불가능해집니다.) 이는 배열을 명시적으로 생성하지 않고 매개변수 배열을 전달할 수 있음을 의미합니다. 배열은 고정 길이의 데이터 집합으로, 수학에서는 1차원 행렬 또는 벡터로 알려져 있습니다. String은 자바의 텍스트 객체를 나타내는 클래스입니다. []는 배열을 의미하고, args는 그 이름입니다. 하지만 잠깐, 우리는 이전에 이 메소드를 실행했는데, 아무것도 제공할 필요가 없었습니다! 이는 필수가 아니지만, 이 목록 후에 인수(코드 본문에서 사용될 메소드에 제공되는 값)를 어떻게 전달할 수 있는지 알게 될 것입니다.
- 'System.out.println("Hello World!");'는 콘솔에 Hello World!를 출력하는 데 사용되는 문장입니다.
- }는 main 메소드 본문의 닫는 괄호입니다.
- }는 클래스 본문의 닫는 괄호입니다.
이 클래스를 실행하면 콘솔에 "Hello World!"가 출력되는 것을 볼 수 있습니다. Figure 3-21에서 main() 메소드가 포함된 클래스를 어떻게 실행하는지 보여주었습니다. 이러한 방식으로 클래스를 실행한 후, IntelliJ IDEA는 해당 실행을 위한 설정을 Run Configuration에 자동으로 저장하며, 이는 드롭다운 목록에 표시되고, 삼각형 녹색 버튼 옆에 위치하여 클릭만으로 클래스를 실행할 수 있게 해줍니다. 이들은 모두 IDE의 헤더에 위치하며, Figure 3-29에서 강조되어 있습니다.
이 두 요소는 매우 중요합니다. 실행 구성을 편집하고, JVM 및 main 메소드에 대한 인수를 추가할 수 있기 때문입니다. 먼저 main 메소드를 수정하여 인수로 무언가를 하도록 해보겠습니다.
public class HelloWorld {
public static void main(String... args) {
System.out.println("Hello " + args[0] + "!");
}
}
Listing 3-31: varargs를 사용한 main 메소드
배열은 요소의 인덱스를 사용하여 접근하며, 자바에서는 0부터 카운트가 시작됩니다. 따라서 배열의 첫 번째 요소는 인덱스 0에서 찾을 수 있고, 두 번째 요소는 인덱스 1에서 찾을 수 있습니다. 그러나 배열은 비어 있을 수 있습니다. 이전 코드 조각에서 인수를 지정하지 않으면 프로그램 실행이 중단되고 콘솔에 명시적인 메시지가 빨간색으로 표시됩니다.
자바 프로그램이 실행 중 오류로 인해 종료되면, 예외가 발생했다고 합니다.
빈 배열에 접근하거나 존재하지 않는 배열 요소에 접근하려고 하면, JVM은 실패가 발생한 위치와 접근하려던 요소의 인덱스를 포함한 ArrayIndexOutOfBoundsException 타입의 객체를 던집니다. 예외 객체는 자바 실행이 예상대로 작동하지 않을 때 개발자에게 알리기 위해 JVM이 사용하며, 코드에서 어디서 문제가 발생했는지, 무엇이 문제를 일으켰는지에 대한 세부 정보를 포함합니다.
이전 코드 조각에서의 수정은 클래스를 실행할 때 제공된 텍스트 값을 출력하도록 합니다. 이제 이 클래스의 실행 구성을 수정하고 인수를 추가해봅시다. Run Configuration 이름 옆의 작은 회색 화살표를 클릭하면 메뉴가 나타납니다. Edit Configurations를 클릭하고 표시된 대화 상자를 검사합니다. Figure 3-30은 메뉴와 대화 창을 보여줍니다.
Figure 3-30: 실행 구성 사용자 정의
이미지에서 주요 요소는 연한 파란색으로 강조 표시되어 있습니다. IntelliJ IDEA는 몇 가지 이전 실행 기록을 저장하며, Maven 빌드 작업도 포함하여 클릭 한 번으로 다시 실행할 수 있도록 해줍니다. Run/Debug Configurations 대화 창의 왼쪽에서 IntelliJ IDEA는 실행 구성을 유형별로 그룹화합니다. 기본적으로 마지막 실행 구성은 창의 오른쪽에 열리며, 이 경우에는 HelloWorld 클래스에 대한 실행 구성입니다. 실행을 위한 다양한 옵션을 설정할 수 있으며, 대부분은 IDE에 의해 자동으로 결정됩니다. 프로그램 인수 또는 main() 메소드의 인수는 빨간색으로 표시된 텍스트 필드에 입력됩니다. 그림에서는 JavaDeveloper를 입력했으며, Apply를 클릭한 다음 Ok 버튼을 누르고 클래스를 실행하면 콘솔에서 Hello World! 대신 이제 "Hello JavaDeveloper!"가 표시됩니다.
이전에 sandbox 프로젝트에서 HelloWorld라는 클래스를 작성했습니다. 이 클래스는 chapter03 프로젝트의 com.apress.bgn.three.helloworld 패키지에 복사되었습니다. 다음은 이 클래스에 더욱 다양한 요소를 추가하는 내용을 설명합니다. Listing 3-32에 이 클래스의 main() 메소드가 재차 묘사되어 있습니다.
package com.apress.bgn.three.helloworld;
import java.util.List;
public class HelloWorld {
public static void main(String... args) {
//System.out.println("Hello " + args[0] + "!");
List<String> items = List.of("1", "a", "2", "a", "3", "a");
items.forEach(item -> {
if (item.equals("a")) {
System.out.println("A");
} else {
System.out.println("Not A");
}
});
}
}
Listing 3-32: 더 복잡한 main 메소드
import java.util.List; 문은 패키지와 클래스 선언 사이에 존재할 수 있는 유일한 유형의 문입니다. 이 문은 자바 컴파일러에게 프로그램에서 java.util.List 객체 타입이 사용될 것임을 알립니다. import 키워드 뒤에 타입의 정규화된 이름이 옵니다. 타입의 정규화된 이름은 패키지 이름(java.util), 점(.), 타입의 간단한 이름(List)으로 구성됩니다. 이 문 없이 HelloWorld 클래스는 컴파일되지 않습니다. 한번 시도해 보세요: 이 문 앞에 “//”를 넣어 그 줄을 컴파일러가 무시하는 주석으로 만드세요. 그러면 관련 코드는 빨간색으로 변하면서 편집기가 오류를 표시할 것입니다.
List<String> items = List.of("1", "a", "2", "a", "3", "a"); 문은 텍스트 값들의 리스트를 생성합니다. 이러한 방식의 리스트 생성은 자바 9에 도입되었습니다. 를 사용하여 리스트에 어떤 종류의 요소가 포함되는지 지정하는 것은 자바 5에서 도입되었으며, 이를 제네릭(generics)이라고 합니다. 리스트의 요소들은 forEach 메소드를 통해 하나씩 순회되며 각각이 "a" 문자와 동일한지 테스트됩니다. 이를 수행하는 전체 표현식은 람다 표현식이라고 하며, 이러한 유형의 구문은 자바 8에서 forEach 메소드와 함께 도입되었습니다.
이제 클래스를 실행하면 콘솔에 A와 Not A가 자신의 줄에 각각 출력되는 것을 볼 수 있습니다.
Not A
A
Not A
A
Not A
A
지금까지 작성한 코드는 몇 가지 유형의 객체를 사용하여 콘솔에 간단한 메시지를 출력합니다. List 객체는 여러 String 객체를 보유하는 데 사용됩니다. 메시지는 System 클래스의 정적 필드인 out 객체에서 호출되는 println 메소드를 사용하여 출력됩니다. 그리고 이들은 코드에서 여러분에게 보이는 객체들일 뿐입니다. 내부적으로, List 요소들은 생성된 Consumer 객체에 의해 처리되며, 이는 간결성을 위해 람다 표현식 하에 감춰져 있습니다. 따라서 앞선 코드는 Listing 3-33처럼 확장될 수 있습니다.
package com.apress.bgn.three.helloworld;
import java.util.List;
import java.util.function.Consumer;
public class HelloWorld {
public static void main(String... args) {
List<String> items = List.of("1", "a", "2", "a", "3", "a");
items.forEach(new Consumer<String>() {
@Override
public void accept(String item) {
if (item.equals("a")) {
System.out.println("A");
} else {
System.out.println("Not A");
}
}
});
}
}
Listing 3-33: 더 복잡한 main 메소드
마무리하기 전에 또 다른 깔끔한 것을 보여드리고자 합니다. forEach 블록의 내용을 한 줄로 작성할 수 있습니다:
items.forEach(item -> System.out.println(item.equals("a") ? "A" : "Not A"));
위의 줄은 method reference라고 불리는 것을 사용하여 더 간단하게 만들 수 있습니다. 이에 대해서는 책의 후반부에서 자세히 다루겠습니다.
지금은 조금 두려울 수 있지만, 이 책은 각 개념을 명확한 맥락에서 소개하고, 실제 세계의 객체와 사건과 비교하여 쉽게 이해할 수 있도록 합니다. 그리고 그것이 효과가 없더라도, 더 많은 책과 블로그는 물론, 각 JDK에 대한 공식 Oracle 페이지에서 훌륭한 튜토리얼을 제공하니 참고할 수 있습니다. 마음만 먹으면 길이 있습니다.
또한, IDE를 최대한 활용하세요! 코드에서 Control/Command 키를 누른 상태로 객체 타입을 클릭하면 객체 클래스의 코드가 열리고, 해당 클래스가 어떻게 작성되었는지 확인할 수 있으며, 편집기에서 직접 그에 대한 문서를 읽을 수 있습니다. 연습으로 forEach 메소드와 System 클래스에 대해 이렇게 해보세요.
대부분의 스마트 편집기에는 키맵이 있습니다. 이는 특정 동작(예: 네비게이션, 코드 생성, 실행 등)을 수행할 수 있는 키 조합입니다. IntelliJ IDEA 키맵 참조를 인쇄하여 익숙해지세요. 우리의 뇌는 매우 빠르며, 코딩할 때 목표는 가능한 한 생각하는 만큼 빨리 입력하는 것입니다. :)
요약
이 장에서는 자바 애플리케이션의 기본적인 블록을 소개했습니다. 또한, JShell을 사용하여 애플리케이션의 컨텍스트를 벗어나 자바 문을 실행하는 방법을 배웠습니다. 패키지와 모듈을 선언한 자바 코드를 수동으로 컴파일하는 방법도 알아보았습니다.
이 장을 따라하며 한 많은 작업들을 아마 자바 개발자로 취직한 후에는 일상적으로 하게 될 것입니다(기존 코드에서 버그를 찾고 수정하는 데 시간을 보내는 날을 제외하고). JDK에는 애플리케이션 작성에 사용할 수 있는 많은 필드 및 메소드를 가진 클래스가 많기 때문에 문서를 읽는 데도 많은 시간을 보낼 것입니다. 출시되는 각 버전마다 변화가 있기에 계속해서 최신 정보를 유지해야 합니다.
인간의 뇌는 한계가 있습니다. 전문가나 동료 혹은 상사가 모든 JDK 클래스와 메소드를 다 알기를 기대하지는 않을 것입니다. 현명하게 일하고 이 URL https://docs.oracle.com/en/java/javase/17/docs/api/index.html (또는 사용 중인 JDK 버전에 해당하는 URL)을 항상 브라우저에 열어두세요. JDK 클래스나 메소드에 대해 의문이 생기면, 바로 해당 내용을 읽어보세요.
추가 전문 내용들 ===========
Oracle JShell 사용자 가이드는 Oracle의 "Java Shell User’s Guide"에서 확인할 수 있습니다. https://docs.oracle.com/javase/9/jshell/JSHEL.pdf. (2021년 10월 15일에 접속)
JAR 파일이 The Maven Public Repository와 같은 저장소에 호스팅되면 이를 아티팩트라고도 합니다. JAR 파일에 대해 더 알고 싶으시면 Oracle의 "JAR File Overview"를 참조하십시오. https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jarGuide.html. (2021년 10월 15일에 접속)
가장 많이 사용되는 라이브러리는 Log4J 같은 로깅 라이브러리입니다. 이는 "Apache Log4j 2"에서 찾을 수 있으며, https://logging.apache.org/log4j/2.x 와 LogBACK, "Logback Project"는 https://logback.qos.ch에서 확인할 수 있습니다. (모두 2021년 10월 15일에 접속)
더 알고 싶다면, Jar Hell에 대한 훌륭한 기사인 Tech Read의 "What Is Jar Hell?"를 추천합니다. https://tech-read.com/2009/01/13/what-is-jar-hell (아마 직접 코드를 조금 작성한 후에 읽어보시길 권장합니다).
내포된 클래스는 지금 언급하지 않겠습니다. 이에 대해서는 4장에서 다룰 것입니다.
또한, 내포된 것으로 참조될 다른 자바 타입을 정의할 수도 있지만, 그 부분도 나중에 다루겠습니다.
표를 설명한 이유는 Oracle, "Controlling Access to Members of a Class,"로 탐색하는 번거로움을 피하기 위해서입니다. (2021년 10월 15일에 접속)
Maven이나 Gradle 같은 빌드 도구는 하위 프로젝트를 모듈로 참조하기도 하지만, 그 목적은 자바 모듈과 다릅니다.
Jigsaw 프로젝트의 전체 역사는 Open JDK의 "Project Jigsaw"에서 확인할 수 있습니다. http://openjdk.java.net/projects/jigsaw (2021년 10월 15일에 접속)
Run 메뉴 항목 옆에는 클래스를 실행하는 데 사용할 수 있는 키 조합이 표시되어 있습니다.