JAVA 웹개발 과정 국비 13일차 정리

13일차

※ 배우는 과정이므로 정확하지 않을 수 있습니다.
잘못된 점은 알려주시면 수정하도록 하겠습니다!


1. Method Overriding

부모 클래스에서 이미 정의된 메소드를 자식 클래스에서 다시 정의하는 것.
override - 무효로하다 (부모 클래스에 정의된 메소드를 무효로 하다)

// <부모 클래스>
// Grade class 
public double getBonus() {
	return this.point * 0.02; // Silver 클래스 메소드 그대로 적용
}

12일차 예제에서 getBonus 메소드를 Grade 클래스에 쓰고 넘어갔었다.
Silver 등급과 Gold 등급의 보너스 포인트 이율이 다른데, 어떻게 해야할까?

Grade 클래스에 있는 getBonus 메소드의 내용은 실버에 맞는 내용이므로, Grade 를 상속받는 Silver 클래스는 Grade 의 getBonus 메소드를 쓸 수 있으므로 그대로 넘어가도 무관하다.
그러나 Gold 등급의 보너스 포인트 이율은 3%로, Silver 등급과는 다르다.
여기서 Method Overriding 개념을 사용해야 한다.

// Gold class extends Grade
public double getBonus() {
	return this.getPoint() * 0.03;  // Gold 클래스에 맞는 이율로 바꿔준다
}

위와 같이 메소드 오버라이딩을 해주면, 기존 Grade 에 있던 getBonus 메소드는 사라지고, Gold 에 있는 getBonus 메소드를 사용할 수 있게 된다.
대신 point 변수가 private로 사용되었기때문에, getter를 만들어주어야 한다.

잘 생각해보면, getBonus 메소드의 내용이 중요한 것이 아니라, Silver와 Gold가 공통적으로 가지고 있다는 점이 중요한 것이라는 것을 알 수 있다.
현재 내용은 Silver에만 적용이 되는 것이기 때문이다.
그리고 이런 식으로 만들게 되면, 오버라이드를 안하더라도 오류가 나지 않아서 다른 등급의 클래스를 만들 때 오버라이드를 잊어버리면 Silver 등급의 보너스 이율과 같아져버리면서 치명적인 실수가 나오게 된다.

  1. 내용 자체에는 의미가 없을 때
  2. 하위 클래스에서 오버라이딩을 안하면 문제가 생길 때

따라서 위 두가지 상황이 존재한다면, 반드시 오버라이드를 해야하는 메소드로 만들어주자.
Grade 클래스의 getBonus 메소드를 바꿔보자.

1) 추상 메소드 만들기

① 내용을 빼버린다

내용은 중요하지 않으니 없애버리자.

public double getBonus() {
	// 내용 지우기
}

② 리턴 값 지우기

리턴 값이 없어졌기 때문에 메소드 자체에 에러가 난다.
리턴 값도 지워버린다.

③ 중괄호 지우기

그래도 에러.. 그러면 중괄호도 지워버리자.
그래도 여전히 에러가 난다.

④ 맨 앞에 abstract을 붙여주자

getBonus 메소드를 내용이 중요하지 않은, 오버라이드를 꼭 해야하는 애로 만들고 싶은데.. 컴퓨터가 내 생각을 읽을 수 있을리가 없다.
그래서 abstract을 붙여줘서 이 메소드는 추상적인 건데, 이런 메소드가 있다고 컴퓨터를 설득시켜야 한다.

abstract public double getBonus()

⑤ 확신의 abstract 클래스

설득을 했는데도 불구하고 컴퓨터는 믿지 못한다.
말은 그렇게 했는데 믿고 OK 했다가 호출해서 불러오면 어떡해? 하면서 아직 찜찜한 구석이 남아있는 것이다.
여기서 컴퓨터에게 확신을 줘야한다.
abstract을 클래스 앞에도 붙여줘서 new 안쓸게! 인스턴스 생성 안할게! 실체화 안할게! 하면서 클래스도 추상적으로 바꿔버리는 것이다.
이제 컴퓨터는 메소드를 꺼내쓸 수가 없어졌으니까 불안함이 완전히 사라진다.
그래서 에러는 더이상 생기지 않는다.

