Posts 학원 #29일차: 캡슐화와 접근제어, 다형성과 형변환, 제네릭, CRUD
Post
Cancel

학원 #29일차: 캡슐화와 접근제어, 다형성과 형변환, 제네릭, CRUD

미니 프로젝트 CRUD 완성

어제는 mini pms 프로젝트에 detail() 메서드를 구현하는 것까지 했다. 오늘은 update()와 delete()를 추가하여 CRUD를 완성했다.

제네릭

제네릭(Generic)이란?

다양한 타입의 객체들을 다루는 메서드컬렉션 클래스컴파일 시의 타입 체크를 해주는 기능이다. 객체의 타입을 컴파일 시에 체크하기 때문에 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.

자바에서 제안하는 변수(타입 파라미터) 이름은 다음과 같다. T - type E - element K - key V - value N - number 여러 개의 타입 파라미터를 선언해야 할 경우 보통 두 번째 파라미터부터 다음 알파벳을 사용한다. S, U, V 등 예) class Box<T,S,U,V> {…}

제네릭 등장 배경

객체들을 저장하기 위한 클래스를 정의해야 할 때 어떻게 할 수 있을까?

1단계: 각 타입에 대한 클래스들을 정의한다.

  • 같은 일을 하는 클래스임에도 다루는 객체 타입이 다르다는 이유로 반복적으로 정의해야 한다.
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// 제네릭(Generic) - 사용 전
package com.eomcs.generic.ex01;

public class Exam0110 {
  
  // 제네릭 문법이 없다면 각 타입의 객체를 저장하기 위해 
  // 다음과 같이 각 타입에 대한 클래스를 정의해야 한다.
  //
  static class MemberBox {
    Member value;
    
    public void set(Member value) {
      this.value = value;
    }
    
    public Member get() {
      return value;
    }
  }
  
  static class StringBox {
    String value;
    
    public void set(String value) {
      this.value = value;
    }
    
    public String get() {
      return value;
    }
  }
  
  static class IntegerBox {
    Integer value;
    
    public void set(Integer value) {
      this.value = value;
    }
    
    public Integer get() {
      return value;
    }
  }
  
  public static void main(String[] args) {
    
    // Member 객체를 저장하려면 MemberBox를 사용해야 한다.
    MemberBox box1 = new MemberBox();
    box1.set(new Member("홍길동", 20)); // 값 저장
    Member m = box1.get(); // 값 꺼내기
    System.out.println(m);
    
    // String 객체를 저장하려면 StringBox를 사용해야 한다.
    StringBox box2 = new StringBox();
    box2.set(new String("Hello"));
    String str = box2.get();
    System.out.println(str);
    
    // Integer 객체는 IntegerBox를 사용해서 저장한다.
    IntegerBox box3 = new IntegerBox();
    box3.set(100); // auto-boxing ==> Integer.valueOf(100)
    int i = box3.get(); // auto-unboxing ==> box3.get().intValue() 
    System.out.println(i);
    
    // 해결책?
    // => 다양한 타입의 객체를 저장할 수 있도록 다형성의 다형적 변수 특징을 이용하여 
    //    값을 저장하는 인스턴스 변수를 Object 타입으로 정의한다.
  }
}

2단계: 다형적 변수 활용

  • 인스턴스 변수 타입을 모든 자바 객체를 받을 수 있는 Object로 선언한다.
    • 값을 꺼낼 때 원래의 타입으로 형변환해야 해야 한다.
    • 다루는 객체를 특정 타입의 객체로 제한할 수 없다.
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 제네릭(Generic) - 사용 전: 다형적 변수 활용
package com.eomcs.generic.ex01;

public class Exam0111 {
  
  // 다양한 객체를 저장하는 Box를 만들기 위해 
  // 인스턴스의 변수 타입을 Object로 선언한다.
  // => 그러면 다음과 같이 한 개의 Box 클래스만 정의하면 된다.
  //
  static class ObjectBox {
    Object value;
    
    public void set(Object value) {
      this.value = value;
    }
    
    public Object get() {
      return value;
    }
  }
  
