본문 바로가기

국비 교육

2020.9.1일자 수업 : CRUD 실습, 제네릭

 실습 - CRUD 완성 

git/eomcs-java-project-200/mini-pms-18

어제는 detail() 메서드를 구현하는 것까지 했다. 오늘은 update()와 delete()를 추가하여 CRUD를 완성한다.

 

훈련 목표

  • update()를 추가한다
  • delete()를 추가한다

1단계 : update()를 각 Handler 클래스에 추가한다. ProjectHandler와 TaskHandler의 유효한 회원 검사 과정도 함께 추가한다.

public void update() {
    System.out.println("[회원 변경]");
    int no = Prompt.inputInt("번호? ");
    Member member = findByNo(no);
    if (member == null) {
      System.out.println("해당 번호의 회원이 없습니다.");
      return;
    } else {
      String name = Prompt.inputString(String.format("이름(%s)? ", member.getName()));
      String email = Prompt.inputString(String.format("이메일(%s)? ", member.getEmail()));
      String photo = Prompt.inputString(String.format("사진(%s)? ", member.getPhoto()));
      String tel = Prompt.inputString(String.format("전화(%s)? ", member.getTel()));
      if (Prompt.inputString("정말 변경하시겠습니까?(y/N)").equalsIgnoreCase("Y")) {
        member.setName(name);
        member.setEmail(email);
        member.setPhoto(photo);
        member.setTel(tel);
        System.out.println("회원을 변경했습니다.");
      } else {
        System.out.println("회원 변경을 취소했습니다.");
      }
    }
  }
public void update() { 
    System.out.println("[프로젝트 변경]");
    int no = Prompt.inputInt("번호? ");
    Project project = findByNo(no);
    if (project == null) {
      System.out.println("해당 번호의 프로젝트가 없습니다.");
    } else {
      String title = Prompt.inputString(String.format("프로젝트명(%s)? ", project.getTitle()));
      String content = Prompt.inputString(String.format("내용(%s)? ", project.getContent()));
      Date startDate = Prompt.inputDate(String.format("시작일(%s)? ", project.getStartDate()));
      Date endDate = Prompt.inputDate(String.format("종료일(%s)? ", project.getEndDate()));
      String name;
      while (true) {
        name = Prompt.inputString(String.format("만든이?(%s)(취소: 빈 문자열) ", project.getOwner()));

        if (name.length() == 0) {
          System.out.println("프로젝트 변경을 취소합니다.");
          return;
        } else if (memberHandler.findByName(name) != null) {
          break;
        }

        System.out.println("등록된 회원이 아닙니다.");
      }
      
      StringBuilder members = new StringBuilder();
      while (true) {
        String teamMember = Prompt.inputString("팀원?(완료: 빈 문자열) ");

        if (teamMember.length() == 0) {
          break;
        } else if (memberHandler.findByName(teamMember) != null) {
          if (members.length() > 0) {
            members.append(",");
          }
          members.append(teamMember);
        } else {
          System.out.println("등록된 회원이 아닙니다.");
        }
      }
      
      if (Prompt.inputString("정말 변경하시겠습니까?(y/N)").equalsIgnoreCase("Y")) {
        project.setTitle(title);
        project.setContent(content);
        project.setStartDate(startDate);
        project.setEndDate(endDate);
        project.setOwner(name);
        project.setMembers(members.toString());
        System.out.println("프로젝트를 변경했습니다.");
      } else {
        System.out.println("프로젝트 변경을 취소했습니다.");
      }
    }
  }
public void update() {
    System.out.println("[작업 변경]");
    int no = Prompt.inputInt("번호? ");
    Task task = findByNo(no);
    if (task == null) {
      System.out.println("해당 번호의 작업이 없습니다.");
    } else {
      String content = Prompt.inputString(String.format("내용(%s)? ", task.getContent()));
      Date deadline = Prompt.inputDate(String.format("마감일(%s)? ", task.getDeadline()));
      int status = Prompt.inputInt(String.format("상태(%s)?\n0: 신규\n1: 진행중\n2: 완료\n> ", task.getStatus()));
      String name;
      while (true) {
        name = Prompt.inputString(String.format("담당자(%s)?(취소: 빈 문자열) ", task.getOwner()));
        if (name.length() == 0) {
          System.out.println("작업 등록을 취소합니다.");
          return;
        } else if (memberHandler.findByName(name) != null) {
          break;
        }
        System.out.println("등록된 회원이 아닙니다.");
      }
      if (Prompt.inputString("정말 변경하시겠습니까?(y/N)").equalsIgnoreCase("y")) {
        task.setContent(content);
        task.setDeadline(deadline);
        task.setStatus(status);
        task.setOwner(name);
        System.out.println("작업을 변경했습니다.");
      }
    }
  }

