공공부하자개발 · 영어 학습 노트
자바
초급자바의 뼈대0/8 완료
  • 00개발 환경과 첫 프로그램
  • 01변수, 데이터 타입, 형변환
  • 02연산자와 제어문
  • 031차원 · 2차원 배열
  • 04클래스, 객체, 생성자, 오버로딩
  • 05접근 제어자와 캡슐화
  • 06메서드 활용 패턴 (초급)
  • 07enum, 패키지와 import, static 과 final
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 초급 › 04 / 8

클래스, 객체, 생성자, 오버로딩

섹션 7진행 0 / 8
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

4. 응용 변형 예제

객체를 만드는 방법, 상태를 두는 위치(static vs 인스턴스), 객체끼리 협력하는 모양, null과 공유 참조를 다루는 법을 조금씩 바꿔 가며 연습합니다.

변형 1: 생성자 체이닝 → 정적 팩토리 메서드

예제 1은 생성자를 여러 개 두고 this(...)로 연결했습니다. 생성자를 하나만 private으로 두고 of, withoutEmail, guest 같은 이름 있는 static 메서드로 객체를 만들면 "무엇을 만드는지"가 호출부에 드러납니다. 생성자가 셋 이상 생기거나 같은 매개변수 타입 조합이 겹칠 때 이 방식을 택합니다.

java
public class PersonFactory {
    static class Person {
        String name;
        int age;
        String email;

        private Person(String name, int age, String email) {   // 생성자는 하나만
            this.name = name;
            this.age = age;
            this.email = email;
        }

        static Person of(String name, int age, String email) { return new Person(name, age, email); }
        static Person withoutEmail(String name, int age) { return new Person(name, age, "unknown@example.com"); }
        static Person guest() { return new Person("손님", 0, "guest@example.com"); }

        @Override
        public String toString() { return name + "(" + age + ", " + email + ")"; }
    }

    public static void main(String[] args) {
        System.out.println(Person.of("김민수", 18, "kim@shop.com"));   // 출력: 김민수(18, kim@shop.com)
        System.out.println(Person.withoutEmail("이서연", 30));          // 출력: 이서연(30, unknown@example.com)
        System.out.println(Person.guest());                             // 출력: 손님(0, guest@example.com)
        // 다른 클래스에서 new Person(...) → 컴파일 오류: private
    }
}

변형 2: static 유틸 메서드 vs 인스턴스 메서드 — 할인 계산

상태가 필요 없는 계산은 static 메서드로 두고 값을 매번 넘깁니다. 같은 비율을 여러 번 쓴다면 비율을 필드로 기억하는 객체가 편합니다. 둘 중 하나를 고르는 기준은 "이 값이 호출마다 바뀌는가, 한 번 정해지면 고정인가"입니다.

java
public class DiscountCalc {
    // 방식 A: 상태 없음 → static. 비율을 매번 넘긴다
    static int applyRate(int price, int percent) {
        return price - price * percent / 100;
    }

    // 방식 B: 비율을 객체가 기억 → 같은 정책을 여러 상품에 적용
    static class Discount {
        final int percent;
        Discount(int percent) { this.percent = percent; }
        int apply(int price) { return applyRate(price, percent); }   // A를 재사용
    }

    public static void main(String[] args) {
        System.out.println(applyRate(10_000, 15));        // 출력: 8500
        System.out.println(applyRate(10_000, 0));         // 출력: 10000

        Discount vip = new Discount(30);
        int[] cart = {12_000, 5_500, 30_000};
        int total = 0;
        for (int p : cart) total += vip.apply(p);
        System.out.println("VIP 합계: " + total);          // 출력: VIP 합계: 33250
    }
}

변형 3: 주문과 주문 항목 — 객체가 객체를 담는다

예제 5의 재고 관리처럼, 주문(Order)이 항목(OrderItem) 배열을 갖고 합계 계산을 항목에게 맡깁니다. 주문 번호는 static 카운터로 발급하고, 각 객체는 자기 데이터만 책임집니다.

java
public class OrderDemo {
    static class OrderItem {
        String product;
        int price;
        int qty;

        OrderItem(String product, int price, int qty) {
            this.product = product;
            this.price = price;
            this.qty = qty;
        }

        int subtotal() { return price * qty; }
    }

    static class Order {
        static int nextId = 1;              // 모든 주문이 공유
        final int id;
        final String customer;
        OrderItem[] items = new OrderItem[5];
        int count = 0;

        Order(String customer) {
            this.id = nextId++;
            this.customer = customer;
        }

        void add(OrderItem item) {
            if (count < items.length) items[count++] = item;
        }