  public static void main(String[] args) {
    
    ObjectBox box1 = new ObjectBox();
    box1.set(new Member("홍길동", 20)); // 값 저장
    Member m = (Member) box1.get(); // 값을 꺼낼 때 형변환해야 한다.
    System.out.println(m);
    
    ObjectBox box2 = new ObjectBox();
    box2.set(new String("Hello"));
    String str = (String) box2.get(); // 값을 꺼낼 때 원래의 타입으로 형변환 한다.
    System.out.println(str);
    
    ObjectBox box3 = new ObjectBox();
    box3.set(100); // auto-boxing ==> Integer.valueOf(100)
    int i = (int) box3.get(); // 값을 꺼낼 때 primitive date type을 지정하면 
                              // 자동으로 auto-unboxing으로 바꾼다.
    System.out.println(i);
    
    // 객체의 타입 별로 Box 클래스를 구분해서 쓰지 않으니 코딩이 편하다. 
    // 단 값을 꺼낼 때 원래의 타입으로 바꾸기 위해 형변환(type casting) 해야 하는 불편함이 있다.
    //
    
    // Member 객체를 저장하기 위해 다음과 같이 Box를 준비했다고 가정하자!
    ObjectBox box2 = new ObjectBox();
    
    // 원래는 다음과 같이 Member 객체를 넣으려고 box2을 준비한 것이다.
    box1.set(new Member("홍길동", 20));
    
    // 원래의 목적과 다르게 다음과 같이 Member가 아닌 String 객체를 넣어도 막을 방법이 없다.  
    box1.set(new String("Helllo"));  
    // set()의 파라미터 타입이 모든 자바 객체를 받을 수 있는 Object이기 때문이다.
      
  }
}

3단계: 제네릭 사용 (다형적 변수의 한계 극복)

  • 제네릭을 통해 한 개의 클래스로 형변환 없이 특정 타입 전용 클래스로 만들 수 있다.
  • 타입 파라미터: 클래스에서 다룰 객체의 타입을 파라미터로 받는다.
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class Exam0113 {

  // Box 클래스에서 다룰 객체의 타입을 파라미터로 받는다.
  static class Box<T> {
    T value;

    public void set(T value) {
      this.value = value;
    }

    public T get() {
      return value;
    }
  }

  public static void main(String[] args) {

    // 제네릭을 사용하여 정의된 클래스를 사용할 경우에는 
    // 클래스의 레퍼런스를 정의할 때 어떤 타입의 값을 다룰 것인지 제한을 걸어야 한다.
    // 즉 지정해야 한다.
    Box<Member> box1;

    // 인스턴스를 생성할 때도 어떤 타입의 값을 다룰 것인지 지정해야 한다.
    box1= new Box<Member>();

    // 객체를 생성할 때 타입 지정을 생략할 수 있다.
    box1 = new Box<>();

    // <> 빼면 경고가 뜨며, 내부적으로는 Object를 타입으로 지정한 것으로 다룬다.
    // 즉 다음 코드는 box1 = new Box<Object>() 와 같다.
    // 
    //box1 = new Box(); // 경고!

    // 메서드를 사용할 때 그 타입 전용 클래스인 것처럼 쓰면 된다.
    box1.set(new Member("홍길동", 20));

    // 따라서 다음과 같이 값을 꺼낼 때도 따로 형변환 할 필요가 없다.
    Member m = box1.get();
    System.out.println(m);

    // 제네릭의 특징
    // => 지정한 타입 전용 클래스로 동작한다.
    // => 그래서 타입이 아닌 경우 메서드를 호출할 수 없다.
    //
    //box1.set(new String("Hello!")); // 컴파일 오류!

    // 결론:
    // => 제네릭 문법을 사용하면 Object 타입을 사용하는 것 보다 편리하다.
    // => 각 타입 별로 클래스를 따로 정의한듯한 효과가 있다.
  }
}

제네릭의 특징

  • 다루는 타입을 제한할 수 있다.
    • ArrayList가 어떤 종류(타입, 클래스)의 객체를 저장할 것인지 지정할 수 있다. 지정된 타입 외에 다른 타입은 저장할 수 없다.
1
2
3
4
// 클래스명 옆에 다루고자 하는 타입의 이름을 지정한다.
ArrayList<Member> list = new ArrayList<Member>();
list.add(new Member("홍길동", 20));
//list.add(new String("Hello"));
  • 제네릭을 지정하면 그와 관련된 메서드의 타입 정보가 자동으로 바뀐다. => 형변환 필요 X
1
2
3
4
5
6
7
8
9
10
11
12
Member member = list.get(0);
System.out.println(member.name);

// 만약 제네릭이라는 문법이 없다면?
// 1) 값을 꺼낼 때 마다 형변환을 해야 한다.
//    예) Member member = (Member) list.get(0);
//
// 2) 형변환하기 싫다면 값의 종류별로 ArrayList를 만들어야 한다.
//    예) class MemberArrayList {...}
//
// 제네릭 문법이 존재하므로,
// => 한 개의 클래스로 형변환 없이 특정 타입 전용 클래스로 만들 수 있다.