2단계 : delete()를 각 Handler 클래스에 추가한다. 항목을 지우는 ArrayList의 remove() 메서드를 사용하려면, 인덱스를 알아야하므로 번호로 객체의 인덱스를 찾는 메서드를 Handler에 추가한다.

 

public void delete() {
    System.out.println("[회원 변경]");
    int no = Prompt.inputInt("번호? ");
    int index = indexOf(no);
    if (index == -1) {
      System.out.println("해당 번호의 회원이 없습니다.");
      return;
    } else {
      if (Prompt.inputString("정말 삭제하시겠습니까?(y/N) ").equalsIgnoreCase("y")) {
        memberList.remove(index);
        System.out.println("회원을 삭제했습니다.");
      } else {
        System.out.println("회원 삭제를 취소했습니다.");
      }
    }
  }
    private int indexOf(int no) {
    for (int i = 0; i < memberList.size(); i++) {
      if (memberList.get(i).getNo() == no) {
        return i;
      }
    }
    return -1;
  }

 

 


 제네릭 Generic 

git/eomcs-java-basic/src/main/java com.eomcs.generic.ex01~ex03

제네릭 문법이 없다면

  • 다루는 데이터 타입만 다르고, 기능은 비슷한 여러 클래스들을 클래스별로 모두 만들어야한다.
  • 이를 Object 클래스로 모두 통일한 한 클래스를 만들었다고 가정해도
    • 원하지 않는 데이터 타입의 객체를 필드에 집어넣는 것을 막을 수 없다.
    • 필드의 값들을 꺼낼때마다 Object에서 원하는 클래스 타입으로 명시적 형변환을 해줘야한다.

제네릭 문법은 클래스에서 다루는 값들의 클래스 타입을 변수화시킴으로써

  • 다루는 클래스 타입만 다르고, 기능은 비슷한 여러 클래스를 하나로 정의할 수 있다.
  • 원하지 않는 클래스 타입의 값을 필드에 넣을 때 컴파일 단계에서 에러를 띄울 수 있다.
  • 필드의 값들을 꺼낼 때마다 원하는 클래스 타입으로 꺼내어 명시적 형변환 과정을 생략할 수 있다

제네릭이란?

다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크를 해주는 기능이다.

컴파일 시에 체크하기 때문에 타입 안정성을 높이고, 형변환의 번거로움이 줄어든다.

다른 말로는 클래스가 다루는 객체의 타입을 변수화하는 기능이다. 이 변수를 타입 파라미터라고 부른다. 그러나 정말로 내부적으로 변수화되어 원하는 데이터 타입이 들어가는 것은 아니다. 언제까지나 제네릭은 변수에 관한 오류를 컴파일 단계에서 방지하기 위한 개념이다.

 

흔히 쓰이는 타입 변수명

  • E - Element (used extensively by the Java Collections Framework)
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S,U,V etc. - 2nd, 3rd, 4th types

대표적인 제네릭 사용 클래스 ArrayList

특징 1 : ArrayList를 선언할 때 지정한 타입이 아닌 경우에는 컴파일 오류가 발생한다.

  ArrayList<Member> list = new ArrayList<Member>();
  list.add(new Member("홍길동", 20));

//list.add(new String("Hello"));
//list.add(new Integer(100));
//list.add(new HashSet());

특징 2: 제네릭을 지정하면 그와 관련된 메서드의 타입 정보가 자동으로 바뀐다. 즉, 형변환하는 번거로움이 없다.=

Member member = list.get(0);
System.out.println(member.name);
System.out.println(member.age);

제네릭을 사용하는 방법

클래스 명 옆에 다루고자 하는 타입의 이름을 지정한다. 

//    클래스명<타입명>
ArrayList<Member> list = new ArrayList<Member>();

// => 레퍼런스 선언에 제레릭 정보가 있다면 new 연산자에서는 생략할 수 있다.
ArrayList<Member> list2 = new ArrayList<>(); // OK!

// 제네릭 문법으로 레퍼런스 변수를 선언할 때는 타입명을 생략할 수 없다.
//ArrayList<> list4; // 컴파일 오류!