abstract public class Grade {
	abstract public double getBonus();
}

⑥ 추상 클래스..?

쓰려고 만든 클래스인데 abstract을 붙여서 못쓰게 만들어버리면 어떡해?
라고 생각할 수 있지만, 회원 관리 시스템에서 Silver와 Gold는 쓰지만, Grade를 인스턴스화 해서 쓰는 코드는 존재하지 않는다.
Grade 클래스는 원래 설계 목적으로 만든 클래스이기 때문이다.

⑦ 뜬금없이 Silver 클래스에 오류가..!

Grade 클래스는 이제 더이상 오류가 존재하지 않는다.
그러나 그 순간 바로 Silver 클래스가 오류가 생겼다.
Grade 에 있던 getBonus 메소드의 내용이 실버에는 맞았기때문에, 따로 오버라이딩을 하지 않고 건너 뛰었기 때문이다.
현재 getBonus 메소드는 반드시 오버라이드를 해야하는 메소드로 바뀌었기 때문에, Silver 클래스에서도 오버라이드를 해줘야한다.

//Silver class extends Grade
public double getBonus() {
	return this.getPoint() * 0.02;
}

2) Override VS Implement

  • Override
    상위 클래스로부터 물려받은 것(추상 메소드X)을 무효화시키고 내 걸로 만드는 것.
  • Implement
    상위 클래스로부터 물려받은 것(추상 메소드O)를 재정의(구현)하는 것.

대체적으로 오버라이드로 혼용하여 사용한다.

3) 추상화

이렇게 추상메소드, 추상클래스를 만드는 것을 추상화라고 하며,
추상화를 통해 코드를 강제시킬 수 있다는 장점이 있다.
왜 장점인지는 나중에 배운다~

4) Interface (추상 클래스)

지금은 이런게 있다~ 정도만 알고 넘어가자.

  • class 같은 개념 (설계도)
  • 모든 것이 다 추상체 (완전추상체)
  • 내용이 구현된 것들이 존재할 수 없다.
  • 메소드를 만들었을 때 눈에 보이지 않지만, 앞에 abstract이 생략되어 붙어있다.

2. 다형성

1) 다형성이란

다형성을 그냥 읽어보면 다양한 형질을 가진 형태라는 뜻이다. 자바 문법에서는 다른 타입의 값을 가질 수가 없는데 무슨 소리일까?
모두가 갖는 성질은 아니며, 다형성을 가지기 위한 조건이 필요하다.
다형성은 상속 관계에서만 발현이 된다.

클래스 간 상속관계에서만 발현되는 서로 다른 타입의 값을 가질 수 있는 성질.

2) 다형성의 규칙

상위 클래스 참조 변수는 자신을 상속 받는 하위 클래스 인스턴스의 주소를 저장할 수 있다. (하위 클래스 안에 상위 클래스의 내용이 있으므로 인식이 가능)

A, B 클래스가 있다고 하자.
그리고 B는 A를 상속받는 하위클래스이다.
아래 코드에서 1번은 오류가 나지 않지만, 2번은 오류가 난다.

// B extends A 
A a = new B();  // 1. 정상동작
B b = new A();  // 2. 오류

B는 A의 기능을 가지고 extends 했기 때문에, A를 포함하고 있다고 말할 수 있다.
그림으로 나타내보면 image 더 이해가 쉽다.
A형 변수 a는 자신에게 A형 인스턴스의 주소를 저장해야한다고 알고있다.
B는 내부에 A를 가지고 있다. 따라서 변수 a가 B의 인스턴스의 주소를 가리킨다고해도, 그 안에 A가 있기 때문에 문제되지 않는 것이다.
그러나 반대의 경우에는 변수 b가 A의 인스턴스의 주소를 가리킨다고 한다면, A 인스턴스에는 B가 없기 때문에 오류가 날 수 밖에 없다.

3) Up & Down Casting

