minstudio

Java의 특징과 JVM 아키텍처

자바(Java)는 "Write Once, Run Anywhere" (한 번 작성하면 어디서든 실행된다) 라는 철학을 바탕으로 만들어졌습니다. C언어처럼 운영체제(OS)에 종속적인 기계어로 바로 번역되는 것이 아니라, 바이트코드(Bytecode)라는 중간 언어로 번역되기 때문입니다.

이 바이트코드를 읽고 각 운영체제가 알아들을 수 있도록 통역해주는 가상 머신이 바로 JVM(Java Virtual Machine)입니다. 따라서 윈도우용, 맥용, 리눅스용 JVM만 설치되어 있다면, 하나의 자바 프로그램이 모든 환경에서 동일하게 실행됩니다.

☕ 자바 프로그램의 실행 흐름 (JVM) Hello.java 개발자 소스코드 javac (컴파일러) Hello.class 바이트코드 JVM (Java Virtual Machine) Windows 기계어 macOS 기계어 Linux 기계어
public class Main {
    // 자바 프로그램의 시작점 (Entry Point)
    // JVM이 가장 먼저 실행하는 메서드입니다.
    public static void main(String[] args) {
        System.out.println("Hello, Java World!");
        
        // JRE(Java Runtime Environment)에 내장된 정보 확인
        String version = System.getProperty("java.version");
        System.out.println("현재 자바 버전: " + version);
    }
}
변수와 자료형 (원시 타입 vs 참조 타입)

자바는 아주 엄격한 타입 시스템을 가집니다. 변수에는 크게 원시 타입(Primitive Type)참조 타입(Reference Type) 두 가지가 있습니다.

int, double, boolean 같은 원시 타입은 데이터 그 자체가 Stack(스택) 메모리에 직접 저장됩니다. 빠르고 가볍습니다. 반면 String이나 개발자가 만든 Class 객체 같은 참조 타입은 실제 데이터가 Heap(힙) 메모리에 저장되고, Stack에는 그 Heap 메모리의 '주소(리모컨)'만 보관됩니다.

💾 자바 메모리 구조 (Stack vs Heap) Stack 메모리 int age = 30 String name = 주소(0x123) Heap 메모리 객체 (주소: 0x123) "Minstudio"
public class Main {
    public static void main(String[] args) {
        // 1. 원시 타입 (Primitive Type)
        // 값 자체가 Stack 메모리에 저장됩니다.
        int age = 30;
        double height = 175.5;
        boolean isStudent = false;

        // 2. 참조 타입 (Reference Type)
        // 실제 데이터는 Heap에, 변수(name)에는 Heap의 주소가 담깁니다.
        String name = new String("Minstudio");
        
        // 자바에서 문자열은 특별 취급되어, new 없이도 생성 가능합니다. (String Pool)
        String job = "Developer"; 

        // 3. 참조 타입의 주의점 (주소 비교 vs 값 비교)
        String a = new String("Java");
        String b = new String("Java");

        // == 연산자는 '주소'를 비교합니다. (false)
        System.out.println("a == b : " + (a == b)); 

        // .equals() 메서드는 실제 '값'을 비교합니다. (true)
        System.out.println("a.equals(b) : " + a.equals(b));
    }
}
제어문과 반복문 (Control Flow)

프로그램의 흐름을 조작하는 제어문과 반복문입니다. 조건문 if-else와 다중 분기문 switch, 그리고 지정된 횟수만큼 반복하는 for문과 조건이 참일 때까지 반복하는 while문이 있습니다.

특히 Java 14부터는 화살표(->)를 사용하여 break 없이 간결하게 작성할 수 있는 강화된 switch 표현식(Switch Expressions)이 추가되어 실무에서 널리 사용되고 있습니다.

public class Main {
    public static void main(String[] args) {
        // 1. if-else 조건문
        int score = 85;
        if (score >= 90) {
            System.out.println("A 등급");
        } else if (score >= 80) {
            System.out.println("B 등급");
        } else {
            System.out.println("C 등급");
        }

        // 2. 모던 Java (14+) Switch 표현식
        // break를 생략할 수 있고, 화살표(->)로 직관적인 표현이 가능합니다.
        String day = "MON";
        String status = switch (day) {
            case "MON", "TUE", "WED", "THU", "FRI" -> "평일 (출근)";
            case "SAT", "SUN" -> "주말 (휴식)";
            default -> "알 수 없는 요일";
        };
        System.out.println("오늘의 상태: " + status);

        // 3. for 반복문과 배열 순회 (향상된 for문)
        String[] fruits = {"Apple", "Banana", "Cherry"};
        for (String fruit : fruits) {
            System.out.println("과일 이름: " + fruit);
        }
    }
}
클래스와 인스턴스 (객체지향의 시작)

