콘텐츠로 이동

Java 문법과 객체

🎯 이 장에서 배우는 것

  • 변수·자료형·연산자, 제어문, 메서드·배열·문자열
  • 객체지향 4원칙(캡슐화·상속·다형성·추상화)을 코드로
  • if-else를 객체 협력으로 바꾸는 첫 감각

단계: 1단계 — Java Core · 앞 장: java-study-ch01 · 다음 장: java-study-ch03

따라 하는 법: 위에서 아래로 읽으며 코드를 직접 쳐본다. 각 절의 코드를 직접 쳐보고, 끝의 2.9 실전문제를 풀어본다. 막히면 concept-oop.

실습 프로젝트: 이 장의 실습 예제 파일(2.4·2.5와 각 절의 ✏️ 직접 해보기)은 java-study-ch01 1.2에서 만든 hello-java 프로젝트의 src/main/java/com/example/ch02/ 아래에 만듭니다(패키지 com.example.ch02). Gradle(gradle init) 프로젝트도 같은 경로 구조를 쓰며, 소스가 app/ 하위에 생성된 경우에만 app/src/main/java/...로 읽으면 됩니다.


2.0 기본 문법: 변수, 자료형, 연산자

🎯 목표: 변수·기본형/참조형·형 변환·연산자를 구분해 쓰고, ==equals의 차이를 코드로 설명한다.

개요

이 문서는 Java를 처음 다시 정리할 때 가장 먼저 확인해야 하는 기본 문법을 다룹니다. 변수, 자료형, 연산자는 너무 기초처럼 보이지만, 이후 객체지향과 컬렉션, 예외 처리 문서를 읽을 때 계속 기반이 되는 내용입니다.

왜 중요한가

기초 문법은 단순 암기 영역이 아니라, 코드가 실제로 메모리에 어떻게 저장되고 어떤 결과를 만들어내는지 이해하는 출발점입니다. 여기서 흔들리면 이후 클래스, 객체, 컬렉션 문서도 추상적으로 느껴지기 쉽습니다.

1. 변수란 무엇인가

변수는 값을 저장하기 위한 이름 있는 공간입니다.

int age = 20;
String name = "Kim";
System.out.println(name + " : " + age);
예상 결과
Kim : 20
핵심 포인트
  • 변수는 타입과 이름을 함께 가집니다.
  • 같은 변수에는 같은 타입의 값만 저장할 수 있습니다.
  • 읽기 좋은 변수 이름이 코드 이해 속도를 크게 좌우합니다.

2. 기본 자료형

Java의 기본 자료형은 값을 직접 저장합니다.

  • byte, short, int, long: 정수
  • float, double: 실수
  • char: 문자
  • boolean: 참/거짓
int count = 10;
double price = 19.99;
char grade = 'A';
boolean active = true;

3. 참조 자료형

문자열, 배열, 객체는 참조 자료형입니다.

String title = "Java";
int[] numbers = {1, 2, 3};

기본 자료형과 달리 실제 값 자체보다, 값을 가리키는 참조 개념을 함께 이해해야 합니다.

4. 형 변환

자동 형 변환

작은 범위의 값을 큰 범위로 옮길 때는 자동으로 변환됩니다.

int num = 10;
double result = num;
강제 형 변환

큰 범위의 값을 작은 범위로 옮길 때는 명시적으로 변환해야 합니다.

double value = 3.14;
int result = (int) value;

이 경우 소수점 아래 값은 잘립니다.

5. 산술 연산자

  • +, -, *, /, %
int a = 10;
int b = 3;
System.out.println(a + b);
System.out.println(a / b);
System.out.println(a % b);
예상 결과
13
3
1

정수 나눗셈은 결과도 정수라는 점을 자주 실수합니다.

6. 비교 연산자와 논리 연산자

  • 비교 연산자: ==, !=, >, <, >=, <=
  • 논리 연산자: &&, ||, !
int age = 20;
boolean adult = age >= 19;
boolean weekend = true;
boolean canRest = adult && weekend;

7. 증감 연산자와 대입 연산자

int count = 0;
count++;
count += 5;

이 문법은 짧지만, 반복문과 조건문에서 자주 사용되므로 손에 익혀두는 편이 좋습니다.

8. 문자열과 연산

문자열은 + 연산으로 이어 붙일 수 있습니다.

String name = "Park";
int age = 20;
System.out.println(name + " is " + age + " years old.");

다만 반복문 안에서 문자열을 많이 붙일 때는 나중에 StringBuilder를 고려해야 합니다(2.2 8절에서 자세히 다룹니다).

자주 하는 실수

  • ==equals()를 구분하지 않는 것
  • 정수 나눗셈 결과를 실수처럼 기대하는 것
  • 형 변환 시 데이터 손실을 무시하는 것
  • 변수 이름을 너무 짧거나 모호하게 쓰는 것

실무 연결 포인트

기초 문법은 이후 DTO 필드 타입, 엔티티 속성, 조건 분기, API 응답 처리, 컬렉션 순회까지 모든 곳에 이어집니다. 즉 이 문서는 가장 단순하지만, 가장 넓게 연결되는 문서입니다.

✏️ 기본 문법 직접 해보기

int·double·String 변수를 선언하고, 형 변환 결과와 == vs equals 비교 결과를 직접 출력해 차이를 확인하라.

==equals의 차이는 2.2 7절에서 자세히 다루므로, 여기서는 두 비교의 결과가 서로 다르게 나온다는 사실만 먼저 관찰하면 충분합니다.

실습 순서

  1. 파일 생성hello-java 프로젝트에 src/main/java/com/example/ch02/practice/TypePractice.java를 만듭니다.
  2. 뼈대 입력 — 아래 뼈대를 그대로 입력합니다.

    package com.example.ch02.practice;
    
    public class TypePractice {
        public static void main(String[] args) {
            // 1) int·double·String 변수를 선언하고 출력한다
            // 2) (int) 형 변환 결과를 출력한다
            // 3) ==와 equals 비교 결과를 출력한다
        }
    }
    
  3. 하나씩 구현 — 주석의 과제를 한 항목씩 구현합니다.

  4. 실행·확인mvn compile exec:java -Dexec.mainClass="com.example.ch02.practice.TypePractice" — 추가할 때마다 다시 실행해 출력을 확인합니다.

정리

변수, 자료형, 연산자는 Java 문법의 출발점입니다. 어렵지 않지만, 이 개념을 정확히 이해하면 이후 문서들을 훨씬 빠르고 안정적으로 읽을 수 있습니다.

한 줄 정리

기본 문법의 핵심은 문법 자체보다 값의 타입과 계산 결과가 어떻게 결정되는지 정확히 이해하는 것입니다.


2.1 제어문: 조건문, 반복문

🎯 목표: if·switch·for·while을 상황에 맞게 골라 흐름을 제어한다.

개요

이 문서는 Java에서 조건문과 반복문을 어떻게 읽고 써야 하는지 정리한 기초 가이드입니다. 제어문은 단순히 문법 블록을 외우는 주제가 아니라, 프로그램의 흐름을 어떤 기준으로 분기하고 반복할지 결정하는 도구입니다.

왜 중요한가

변수와 연산자를 이해해도, 흐름 제어가 흔들리면 프로그램은 원하는 방향으로 동작하지 않습니다. 실제 Java 코드의 대부분은 조건을 검사하고, 반복해서 처리하고, 특정 시점에 멈추는 흐름 제어 위에 서 있습니다.

1. 조건문은 분기 기준을 코드로 표현하는 방법이다

조건문은 어떤 상황에서 어떤 코드를 실행할지 결정합니다. 핵심은 문법보다 분기 기준이 명확한가입니다.

if

조건이 하나일 때 가장 단순한 형태입니다.

if (score >= 60) {
    System.out.println("합격");
}

if-else

참과 거짓 두 갈래가 명확할 때 사용합니다.

int score = 75;
if (score >= 60) {
    System.out.println("합격");
} else {
    System.out.println("불합격");
}
예상 결과
합격

if-else if-else

조건이 여러 갈래로 나뉠 때 사용합니다.

if (score >= 90) {
    System.out.println("A");
} else if (score >= 80) {
    System.out.println("B");
} else {
    System.out.println("C");
}
이때 중요한 것은 위에서부터 읽히므로, 더 구체적이거나 더 우선순위가 높은 조건을 먼저 배치해야 한다는 점입니다.

2. switch는 값 중심 분기에 적합하다

switch는 하나의 값을 기준으로 여러 경우를 나누는 데 적합합니다.

switch (day) {
    case "MONDAY" -> System.out.println("월요일");
    case "TUESDAY" -> System.out.println("화요일");
    default -> System.out.println("기타 요일");
}
여러 개의 범위 조건이나 복잡한 비교식은 보통 if-else가 더 자연스럽고, 고정된 값 비교는 switch가 더 읽기 쉽습니다.

3. 반복문은 같은 작업을 여러 번 수행하는 구조다

반복문의 핵심은 "몇 번 도는가"보다, 언제 시작하고 언제 멈추는가를 분명히 하는 것입니다.

for

반복 횟수가 비교적 분명할 때 적합합니다.

for (int i = 0; i < 5; i++) {
    System.out.println(i);
}
예상 결과
0
1
2
3
4
배열이나 리스트 순회처럼 인덱스가 필요한 상황에서도 자주 사용합니다.

while

종료 시점이 조건에 더 강하게 묶여 있을 때 적합합니다.

while (balance > 0) {
    balance -= 1000;
}

do-while

본문을 최소 한 번은 실행해야 할 때 사용합니다.

do {
    input = scanner.nextLine();
} while (input.isBlank());
실무에서는 forwhile이 훨씬 자주 보이고, do-while은 제한적으로 사용됩니다.

4. 반복 제어 키워드도 함께 이해해야 한다

break

가장 가까운 반복문이나 switch를 즉시 종료합니다.

