안녕하세요! 여러분들의 궁금증을 해결해 줄 다람쥐입니다.
한빛미디어 혼공학습단 9기 혼공자바 1주차 포스팅입니다.
1주차는 혼공자바 Chapter 1 ~ 5 ( 자바 설치, 이클립스 설치, 변수와 타입, 연산자, 조건문과 반복문, 배열, 열거 타입 ) 를 공부합니다.
혼자 공부하는 자바를 공부하며 궁금한 점 위주로 조사하고 정리했습니다.
혼공 학습단 9기 1주차 기본 미션으로 JDK 설치 화면을 인증합니다.
혼공 학습단 9기 1주차 선택 미션으로 학습 스케쥴을 직접 짜고 공유합니다.
챌린지할 개인 미션은 바이트 코드로 기본 문법 분석하기 입니다.
다람쥐의 실습 환경은 아래와 같습니다.
- 기종 : MacBook Pro(16형, 2021년 모델), Apple M1 Pro 칩
- 메모리 : 32 GB
- 운영체제 : macOS Monterey 12.0.1
- 터미널 (명령 프롬프트) : zsh (기본)
1. 자바 프로그램을 실행할 때도 일반 사용자도 JDK 를 설치해야 할까?
자바 개발 도구 ( JDK, Java Development Kit ) 는 자바로 프로그램을 개발하기 위한 라이브러리와 컴파일러 ( javac ), 개발 문서 ( javadoc ) 를 제공합니다.
그러면 일반 사용자가 자바 프로그램을 실행할 때는 어떻게 해야 할까요?
개발자도 아닌데 JDK 를 깔아야 할까요? 아니면 그대로 실행이 가능할까요?
자바 프로그램은 모두 JVM ( 자바 가상 머신, Java Virtual Machine ) 위에서 실행합니다.
JVM 자바 가상 머신은 자바로 컴파일한 바이트코드를 읽고 각 운영체제에 맞게 기계어로 번역하고 최적화되어 실행합니다.
자바 뿐 아니라 코틀린(Kotlin), 스칼라(Scala) 등 자바 바이트코드로 컴파일을 지원하는 언어의 실행도 가능합니다.
자바와 다른 언어지만 같은 자바 바이트코드를 생성하여 JVM 에서 실행 시킬 수 있기 때문입니다.
자바 바이트코드를 실행시키기 위해선 결국 JVM 이 필요하다는 사실을 눈치 챌 수 있을 겁니다.
JDK 만 제공해주는 게 아닌 Java 바이트 코드를 실행시켜줄 JVM 자바 가상 머신을 제공하는 자바 런타임 환경 ( JRE, Java Runtime Environment ) 을 제공합니다.
따라서 일반 사용자가 자바 프로그램만을 실행하기 위해서라면 JRE 을 설치해야 합니다.
JDK 를 설치한 사람은 이미 JRE 를 포함하기에 별도로 설치할 필요는 없습니다.
자바 개발 키트를 공식으로 지원하는 Oracle 에서 일반 사용자를 위한 JRE 를 제공합니다.
JDK 설치와 마찬가지로 자동으로 사용자의 운영체제(Windows, Linux, Mac)와 아키텍처(Intel/AMD/ARM(ex. M1 Chip))
Oracle SE (Standard Edition) JRE 설치 : https://www.java.com/ko/download/
Oracle SE (Standard Edition) JDK, JRE 설치 : https://www.oracle.com/java/technologies/downloads/
1.1. JDK 설치 인증
이전에 AdoptOpenJDK 11 버전을 설치하고 자바 버전을 관리해주는 jEnv 애플리케이션을 사용 중입니다.
$ jenv global 1.8
명령어로 전역 java 경로를 openjdk 1.8 버전으로 변경할 수 있습니다.
오라클 JDK 는 Oracle SE JDK 설치 링크에서 macOS x64 DMG Installer 를 설치했습니다.
JDK 를 설치하면 맥 기준 /Library/Java/JavaVirtualMachines/ 에 설치가 됩니다.
OracleJDK 1.8 버전을 jenv 에 추가하여
기존의 AdoptOpenJDK 11 버전에서 OracleJDK 1.8 버전으로 수정해보자!
$ echo $JAVA_HOME
/Users/XXXXXX/.jenv/versions/11.0.11
$ jenv add /Library/Java/JavaVirtualMachines/jdk1.8.0_351.jdk/Contents/Home
oracle64-1.8.0.351 added
1.8.0.351 added
1.8 already present, skip installation
1.8.0.351 already present, skip installation
$ jenv versions
system
1.8
1.8.0.292
1.8.0.351
11
11.0
* 11.0.11 (set by /Users/XXXXXX/.jenv/version)
14
14.0
14.0.2
openjdk64-1.8.0.292
openjdk64-11.0.11
openjdk64-14.0.2
oracle64-1.8.0.351
$ jenv global oracle64-1.8.0.351
$ echo $JAVA_HOME
/Users/XXXXXX/.jenv/versions/oracle64-1.8.0.351
$ java -version
java version "1.8.0_351"
Java(TM) SE Runtime Environment (build 1.8.0_351-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.351-b10, mixed mode)
$ javac -version
javac 1.8.0_351
jenv 에 oracle64-1.8.0.351 버전이 추가됐다.
jenv global 명령어로 전역적으로 oracle64-1.8.0.351 버전을 사용하도록 설정하였고 JAVA_HOME 경로가 수정되었다.
java 와 javac 의 버전을 확인해보자 OracleJDK 1.8 버전으로 변경된 걸 확인할 수 있다.
2. 오라클 JDK와 오픈 JDK 둘 중 무엇이 다르고 무엇을 사용해야할까?
위에서 OpenJDK 가 아닌 AdoptOpenJDK 설치한 걸 인증했는데요~
JDK 를 제공하는 배급처는 생각보다 다양합니다.
예전에 라인 기술 블로그에서 오픈 JDK 를 적용한 사례를 소개해준 포스팅이 있는데요.
Oracle, OpenJDK, AdoptOpenJDK 뿐 아니라 AZUL, OpenJ9 (IBM) 등도 있습니다.
자세한 내용은 해당 포스팅을 참고해보세요! 실제 글로벌 기업에서 어떻게 버전을 고려하고 있는지 알 수 있어서 좋았습니다.
혼자 공부하는 자바에서는 둘의 차이를 간단하게 설명하고 오라클 JDK 를 설치하는데요~
오라클 JDK 는 오픈 JDK 를 기반으로 만든 것이기에 사용상의 차이점은 거의 없습니다.
개발 및 학습용으로는 둘 다 무료지만 상업용으로 사용할 때 차이점이 있습니다.
바로 오라클 JDK 가 상업용일 때 유료로 라이센스 비용을 지불해야 합니다.
라이선스 비용을 지불하는 만큼 오라클의 LTS (Long Term Support, 장기 지원) 서비스가 지원됩니다.
기술 지원과 버그를 개선한 업데이트 버전을 꾸준하게 받을 수 있습니다.
2023년 1월 기준 Oracle JDK 8 버전의 업데이트 지원 기간은 2030년 12월까지입니다.
Oracle JDK 11 버전 업데이트 지원 기간 : 2026년 9월
Oracle JDK 17 버전 업데이트 지원 기간 : 2024년 9월
Oracle JDK 19 버전 업데이트 지원 기간 : 2023년 3월 ( Oracle JDK 20 으로 대체될 예정 )
Oracle JDK 사이트 : https://www.oracle.com/java/technologies/downloads/
Open JDK 사이트 : https://openjdk.org/
3. 자바 버전 매니저? jenv 는 뭔가요?
jEnv 는 Linux / Mac OS 버전에서 지원하는 자바 버전을 쉽게 관리해주는 CLI 도구입니다.
로컬에 설치된 JDK 의 여러 버전을 등록하여 원할 때 마다 변경할 수 있습니다.
JAVA_HOME 환경 변수또한 사용하고자 하는 버전에 맞게 설정해줍니다.
OpenJDK 뿐 아니라 OracleJDK 또한 설치가 가능합니다.
기본적인 사용법은 위에서 OracleJDK 를 적용할 때와 비슷합니다!
jEnv 홈페이지 : https://www.jenv.be/
jEnv 깃허브 : https://github.com/jenv/jenv
4. 이클립스 설치하기 & 한글 인코딩 이슈 해결하기
혼자 공부하는 자바에선 이클립스를 설명하고 설치합니다. 기업체에서 많이 사용하는 전문 개발 툴이고 플러그인이 이클립스용으로 제작되는 경우가 많습니다. 이클립스 설치는 아래에서 할 수 있습니다.
이클립스 설치 링크 : https://www.eclipse.org/downloads/
운영체제에 맞게 이클립스 다운로드가 가능합니다.
2022-12 R 설치버전부터 macOS, Windows 와 Linux JRE 를 포함한다고 하네요.
저는 macOS 의 ARM 계열 (M1 이상) 이므로 'Download AArch64' 버튼으로 dmg 파일을 다운로드 받아 설치했습니다.
혼공자바를 실습하는 정도라면 첫 번째 'Eclipse IDE for Java Developers' 면 충분합니다.
저는 스프링 공부까지 고려하고 있기에 두 번째 'Eclipse IDE for Enterprise Java and Web Developers' 를 선택했습니다.
JRE 17+ VM 을 같이 설치할 수 있습니다. JRE 18, JRE 19 버전까지 설치할 수 있습니다.
기본으로 설정되어있는 JRE 17 를 선택했습니다.
이클립스 설치 후에 상단 메뉴 File > Open Projects form File System... 버튼을 누릅니다.
예제 소스를 불러와 모든 폴더를 선택하여 불러옵니다.
아래 화면상에선 제가 이미 불러와서 체크가 안되어 있는데, 기본으로 모두 체크되어 있을 겁니다.
그런데 웬 걸..?
영어는 모두 잘 나오지만 한글이 모두 깨져있어요.
예제 소스 작성 시 UTF-8 이 아닌 다른 문자열 인코딩을 사용해서 생긴 오류로 보입니다.
문자열 인코딩은 사용자가 입력한 글자를 데이터로 변환하는 방법인데요.
영어는 모두 1바이트로 표현되지만 그 외의 나라 언어는 여러 문자열 인코딩 방법으로 확장됐습니다.
그래서 한글, 한자, 일본어, 중국어 등의 문자는 나중에 만들어져서 문자열 인코딩 방법이 가지각색인데요.
macOS Eclipse 에선 UTF-8 문자열 인코딩을 기본으로 사용합니다.
한 글자를 보통 1~4 바이트를 사용하고 있습니다.
예제 소스 코드의 인코딩 방식을 조사하다보니 CP949(MS949) 방식을 사용하고 있었습니다.
MS949 방식은 마이크로소프트에서 독자적으로 만든 규격으로 한글 인코딩을 지원합니다.
EUC-KR 을 확장했으며 통합 완성형, 확장 완성형으로도 불립니다.
이클립스에서 프로젝트를 오른쪽 클릭하여 Properties 를 누릅니다.
Text file encoding 에서 Other 옵션을 체크합니다.
MS949 또는 CP949 를 설정하여 Apply 버튼 또는 Apply and Close 버튼을 누릅니다.
프로젝트별 설정 말고 전역적으로 설정하려면 상단 메뉴 Eclipse > Preferences 에서 encoding 을 검색하여
General > Workspace 설정에 들어갑니다.
맨 아래 쪽 Text file encoding 에서 Other 옵션을 체크하여 MS949 를 선택합니다.
그러나 다른 프로젝트를 할 때 호환이 되지 않을 수 있기에 전역으로 변경하는 건 권장하진 않습니다.
윈도우즈의 경우 기본이 MS949 인 경우가 있을텐데, 가급적이면 다른 개발자와의 호환성을 위해
UTF-8 으로 전역 설정하는 걸 추천드립니다.
추가로 프로젝트에 아래처럼 프로젝트에 JRE 라이브러리라 포함이 안되어있다면 실행이 되지 않습니다.
좌측 프로젝트 편집기에서도 알 수 없는 빨간색 x 표시가 쳐져있고 소스 파일에도 빨간색 밑줄이 쳐져있을 겁니다.
맞는 JRE 설정 ( JavaSE-1.8, JavaSE-11 등 ) 을 하여 설정해주면 오류와 밑줄이 없어지고 실행됩니다.
5. 자바 코드를 수정할 때 다른 편집기는 없을까?
5.1. 인텔리제이
자바 IDE, 자바 에디터로 유명한 편집기는 다들 들어보셨을 텐데요.
바로 젯브레인사의 인텔리제이가 있습니다.
자바 개발부터 엔터프라이즈 개발까지 모두 지원합니다.
인텔리제이 설치 링크는 아래에 있습니다.
인텔리제이 설치 링크 : https://www.jetbrains.com/ko-kr/idea/download/
Jetbrains 사에서 학생 지원도 하고 있으므로 참고바랍니다.
Community Edition 버전만으로도 자바 개발부터 스프링 개발까지 모두 가능합니다.
이클립스를 SI 계열 대기업, 중소 기업에서 자체 제작한 플러그인과 함께 많이 사용하고 있지만,
그 외 IT 기업과 스타트업에선 인텔리제이를 더 많이 사용하고 있습니다.
인텔리제이는 이클립스와 다르게 프로젝트 생성 방식이 다릅니다.
Gradle 같은 빌드 도구를 활용해 프로젝트를 생성하고 실행합니다.
엔터프라이즈 서비스 개발 (스프링 프레임워크) 까지 염두해두고 있다면 Gradle 과 같은 빌드 도구와 친숙해지는 게 중요합니다.
이클립스에서 새 프로젝트를 생성할 때 .classpath, .project XML 파일이 생깁니다.
이는 이클립스에서 프로젝트 소스 코드와 클래스 경로를 관리하기 위함인데요.
다른 편집기 (인텔리제이, VSCode 등) 에선 쓰이지 않기에 그대로 보입니다.
인텔리제이에서 java & gradle 생성된 프로젝트 구조는 이클립스의 구조와 다릅니다.
build.gradle, gradlew, gradlew.bat, settings.gradle 파일이 대신 생겼습니다.
Gradle 빌드 도구는 IDE 에 종속되지 않은 도구이기에, Gradle 빌드 도구와 연결된 모든 에디터에서 활용할 수 있습니다.
// build.gradle
plugins {
id 'java'
}
group 'org.example'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}
test {
useJUnitPlatform()
}
// settings.gradle
rootProject.name = 'javaexmple'
build.gradle 은 Groovy 언어로 작성되었고 프로젝트를 구성해줍니다.
빌드할 때 여러 의존성을 저장소 (레포지토리) 에서 다운로드 받아줍니다.
기본으로 구성된 의존성은 테스트 코드를 실행시킬 때 쓰일 JUnit 입니다.
testImplementation, testRunTimeOnly 메서드에 라이브러리명과 버전이 함께 입력됩니다.
빌드 시에 해당 버전에 맞는 라이브러리를 다운로드 받아줍니다.
5.2. VS Code ( Visual Studio Code )
VS Code 는 Microsoft 에서 제공하는 가벼운 에디터입니다.
기존 Atom, Sublime Text, EditPlus 등에 대항하기 위해 출시된 제품입니다.
처음 설치 시에는 에디터 기능밖에 없지만 각종 플러그인으로 사용하고 싶은 언어에 맞게 셋팅할 수가 있습니다.
VS Code 홈페이지 : https://code.visualstudio.com/
VS Code 를 실행한 후 왼쪽 네모 블록들이 있는 아이콘을 선택한 후
Extension Pack for Java 를 검색하여 설치합니다.
VS Code 에서 Java 언어에 대한 코드 네비게이션(클래스 이동), 자동 완성, 리팩터링, 코드 스니펫, 디버깅, JUnit 테스트 실행 등을 지원합니다.
Gradle 을 사용하는 경우 Gradle for Java, 스프링 부트를 사용하려는 경우 Spring initializr Java Support 플러그인을 설치하면 개발이 편해집니다.
자바 소스 코드 파일에서 우측 위에 자바를 실행할 수 있는, 디버그할 수 있는 버튼이 생겼습니다.
Run Java 버튼을 누르면 하단 터미널에서 컴파일 후 실행됩니다.
/usr/bin/env /Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home/bin/java -cp /Users/XXXXXX/Documents/Workspaces/javaexmple/bin/main Hello
$ ~/Documents/Workspaces/javaexmple /usr/bin/env /Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home/
bin/java -cp /Users/XXXXXX/Documents/Workspaces/javaexmple/bin/main Hello
Hello, Java
6. ++i 와 i = i + 1 의 연산 속도 비교 시 바이트 코드를 비교했는데, 어떻게 확인할 수 있을까?
혼공 자바 챕터 03-2 연산자의 종류에서 변수 증감식을 직접 바이트 코드로 추출하여 성능에 우위가 실제로 있는지 확인하는 게 있었는데요~ 바이트 코드를 직접 확인해보려면 어떻게 해야 할까요?
아래 코드는 ++i 와 j = j + 1 코드가 담겼습니다.
클래스 파일로 만들고 클래스 파일을 바이트 코드로 변환한 걸 확인해 보아요.
public class Hello {
public static void main(String[] args) {
int i = 0;
++i;
int j = 0;
j = j + 1;
System.out.println("i value = " + i);
System.out.println("j value = " + j);
}
}
$ javac Hello.java
$ ls
Hello.class Hello.java
$ javap -c Hello
Compiled from "Hello.java"
public class Hello {
public Hello();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_0
1: istore_1
2: iinc 1, 1
5: iconst_0
6: istore_2
7: iload_2
8: iconst_1
9: iadd
10: istore_2
11: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
14: iload_1
15: invokedynamic #3, 0 // InvokeDynamic #0:makeConcatWithConstants:(I)Ljava/lang/String;
20: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
23: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
26: iload_2
27: invokedynamic #5, 0 // InvokeDynamic #1:makeConcatWithConstants:(I)Ljava/lang/String;
32: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
35: return
javac 로 컴파일 한 걸 javap 로 바이트 코드를 보았더니 책에 있는 예제와는 다른 결과가 나왔어요.
둘이 다른 바이트 코드로 변역된 걸 확인할 수가 있었어요.
0: iconst_0
1: istore_1
2: iinc 1, 1
5: iconst_0
6: istore_2
7: iload_2
8: iconst_1
9: iadd
10: istore_2
근데 더 이상한 점은 IDE 에서 만든 바이너리 코드는 다르다는 점인데요.
$ javap -c Hello
Compiled from "Hello.java"
public class Hello {
public Hello();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_0
1: istore_1
2: iinc 1, 1
5: iconst_0
6: istore_2
7: iinc 2, 1
10: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
13: new #22 // class java/lang/StringBuilder
16: dup
17: ldc #24 // String i value =
19: invokespecial #26 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
22: iload_1
23: invokevirtual #29 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
26: invokevirtual #33 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
29: invokevirtual #37 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
32: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
35: new #22 // class java/lang/StringBuilder
38: dup
39: ldc #42 // String j value =
41: invokespecial #26 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
44: iload_2
45: invokevirtual #29 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
48: invokevirtual #33 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
51: invokevirtual #37 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
54: return
}
직접 컴파일 한 것과 다르게 바이트 코드가 똑같이 되어 있습니다.
IDE 에서 컴파일 할 때 무언가 최적화 옵션이 들어가는 걸로 보이네요!
7. if 문과 switch 문은 언제 써야 하는 걸까?
혼공자바 챕터 4에선 조건문을 배웁니다.
조건문으로 if 문과 switch 문이 있습니다.
둘의 차이점은 if 문에선 조건 범위 연산을 비롯한 true 와 false 를 알려주는 다양한 연산으로 조건을 검사할 수 있는 반면에
switch 문은 특정 값 (case) 으로 분기를 처리할 수 있습니다.
그렇다면 둘은 무슨 차이가 있고 언제 쓰는 게 좋을까요?
우선 차이점 부터 살펴봐요.
아래 코드를 한 번 봐볼까요?
public class Hello {
public static void main(String[] args) {
int num = (int) (Math.random() * 6) + 1;
if (num == 1) {
System.out.println("1번이 나왔습니다.");
} else if (num == 2) {
System.out.println("2번이 나왔습니다.");
} else if (num == 3) {
System.out.println("3번이 나왔습니다.");
} else if (num == 4) {
System.out.println("4번이 나왔습니다.");
} else if (num == 5) {
System.out.println("5번이 나왔습니다.");
} else {
System.out.println("6번이 나왔습니다.");
}
System.out.println();
switch (num) {
case 1:
System.out.println("1번이 나왔습니다.");
break;
case 2:
System.out.println("2번이 나왔습니다.");
break;
case 3:
System.out.println("3번이 나왔습니다.");
break;
case 4:
System.out.println("4번이 나왔습니다.");
break;
case 5:
System.out.println("5번이 나왔습니다.");
break;
default:
System.out.println("6번이 나왔습니다.");
break;
}
}
}
if 문은 True, False 를 알려주는 조건식으로 분기를 처리합니다.
조건식이 True 인 경우에만 괄호 { } 안 분기를 실행하고 나머지는 분기 밖으로 빠져나오거나 Else 분기로 빠지게 됩니다.
반면, Switch 문은 변수(num)를 넣어 조건 식이 아닌 특정한 값을 직접 분기 조건으로 설정합니다.
특정한 값 이외는 모두 default 분기로 이동합니다.
둘의 차이는 조건식을 활용하느냐 또는 특정한 값을 코드로 표현하느냐로 보였습니다.
Switch 문은 모두 If 문으로도 표현할 수가 있습니다.
If 문으로도 등등 연산(==) 으로 특정한 값인지 조건식으로 확인할 수가 있는데요.
Switch 문은 불필요한 조건식 없이 바로 어떤 값들이 분기에 처리되는지 확인이 가능합니다.
다음에는 바이트 코드 관점으로도 한 번 봐볼까요?
위 코드를 바이트 코드로 변환했습니다.
$ javap -c Hello
Compiled from "Hello.java"
public class Hello {
public Hello();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: invokestatic #16 // Method java/lang/Math.random:()D
3: ldc2_w #22 // double 6.0d
6: dmul
7: d2i
8: iconst_1
9: iadd
10: istore_1
11: iload_1
12: iconst_1
13: if_icmpne 27
16: getstatic #24 // Field java/lang/System.out:Ljava/io/PrintStream;
19: ldc #30 // String 1번이 나왔습니다.
21: invokevirtual #32 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
24: goto 99
27: iload_1
28: iconst_2
29: if_icmpne 43
32: getstatic #24 // Field java/lang/System.out:Ljava/io/PrintStream;
35: ldc #38 // String 2번이 나왔습니다.
37: invokevirtual #32 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
40: goto 99
43: iload_1
44: iconst_3
45: if_icmpne 59
48: getstatic #24 // Field java/lang/System.out:Ljava/io/PrintStream;
51: ldc #40 // String 3번이 나왔습니다.
53: invokevirtual #32 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
56: goto 99
59: iload_1
60: iconst_4
61: if_icmpne 75
64: getstatic #24 // Field java/lang/System.out:Ljava/io/PrintStream;
67: ldc #42 // String 4번이 나왔습니다.
69: invokevirtual #32 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
72: goto 99
75: iload_1
76: iconst_5
77: if_icmpne 91
80: getstatic #24 // Field java/lang/System.out:Ljava/io/PrintStream;
83: ldc #44 // String 5번이 나왔습니다.
85: invokevirtual #32 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
88: goto 99
91: getstatic #24 // Field java/lang/System.out:Ljava/io/PrintStream;
94: ldc #46 // String 6번이 나왔습니다.
96: invokevirtual #32 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
99: getstatic #24 // Field java/lang/System.out:Ljava/io/PrintStream;
102: invokevirtual #48 // Method java/io/PrintStream.println:()V
105: iload_1
106: tableswitch { // 1 to 5
1: 140
2: 151
3: 162
4: 173
5: 184
default: 195
}
140: getstatic #24 // Field java/lang/System.out:Ljava/io/PrintStream;
143: ldc #30 // String 1번이 나왔습니다.
145: invokevirtual #32 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
148: goto 203
151: getstatic #24 // Field java/lang/System.out:Ljava/io/PrintStream;
154: ldc #38 // String 2번이 나왔습니다.
156: invokevirtual #32 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
159: goto 203
162: getstatic #24 // Field java/lang/System.out:Ljava/io/PrintStream;
165: ldc #40 // String 3번이 나왔습니다.
167: invokevirtual #32 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
170: goto 203
173: getstatic #24 // Field java/lang/System.out:Ljava/io/PrintStream;
176: ldc #42 // String 4번이 나왔습니다.
178: invokevirtual #32 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
181: goto 203
184: getstatic #24 // Field java/lang/System.out:Ljava/io/PrintStream;
187: ldc #44 // String 5번이 나왔습니다.
189: invokevirtual #32 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
192: goto 203
195: getstatic #24 // Field java/lang/System.out:Ljava/io/PrintStream;
198: ldc #46 // String 6번이 나왔습니다.
200: invokevirtual #32 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
203: return
}
바이트 코드 관점으로 보았을 때 if 문은 조건에 맞지 않으면 계속 다음 조건으로 내려갑니다.
해당 조건에 맞는 분기 코드를 실행한 후 모든 분기가 끝난 라인으로 이동합니다. ( goto 99 )
반면 switch 문은 tableswitch 란게 등장을 하네요!
정해진 값에 따라 바로 이동할 수 있는 테이블처럼 보이는데요.
if 문과 다르게 위에서 차례대로 조건을 확인하지 않고 바로 해당하는 분기의 코드로 이동하는 걸 확인할 수 있습니다.
if 문과 switch 문은 어떻게 하면 잘 쓸까요?
분기가 나뉜다는 건 그 만큼 코드를 읽을 때 생각을 많이 하게 됩니다.
if 문은 조건식이 명확해야 이해하기 편합니다.
개발자 업무 중에선 동료 코드를 리뷰하는 시간이 있습니다.
다른 사람의 코드를 보며 의도를 이해하고 고쳐야할 점을 리뷰 해야한다고 가정해보죠!
자세히 알 필요까진 없고 대강 흐름만 파악하면 돼요.
public class Hello {
public static void main(String[] args) {
int studentScore = 95;
System.out.println("studentScore=" + studentScore);
if (studentScore >= 90) {
// ...
System.out.println("Student Score is high!");
} else {
// ...
System.out.println("Student Score is normal.");
}
}
}
public class Hello {
public static void main(String[] args) {
int studentScore = 95;
System.out.println("studentScore=" + studentScore);
final int HIGH_SCORE_MINIMUM = 90;
boolean isStudentScoreHigh = studentScore >= HIGH_SCORE_MINIMUM;
if (isStudentScoreHigh) {
// ...
System.out.println("Student Score is high!");
} else {
// ...
System.out.println("Student Score is normal.");
}
}
}
첫 번째 예시 코드를 읽을 때에 90 이라는 숫자의 의미를 한 번에 알아채기 힘듭니다.
조건식의 좌측 항과 우측 항 두 개를 모두 이해해야하고, 매직 넘버(90)가 있다면 그 의미가 무엇인지 추론해야 합니다.
만약 사용자에게 문자열로 알려주는 System.out.println 메서드가 없었다면, 별도의 주석이 없었다면
왜 90 이상인지 확인하는 조건식을 만들었는지, 정말 90 이라는 숫자를 쓰는게 올바른지 이해가 안 갈 수 있습니다.
두 번째 예시 코드는 조건식에 이름을 부여했습니다.
boolean 변수로 조건식에 이름을 짓고 if 문의 조건식으로 사용했습니다.
추가로 90 이라는 숫자에 상수로 이름을 부여했습니다.
isStudentScoreHigh 이름으로 학생의 점수가 높은지 검증하는 구나, 하고 넘어갈 수도 있습니다.
조건식이 궁금하다면 변수에 어떤 조건식이 있는지, 변수 정의로 이동하여 조건식을 확인할 수 있습니다.
studentScore 가 최고 점수 경계를 넘는지 확인하는 조건식임을 확인할 수 있습니다.
구체적인 최고 점수 경계를 보고 싶다면, 정의로 이동하여 90 이라는 값임을 확인할 수 있습니다.
이전 예시와는 다르게 이름으로 먼저 이해를 한 다음에, 궁금하면 실제 값을 확인해보는 쪽으로 눈을 이동시킬 수 있습니다.
switch 문 또한 분기 처리되어야 하는 변수가 어떤 변수인지 알기 쉽고
각 case 에 적은 특정한 값들도 값이 아닌 이름을 부여한다면 이해하기가 쉽습니다.
public class Hello {
public static void main(String[] args) {
int dir = 0;
switch (dir) {
case 0:
// ...
break;
case 1:
// ...
break;
case 2:
// ...
break;
case 3:
// ...
break;
default:
// ...
}
}
}
public class Hello {
enum Direction {
NORTH, SOUTH, WEST, EAST
};
public static void main(String[] args) {
Direction inputDirection = Direction.NORTH;
switch (inputDirection) {
case NORTH:
// ...
break;
case SOUTH:
// ...
break;
case WEST:
// ...
break;
case EAST:
// ...
break;
}
}
}
첫 번째 예시는 dir 짧은 정수형 변수를 정의합니다.
switch 문으로 0, 1, 2, 3 값으로 분기를 처리하고 있습니다.
dir 변수가 무엇인지 생각해야하고 0 ~ 3 값이 무엇인지 각 분기마다 확실하게 확인해줘야 합니다.
해당 값이 가지는 분기 로직이 정말 올바른지 비교해야하기 때문이죠.
설령 방향이란 걸 알았다고 해도 누구는 순서를 상하좌우를 떠올릴수도, 동서남북을 떠올릴 수 있기 때문이죠.
두 번째 예시는 열거 타입을 활용하여 switch 문의 레이블로 분기 처리했습니다.
어떤 값으로 분기가 되는지 이름이 부여되어서 이해하기 한결 쉬워졌습니다.
요약하자면 이름을 가진 공간은 더욱 이름이 명확한지 재점검을 하고, 이름이 없는 데이터는 명확한 이름을 부여하면 되는 거였습니다!
8. null 관리를 어떻게 해야할까?
값이 null 인 객체를 참조하면 NullPointerException 이 발생됩니다.
컴파일 단에서 발견되지 않는다는 문제점이 있습니다.
그렇다면 null 관리를 어떻게 하면 잘 하는 걸까요?
결론부터 말하자면 최대한 null 을 쓰지 않는 걸 권장합니다.
null 데이터가 와도 자연스레 예외 처리가 되도록 코드를 작성합니다.
또는 논리적으로 null 데이터가 들어올 수 있지만 들어오지 않는 것처럼 가정하고, null 일 가능성이 있는 걸 다른 메서드로 보내지 않습니다.
예상치 못한 null 데이터가 오면 아싸리 오류를 빨리 눈치챌 수 있도록 애플리케이션을 종료시키는 법도 있습니다.
아래 참고자료를 이용했습니다.
https://www.baeldung.com/java-avoid-null-check
첫 번째 방법은 정적 코드 분석 도구를 사용합니다. FindBugs 라이브러리를 활용하여 @Nullable, @NonNull 어노테이션으로 매개변수를 관리합니다.
public void accept(@NonNull Object param) {
System.out.println(param.toString());
}
accept 메서드를 호출하는 코드 중에 Null 을 보낼 수 있는지 컴파일 타임에 FindBugs 가 확인하여 경고를 알려줄 수 있습니다.
두 번째 방법은 assert 검증을 이용합니다. 검증 예외를 활성화하면 AssertionError 예외를 일으킬 수 있습니다.
public void accept(Object param){
assert param != null;
doSomething(param);
}
다만 JVM 에서 assert 기능이 비활성화될 수 있고 Unchecked Exception 을 일으킬 수 있기에 추천하는 방법은 아닙니다.
세 번째 방법은 메서드 도입 부분에 null 체크를 합니다.
실패 조건을 제일 먼저 작성하여 메서드를 읽을 때 실패 조건을 배제할 수 있도록 합니다.
public void goodAccept(String one, String two, String three) {
if (one == null || two == null || three == null) {
throw new IllegalArgumentException();
}
process(one);
process(two);
process(three);
}
public void badAccept(String one, String two, String three) {
if (one == null) {
throw new IllegalArgumentException();
} else {
process(one);
}
if (two == null) {
throw new IllegalArgumentException();
} else {
process(two);
}
if (three == null) {
throw new IllegalArgumentException();
} else {
process(three);
}
}
네 번째 방법은 랩핑 클래스 대신 기본 자료형을 사용합니다.
기본 자료형에 null 이 들어간다면 컴파일 단에서 오류를 발생합니다.
그러나 랩핑 클래스를 사용하는 매개변수에 null 을 전달한다면 컴파일 단에서 발견하지 못합니다.
public static int primitiveSum(int a, int b) {
return a + b;
}
public static Integer wrapperSum(Integer a, Integer b) {
return a + b;
}
// client code
int sum = primitiveSum(null, 2);
다섯 번째 방법은 없는 배열, 컬렉션을 반환할 때 null 대신 빈 배열, 빈 컬렉션을 사용합니다.
public List<String> names() {
if (userExists()) {
return Stream.of(readName()).collect(Collectors.toList());
} else {
return Collections.emptyList();
}
}
public String[] names() {
if (userExists()) {
return readNames()
} else {
return []
}
}
9. 학습 스케쥴 공유
학습 스케쥴을 직접 짜보려고 합니다!
다시 혼공자바 진도를 확인해 봅시다.
혼공학습단 9기 혼공자바 2주차는 클래스, 상속, 인터페이스, 중첩 클래스 & 중첩 인터페이스 중심입니다.
클래스, 상속, 인터페이스, 중첩 클래스 & 중첩 인터페이스를 구현했을 때 바이트 코드로 변환하면 각 영역에서 어떻게 표시되는지 확인해 보고자 합니다. 머릿 속으로는 이해가 가지만 JVM 상에서 어떻게 표현되는지 궁금하더라고요!
또 객체지향을 어떻게 하면 잘 쓸 수 있는지 공부해 보려고 합니다.
혼공학습단 9기 혼공자바 3주차는 예외 처리, 기본 API 클래스입니다.
예외에서 무엇이 Checked Exception 인지, Unchecked Exception 인지 구분 하는게 중요한 것 같더라고요.
스프링 프레임워크에서도 동작이 달라지는 게 있어서 한 번 쯤은 정리해보려고 합니다.
또 예외 처리를 메서드 안에서 할 지, 또는 메서드 밖으로 던질지는 어떤 기준으로 결정하면 좋을지도 궁금합니다!
혼공학습단 9기 혼공자바 4주차는 스레드입니다. 멀티 스레드와 스레드 제어를 배우게 되는데요.
스레드 간 동기화는 어떻게 하는지, 스레드에 안전한 프로그래밍은 어떻게 해야 하는지, 많은 스레드를 어떻게 효율적으로 관리할 수 있는지 공부해보려고 합니다.
각 스레드마다 어떤 자원을 사용하고 있는지 어떻게 모니터링해야 하며 덤프를 어떻게 출력할 수 있을지, 스레드 교착 상태를 어떻게 분석하면 좋을지 궁금합니다.
자바 언어에서의 비동기 처리는 어떻게 하면 좋을지도 궁금합니다.
자바 면접 질문으로 많이 나오는 것 같은데 어떤 면접 질문이 있을지도 정리해보려고 합니다.
혼공학습단 9기 혼공자바 5주차는 컬렉션 프레임워크입니다.
자주 쓰이는 컬렉션 프레임워크 사용법과 언제 어떨 때 써야하는지, 조심해야할 점은 무엇인지 공부해보려고 합니다.
Iterator 가 무엇인지 스트림이 무엇인지도 조사해보려고 합니다.
스레드와 마찬가지로 자바 면접 단골 질문으로 어떤 면접 질문이 있을지도 정리해보려고 합니다.
혼공학습단 9기 혼공자바 6주차는 입출력 스트림입니다.
기본적인 스트림 종류를 알아보고 어떻게 쓰이는지 정리하고자 합니다.
콘솔, 파일과 연결된 스트림 외에 어떤 장치와 연결될 수 있는지, 네트워크 통신은 어떻게 되는지도 궁금합니다.
자바 버전이 올라가며 또 새로 추가된 스트림은 어떤 게 있는지도 조사해보려고 합니다.
다음 주 혼공학습단 9기 포스팅도 기대해주세요!
혼공학습단 9기 다른 글 보러가기
2023.01.08 - [자유/대외 활동] - [혼공학습단 #1] 한빛미디어 혼공학습단 9기 선정
'자유 > 대외 활동' 카테고리의 다른 글
[혼공학습단 9기] #4. 3주차 예외, 200% 이해하기 (2) | 2023.01.17 |
---|---|
[혼공학습단 9기] #3. 2주차 객체 지향 프로그래밍을 왜 쓸까? (0) | 2023.01.10 |
[혼공학습단 #1] 한빛미디어 혼공학습단 9기 선정 (4) | 2023.01.08 |
대학생 IT 연합 동아리 총정리 (23년 1월 기준, 데이터 분석과 머신러닝 동아리 추가) (5) | 2023.01.07 |
한빛미디어 나는 리뷰어다 2022 도서 서평단 2월 도서 신청 후기 (0) | 2022.02.04 |
댓글