자바는 객체지향(Object-Oriented) 언어입니다. 객체지향 프로그래밍은 상태(변수)와 행동(메서드)을 하나의 덩어리로 묶어서 관리하는 패러다임입니다.

클래스(Class)는 객체를 만들기 위한 '설계도' 또는 '붕어빵 틀'입니다. 이 틀을 이용해 메모리(Heap)에 실제로 찍어낸 실체를 인스턴스(Instance) 혹은 '객체'라고 부릅니다. 붕어빵 틀은 하나지만, 팥 붕어빵, 슈크림 붕어빵처럼 서로 다른 상태를 가진 여러 인스턴스를 무한히 만들어낼 수 있습니다.

🏭 클래스와 인스턴스 (붕어빵 틀 비유) class 붕어빵 (설계도) 상태: String 앙금 행동: 굽기() new 연산자 인스턴스 1 (메모리) 앙금 = "팥" 인스턴스 2 (메모리) 앙금 = "슈크림"
// 붕어빵 틀(클래스) 정의
class FishBread {
    // 필드 (상태)
    // private: 외부에서 마음대로 앙금을 바꾸지 못하게 캡슐화(보호)합니다.
    private String filling; 

    // 생성자 (인스턴스가 생성될 때 호출되는 초기화 메서드)
    public FishBread(String filling) {
        this.filling = filling;
    }

    // 메서드 (행동)
    public void bake() {
        System.out.println(this.filling + " 붕어빵이 따끈하게 구워졌습니다!");
    }
}

public class Main {
    public static void main(String[] args) {
        // new 키워드를 사용하여 메모리에 실제 인스턴스(객체)를 찍어냅니다.
        FishBread redBeanBread = new FishBread("팥");
        FishBread creamBread = new FishBread("슈크림");

        // 각각의 인스턴스는 자신만의 독립적인 상태를 가지고 행동합니다.
        redBeanBread.bake(); // 출력: 팥 붕어빵이 따끈하게 구워졌습니다!
        creamBread.bake();   // 출력: 슈크림 붕어빵이 따끈하게 구워졌습니다!
    }
}
상속과 다형성 (Inheritance & Polymorphism)

상속(Inheritance)은 부모 클래스가 가진 변수와 메서드를 자식 클래스가 그대로 물려받아 재사용하는 문법입니다. extends 키워드를 사용하며, 코드 중복을 획기적으로 줄여줍니다.

부모에게 물려받은 행동을 자식의 입맛에 맞게 재정의하는 것을 오버라이딩(Overriding)이라고 합니다. 이를 바탕으로, 부모 타입의 변수에 다양한 자식 객체들을 번갈아 끼워 넣으며 유연하게 동작시키는 객체지향의 꽃, 다형성(Polymorphism)을 구현할 수 있습니다.

🧬 상속(extends)과 오버라이딩 Animal (부모 클래스) 소리내기(): "..." Dog (자식 클래스) 소리내기(): "멍멍!" (Overriding) Cat (자식 클래스) 소리내기(): "야옹~" (Overriding)
// 부모 클래스
class Animal {
    public void makeSound() {
        System.out.println("동물이 소리를 냅니다.");
    }
}

// 자식 클래스 (Dog는 Animal을 상속받음)
class Dog extends Animal {
    // 부모의 메서드를 자식의 입맛에 맞게 재정의(Overriding)
    @Override
    public void makeSound() {
        System.out.println("멍멍!");
    }
}

class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("야옹~");
    }
}