continue

현재 반복만 건너뛰고 다음 반복으로 넘어갑니다. 이 두 키워드는 편리하지만, 많이 쓰면 흐름이 눈에 잘 들어오지 않을 수 있으므로 조건 구조 자체를 단순하게 만드는 편이 더 좋은 경우가 많습니다.

5. 초중급 단계에서 자주 하는 실수

  • if 조건을 중복해서 써서 분기 구조가 불필요하게 길어지는 것
  • switchif-else를 상황에 맞게 고르지 않고 습관대로만 쓰는 것
  • 반복문의 종료 조건을 잘못 잡아 무한 루프를 만드는 것
  • 반복문 안에서 너무 많은 책임을 처리해 가독성을 떨어뜨리는 것

실무 연결 포인트

제어문은 모든 계층에서 등장합니다. 입력 검증, 권한 분기, 상태 변경, 컬렉션 순회, 예외 처리 흐름까지 모두 제어문 위에서 구성됩니다. 즉, 기초 문법처럼 보이지만 실제로는 가장 오래 쓰는 도구입니다.

정리

조건문은 분기 기준을 코드로 표현하는 도구이고, 반복문은 같은 작업을 안전하게 반복하는 도구입니다. 둘 다 문법 자체보다 조건과 종료 시점을 명확하게 드러내는 것이 핵심입니다.

한 줄 정리

제어문의 핵심은 문법을 외우는 것이 아니라, 프로그램 흐름이 왜 그렇게 갈라지고 반복되는지 설명할 수 있는 것입니다.

2.2 메서드와 배열, 문자열

🎯 목표: 메서드로 로직을 분리하고 배열·문자열을 다루며, Java의 값 전달(pass-by-value)을 이해한다.

개요

이 문서는 제어문 다음 단계에서 반드시 정리해야 하는 메서드, 배열, 문자열을 한 흐름으로 묶은 가이드입니다. 이 세 가지는 문법 항목처럼 보이지만, 실제로는 코드를 나누고, 여러 값을 다루고, 텍스트를 안전하게 처리하는 기본 도구입니다.

왜 여기서 배우는가

변수와 제어문만으로도 작은 코드는 만들 수 있습니다. 하지만 로직이 길어지고 입력이 늘어나면 같은 코드를 반복하게 됩니다. 이때 메서드는 로직을 나누고, 배열은 여러 값을 같은 규칙으로 다루게 해주며, 문자열은 사용자 입력과 파일, HTTP 요청처럼 실무에서 자주 만나는 텍스트 데이터를 처리하게 해줍니다.

1. 메서드는 로직에 이름을 붙이는 단위다

메서드는 특정 작업을 하나의 이름 아래로 묶습니다. 좋은 메서드는 길지 않고, 하나의 책임이 분명합니다.

public int sum(int left, int right) {
    return left + right;
}

메서드를 읽을 때는 세 가지를 먼저 봐야 합니다.

  • 입력이 무엇인지
  • 결과가 무엇인지
  • 부수 효과가 있는지 메서드가 많아지는 것이 중요한 것이 아니라, 읽는 사람이 의도를 빠르게 이해할 수 있는 단위로 분리되는 것이 중요합니다.

2. 매개변수와 반환값을 분명히 구분해야 한다

매개변수는 메서드가 외부에서 받는 값이고, 반환값은 메서드가 계산해서 돌려주는 값입니다.

public boolean isAdult(int age) {
    return age >= 20;
}

초보 단계에서 흔한 실수는 메서드가 계산도 하고 출력도 하고 상태도 바꾸는 식으로 여러 책임을 동시에 갖는 것입니다. 먼저는 입력과 출력이 분명한 메서드를 만드는 습관이 더 중요합니다.

3. Java는 인자를 값으로 전달한다

Java에서는 기본형이든 참조값이든 항상 값이 전달됩니다. 객체를 넘기면 객체 자체가 복사되는 것이 아니라, 그 객체를 가리키는 참조값이 복사됩니다.

이 차이를 이해해야 메서드 안에서 값을 바꿨을 때 무엇이 바뀌고 무엇이 안 바뀌는지 설명할 수 있습니다.

4. 배열은 같은 타입의 값을 순서대로 다루는 가장 기본 구조다

배열은 길이가 고정된 자료구조입니다.

int[] scores = {90, 80, 100};
for (int i = 0; i < scores.length; i++) {
    System.out.println(scores[i]);
}

배열에서 꼭 기억할 기준은 아래와 같습니다.

  • 인덱스는 0부터 시작합니다.
  • 길이는 생성 시점에 정해집니다.
  • 범위를 벗어나면 ArrayIndexOutOfBoundsException이 발생합니다. 배열은 컬렉션 프레임워크를 배우기 전 단계에서, 반복 처리와 인덱스 기반 접근을 익히는 데 가장 좋은 출발점입니다.

5. 배열 변수와 배열 객체를 구분해야 한다

int[] numbers = new int[3];

여기서 numbers는 참조 변수이고, 실제 배열 객체는 힙 메모리에 생성됩니다. 그래서 배열을 다른 변수에 대입하면 값이 복사되는 것이 아니라 같은 배열을 가리키게 됩니다.

int[] source = {1, 2, 3};
int[] target = source;
target[0] = 99;

System.out.println(source[0]);
System.out.println(target[0]);
예상 결과
99
99

이 경우 source[0]도 99가 됩니다. 같은 배열을 보고 있기 때문입니다.

6. 문자열은 배열이 아니라 불변 객체다

문자열은 문자 묶음처럼 보이지만 Java에서는 String 객체입니다. 그리고 한 번 만들어진 문자열은 바뀌지 않습니다.

String name = "java";
String upper = name.toUpperCase();

System.out.println(name);  // java
System.out.println(upper); // JAVA

문자열 메서드는 원본을 바꾸기보다 새 문자열을 돌려주는 경우가 많습니다. 이 특성을 알아야 디버깅할 때 혼란이 줄어듭니다.

7. 문자열 비교는 ==가 아니라 equals를 우선한다

==는 두 참조가 같은 객체를 가리키는지 비교하고, equals는 문자열 내용이 같은지 비교합니다.

String a = new String("spring");
String b = new String("spring");

System.out.println(a == b);
System.out.println(a.equals(b));
예상 결과
false
true

실무에서 문자열 비교 실수는 인증, 권한, 상태값 분기 같은 곳에서 바로 버그로 이어집니다.

8. 문자열을 많이 이어 붙이면 StringBuilder를 고려한다

반복문 안에서 문자열을 계속 더하면 불필요한 객체가 많이 생길 수 있습니다.

StringBuilder builder = new StringBuilder();
for (int i = 0; i < 3; i++) {
    builder.append(i);
}
String result = builder.toString();

초반에는 성능 최적화보다 개념 이해가 우선이지만, 문자열이 불변이라는 사실과 StringBuilder가 왜 필요한지는 여기서 잡아 두는 편이 좋습니다.

9. 세 개념은 함께 써야 감이 잡힌다

예를 들어 학생 점수 목록이 배열로 들어오면, 평균 계산 로직은 메서드로 분리하고, 이름이나 상태 메시지는 문자열로 다루게 됩니다.

즉, 메서드는 구조를 만들고, 배열은 데이터를 담고, 문자열은 사용자와 시스템이 주고받는 텍스트를 표현합니다.

자주 하는 실수

  • 메서드 이름은 있는데 실제 책임이 너무 많은 경우
  • 배열 길이를 확인하지 않고 인덱스에 접근하는 경우
  • 배열 복사와 참조 공유를 구분하지 못하는 경우
  • 문자열 비교에 ==를 사용하는 경우
  • 반복 문자열 결합을 무조건 +로만 처리하는 경우

공식 문서 기준으로 더 보면 좋은 자료

✏️ 메서드와 배열, 문자열 직접 해보기

정수 배열의 합과 평균을 반환하는 메서드를 만들고, 인자로 넘긴 배열이 메서드 안에서 어떻게 다뤄지는지 출력으로 확인하라.

실습 순서

  1. 파일 생성hello-java 프로젝트에 src/main/java/com/example/ch02/practice/ArrayStatsPractice.java를 만듭니다.
  2. 뼈대 입력 — 아래 뼈대를 그대로 입력합니다.

    package com.example.ch02.practice;
    
    public class ArrayStatsPractice {
        public static void main(String[] args) {
            // 1) 정수 배열을 선언하고, 합을 반환하는 sum(int[]) 메서드를 만들어 출력한다
            // 2) 평균을 반환하는 average(int[]) 메서드를 만들어 출력한다
            // 3) 메서드 안에서 배열 요소를 바꾼 뒤, 호출한 쪽 배열도 바뀌는지 출력으로 확인한다
        }
    }
    
  3. 하나씩 구현 — 주석의 과제를 한 항목씩 구현합니다.

  4. 실행·확인mvn compile exec:java -Dexec.mainClass="com.example.ch02.practice.ArrayStatsPractice" — 추가할 때마다 다시 실행해 출력을 확인합니다.

정리

메서드, 배열, 문자열은 각각 따로 외우는 문법 목록이 아닙니다. 이 셋은 작은 프로그램을 읽기 쉬운 구조로 바꾸고, 여러 데이터를 다루고, 텍스트 입력을 처리하게 만드는 가장 기본적인 도구입니다.

한 줄 정리

좋은 Java 입문은 변수와 제어문 다음에 메서드, 배열, 문자열을 한 묶음으로 이해하는 데서부터 안정됩니다.


2.3 객체지향 프로그래밍

🎯 목표: 캡슐화·상속·다형성·추상화 4원칙을 한 예제로 시연한다.

개요

이 문서는 Java에서 객체지향 프로그래밍을 어떻게 이해해야 하는지 정리한 핵심 이론 문서입니다. 객체지향은 문법 항목 하나가 아니라, 상태와 행위를 함께 묶고 책임을 나누는 사고 방식에 가깝습니다.

