본문 바로가기

타입별 병원의 중복

타입별 병원 클래스가 입력 안전성을 얻는 대신 비교·검진 로직을 복제해 서로 다른 결과를 내는 문제를 관찰하고 공통 알고리즘의 변화 축을 찾습니다.

개 전용 병원은 Dog만 받고 고양이 전용 병원은 Cat만 받아야 한다는 요구는 전용 클래스로 쉽게 지킬 수 있습니다.

메서드 시그니처 자체가 다른 종의 입력을 거부하기 때문입니다.

문제는 검진과 크기 비교 알고리즘이 거의 같은데도 각 클래스에 복사된다는 점입니다.

한쪽만 수정되면 타입 안전성은 유지되지만 업무 정책이 갈라집니다.


비교식 복사와 정책 불일치

두 병원은 저장 타입만 다르고 구조가 같습니다.

CatHospital을 복사하면서 비교 연산자를 잘못 바꿔 작은 고양이를 반환합니다.

컴파일과 실행은 모두 성공하므로 결과를 확인하지 않으면 놓칩니다.

lab/DuplicatedHospitalPolicyDriftBug.java
public final class DuplicatedHospitalPolicyDriftBug {
    public static void main(String[] args) {
        DogHospital dogs = new DogHospital();
        dogs.set(new Dog("small-dog", 30));
        System.out.println("dog=" + dogs.bigger(new Dog("big-dog", 70)).name());

        CatHospital cats = new CatHospital();
        cats.set(new Cat("small-cat", 20));
        System.out.println("cat=" + cats.bigger(new Cat("big-cat", 60)).name());
    }

    private static final class DogHospital {
        private Dog animal;
        void set(Dog animal) { this.animal = animal; }
        Dog bigger(Dog target) { return animal.size() > target.size() ? animal : target; }
    }

    private static final class CatHospital {
        private Cat animal;
        void set(Cat animal) { this.animal = animal; }
        Cat bigger(Cat target) { return animal.size() < target.size() ? animal : target; }
    }

    private record Dog(String name, int size) { }
    private record Cat(String name, int size) { }
}
의도와 다른 관찰 결과
dog=big-dog
cat=small-cat

Cat 전용 타입이라는 계약은 깨지지 않았지만 “더 큰 동물을 반환한다”는 공통 정책은 깨졌습니다.

중복은 단순한 줄 수 문제가 아니라 같은 변경 이유를 가진 코드가 여러 곳에서 독립적으로 진화할 가능성입니다.

개 비교는 30 > 70이 거짓이어서 새 대상 big-dog를 반환합니다. 고양이 비교는 20 < 60이 참이어서 저장한 small-cat을 반환합니다. 선택한 객체의 타입은 두 경우 모두 맞으므로 타입 검사만 통과한 결과로 업무 정책의 성공을 판단할 수 없습니다.

동률도 별도 정책입니다. 바로 뒤 미리보기의 > 비교는 같을 때 새 대상을 선택하고, 마지막 연습의 >= 비교는 저장한 환자를 선택합니다. 두 프로그램이 다른 동률 정책을 사용하는 것을 확인한 뒤 요구에 맞는 쪽을 정합니다.


전용 클래스의 컴파일 보호

DogHospital의 set과 bigger는 Dog만 받으며 반환도 Dog입니다.

Cat을 전달하면 실행 전에 막힙니다.

Object 기반 통합처럼 잘못 넣고 나중에 캐스팅하는 위험은 없습니다.

lab/WrongSpeciesHospitalFailure.java
public final class WrongSpeciesHospitalFailure {
    public static void main(String[] args) {
        DogHospital hospital = new DogHospital();
        hospital.set(new Cat("cat", 40));
    }

    private static final class DogHospital {
        private Dog animal;
        void set(Dog animal) { this.animal = animal; }
    }

    private record Dog(String name, int size) { }
    private record Cat(String name, int size) { }
}
javac 핵심 진단
error: incompatible types: Cat cannot be converted to Dog

따라서 통합의 목표는 전용 클래스의 타입 보호를 포기하는 것이 아닙니다.

입력과 반환의 구체 타입은 유지하면서 같은 알고리즘만 한 구현으로 모아야 합니다.

타입 보호와 더 큰 동물 선택은 별개다

개 병원의 30>70은 거짓, 고양이 병원의 20<60은 참이므로 작은 고양이가 선택된다. 별도 원문의 Cat 인수는 Dog 전용 set에서 컴파일 거부된다.

타입 안전 · 비교 정책

타입 보호와 더 큰 동물 선택은 별개다

타입이 맞아도 더 큰 동물을 고르는 비교식은 틀릴 수 있습니다. 실제 두 비교와 별도 컴파일 실패를 함께 봅니다.