public class Main {
    public static void main(String[] args) {
        // 다형성 (Polymorphism): 부모 타입의 변수에 자식 객체를 담을 수 있습니다. (업캐스팅)
        Animal myDog = new Dog();
        Animal myCat = new Cat();

        // 동일한 메서드를 호출하지만, 실제 담겨있는 객체에 따라 다르게 작동합니다.
        myDog.makeSound(); // 출력: 멍멍!
        myCat.makeSound(); // 출력: 야옹~
        
        // 배열을 사용하면 다양한 자식 객체들을 하나의 부모 그룹으로 관리할 수 있어 강력합니다.
        Animal[] pets = { new Dog(), new Cat() };
        for (Animal pet : pets) {
            pet.makeSound();
        }
    }
}
추상 클래스와 인터페이스 (Interface)

객체지향 설계에서 가장 중요한 원칙 중 하나는 "구현이 아닌 역할(인터페이스)에 의존하라"입니다.

자바의 인터페이스(Interface)는 껍데기(메서드 이름과 반환 타입)만 존재하고 내부는 비어있는 규약입니다. 220V 콘센트(인터페이스) 규격만 맞추면, 삼성 TV든 LG 냉장고든(구현 클래스) 벽에 꽂아 사용할 수 있는 것과 같은 원리입니다. 자바는 다중 상속을 지원하지 않지만, 인터페이스는 한 클래스가 여러 개를 동시에 구현(implements)할 수 있습니다.

🔌 인터페이스 (역할과 구현의 분리) <<Interface>> PowerPlug (220V) SamsungTV 클래스 220V 규격에 맞춰 제작됨 LG_Refrigerator 클래스 220V 규격에 맞춰 제작됨
// 인터페이스 정의 (구현부가 없는 추상 메서드들의 집합)
interface UsbConnectable {
    void connect(); // 인터페이스의 메서드는 기본적으로 public abstract 입니다.
}

// 클래스가 인터페이스를 구현(implements)할 때는 반드시 내부 메서드를 완성해야 합니다.
class Mouse implements UsbConnectable {
    @Override
    public void connect() {
        System.out.println("마우스가 USB 포트에 연결되었습니다. 띠링!");
    }
}

class Keyboard implements UsbConnectable {
    @Override
    public void connect() {
        System.out.println("키보드가 USB 포트에 연결되었습니다. 타닥타닥.");
    }
}

public class Main {
    public static void main(String[] args) {
        // 인터페이스 타입 변수로 사용 (다형성)
        // 컴퓨터 입장에서는 마우스인지 키보드인지 알 필요 없이, 'USB 연결 가능한 기기'면 모두 수용합니다.
        UsbConnectable device1 = new Mouse();
        UsbConnectable device2 = new Keyboard();

        device1.connect(); // 출력: 마우스가 USB 포트에 연결되었습니다. 띠링!
        device2.connect(); // 출력: 키보드가 USB 포트에 연결되었습니다. 타닥타닥.
    }
}
예외 처리 (Exception Handling)

프로그램을 실행하다 보면 0으로 숫자를 나누거나, 없는 파일을 읽으려 하는 등 예상치 못한 오류(Exception)가 발생합니다. 아무런 조치를 취하지 않으면 프로그램은 즉시 강제 종료됩니다.

자바에서는 try-catch-finally 블록을 통해 예외를 우아하게 처리합니다. 위험한 코드는 try에 넣고, 오류가 터지면 catch에서 대신 수습하며, 오류 여부와 상관없이 무조건 실행해야 하는 정리 작업(파일 닫기 등)은 finally에 작성합니다.

public class Main {
    public static void main(String[] args) {
        int[] numbers = {1, 2, 3};

        try {
            // 예외가 발생할 가능성이 있는 위험한 코드
            System.out.println("배열의 5번째 요소 접근 시도...");
            
            // 배열 크기는 3인데 5번째 인덱스에 접근하므로 여기서 오류 폭발! (ArrayIndexOutOfBoundsException)
            System.out.println(numbers[4]); 
            
            System.out.println("이 코드는 실행되지 않습니다.");

        } catch (ArrayIndexOutOfBoundsException e) {
            // 특정 예외가 터졌을 때 수습하는 블록
            System.out.println("경고: 배열의 범위를 벗어났습니다!");
            System.out.println("에러 메시지: " + e.getMessage());

        } catch (Exception e) {
            // 그 외의 모든 예외를 잡는 최상위 Exception (안전망)
            System.out.println("알 수 없는 오류 발생: " + e.getMessage());

        } finally {
            // 예외 발생 여부와 상관없이 100% 실행되는 블록 (보통 리소스 해제용)
            System.out.println("finally: 예외 검사가 모두 종료되었습니다.");
        }
        
        System.out.println("프로그램이 강제 종료되지 않고 여기까지 정상적으로 도달했습니다.");
    }
}
제네릭 (Generics)