여기서 한가지 의문점은, A a = new B(); 이 코드에서 a에 .을 찍으면 A로 동작할까? B로 동작할까?
변수 자료형 자체는 A형인데, 그 안에 들어있는 인스턴스는 B형이다.
결론은 a에 .을 찍으면 A형 인스턴스로 동작한다.

여기서 아쉬운 점은 상위 클래스 참조 변수에 다른 자료형을 담는 것 까진 좋았는데, 결국 꺼내서 쓸 수 있는 자료형은 참조 변수의 자료형 그대로라는 것이다.
그럼.. 쓰는 의미가 없는 거 아닐까?
사용할 수 있는 방법이 따로 있다!
바로 강제 형변환을 사용하는 것이다.

A a = new B();
a.funcA();  // A 클래스 메소드 사용 가능 
((B)a).funcB();  // B 클래스 메소드 사용 가능
  • Up casting (= 기본자료형의 Promotion)
    • 하위 클래스의 자료형이 상위 클래스 자료형으로 바뀌는 경우
    • A a = new B();
  • Down casting (= 기본자료형의 Casting)
    • 상위 클래스의 자료형이 하위 클래스 자료형으로 바뀌는 경우
    • ((B)a).funcB();

4) Down Casting을 안하고 쓸 수 있는 방법

오버라이딩 관계에 있는 메소드.

A 클래스 안에는 funcA 메소드가 있고, B 클래스 안에는 funcB 메소드가 있다.
A형 참조변수 a는 B형 인스턴스를 참조하지만, 내부적으로는 A형 인스턴스의 주소를 참조하고 있는 상태이다.
따라서 A형 인스턴스 안에는 funcA 메소드가 있을 것이고, B형 인스턴스 안에는 funcB가 있을 것이다.
그런데 만약 A를 상속받는 B 클래스에서, funcA 메소드를 오버라이딩 한다면 A 인스턴스 안에 있는 funcA 메소드는 무효화되어 사라진다.
이 상황에서 funcA를 호출하면, B 인스턴스에 있는 funcA를 호출하는 것과 동일한 상황이 된다.

5) 다형성을 쓰는 이유

마찬가지로 회원 관리 시스템 예제 문제를 리팩토링하며 다형성을 쓰는 이유에 대해 알아보자.

① 10개, 100개 등급이 있다면?

현재 회원 관리 시스템에 등급은 Silver 와 Gold 두가지 뿐이다. 그러나 앞서 말했듯이 시스템이 개편되어 등급이 10개, 100개로 늘어난다면?
이 때, 다형성을 이용하면 등급별 배열을 10개, 100개 만들어 줄 필요가 없어진다.

// <수정 전>
// Manager class
private Silver[] silverMembers = new Silver[10];
private int silverIndex = 0;
   
private Gold[] goldMembers = new Gold[10];
private int goldIndex = 0;

Silver 와 Gold 를 둘 다 넣을 수 있는 자료형은 Grade이다.
따라서 Grade형 배열을 만들어주면 된다.

// <수정 후>
// Manager class
private Grade[] members = new Grade[10];
private int index = 0;

② 추상 클래스에.. new.. ?

추상 클래스 Grade 에 new를 할 수 없다고 했는데, 이상하다.
뭔가 잘못된 것 같다.
여기서 헷갈리지 말아야 한다.
Grade를 new한 것이 아니라, Grade[]를 new한 것이다.
두 상황은 아예 다른 상황이다. Grade형 변수를 10개 만든거지 Grade 인스턴스 10개를 만든 것이 아니기 때문이다.

③ 오버라이딩과 다형성

