본문 바로가기

국비 교육

2020.8.31일자 수업 : 첫 비대면 수업

일주일동안 학원이 폐쇄되더니 결국 비대면 수업이 시작됐다. 그것만으로도 같은 내용을 듣는데, 수업의 난이도가 훨씬 올라갔다. 제발 일주일만 하고, 다시 학원으로 돌아갈 수 있기를 바랄뿐이다.

 

 실습 - 캡슐화 

git/eomcs-java-project-2020/mini-pms-15

* 저번 수업에 한 것.

응집력을 높이기 위해 각 MemberHandler, ProjectHandler, BoardHandler, TaskHandler 클래스의 역할을 잘게 쪼개서 전문화시켰다.
즉 BoardHandler 클래스를 UI역할(BoardHandler)과 데이터를 처리하는 역할(BoardList)로 나눴다.

구체적인 구현 내용을 감추어 유지보수와 사용을 쉽게 해주는 문법이 캡슐화

 

이렇게 만들어진 것이

  • Board - 도메인 객체
  • BoardList - DAO(Data Access Object) 데이터 처리 역할을 수행하는 객체 
  • BoardHandler - UI 역할을 수행하는 객체

* 도메인 객체

= 객체(Value Object) = 모델 객체(Model Object) = DTO(Data Transfer Objext - 데이터를 실어나르는 객체) 

 

업무 분석 과정에서 도출한 핵심 개념을 표현하는 클래스 이다.

도메인 클래스는 업무에서 다뤄지는 정보필드로 선언하고 그 정보를 처리하는 행위메서드로 정의한다.

예) Board, Member, Projet, Task

 

* 서비스 객체

실무에서는 도메인 클래스를 좀 더 쉬운 방식으로 다루기 위해 정보와 행위를 분리한다.

업무 정보는 필드와 getter/setter 로 구성된 VO 클래스로 정의되고 데이터 타입으로서 역할을 한다.

업무 행위를 표현한 메서드는 Service 클래스로 정의한다.

Service 객체의 예) BoardVO/BoardService, MemberVO/MemberService 등

캡슐화

특정 역할을 수행하는 코드를 클래스라는 캡슐에 감추고 메서드라는 도구를 통해 해당 코드를 이용하게 하는 기법이다.

이를 통해 기능 구현에 대한 코드의 상세 내용을 감춤으로써 코드의 유지보수가 쉬워지고, 이용하기가 편해진다.

기존에 정의한 도메인 클래스에 접근 제어 문법 적용하여 인스턴스 필드의 값을 외부에서 직접 접근하지 못하게 막고

세터게터 메서드를 통해 값을 조회하고 변경하게 할 것이다. 

 

모든 필드를 다 막을 필요는 없지만, 일관성을 위해서 모든 필드를 막는 것이 좋다.

 

캡슐화의 핵심은 구성 요소에 대한 접근을 적절하게 통제하는 것이다. 필드는 내부에서만 접근하도록 제한하고, 필드를 다루는 메서드만 외부에서 호출할 수 있도록 공개한다. 메서드가 내부에서만 사용된다면 그것도 막는다.

 

* 팁

일단 필드와 메서드 상관없이 막고, 외부에서 필요한 경우 조금씩 공개해주는 것이 좋다.

 

객체지향은 원래 좀 과도하게 리팩토링을 하는 경우가 많다. 그러나 초보자인 우리는 일단 그런것을 신경쓸 때가 아니다. 과한 코딩에 대한 고민은 숙련된 개발자가 된 후에 해도 괜찮다.

 

훈련 목표 

  • 도메인 클래스의 인스턴스 필드를 private 접근제어자로 막는다.

  • 각 private으로 통제되는 인스턴스에 대한 세터와 게터 메서드를 만들어 외부에서 메서드를 통해 필드들에 접근할 수 있도록 한다.

1단계 : 각 도메인 클래스의 인스턴스 필드를 private 접근 제어자로 막고, 그에 대한 세터와 게터 메서드를 만든다.

package com.eomcs.pms.domain;