왜 중요한가

Java를 배우다 보면 클래스, 인터페이스, 상속, 생성자 같은 문법은 금방 보입니다. 하지만 객체지향의 진짜 핵심은 문법 이름을 아는 것이 아니라, 어떤 책임을 어떤 객체에 두어야 코드가 덜 흔들리는지 판단하는 데 있습니다.

1. 클래스와 객체를 먼저 정확히 구분할 것

클래스는 설계도이고, 객체는 그 설계도로부터 만들어진 실행 단위입니다. (참고 코드 — 클래스와 객체 구분을 보여주는 이론 예시입니다.)

public class Account {
    private final String accountNumber;
    private int balance;

    public Account(String accountNumber, int balance) {
        this.accountNumber = accountNumber;
        this.balance = balance;
    }

    public void deposit(int amount) {
        balance += amount;
    }

    public int getBalance() {
        return balance;
    }
}
위 코드에서 Account는 클래스이고, new Account("123-456", 10000)으로 만들어진 값이 객체입니다.

2. 객체지향의 출발점은 데이터가 아니라 책임이다

초보 단계에서는 객체를 필드 묶음처럼 보기 쉽습니다. 하지만 객체지향에서는 무엇을 가지고 있는가보다 무엇을 책임지는가를 먼저 보는 편이 더 정확합니다. 예를 들어 계좌의 잔액 변경 규칙은 외부 코드가 직접 계산하기보다, Account 객체가 스스로 처리하는 편이 자연스럽습니다.

3. 캡슐화는 getter/setter 자동 생성과 다르다

캡슐화의 목적은 필드를 숨기는 행위 자체가 아니라, 잘못된 상태 변경을 막는 것입니다. (참고 코드 — 위 Account와 구분하려고 이름을 SafeAccount로 두었습니다.)

public class SafeAccount {
    private int balance;

    public void withdraw(int amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("출금 금액은 0보다 커야 합니다.");
        }
        if (balance < amount) {
            throw new IllegalStateException("잔액이 부족합니다.");
        }
        balance -= amount;
    }
}
이 구조가 중요한 이유는, 외부 코드가 balance를 직접 수정하지 못하고 객체의 규칙을 통과해야 하기 때문입니다.

4. 상속보다 먼저 합성을 의식할 것

상속은 공통 구조를 재사용하는 데 유용하지만, 모든 재사용 문제를 상속으로 해결하려 하면 결합도가 높아집니다.

  • 상속: is-a 관계가 분명할 때
  • 합성: 다른 객체를 내부에 두고 협력할 때 실무에서는 상속보다 합성이 더 안전한 경우가 많습니다.

5. 다형성은 같은 메시지에 다른 응답을 만드는 구조다

다형성의 핵심은 부모 타입으로 다루더라도 실제 구현에 따라 동작이 달라지는 것입니다. (참고 코드 — 하나의 인터페이스에 두 구현을 두는 이론 예시입니다.)

public interface DiscountPolicy {
    int discount(int price);
}

public class FixedDiscountPolicy implements DiscountPolicy {
    @Override
    public int discount(int price) {
        return 1000;
    }
}

public class RateDiscountPolicy implements DiscountPolicy {
    @Override
    public int discount(int price) {
        return price / 10;
    }
}
이 구조에서는 주문 서비스가 구체 구현이 아니라 DiscountPolicy 인터페이스에 의존할 수 있습니다. 정책이 바뀌어도 서비스 전체를 고치지 않아도 된다는 점이 중요합니다.

6. 추상화와 인터페이스는 변경 가능성을 다루는 도구다

추상화는 복잡한 세부 구현을 가리고, 사용자가 알아야 할 최소한의 계약만 드러내는 방식입니다. 인터페이스는 특히 협력 지점을 분리할 때 강력합니다. 다만 인터페이스를 많이 만드는 것 자체가 좋은 설계는 아닙니다. 실제로 교체 가능성이 있는 지점에만 추상화를 두는 편이 낫습니다.

7. 생성자는 객체의 시작 상태를 보장해야 한다

좋은 생성자는 "이 객체가 유효한 상태로 시작한다"는 약속을 만듭니다.

  • 필수 값은 생성자에서 받기
  • 불완전한 객체를 만들지 않기
  • 생성 이후 거의 바뀌지 않는 값은 final로 고정하기 이 기준이 잡히면 객체를 더 신뢰할 수 있고, 테스트도 쉬워집니다.

8. 초중급 단계에서 자주 하는 실수

  • 객체를 단순 DTO처럼만 사용하고 규칙은 서비스에 몰아넣는 것
  • 캡슐화를 말하면서 실제로는 무의미한 setter를 모두 여는 것
  • 재사용을 이유로 상속을 과하게 쓰는 것
  • 인터페이스를 너무 일찍 도입해 구조만 복잡하게 만드는 것

실무 연결 포인트

Spring에서도 객체지향 원칙은 그대로 이어집니다. DI, 전략 패턴, 도메인 모델링, 테스트 가능한 구조는 모두 구현이 아니라 역할에 의존한다는 객체지향 감각 위에서 잘 작동합니다.

정리

객체지향 프로그래밍은 클래스를 많이 만드는 기술이 아닙니다. 상태와 행위를 함께 묶고, 책임을 분리하고, 변경 가능성이 큰 지점을 느슨하게 연결하는 설계 방식입니다.

한 줄 정리

객체지향의 핵심은 데이터를 객체에 넣는 것이 아니라, 책임을 객체에 배치하는 것입니다.

2.4 객체지향 원칙별 샘플 코드

🎯 목표: 객체지향 4원칙 각각을 최소 예제로 구분해 설명한다.

이 문서를 읽는 기준

객체지향 원칙은 정의를 외우는 순간보다, 같은 메시지를 보내도 객체 타입에 따라 다른 동작이 나온다는 장면을 직접 볼 때 훨씬 빨리 잡힙니다. 아래 예시는 게임 예제 흐름을 단순화한 것으로, 다형성과 오버라이딩을 가장 짧게 보여줍니다.

파일: src/main/java/com/example/ch02/EnemyAttackDemo.java

package com.example.ch02;

class Enemy {
    public String name;

    public Enemy(String name) {
        this.name = name;
    }

    public void attack() {
        System.out.println(name + "이 무기로 공격합니다!");
    }
}

class Goblin extends Enemy {
    public Goblin(String name) {
        super(name);
    }

    @Override
    public void attack() {
        System.out.println(name + "이 단검으로 찌릅니다!");
    }
}

class Troll extends Enemy {
    public Troll(String name) {
        super(name);
    }

    @Override
    public void attack() {
        System.out.println(name + "이 몽둥이로 내리칩니다!");
    }
}

public class EnemyAttackDemo {
    public static void main(String[] args) {
        Enemy goblin = new Goblin("고블린");
        Enemy troll = new Troll("트롤");
        goblin.attack();
        troll.attack();
    }
}

프로젝트 루트에서 아래 한 줄로 실행합니다. 이 절(2.4)의 다른 예제도 -Dexec.mainClass 값만 해당 Demo 클래스로 바꾸면 같은 방식으로 실행됩니다.

mvn compile exec:java -Dexec.mainClass="com.example.ch02.EnemyAttackDemo"
# Gradle 프로젝트: ./gradlew compileJava && java -cp build/classes/java/main com.example.ch02.EnemyAttackDemo
예상 결과
고블린이 단검으로 찌릅니다!
트롤이 몽둥이로 내리칩니다!

이 장면에서 중요한 점은 변수 타입이 둘 다 Enemy인데도 실제 동작은 하위 클래스 구현이 실행된다는 것입니다. 즉, 객체지향 원칙은 문법 이름보다 책임 분리와 메시지 위임으로 읽는 편이 더 정확합니다.

1. 캡슐화(Encapsulation)

2.3 3절에서 캡슐화의 목적이 잘못된 상태 변경을 막는 것임을 확인했습니다. 아래 두 예제는 그 원칙을 실행 코드로 보여줍니다 — 예제 1은 setter의 유효성 검사로, 예제 2는 입출금 메서드로만 잔액을 바꾸게 하는 방식으로 상태를 지킵니다.

예제 1: Private 필드와 Public 메서드 사용

파일: src/main/java/com/example/ch02/EncapsulationDemo.java

package com.example.ch02;

class Person {
    // Private 필드
    private String name;
    private int age;

    // Public getter와 setter 메서드
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name; // 'this'는 인스턴스 변수를 가리킵니다.
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        if(age >= 0) { // 나이의 유효성 검사
            this.age = age;
        }
    }
}

// 사용 예시
public class EncapsulationDemo {
    public static void main(String[] args) {
        Person person = new Person();
        person.setName("김자바");
        person.setAge(20);
        person.setAge(-5); // 유효성 검사에 걸려 무시됩니다.
        System.out.println(person.getName() + " : " + person.getAge());
    }
}
예상 결과
김자바 : 20

설명:

Person 클래스는 nameage 필드를 private으로 선언하여 외부에서 직접 접근하지 못하게 합니다.

대신 public 메서드인 getName(), setName(), getAge(), setAge()를 통해서만 접근 가능합니다.

예제 2: 은행 계좌 클래스

파일: src/main/java/com/example/ch02/BankAccountDemo.java

package com.example.ch02;

class BankAccount {
    private double balance;

    // 생성자
    public BankAccount(double initialBalance) {
        balance = initialBalance;
    }

    // 입금 메서드
    public void deposit(double amount) {
        if(amount > 0) {
            balance += amount;
        }
    }

    // 출금 메서드
    public void withdraw(double amount) {
        if(amount > 0 && amount <= balance) {
            balance -= amount;
        }
    }

    // 잔액 조회 메서드 (읽기 전용)
    public double getBalance() {
        return balance;
    }
}