종의 타입 검사와 업무 비교 정책 원문 A의 개는 30 > 70이 거짓이라 big-dog를, 고양이는 20 < 60이 참이라 small-cat을 반환합니다. 두 결과의 타입은 맞아도 정책이 어긋납니다. 원문 B에서 DogHospital에 Cat을 넣는 시도는 컴파일 실패입니다. 실선은 각 경우의 검사와 선택 결과입니다. 원문 A · 개 선택 저장·새 대상 small-dog 30 big-dog 70 개 정책은 > 30 > 70은 false 새 대상 반환 dog=big-dog 원문 A · 고양이 선택 저장·새 대상 small-cat 20 big-cat 60 고양이 정책은 < 20 < 60은 true 저장 대상 반환 cat=small-cat 원문 B · 종 불일치 Dog 전용 계약 DogHospital Cat 넣기 시도 set에 Cat 전달 컴파일 실패 Cat → Dog 불가 첫 두 영역은 업무 정책 불일치 세 번째는 컴파일 타입 오류 실선: 각 경우의 검사·결과
종의 타입 검사와 업무 비교 정책 원문 A의 개는 30 > 70이 거짓이라 big-dog를, 고양이는 20 < 60이 참이라 small-cat을 반환합니다. 두 결과의 타입은 맞아도 정책이 어긋납니다. 원문 B에서 DogHospital에 Cat을 넣는 시도는 컴파일 실패입니다. 실선은 각 경우의 검사와 선택 결과입니다. 원문 A · 개 선택 저장·새 대상 small-dog 30 big-dog 70 개 정책은 > 30 > 70은 false 새 대상 반환 dog=big-dog 원문 A · 고양이 선택 저장·새 대상 small-cat 20 big-cat 60 고양이 정책은 < 20 < 60은 true 저장 대상 반환 cat=small-cat 원문 B · 종 불일치 Dog 전용 계약 DogHospital Cat 넣기 시도 set에 Cat 전달 컴파일 실패 Cat → Dog 불가 첫 두 영역은 업무 정책 불일치 세 번째는 컴파일 타입 오류 실선: 각 경우의 검사·결과

개 비교

검사 또는 비교식
DogHospital: 저장한 크기 30, 비교 대상 70.
30 > 70은 거짓입니다.
실제 선택과 결과
대상 big-dog를 선택합니다.
dog=big-dog

고양이 비교

검사 또는 비교식
CatHospital: 저장한 크기 20, 비교 대상 60.
20 < 60은 참입니다.
실제 선택과 결과
저장한 small-cat을 선택합니다.
cat=small-cat
컴파일은 성공하지만 더 큰 동물이라는 요구를 어깁니다.

다른 종 입력

검사 또는 비교식
별도 원문에서 DogHospital.set(Dog)에 Cat을 전달합니다.
실제 선택과 결과
컴파일 단계에서 거부합니다.
Cat은 Dog 인수로 변환할 수 없습니다.

앞의 두 행은 DuplicatedHospitalPolicyDriftBug의 출력이고, 마지막 행은 WrongSpeciesHospitalFailure의 컴파일 결과입니다. 타입 검사는 비교 연산자가 업무 요구에 맞는지까지 증명하지 않습니다.


공통점과 변화 축

두 병원에서 같은 것은 한 환자를 보관하고 이름·크기를 출력하며 두 개체의 크기를 비교하는 흐름입니다.

다른 것은 구체 타입과 울음 같은 종별 행동입니다.

공통 부모나 인터페이스가 이름·크기 계약을 제공하면 알고리즘은 그 계약만 사용할 수 있습니다.

src/BoundedHospitalPreview.java
public final class BoundedHospitalPreview {
    public static void main(String[] args) {
        Hospital<Dog> dogs = new Hospital<>();
        dogs.set(new Dog("d1", 30));
        Dog biggerDog = dogs.bigger(new Dog("d2", 80));

        Hospital<Cat> cats = new Hospital<>();
        cats.set(new Cat("c1", 70));
        Cat biggerCat = cats.bigger(new Cat("c2", 40));

        System.out.println("dog=" + biggerDog.name());
        System.out.println("cat=" + biggerCat.name());
    }

    private static final class Hospital<T extends Animal> {
        private T animal;
        void set(T animal) { this.animal = animal; }
        T bigger(T target) { return animal.size() > target.size() ? animal : target; }
    }

    private interface Animal {
        String name();
        int size();
    }

    private record Dog(String name, int size) implements Animal { }
    private record Cat(String name, int size) implements Animal { }
}
dog=d2
cat=c1

T extends Animal은 Hospital이 Animal의 size를 사용할 수 있게 합니다. 이 예제의 Hospital<Dog>와 Hospital<Cat>은 각각 선택한 타입으로 입력과 반환을 검사합니다. Hospital<Animal>을 선택하면 여러 Animal 하위 타입을 담을 수 있습니다.

여기서는 전체 모습을 먼저 확인하고, 제한이 없을 때 왜 메서드를 호출할 수 없는지는 뒤 문서에서 컴파일 실패로 분석합니다.


중복 제거와 제네릭의 관계

Dog와 Cat의 검진 절차가 실제로 달라지기 시작하면 하나의 Hospital에 조건문을 늘리는 것보다 별도 전략이나 타입별 클래스가 낫습니다.