여러개의 타입 파라미터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Exam0130 {
  // 자바에서 제안하는 "타입 파라미터"의 이름으로 정의한다면,
  static class A2<T,S,U> {
    T v1;
    S v2;
    U v3;
  }
  
  public static void main(String[] args) {
    A<String,Integer,Member> obj = new A<>();
    
    obj.v1 = new String("Hello");
    obj.v2 = new Integer(100);
    obj.v3 = new Member("홍길동", 20);
  }
}

레퍼런스와 인스턴스 생성

  • 사용법: 클래스명 옆에 다루고자 하는 타입의 이름을 지정한다. 클래스명<타입명>
  • 레퍼런스 선언에 제네릭 정보가 있다면 new 연산자에서는 생략할 수 있다.
  • 제네릭 문법으로 레퍼런스 변수를 선언할 때는 타입명을 생략할 수 없다.
1
2
3
4
5
6
7
ArrayList<Member> list = new ArrayList<Member>();

ArrayList<Member> list2 = new ArrayList</*Member*/>(); // OK!
ArrayList<Member> list3;
list3 = new ArrayList<>(); // OK!

//ArrayList<> list4; // 컴파일 오류!
  • 레퍼런스 변수 선언 시 <타입명>을 생략할 경우
    • ArrayList list1ArrayList<?> list와 같다.
    • 따라서 다루는 타입에 상관 없이 ArrayList를 선언하고 싶다면, 명확하게 <?>를 붙이자.
  • 타입 파라미터로 특정 클래스를 지정한 경우, 해당 타입과 그 하위 클래스 타입들이 다뤄진다.

제네릭과 파라미터

인스턴스와 레퍼런스 타입 파라미터의 관계는 메서드의 아규먼트와 파라미터일 때도 동일하게 적용된다.

메서드(ArrayList)

  • 특정 타입으로 제한하는 문법이 무용지물이 된다.
  • 따라서 제네릭으로 선언된 클래스를 사용할 때는 반드시 타입 파라미터를 지정해야 한다.
  • 제네릭 문법의 목적은 코드 안정성을 추구하는 것이다.
  • 원하는 타입이 아닌 다른 타입의 값을 지정하는 오류(타입 오류)를 줄이기 위해 만든 문법이다.
  • 제네릭 문법의 대상은 컴파일러다.
  • 즉 컴파일 단계에서 최대한으로 타입 오류를 잡아내는 것이 목적이다.
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
  public static void main(String[] args) {
    // => 제네릭의 타입을 지정하지 않으면, 
    //    다음과 같이 다양한 종류의 ArrayList를 파라미터로 넘길 수 있다.

    m1(new ArrayList()); // OK
    m1(new ArrayList<A>()); // OK
    m1(new ArrayList<B1>()); // OK
    m1(new ArrayList<B2>()); // OK
    m1(new ArrayList<C>()); // OK
    System.out.println("실행 완료!");
  }

  // 제네릭의 타입을 지정하지 않으면 
  // 위와 같이 특정 타입으로 제한하는 문법이 무용지물이 된다.
  // 따라서 제네릭으로 선언된 클래스를 사용할 때는 반드시 타입 파라미터 값을 지정하라!
  static void m1(ArrayList list) {
    list.add(new Object());
    list.add(new A());
    list.add(new B1());
    list.add(new B2());
    list.add(new C());

    System.out.println(list.get(0));
    System.out.println(list.get(1));
    System.out.println(list.get(2));
    System.out.println(list.get(3));
    System.out.println(list.get(4));
  }

메서드(ArrayList<?>)

  • 모든 타입에 대해 ArrayList 객체를 파라미터로 넘길 수 있다.
  • 다만 메서드 내부에서는 타입 검사를 할 수 없기 때문에 타입 검사가 필요한 코드(ex: add())를 사용한 경우 컴파일 오류가 발생한다.
    • 아래 코드에서 컴파일러는 파라미터로 받은 ArrayList가 어떤 타입의 값을 다루는 지 알 수 없기 때문에 그 타입인지 검사해야 하는 메서드(ex: add())를 사용할 때는 컴파일을 명확하게 해줄 수 없다. 따라서 컴파일 오류를 발생시키는 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  static void m1(ArrayList<?> list) {
    /* 
    list.add(new Object());
    list.add(new A());
    list.add(new B1());
    list.add(new B2());
    list.add(new C());
     */
	
    Object obj1 = list.get(0);

    // 다음의 경우도 마찬가지로 get()의 리턴 타입이 무엇인지 결정할 수 없기 때문에 
    // 컴파일 해 줄수 없다. 그래서 컴파일 오류가 발생한다. 
    //A obj2 = list.get(1); // 컴파일 오류!

    // println()의 파라미터 타입이 Object 이기 때문에 다음 코드는 오류가 아니다.
    System.out.println(list.get(0));
    System.out.println(list.get(1));
    System.out.println(list.get(2));
    System.out.println(list.get(3));
    System.out.println(list.get(4));
  }

