타입별 병원의 중복
타입별 병원 클래스가 입력 안전성을 얻는 대신 비교·검진 로직을 복제해 서로 다른 결과를 내는 문제를 관찰하고 공통 알고리즘의 변화 축을 찾습니다.
개 전용 병원은 Dog만 받고 고양이 전용 병원은 Cat만 받아야 한다는 요구는 전용 클래스로 쉽게 지킬 수 있습니다.
메서드 시그니처 자체가 다른 종의 입력을 거부하기 때문입니다.
문제는 검진과 크기 비교 알고리즘이 거의 같은데도 각 클래스에 복사된다는 점입니다.
한쪽만 수정되면 타입 안전성은 유지되지만 업무 정책이 갈라집니다.
비교식 복사와 정책 불일치
두 병원은 저장 타입만 다르고 구조가 같습니다.
CatHospital을 복사하면서 비교 연산자를 잘못 바꿔 작은 고양이를 반환합니다.
컴파일과 실행은 모두 성공하므로 결과를 확인하지 않으면 놓칩니다.
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-catCat 전용 타입이라는 계약은 깨지지 않았지만 “더 큰 동물을 반환한다”는 공통 정책은 깨졌습니다.
중복은 단순한 줄 수 문제가 아니라 같은 변경 이유를 가진 코드가 여러 곳에서 독립적으로 진화할 가능성입니다.
전용 클래스의 컴파일 보호
DogHospital의 set과 bigger는 Dog만 받으며 반환도 Dog입니다.
Cat을 전달하면 실행 전에 막힙니다.
Object 기반 통합처럼 잘못 넣고 나중에 캐스팅하는 위험은 없습니다.
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) { }
}error: incompatible types: Cat cannot be converted to Dog따라서 통합의 목표는 전용 클래스의 타입 보호를 포기하는 것이 아닙니다.
입력과 반환의 구체 타입은 유지하면서 같은 알고리즘만 한 구현으로 모아야 합니다.
공통점과 변화 축
두 병원에서 같은 것은 한 환자를 보관하고 이름·크기를 출력하며 두 개체의 크기를 비교하는 흐름입니다.
다른 것은 구체 타입과 울음 같은 종별 행동입니다.
공통 부모나 인터페이스가 이름·크기 계약을 제공하면 알고리즘은 그 계약만 사용할 수 있습니다.
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=c1T extends Animal은 Hospital이 Animal의 size를 사용할 수 있게 하고, 한 인스턴스 안에서는 T가 동일하게 유지되도록 합니다.
여기서는 전체 모습을 먼저 확인하고, 제한이 없을 때 왜 메서드를 호출할 수 없는지는 뒤 문서에서 컴파일 실패로 분석합니다.
중복 제거와 제네릭의 관계
Dog와 Cat의 검진 절차가 실제로 달라지기 시작하면 하나의 Hospital에 조건문을 늘리는 것보다 별도 전략이나 타입별 클래스가 낫습니다.
예를 들어 개는 예방접종 번호가 필요하고 고양이는 실내 생활 여부를 검사한다면 종별 정책은 각각의 객체에 남겨야 합니다.
공통 비교 알고리즘과 종별 검진 정책을 분리할 수 있습니다.
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 모두에서 확인하세요.
공통 메서드는 한 번만 작성해야 합니다.
해설 보기
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>=라는 동률 정책이 한곳에 있으므로 두 타입의 결과가 어긋나지 않습니다.
요구가 “새 환자를 우선”으로 바뀌어도 한 문장만 수정합니다.