// 사용 예시
public class BankAccountDemo {
    public static void main(String[] args) {
        BankAccount account = new BankAccount(10000);
        account.deposit(5000);
        account.withdraw(3000);
        System.out.println("잔액: " + account.getBalance());
    }
}
예상 결과
잔액: 12000.0

설명:

BankAccount 클래스는 balance 필드를 private으로 선언하고, 입출금을 위한 메서드를 제공합니다. 이를 통해 데이터의 무결성을 유지하고, 직접적인 접근을 방지합니다.

2. 상속(Inheritance)

부모 클래스의 필드·메서드를 자식 클래스가 물려받는 구조를 세 예제로 확인합니다 — 예제 1·2는 물려받은 기능에 자식 고유 기능을 더하는 형태이고, 예제 3은 물려받은 메서드를 재정의(오버라이딩)까지 하는 형태입니다.

예제 1: 동물과 개 클래스

파일: src/main/java/com/example/ch02/InheritanceDemo.java

package com.example.ch02;

// 부모 클래스
class Animal {
    public void eat() {
        System.out.println("동물이 먹습니다.");
    }
}

// 자식 클래스
class Dog extends Animal {
    public void bark() {
        System.out.println("개가 짖습니다.");
    }
}

// 사용 예시
public class InheritanceDemo {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.eat();  // 부모 클래스로부터 상속된 메서드
        dog.bark(); // 자식 클래스에서 정의한 메서드
    }
}

설명:

Dog 클래스는 Animal 클래스를 상속받아 eat() 메서드를 사용할 수 있으며, 자신만의 bark() 메서드를 추가로 제공합니다.

예제 2: 차량과 자동차 클래스

파일: src/main/java/com/example/ch02/Car.java

package com.example.ch02;

// 부모 클래스
class Vehicle {
    protected String brand = "현대";

    public void honk() {
        System.out.println("빵빵!");
    }
}

// 자식 클래스
public class Car extends Vehicle {
    private String modelName = "소나타";

    public static void main(String[] args) {
        Car myCar = new Car();
        myCar.honk(); // 부모 클래스로부터 상속된 메서드
        System.out.println(myCar.brand + " " + myCar.modelName);
    }
}

설명:

Car 클래스는 Vehicle 클래스를 상속받아 brand 필드와 honk() 메서드를 사용할 수 있습니다.

예제 3: 직원과 매니저 클래스

파일: src/main/java/com/example/ch02/ManagerDemo.java

package com.example.ch02;

class Staff {
    public void work() {
        System.out.println("직원이 일합니다.");
    }
}

class Manager extends Staff {
    @Override
    public void work() {
        System.out.println("매니저가 일하고 있습니다.");
    }

    public void manage() {
        System.out.println("매니저가 관리하고 있습니다.");
    }
}

public class ManagerDemo {
    public static void main(String[] args) {
        Staff emp = new Staff();
        emp.work(); // 출력: 직원이 일합니다.

        Manager mgr = new Manager();
        mgr.work(); // 출력: 매니저가 일하고 있습니다.
        mgr.manage(); // 출력: 매니저가 관리하고 있습니다.
    }
}

설명:

Manager 클래스는 Staff 클래스를 상속받아 work() 메서드를 재정의하고, 추가로 manage() 메서드를 제공합니다. (직원 클래스 이름은 2.5의 Employee 예제와 겹치지 않게 Staff로 두었습니다.)

3. 다형성(Polymorphism)

같은 이름의 호출이 상황에 따라 다르게 동작하는 세 가지 형태를 봅니다 — 예제 1은 매개변수에 따라 갈리는 오버로딩, 예제 2는 부모 타입 변수로 호출해도 실제 객체의 구현이 실행되는 오버라이딩, 예제 3은 인터페이스 타입 하나로 서로 다른 구현을 다루는 방식입니다.

예제 1: 메서드 오버로딩 (Overloading)

파일: src/main/java/com/example/ch02/OverloadingDemo.java

package com.example.ch02;

class Calculator {
    // 두 개의 정수 합
    public int add(int a, int b) {
        return a + b;
    }

    // 세 개의 정수 합
    public int add(int a, int b, int c) {
        return a + b + c;
    }

    // 두 개의 실수 합
    public double add(double a, double b) {
        return a + b;
    }
}

// 사용 예시
public class OverloadingDemo {
    public static void main(String[] args) {
        Calculator calc = new Calculator();
        System.out.println(calc.add(2, 3)); // 출력: 5
        System.out.println(calc.add(2, 3, 4)); // 출력: 9
        System.out.println(calc.add(2.5, 3.5)); // 출력: 6.0
    }
}

설명:

Calculator 클래스는 같은 이름의 add 메서드를 여러 개 정의하여, 입력 매개변수에 따라 다른 동작을 수행합니다.

예제 2: 메서드 오버라이딩 (Overriding)과 다형성

파일: src/main/java/com/example/ch02/OverridingDemo.java

package com.example.ch02;

class Pet {
    public void makeSound() {
        System.out.println("동물이 소리를 냅니다.");
    }
}

class Cat extends Pet {
    @Override
    public void makeSound() {
        System.out.println("고양이가 야옹합니다.");
    }
}

class Puppy extends Pet {
    @Override
    public void makeSound() {
        System.out.println("개가 멍멍합니다.");
    }
}

public class OverridingDemo {
    public static void main(String[] args) {
        Pet myAnimal = new Pet();
        Pet myCat = new Cat();
        Pet myDog = new Puppy();

        myAnimal.makeSound(); // 출력: 동물이 소리를 냅니다.
        myCat.makeSound();    // 출력: 고양이가 야옹합니다.
        myDog.makeSound();    // 출력: 개가 멍멍합니다.
    }
}

설명:

부모 타입 Pet 변수에 담아도 실제 객체(Cat, Puppy)의 오버라이딩된 메서드가 실행됩니다. (상속 절의 Animal/Dog와 겹치지 않게 Pet/Puppy로 두었습니다.)

예제 3: 인터페이스와 다형성

파일: src/main/java/com/example/ch02/PolymorphismDemo.java

package com.example.ch02;

interface Shape {
    public double getArea();
}

class Circle implements Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }
}

class Rectangle implements Shape {
    private double width, height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double getArea() {
        return width * height;
    }
}

public class PolymorphismDemo {
    public static void main(String[] args) {
        Shape s1 = new Circle(5);
        Shape s2 = new Rectangle(4, 6);

        System.out.println("원의 면적: " + s1.getArea());
        System.out.println("사각형의 면적: " + s2.getArea());
    }
}

설명:

Shape 인터페이스를 구현한 CircleRectangle 클래스는 getArea() 메서드를 각각 다르게 구현합니다. 이를 통해 인터페이스 타입의 변수로 다양한 객체를 다룰 수 있습니다.

4. 추상화(Abstraction)

공통 계약만 드러내고 세부 구현을 감추는 방식을 세 예제로 확인합니다 — 예제 1은 추상 클래스, 예제 2는 인터페이스, 예제 3은 공통 흐름을 부모가 고정하는 템플릿 메서드 패턴입니다(같은 패턴을 2.5에서 더 깊게 다룹니다).

예제 1: 추상 클래스 사용

파일: src/main/java/com/example/ch02/AbstractionDemo.java

package com.example.ch02;

abstract class AbstractAnimal {
    public abstract void makeSound();

    public void sleep() {
        System.out.println("동물이 잠을 잡니다.");
    }
}

class Beagle extends AbstractAnimal {
    @Override
    public void makeSound() {
        System.out.println("개가 멍멍합니다.");
    }
}

public class AbstractionDemo {
    public static void main(String[] args) {
        AbstractAnimal dog = new Beagle();
        dog.makeSound(); // 출력: 개가 멍멍합니다.
        dog.sleep();     // 출력: 동물이 잠을 잡니다.
    }
}

설명:

AbstractAnimal 추상 클래스는 makeSound() 추상 메서드를 선언하여 하위 클래스에서 구현하도록 합니다. Beagle 클래스는 이를 구현하여 구체적인 동작을 정의합니다. (상속 절의 Animal/Dog와 겹치지 않게 이름을 바꿔 두었습니다.)

예제 2: 인터페이스 사용

파일: src/main/java/com/example/ch02/RemoteControlDemo.java

package com.example.ch02;

interface RemoteControl {
    void turnOn();
    void turnOff();
}

class Television implements RemoteControl {
    @Override
    public void turnOn() {
        System.out.println("TV를 켭니다.");
    }

    @Override
    public void turnOff() {
        System.out.println("TV를 끕니다.");
    }
}

public class RemoteControlDemo {
    public static void main(String[] args) {
        RemoteControl tv = new Television();
        tv.turnOn();  // 출력: TV를 켭니다.
        tv.turnOff(); // 출력: TV를 끕니다.
    }
}

설명:

RemoteControl 인터페이스는 전자 기기의 공통 동작을 정의하고, Television 클래스는 이를 구현하여 구체적인 동작을 제공합니다.

예제 3: 추상 클래스를 이용한 템플릿 메서드 패턴

파일: src/main/java/com/example/ch02/TemplateMethodDemo.java

package com.example.ch02;

abstract class Game {
    abstract void initialize();
    abstract void startPlay();
    abstract void endPlay();

    // 템플릿 메서드
    public final void play() {
        initialize();
        startPlay();
        endPlay();
    }
}

class Football extends Game {
    @Override
    void initialize() {
        System.out.println("축구 게임 초기화!");
    }

    @Override
    void startPlay() {
        System.out.println("축구 게임 시작!");
    }

    @Override
    void endPlay() {
        System.out.println("축구 게임 종료!");
    }
}

public class TemplateMethodDemo {
    public static void main(String[] args) {
        Game game = new Football();
        game.play();
        /*
        출력:
        축구 게임 초기화!
        축구 게임 시작!
        축구 게임 종료!
        */
    }
}

