어제 배웠던 예제는 단순히 클래스를 여러개로 나눠서 DTO와, DAO를 구분하고 getter, setter 함수를 활용하여 캡슐화를 익히는 과제였습니다. 오늘은 더 나아가 상속의 개념을 접목시켜 예제를 풀어보고자 합니다.
무엇을 위해서 작동하는 코드를 만들고 싶은지를 일단 잘 정리해야 합니다. 우리는 이 코드를 통해서 가상의 야구팀을 만들고, 그 안에 선수들의 기본적인 정보를 입력, 삭제, 수정, 그리고 출력하여 확인하고 싶습니다. 그렇다면 여기서 DTO는 기본적인 정보들을 담고 있는 부분이 될 것이고 각각의 함수는 DAO에 담기게 됩니다. 마지막으로 메인 함수에서는 잘 만들어놓은것을 하나하나씩 쏙쏙 뽑아서 최종적으로 실행을 하게 될 것입니다.
먼저, 앞전과 같이 크게 세 부분으로 나눠서 코드를 짤 수 있겠습니다. 제일 기본적인 틀이 되는 main클래스부터 만들겠습니다.
package main;
import java.util.Scanner;
import dao.MemberManage;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
MemberManage mm = new MemberManage();
while(true) {
System.out.println("어떤 업무를 원하십니까?");
System.out.println("1. 선수 추가");
System.out.println("2. 선수 삭제"); //이름 스캐너
System.out.println("3. 선수 검색"); //이름 스캐너
System.out.println("4. 선수 수정"); // 승, 패, 방어율
System.out.println("5. 전체 출력");
System.out.print("원하는 업무의 번호를 입력하세요");
int key = sc.nextInt();
switch(key){
case 1:
System.out.println("1. 선수 추가");
mm.insert();
break;
case 2 :
System.out.println("2. 선수 삭제");
mm.delete();
break;
case 3 :
System.out.println("3. 선수 검색");
mm.select();
break;
case 4 :
System.out.println("4. 선수 수정");
mm.update();
break;
case 5 :
System.out.println("모든 데이터를 불러옵니다.");
mm.allPrint();
break;
default : System.out.println("올바르지 않은 입력값입니다. 다시 입력해주세요");
}
}
}
}
main함수에 정말 필요한 것들만 간결하게 넣기 위해 이 모든 세분화를 하는 것이므로 Main클래스는 일목요연하고 단정하게 보입니다. 먼저 사용자에게 어떤 업무를 하고 싶은건지 메뉴를 선택할 수 있게끔 보여주는 부분을 출력합니다. 만약 사용자가 1. 선수 추가를 선택했다고 해서 추가만 하고 끝날 것이 아니기 때문에 while(true)를 통해서 계속해서 반복적으로 작동하게 만들어줘야 합니다. 스캐너로 받아온 값에 따라서 스위치 케이스 안에 각각 해당되는 기능이 실행되게끔 합니다. 예를 들어 mm.insert는 MemberManage(DAO)라는 인스턴스 안의 함수입니다. 앞서 Member Manage mm = new MemberManage();를 통해서 새로운 인스턴스를 생성하였는데요, 마지막에 만약 사용자가 1~5 사이의 숫자가 아닌 다른 숫자를 넣거나 이상한 값을 넣었을 경우에는 default가 실행되도록 합니다.
이번에는 실제 정보를 담고 있는 DTO를 먼저 다루겠습니다. pitcher(투수)와 batter(타자)는 공통적으로 human(인간)이라는 공통점을 지니고 있습니다. 그러므로, 두 클래스의 부모 클래스가 human클래스이고 human클래스의 자식 클래스는 투수와 타자가 되는 것입니다. 자식 클래스는 부모 클래스의 속성을 상속받습니다. 그러므로 자식 클래스 안에 전부 다 구구절절 쓰지 않아도 간단하게 뽑아서 올 수 있는 것입니다.
package dto;
public class Human {
private int number;
private String name;
private int age;
private int height;
public Human(){
}
public Human(int number, String name, int age, int height) {
this.number = number;
this.name = name;
this.age = age;
this.height = height;
// TODO Auto-generated constructor stub
}
@Override
public String toString() {
return " number=" + number + ", name=" + name +
", age=" + age + ", height=" + height + " ";
}
dto패키지 안에 먼저 부모 클래스인 Human클래스를 만들어줍니다. 인간에게 여러가지 속성이 있겠지만 이번 프로그램의 취지에 맞게 일단은, 선수의 번호, 이름, 나이, 키를 저장해주려고 합니다. 여기서 정보를 은닉하기 위해 private으로 한것까지는 이해가 되는데, 왜 굳이 { } 안에 아무것도 없는 디폴트 생성자를 만들어준것인지 의문이 들 수 있겠습니다. 정작 사용하는 것은 네 가지 매개변수가 담겨있는 두번째 것 인것 같지 않나요? 저는 그랬습니다. 그런데 여기서 디폴트 생성자 public Human(){ }을 꼭 넣어줘야 하는 이유는 뭐 쉽게 말하자면~ 원래 그런거야~ 그냥 그러려니 해 할수도 있겠지만. 사실 디폴트 생성자는 우리가 생성자를 아무것도 넣지 않았을 때 매개변수도 없고, 구현 코드도 없는 말 그대로 컴퓨터가 만들어서 던져주는 그런 것입니다. 만약에 개발자가 단 하나의 생성자라도 직접 만들어서 추가한다면 디폴트 생성자는 만들어지지 않습니다. 개발자가 하지 않아서! 컴퓨터가 하는 일이라..생각해보면 중요한 일이기 때문에 컴퓨터가 굳이 만들어서 넣어주는 것이 아닐까요? 만약 정말 디폴트 생성자를 왜 넣어줘야 하는지도 모르겠고 나는 체제에 반항하겠다 하는 심정으로 매개변수를 가진 생성자만 만들어줬다고 생각해봅시다. 그런데, 자식 생각해서라도 열심히 벌어야 한다는 말처럼, 자식 클래스가 생긴다면 어떨까요? 자식 클래스가 부모 클래스에게 super();를 요구해버린다면? 자식 클래스에서 매개변수값이 없는 생성자를 만들어야 하는데 부모클래스가 갖고 있지 않기 때문에 만들수가 없다면?? 그런 겨우에는 줄 수 있는 게 하나도 없어 난감해지겠죠. 그러므로 정리하자면, 매개변수를 가진 생성자가 있는데 굳이 왜 만들어야 하냐는 질문도 논쟁의 요소는 될 수 있겠으나, 만들어놓지 않았을 때보다 만들어놨을 때의 장점이 더 크기 때문에 만드는 것이라고 생각하면 좋겠습니다.
일단 우리는 값을 받아와야 하기 때문에 int number, String name, int age, int height를 매개변수로 하는 함수를 만들어주고 그 안에 this.를 써서 이 객체의 number는 매개변수로 받아온 값이다라고 정해줬습니다. toString()전에 getter/setter가 나오는데 이 부분은 너무 길고, 이클립스가 알아서 만들어주기 때문에 생략하였습니다. 눈에 띄는 것은 Object클래스의 toString()함수입니다. 우리가 한 가지의 자료형으로 통일해야 하는 배열의 단점을 벗어나기 위해서 객체의 인스턴스를 만들어서 그 안에 값을 저장해뒀습니다. 따라서 Int형인 number와 String형인 name 가 자료형이 다름에도 불구하고 같이 묶일 수 있는 것이죠.
그렇다면, 배열의 경우 포문을 돌리거나 향상된 for문(ex.for(String lang : strArray){System.out.println(lang);}처럼 만들어서 뽑아낼 수가 있겠는데 객체는 이러한 방법으로 우리가 원하는 정보를 가시적으로 보여주지 않습니다. 만약 그냥 단순히 새로 만든 인스턴스인 mm을 System.out.println(mm.toString());을 통해서 뽑아낸다고 생각해봅시다. 이클립스에 돌려보니 dao.MemberManage@7d4991ad라는 이상한 16진수의 해시코드가 나오네요. 저걸 봐서는 우리는 도통 아무것도 알 수가 없겠습니다. 우리가 이해할 수 있는 문자열 형태로 객체의 정보를 받고 싶기 때문에 따라서 오버라이드 하겠습니다.
public String toString(){
return 어쩌구저쩌구;
}
이것은 return값을 이렇게 만들어라 특정해서 String형의 자료형으로 반환하는 toString()함수입니다. 기존의 기능에서 벗어나 오버라이딩 했기 때문에 위에 @Override가 적혀 있습니다. 그렇지 않으면 원래의 기능이라고 헷갈릴 수 있기 때문입니다. @Override라는 애노테이션(주석)이 붙어 있을 때에는 메서드가 재정의된 메서드임을 컴파일러에게 알려주고, 만약에 메서드의 선언부가 다르다면 컴파일 오류를 발생시켜 개발자가 실수하지 않도록 합니다. @Override의 경우에는 재정의된 메서드라는 정보를 제공합니다.
package dto;
public class Pitcher extends Human {
private int win;
private int lose;
private double defense;
public Pitcher() {
}
public Pitcher(int number, String name, int age, int height, int win, int lose, double defense) {
super(number, name, age, height);
this.win = win;
this.lose = lose;
this.defense = defense;
}
@Override
public String toString() {
return "Pitcher [ " + super.toString() +", win= " + win +
", lose=" + lose + ", defense=" + defense + "]";
}
Pitcher(투수) 클래스입니다. Human 클래스에서 상속받기 때문에 Pitcher클래스 옆에 extends Human이 먼저 보입니다.
안의 값들은 다 private으로 지정해주구요~ 여기서 매개변수를 가진 Pitcher함수에는 아까 Human에서는 볼 수 없었던 것이 있습니다. 바로 super(number, name, age, height)입니다. super();는 자식클래스가 부모클래스에 접근할 때 사용하는 것입니다. 상속을 받기 때문에 자식 클래스는 부모클래스의 주소를 알고 있는데, 그 주소를 가지고 있는게 바로 super입니다. 또한 자식 클래스의 생성자를 호출할때도 사용합니다. 여기서 단순히 super();를 호출하게 되면 우리가 원하는 값을 들고 있는 생성자를 불러올 수 없기 때문에 매개변수가 들어있는 함수라면 똑같이 super(number, name, age, height)처럼 매개변수가 있는 생성자를 불러와줘~ 해야 합니다. 그 후에 마찬가지로 마지막에 우리가 읽을 수 있는 형태로 표현하기 위해서는 오버라이딩을 하는데, super(number, name, age, height)를 요소별로 다 적으려면 너무 길기 때문에 중간에 부모클래스에서 받아오는 것은 super.toString()으로 처리해줍니다.
package dto;
public class Batter extends Human {
private int tasu;
private int antasu;
private double avg;
public Batter() {
}
public Batter(int number, String name, int age, int height,
int tasu, int antasu, double avg) {
super(number, name, age, height);
this.tasu = tasu;
this.antasu = antasu;
this.avg = avg;
}
@Override
public String toString() {
return "Batter [" + super.toString() + "tasu=" + tasu +
", antasu=" + antasu + ", avg=" + avg + "]";
}
Batter(타자)클래스 역시 투수클래스와 크게 다르지 않고 마찬가지입니다.
package dao;
import java.util.Scanner;
import dto.Batter;
import dto.Human;
import dto.Pitcher;
public class MemberManage {
Scanner sc;
private Human stArray[] =null;
private int count;
private int memberNum;
public MemberManage() {
sc = new Scanner(System.in);
stArray = new Human[20];
count = 0;
memberNum = 1001;
}
이제부터는 실제로 데이터를 가지고 이렇게 저렇게 요리를 해보겠습니다. 먼저 MemberManage 클래스를 만들어서 이 곳에 멤버변수로 Human 클래스의 stArray[] 배열을 선언해줍니다. 이 배열 안에는 모든 선수들의 기본 정보가 들어갈 것입니다. int형 count는 한 명의 선수가 입력될때마다 증가하게끔 하였습니다. 즉 첫번째 선수가 등록되면 stArray[count]이므로 stArray[0], 0번째 요소가 되고 두 번째 선수는 stArray[1]이 됩니다. 이어서 우리가 선수의 번호를 하나하나 등록해주지 않고 입력된 순서에 맞게 자동적으로 입력되게끔 하기 위해 memberNum을 만들어서 입력해줬습니다. 투수와 타자를 구분하기 위해서 투수는 1001부터 시작하고, 타자는 2001부터 시작하게끔 하여 시작하는 숫자로 구분합니다.
//TODO 1. 선수 정보 입력
public void insert() {
System.out.println("지금부터 선수 정보 입력을 시작합니다.");
System.out.println("포지션이 pitcher[0]입니까? batter[1]입니까?");
int ps = sc.nextInt();
System.out.print("선수명>>");
String name = sc.next();
//선수번호 1000 -> 투수, 2000 -> 타자
System.out.print("선수 나이>>");
int age = sc.nextInt();
System.out.print("선수 신장>>");
int height = sc.nextInt();
if(ps==0) {
System.out.println("지금부터 pitcher 정보 입력을 시작합니다.");
System.out.print("승>>");
int win = sc.nextInt();
System.out.print("패>>");
int lose = sc.nextInt();
System.out.print("방어율>>");
double defense = sc.nextDouble();
stArray[count] = new Pitcher(memberNum, name,age,height,win,lose,defense);
System.out.println(stArray[count]);
}else if(ps==1) {
System.out.println("지금부터 batter 정보 입력을 시작합니다.");
System.out.print("타수>>");
int tasu = sc.nextInt();
System.out.print("안타수>>");
int antasu = sc.nextInt();
System.out.print("타율>>");
double avg = sc.nextDouble();
stArray[count] = new Batter(memberNum+1000, name,age,height,tasu, antasu, avg);
System.out.println(stArray[count]);
}
count ++;
memberNum++;
}
제일 첫 번째로 해야 할 일은 선수의 정보를 입력받는 것입니다. 일단 기본적으로 선수의 이름, 나이, 키를 입력받아서 stArray[count]에 저장하려고 하는데 그러나 여기서 한 가지 문제에 봉착하게 되는데, 바로 투수에게 원하는 정보와 타자에게 원하는 정보가 다르다는 것입니다. 그래서 우리는 입력을 아예 시작하기 전에 선수가 투수인지 타자인지를 사용자에게 키보드로 입력해달라고 요청합니다. 사용자의 입력에 따라서 ps가 0이면 투수이고, 1이면 타자가 됩니다. 이름과 나이, 키는 포지션 여부와 상관없이 진행이 되며 만약 0을 입력했을 경우에는 투수에게 필요한 승, 패, 방어율을 입력받고 총 6가지를 다 입력받았을 때 비로소 stArray[count]에 저장이 됩니다. 앞서 memberNum 같은 경우에는 자동적으로 입력받도록 했기 때문에 패스하고, 뒤의 요소들은 스캐너로 입력받은 값이 저장되게 됩니다. 확인을 위해서 println함수를 넣어 체크합니다. 한 명의 선수 등록이 다 끝나면 count와 memberNum은 당연히 하나씩 증가하게 됩니다.
//TODO search 함수 추가
public int search(String name) {
int index = -1;
for(int i = 0; i<stArray.length;i++) {
if(stArray[i]!=null&&!stArray[i].getName().equals("")) {
if(name.equals(stArray[i].getName())) {
index = i;
break;
}
}
}
return index;
}
다음으로는 선수의 정보를 삭제하고, 검색하고, 수정하고 싶습니다. 그러나 그 전에 해둬야 할 일이 필요할 때 써먹을 수 있도록 search함수를 미리 만드는 것입니다. search함수는 매개변수로 이름을 받습니다. 만약 사용자가 홍길동 선수에 대해 작업을 실행하고 싶다면 "홍길동" 을 검색하게끔 합니다. 초기화된 index는 -1입니다. 그 이유는 명단에 음수의 인덱스를 가진 선수는 존재하지 않을 것이고, 즉 명단에 존재하지 않는다면 그 명단 내에서도 똑같은 이름을 가진 선수가 없다는 뜻이 되기 때문입니다. 배열 내부 선수 전체를 대상으로 검색할 것이기 때문에 for문을 돌려줍니다. (만약 배열요소의 값에 null값이 존재하지 않고, 이름란에 공백이 없다면)을 기본적인 조건으로 먼저 걸어줍니다. 그 다음에 사용자가 입력한 이름의 값이 명단에서 뽑아온 값이 일치하다면 인덱스를 -1에서 그 선수의 순서로 바꾸고 return값으로 받는 것입니다. 여기서 받은 return값은 삭제, 검색, 수정에 사용됩니다.
//TODO 2. 선수 정보 삭제
public void delete() {
System.out.print("삭제할 선수 이름 : " );
String name = sc.next();
int index = search(name);
if(index == -1) {
System.out.println("선수 데이터를 찾지 못했습니다.");
return;
}else {
System.out.println(stArray[index].toString());
}
stArray[index].setNumber(0);
stArray[index].setName("");
stArray[index].setAge(0);
stArray[index].setHeight(0);
System.out.println("선수 데이터를 삭제했습니다.");
}
입력했던 선수를 삭제하는 과정입니다. 삭제할 선수의 입력을 스캐너로 입력받아서 search함수를 통해 인덱스를 찾아냅니다. 물론 index가 -1이 나올 경우는 명단에 아예 존재하지 않는 쌩뚱맞은 사람의 이름을 검색했을 경우가 되겠습니다. 일치하는 선수를 찾게되면 그 선수의 기존 정보를 출력하게끔 하고 그 후에 모든 값을 없애버립니다. int형의 number, age, height 같은 경우에는 0으로, String형의 name은 공백으로 만들어줬습니다. 모든 작업이 끝난 후 선수 데이터를 삭제했습니다 문구를 띄워 작업이 완료됐음을 알립니다.
//TODO 3. 선수 정보 검색
public void select() {
System.out.println("검색할 선수의 이름을 입력하세요");
String name = sc.next();
int index = search(name);
if(index == -1) {
System.out.println("존재하지 않는 선수입니다. 다시 검색하세요");
return;
}else {
System.out.println(stArray[index].toString());
}
}
검색은 삭제의 과정에서 정보값을 지우는 작업만 빠진 것이라고 생각하시면 됩니다.
//TODO 4. 선수 정보 수정
public void update() {
System.out.print("수정할 선수 이름 : " );
String name = sc.next();
int index = search(name);
if(index != -1) {
System.out.println("지금부터 " + name + "의 정보를 수정하겠습니다.");
}else {System.out.println("해당되는 선수가 없습니다.");
}
if(stArray[index] instanceof Pitcher) {
Pitcher p = (Pitcher)stArray[index];
System.out.print("승 : ");
int win = sc.nextInt();
System.out.print("패 : ");
int lose = sc.nextInt();
System.out.print("방어율 : ");
double defense = sc.nextDouble();
p.setWin(win);
p.setLose(lose);
p.setDefense(defense);
System.out.println("Pitcher 데이터를 수정하였습니다.");
}
else if(stArray[index]instanceof Batter) {
Batter b = (Batter)stArray[index];
System.out.print("타수 : ");
int tasu = sc.nextInt();
System.out.print("패 : ");
int antasu = sc.nextInt();
System.out.print("방어율 : ");
double avg = sc.nextDouble();
b.setTasu(tasu);
b.setAntasu(antasu);
b.setAvg(avg);
System.out.println("Batter 데이터를 수정하였습니다.");
}
}
여기가 조금 까다로운 수정 부분입니다. 일단 명단 내에 존재하는 선수들을 대상으로 하는것임을 확실히 해야하기 때문에 index가 -1이 아닌 경우에만 한정짓습니다. 여기서 상속의 instanceof가 등장합니다. 만약 모든 선수 가운데 투수인 홍길동 선수의 승,패,방어율을 수정한다고 가정합시다. 그렇다면 그 홍길동 선수가 가지고 있는 정보값이 승, 패, 방어율이라는 것을 알기위해 먼저 투수인지 타자인지를 확인해야 할 것입니다. 앞서 우리는 상속의 개념을 사용해서 human의 정보값을 전달받았는데요, 그냥 갖고만 있을 때는 별 문제가 되지 않는 것이 그 자료를 써먹으려면 약간의 수고가 필요합니다.
예를 들어서 앞서 우리가 정보 입력의 과정을 단순하게 하기 위해서
stArray[count] = new Pitcher(memberNum, name,age,height,win,lose,defense);
이런식으로 했던 것을 기억할것입니다. stArray[count]는 Human클래스에 속하는 것인데 Pitcher클래스의 새로운 인스턴스가 되었고, 아무런 문제도 없이 컴파일이 잘 됐습니다. 이는 Pitcher클래스와 Batter클래스가 모두 Human클래스에서 상속받은 클래스이기 때문입니다. 그렇게 되면 이 배열 에는 Pitcher와 Batter를 모두 사용할 수 있을 뿐만 아니라 자식 클래스인 Pitcher와 Batter클래스의 인스턴스가 추가될때에도 모두 묵시적으로 부모클래스인 Human형으로 형변환이 이뤄집니다. 부모이기 때문에 자연스럽게 이뤄졌던 형변환이 다시 원래의 형변환으로 바뀌는 것이 다운 캐스팅입니다. 그리고 그 다운캐스팅을 위해 우리는 그럼 네가 원래 무슨 자료형이였지? 확인하는 구간이 필요한데 이때 instanceof를 씁니다.
if(stArray[index] instanceof Pitcher) {
Pitcher p = (Pitcher)stArray[index];
이것을 보면 Pitcher형으로 생성된 인스턴스가 맞다면 반환 값이 true가 나올 것이고 그 경우에 다운캐스팅이 이뤄지게 됩니다. human형으로 머물러있던 stArray가 Pitcher형으로 바뀌게 되고 그제야 비로소 setWin, setLose, setDefense를 수정할 수 있는 권한을 가지게 되는 것입니다. Pitcher클래스 내에서 p 인스턴스를 생성해서 그것의 win, lose, defense를 setter메서드를 통해 수정할 수 있게 되는 것입니다. Batter클래스의 경우에도 기본 원리는 동일합니다.
//TODO 모든 선수 불러오기
public void allPrint() {
for(int i = 0; i<stArray.length;i++) {
if(stArray[i] != null&&!stArray[i].getName().equals("")) {
System.out.println(stArray[i].toString());
}
}
그동안 입력했던 선수들의 정보값이 잘 들어갔는지 확인하기 위해서 모든 선수를 출력해보고자 합니다. 이 경우에는 search함수에서 했던 것처럼 모든 배열의 요소들을 대상으로 null값이 아니고 공백이 없는 선수들만 출력하게 하는데요, 삭제 과정에서 했던 과정을 떠올리시면 삭제당한 선수들은 당연히 나오지 않겠지요~
'Java > 자바 문제 풀이' 카테고리의 다른 글
DAO, DTO를 활용해서 학생 성적 정보 입력하기(get,set) (0) | 2021.05.18 |
---|---|
배열과 논리연산자를 이용한 야구 게임 기초 (0) | 2021.05.10 |