레퍼런스와 인스턴스 생성

레퍼런스 변수에 특정 객체의 타입을 지정하면 주소를 할당할 수 있는 인스턴스는 다음과 같다.

  • 타입 파라미터 미지정한 인스턴스 : 가능은 하지만 이렇게 사용할 경우, 인스턴스 안에 원하는 데이터 타입이 아닌 객체가 들어있을 때 형변환 과정에 오류가 생길 수 있으므로 쓰지 않는 것이 좋다. 이렇게 코드를 짜면 객체가 raw type이라는 경고가 뜬다.
  • 타입 파라미터 생략 new ArrayList<>() : <>은 레퍼런스 변수와 같은 타입의 타입 파라미터을 지정한다는 의미이다.
  • 레퍼런스 변수와 같은 타입의 타입 파라미터 
ArrayList<Object> list1;

list1 = new ArrayList(); 
// 이렇게 사용하지 말고, 명확히 제네릭의 타입을 지정하라.
// 이렇게 사용하면, 인스턴스 안에 원하는 데이터 타입이 아닌 객체가 있을 경우
// 형변환에 문제가 생길 위험이 있다.

list1 = new ArrayList<>();
// <>은 레퍼런스 변수와 같은 타입의 파라미터 타입을 지정한다는 의미이다.

list1 = new ArrayList<Object>();

* 레퍼런스와 인스턴스의 타입 파라미터들의 관계에는 다형성이 적용되지 않는다. 즉, 위에 세  경우 말고는 다른 타입을 지정한 인스턴스의 주소를 레퍼런스에 저장할 수 없다.

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<A> list1;

  list1 = new ArrayList(); // 이렇게 사용하지 말고, 명확히 제네릭의 타입을 지정하라.
//list1 = new ArrayList<Object>(); // 컴파일 오류!
  list1 = new ArrayList<>();
  list1 = new ArrayList<A>();
//list1 = new ArrayList<B1>(); // 컴파일 오류!
//list1 = new ArrayList<B2>(); // 컴파일 오류!
//list1 = new ArrayList<C>(); // 컴파일 오류!

타입 파라미터와 다룰 수 있는 타입

타입 파라미터로 특정 클래스를 지정한 경우, 해당 타입과 그 하위 클래스 타입들이 다뤄진다.

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

list1.add(new String()); 
list1.add(new java.util.Date());
list1.add(new Integer(100));
list1.add(new StringBuffer());
  static class A {}
  static class B1 extends A {}
  static class B2 extends A {}
  static class C extends B1 {}


  ArrayList<Object> list = new ArrayList<>();
  list.add(new Object());
  list.add(new A());
  list.add(new B1());
  list.add(new B2());
  list.add(new C());

  ArrayList<B1> list2 = new ArrayList<>();
//list.add(new Object()); // 컴파일 오류
//list.add(new A()); // 컴파일 오류
  list.add(new B1());
//list.add(new B2()); // 컴파일 오류
  list.add(new C());

 

파라미터 타입이 지정되지 않은 인스턴스의 주소를 저장한 레퍼런스는 인스턴스와 상관없이 레퍼런스 변수가 갖는 타입 파라미터를 기준으로 컴파일 한다. 즉 레퍼런스 변수의 타입 파라미터 객체 혹은 그 하위 객체들이 다뤄진다. 그런데 만약 인스턴스에 이미 해당 파라미터 타입 이외의 객체가 들어있다면 그것들을 꺼낼 때 형변환에 문제가 발생할 수 있다.

  ArrayList list = new ArrayList();
  list.add(new Object());
  list.add(new Member());

  ArrayList<String> list2 = list;
  list2.add(new String());
//list.add(new Object()); // 컴파일 에러

  for (int i = 0; i < list2.size(); i++) {
    String temp = list2.get(i);
    System.out.println(temp);
  }
// 기존 값은 String 타입이 아닌 것이 있어 ClassCastException이 발생한다.

타입 파라미터 생략과 <?>

타입 파라미터 생략

만약 레퍼런스 변수 옆에 타입명에 생략하면 거기에 들어가는 인스턴스의 타입 파라미터와 상관없이 어떤 검사도 하지 않고 무조건 Object의 형태로 객체를 다룬다. 다만 이렇게 사용하면 해당 객체가 raw type이라는 경고가 뜬다.

