Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
Tags
- 자바의 정석 기초편 ch7
- 스프링 mvc1 - 서블릿
- 자바 중급2편 - 컬렉션 프레임워크
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch6
- 자바의 정석 기초편 ch13
- 자바의 정석 기초편 ch14
- 코드로 시작하는 자바 첫걸음
- 스프링 입문(무료)
- 자바의 정석 기초편 ch2
- 스프링 mvc1 - 스프링 mvc
- 자바의 정석 기초편 ch12
- 자바 중급1편 - 날짜와 시간
- 자바의 정석 기초편 ch5
- jpa - 객체지향 쿼리 언어
- 스프링 db1 - 스프링과 문제 해결
- @Aspect
- 2024 정보처리기사 수제비 실기
- 스프링 mvc2 - 로그인 처리
- 스프링 mvc2 - 검증
- jpa 활용2 - api 개발 고급
- 자바의 정석 기초편 ch4
- 2024 정보처리기사 시나공 필기
- 자바의 정석 기초편 ch9
- 자바 기본편 - 다형성
- 스프링 고급 - 스프링 aop
- 게시글 목록 api
- 스프링 db2 - 데이터 접근 기술
- 스프링 mvc2 - 타임리프
- 자바의 정석 기초편 ch1
Archives
- Today
- Total
나구리의 개발공부기록
제네릭, 프로젝트 환경 구성과 제네릭이 필요한 이유, 다형성을 통한 중복 해결 시도, 제네릭 적용, 제네릭 용어와 관례, 제네릭 활용 예제 본문
인프런 - 실전 자바 로드맵/실전 자바 - 중급 2편
제네릭, 프로젝트 환경 구성과 제네릭이 필요한 이유, 다형성을 통한 중복 해결 시도, 제네릭 적용, 제네릭 용어와 관례, 제네릭 활용 예제
소소한나구리 2025. 2. 2. 00:45728x90
출처 : 인프런 - 김영한의 실전 자바 - 중급2편 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 프로젝트 환경 구성과 제네릭이 필요한 이유
1) 프로젝트 환경 구성
(1) 프로젝트 생성
- 인텔리제이에서 NEW PROJECT 선택
- Name: 4.java-mid2
- Location: 원하는 위치
- Build system: IntelliJ
- JDK: 21
2) 제네릭이 필요한 이유
(1) IntegerBox, StringBox
- 숫자와 문자열을 각각 보관하고 꺼낼 수 있는 기능을 가진 클래스들
package generic.ex1;
public class IntegerBox {
private Integer value;
public void set(Integer value) {
this.value = value;
}
public Integer get() {
return value;
}
}
package generic.ex1;
public class StringBox {
private String value;
public void set(String value) {
this.value = value;
}
public String get() {
return value;
}
}
(2) BoxMain1
- 숫자와 문자열을 보관하는 IntegerBox, StringBox를 생성하고 각 값을 입력한 뒤 출력하는 코드
package generic.ex1;
public class BoxMain1 {
public static void main(String[] args) {
IntegerBox integerBox = new IntegerBox();
integerBox.set(10);
Integer integer = integerBox.get();
System.out.println("integer = " + integer);
StringBox stringBox = new StringBox();
stringBox.set("hello");
String str = stringBox.get();
System.out.println("str = " + str);
}
}
/* 실행 결과
integer = 10
str = hello
*/
(3) 문제
- 만약, 이후에 Double, Boolean을 포함하는 다양한 타입을 담는 박스가 필요하다면 각각의 타입별로 DoubleBox, BooleanBox와 같이 클래스를 새로 만들어야 할 텐데 담아야 할 타입이 수십개라면 이러한 형태로 수십 개의 Box 클래스를 생성해야함
- 이 문제를 해결할 수 있는 방법은 무엇일까? 지금 Box 클래스들보면 여러개를 만들어도 구조가 비슷함
2. 다형성을 통한 중복 해결 시도
1) Object로 문제 해결 시도
(1) ObjectBox
- Object는 모든 타입의 부모이기 때문에 다형성(다형적 참조)를 사용하여 이 문제를 간단히 해결할 수 있음
- 내부에 Object value를 가지고 있으며, Object는 모든 타입의 부모이기 때문에 모든 타입을 ObjectBox에 보관할 수 있음
package generic.ex1;
public class ObjectBox {
private Object value;
public void set(Object value) {
this.value = value;
}
public Object get() {
return value;
}
}
(2) BoxMain2
- ObjectBox를 활용하면 캐스팅을 활용하여 정상적으로 동작하도록 하는 것 같지만, 몇 가지 문제가 있음
package generic.ex1;
public class BoxMain2 {
public static void main(String[] args) {
ObjectBox integerBox = new ObjectBox();
integerBox.set(10);
Integer integer = (Integer) integerBox.get();
System.out.println("integer = " + integer);
ObjectBox stringBox = new ObjectBox();
stringBox.set("hello");
String string = (String) stringBox.get();
System.out.println("string = " + string);
// 잘못된 타입의 인수 전달 시
integerBox.set("문자100");
Integer result = (Integer) integerBox.get(); // String -> Integer 캐스팅 예외 발생
System.out.println("result = " + result);
}
}
/* 실행 결과
integer = 10
string = hello
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
at generic.ex1.BoxMain2.main(BoxMain2.java:17)
*/
(3-1) 반환 타입이 맞지 않는 문제
- new ObjectBox()로 integerBox를 만들어서 숫자를 입력하는 부분에는 문제가 없지만 integerBox.get()으로 호출할 때 반환타입이 Object로 반환됨
- 그래서 직접 (Integer)로 타입 캐스팅 코드를 작성하여 직접 다운 캐스팅을 해야하며 stringBox의 경우에도 마찬가지로 직접 다운 캐스팅을 해야함
(3-2) 잘못된 타입의 인수 전달 문제
- 개발자의 의도는 integerBox에는 변수 이름과 같이 숫자 타입이 입력되기를 기대하였으나, set()메서드는 모든 타입의 부모인 Object를 매개변수로 받기 때문에 모든 타입을 입력 받을 수 있음
- 즉, integerBox.set()메서드에 숫자 타입이 아닌 문자열을 입력해도 자바 언어 입장에서는 아무런 문제가 나타나지 않지만 잘못된 타입의 값을 꺼낼 때 캐스팅을 할 수 없어 문제가 발생함
- 숫자가 들어있을 것으로 예상한 박스에 문자열이 들어가고 다운 캐스팅 시 String을 Integer로 캐스팅할 수 없다는 예외가 발생하면서 프로그램이 종료하게 됨
(4) 정리
- 다형성을 활용한 덕분에 코드의 중복을 제거하고 기존 코드를 재사용할 수 있게 되었으나 입력할 때 실수로 원하지 않는 타입이 들어갈 수 있는 타입 안정성 문제가 발생하게 되었음
- 그리고 반환 시점에 Object를 반환하기 때문에 원하는 타입을 정확하게 받을 수 없고 위험한 다운 캐스팅을 시도해야함
- 지금까지 개발한 코드들은 코드 재사용과 타입 안정성이라는 2마리 토끼를 한번에 잡을 수 없음
- BoxMain1은 각각의 타입별로 IntegerBox, StringBox와 같이 클래스를 모두 정의하여 타입을 잘못입력하면 컴파일 시점에 모든 오류를 잡아주기 때문에 타입안정성이 매우 뛰어나지만 코드 재사용성이 없음
- BoxMain2는 ObjectBox를 사용하여 다형성으로 하나의 클래스만 정의하여 사용할 수 있어 코드 재사용성이 매우 높지만 모든 타입을 입력받을 수 있어 타입 안정성이 좋지 않은 치명적인 문제가 있음
3. 제네릭 적용
1) 제네릭 적용 예제
(1) GenericBox<T>
- 제네릭을 사용하면 코드 재사용과 타입 안정성이라는 두 마리 토끼를 한번에 잡을 수 있음
- <>를 사용한 클래스를 제네릭 클래스라하며 <>기호를 보통 다이아몬드라고 함
- 제네릭 클래스를 사용할 때는 Integer, String 같은 타입을 미리 결정하지 않는 대신 클래스명 오른쪽에 <T>와 같이 선언하면 됨
- 여기서 T를 타입 매개변수라고 하며, 이 타입 매개변수가 이후에 Integer, String과 같이 실제 타입으로 변함
- 클래스 내부에 T 타입이 필요한 곳에 T value와 같이 타입 매개변수를 적으면 됨
package generic.ex1;
public class GenericBox<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
(2) BoxMain3
- 제네릭 클래스를 생성하는 시점에 <> 사이에 원하는 타입을 지정하면 앞서 정의한 GenericBox의 T(타입 매개변수)가 지정한 타입으로 변환된 다음 생성됨
- 즉, T에 Integer를 적용하면 GenericBox<T>에 정의된 모든 T가 Integer로 변하여 set(Integer value)가 되어 Integer 타입만 담을 수 있고 get()메서드의 반환타입도 Integer가 되어 Integer 타입이 반환됨
- 마찬가지로 GenericBox<String>으로 적용하면 T가 모두 String으로 적용되어 new GenericBox<원하는 타입>()으로 GenericBox객체를 생성하는 시점에 원하는 타입을 마음껏 지정할 수 있음
package generic.ex1;
public class BoxMain3 {
public static void main(String[] args) {
GenericBox<Integer> integerBox = new GenericBox<Integer>(); // 생성 시점에 T의 타입 결정
integerBox.set(10); // Integer 타입만 허용
// integerBox.set("문자100"); // 컴파일 오류 발생, Integer 타입만 허용 됨
Integer integer = integerBox.get(); // Integer 타입이 반환 되어 다운 캐스팅이 필요 없음
System.out.println("integer = " + integer);
GenericBox<String> stringBox = new GenericBox<String>();
stringBox.set("Hello"); // String 타입만 허용
String string = stringBox.get(); // String 타입 반환
System.out.println("string = " + string);
// 원하는 모든 타입 사용 가능
GenericBox<Double> doubleBox = new GenericBox<Double>();
doubleBox.set(3.14);
Double doubleValue = doubleBox.get();
System.out.println("doubleValue = " + doubleValue);
}
}
/* 실행 결과
integer = 10
string = Hello
doubleValue = 3.14
*/
** 참고
- 기본형은 안되고 래퍼형만 가능함
- 제네릭을 도입한다고 해서 앞서 설명한 GenericBox<String>, GenericBox<Integer>와 같은 코드가 실제 만들어지는 것은 아니고 자바 컴파일러가 입력된 타입 정보를 기반으로 이런 코드가 있다고 가정하여 컴파일 과정에 타입 정보를 반영함
- 이 과정에서 타입이 맞지 않으면 컴파일 오류가 발생하며 더 자세한 내용은 뒤에서 설명함
(3) 타입 추론
- 첫번째 줄의 코드를 보면 변수를 선언할 때와 객체를 생성할 때 <Integer>가 두번 나옴
- 자바는 왼쪽에 있는 변수를 선언할 때 <Integer>를 보고 오른쪽에 있는 객체를 생성할 때 필요한 타입 정보를 얻을 수 있으므로 오른쪽의 객체 생성 코드에는 new GenericBox<>(); 처럼 타입을 생략할 수 있음
- 이렇게 자바가 스스로 타입 정보를 추론하여 생략할 수 있는 것을 타입 추론이라고 함
- 타입 추론은 그냥 되는 것은 아니고 읽을 수 있는 타입 정보가 주변에 존재해 자바 컴파일러가 타입을 추론할 수 있는 상황에만 가능함
GenericBox<Integer> integerBox = new GenericBox<Integer>(); // 타입 직접 입력
GenericBox<Integer> integerBox2 = new GenericBox<>(); // 타입 추론
4. 제네릭 용어와 관례
1) 제네릭 용어
(1-1) 제네릭의 핵심
- 사용할 타입을 미리 결정하지 않는 것이 핵심
- 클래스 내부에서 사용하는 타입을 클래스를 정의하는 시점에 결정하는 것이 아니라 실제 사용하는 생성 시점에 타입을 결정하는 것임
- 쉽게 비유하면 메서드의 매개변수와 인자의 관계와 비슷함
(1-2) 메서드에 필요한 값을 메서드 정의 시점에 미리 결정
- 메서드에 필요한 값을 이렇게 메서드 정의 시점에 미리 결정하게 되면 이 메서드는 오직 "hello"라는 값만 출력할 수 있으므로 재사용성이 떨어짐
void method1() {
println("hello");
}
(1-3) 메서드에 필요한 값을 인자를 통해 매개변수로 전달해서 결정
- 메서드에 필요한 값을 메서드를 정의하는 시점에 미리 결정하지 않고 매개변수를 지정하여 메서드를 사용할 때 원하는 값을 인자("hello", "hi")로 전달하여 메서드를 실제 사용하는 시점으로 미룰 수 있음
- 이 메서드는 실행 시점에 얼마든지 다른 값을 받아서 처리할 수 있기 때문에 다양한 값을 처리할 수 있고, 재사용성이 크게 늘어남
void method2(String param) {
println(param);
}
void main() {
method2("hello");
method2("hi");
}
(1-4) 메서드의 매개변수와 인자
- 매개변수(Parameter): String param
- 인자, 인수(Argument): arg
- 메서드의 매개변수에 인자를 전달하여 메서드의 사용 값을 결정함
void method(String param) //매개변수
void main() {
String arg = "hello";
method(arg) //인수 전달
}
(2) 제네릭의 타입 매개변수와 타입 인자
- 제네릭도 앞서 설명한 메서드의 매개변수와 인자의 관계와 비슷하게 작동하는데, 제네릭 클래스를 정의할 때 내부에서 사용할 타입을 미리 결정하지 않고 해당 클래스를 실제 사용하는 생성 시점에 내부에서 사용할 타입을 결정함
- 차이가 있다면 메서드의 매개변수는 사용할 값에 대한 결정을 나중으로 미루는 것이지만, 제네릭의 타입 매개변수는 사용할 타입에 대한 결정을 나중으로 미루는 것임
- 정리하면 메서드는 매개변수에 인자를 전달하여 사용할 값을 결정하고, 제네릭 클래스는 타입 매개변수에 타입 인자를 전달해서 사용할 타입을 결정함
- 제네릭에서 사용하는 용어도 매개변수, 인자의 용어를 그대로 가져다가 사용하지만 값이 아니라 타입을 결정하는 것이기 때문에 앞에 타입을 붙임
- 타입 매개변수: GenericBox<T> 에서 T
- 타입 인자: GenericBox<Integer>, GenericBox<String> 에서 Integer와 String
- 제네릭 타입의 타입 매개변수 <T>에 타입 인자를 전달하여 제네릭의 사용타입을 결정하게 됨
(3) 용어 정리
- 제네릭(Generic) 단어: 일반적인, 범용적인이라는 영어 단어로 특정 타입에 속한 것이 아니라 일반적으로, 범용적으로 사용할 수 있다는 뜻임
- 제네릭 타입(Generic Type)
- 클래스나 인터페이스를 정의할 때 타입 매개변수를 사용하는 것을 말하며 제네릭 클래스, 제네릭 인터페이스를 모두 합쳐서 제네릭 타입이라고 말함(타입은 클래스, 인터페이스, 기본형을 모두 합쳐서 부르는 말임)
- 예를 들어 class GenericBox<T> { ... } 라고 되어있으면 GenericBox<T>가 제네릭 타입임 - 타입 매개변수(Type Parameter): 제네릭 타입이나 메서드에서 사용되는 변수로 실제 타입으로 대체됨, GenericBox<T>에서 T가 타입 매개변수
- 타입 인자(Type Argument): 제네릭 타입을 사용할 때 제공되는 실제 타입, GenericBox<Integer>에서 Integer가 타입 인자
2) 제네릭 관례
(1) 제네릭 명명 관례
- 타입 매개변수는 일반적인 변수명처럼 소문자로 사용해도 문제는 없지만 일반적으로 대문자를 사용하고 용도에 맞는 단어의 첫글자를 사용하는 관례를 따름
- 주로 아래의 키워드를 사용함
- E - Element
- K - Key
- N - Number
- T - Type
- V - Value
- S,U,V etc. - 2nd, 3rd, 4th types
(2) 제네릭 기타
- class Data<K, V> { ... } 처럼 한번에 여러 타입 매개변수를 선언할 수 있음
- 타입 인자로 기본형을 사용할 수 없음
3) 로 타입 - row type
(1) RawTypeMain
- 제네릭 타입을 사용할 때는 항상 <>를 사용해서 사용시점에 원하는 타입을 지정해야 하지만, 아래의 예제처럼 지정하지 않아도 사용 가능한데, 이런것을 로 타입(raw type) 또는 원시 타입이라고 함
- 이렇게 원시 타입을 사용하면 내부의 타입 매개변수가 Object로 사용된다고 이해하면 됨
- 자바의 제네릭이 자바가 처음 등장할 때부터 있었던 것이 아니라 자바가 오랜기간 사용된 이후에 등장했기 때문에 제네릭이 없던 시적의 과거 코드와의 하위 호환이 필요했기 때문에 어쩔 수 없이 이런 로 타입을 지원하게 되었음
- 즉, 하위 호환성을 위해서 존재하는 타입이기 때문에 로 타입(원시 타입)은 사용하지 말아야 하며 Object 타입을 사용해야 한다면 타입 인자로 GenericBox<Object>로 Object를 지정해서 사용하면 됨
package generic.ex1;
public class RawTypeMain {
public static void main(String[] args) {
GenericBox integerBox = new GenericBox();
// GenericBox<Object> integerBox = new GenericBox<>(); // 권장
integerBox.set(10);
Integer result = (Integer) integerBox.get();
System.out.println("result = " + result);
}
}
5. 제네릭 활용 예제
1) 제네릭 활용 예제
(1) 구조
- generic.animal이라는 별도의 패키지에 생성
(2) Animal
- 이름, 크기 정보를 가지는 부모클래스로 toString()을 IDE 통해서 오버라이딩하였고 생성자와 게터가 있음
package generic.animal;
public class Animal {
private String name;
private int size;
public Animal(String name, int size) {
this.name = name;
this.size = size;
}
public String getName() {
return name;
}
public int getSize() {
return size;
}
public void sound() {
System.out.println("동물 울음 소리");
}
@Override
public String toString() {
return "Animal{" +
"name='" + name + '\'' +
", size=" + size +
'}';
}
}
(3) Dog, Cat
- Animal을 상속받고 sound()메서드를 각각 클래스에 맞게 오버라이딩
- 부모 클래스에 정의된 생성자가 있기 때문에 맞춰서 super(name, size)를 호출함
package generic.animal;
public class Dog extends Animal {
public Dog(String name, int size) {
super(name, size);
}
@Override
public void sound() {
System.out.println("멍멍");
}
}
public class Cat extends Animal {
public Cat(String name, int size) {
super(name, size);
}
@Override
public void sound() {
System.out.println("야옹");
}
}
(4) Box<T>
- 객체를 보관할 수 있는 제네릭 클래스
package generic.ex2;
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
(5) AnimalMain1
- Box 제네릭 클래스의 타입 인자로 각각의 타입을 전달하여 생성하면 타입에 맞는 동물을 보관하고 꺼낼 수 있음
- Box<Dog>, Box<Cat>, Box<Animal>로 각각 타입을 보관할 수 있음
package generic.ex2;
public class AnimalMain1 {
public static void main(String[] args) {
Animal animal = new Animal("동물", 0);
Dog dog = new Dog("멍멍이", 100);
Cat cat = new Cat("야옹이", 50);
Box<Dog> dogBox = new Box<>();
dogBox.set(dog);
Dog findDog = dogBox.get();
System.out.println("findDog = " + findDog);
Box<Cat> catBox = new Box<>();
catBox.set(cat);
Cat findCat = catBox.get();
System.out.println("findCat = " + findCat);
Box<Animal> animalBox = new Box<>();
animalBox.set(animal);
Animal findAnimal = animalBox.get();
System.out.println("findAnimal = " + findAnimal);
}
}
/* 실행 결과
findDog = Animal{name='멍멍이', size=100}
findCat = Animal{name='야옹이', size=50}
findAnimal = Animal{name='동물', size=0}
*/
(6) AnimalMain2
- 여기서 Box<Animal>인 경우 타입 매개변수 T에 타입 인자 Animal을 대입하면 set(Animal value)가 되므로 Animal()의 하위 타입인 Dog, Cat도 전달할 수 있음
- 물론 꺼낼 때는 반환타입이 Animal이기 때문에 Animal타입으로만 꺼낼 수 있음
package generic.ex2;
public class AnimalMain2 {
public static void main(String[] args) {
Animal animal = new Animal("동물", 0);
Dog dog = new Dog("멍멍이", 100);
Cat cat = new Cat("야옹이", 50);
Box<Animal> animalBox = new Box<>();
animalBox.set(animal);
animalBox.set(dog);
animalBox.set(cat);
Animal findAnimal = animalBox.get();
System.out.println("findAnimal = " + findAnimal);
}
}
/* 실행 결과
findAnimal = Animal{name='야옹이', size=50}
*/
6. 문제와 풀이
1) 제네릭 기본1
(1) 문제 설명
- 다음 코드와 실행 결과를 참고하여 Container 클래스를 생성
- Container 클래스는 제네릭을 사용해야 함
더보기
package generic.test.ex1;
public class ContainerTest {
public static void main(String[] args) {
Container<String> stringContainer = new Container<>();
System.out.println("빈값 확인1: " + stringContainer.isEmpty());
stringContainer.setItem("data1");
System.out.println("저장 데이터: " + stringContainer.getItem());
System.out.println("빈값 확인2: " + stringContainer.isEmpty());
Container<Integer> integerContainer = new Container<>();
integerContainer.setItem(10);
System.out.println("저장 데이터: " + integerContainer.getItem());
}
}
실행 결과
빈값 확인1: true
저장 데이터: data1
빈값 확인2: false
저장 데이터: 10
(2) 정답
더보기
package generic.test.ex1;
public class Container<T> {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
public boolean isEmpty() {
return item == null;
}
}
2) 제네릭 기본2
(1) 문제 설명
- 다음 코드와 실행 결과를 참고하여 Pair 클래스를 생성
- Pair 클래스는 제네릭을 사용해야 함
더보기
package generic.test.ex2;
public class PairTest {
public static void main(String[] args) {
Pair<Integer, String> pair1 = new Pair<>();
pair1.setFirst(1);
pair1.setSecond("data");
System.out.println(pair1.getFirst());
System.out.println(pair1.getSecond());
System.out.println("pair1 = " + pair1);
Pair<String, String> pair2 = new Pair<>();
pair2.setFirst("key");
pair2.setSecond("value");
System.out.println(pair2.getFirst());
System.out.println(pair2.getSecond());
System.out.println("pair2 = " + pair2);
}
}
실행 결과
1
data
pair1 = Pair{first=1, second=data}
key
value
pair2 = Pair{first=key, second=value}
(2) 정답
더보기
package generic.test.ex2;
public class Pair<K, V> {
private K first;
private V second;
public K getFirst() {
return first;
}
public void setFirst(K first) {
this.first = first;
}
public V getSecond() {
return second;
}
public void setSecond(V second) {
this.second = second;
}
@Override
public String toString() {
return "Pair{" +
"first=" + first +
", second=" + second +
'}';
}
}
728x90