예를 들어 개는 예방접종 번호가 필요하고 고양이는 실내 생활 여부를 검사한다면 종별 정책은 각각의 객체에 남겨야 합니다.

공통 비교 알고리즘과 종별 검진 정책을 분리할 수 있습니다.

src/HospitalWithSpeciesPolicy.java
public final class HospitalWithSpeciesPolicy {
    public static void main(String[] args) {
        Hospital<Dog> hospital = new Hospital<>(dog -> "dog-check=" + dog.vaccineId());
        hospital.admit(new Dog("dori", 55, "VAC-7"));
        System.out.println(hospital.checkup());
    }

    private static final class Hospital<T extends Sized> {
        private final Checkup<T> checkup;
        private T patient;

        Hospital(Checkup<T> checkup) { this.checkup = checkup; }
        void admit(T patient) { this.patient = patient; }
        String checkup() { return checkup.inspect(patient); }
        T bigger(T other) { return patient.size() >= other.size() ? patient : other; }
    }

    private interface Sized { int size(); }
    private interface Checkup<T> { String inspect(T target); }
    private record Dog(String name, int size, String vaccineId) implements Sized { }
}
dog-check=VAC-7

컨테이너는 공통 저장·비교만 맡고 종별 검진은 주입된 정책이 담당합니다.

제네릭이 모든 행동을 같은 것으로 만들지 않고 타입 관계를 보존한 채 변화 이유를 분리합니다.


타입 통합의 판단 기준

공통 알고리즘의 변경이 늘 함께 일어나고 타입만 다르면 제네릭 통합의 이점이 큽니다.

클래스 이름만 비슷하고 업무 규칙·필드·수명이 각각 변한다면 억지 통합은 조건문과 타입 검사를 늘립니다.

다음 질문으로 경계를 정합니다.

  • 저장·조회·비교 순서가 모든 타입에서 같은가?
  • 공통 상한이 제공해야 할 최소 행동은 무엇인가?
  • 한 컨테이너 안에서 입력과 반환이 같은 구체 타입이어야 하는가?
  • 종별 정책은 객체나 전략으로 분리할 수 있는가?
  • 새 종을 추가할 때 기존 알고리즘을 수정하지 않아도 되는가?

테스트도 공통 계약과 종별 정책으로 나눕니다.

bigger의 경계값과 동률 정책은 모든 타입에 같은 계약 테스트를 적용하고, 종별 검진은 개별 테스트로 확인합니다.

중복 제거 뒤 한 구현의 버그가 모든 타입에 퍼질 수 있으므로 공통 알고리즘 검증은 더 중요해집니다.


연습 문제

Hospital<T extends Sized>가 같은 크기일 때 기존 환자를 유지하도록 만들고 Dog와 Cat 모두에서 확인하세요.

공통 메서드는 한 번만 작성해야 합니다.

Sized는 name()과 size()를 제공하며, Hospital은 생성자에서 현재 환자를 받습니다. 환자와 비교 대상은 null이 아니고 크기는 이 입력 그대로 비교합니다. Dog는 first-dog 50과 second-dog 50, Cat은 first-cat 40과 second-cat 40을 사용합니다. bigger는 선택한 객체를 반환할 뿐 저장 환자를 바꾸지 않습니다.

독립적으로 Cat의 새 대상만 second-cat 41로 바꾸면 어떤 이름을 반환할지, 다음에 third-cat 39를 비교할 때도 41이 저장되어 있다고 볼 수 있을지 예상하세요.

해설 보기
src/HospitalTiePolicyExercise.java
public final class HospitalTiePolicyExercise {
    public static void main(String[] args) {
        Hospital<Dog> dogs = new Hospital<>(new Dog("first-dog", 50));
        Hospital<Cat> cats = new Hospital<>(new Cat("first-cat", 40));

        System.out.println(dogs.bigger(new Dog("second-dog", 50)).name());
        System.out.println(cats.bigger(new Cat("second-cat", 40)).name());
    }

    private static final class Hospital<T extends Sized> {
        private final T patient;
        Hospital(T patient) { this.patient = patient; }
        T bigger(T other) { return patient.size() >= other.size() ? patient : other; }
    }

    private interface Sized { String name(); int size(); }
    private record Dog(String name, int size) implements Sized { }
    private record Cat(String name, int size) implements Sized { }
}
first-dog
first-cat

>=라는 동률 정책이 한곳에 있으므로 두 타입의 결과가 어긋나지 않습니다.

요구가 “새 환자를 우선”으로 바뀌어도 한 문장만 수정합니다.

독립 입력의 첫 비교는 40 >= 41이 거짓이라 second-cat을 반환합니다. 저장 필드는 계속 first-cat 40이므로 다음 39와의 비교는 first-cat을 반환합니다. 한 번 반환한 승자를 계속 현재 환자로 삼으려면 별도의 상태 변경 요구와 구현이 필요합니다.