자바 배열이나 리스트에 다양한 타입의 데이터를 마구잡이로 넣다 보면, 나중에 꺼내 쓸 때 어떤 타입인지 몰라 에러가 발생하기 쉽습니다. 이를 방지하기 위해 제네릭(Generics) <T>을 사용합니다.

제네릭은 클래스나 메서드를 정의할 때 타입을 미리 정하지 않고, 실제 사용할 때(객체를 생성할 때) 타입을 지정하는 기법입니다. 이 덕분에 컴파일 시점에 강력한 타입 체크가 가능해지며, 꺼낼 때마다 형변환(Casting)을 해야 하는 번거로움이 사라집니다.

// 타입을 <T>라는 임시 변수로 뚫어놓은 제네릭 클래스 설계
class Box<T> {
    private T item; // T는 생성 시점에 String, Integer 등으로 치환됩니다.

    public void setItem(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }
}

public class Main {
    public static void main(String[] args) {
        // 1. 객체를 생성할 때 <T> 자리에 들어갈 구체적 타입을 명시합니다. (타입 파라미터)
        Box<String> stringBox = new Box<>(); // Java 7 이상부터 뒤쪽 다이아몬드 연산자<> 생략 가능
        
        stringBox.setItem("소중한 반지");
        // stringBox.setItem(100); // 에러 발생! String만 넣을 수 있도록 컴파일러가 완벽 차단.

        // 캐스팅(형변환) 없이 바로 String으로 꺼내 쓸 수 있습니다.
        String myItem = stringBox.getItem();
        System.out.println("상자 안의 물건: " + myItem);


        // 2. 다른 타입으로도 재사용 가능!
        Box<Integer> numberBox = new Box<>(); // 원시타입 int는 안되고 Wrapper 클래스인 Integer 사용
        numberBox.setItem(999);
        System.out.println("상자 안의 숫자: " + numberBox.getItem());
    }
}
자바 컬렉션 프레임워크 (List, Set, Map)

데이터를 효율적으로 저장하고 가공하기 위해 자바가 기본 제공하는 표준 자료구조 모음집이 컬렉션 프레임워크(Collection Framework)입니다. 대표적으로 3가지 인터페이스가 사용됩니다.

  • List (ArrayList): 순서가 보장되며, 데이터 중복을 허용합니다. (대기표 방식)
  • Set (HashSet): 순서가 없고, 데이터 중복을 허용하지 않습니다. (장바구니 방식)
  • Map (HashMap): Key-Value(키-값) 쌍으로 이루어져 있으며, Key는 중복 불가능합니다. (사전 방식)
🗂️ 핵심 자료구조 비교 List (ArrayList) [0] Apple [1] Banana [2] Apple (중복 허용) Set (HashSet) Apple Banana 중복 불가! 순서 없음! Map (HashMap) "A" Apple "B" Banana
import java.util.ArrayList;
import java.util.HashSet;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        // 1. List (순서 보장, 중복 허용)
        List<String> list = new ArrayList<>();
        list.add("Java");
        list.add("Python");
        list.add("Java"); // 중복 가능
        System.out.println("List: " + list); // [Java, Python, Java]

        // 2. Set (순서 없음, 중복 불가)
        Set<String> set = new HashSet<>();
        set.add("Java");
        set.add("Python");
        set.add("Java"); // 중복 무시됨
        System.out.println("Set: " + set); // [Java, Python] (순서 랜덤)

        // 3. Map (Key-Value 쌍, Key 중복 불가)
        Map<String, Integer> scores = new HashMap<>();
        scores.put("Alice", 90);
        scores.put("Bob", 85);
        scores.put("Alice", 95); // 같은 키로 넣으면 덮어씌워짐

        System.out.println("Map: " + scores); // {Bob=85, Alice=95}
        System.out.println("Alice의 점수: " + scores.get("Alice")); // 95
    }
}
모던 자바: 람다식과 스트림 API (Lambda & Stream)