import java.sql.Date;

public class Board {
  private int no;
  private String title;
  private String content;
  private String writer;
  private Date registeredDate;
  private int viewCount;
  
  public int getNo() {
    return no;
  }
  public void setNo(int no) {
    this.no = no;
  }
  public String getTitle() {
    return title;
  }
  public void setTitle(String title) {
    this.title = title;
  }
  public String getContent() {
    return content;
  }
  public void setContent(String content) {
    this.content = content;
  }
  public String getWriter() {
    return writer;
  }
  public void setWriter(String writer) {
    this.writer = writer;
  }
  public Date getRegisteredDate() {
    return registeredDate;
  }
  public void setRegisteredDate(Date registeredDate) {
    this.registeredDate = registeredDate;
  }
  public int getViewCount() {
    return viewCount;
  }
  public void setViewCount(int viewCount) {
    this.viewCount = viewCount;
  }
}

2단계 : Handler 클래스에서 필드를 직접 사용하는 코드를 세터와 게터 메서드를 호출하는 코드로 수정한다.

package com.eomcs.pms.handler;

import java.sql.Date;
import com.eomcs.pms.domain.Board;
import com.eomcs.util.Prompt;

public class BoardHandler {
  
  BoardList boardList = new BoardList();

  public void add() {
    System.out.println("[게시물 등록]");

    Board board = new Board();
    board.setNo(Prompt.inputInt("번호? "));
    board.setTitle(Prompt.inputString("제목? "));
    board.setContent(Prompt.inputString("내용? "));
    board.setWriter(Prompt.inputString("작성자? "));
    board.setRegisteredDate(new Date(System.currentTimeMillis()));
    board.setViewCount(0);

    boardList.add(board);

    System.out.println("게시글을 등록하였습니다.");
  }

  public void list() {
    System.out.println("[게시물 목록]");
    Board[] boards = boardList.toArray();
    for (Board board : boards) {
      System.out.printf("%d, %s, %s, %s, %d\n",
          board.getNo(),
          board.getTitle(),
          board.getWriter(),
          board.getRegisteredDate(),
          board.getViewCount());
    }
  }
}

 실습 2 - 다형성과 형변환 

git/eomcs-java-project-2020/mini-pms-16

다형성

한 방식, 한 이름으로 다양한 타입의 데이터나 메서드를 다루는 기법

  • 다형적 변수 - 같은 이름의 변수를 사용하여 여러 타입의 데이터를 다룬다.
    => 한 개의 변수로 다양한 종류의 값을 다룰 수 있다.
  • 오버로딩 - 같은 이름의 메서드를 사용하여 전달되는 아규먼트에 따라 호출될 메서드를 달리한다.
    => 같은 기능을 하는 메서드에 대해 같은 이름을 사용하여 프로그래밍의 일관성을 유지할 수 있다.
  • 오버라이딩 - 부모 메서드와 같은 이름의 시그니처를 갖는 메서드를 정의하여 객체의 타입에 따라 호출될 메서드를 달리한다.
    => 상속 받은 메서드를 서브 클래스의 역할에 맞게 재정의할 수 있어 프로그래밍의 일관성을 제공한다.

훈련 목표

  • 다형적 변수를 이용하여 Board, Member, Project, Task 타입의 객체를 모두 다룰 수 있는 ArrayList 클래스를 정의한다.

  • Board, Member, Project, Task 타입에 따라 개별적으로 만든 XxxList 클래스를 ArrayList로 교체한다.

  • 원래 타입의 객체를 다룰 때는 형변환을 이용한다.

1단계 : ArrayList를 만들고 각 클래스 타입의 배열을 Object 배열로 바꾼다. 

package com.eomcs.pms.handler;

import java.util.Arrays;

public class ArrayList {
  
  private static final int DEFAULT_SIZE = 100;
  private Object[] list = new Object[DEFAULT_SIZE];
  private int size = 0;
  
  public ArrayList() {}
  
  public ArrayList(int length) {
    if (length > DEFAULT_SIZE) {
      list = new Object[length];
    }
  }
  