ArrayList list1; 
list1 = new ArrayList();
list1 = new ArrayList<>();
list1 = new ArrayList<Object>();
list1 = new ArrayList<String>();
list1 = new ArrayList<Member>();

list.add(new Object());
list.add(new A());
list.add(new B1());
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));

// 모두 ok 되고, Object의 형태로 들어가고, 값을 꺼낼 때도 Object로 나온다.

<?>

이 경고를 공식으로 무시하기 위해서는 <?> 를 추가한다. 이것도 마찬가지로 어떤 타입 파라미터를 갖는 인스턴스의 주소도 할당받을 수 있다. 그러나  해당 객체로 타입 검사를 해야하는 메서드를 호출하면 컴파일 오류가 발생한다. 즉, 해당 레퍼런스 변수에 들어가는 인스턴스가 가진 타입 파라미터에 따라 결과가 달라지는 경우에는, 이를 무시하지 않고 에러를 띄운다는 것이다.

ArrayList<?> list2; 
list2 = new ArrayList(); 
list2 = new ArrayList<>();
list2 = new ArrayList<Object>();
list2 = new ArrayList<String>();
list2 = new ArrayList<Member>();

list2.add(new Object()):
list2.add(new A());
list2.add(new B1());
list2.add(new C());
// 모두 컴파일 에러
// 컴파일러는 파라미터로 받은 ArrayList가 어떤 타입의 값을 다루는 지 알수 없다

Object obj1 = list.get(0);
// 이것은 어떤것을 리턴하든 오류가 나지 않기 때문에
// ok!

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));
// println()의 파라미터 타입이 Object이기 때문에 다음 코드는 오류가 아니다.

 제네릭과 파라미터

지금까지 정의된 인스턴스와 레퍼런스의 타입 파라미터들의 관계는 객체가 메서드의 파라미터로 선언될 때도 동일하게 적용된다.

static void m1(ArrayList<Object> list) {
    list.add(new Object());
    list.add(new A());
    list.add(new B1());
    list.add(new B2());
    list.add(new C());
  }
  
  public static void main(String[] args) {
  m1(new ArrayList<Object>());
//m1(new ArrayList<A>());  // 컴파일 오류!
//m1(new ArrayList<B1>()); // 컴파일 오류!
//m1(new ArrayList<B2>()); // 컴파일 오류!
//m1(new ArrayList<C>());
}
  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));
  }
  
  public static void main(String[] args) {

    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());
     */
  }

  public static void main(String[] args) {
    m1(my1); // OK
    m1(new ArrayList<A>());  // OK
    m1(new ArrayList<B1>()); // OK
    m1(new ArrayList<B2>()); // OK
    m1(new ArrayList<C>()); // OK
  }

<? extends __ >