메서드(ArrayList<A>)

  • A 타입에 대해서만 ArrayList 객체를 파라미터로 넘길 수 있다.
  • A 타입의 하위 타입은 불가능하다.
    • A타입의 하위 타입을 받기 위해서는 <? extends A> 문법을 사용해야 한다.
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.eomcs.generic.ex01;

import java.util.ArrayList;

public class Exam0223 {

  static class A {}
  static class B1 extends A {}
  static class B2 extends A {}
  static class C extends B1 {}

  public static void main(String[] args) {
    // m1(ArrayList<B1>)
    // => B1 타입에 대해서만 ArrayList 객체를 파라미터로 넘길 수 있다.
    // => B1의 하위 타입은 불가능
    //
    ArrayList list = new ArrayList(); // 모든 타입의 객체를 다 저장할 수 있다.
    list.add(new B1());
    list.add(new String());
    list.add(new java.util.Date());
    list.add(new StringBuffer());

    m1(list); // 컴파일 오류는 아니지만, 타입을 지정하지 않는 것은 바람직하지 않다.
    // 왜?
    //  => 타입을 지정하지 않은 ArrayList에는 B1이 아닌 다른 타입의 객체가 들어갈 수 있다.
    //  => 그런데 m1()에서는 B1만 들어있을 것이라 확신하고, ArrayList를 사용할 것이다.
    //  => 그래서 m1()을 실행하는 중에 get()을 호출할 때 형변환 문제가 발생한다.

    //m1(new ArrayList<Object>()); // 컴파일 오류!
    //m1(new ArrayList<A>()); // 컴파일 오류!
    //m1(new ArrayList<B1>()); // OK!
    //m1(new ArrayList<B2>()); // 컴파일 오류!
    //m1(new ArrayList<C>()); // 컴파일 오류!
  }

  static void m1(ArrayList<B1> list) {
    // 컴파일러는 파라미터로 넘어온 ArrayList가  어떤 타입이든 간에 
    // 문법적으로 B1 타입의 값만 다룬다.

    //list.add(new Object()); // 컴파일 오류!
    //list.add(new A()); // 컴파일 오류!
    list.add(new B1());
    //list.add(new B2()); // 컴파일 오류!
    list.add(new C());

    // list에 B1 타입이 아닌 다른 타입을 저장할 때는 아무런 문제가 발생하지 않았지만,
    // ArrayList에서 값을 꺼낼 때 메서드에서 지정한 <B1> 타입이 아니면 ClassCastException 발생!
    for(int i = 0; i < list.size(); i++) {
      B1 temp = list.get(i);
      System.out.println(temp);
    }
  }
}

메서드(ArrayList<? extends A>)

  • A 타입 및 그 하위 타입에 대해서 ArrayList 객체를 파라미터로 넘길 수 있다.
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
29
30
31
32
33
package com.eomcs.generic.ex01;

import java.util.ArrayList;

public class Exam0224 {

  static class A {}
  static class B1 extends A {}
  static class B2 extends A {}
  static class C extends B1 {}

  public static void main(String[] args) {
    // m1(ArrayList<? extends B1>)
    // => A 타입 및 그 하위 타입에 대해서 ArrayList 객체를 파라미터로 넘길 수 있다.
    //
    //m1(new ArrayList<Object>()); // 컴파일 오류!
    //m1(new ArrayList<A>()); // 컴파일 오류!
    m1(new ArrayList<B1>()); 
    //m1(new ArrayList<B2>()); // 컴파일 오류!
    m1(new ArrayList<C>()); 
  }

  static void m1(ArrayList<? extends B1> list) {
    // 파라미터로 받은 ArrayList가 구체적으로 어떤 타입의 값을 다루는 것인지 
    // 결정되지 않았기 때문에 컴파일러는 다음 코드가 옳은지 검사할 수 없다.
    // 그래서 컴파일 오류가 발생한다.
    //list.add(new B1()); // 컴파일 오류!

    Object obj1 = list.get(0);
    B1 obj2 = list.get(0);
    //C obj3 = list.get(0); // 컴파일 오류!
  }
}