  public void add(Object obj) {
    if (size == list.length) {
      list = Arrays.copyOf(list, list.length + (list.length >> 1));
    }
    list[size++] = obj;
  }
  
  public Object get(int index) {
    return list[index];
  }
  
  public Object[] toArray() {
    Object[] newList = new Object[size];
    System.arraycopy(list, 0, newList, 0, size);
    return newList;
  }
  
  public int size() {
    return size;
  } 
}

2단계 : Handler 클래스들에서 BoardList, MemberList, ProjectList, MemberList, TaskList를 사용하던 코드를 모두 ArrayList를 사용하게끔 바꿔준다. 대신 ArrayList의 필드인 배열은 Member, Project..들의 배열이 아니라 Object배열이기 때문에 배열에서 각 항목을 꺼내면 각 객체에 맞게 명시적 형변환을 해줘야한다.

package com.eomcs.pms.handler;

import java.sql.Date;
import com.eomcs.pms.domain.Board;
import com.eomcs.util.Prompt;

public class BoardHandler {
  // ArrayList 객체 생성
  ArrayList boardList = new ArrayList();

  public void add() {
    System.out.println("[게시물 등록]");

    Board board = new Board();
    board.setNo(Prompt.inputInt("번호? "));
    board.setTitle(Prompt.inputString("제목? "));
    board.setContent(Prompt.inputString("내용? "));
    board.setWriter(Prompt.inputString("작성자? "));
    board.setRegisteredDate(new Date(System.currentTimeMillis()));
    board.setViewCount(0);

    boardList.add(board);

    System.out.println("게시글을 등록하였습니다.");
  }

  public void list() {
    System.out.println("[게시물 목록]");
    // toArray() 리턴값을 Object배열 주소 변수에 받는다.
    Object[] boards = boardList.toArray();
    for (Object obj : boards) {
    // 각 항목을 다시 객체에 맞게 형변환한다.
      Board board = (Board) obj;
      System.out.printf("%d, %s, %s, %s, %d\n",
          board.getNo(),
          board.getTitle(),
          board.getWriter(),
          board.getRegisteredDate(),
          board.getViewCount());
    }
  }
}

 실습 3 - 제네릭 이용 

git/eomcs-java-project-2020/mini-pms-17

 

ArrayList의 필드인 list를 Object 배열로 지정하게 되면  다음과 같이 생뚱맞은 데이터 타입의 객체를 집어넣는 것을 막지 못한다.