        int total() {
            int sum = 0;
            for (int i = 0; i < count; i++) sum += items[i].subtotal();   // 계산은 항목에게
            return sum;
        }

        @Override
        public String toString() { return "주문#" + id + " " + customer + " " + count + "건 " + total() + "원"; }
    }

    public static void main(String[] args) {
        Order o1 = new Order("김민수");
        o1.add(new OrderItem("키보드", 89_000, 1));
        o1.add(new OrderItem("마우스", 25_000, 2));
        Order o2 = new Order("이서연");
        o2.add(new OrderItem("모니터", 350_000, 1));

        System.out.println(o1);             // 출력: 주문#1 김민수 2건 139000원
        System.out.println(o2);             // 출력: 주문#2 이서연 1건 350000원
        System.out.println(Order.nextId);   // 출력: 3
    }
}

변형 4: 엣지 케이스 — null 칸과 없는 상품

객체 배열은 만들자마자 모든 칸이 null이고, 중간 칸이 비어 있을 수도 있습니다. 순회할 때 null 칸을 건너뛰고, 찾지 못했을 때 null을 돌려주는 메서드의 결과를 바로 쓰지 않도록 합니다. Objects.equals, Objects.toString은 null이어도 예외를 내지 않는 도우미입니다.

java
import java.util.Objects;

public class NullSafety {
    static class Product {
        String code;
        String name;
        Product(String code, String name) { this.code = code; this.name = name; }
    }

    static Product find(Product[] items, String code) {
        for (Product p : items) {
            if (p != null && p.code.equals(code)) return p;   // null 칸 건너뛰기
        }
        return null;                                          // 못 찾음
    }

    static String nameOf(Product p) {
        return p == null ? "(없음)" : p.name;                  // null을 쓰기 전에 검사
    }

    public static void main(String[] args) {
        Product[] items = new Product[4];             // 4칸 모두 null
        items[0] = new Product("A1", "노트북");
        items[2] = new Product("C3", "모니터");        // 1번 칸은 비어 있음

        System.out.println(nameOf(find(items, "C3")));    // 출력: 모니터
        System.out.println(nameOf(find(items, "Z9")));    // 출력: (없음)

        Product p = null;
        System.out.println(Objects.equals(p, null));       // 출력: true   (p.equals(null)은 NPE)
        System.out.println(Objects.toString(p, "기본값"));  // 출력: 기본값
        try {
            System.out.println(p.name);
        } catch (NullPointerException e) {
            System.out.println("NPE: null의 필드를 읽으려 함");   // 출력: NPE: null의 필드를 읽으려 함
        }
    }
}

변형 5: 복사 생성자 — 공유 참조 방어

예제 4에서 본 것처럼 참조를 대입하면 같은 객체를 가리킵니다. 독립된 사본이 필요하면 다른 객체를 받아 값을 베끼는 생성자(복사 생성자)를 만듭니다. 안에 배열이나 다른 객체가 있으면 그것도 clone이나 새 객체로 복사해야 진짜 독립됩니다.

java
import java.util.Arrays;

public class CopyConstructor {
    static class Wallet {
        int balance;
        int[] coins;

        Wallet(int balance, int[] coins) {
            this.balance = balance;
            this.coins = coins;
        }

        Wallet(Wallet other) {                              // 복사 생성자
            this(other.balance, other.coins.clone());       // 배열도 새로 복사
        }

        @Override
        public String toString() { return balance + " " + Arrays.toString(coins); }
    }

    public static void main(String[] args) {
        Wallet original = new Wallet(1000, new int[]{500, 100});

        Wallet alias = original;                 // 같은 객체를 가리킴
        Wallet copy = new Wallet(original);      // 독립된 새 객체

        alias.balance = 0;
        alias.coins[0] = 0;
        System.out.println("원본: " + original);   // 출력: 원본: 0 [0, 100]
        System.out.println("복사: " + copy);       // 출력: 복사: 1000 [500, 100]
        System.out.println(original == alias);     // 출력: true
        System.out.println(original == copy);      // 출력: false
    }
}
응용 변형 예제
  • 변형 1: 생성자 체이닝 → 정적 팩토리 메서드
  • 변형 2: static 유틸 메서드 vs 인스턴스 메서드 — 할인 계산
  • 변형 3: 주문과 주문 항목 — 객체가 객체를 담는다
  • 변형 4: 엣지 케이스 — null 칸과 없는 상품
  • 변형 5: 복사 생성자 — 공유 참조 방어
이전 섹션3 코드 예제4 / 7다음 섹션5 자주 하는 실수 (Tip)