설명:

Game 추상 클래스는 게임 진행의 템플릿을 정의하고, 구체적인 구현은 하위 클래스인 Football에서 제공합니다.


✏️ 객체지향 원칙별 샘플 코드 직접 해보기

위 샘플 중 하나에서 필드를 private로 바꾸고, 외부가 메서드로만 접근하도록 캡슐화를 강화해 보라.

실습 순서

  1. 파일 열기 — 위 Car.java(2.4 상속 예제 2)
  2. 수정protected String brand 필드를 private로 바꾸고, getter 메서드로만 읽게 고칩니다.
  3. 재실행 — 이 절(2.4) 대표 실행 명령 재사용 — -Dexec.mainClass 값만 com.example.ch02.Car로 변경

2.5 추상 클래스 활용 예제

🎯 목표: 추상 클래스와 템플릿 메서드로 공통 골격과 가변 부분을 분리한다.

이 문서를 읽는 기준

추상 클래스는 "new로 바로 만들 수 없는 클래스"라는 정의만 외우면 오래 남지 않습니다. 더 중요한 것은 공통 흐름은 부모가 잡고, 달라지는 부분만 자식이 채운다는 구조를 보는 것입니다.

템플릿 메서드 예제를 단순화하면 아래처럼 읽을 수 있습니다.

파일: src/main/java/com/example/ch02/CsvProcessorDemo.java

package com.example.ch02;

abstract class AbstractDataProcessor {
    public final void process() {
        System.out.println("--- " + getClass().getSimpleName() + " 실행 ---");
        String data = selectData();
        String processedData = transformData(data);
        saveData(processedData);
    }

    private String selectData() {
        System.out.println("[공통] 데이터베이스에서 데이터를 조회합니다.");
        return "id,name,role";
    }

    private void saveData(String data) {
        System.out.println("[공통] 변환된 데이터를 파일에 씁니다: " + data);
        System.out.println("[저장] " + getFileName() + " 파일이 성공적으로 생성되었습니다.");
    }

    protected abstract String transformData(String data);
    protected abstract String getFileName();
}

class CsvDataProcessor extends AbstractDataProcessor {
    @Override
    protected String transformData(String data) {
        System.out.println("[CSV] 데이터를 쉼표(,)로 구분된 형식으로 변환합니다.");
        return data.replace(",", ", ");
    }

    @Override
    protected String getFileName() {
        return "data.csv";
    }
}

// 사용 예시
public class CsvProcessorDemo {
    public static void main(String[] args) {
        AbstractDataProcessor processor = new CsvDataProcessor();
        processor.process();
    }
}

프로젝트 루트에서 아래 한 줄로 실행합니다. 이 절(2.5)의 다른 예제도 -Dexec.mainClass 값만 해당 Demo 클래스로 바꾸면 같은 방식으로 실행됩니다.

mvn compile exec:java -Dexec.mainClass="com.example.ch02.CsvProcessorDemo"
# Gradle 프로젝트: ./gradlew compileJava && java -cp build/classes/java/main com.example.ch02.CsvProcessorDemo
예상 결과
--- CsvDataProcessor 실행 ---
[공통] 데이터베이스에서 데이터를 조회합니다.
[CSV] 데이터를 쉼표(,)로 구분된 형식으로 변환합니다.
[공통] 변환된 데이터를 파일에 씁니다: id, name, role
[저장] data.csv 파일이 성공적으로 생성되었습니다.

이 예제에서 핵심은 process() 전체 순서는 부모 클래스가 고정하고, 실제 변환 규칙과 파일명만 자식 클래스가 결정한다는 점입니다. 즉, 추상 클래스는 단순 상속 문법이 아니라 공통 뼈대를 재사용하면서도 확장 지점을 열어 두는 도구입니다.

예제 1: 직원 관리 시스템

추상 클래스와 구체 클래스

정규직과 계약직의 급여 계산식이 서로 다른 상황입니다. 공통 정보(이름·ID)와 출력은 부모가 들고, 계산식만 자식이 채우는 분담을 봅니다.

파일: src/main/java/com/example/ch02/EmployeeSalaryDemo.java

package com.example.ch02;

// 추상 클래스
abstract class Employee {
    protected String name;
    protected int id;

    public Employee(String name, int id) {
        this.name = name;
        this.id = id;
    }

    // 추상 메서드
    public abstract double calculateSalary();

    // 일반 메서드
    public void displayInfo() {
        System.out.println("이름: " + name);
        System.out.println("ID: " + id);
    }
}

// 정규직 직원 클래스
class FullTimeEmployee extends Employee {
    private double annualSalary;

    public FullTimeEmployee(String name, int id, double annualSalary) {
        super(name, id);
        this.annualSalary = annualSalary;
    }

    @Override
    public double calculateSalary() {
        return annualSalary / 12; // 월급 계산
    }
}

// 계약직 직원 클래스
class ContractEmployee extends Employee {
    private double hourlyRate;
    private int hoursWorked;

    public ContractEmployee(String name, int id, double hourlyRate, int hoursWorked) {
        super(name, id);
        this.hourlyRate = hourlyRate;
        this.hoursWorked = hoursWorked;
    }

    @Override
    public double calculateSalary() {
        return hourlyRate * hoursWorked; // 급여 계산
    }
}

// 사용 예시
public class EmployeeSalaryDemo {
    public static void main(String[] args) {
        Employee emp1 = new FullTimeEmployee("홍길동", 101, 60000000);
        Employee emp2 = new ContractEmployee("김영희", 102, 20000, 160);

        emp1.displayInfo();
        System.out.println("월급: " + emp1.calculateSalary() + "원\\n");

        emp2.displayInfo();
        System.out.println("급여: " + emp2.calculateSalary() + "원");
    }
}

설명:

Employee 추상 클래스는 공통 필드와 메서드를 가지며, calculateSalary() 추상 메서드를 통해 직원 유형에 따라 급여 계산 방법을 다르게 구현.


예제 2: 그래픽 요소의 계층 구조

추상 클래스와 다형성의 결합

원과 사각형처럼 그리는 방법은 다르지만 이동 방식은 같은 도형들입니다. 추상 클래스가 공통 동작(move)을 제공하고, 부모 타입 변수로 서로 다른 draw()가 실행되는 다형성을 함께 봅니다.

파일: src/main/java/com/example/ch02/GraphicDemo.java

package com.example.ch02;

// 추상 클래스
abstract class Graphic {
    // 추상 메서드
    public abstract void draw();

    // 공통 메서드
    public void move(int x, int y) {
        System.out.println("좌표 (" + x + ", " + y + ")로 이동합니다.");
    }
}

// 구체적인 그래픽 요소 클래스
class GraphicCircle extends Graphic {
    private int radius;

    public GraphicCircle(int radius) {
        this.radius = radius;
    }

    @Override
    public void draw() {
        System.out.println("반지름이 " + radius + "인 원을 그립니다.");
    }
}

class GraphicRectangle extends Graphic {
    private int width, height;

    public GraphicRectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public void draw() {
        System.out.println("가로 " + width + ", 세로 " + height + "인 사각형을 그립니다.");
    }
}

// 사용 예시
public class GraphicDemo {
    public static void main(String[] args) {
        Graphic circle = new GraphicCircle(5);
        Graphic rectangle = new GraphicRectangle(4, 6);

        circle.draw();
        circle.move(10, 20);

        rectangle.draw();
        rectangle.move(30, 40);
    }
}

설명:

Graphic 추상 클래스는 그래픽 요소의 공통 동작을 정의하며, GraphicCircleGraphicRectangle 클래스는 이를 상속받아 구체적인 draw() 메서드를 구현. (2.4 다형성 절의 Circle/Rectangle과 겹치지 않게 이름을 바꿔 두었습니다.)


예제 3: 템플릿 메서드 패턴의 응용

데이터 처리 과정의 추상화

읽기→처리→저장 순서는 고정하고 처리 단계만 CSV·XML별로 갈라지는 구조입니다. 절 첫머리의 CsvProcessorDemo와 같은 템플릿 메서드 패턴을 두 형식으로 확장한 예제입니다.

파일: src/main/java/com/example/ch02/DataProcessorDemo.java

package com.example.ch02;

abstract class DataProcessor {
    // 템플릿 메서드
    public final void process() {
        readData();
        processData();
        saveData();
    }

    // 공통 구현 메서드
    public void readData() {
        System.out.println("데이터를 읽습니다.");
    }

    // 하위 클래스에서 구현할 추상 메서드
    public abstract void processData();

    // 공통 구현 메서드
    public void saveData() {
        System.out.println("데이터를 저장합니다.");
    }
}

// CSV 데이터 처리 클래스
class CSVDataProcessor extends DataProcessor {
    @Override
    public void processData() {
        System.out.println("CSV 데이터를 처리합니다.");
    }
}

// XML 데이터 처리 클래스
class XMLDataProcessor extends DataProcessor {
    @Override
    public void processData() {
        System.out.println("XML 데이터를 처리합니다.");
    }
}

// 사용 예시
public class DataProcessorDemo {
    public static void main(String[] args) {
        DataProcessor csvProcessor = new CSVDataProcessor();
        csvProcessor.process();
        /*
        출력:
        데이터를 읽습니다.
        CSV 데이터를 처리합니다.
        데이터를 저장합니다.
        */

        DataProcessor xmlProcessor = new XMLDataProcessor();
        xmlProcessor.process();
        /*
        출력:
        데이터를 읽습니다.
        XML 데이터를 처리합니다.
        데이터를 저장합니다.
        */
    }
}

설명:

DataProcessor 추상 클래스는 데이터 처리 흐름을 정의하는 템플릿 메서드 process()를 제공. 세부적인 데이터 처리 부분은 하위 클래스에서 구현.


예제 4: 게임 캐릭터의 능력 정의

추상 클래스와 인터페이스의 조합

