Repository

1단계: Java 기초 다지기 본문

Java

1단계: Java 기초 다지기

Mr.Manager 2025. 12. 11. 20:18
반응형

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이 필요한가?

  1. Maven, Gradle 등 빌드 도구가 참조
  2. Tomcat, Spring Boot 같은 서버가 참조
  3. IDE가 프로젝트별 JDK 버전 관리에 사용
  4. 스크립트에서 일관된 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 설정

  1. Project Structure 설정
    ```
    File → Project Structure → Project
  • SDK: 21
  • Language Level: 21 (Preview features enabled)
  1. Maven/Gradle 빌드 도구 JDK 설정
    ```
    Settings → Build, Execution, Deployment → Build Tools → Maven
  • JDK for importer: Project SDK
  • Maven home: Bundled (3.9.5)
  1. 실무 권장 플러그인
  • SonarLint: 코드 품질 실시간 검사
  • JRebel: Hot reload (상용)
  • Key Promoter X: 단축키 학습

첫 Java 프로그램과 실행 원리

Hello World - 단순해 보이지만 깊이가 있다

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

각 키워드의 의미

  1. public: 접근 제어자, 어디서든 접근 가능
  2. class: Java는 모든 코드가 클래스 안에 존재
  3. static: 객체 생성 없이 호출 가능 (JVM이 직접 호출)
  4. void: 반환값 없음
  5. main: JVM이 찾는 프로그램 진입점 (Entry Point)
  6. 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 생성
    }
}

불변성의 이점

  1. Thread-Safety: 동기화 없이 멀티스레드 안전
  2. String Pool 사용 가능: 메모리 절약
  3. 해시코드 캐싱: HashMap 성능 향상
  4. 보안: 패스워드, 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: return

2. 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언어를 사용하면서 기초, 환경에 대한 설정 및 내용들을 다뤄보았습니다.
현업에서 개발할 당시 고민하였뎐 내용들을 이렇게 정리해보니 좀더 뚜렷하게 이해할 수 있었는데요,
다양한 사용법들이 존재하고, 이를 적재적소에 사용하는 것이 중요하다 생각합니다.

반응형