객체 생성 패턴 및 공변성

hs·2025년 11월 23일

객체 생성 패턴

점층적 생성자 패턴

필수 인자부터 시작하여 선택적 인자를 단계적으로 늘린 생성자들을 오버로딩하는 방식

public class Coffee {
    private final String type; // 필수
    private final boolean sugar; // 선택
    private final boolean milk; // 선택
    private final String topping; // 선택

    public Coffee(String type) {
        this(type, false, false, null);
    }

    public Coffee(String type, boolean sugar) {
        this(type, sugar, false, null);
    }

    public Coffee(String type, boolean sugar, boolean milk) {
        this(type, sugar, milk, null);
    }

    public Coffee(String type, boolean sugar, boolean milk, String topping) {
        this.type = type;
        this.sugar = sugar;
        this.milk = milk;
        this.topping = topping;
    }
}

장점

  • 객체를 한 번에 완성된 상태로 생성
  • 불변 객체로 만들기 용이

단점

  • 매개변수가 많아질수록 생성자의 수가 늘어남
  • 어떤 매개변수가 어떤 값을 나타내는지 파악하기 어려움
    • new Coffee("Latte", false, true)에서 false가 설탕인지 우유인지 불분명
  • 클라이언트 코드를 작성하거나 읽기 어려움

자바빈즈 패턴

기본 생성자로 생성 후 setter로 속성 설정

Car car = new Car();
car.setModel("Sonata");
car.setYear(2025);
car.setColor("White");

장점

  • 매개변수 이름이 노출 → 가독성 좋은 API

단점

  • 여러 번 메서드 호출해야 함
  • 생성 이후까지 일관성이 보장되지 않음
  • 불변 객체 불가 → 스레드 안전성 부족
  • 유효성 검사(검증) 시점 불명확

빌더 패턴

  • 객체 생성 과정을 분리 → 동일한 생성 절차로 다양한 형태로 객체 생성
    1. 필수 매개변수로 생성자(또는 팩터리 메서드)를 호출하여 빌더 객체를 얻음
      (필요한 객체를 직접 생성하지 않음)
    2. 빌더 객체가 제공하는 일종의 세터 메서드로 선택 매개변수를 설정
    3. build() 메서드를 호출하여 최종 객체를 얻음
  • 가독성 + 불변성 + 유연성
  • 빌드하려는 클래스 내부에 정적 멤버 클래스로 만들어 둠
  • 빌더의 세터 메서드들은 빌더 자신을 반환 → 연쇄적으로 호출 가능(메서드 체이닝, 플로언트 API)
Copublic class Computer {
    private final String cpu; // 필수
    private final int ramGB; // 필수
    private final String storage; // 선택
    private final String graphicsCard; // 선택

    // private 생성자로 외부에서 직접 객체 생성 방지
    private Computer(Builder builder) {
        this.cpu = builder.cpu;
        this.ramGB = builder.ramGB;
        this.storage = builder.storage;
        this.graphicsCard = builder.graphicsCard;
    }

    public static class Builder {
        private final String cpu;
        private final int ramGB;
        private String storage = "256GB SSD"; // 기본값
        private String graphicsCard = "Integrated"; // 기본값

        // 필수 매개변수를 받는 빌더 생성자
        public Builder(String cpu, int ramGB) {
            this.cpu = cpu;
            this.ramGB = ramGB;
        }

        public Builder storage(String storage) {
            this.storage = storage;
            return this; // 빌더 자신을 반환하여 체이닝 가능
        }

        public Builder graphicsCard(String graphicsCard) {
            this.graphicsCard = graphicsCard;
            return this;
        }

        public Computer build() {
            return new Computer(this);
        }
    }

}

// 사용 예시
Computer gamingPC = new Computer.Builder("Intel i9", 32)
                                .storage("1TB NVMe SSD")
                                .graphicsCard("NVIDIA RTX 4080")
                                .build();

Computer officePC = new Computer.Builder("Intel i5", 16)
                               .build(); // 선택 매개변수 생략 시 기본값 사용

장점

  • 매개변수의 의미를 명확히 알 수 있다
  • 필요한 매개변수만 설정할 수 있어 유연하다
  • 불변 객체 유지 가능하다
  • 객체 생성 전까지 외부 노출 X → 완전한 상태의 객체 보장한다
  • 플루언트 API(메서드 체이닝)를 통해 간결한 코드 작성 가능하다
  • 상속 관계의 계층적으로 설계된 클래스와 함께 사용하기 좋다

단점

  • 빌더 클래스 작성 + 유지보수 비용 증가 → 단순한 객체에는 오버 엔지니어링

공변성 & 불공변성

  • 서브타입 관계가 제네릭 타입에도 유지되는 특성
  • B가 A의 서브타입일 때, List<B>List<A>의 서브타입으로 간주

서브타입

특정 타입이 다른 타입의 기능을 모두 포함하거나 확장하는 관계

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

Animal myAnimal = new Dog();

제네릭 타입

클래스나 메서드에서 사용할 타입을 일반화하는 방법

List<Animal> animals = new ArrayList<>();
animals.add(new Animal());
animals.add(new Dog());
animals.add(new Cat());

배열의 공변성 (Java의 배열은 공변적)

// Dog 배열을 Animal 배열에 할당가능 -> 런타임 오류의 가능성 내포
Animal[] animals = new Dog[10]; 

// ArrayStoreException이 발생 가능 -> 실제 배열은 Dog 타입만 담을 수 있음
animals[0] = new Cat();

불공변성(Invariance)

Java의 제네릭은 기본적으로 불공변성

List<Dog> dogList = new ArrayList<>();
List<Animal> animalList = dogList; // 컴파일 오류

공변성 (와일드카드 사용)

List<Dog> dogList = new ArrayList<>();
List<? extends Animal> animalList = dogList; // 가능
// animalList.add(new Cat()); // 컴파일 오류

공변 반환 타이핑

오버라이드하는 메서드의 반환 타입이 오버라이드되는 메서드의 반환 타입의 서브타입이 될 수 있도록 허용하는 특성

class Animal {
    public Animal produce() {
        System.out.println("동물 객체 생성");
        return new Animal();
    }
}

class Dog extends Animal {
    @Override
    public Dog produce() { // 공변 반환 타이핑
        System.out.println("개 객체 생성");
        return new Dog();
    }
}

class Cat extends Animal {
    @Override
    public Cat produce() { 
        System.out.println("고양이 객체 생성");
        return new Cat();
    }
}

Animal animalProducer = new Animal();
Dog dogProducer = new Dog();
Cat catProducer = new Cat();

Animal a = animalProducer.produce(); // 반환 타입은 Animal
Dog d = dogProducer.produce();       // 반환 타입은 Dog (캐스팅 필요 없음)
Cat c = catProducer.produce();       // 반환 타입은 Cat (캐스팅 필요 없음)

// 다형성
Animal polyProducer = new Dog();
// polyProducer.produce()의 실제 반환 타입은 Dog이지만,
// 컴파일러는 Animal 타입으로 간주하므로 Dog 타입으로 받으려면 캐스팅 필요
Dog d2 = (Dog) polyProducer.produce();
profile
sh

0개의 댓글