Java 8 버전에 도입된 람다(Lambda)와 스트림(Stream) API는 자바의 패러다임을 바꾼 혁명적인 기능입니다. 기존에는 컬렉션의 데이터를 거를 때 지저분한 for문과 if문을 남발해야 했습니다.

스트림 API를 사용하면, 원본 데이터를 물줄기(Stream)에 올린 뒤, 조건에 맞게 거르고(filter), 형태를 변환하고(map), 최종적으로 묶는(collect) 과정을 컨베이어 벨트처럼 선언적(Declarative)으로 매우 우아하게 작성할 수 있습니다.

🌊 Stream 파이프라인 컨베이어 벨트 List 데이터 filter() 조건: 원형만 통과 map() 조건: 별 모양으로 변환 collect() 최종 결과 수집
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Apple", "Banana", "Cherry", "Avocado", "Blueberry");

        // [과거 방식] 'A'로 시작하는 과일만 대문자로 바꿔서 새 리스트에 담기
        // 코드가 길어지고 가독성이 떨어집니다.

        // [모던 자바 스트림 방식]
        List<String> result = names.stream()               // 1. 물줄기(스트림) 생성
                .filter(name -> name.startsWith("A"))      // 2. 중간 연산: 'A'로 시작하는 데이터만 통과 (람다식)
                .map(String::toUpperCase)                  // 3. 중간 연산: 모두 대문자로 변환 (메서드 참조)
                .collect(Collectors.toList());             // 4. 최종 연산: 결과를 모아서 List로 반환

        System.out.println("결과: " + result); 
        // 출력: 결과: [APPLE, AVOCADO]
    }
}
메모리 관리와 가비지 컬렉터 (Garbage Collection)

C나 C++에서는 개발자가 메모리 할당을 받은 후 다 쓰면 직접 해제(free)해야 했습니다. 실수로 해제하지 않으면 메모리 누수(Memory Leak)가 발생하여 서버가 다운됩니다.

반면 자바는 백그라운드에 가비지 컬렉터(Garbage Collector, GC)라는 로봇 빗자루가 돌아다닙니다. GC는 Heap 메모리를 주기적으로 스캔하면서, Stack이나 다른 곳에서 "더 이상 참조(연결)하고 있지 않은 외딴섬 객체"를 발견하면 알아서 메모리를 청소해 줍니다. 덕분에 개발자는 비즈니스 로직에만 집중할 수 있습니다.

🧹 가비지 컬렉터(GC)의 청소 원리 Stack 메모리 참조 변수 A 참조 변수 B (null) X Heap 메모리 (객체 공간) 객체 1 (사용 중/안전) 객체 2 (외딴섬) (연결 끊김) 🧹 GC 출동!
public class Main {
    public static void main(String[] args) {
        // 객체 생성: Heap 메모리에 공간 할당, user 변수는 그 주소를 가짐
        String user = new String("홍길동"); 

        System.out.println("현재 유저: " + user);

        // 변수에 null을 대입하여 Heap 객체와의 '연결 고리'를 끊음
        user = null; 

        // 이제 "홍길동" 객체는 메모리에 존재하지만, 접근할 방법이 없는 '외딴 섬(Unreachable)'이 됩니다.
        // 백그라운드에서 돌고 있는 Garbage Collector(GC)가 주기적으로 
        // 이런 고아 객체들을 찾아내어 메모리에서 영구히 삭제(청소)합니다.

        // 개발자가 원한다면 GC에게 "지금 웬만하면 청소 좀 해줘" 라고 요청할 순 있지만,
        // 실제 청소를 언제 할지는 100% JVM 맘입니다. (실무에선 직접 호출을 권장하지 않음)
        System.gc();
        
        System.out.println("안심하세요! JVM이 알아서 메모리를 관리해줍니다.");
    }
}
Java의 특징과 JVM 아키텍처
변수와 자료형 (원시 타입 vs 참조 타입)
제어문과 반복문 (Control Flow)
클래스와 인스턴스 (객체지향의 시작)
상속과 다형성 (Inheritance & Polymorphism)
추상 클래스와 인터페이스 (Interface)
예외 처리 (Exception Handling)
제네릭 (Generics)
자바 컬렉션 프레임워크 (List, Set, Map)
모던 자바: 람다식과 스트림 API (Lambda & Stream)
메모리 관리와 가비지 컬렉터 (Garbage Collection)

목차