// <수정 전>
// Main class 
if(menu == 1) {
	// 대충 입력값 받기 귀찮아서 정보 자동으로 넣어버리는 코드
	manager.addMember(new Gold(1001,"Tom",1000));
	manager.addMember(new Silver(1002,"Jack",2000));
	manager.addMember(new Silver(1003,"Susan",3000));
	manager.addMember(new Gold(1004,"Jane",4000));
}else if(menu == 2) {  // 정보 출력 기능
	Gold[] goldMembers = manager.getGoldMembers();
	for(int i=0;i<manager.getGoldIndex();i++) {
		System.out.println(  // Gold 멤버 출력
					goldMembers[i].getId()+"\t"+
					goldMembers[i].getName()+"\t"+
					goldMembers[i].getPoint()+"\t"+
					goldMembers[i].getBonus());
					
	Silver[] goldMembers = manager.getSilverMembers();
	for(int i=0;i<manager.getSilverIndex();i++) {
		System.out.println(  // Silver 멤버 출력
					silverMembers[i].getId()+"\t"+
					silverMembers[i].getName()+"\t"+
					silverMembers[i].getPoint()+"\t"+
					silverMembers[i].getBonus());
	}

수정 전에는 코드가 거의 같은 등급 별 멤버 출력 코드가 있었다면,
수정 후에는 등급을 하나로 합쳐서 출력할 수 있게 조금 더 깔끔하게 변했다.

// <수정 후>
// Main class 
if(menu == 1) {
	// 대충 입력값 받기 귀찮아서 정보 자동으로 넣어버리는 코드
	manager.addMember(new Gold(1001,"Tom",1000));
	manager.addMember(new Silver(1002,"Jack",2000));
	manager.addMember(new Silver(1003,"Susan",3000));
	manager.addMember(new Gold(1004,"Jane",4000));
}else if(menu == 2) {  
	Grade[] members = manager.getMembers();
	for(int i=0;i<manager.getIndex();i++) {
		System.out.println(
				members[i].getId()+"\t"+
				members[i].getName()+"\t"+
				members[i].getPoint()+"\t"+
				members[i].getBonus());
	}

members 배열에서 0번 항목을 꺼내면 Grade형이다. 그러나 이 Grade형 변수 안에는 Gold형 인스턴스가 존재한다.
그러나 이 0번 항목에 .을 찍으면 다형성의 규칙상 Grade의 기능만 보여준다.
다행히 Grade와 Silver, Gold 클래스는 getBonus 메소드를 제외한 모든 메소드가 동일하다.
그래서 Down Casting을 쓰지 않아도 getter 메소드를 사용할 수 있다.
Grade 클래스의 getBonus 메소드는 추상메소드이므로 members[i].getBonus() 상황에서 오버라이딩으로 인스턴스 자신의 getBonus 메소드를 가져온다.
다시말해, members[i]가 Silver면 Silver 클래스의 getBonus 메소드를, Gold면 Gold 클래스의 getBonus를 가져오는 것이다.
만약 getBonus를 오버라이딩 하지 않고, 추상화도 하지 않았다면 if문으로 인스턴스 종류가 무엇인지 일일히 검사해서 출력해줘야하는 번거로움이 있었을 것이다.

3. Collection Framework

3번 저장소 문제를 해결하는 방법을 알아보자.
지금 코드를 짜는 건 Compile Time 이전에 배열의 크기를 정해주는 것인데, 실제 멤버의 수는 Compile Time 이후 Runtime(실행시점)에 배열의 크기가 정해진다.
그래서 배열을 쓰는 것은 앞뒤가 맞지 않는다.
이 부분을 해결할 수 있는 방법은 무엇일까?
배열이 스스로 사이즈를 조절할 수 있는 방법이 없을까?

이런 고민은 오~래전부터 개발자들이 해왔던 고민들 중에 하나이다.
그만큼 많은 연구와 고민 끝에 개발자 선배님들은 이런 방법을 클래스 라이브러리로 만드는데 성공했다!

1) ArrayList (동적 배열, 스마트 배열)

초기 Vector라는 Dynamic Array(동적 배열) 클래스 라이브러리가 존재했지만,
시간이 흘러 이름이 변해 현재는 ArrayList 라는 이름을 가진다.

스스로 사이즈 조절이 가능한 동적 배열 클래스 라이브러리

그러나 컴퓨터의 성능이 좋아짐에 따라 처리할 수 있는 데이터의 양이 증가하게 되어 단점이라고 생각하지 못했던 기능들이 대량의 데이터를 처리할 때는 한계가 있음을 알게 되었다.
바로 가운데쪽에서 데이터가 삭제될 때, 배열이 앞으로 땡겨지며 복사가 엄청나게 일어나서 성능이 저하되는 것이다.

① Array VS ArrayList

예제를 통해 두 배열의 차이를 알아보자.

ⓐ 입력
// Array
String[] arr = new String[3];
arr[0] = "Hello";
arr[1] = "Java";
arr[2] = "World";

// ArrayList
ArrayList arr = new ArrayList();  // 사이즈 명시 X
arr.add("Hello");  // array와는 달리 몇번에 넣겠다는 게 뜨지 않음
arr.add("Java");   // arraylist는 앞에 빈칸이 있는 것을 허용하지 않음 
arr.add("World");  // -> 동적배열이기때문
ⓑ 출력
// Array
System.out.println(arr[0]);
System.out.println(arr[1]);
System.out.println(arr[2]);

// ArrayList
System.out.println(arr.get(0));
System.out.println(arr.get(1));
System.out.println(arr.get(2));
ⓒ 0번 인덱스값 삭제
// Array
arr[0] = arr[1];
arr[1] = arr[2];
arr[2] = null;

// ArrayList
arr.remove(0);  // 자동으로 앞으로 땡겨지므로 다른 코드를 더 쓸필요가 없음
ⓓ 0번 인덱스에 “Good” 삽입
// Array
arr[2] = arr[1];
arr[1] = arr[0];
arr[0] = "Good";

// ArrayList
arr.add(0, "Good");  
ⓔ 3번 인덱스에 “Hello” 삽입
// Array
arr[3] = "Hello";  // 에러! : 지정된 크기를 넘어섰음.

// ArrayList
arr.add("Hello");  // 데이터 개수에 제한 없음 (메모리가 허가하는 한..)
ⓕ 배열의 길이
// Array
System.out.println(arr.length);

// ArrayList
System.out.println(arr.size());

② 조상 Object

Array와 달리 ArrayList는 데이터 타입을 명시하지 않는다. ArrayList의 데이터 타입은 뭘까?
ArrayList에는 String, int, double 심지어는 new Scanner(System.in) 까지 다 입력이 가능하다.
바로 다형성을 이용하고 있는 것이다.
그렇다는 것은.. 이 모든 값의 부모 클래스가 있다는 이야기가 된다.

Object : 자바에 존재하는 모든 클래스의 최고 조상

자바에 존재하는 모든 클래스는 extends Object를 하고 있다. (다만 보이지 않는 것)
근데 분명 클래스는 1개만 extends 할 수 있다고 했었는데?
A, B 클래스가 있을 때, B extends A 라면, B는 Object를 상속받을 수 없는 것 아닐까?
라고 생각할 수 있지만, A 클래스를 보면 A extends Object가 생략된 것이므로
B -> A -> Object 순서로 상속받게 되어 결국 B는 Object를 상속받을 수 밖에 없다.

Grade 생성자에 super() 가 있었던 이유도 설명이 된다.
바로 Object를 가리키는 것이다.

③ Wrapper Class

Object o는 참조 변수로, 값이 아니라 주소를 저장할 수 있다.
아래 코드를 보면 123과 3.14는 주소가 아니라 값인데 어떻게 저장할 수 있는 것일까?

Object o = new Scanner(System.in);
o = 123;  // new Integer(123);
o = 3.14;  // new Double(3.14);
o = "Hello";
o = new Exam_03_ArrayList();

원래는 안돼야하는 것이 맞다.
그러나 이 상황에서 자바는 자동으로 값을 참조형으로 바꿔준다.
이와 같이 기본형 타입을 객체로 다루기 위해 자바에는 Wrapper Class라는 기본형을 가지는 클래스가 존재한다.
예를 들면, byte 는 Byte 클래스, short 는 Short 클래스, int 는 Integer 클래스…
따라서 ArrayList는 Object형 변수들의 집합이라는 것을 알 수 있다.

④ ArrayList 의 단점

ArrayList가 Object형 변수들의 집합이라는 것은 다시말하면, 배열에 있는 값들은 모두 Object형으로 꺼내진다는 이야기가 된다.

ArrayList arr = new ArrayList();
arr.add("Hello");

System.out.println((String)arr.get(0)).length());

위와 같이 배열의 0번에 있는 String형 값 “Hello”의 길이를 찾으려고 할때, arr.get(0)은 String형이 아니라 Object형이기 때문에 length메소드를 쓸 수가 없다.
따라서 무조건 Down Casting을 해줘야만 하는 번거로움이 있다.
이 불편함을 해소하는 방법은… 투비컨티뉴…

2) LinkedList