모든 캐릭터가 공유하는 동작은 추상 클래스로, 마법사만 가지는 능력은 인터페이스로 나누는 조합입니다. 상속 계층 하나로 다 담지 않고 역할별 계약을 분리하는 감각을 봅니다.

파일: src/main/java/com/example/ch02/GameCharacterDemo.java

package com.example.ch02;

// 추상 클래스
abstract class Character {
    protected String name;

    public Character(String name) {
        this.name = name;
    }

    // 추상 메서드
    public abstract void attack();

    // 공통 메서드
    public void move() {
        System.out.println(name + "이(가) 이동합니다.");
    }
}

// 인터페이스
interface Magic {
    void castSpell();
}

// 전사 클래스
class Warrior extends Character {
    public Warrior(String name) {
        super(name);
    }

    @Override
    public void attack() {
        System.out.println(name + "이(가) 검으로 공격합니다.");
    }
}

// 마법사 클래스
class Wizard extends Character implements Magic {
    public Wizard(String name) {
        super(name);
    }

    @Override
    public void attack() {
        System.out.println(name + "이(가) 지팡이로 공격합니다.");
    }

    @Override
    public void castSpell() {
        System.out.println(name + "이(가) 마법을 시전합니다.");
    }
}

// 사용 예시
public class GameCharacterDemo {
    public static void main(String[] args) {
        Character warrior = new Warrior("아서");
        warrior.move();
        warrior.attack();

        Wizard wizard = new Wizard("메르린");
        wizard.move();
        wizard.attack();
        wizard.castSpell();
    }
}

설명: Character 추상 클래스는 게임 캐릭터의 기본 동작을 정의하고, Magic 인터페이스는 마법사만의 능력을 정의. WarriorWizard 클래스는 각각 이를 구현하여 캐릭터의 특성을 나타냄.


예제 5: 가전 제품의 전원 관리

추상 클래스의 기본 구현 제공

전원 켜기·끄기 같은 공통 동작은 추상 클래스가 완성된 구현으로 제공하고, 제품별 기능만 추상 메서드로 남기는 구조입니다. 전원이 꺼진 상태의 분기까지 함께 봅니다.

파일: src/main/java/com/example/ch02/ApplianceDemo.java

package com.example.ch02;

abstract class Appliance {
    protected boolean isOn = false;

    // 전원 켜기
    public void turnOn() {
        isOn = true;
        System.out.println(getClass().getSimpleName() + " 전원이 켜졌습니다.");
    }

    // 전원 끄기
    public void turnOff() {
        isOn = false;
        System.out.println(getClass().getSimpleName() + " 전원이 꺼졌습니다.");
    }

    // 추상 메서드: 기능 수행
    public abstract void performFunction();
}

// 냉장고 클래스
class Refrigerator extends Appliance {
    @Override
    public void performFunction() {
        if(isOn) {
            System.out.println("냉장고가 냉각을 시작합니다.");
        } else {
            System.out.println("냉장고 전원이 꺼져 있습니다.");
        }
    }
}

// 세탁기 클래스
class WashingMachine extends Appliance {
    @Override
    public void performFunction() {
        if(isOn) {
            System.out.println("세탁기가 세탁을 시작합니다.");
        } else {
            System.out.println("세탁기 전원이 꺼져 있습니다.");
        }
    }
}

// 사용 예시
public class ApplianceDemo {
    public static void main(String[] args) {
        Appliance fridge = new Refrigerator();
        fridge.turnOn();
        fridge.performFunction();

        Appliance washer = new WashingMachine();
        washer.performFunction();
        washer.turnOn();
        washer.performFunction();
    }
}

설명: Appliance 추상 클래스는 가전 제품의 공통 동작(전원 관리)을 구현하고, 구체적인 기능은 추상 메서드 performFunction()을 통해 하위 클래스에서 정의.


예제 6: 온라인 주문 처리 시스템

추상 클래스의 부분 구현 활용

주문 흐름 가운데 결제 방법은 반드시 자식이 정하고, 선물 포장 여부는 필요한 자식만 재정의하는 구조입니다. 기본값을 돌려주다가 선택적으로 바꾸는 후크(hook) 메서드가 처음 등장하는 예제입니다.

파일: src/main/java/com/example/ch02/OrderProcessorDemo.java

package com.example.ch02;

abstract class OrderProcessor {
    // 주문 처리 전체 흐름
    public void processOrder() {
        selectItem();
        makePayment();
        if(isGift()) {
            wrapGift();
        }
        shipItem();
    }

    // 공통 구현 메서드
    public void selectItem() {
        System.out.println("상품을 선택했습니다.");
    }

    // 추상 메서드
    public abstract void makePayment();

    // 후크 메서드 (필요에 따라 오버라이드)
    public boolean isGift() {
        return false;
    }

    // 공통 구현 메서드
    public void wrapGift() {
        System.out.println("선물 포장을 합니다.");
    }

    // 공통 구현 메서드
    public void shipItem() {
        System.out.println("상품을 배송합니다.");
    }
}

// 신용카드 결제 클래스
class CreditCardOrder extends OrderProcessor {
    @Override
    public void makePayment() {
        System.out.println("신용카드로 결제합니다.");
    }
}

// 페이팔 결제 클래스
class PayPalOrder extends OrderProcessor {
    @Override
    public void makePayment() {
        System.out.println("PayPal로 결제합니다.");
    }

    @Override
    public boolean isGift() {
        return true;
    }
}

// 사용 예시
public class OrderProcessorDemo {
    public static void main(String[] args) {
        OrderProcessor order1 = new CreditCardOrder();
        order1.processOrder();
        /*
        출력:
        상품을 선택했습니다.
        신용카드로 결제합니다.
        상품을 배송합니다.
        */

        OrderProcessor order2 = new PayPalOrder();
        order2.processOrder();
        /*
        출력:
        상품을 선택했습니다.
        PayPal로 결제합니다.
        선물 포장을 합니다.
        상품을 배송합니다.
        */
    }
}

설명: OrderProcessor 추상 클래스는 주문 처리의 전체 흐름을 정의하며, 결제 방법과 선물 포장 여부는 하위 클래스에서 결정.


예제 7: 교육 과정 관리

추상 클래스에서 공통 동작과 추상화된 동작 정의

수강 흐름 다섯 단계 중 시험 방식 하나만 온라인·오프라인별로 달라지는 구조입니다. 고정 단계가 많고 가변 지점이 하나뿐일 때 템플릿이 얼마나 단순해지는지 봅니다.

파일: src/main/java/com/example/ch02/CourseDemo.java

package com.example.ch02;

abstract class Course {
    public void takeCourse() {
        enroll();
        attendLectures();
        submitAssignments();
        takeExams();
        getCertificate();
    }

    // 공통 구현 메서드
    public void enroll() {
        System.out.println("과정에 등록했습니다.");
    }

    public void attendLectures() {
        System.out.println("강의를 듣습니다.");
    }

    public void submitAssignments() {
        System.out.println("과제를 제출합니다.");
    }

    // 추상 메서드
    public abstract void takeExams();

    // 공통 구현 메서드
    public void getCertificate() {
        System.out.println("수료증을 받습니다.");
    }
}

// 온라인 과정 클래스
class OnlineCourse extends Course {
    @Override
    public void takeExams() {
        System.out.println("온라인 시험을 봅니다.");
    }
}

// 오프라인 과정 클래스
class OfflineCourse extends Course {
    @Override
    public void takeExams() {
        System.out.println("오프라인 시험을 봅니다.");
    }
}

// 사용 예시
public class CourseDemo {
    public static void main(String[] args) {
        Course onlineCourse = new OnlineCourse();
        onlineCourse.takeCourse();
        /*
        출력:
        과정에 등록했습니다.
        강의를 듣습니다.
        과제를 제출합니다.
        온라인 시험을 봅니다.
        수료증을 받습니다.
        */

        Course offlineCourse = new OfflineCourse();
        offlineCourse.takeCourse();
        /*
        출력:
        과정에 등록했습니다.
        강의를 듣습니다.
        과제를 제출합니다.
        오프라인 시험을 봅니다.
        수료증을 받습니다.
        */
    }
}

설명: Course 추상 클래스는 교육 과정의 일반적인 흐름을 정의하고, 시험 방식은 추상 메서드로 선언하여 하위 클래스에서 구체화.


예제 8: 파일 시스템 구성

추상 클래스와 재귀적 구조 표현

파일과 디렉터리를 같은 부모 타입으로 묶어, 중첩된 트리 구조를 재귀 호출 한 번으로 출력하는 예제입니다. 디렉터리가 자기 안의 구성 요소에게 같은 display를 위임하는 지점이 핵심입니다.

파일: src/main/java/com/example/ch02/FileSystemDemo.java

package com.example.ch02;

import java.util.ArrayList;
import java.util.List;

abstract class FileSystemComponent {
    protected String name;

    public FileSystemComponent(String name) {
        this.name = name;
    }

    // 추상 메서드
    public abstract void display(String indent);
}

// 파일 클래스
class File extends FileSystemComponent {
    public File(String name) {
        super(name);
    }

    @Override
    public void display(String indent) {
        System.out.println(indent + "파일: " + name);
    }
}

// 디렉토리 클래스
class Directory extends FileSystemComponent {
    private List<FileSystemComponent> components = new ArrayList<>();

    public Directory(String name) {
        super(name);
    }

    public void addComponent(FileSystemComponent component) {
        components.add(component);
    }

    @Override
    public void display(String indent) {
        System.out.println(indent + "디렉토리: " + name);
        for(FileSystemComponent component : components) {
            component.display(indent + "    ");
        }
    }
}