제네릭 객체의 메서드 사용

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
public class Exam0231 {

  static class A {}
  static class B1 extends A {}
  static class B2 extends A {}
  static class C extends B1 {}

  public static void main(String[] args) {

    ArrayList<B1> list1 = new ArrayList<>();

    // ArrayList의 항목 타입을 B1으로 지정했기 때문에  
    // ArrayList의 메서드 파라미터나 리턴 타입은 B1으로 설정된다.

    // => add(B1)
    //list1.add(new Object()); // 컴파일 오류!
    //list1.add(new String()); // 컴파일 오류!
    //list1.add(new Integer(100)); // 컴파일 오류!
    //list1.add(new Member("홍길동", 20)); // 컴파일 오류!
    //list1.add(new A()); // 컴파일 오류!
    list1.add(new B1());
    //list1.add(new B2()); // 컴파일 오류!
    list1.add(new C());

    System.out.println("종료!");
  }
}

제네릭 응용: HashMap

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
29
30
31
public class Exam0310 {
  public static void main(String[] args) {
    // 제네릭 정보가 필요한 클래스를 사용할 때는 
    // 어떤 클래스를 다룰 것인지 타입이름을 알려줘야 한다.
    // 그런데 다음 코드처럼 "key 값으로 어떤 클래스를 사용할 것이고"
    // "value 값으로 어떤 클래스를 사용할 것인지" 알려주지 않으면
    // 컴파일러가 경고를 띄운다. 물론 경고시 무시해도 실행에 문제가 없다.
    // Object 타입으로 지정하더라도 클래스 이름을 명시하는 것이 좋다.
    HashMap map = new HashMap(); // Object key, Object value를 다룬다.
    map.put("aaa", "문자열");
    map.put(new Integer(100), new Member("홍길동", 20));

    // 위와 같이 제네릭 정보를 넘기지 않는 것 보다,
    // 차라리 다음과 같이 명확하게 Object 타입을 지정하는 것이 좋다. 
    // => 어떤 타입의 key와 어떤 타입의 value를 저장할 것인지
    //    다른 개발자에게 명확하게 알려주는 효과가 있기 때문에 다음을 권장한다.
    HashMap<Object,Object> map2 = new HashMap<>();
    map2.put("aaa", "문자열");
    map2.put(new Integer(100), new Member("홍길동", 20));

    // key: String
    // value: Member
    HashMap<String,Member> map3 = new HashMap<>();
    //map3.put("aaa", "문자열"); // 컴파일 오류!
    //map3.put(new Integer(100), new Member("홍길동", 20)); // 컴파일 오류!
    map3.put("aaa", new Member("홍길동", 20)); // OK!

    // 제네릭을 지정하면 값을 꺼낼 때 형변환할 필요가 없다.
    Member m = map3.get("aaa"); // OK!

  }

제네릭 타입의 수퍼클래스 지정

​ Member

​ /\

​ Teacher Student Manager

​ Administrator

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
29
30
31
32
33
34
35
36
37
38
39
public class Exam0120 {

  // 제네릭의 타입을 지정할 때 수퍼 클래스를 지정하면 그 타입 및 서브 타입만 가능하다.
  // => 다음 클래스의 타입 파라미터에는 
  //    Manager 나 그 하위 클래스만 지정할 수 있다. 
  //
  static class ArrayList<T extends Manager> {
    T[] arr;
    int index = 0;

    @SuppressWarnings("unchecked")
    public ArrayList(Class<?> clazz) {
      this.arr = (T[])Array.newInstance(clazz, 10);
    }

    public void add(T v) {
      arr[index++] = v;
    }

    public T get(int index) {
      return arr[index];
    }
  }

  public static void main(String[] args) {

    //ArrayList<Member> obj = new ArrayList<>(Member.class); // 컴파일 오류!
    //ArrayList<Teacher> obj = new ArrayList<>(Teacher.class); // 컴파일 오류!
    //ArrayList<Student> obj = new ArrayList<>(Student.class); // 컴파일 오류!

    ArrayList<Manager> obj = new ArrayList<>(Manager.class); // OK!
    //ArrayList<Administrator> obj = new ArrayList<>(Administrator.class); // OK!

    obj.add(new Manager());
    obj.add(new Administrator());

    System.out.println(obj.get(0));
    System.out.println(obj.get(1));
  }
This post is licensed under CC BY 4.0 by the author.

[JS] 생활코딩 #30: 재귀함수

학원 #30일차: CRUD, 제네릭문법

Loading comments from Disqus ...