데이터가 삭제될 때 성능이 저하되는 ArrayList 의 단점을 보완하기 위해 만들어진 것이 LinkedList 이다.
노드에 값을 저장하고, 다음노드의 위치정보를 저장함으로써 이어지는 비순차적인 배열이다.
값을 삭제할 때는 편하지만, 그렇다고 무조건 LinkedList가 좋은 것은 아니다.
ArrayList에 비해 저장하는 값이 많아서, 메모리를 더 사용한다는 단점이 있다.

3) Hash & Tree

ArrayList와 LinkedList 모두 검색할 때의 성능은 좋지 않은 편이여서,
검색을 효율적으로 할 수 있는 Hash나 Tree 도 개발되었다.
Hash는 조금 어려운 편이라서 패스..
어쨌든 Hash나 Tree는 대량의 데이터를 처리하는데 유용하게 쓰인다고 한다.

4) ArrayList VS LinkedList VS Hash / Tree

각자의 장단점이 있기 때문에, 적합한 것으로 선택하는 것이 중요하다.

  • ArrayList
    • 장점 : 스스로 사이즈를 조정하고 빈 공간을 채워주는 기능을 수행
    • 단점 : 비순차적 (중간에서 갑자기) 데이터를 삭제할 경우 비정상적인 성능 저하
  • LinkedList
    • 장점 : 비순차적 데이터 삭제에 효율적으로 반응 / 성능저하 없음
    • 단점 : 순차적 데이터 입력에 대해 동적배열보다 상대적으로 느림
  • ArrayList / LinkedList
    • 공통적 단점 : 대량 데이터가 보관되어 있을 시 검색 성능이 낮음
  • Hash / Tree
    • 장점 : 데이터 검색 성능이 비약적으로 향상
    • 단점 : 데이터 입력성능은 포기하다시피 구성