// 사용 예시
public class FileSystemDemo {
    public static void main(String[] args) {
        Directory root = new Directory("루트");
        File file1 = new File("파일1.txt");
        File file2 = new File("파일2.txt");

        Directory subDir = new Directory("서브디렉토리");
        File file3 = new File("파일3.txt");

        root.addComponent(file1);
        root.addComponent(file2);
        root.addComponent(subDir);

        subDir.addComponent(file3);

        root.display("");
        /*
        출력:
        디렉토리: 루트
            파일: 파일1.txt
            파일: 파일2.txt
            디렉토리: 서브디렉토리
                파일: 파일3.txt
        */
    }
}

설명: FileSystemComponent 추상 클래스는 파일과 디렉터리의 공통 동작을 정의하며, 재귀적인 구조를 표현하기 위해 디렉터리 내에 구성 요소를 추가. import 문은 한 파일 규칙에 맞게 맨 위로 올렸습니다.


예제 9: 메시지 전송 시스템

추상 클래스의 계층 구조 확장

이메일·SMS처럼 전송 수단이 달라도 보낸 사람·받는 사람 구조는 같은 상황입니다. 공통 필드는 부모가 들고, 전송 방식만 자식이 구현합니다.

파일: src/main/java/com/example/ch02/MessageSenderDemo.java

package com.example.ch02;

abstract class MessageSender {
    protected String sender;
    protected String receiver;

    public MessageSender(String sender, String receiver) {
        this.sender = sender;
        this.receiver = receiver;
    }

    // 추상 메서드
    public abstract void sendMessage(String message);
}

// 이메일 전송 클래스
class EmailSender extends MessageSender {
    public EmailSender(String sender, String receiver) {
        super(sender, receiver);
    }

    @Override
    public void sendMessage(String message) {
        System.out.println("이메일 전송");
        System.out.println("보낸 사람: " + sender);
        System.out.println("받는 사람: " + receiver);
        System.out.println("메시지: " + message);
    }
}

// SMS 전송 클래스
class SMSSender extends MessageSender {
    public SMSSender(String sender, String receiver) {
        super(sender, receiver);
    }

    @Override
    public void sendMessage(String message) {
        System.out.println("SMS 전송");
        System.out.println("보낸 사람: " + sender);
        System.out.println("받는 사람: " + receiver);
        System.out.println("메시지: " + message);
    }
}

// 사용 예시
public class MessageSenderDemo {
    public static void main(String[] args) {
        MessageSender email = new EmailSender("admin@example.com", "user@example.com");
        email.sendMessage("안녕하세요, 이메일입니다.");

        MessageSender sms = new SMSSender("010-1234-5678", "010-8765-4321");
        sms.sendMessage("안녕하세요, SMS입니다.");
    }
}

설명: MessageSender 추상 클래스는 메시지 전송의 공통 인터페이스를 정의하고, EmailSenderSMSSender 클래스는 각각 이메일과 SMS 전송 방법을 구현.


예제 10: 추상 클래스의 다중 수준 상속

추상 클래스의 계층적 설계

추상 클래스가 추상 클래스를 상속하며 계약을 단계적으로 넓히고, 맨 아래 구체 클래스가 전부 구현하는 구조입니다. 중간 계층을 언제 추상으로 남길지 감을 잡는 예제입니다.

파일: src/main/java/com/example/ch02/MultiLevelInheritanceDemo.java

package com.example.ch02;

// 최상위 추상 클래스
abstract class Transport {
    public abstract void move();
}

// 중간 추상 클래스
abstract class AirTransport extends Transport {
    public abstract void fly();
}

// 구체 클래스
class Airplane extends AirTransport {
    @Override
    public void move() {
        System.out.println("비행기가 활주로를 이동합니다.");
    }

    @Override
    public void fly() {
        System.out.println("비행기가 하늘을 납니다.");
    }
}

// 사용 예시
public class MultiLevelInheritanceDemo {
    public static void main(String[] args) {
        Airplane airplane = new Airplane();
        airplane.move();
        airplane.fly();
    }
}

설명: Transport 추상 클래스를 상속한 AirTransport 추상 클래스는 추가적인 추상 메서드를 정의하며, Airplane 클래스는 이를 모두 구현. (2.4 상속 절의 Vehicle과 겹치지 않게 이름을 바꿔 두었습니다.)


결론

추상 클래스는 공통된 동작과 인터페이스를 정의하면서도, 하위 클래스에서의 구체적인 구현을 강제함으로써 코드의 유연성과 재사용성을 향상시킵니다. 다양한 상황에서 추상 클래스를 활용하면 구조적인 설계를 할 수 있으며, 이를 통해 유지보수성과 확장성이 뛰어난 코드를 작성할 수 있습니다.


자주 나는 에러 → 원인 (2.4·2.5 실습 공통)

에러 메시지 원인 → 확인할 것
class XxxDemo is public, should be declared in a file named XxxDemo.java 파일명과 public 클래스명이 다름 → 파일명을 리드인의 클래스명과 똑같이
duplicate class: com.example.ch02.Animal 앞 예제와 같은 클래스명이 패키지에 이미 있음 → 이 장의 예제 클래스명은 서로 겹치지 않게 지어져 있으니, 임의로 이름을 바꿨다면 원래 이름으로
Could not find or load main class com.example.ch02... -Dexec.mainClass 오타 또는 com.example.ch02. 접두 누락 → 리드인의 파일명(= 클래스명)과 대조
error: class, interface, or enum expected (파일 첫머리) package com.example.ch02;가 첫 줄이 아니거나 import가 클래스 선언 뒤에 있음 → 코드블록 순서 그대로 복사
컴파일은 되는데 실행 시 ClassNotFoundException 파일을 src/main/java/com/example/ch02/가 아닌 곳에 만듦 → 디렉터리 경로와 package 선언 일치 확인

✏️ 추상 클래스 활용 예제 직접 해보기

예제 중 하나에 새 하위 클래스를 추가하되 상위(골격) 코드는 수정하지 말고 확장만으로 동작시켜 보라(OCP).

실습 순서

  1. 파일 열기 — 위 GraphicDemo.java(2.5 예제 2)
  2. 수정GraphicTriangle 클래스를 추가하고, Graphic 추상 클래스는 그대로 둔 채 main에서 그리게 고칩니다.
  3. 재실행 — 위 절(2.5)의 실행 명령 재사용 — mvn compile exec:java -Dexec.mainClass="com.example.ch02.GraphicDemo"

2.9 객체지향 실전문제

🎯 목표: 앞에서 배운 객체지향을 요구사항 분해에 직접 적용한다.

개요

이 문서는 객체지향 프로그래밍 장을 읽은 뒤 바로 이어서 푸는 실전문제입니다. 클래스와 객체, 생성자, 캡슐화, 상속, 다형성, 추상화, 인터페이스, 예외 처리까지 Java 학습의 기초 주제를 다시 확인하는 데 목적이 있습니다.

어떻게 활용하면 좋은가

  • 각 문제를 먼저 스스로 풀어봅니다.
  • 코드만 작성하지 말고, 왜 그런 구조를 선택했는지 설명해봅니다.
  • 정답 예시는 한 가지 구현 방식일 뿐이며, 더 나은 이름이나 구조가 가능한지 비교해봅니다.

문제 구성

1. 객체 모델링과 OOP 개념 정리

  • 현실 세계의 대상을 클래스로 옮기는 문제
  • 캡슐화, 상속, 다형성, 추상화의 의미를 설명하는 문제

2. 클래스와 객체

  • 필드와 메서드를 정의하고 객체를 생성하는 문제
  • 여러 객체를 만들어 공통 구조를 확인하는 문제

3. 생성자

  • 기본 생성자와 매개변수 생성자를 비교하는 문제
  • this를 활용해 상태를 초기화하는 문제

4. 캡슐화

  • private 필드와 getter, setter의 역할을 확인하는 문제
  • 잘못된 입력을 방어하는 검증 로직을 추가하는 문제

5. 상속과 다형성

  • 부모 클래스와 자식 클래스의 공통점과 차이를 설계하는 문제
  • 메서드 오버라이딩과 다형적 호출을 확인하는 문제

6. 추상화와 인터페이스

  • 추상 클래스와 인터페이스의 역할 차이를 구분하는 문제
  • 구현체가 공통 계약을 어떻게 따르는지 확인하는 문제

7. 예외 처리

  • 실행 중 발생 가능한 오류를 다루는 문제
  • try-catch-finally의 흐름을 이해하는 문제

예시 문제

문제 1. Wallet 클래스를 캡슐화 방식으로 수정하기

상태를 외부에서 직접 바꿀 수 있던 지갑 클래스를, 객체 스스로 입출금 규칙을 지키는 형태로 고치는 문제입니다. 아래는 수정을 마친 정답 예시입니다(참고 코드).

class Wallet {
    private int balance;

    public void deposit(int amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("입금 금액은 0보다 커야 합니다.");
        }
        balance += amount;
    }

    public void withdraw(int amount) {
        if (amount <= 0 || amount > balance) {
            throw new IllegalArgumentException("출금 금액이 올바르지 않습니다.");
        }
        balance -= amount;
    }

    public int getBalance() {
        return balance;
    }
}
이 문제의 핵심은 상태를 외부에서 직접 바꾸지 못하게 막고, 객체 스스로 자신의 규칙을 지키게 만드는 것입니다.

문제 2. 다형성을 이용한 급여 계산

정규직과 시간제 직원의 급여를 같은 메서드 호출 calculatePay()로 계산하도록 설계하는 문제입니다. 아래는 정답 예시입니다(참고 코드).

abstract class Worker {
    abstract int calculatePay();
}

class FullTimeWorker extends Worker {
    private final int monthlySalary;

    FullTimeWorker(int monthlySalary) {
        this.monthlySalary = monthlySalary;
    }

    @Override
    int calculatePay() {
        return monthlySalary;
    }
}

class PartTimeWorker extends Worker {
    private final int hourlyRate;
    private final int hours;

    PartTimeWorker(int hourlyRate, int hours) {
        this.hourlyRate = hourlyRate;
        this.hours = hours;
    }