원래는 파라미터는 레퍼런스 변수와 인스턴스의 타입 파라미터가 동일해야만 하며(생략된 경우, 혹은 미지정, <?> 제외) 다형성이 적용되지 않지만 <? extends __ >를 사용하면 지정된 객체 하위의 타입까지 타입 파라미터로 지정될 수 있다.

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) {
  //list.add(new B1()); // 컴파일 오류!

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

그러나 여전히 어떤 파라미터 타입이 지정될지 결정되지 않았기 때문에 검사 결과가 달라질 수 있는 코드에서는 컴파일 오류가 발생한다.

위의 코드를 살펴보면 <? extends B1> 이므로 B1, C가 타입 파라미터로 지정될 수 있다. 따라서 get()의 결과의 타입이 B1일 수도, 혹은 C일 수도 있다.

  • B1 obj2 = list.get(0); 는 어떤 경우에도 오류가 없다.
  • C obj3 = list.get(0); 는 get()이 B1이면 오류가 발생한다.

제네릭 객체의 메서드 사용

타입 파라미터를 지정하면 해당 객체의 메서드들의 파라미터리턴 타입은 지정된 타입 파라미터로 설정된다.

ArrayList<E>로 예를 들 수 있다.

  • add 메서드의 파라미터 타입
  • get 메서드의 리턴 타입
  ArrayList<B1> list1 = new ArrayList<>();
    
//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());

제네릭 명시의 필요성

제네릭 정보가 필요한 클래스를 사용할 때는 Object 타입으로 지정할 지라도 클래스 이름을 명시하는 것이 좋다. 어떤 타입의 타입을 사용할 것인지 다른 개발자에게 명확하게 알려주는 효과가 있기 때문이다. 또한 값을 꺼낼 때 형변환할 필요가 없다.

  HashMap<Object,Object> map2 = new HashMap<>();
  map2.put("aaa", "문자열");
  map2.put(new Integer(100), new Member("홍길동", 20));

  HashMap<String, Member> map3 = new HashMap<>();
//map3.put("aaa", "문자열");
//map3.pit(new Integer(100), new Member("홍길동", 20));
  map3.put("aaa", new Member("홍길동", 20));

  Member m = mp3.get("aaa");

LinkedList

제네릭 적용 전

  • 원하는 객체가 아닌 다른 타입의 객체를 저장하는 것을 막을 수 없다.
  • 값을 꺼내 사용할 때마다 형변환해줘야 한다.
LinkedList list = new LinkedList();
list.add(new Member("홍길동", 20));
list.add(new Member("임꺽정", 30));
list.add(new Member("유관순", 16));
    
list.add(new String("문자열!!!"));
    
for (int i = 0; i < list.size(); i++) {
  Member member = (Member) list.get(i);
  System.out.printf("%s(%d)\n", member.name, member.age);
}

제네릭 적용 후

  • 지정된 타입 이외의 객체일 경우 컴파일 오류가 발생하기 때문에 유효하지 않은 값을 막을 수 있다.
  • 값을 꺼낼 때 굳이 형변환을 할 필요가 없다.
LinkedList2<Member> list = new LinkedList2<>();
list.add(new Member("홍길동", 20));
list.add(new Member("임꺽정", 30));
list.add(new Member("유관순", 16));

//list.add(new String("문자열!!!")); // 컴파일 오류!
    
for (int i = 0; i < list.size(); i++) {
  Member member = list.get(i);
  System.out.printf("%s(%d)\n", member.name, member.age);
}

E[]를 사용하는 ArrayList

저번에 구현한 ArrayList는 Object[]를 생성하여 사용하는 대신 add() 의 파라미터나 get()의 리턴 객체의 타입을 E로 지정했다. 따라서 get 메서드에서는 뽑아낸 항목을 형변환을 한 후 리턴했다. 그러나 E[]를 안에서 생성하여 사용할 수 있는 방법도 있다. Array.newInstance 메서드를 사용하면 원하는 클래스 타입의 배열을 생성할 수 있다. 그런데 이 메서드의 파라미터로 원하는 클래스 타입을 필요로 하기 때문에 ArrayList 생성자의 파라미터로 클래스 타입을 받도록 하고, 이를 newInstance 메서드의 파라미터로 넘겨준다. 따라서 타입 파라미터와 생성자의 파라미터는 같아야한다. 혹은 생성자의 파라미터가 타입 파라미터보다 상위 클래스여도 되지만 큰 의미는 없을 것이다.

package com.eomcs.generic.ex03;

import java.lang.reflect.Array;

public class Exam0110 {

  static class ArrayList<T> {
    T[] arr;
    int index = 0;

    @SuppressWarnings("unchecked")
    public ArrayList(Class<?> clazz) {
      //this.arr = new T[10]; // 컴파일 오류!

      // 다음과 같이 Array.newInstance()로 배열을 생성해야 한다.
      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);
    obj.add(new Member());
    obj.add(new Student());
    obj.add(new Teacher());
    obj.add(new Manager());
    obj.add(new Administrator());

    System.out.println(obj.get(0));
    System.out.println(obj.get(1));
    System.out.println(obj.get(2));
    System.out.println(obj.get(3));

  }

}

파라미터 타입에 Super 클래스를 지정한 후

<? extends __> 를 이용하면 생성하는 인스턴스의 타입 파라미터를 특정 클래스 밑으로 제한할 수 있을 것이다. 예를 들어 

 

              Member

         /          |           \

 Teacher  Student Manager

                                  |

                         Administrator

 

다음과 같은 상속 관계를 가진 클래스가 있다고 할 때, ArrayList의 선언부에서 <T extends Manager>를 추가하면 타입 파라미터는 Manager 클래스와 그 하위 클래스인 Administrator로 제한된다.

package com.eomcs.generic.ex03;

import java.lang.reflect.Array;

public class Exam0120 {

  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> obj1 = new ArrayList<>(Manager.class); // OK!
    ArrayList<Administrator> obj2 = new ArrayList<>(Administrator.class); // OK!

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

    System.out.println(obj1.get(0));
    System.out.println(obj1.get(1));
  }
}