public void add() {
    System.out.println("[게시물 등록]");

    Board board = new Board();
    board.setNo(Prompt.inputInt("번호? "));
    board.setTitle(Prompt.inputString("제목? "));
    board.setContent(Prompt.inputString("내용? "));
    board.setWriter(Prompt.inputString("작성자? "));
    board.setRegisteredDate(new Date(System.currentTimeMillis()));
    board.setViewCount(0);

    boardList.add(new String("haha");

    System.out.println("게시글을 등록하였습니다.");
  }

또 한편으로는 값을 꺼낼때마다 항상 다음과 같은 형변환을 필요로 한다. Object => 원하는 데이터 타입

public void list() {
    System.out.println("[게시물 목록]");

    Object[] boards = boardList.toArray();
    
    for (Object obj : boards) {
      Board board = Object obj;
      System.out.printf("%d, %s, %s, %s, %d\n",
          board.getNo(),
          board.getTitle(),
          board.getWriter(),
          board.getRegisteredDate(),
          board.getViewCount());
    }

따라서 제네릭을 사용하여 이러한 단점을 해결한다.

 

제네릭(generic) 문법을 이용하면,

 

  • 같은 일을 하는 클래스 정의할 때 타입 별로 중복해서 정의할 필요가 없기 때문에 코드의 재사용성을 높인다.
  • 지정된 타입의 객체만 다루도록 제한할 수 있어 코드의 안정성을 높인다. 사용할 객체의 타입을 지정한 후 잘못된 타입의 객체를 사용할 때 컴파일 오류가 발생한다. 컴파일 할 때 타입 검사를 진행하기 때문에 빠른 시점에 타입 안정성을 어긴 오류를 찾아 낼 수 있다.

훈련 목표

ArrayList에 제네릭을 사용해서 명시적 형변환 과정을 생략하고 원하지 않는 타입의 객체를 배열에 넣지 못하도록 안정성을 높인다.

 

1단계 : ArrayList에 <E> 타입 파라미터를 추가하고 add()의 파라미터 타입get()의 리턴 타입E로 지정한다. 이 과정에서 Object 변수를 하위 클래스의 변수로 명시적 형변환하기 때문에 이에 대한 경고가 뜬다. 이 경고를 무시할 수 있도록 @SuppressWarnings("unchecked") 애노테이션을 붙인다.

toArray()는 세 가지로 늘린다. 

  • Object[] toArray() : 리스트를 Object[]에 복사하여 그대로 리턴
  • E[] toArray(E[] arr) : 리스트를 파라미터로 받은 E[] 타입 배열에 복사하여 그것을 리턴,
    사이즈보다 작은 리스트를 받았다면 새로 E[]를 생성하여 거기에 복사하고 리턴
  • E[] toArray(Class <? extends E> classType) : E[]의 클래스 타입을 받아 그것으로 newInstance메서드를 통해 E[]를 생성하여 거기에 리스트를 복사하고 리턴
package com.eomcs.util;

import java.lang.reflect.Array;
import java.util.Arrays;

public class ArrayList<E> {
  
  private static final int DEFAULT_SIZE = 100;
  private Object[] list = new Object[DEFAULT_SIZE];
  private int size = 0;
  
  public ArrayList() {}
  
  public ArrayList(int length) {
    if (length > DEFAULT_SIZE) {
      list = new Object[length];
    }
  }
  
  public void add(E obj) {
    if (size == list.length) {
      list = Arrays.copyOf(list, list.length + (list.length >> 1));
    }
    list[size++] = obj;
  }
  
  @SuppressWarnings("unchecked")
  public E get(int index) {
    return (E) list[index];
  }
  
  public Object[] toArray() {
    Object[] newList = new Object[size];
    System.arraycopy(list, 0, newList, 0, size);
    return newList;
  }
  
  @SuppressWarnings("unchecked")
  public E[] toArray(E[] arr) {
    if (arr.length < size()) {
      arr = (E[]) Array.newInstance(arr.getClass().getComponentType(), size());
      for (int i = 0; i < arr.length; i++) {
        arr[i] = (E) list[i];
      }
      return arr;
    } else {
      System.arraycopy(list, 0, arr, 0, size());
      return arr;
    }
  }
  
  @SuppressWarnings("unchecked")
  public E[] toArray(Class<? extends E[]> classType) {
    return Arrays.copyOf(list, size(), classType);
  }
  
  public int size() {
    return size;
  } 
}

2단계 : Handler클래스에 가서 필드인 리스트 클래스의 타입 파라미터를 각 도메인 객체로 지정한다. list()에서 toArray()를 호출하는 코드에 클래스 타입이나 배열 객체를 파라미터로 추가한다. 

package com.eomcs.pms.handler;

import java.sql.Date;
import com.eomcs.pms.domain.Board;
import com.eomcs.util.ArrayList;
import com.eomcs.util.Prompt;

public class BoardHandler {

  // BoardHandler가 사용할 BoardList 객체를 준비한다.
  ArrayList<Board> boardList = new ArrayList<>();

  public void add() {
    System.out.println("[게시물 등록]");

    Board board = new Board();
    board.setNo(Prompt.inputInt("번호? "));
    board.setTitle(Prompt.inputString("제목? "));
    board.setContent(Prompt.inputString("내용? "));
    board.setWriter(Prompt.inputString("작성자? "));
    board.setRegisteredDate(new Date(System.currentTimeMillis()));
    board.setViewCount(0);

    boardList.add(board);

    System.out.println("게시글을 등록하였습니다.");
  }

  public void list() {
    System.out.println("[게시물 목록]");
    
    // Board[] boards = boardList.toArray(new Board[] {});
    Board[] boards = boardList.toArray(Board[].class);
    for (Board board : boards) {
      System.out.printf("%d, %s, %s, %s, %d\n",
          board.getNo(),
          board.getTitle(),
          board.getWriter(),
          board.getRegisteredDate(),
          board.getViewCount());
    }
  }
}

* E[]를 생성하여 복사하는 세 가지 방법

  • E[]는 new 연산자를 통해 생성할 수 없는 대신, Array.newInstance() 메서드를 사용하여 E[]를 생성할 수 있다. 따라서 newInstance()로 E[]를 생성한 뒤 직접 for문을 통해 항목을 복사하거나, System.arraycopy() 메서드를 사용할 수 있다.
    • for문 사용
    • System.arraycopy() 메서드 사용
// newInstance() 메서드를 통해 E[] 생성

//1. for 문 사용
  public E[] toArray(Class<? extends E[]> classType) {
    // newInstance()의 리턴 타입은 Object이기 때문에 원하는 배열 타입으로 형변환해야한다.
    E[] newArr = (E[]) Array.newInstance(classType, this.size());
    for (int i = 0; i < this.size(); i++) {
      newArr[i] = this.get(i);
    }
    return newArr;
  }
//2. System.arraycopy() 메서드 사용  
  public E[] toArray(Class<? extends E[]> classType) {
    // newInstance()의 리턴 타입은 Object이기 때문에 원하는 배열 타입으로 형변환해야한다.
    E[] newArr = (E[]) Array.newInstance(classType, this.size());=
    System.arraycopy(elementData, 0, newArr, 0, this.size());
    return newArr;
  }

 

  • 혹은 E[]를 구현부 안에서 생성하고 생성한 배열에 원본을 복사해서 넣어주는 Arrays.copyOf() 메서드를 사용하면, 직접 E[]를 생성할 필요가 없다.
  public E[] toArray(Class<? extends E[]> classType) {
    return Arrays.copyOf(this.elementData, this.size, classType);
  }

* class 필드와 getClass() 메서드 차이

  • class 필드Class 라는 클래스만 갖고 호출가능한 스태틱 필드이다.
  • getClass()는 Class의 인스턴스를 갖고 호출해야하는 인스턴스 메서드이다.

다만 둘이 리턴하는 객체는 같은 주소의 객체이다.

String s = new String();
Class classInfo = String.class
System.out.println(classInfo);

Class classInfo2 = s.getClass();
System.out.println(classInfo2);
System.out.println(classInfo2 == calssInfo2); // true : 주소가 같기 때문이다.

 

* toArray()의 파라미터로 배열 객체를 주는 방법

  • Member[] members = memberList.toArray(new Member[memberList.size()]); - 사이즈에 딱 맞는 배열을 준다.
  • Member[] members = memberList.toArray(new Member[] {}); - 빈 배열을 주어 메서드 안에서 새로운 배열을 생성하게 한다.
    (장점 : 간단하다. 단점 : 가비지가 생긴다)

 실습 3 - CRUD 완성 

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

CRUD란?

CRUD는 데이터의 생성(Create), 조회(Read/Retrieve), 변경(Update), 삭제(Delete)을 가리키는 용어

 

훈련 목표

  • 기존의 ArrayList 에 값을 조회, 삽입, 변경, 삭제하는 기능을 추가한다.

  • 게시글의 상세 조회, 변경, 삭제 기능을 추가한다.

  • 회원 정보의 상세 조회, 변경, 삭제 기능을 추가한다.

  • 프로젝트 정보의 상세 조회, 변경, 삭제 기능을 추가한다.

  • 작업 정보의 상세 조회, 변경, 삭제 기능을 추가한다.

1단계 : 직접 구현한 MyArrayList가 이미 조회, 삽입, 변경, 삭제하는 기능이 있으므로 이에 제네릭 문법을 적용한 뒤 이것을 그대로 사용한다. 

package com.eomcs.util;

import java.lang.reflect.Array;
import java.util.Arrays;

// ArrayList 가 다룰 객체의 타입을 파라미터로 받을 수 있도록 '타입 파라미터'를 선언한다. 
public class ArrayList<E> {

  static final int DEFAULT_CAPACITY = 3;
  Object[] elementData;
  int size = 0;

  public ArrayList() {
    elementData = new Object[DEFAULT_CAPACITY];
  }

  public ArrayList(int initialCapacity) {
    if (initialCapacity <= DEFAULT_CAPACITY) {
      elementData = new Object[DEFAULT_CAPACITY];
    } else {
      elementData = new Object[initialCapacity];
    }
  }

  public boolean add(E e) {
    if (size == elementData.length) {
      grow();
    }
    elementData[size++] = e;
    return true;
  }

  private void grow() {
    int newCapacity = elementData.length + (elementData.length >> 1);
    elementData = Arrays.copyOf(elementData, newCapacity);
  }

  public void add(int index, E element) {
    if (size == elementData.length) {
      grow();
    }
    if (index < 0 || index > size) {
      throw new ArrayIndexOutOfBoundsException("인덱스가 유효하지 않습니다.");
    }
    for (int i = size; i > index ; i--) {
      elementData[i] = elementData[i - 1];
    }
    elementData[index] = element;
    size++;
  }

  @SuppressWarnings("unchecked")
  public E get(int index) {
    if (index < 0 || index >= size) {
      throw new ArrayIndexOutOfBoundsException("인덱스가 유효하지 않습니다.");
    }
    return (E) elementData[index];
  }

  @SuppressWarnings("unchecked")
  public E set(int index, E element) {
    if (index < 0 || index >= size) {
      throw new ArrayIndexOutOfBoundsException("인덱스가 유효하지 않습니다.");
    }
    Object old = elementData[index];
    elementData[index] = element;
    return (E) old;
  }

  @SuppressWarnings("unchecked")
  public E remove(int index) {
    Object old = elementData[index];

    System.arraycopy(
        elementData, // 복사 대상
        index + 1, // 복사할 항목의 시작 인덱스
        elementData, // 목적지
        index, // 복사 목적지 인덱스
        this.size - (index + 1) // 복사할 항목의 개수
        );

    size--;
    elementData[size] = null;
    return (E) old;
  }

  public int size() {
    return this.size;
  }

  public Object[] toArray() {
    Object[] arr = Arrays.copyOf(elementData, this.size);
    return arr;
  }

  @SuppressWarnings("unchecked")
  public E[] toArray(E[] arr) {
    if (arr.length < this.size) {
      // 파라미터로 받은 배열이 작을 때는 새 배열을 만들어 리턴.
      return (E[]) Arrays.copyOf(this.elementData, this.size, arr.getClass());
    }
    System.arraycopy(this.elementData, 0, arr, 0, this.size);
    return arr; // 넉넉할 때는 파라미터로 받은 배열을 그대로 리턴. 
  }
  
  public E[] toArray(Class<? extends E[]> classType) {
    return Arrays.copyOf(this.elementData, this.size, classType);
  }
}

2단계 : Handler 클래스들에 상세 정보를 조회하는 detail() 메서드를 구현한다. 이를 위해서 번호로 해당 멤버 객체를 찾는 findByName() 메서드도 함께 추가한다.

  public void detail() {
    System.out.println("[회원 조회]");
    int no = Prompt.inputInt("번호? ");
    Member member = findByNo(no);
    if (member == null) {
      System.out.println("해당 번호의 회원을 찾지 못했습니다.");
    } else {
      System.out.printf("이름: %s\n", member.getName());
      System.out.printf("이메일: %s\n", member.getEmail());
      System.out.printf("암호: %s\n", member.getPassword());
      System.out.printf("사진: %s\n", member.getPhoto());
      System.out.printf("전화: %s\n", member.getTel());
    }
  }
  
  private Member findByNo(int no) {
    for (int i = 0; i < memberList.size(); i++) {
      if (memberList.get(i).getNo() == no) {
        return memberList.get(i);
      }
    }
    return null;
  }