5) Collection Framework

Collection : 데이터 변수가 여러개 모여있는 구조 Collection Framework(Library) : Collection을 클래스 라이브러리로 만든 것

배열, ArrayList, LinkedList … 이런 것들을 모두 Collection 이라고 한다.

4. toString()

참조변수를 그냥 출력하게 되면, .toString()이 뒤에 붙어야하지만 암묵적 문법으로 인해 생략된다.
.toString()라는 메소드는 Object 클래스에서 물려받은 메소드이다.
즉, 우리가 만든 것을 출력했을 때, 어떤 걸 출력하느냐는 우리가 정한 것이 아니라 Object 클래스가 정해놓은 것이다.

왜 아래 세가지 참조변수는 출력값이 다 다르게 나오는 걸까?

Gold g = new Gold();
System.out.println(g); // System.out.println(g.toString());
// grade.Gold@73a28541

Scanner sc = new Scanner(System.in);
System.out.println(sc);
// java.util.Scanner[delimiters=\p{javaWhitespace}+...

ArrayList arr = new ArrayList();
arr.add("Hello");
arr.add("Java");
arr.add("World");
System.out.println(arr);
// [Hello, Java, World]

Gold 클래스는 만들 때 toString 메소드를 전혀 건들지 않았다.
따라서 Object 클래스가 만들어놓은 toString 메소드를 따라갈 것이다.
Scanner 클래스는 내가 만든 클래스가 아니다.
아마 만든 사람이 저런 값을 출력하게 만들어 놓은 것으로 보인다.
ArrayList를 만든 사람들은, 편리하게 쓰는 것이 목적이었으므로 배열의 값을 출력하게 만들어놓은 것이다.

결국, 내가 만드는 클래스 안에 toString 메소드를 오버라이딩하면, 참조변수를 출력했을 때 어떤 값이 보이고 싶은지는 내가 만들어낼 수 있는 것이다.

댓글남기기