    @Override
    int calculatePay() {
        return hourlyRate * hours;
    }
}
이 문제는 같은 메시지 calculatePay()를 서로 다른 객체가 각자의 방식으로 처리한다는 점을 이해하는 데 의미가 있습니다.

함께 보면 좋은 추가 문제

  • 게임 관련 시나리오 (→ 2.9-1)는 객체 모델링, 상속, 추상 클래스, 제네릭을 한 번에 묶어 연습하는 확장 문제 모음입니다.

풀이 전 체크 포인트

  • 클래스가 자신의 상태를 스스로 관리하고 있는가
  • 상속이 단순 코드 재사용이 아니라 관계를 표현하고 있는가
  • 인터페이스와 추상 클래스를 구분해서 쓰고 있는가
  • 예외 처리가 흐름 제어가 아니라 오류 처리라는 점을 이해하고 있는가

정리

객체지향 기초 문제는 문법 연습이 아니라 구조 감각을 기르는 훈련에 가깝습니다. 정답을 외우기보다, 왜 이 문제에서 캡슐화나 다형성이 필요한지 설명할 수 있어야 다음 단계 설계 문서를 읽을 때도 흔들리지 않습니다.

한 줄 정리

객체지향 기초 문제의 목적은 정답 코드 작성보다, 객체가 어떤 책임을 가져야 하는지 판단하는 눈을 기르는 데 있습니다.

2.9-1 게임 관련 시나리오

🎯 목표: 캐릭터·아이템·전투를 객체 협력으로 모델링한다. 상속 vs 합성 판단이 핵심.

게임에 필요한 요소들에 대해서 고민해 보고 객체화 하는 연습을 통해 객체 활용에 대한 이해를 높인다.

각 문제 아래의 코드 펜스는 구현을 마쳤을 때 콘솔에 나와야 하는 예상 출력입니다. 정답 코드는 제시하지 않으므로, 출력이 똑같이 나오도록 클래스를 직접 설계합니다. 파일은 hello-java 프로젝트에 com.example.ch02.game처럼 별도 하위 패키지를 만들어 두면 2.4·2.5 예제의 Character·Goblin 같은 클래스와 이름이 겹치지 않습니다.

문제: 게임 캐릭터를 나타내는 Character 클래스를 작성하세요. 이 클래스는 name, health, level 등의 속성을 가지며, attack()defend() 메서드를 포함합니다. 이 클래스를 기반으로 여러 캐릭터 객체를 생성하고, 각 캐릭터의 행동을 구현하세요

용사이() 공격합니다!
악당이() 방어합니다!

문제: Character 클래스를 상속하는 Warrior, Mage, Archer 클래스를 작성하세요. 각 하위 클래스는 고유한 속성(예: mana 또는 range)과 메서드(예: castSpell(), shootArrow())를 추가합니다.

전사이() 공격합니다!
전사이() 강력한 공격을 합니다! 남은 스태미나: 40
마법사이() 공격합니다!
마법사이() 마법을 시전합니다! 남은 마나: 80
궁수이() 공격합니다!
궁수이() 화살을 발사합니다! 남은 화살: 29

문제: Enemy 클래스를 만들고, 이를 상속하는 Goblin, Troll, Dragon 클래스를 작성하세요. 각 클래스에서 attack() 메서드를 오버라이딩하여 적들이 서로 다른 공격 방식을 가지도록 하세요.

고블린이 단검으로 찌릅니다!
트롤이 몽둥이로 내리칩니다!
드래곤이 불을 뿜습니다!

문제: 캐릭터의 경험치와 레벨을 관리하는 Player 클래스를 작성하세요. experiencelevel 필드는 private으로 설정하고, 공개 메서드 gainExperience(int amount)를 통해서만 경험치를 증가시킬 수 있도록 하세요.

Jwon이() 경험치 1000점을 획득했습니다.
Jwon이() 레벨 2 상승했습니다!
Jwon이() 레벨 3 상승했습니다!
Jwon이() 레벨 4 상승했습니다!
Jwon이() 레벨 5 상승했습니다!

문제: 추상 클래스 Item을 만들고, Player 객체를 매개변수로 받는 추상 메서드 use(Player player)를 정의하세요. 이를 상속하는 HealthPotionManaPotion 클래스를 작성하고, 각각의 use(Player player) 메서드를 구현하여 플레이어의 체력과 마나를 회복시키는 로직을 포함하세요.

Jwon이() 체력을 20 회복했습니다.
Jwon이() 마나를 15 회복했습니다.

문제: 위의 모든 코드를 조합하여 아래 결과를 완성 하세요

Jwon이() 경험치 1000점을 획득했습니다.
Jwon이() 레벨 2 상승했습니다!
Jwon이() 레벨 3 상승했습니다!
Jwon이() 레벨 4 상승했습니다!
Jwon이() 레벨 5 상승했습니다!
Jwon의 체력이 -30만큼 변경되어 현재 체력: 70
Jwon의 마나가 -20만큼 변경되어 현재 마나: 30
Jwon의 현재 체력: 70
Jwon의 현재 마나: 30
Jwon이() 체력을 20 회복했습니다.
Jwon이() 마나를 15 회복했습니다.
Jwon의 현재 체력: 70
Jwon의 현재 마나: 30

문제: Movable 인터페이스를 정의하고 move(int x, int y) 메서드를 선언하세요. 이 인터페이스를 구현하는 PlayerNPC 클래스를 작성하여 캐릭터들이 게임 맵에서 이동할 수 있도록 하세요.

용사이() 위치 (10, 20) 이동했습니다.
NPC 상인이() 위치 (15, 25) 이동했습니다.
용사이() 위치 (30, 40) 이동했습니다.
NPC 상인이() 위치 (35, 45) 이동했습니다.

문제: Weapon 클래스를 만들고 attack() 메서드를 정의하세요. 이 클래스를 상속하는 Sword, Bow, Staff 클래스를 작성하고, 각 무기에서 attack() 메서드를 오버라이딩하여 다른 공격 방식을 구현하세요.

무기로 공격합니다.
검으로 베기 공격을 합니다!
활로 화살을 발사합니다!
지팡이로 마법 공격을 합니다!

무기 배열을 사용하여 공격합니다:
무기로 공격합니다.
검으로 베기 공격을 합니다!
활로 화살을 발사합니다!
지팡이로 마법 공격을 합니다!

문제: Spell 클래스를 작성하고, 같은 이름의 cast() 메서드를 다양한 매개변수로 오버로딩하세요.

파이어볼 마법을 시전합니다.
아이스 스피어 마법을 위력 5 시전합니다.
고블린에게 썬더 스트라이크 마법을 위력 10 시전합니다.

문제: 아이템을 저장하는 Inventory<T> 제네릭 클래스를 작성하세요. 제네릭 문법은 java-study-ch03 3.1에서 자세히 다루므로, 아직 배우지 않았다면 3장을 마친 뒤 돌아와 풀어도 됩니다.

무기: 전설의 검이() 인벤토리에 추가되었습니다.
물약: 체력 회복 물약이() 인벤토리에 추가되었습니다.

2.93 제어문: 조건문, 반복문 예제

🎯 목표: 제어문을 반복 문제로 손에 익힌다.

열 문제 모두 정답 코드 없이 문제 설명과 출력 예시만 제시합니다. 2.1에서 익힌 조건문·반복문만으로 풀 수 있으며, 파일은 ✏️ 직접 해보기와 같은 com.example.ch02.practice 패키지에 자유롭게 만들면 됩니다.

문제 1: 1부터 100까지의 숫자 출력

문제 설명: 1부터 100까지의 숫자를 출력하는 프로그램 작성. 출력 예시:

1
2
3
...
100


문제 2: 짝수만 출력하기

문제 설명: 1부터 50까지의 숫자 중 짝수만 출력하는 프로그램 작성. 출력 예시:

2
4
6
...
50


문제 3: 숫자의 합 계산하기

문제 설명: 1부터 10까지의 숫자를 더한 합계를 출력하는 프로그램 작성. 출력 예시:

1부터 10까지의 합: 55


문제 4: 구구단 출력

문제 설명: 사용자가 입력한 숫자의 구구단을 출력하는 프로그램 작성. 출력 예시:

사용자가 입력한 숫자가 5인 경우:
5 * 1 = 5
5 * 2 = 10
...
5 * 9 = 45


문제 5: 홀수의 합 구하기

문제 설명: 1부터 100까지의 숫자 중 홀수의 합을 구하는 프로그램 작성. 출력 예시:

1부터 100까지 홀수의 합: 2500


문제 6: 팩토리얼 계산

문제 설명: 사용자가 입력한 숫자의 팩토리얼 값을 구하는 프로그램 작성. 출력 예시:

사용자가 입력한 숫자가 5인 경우:
5의 팩토리얼 값: 120


문제 7: 배열 요소 합 계산

문제 설명: 정수 배열이 주어졌을 때, 배열의 모든 요소의 합을 구하는 프로그램 작성. 출력 예시:

배열의 요소 합: 150 (배열 요소가 {10, 20, 30, 40, 50}일 경우)


문제 8: 숫자 역순 출력

문제 설명: 100부터 1까지 역순으로 숫자를 출력하는 프로그램 작성. 출력 예시:

100
99
98
...
1


문제 9: 최대값 찾기

문제 설명: 주어진 정수 배열에서 가장 큰 숫자를 찾는 프로그램 작성. 출력 예시:

가장 큰 값: 9 (배열 요소가 {3, 9, 5, 1, 7}일 경우)


문제 10: 구간 합 구하기

문제 설명: 사용자가 입력한 두 숫자 사이의 모든 숫자의 합을 구하는 프로그램 작성. 출력 예시:

10부터 20까지의 합: 165 (사용자가 입력한 숫자가 10과 20인 경우)