| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- 프로그래머스
- 알고리즘
- Gradle
- Unity
- code blocks
- 연습문제
- 플러스 백엔드
- Kotlin
- Spring
- 아키텍처
- Java
- stack
- jre
- 백준
- MSA
- redis
- 탐색
- 오블완
- Kafka
- docker
- 삽입
- EDA
- 이진트리
- jdk
- JPA
- 코딩테스트
- bean
- 트리
- 티스토리챌린지
- event
- Today
- Total
Repository
1단계: Java 기초 다지기 본문
1단계: Java 기초 다지기
4년간 Java를 실무에서 사용하며 얻은 경험과 통찰을 담아, 단순히 문법을 넘어 '왜'와 '어떻게'를 깊이 있게 다루겠습니다.
1.1 Java 개발 환경 구축
JDK 설치와 환경 변수 설정
JDK vs JRE: 개발자가 알아야 할 핵심
JRE (Java Runtime Environment)
- Java 애플리케이션을 실행하기 위한 최소 환경
- 구성요소: JVM + 핵심 라이브러리 (java.lang, java.util 등)
- 컴파일러(javac)가 없어 개발 불가능
- 최종 사용자가 설치하는 환경
JDK (Java Development Kit)
- Java 애플리케이션을 개발하기 위한 전체 패키지
- 구성요소: JRE + 개발 도구 (javac, javadoc, jar, jdb 등)
- 실무 관점: JDK 11+ 부터는 JRE가 별도 배포되지 않음
# JDK 설치 확인
java -version
javac -version
# 출력 예시
java version "21.0.1" 2023-10-17 LTS
Java(TM) SE Runtime Environment (build 21.0.1+12-LTS-29)
Java HotSpot(TM) 64-Bit Server VM (build 21.0.1+12-LTS-29, mixed mode, sharing)
환경 변수 설정의 중요성
JAVA_HOME
# macOS/Linux
export JAVA_HOME=$(/usr/libexec/java_home -v 21)
# Windows
setx JAVA_HOME "C:\Program Files\Java\jdk-21"
왜 JAVA_HOME이 필요한가?
- Maven, Gradle 등 빌드 도구가 참조
- Tomcat, Spring Boot 같은 서버가 참조
- IDE가 프로젝트별 JDK 버전 관리에 사용
- 스크립트에서 일관된 Java 경로 사용 가능
PATH 설정
# macOS/Linux (.zshrc 또는 .bash_profile)
export PATH=$JAVA_HOME/bin:$PATH
# Windows
setx PATH "%JAVA_HOME%\bin;%PATH%"
멀티 JDK 버전 관리
# SDKMAN 사용 (권장)
sdk install java 21.0.1-oracle
sdk install java 17.0.9-tem
sdk use java 21.0.1-oracle
# jEnv 사용 (macOS/Linux)
jenv add /Library/Java/JavaVirtualMachines/jdk-21.jdk/Contents/Home
jenv global 21
jenv local 17 # 프로젝트별 설정
IntelliJ IDEA 설정
- Project Structure 설정
```
File → Project Structure → Project
- SDK: 21
- Language Level: 21 (Preview features enabled)
- Maven/Gradle 빌드 도구 JDK 설정
```
Settings → Build, Execution, Deployment → Build Tools → Maven
- JDK for importer: Project SDK
- Maven home: Bundled (3.9.5)
- 실무 권장 플러그인
- SonarLint: 코드 품질 실시간 검사
- JRebel: Hot reload (상용)
- Key Promoter X: 단축키 학습
첫 Java 프로그램과 실행 원리
Hello World - 단순해 보이지만 깊이가 있다
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
각 키워드의 의미
- public: 접근 제어자, 어디서든 접근 가능
- class: Java는 모든 코드가 클래스 안에 존재
- static: 객체 생성 없이 호출 가능 (JVM이 직접 호출)
- void: 반환값 없음
- main: JVM이 찾는 프로그램 진입점 (Entry Point)
- String[] args: 커맨드라인 인자 배열
컴파일과 실행 과정의 내부
# 1. 컴파일: .java → .class (바이트코드)
javac HelloWorld.java
# 2. 실행: 바이트코드를 JVM이 해석
java HelloWorld
상세 프로세스
[1] Source Code (.java)
↓ javac (Java Compiler)
[2] Bytecode (.class)
↓ java (JVM)
[3] Class Loader → 클래스를 메모리에 로드
↓
[4] Bytecode Verifier → 보안 검증
↓
[5] JIT Compiler → 런타임 최적화
↓
[6] Execution Engine → 실제 실행.class 파일의 비밀
# 바이트코드 디스어셈블
javap -c HelloWorld.class
출력 결과
Compiled from "HelloWorld.java"
public class HelloWorld {
public HelloWorld();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String Hello, World!
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}핵심 이해
getstatic: System.out 필드를 스택에 로드ldc: 상수 풀에서 문자열 로드invokevirtual: println 메서드 호출
JIT 컴파일러의 마법
public class JITExample {
public static void main(String[] args) {
long start = System.nanoTime();
// 첫 10,000번 반복 (Interpreter 모드)
for (int i = 0; i < 10000; i++) {
compute();
}
long interpreted = System.nanoTime() - start;
// 다음 10,000번 반복 (JIT 컴파일 후)
start = System.nanoTime();
for (int i = 0; i < 10000; i++) {
compute();
}
long compiled = System.nanoTime() - start;
System.out.printf("Interpreted: %d ns\n", interpreted);
System.out.printf("JIT Compiled: %d ns\n", compiled);
System.out.printf("Speedup: %.2fx\n", (double)interpreted / compiled);
}
private static int compute() {
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
}
return sum;
}
}
// 실행 결과 예시
// Interpreted: 856230 ns
// JIT Compiled: 142045 ns
// Speedup: 6.03x
JVM 옵션 실무 활용
# 바이트코드 검증 비활성화 (신뢰 환경에서 성능 향상)
java -noverify HelloWorld
# JIT 컴파일 임계값 조정
java -XX:CompileThreshold=5000 HelloWorld
# 컴파일 로그 확인
java -XX:+PrintCompilation HelloWorld
# 인라인 최적화 깊이 설정
java -XX:MaxInlineLevel=15 HelloWorld
1.2 데이터 타입과 연산자
Java의 기본 자료형 완벽 가이드
Primitive vs Reference Type: 메모리와 성능의 차이
Primitive Type (8가지)
| 타입 | 크기 | 범위 | 기본값 | 용도 |
|---|---|---|---|---|
| byte | 1 byte | -128 ~ 127 | 0 | 파일 I/O, 네트워크 통신 |
| short | 2 bytes | -32,768 ~ 32,767 | 0 | 메모리 절약 필요시 |
| int | 4 bytes | -2³¹ ~ 2³¹-1 | 0 | 기본 정수형 |
| long | 8 bytes | -2⁶³ ~ 2⁶³-1 | 0L | 타임스탬프, 큰 수 |
| float | 4 bytes | IEEE 754 | 0.0f | 과학 계산 (정밀도 낮음) |
| double | 8 bytes | IEEE 754 | 0.0d | 기본 실수형 |
| char | 2 bytes | 0 ~ 65,535 | '\u0000' | 유니코드 문자 |
| boolean | 1 bit* | true/false | false | 논리값 |
*실제 메모리는 JVM 구현에 따라 다름 (보통 1 byte)
메모리 배치 비교
class MemoryComparison {
// Primitive: Stack에 직접 저장
int primitiveInt = 42; // 4 bytes
// Reference: Heap에 객체 생성, Stack에는 참조만 저장
Integer wrapperInt = 42; // 16 bytes (객체 헤더 12 + int 4)
}
대용량 데이터 처리
public class PerformanceTest {
public static void main(String[] args) {
int size = 10_000_000;
// Primitive 배열
long start = System.currentTimeMillis();
int[] primitiveArray = new int[size];
for (int i = 0; i < size; i++) {
primitiveArray[i] = i;
}
long primitiveTime = System.currentTimeMillis() - start;
// Wrapper 배열
start = System.currentTimeMillis();
Integer[] wrapperArray = new Integer[size];
for (int i = 0; i < size; i++) {
wrapperArray[i] = i; // Auto-boxing 발생
}
long wrapperTime = System.currentTimeMillis() - start;
System.out.println("Primitive: " + primitiveTime + "ms");
System.out.println("Wrapper: " + wrapperTime + "ms");
System.out.println("Memory diff: " +
(wrapperArray.length * 12L) + " bytes overhead");
}
}
// 실행 결과 예시
// Primitive: 45ms
// Wrapper: 234ms (5x 느림)
// Memory diff: 120,000,000 bytes overhead
Wrapper 클래스의 필요성
1. 컬렉션 프레임워크 사용
// ❌ 불가능: Primitive는 Generic 타입으로 사용 불가
List<int> numbers = new ArrayList<>();
// ✅ 가능
List<Integer> numbers = new ArrayList<>();
numbers.add(42); // Auto-boxing
2. null 표현 가능
// DB에서 null 허용 컬럼 처리
Integer optionalValue = getValueFromDB(); // null 가능
if (optionalValue != null) {
int value = optionalValue; // Auto-unboxing
}
3. 유틸리티 메서드 제공
// 문자열 변환
String binary = Integer.toBinaryString(42); // "101010"
String hex = Integer.toHexString(255); // "ff"
// 파싱
int parsed = Integer.parseInt("123");
Integer safe = Integer.valueOf("456"); // 캐싱 사용
// 비교
Integer.compare(10, 20); // -1
Integer.max(10, 20); // 20
Auto-boxing / Unboxing의 함정
Case 1: 성능 이슈
public class BoxingTrap {
public static void main(String[] args) {
// ❌ 나쁜 예: 반복문에서 Auto-boxing
Long sum = 0L;
for (long i = 0; i < 1_000_000; i++) {
sum += i; // 매 반복마다 Long 객체 생성!
}
// ✅ 좋은 예
long sum2 = 0L;
for (long i = 0; i < 1_000_000; i++) {
sum2 += i;
}
}
}
Case 2: NullPointerException
public class NPETrap {
private Integer count; // 기본값 null
public void increment() {
count++; // NullPointerException! (null을 unboxing 시도)
}
// 해결책
public void incrementSafe() {
if (count == null) count = 0;
count++;
}
}
Case 3: 캐싱 동작 이해
public class CachingBehavior {
public static void main(String[] args) {
// -128 ~ 127 범위는 캐싱됨
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true (같은 객체)
// 범위 밖은 새 객체 생성
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false (다른 객체)
// ✅ 항상 equals() 사용
System.out.println(c.equals(d)); // true
}
}
베스트 프랙티스
public class BestPractices {
// 1. 컬렉션 순회시 Primitive 선호
public long sumList(List<Integer> numbers) {
long sum = 0;
for (int num : numbers) { // Auto-unboxing 1번만
sum += num;
}
return sum;
}
// 2. Stream에서는 전용 타입 사용
public int sumWithStream(List<Integer> numbers) {
return numbers.stream()
.mapToInt(Integer::intValue) // IntStream으로 변환
.sum();
}
// 3. null 체크 패턴
public int safeConvert(Integer value) {
return value != null ? value : 0;
}
// 4. Optional 활용
public Optional<Integer> findValue() {
return Optional.ofNullable(someNullableInteger);
}
}
문자열(String) 깊이 파헤치기
String의 불변성(Immutability): 설계 철학
public final class String {
private final char[] value; // Java 8
// private final byte[] value; // Java 9+ (Compact Strings)
// 모든 메서드가 새로운 String 반환
public String toUpperCase() {
// 내부 char[] 수정 불가, 새 String 생성
}
}
불변성의 이점
- Thread-Safety: 동기화 없이 멀티스레드 안전
- String Pool 사용 가능: 메모리 절약
- 해시코드 캐싱: HashMap 성능 향상
- 보안: 패스워드, URL 등 변조 방지
public class ImmutabilityBenefits {
// 1. Thread-Safe
private static final String SHARED = "공유 문자열";
public void multiThreadAccess() {
// 여러 스레드가 동시에 읽어도 안전
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
executor.submit(() -> System.out.println(SHARED));
}
}
// 2. 해시코드 캐싱
public void hashCodeCaching() {
String key = "cache-key";
int hash1 = key.hashCode(); // 계산 후 캐싱
int hash2 = key.hashCode(); // 캐시된 값 반환 (빠름)
}
// 3. 보안 예제
public void securityExample(String password) {
// String은 메모리에 남아 GC 전까지 노출 위험
// 민감 데이터는 char[] 사용 권장
char[] passwordChars = password.toCharArray();
try {
// 사용
authenticate(passwordChars);
} finally {
Arrays.fill(passwordChars, '0'); // 명시적 초기화
}
}
}
String vs StringBuilder vs StringBuffer
성능 비교 실험
public class StringPerformanceTest {
private static final int ITERATIONS = 100_000;
public static void main(String[] args) {
// 1. String 연결 (최악)
long start = System.currentTimeMillis();
String str = "";
for (int i = 0; i < ITERATIONS; i++) {
str += i; // 매번 새 String 객체 생성!
}
long stringTime = System.currentTimeMillis() - start;
// 2. StringBuilder (빠름, 단일 스레드)
start = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < ITERATIONS; i++) {
sb.append(i);
}
String result2 = sb.toString();
long sbTime = System.currentTimeMillis() - start;
// 3. StringBuffer (빠름, 멀티스레드 안전)
start = System.currentTimeMillis();
StringBuffer sbf = new StringBuffer();
for (int i = 0; i < ITERATIONS; i++) {
sbf.append(i);
}
String result3 = sbf.toString();
long sbfTime = System.currentTimeMillis() - start;
System.out.printf("String: %dms\n", stringTime); // ~25000ms
System.out.printf("StringBuilder: %dms\n", sbTime); // ~15ms
System.out.printf("StringBuffer: %dms\n", sbfTime); // ~20ms
}
}
언제 무엇을 사용할까?
| 상황 | 추천 | 이유 |
|---|---|---|
| 단순 연결 (몇 개) | String | 컴파일러가 최적화 |
| 반복문 내 연결 | StringBuilder | 성능 최적 |
| 멀티스레드 공유 | StringBuffer | Thread-safe |
| 문자열 조작 없음 | String | 불변성 이점 |
예제
public class StringBuilderPatterns {
// 1. JSON 생성
public String buildJson(User user) {
return new StringBuilder(256) // 초기 용량 지정 (중요!)
.append("{")
.append("\"id\":").append(user.getId()).append(",")
.append("\"name\":\"").append(user.getName()).append("\",")
.append("\"email\":\"").append(user.getEmail()).append("\"")
.append("}")
.toString();
}
// 2. SQL 쿼리 빌더
public String buildQuery(QueryParams params) {
StringBuilder sql = new StringBuilder("SELECT * FROM users WHERE 1=1");
if (params.getName() != null) {
sql.append(" AND name LIKE '%")
.append(sanitize(params.getName()))
.append("%'");
}
if (params.getAge() != null) {
sql.append(" AND age = ").append(params.getAge());
}
return sql.toString();
}
// 3. 로그 메시지 생성
public void logMessage(String level, String message, Object... args) {
StringBuilder log = new StringBuilder(128)
.append("[").append(level).append("] ")
.append(LocalDateTime.now()).append(" - ")
.append(String.format(message, args));
System.out.println(log);
}
}
String Pool과 메모리 관리
String Pool의 동작 원리
public class StringPoolDemo {
public static void main(String[] args) {
// 리터럴: String Pool에 저장
String s1 = "Hello";
String s2 = "Hello";
System.out.println(s1 == s2); // true (같은 객체)
// new: Heap에 새 객체 생성
String s3 = new String("Hello");
System.out.println(s1 == s3); // false (다른 객체)
// intern(): 강제로 String Pool 사용
String s4 = s3.intern();
System.out.println(s1 == s4); // true (Pool의 같은 객체)
}
}
메모리 관점
[String Pool (Metaspace/Heap)]
┌─────────────┐
│ "Hello" │ ← s1, s2, s4 참조
└─────────────┘
[Heap]
┌─────────────┐
│ "Hello" │ ← s3 참조 (별도 객체)
└─────────────┘Java 9+ Compact Strings 최적화
// Java 8 이전: 모든 문자를 char(2 bytes)로 저장
String ascii = "Hello"; // 10 bytes (5 chars × 2)
// Java 9+: Latin-1이면 byte(1 byte)로 저장
String ascii = "Hello"; // 5 bytes (5 chars × 1) + 1 flag byte
String unicode = "안녕"; // 4 bytes (2 chars × 2) + 1 flag byte
String 메모리 최적화
public class StringOptimization {
// 1. 큰 문자열 부분 복사시 주의
public String extractBad(String largeString) {
// ❌ largeString 전체를 메모리에 유지
return largeString.substring(0, 10); // Java 6
}
public String extractGood(String largeString) {
// ✅ Java 7+ substring은 새 char[] 생성 (안전)
return largeString.substring(0, 10);
// 명시적 복사 (더 안전)
return new String(largeString.substring(0, 10));
}
// 2. String Pool 사이즈 튜닝
// JVM 옵션: -XX:StringTableSize=1000003 (소수 권장)
// 3. G1GC String Deduplication (Java 8u20+)
// JVM 옵션: -XX:+UseG1GC -XX:+UseStringDeduplication
public void demonstrateDeduplication() {
// 같은 내용이지만 다른 객체
String s1 = new String("duplicate");
String s2 = new String("duplicate");
// G1GC가 자동으로 중복 제거
System.gc();
// 이후 s1, s2가 같은 char[] 공유
}
}
실전 문자열 처리 팁
public class StringPracticalTips {
// 1. 효율적인 문자열 검사
public boolean isEmpty(String str) {
return str == null || str.isEmpty(); // length() == 0
}
public boolean isBlank(String str) {
return str == null || str.isBlank(); // Java 11+
}
// 2. 문자열 조인 (Java 8+)
public String joinWithStream(List<String> items) {
return items.stream()
.collect(Collectors.joining(", ", "[", "]"));
// 결과: [item1, item2, item3]
}
public String joinWithString(List<String> items) {
return String.join(", ", items); // 더 빠름
}
// 3. 문자열 분할 최적화
public String[] splitEfficient(String input) {
// ❌ 정규식 사용 (느림)
return input.split(",");
// ✅ 단순 구분자는 리터럴 사용
// (Pattern.compile() 캐싱됨)
}
public List<String> splitWithStream(String input) {
// Java 8+ Pattern.splitAsStream()
return Pattern.compile(",")
.splitAsStream(input)
.map(String::trim)
.collect(Collectors.toList());
}
// 4. 문자열 포매팅 비교
public void formattingComparison() {
String name = "John";
int age = 30;
// 연결 (간단한 경우)
String s1 = "Name: " + name + ", Age: " + age;
// String.format (가독성)
String s2 = String.format("Name: %s, Age: %d", name, age);
// MessageFormat (국제화)
String s3 = MessageFormat.format("Name: {0}, Age: {1}", name, age);
// Text Blocks (Java 15+)
String s4 = """
Name: %s
Age: %d
""".formatted(name, age);
}
// 5. 문자열 비교 최적화
public boolean compareOptimized(String s1, String s2) {
// 1. 참조 비교 (빠름)
if (s1 == s2) return true;
// 2. null 체크
if (s1 == null || s2 == null) return false;
// 3. 길이 비교 (빠름)
if (s1.length() != s2.length()) return false;
// 4. 내용 비교
return s1.equals(s2);
}
// 6. 대소문자 무시 비교
public boolean equalsIgnoreCaseSafe(String s1, String s2) {
// ❌ 로케일 의존적
return s1.toLowerCase().equals(s2.toLowerCase());
// ✅ 로케일 독립적
return s1.equalsIgnoreCase(s2);
}
// 7. 문자열 인코딩 처리
public byte[] encodeUTF8(String str) {
return str.getBytes(StandardCharsets.UTF_8); // Java 7+
}
public String decodeUTF8(byte[] bytes) {
return new String(bytes, StandardCharsets.UTF_8);
}
// 8. 정규식 패턴 재사용
private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$");
public boolean isValidEmail(String email) {
return EMAIL_PATTERN.matcher(email).matches();
}
}
성능 체크리스트
public class PerformanceChecklist {
// ✅ DO
StringBuilder sb = new StringBuilder(capacity); // 초기 용량 지정
String.join(",", list); // join 사용
str.isEmpty() // 길이 체크
PATTERN.matcher(str) // 패턴 재사용
// ❌ DON'T
for (String s : list) result += s + ","; // 반복문 연결
str.split(",")[0] // 전체 분할 후 1개만 사용
str.equals("") // isEmpty() 사용
Pattern.compile(regex).matcher(str) // 매번 컴파일
}
1.3 제어문과 반복문
반복문 성능 비교: for vs while vs Stream
각 반복문의 특징과 내부 동작
1. 전통적 for 루프
for (int i = 0; i < array.length; i++) {
process(array[i]);
}
특징:
- 인덱스 직접 접근
- 가장 낮은 오버헤드
- 역순, 스킵 등 세밀한 제어 가능
바이트코드:
0: iconst_0 // i = 0
1: istore_1 // store i
2: iload_1 // load i
3: aload_0 // load array
4: arraylength // get length
5: if_icmpge 20 // i >= length? goto 20
8: aload_0 // load array
9: iload_1 // load i
10: aaload // array[i]
11: invokestatic #2 // process()
14: iinc 1, 1 // i++
17: goto 2 // loop
20: return2. Enhanced for (for-each)
for (String item : list) {
process(item);
}
특징:
- 가독성 최고
- Iterator 사용 (내부적으로)
- 인덱스 접근 불가
내부 변환:
// 컴파일러가 이렇게 변환
Iterator<String> iter = list.iterator();
while (iter.hasNext()) {
String item = iter.next();
process(item);
}
3. while 루프
int i = 0;
while (i < array.length) {
process(array[i]);
i++;
}
특징:
- 조건 기반 반복
- 무한 루프 구현 용이
- for와 성능 동일
4. Stream API (Java 8+)
Arrays.stream(array).forEach(this::process);
특징:
- 함수형 스타일
- 병렬 처리 쉬움
- Lazy Evaluation
- 약간의 오버헤드
성능 벤치마크 (JMH)
import org.openjdk.jmh.annotations.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.*;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 10, time = 1)
@Fork(1)
public class LoopBenchmark {
private int[] primitiveArray;
private List<Integer> list;
private static final int SIZE = 1000;
@Setup
public void setup() {
primitiveArray = new int[SIZE];
list = new ArrayList<>(SIZE);
for (int i = 0; i < SIZE; i++) {
primitiveArray[i] = i;
list.add(i);
}
}
@Benchmark
public long traditionalFor() {
long sum = 0;
for (int i = 0; i < primitiveArray.length; i++) {
sum += primitiveArray[i];
}
return sum;
}
@Benchmark
public long enhancedFor() {
long sum = 0;
for (int num : primitiveArray) {
sum += num;
}
return sum;
}
@Benchmark
public long whileLoop() {
long sum = 0;
int i = 0;
while (i < primitiveArray.length) {
sum += primitiveArray[i++];
}
return sum;
}
@Benchmark
public long stream() {
return Arrays.stream(primitiveArray)
.mapToLong(i -> i)
.sum();
}
@Benchmark
public long parallelStream() {
return Arrays.stream(primitiveArray)
.parallel()
.mapToLong(i -> i)
.sum();
}
@Benchmark
public long iteratorLoop() {
long sum = 0;
Iterator<Integer> iter = list.iterator();
while (iter.hasNext()) {
sum += iter.next();
}
return sum;
}
}
// 실행 결과 (예시, 환경마다 다름)
// Benchmark Mode Cnt Score Error Units
// traditionalFor avgt 10 450.2 ± 12.3 ns/op
// enhancedFor avgt 10 452.1 ± 10.8 ns/op
// whileLoop avgt 10 451.5 ± 11.2 ns/op
// stream avgt 10 890.3 ± 25.6 ns/op
// parallelStream avgt 10 2100.5 ± 85.3 ns/op (작은 데이터)
// iteratorLoop avgt 10 780.4 ± 18.9 ns/op
대용량 데이터 벤치마크 (10,000,000 요소)
@Param({"10000000"})
private int size;
// 결과
// traditionalFor avgt 10 8.2 ± 0.3 ms/op
// stream avgt 10 15.1 ± 0.8 ms/op
// parallelStream avgt 10 3.5 ± 0.2 ms/op (병렬 처리 빛남)
상황별 최적의 선택
시나리오 1: 배열 순회 (읽기만)
public class ReadOnlyIteration {
// ✅ 최선: Enhanced for (가독성)
public void bestChoice(String[] array) {
for (String item : array) {
System.out.println(item);
}
}
// 대안: Traditional for (인덱스 필요시)
public void withIndex(String[] array) {
for (int i = 0; i < array.length; i++) {
System.out.println(i + ": " + array[i]);
}
}
}
시나리오 2: 리스트 요소 제거
public class RemoveElements {
// ❌ ConcurrentModificationException
public void wrong(List<String> list) {
for (String item : list) {
if (item.startsWith("remove")) {
list.remove(item); // Exception!
}
}
}
// ✅ Iterator 사용
public void correct(List<String> list) {
Iterator<String> iter = list.iterator();
while (iter.hasNext()) {
String item = iter.next();
if (item.startsWith("remove")) {
iter.remove(); // 안전
}
}
}
// ✅ Java 8+ removeIf
public void modern(List<String> list) {
list.removeIf(item -> item.startsWith("remove"));
}
}
시나리오 3: 필터링 & 변환
public class FilterAndTransform {
// 전통적 방식
public List<String> traditional(List<User> users) {
List<String> result = new ArrayList<>();
for (User user : users) {
if (user.getAge() >= 18) {
result.add(user.getName().toUpperCase());
}
}
return result;
}
// ✅ Stream (가독성, 유지보수성)
public List<String> withStream(List<User> users) {
return users.stream()
.filter(user -> user.getAge() >= 18)
.map(User::getName)
.map(String::toUpperCase)
.collect(Collectors.toList());
}
}
시나리오 4: 대용량 데이터 병렬 처리
public class ParallelProcessing {
// 순차 처리
public long sequentialSum(int[] data) {
return Arrays.stream(data)
.mapToLong(i -> expensiveComputation(i))
.sum();
}
// ✅ 병렬 처리 (CPU-bound 작업)
public long parallelSum(int[] data) {
return Arrays.stream(data)
.parallel()
.mapToLong(i -> expensiveComputation(i))
.sum();
}
private long expensiveComputation(int n) {
// 복잡한 계산 시뮬레이션
return IntStream.range(0, 1000)
.map(i -> i * n)
.sum();
}
}
시나리오 5: 조기 종료 (Short-circuit)
public class EarlyExit {
// 전통적 방식
public boolean containsTraditional(List<String> list, String target) {
for (String item : list) {
if (item.equals(target)) {
return true; // 즉시 종료
}
}
return false;
}
// ✅ Stream (Lazy Evaluation)
public boolean containsStream(List<String> list, String target) {
return list.stream()
.anyMatch(item -> item.equals(target)); // 찾으면 즉시 종료
}
// 성능 비교: 두 방법 모두 효율적
}
실무 의사결정 가이드
public class DecisionGuide {
/**
* 반복문 선택 체크리스트
*/
// 1. 단순 순회, 가독성 중요 → Enhanced for
public void simpleIteration(List<String> items) {
for (String item : items) {
process(item);
}
}
// 2. 인덱스 필요 → Traditional for
public void needIndex(String[] items) {
for (int i = 0; i < items.length; i++) {
System.out.println(i + ": " + items[i]);
}
}
// 3. 요소 제거 → Iterator
public void removeElements(List<String> items) {
items.removeIf(item -> item.isEmpty());
}
// 4. 함수형 변환, 필터링 → Stream
public List<String> transform(List<User> users) {
return users.stream()
.filter(u -> u.isActive())
.map(User::getName)
.collect(Collectors.toList());
}
// 5. CPU-bound 대용량 처리 → Parallel Stream
public double parallelComputation(List<Data> data) {
return data.parallelStream()
.mapToDouble(this::heavyComputation)
.average()
.orElse(0.0);
}
// 6. I/O-bound 작업 → CompletableFuture
public List<Result> ioIntensiveTasks(List<String> urls) {
List<CompletableFuture<Result>> futures = urls.stream()
.map(url -> CompletableFuture.supplyAsync(() -> fetchData(url)))
.collect(Collectors.toList());
return futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
// 7. 무한 루프, 조건 기반 → while
public void conditionalLoop() {
while (isRunning()) {
processNextTask();
}
}
// 8. 범위 기반 반복 → IntStream
public void rangeBasedLoop() {
IntStream.range(0, 100)
.forEach(i -> System.out.println(i));
}
}
Stream API 고급 활용
public class AdvancedStreamUsage {
// 1. Lazy Evaluation 이해
public void lazyEvaluation() {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = numbers.stream()
.filter(n -> {
System.out.println("Filter: " + n);
return n > 2;
})
.map(n -> {
System.out.println("Map: " + n);
return n * 2;
});
// 여기까지는 출력 없음 (Lazy)
System.out.println("Stream created");
// Terminal operation이 호출될 때 실행
stream.forEach(System.out::println);
}
// 2. Short-circuit 연산 활용
public Optional<String> findFirst(List<String> items) {
return items.stream()
.filter(s -> s.length() > 10)
.findFirst(); // 첫 번째만 찾고 종료
}
// 3. Collectors 고급 활용
public Map<String, List<User>> groupByDepartment(List<User> users) {
return users.stream()
.collect(Collectors.groupingBy(User::getDepartment));
}
public Map<String, Long> countByDepartment(List<User> users) {
return users.stream()
.collect(Collectors.groupingBy(
User::getDepartment,
Collectors.counting()
));
}
// 4. 커스텀 Collector
public String customCollector(List<String> items) {
return items.stream()
.collect(Collector.of(
StringBuilder::new, // supplier
(sb, s) -> sb.append(s).append(","), // accumulator
(sb1, sb2) -> sb1.append(sb2), // combiner
StringBuilder::toString // finisher
));
}
// 5. Parallel Stream 주의사항
public void parallelStreamPitfalls() {
List<Integer> numbers = IntStream.range(0, 1000)
.boxed()
.collect(Collectors.toList());
// ❌ 작은 데이터: 오버헤드가 더 큼
long sum1 = numbers.parallelStream().mapToLong(i -> i).sum();
// ❌ Stateful 연산: 순서 보장 안됨
List<Integer> result = numbers.parallelStream()
.map(i -> i * 2)
.sorted() // 병렬 처리 후 재정렬 (비효율)
.collect(Collectors.toList());
// ✅ Stateless, CPU-bound, 대용량 데이터
long sum2 = IntStream.range(0, 10_000_000)
.parallel()
.map(i -> i * i)
.sum();
}
}
성능 최적화 종합 가이드
public class OptimizationGuide {
// 1. 배열 길이 캐싱
public void cacheLength(int[] array) {
// ❌ 매 반복마다 array.length 호출
for (int i = 0; i < array.length; i++) {
process(array[i]);
}
// ✅ 길이 캐싱 (컴파일러가 최적화하지만 명시적으로)
int len = array.length;
for (int i = 0; i < len; i++) {
process(array[i]);
}
}
// 2. 역순 반복 (특정 상황)
public void reverseIteration(int[] array) {
// 0과 비교가 더 빠름 (CPU 캐시)
for (int i = array.length - 1; i >= 0; i--) {
process(array[i]);
}
}
// 3. 메서드 추출 비용
public void methodCallCost(List<String> list) {
// ❌ 매 반복마다 메서드 호출
for (int i = 0; i < list.size(); i++) {
process(list.get(i));
}
// ✅ 변수에 저장
int size = list.size();
for (int i = 0; i < size; i++) {
process(list.get(i));
}
// ✅✅ Enhanced for (내부적으로 최적화됨)
for (String item : list) {
process(item);
}
}
// 4. Loop Unrolling (수동)
public int sumUnrolled(int[] array) {
int sum = 0;
int i = 0;
int len = array.length;
// 4개씩 처리
for (; i < len - 3; i += 4) {
sum += array[i];
sum += array[i + 1];
sum += array[i + 2];
sum += array[i + 3];
}
// 나머지 처리
for (; i < len; i++) {
sum += array[i];
}
return sum;
}
// 5. Stream 최적화
public List<String> optimizedStream(List<User> users) {
return users.stream()
.filter(User::isActive) // Stateless
.map(User::getName) // Stateless
.distinct() // Stateful (필요시만)
.collect(Collectors.toList());
}
private void process(Object item) {
// 처리 로직
}
}
마치며
이번 글에서는 Java언어를 사용하면서 기초, 환경에 대한 설정 및 내용들을 다뤄보았습니다.
현업에서 개발할 당시 고민하였뎐 내용들을 이렇게 정리해보니 좀더 뚜렷하게 이해할 수 있었는데요,
다양한 사용법들이 존재하고, 이를 적재적소에 사용하는 것이 중요하다 생각합니다.
'Java' 카테고리의 다른 글
| 3단계: Java 컬렉션 & 제네릭 (0) | 2025.12.11 |
|---|---|
| 2단계: Java 객체지향 프로그래밍 (0) | 2025.12.11 |
| [Java] Java프로그램이 실행되기까지: JDK, JRE, JVM (2) | 2025.04.07 |
| 제네릭과 오버로딩: 왜 "B0"이 출력될까? (1) | 2025.02.11 |
| [ThreadLocal] Thread영역에 데이터를 저장하고 싶어요 (1) | 2025.01.27 |