객체지향에서 중요한 것을 클래스를 빨리 파악하는 것이다. 이 클래스가 다른 클래스와 어떤 관계를 맺는지, 어떤 일을 하는지, 그리고 어떤 멤버들이 있는지 파악한다.
메인 메서드는 프로그램의 진입점(entry point)이다. 따라서 메인 메서드의 접근자는 항상 public이어야 한다. 메인 메서드가 없으면 프로그램을 시작할 수 없다.
CRUD
대부분의 컴퓨터 소프트웨어가 가지는 기본적인 데이터 처리 기능 4가지를 묶어서 일컫는 말이다. 이러한 4개 조작을 모두 할 수 없다면 그 소프트웨어는 완전하다고 할 수 없다.
이름 | 조작 | SQL |
---|---|---|
Create | 생성 | INSERT |
Read(Retrieve) | 읽기 | SELECT |
Update | 갱신 | UPDATE |
Delete | 삭제 | DELETE |
클래스 기능 추가 방법: 상속의 필요성 이해
1. 기존의 클래스를 변경한다.
- 기존 소스 코드를 변경하면 기존 클래스를 사용해 만든 프로그램도 영향을 받고, 심각한 오류가 발생할 수 있다.
- 계속 덧붙이다 보면 누더기 코드가 될 가능성이 있다.
- 쓰지 않는 코드가 계속 누적되는 문제가 있다.
2. 기존 클래스를 복사하여 새 클래스를 만든다.
- 같은 일을 하는 여러 클래스가 존재하면 관리하기 힘들어진다.
- 원본에 있는 기능에 문제가 있으면 전체를 다 바꿔야 하기 때문에 유지 보수가 힘들다.
- 위의 그림같이 버그가 있으면 버그도 복사되어 버그 수정할 때 하나하나 다 수정해줘야 한다.
3. 상속을 이용하여 기능을 추가한 클래스를 사용한다.
- 기존 코드에 문제가 있으면 그 코드만 수정하면 된다. 수정사항은 해당 코드를 받아 만든 모든 클래스에 자동으로 적용된다.
- 기존 코드에 문제가 있으면 해당 코드를 수정하는 순간 그 코드를 상속 받아 만든 모든 클래스에 자동으로 적용된다.
- 유지보수가 용이하다.
- 즉, 상속은 코드 재사용성을 높인다. 기존 코드의 수정을 최소화하면서 새 기능을 추가할 수 있게 해 준다. 이것이 바로 상속 문법이 등장한 이유!!
버전업을 할 때 자바는 메서드 및 클래스를 바로 삭제하지 않고 @deprecated 주석을 단다. 과거 호환성이 좋은 장점이 있다. 그러나 계속 기능을 추가하면서도 지원하지 않는 클래스 및 메서드를 삭제하지 않아 누더기 코드가 된다는 단점이 있다. 반대로 파이썬은 과거 호환성이 낮다(파이썬2와 3이 서로 호환이 되지 않는다.)
클래스 로딩 과정
상속
상속이란?
기존에 만든 클래스를 자신의 코드처럼 사용하는 기법이다. 보통 기존 코드를 손대지 않고 새 코드를 덧붙일 때 많이 사용한다. 상속해주는 클래스는 수퍼클래스(super class), 부모클래스(parents class)라고 한다. 다른 클래스를 상속 받는 클래스는 서브클래스(sub class), 자식클래스(child class)라고 한다.
인스턴스 생성 절차
- 서브 클래스가 사용한다고 선언한 수퍼클래스를 먼저 메모리에 로딩한다.
- 서브 클래스를 메모리에 로딩한다.
- 수퍼 클래스에 선언된 대로 인스턴스 변수를 Heap에 만든다.
- 서브클래스에 선언된 대로 인스턴스 변수를 Heap에 만든다.
오해하지 말자
- 서브 클래스가 수퍼 클래스의 코드를 갖고 있는 것이 아니다. 상속은 기존 코드를 그대로 가져오는 자동복사 개념이 아니다. (이름 때문에 이렇게 종종 오해된다.)
- 서브 클래스는 단지 수퍼 클래스의 링크 정보만 갖고 있다. 따라서 서브 클래스를 사용하려면 반드시 수퍼 클래스가 있어야 한다.
- 수퍼클래스.class 파일을 삭제하면
java.lang.NoClassDefFoundError
가 뜬다. 즉, 상속받은 클래스가 없으면 실행 자체가 되지 않는다는 말이다. 상속은 서브클래스가 수퍼클래스 사용하겠다는 선언이다.
- 수퍼클래스.class 파일을 삭제하면
생성자 호출 순서
서브클래스의 인스턴스를 생성하면, 수퍼클래스의 멤버와 서브클래스의 멤버가 모두 합쳐진 하나의 인스턴스가 생성된다. 이 때 수퍼 클래스의 멤버의 초기화 작업이 수행되어야 하기 때문에 서브 클래스의 생성자에서 수퍼 클래스의 생성자가 호출되어야 한다. 생성자의 첫 줄에서 수퍼클래스 생성자를 호출해야 하는 이유는 서브클래스의 멤버가 수퍼클래스의 멤버를 사용할 수도 있기 때문에 먼저 초기화되어 있어야 하기 때문이다. 수퍼 클래스 생성자의 호출은 클래스 상속관계를 거슬러 올라가면서 최고 조상인 Object클래스의 생성자인 Object()로 갈 때까지 계속 반복된다.
super()
는 수퍼클래스의 생성자를 호출하는 데 사용된다. Object클래스를 제외한 모든 클래스의 생성자 첫 줄에는 생성자 this() 또는 super()를 호출해야 한다. 그렇지 않으면 컴파일러가 자동적으로 super(); 를 생성자의 첫 줄에 삽입한다.
단, 컴파일러는 기본적으로 수퍼클래스의 기본 생성자(super())를 호출하기 때문에, 파라미터로 값을 받는 특정 생성자를 호출할 경우 이를 개발자가 명시해주어야 한다.
예제
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
class A {
int v1;
A() {
System.out.println("A()생성자");
}
}
class B {
int v2;
B() {
System.out.println("B()생성자");
}
}
class C {
int v3;
C() {
System.out.println("C()생성자")
}
}
public static void main(String[] args) {
C obj = new C();
obj.v1 = 100;
obj.v2 = 200;
obj.v3 = 300;
System.out.printf("v1=%d, v2=%d, v3=%d\n", obj.v1, obj.v2, obj.v3);
}
// 결과
// A()생성자
// B()생성자
// C()생성자
// 100, 200, 300
위의 코드를 그림으로 나타내면 다음과 같다.
java.lang.Object
Class Object
is the root of the class hierarchy. Every class has Object
as a superclass. All objects, including arrays, implement the methods of this class.
다음은 java.lang.Object의 인스턴스 메서드들이다. 자바의 모든 클래스는 다음과 같은 메서드들을 다 가지고 있다. 모든 클래스의 최고 조상은 Object이기 때문이다. 자바의 모든 객체는 이 메서드들을 호출할 수 있다.
toString()
equals()
hashCode()
getClass()
: 어느 클래스에서 사용되니?clone()
: 인스턴스 복제finalize()
- 가비지 컬렉터가 인스턴스를 제거하기전에 바로 호출하는 메서드
- c++의
distructor()
와 비슷한 역할을 한다. 다만 자바에서는 c++과 달리 이 메서드가 호출된다는 보장이 없다. 자바의 경우 메모리가 충분하면 가비지 컬렉터가 실행되지 않기 때문이다. - 어차피 프로그램이 종료되면 메모리는 다 해제되니까 이 메서드는 굳이 신경쓰지 않아도 된다.
단일상속
- C++에서는 다중상속을 허용한다. 그러나 클래스간의 관계가 복잡해지고, 서로 다른 클래스로부터 상속받은 멤버 간의 이름이 같은 경우 구별할 수 없다는 단점이 있다.
- 따라서 Java에서는 다중상속의 장점(복합적인 기능을 가진 클래스 쉽게 만들 수 있음)에도 불구하고 단일 상속만을 허락한다.
객체지향 프로그래밍의 핵심
업무에서 다뤄야 할 사람, 사물, 개념의 특징을 분석하여 data로 정의하고, 컴퓨터를 이용하여 처리할 업무의 행위를 분석하여 method로 정의하는 문법을 class라고 한다. 이렇게 class로 정의하는 것을 추상화라고 한다.
추상화(abstraction)을 하는 방법에는 크게 3가지이다. 첫번째는 상속(inheritance)으로, 코드를 재사용하기 쉽게 만들어준다. 두번째는 다형성(polimorphism)으로, 코드 작성을 쉽게 해준다. 다형성에는 다형변수, 오버로딩, 오버라이딩 개념이 있다. 세번째는 캡슐화(encapsulation)으로 코드 사용 범위를 제어한다.
다형성 - 다형적 변수
객체지향개념에서 다형성이란 여러 가지 형태를 가질 수 있는 능력을 말한다. 자바에서는 상위클래스 타입의 레퍼런스로 하위클래스의 인스턴스를 참조할 수 있도록 하였다는 의미이다. 하위클래스는 항상 상위 클래스의 모든 것을 사용할 수 있기 때문이다.
다음의 실습을 통해 다형성을 이해해보자.
상위 클래스의 레퍼런스로 하위 클래스의 인스턴스를 가리킬 때 주의할 점이 있다. 레퍼런스가 실제 하위인스턴스를 가리키고 있다 하더라도, 레퍼런스 타입의 범위를 벗어나서 사용할 수는 없다는 것이다. 자바 컴파일러는 레퍼런스가 실제 어떤 인스턴스를 가리키는지 따지지 않고 레퍼런스의 클래스를 보고 사용할 수 있는 변수/메서드를 판단하기 때문이다.
그럼에도 불구하고 레퍼런스가 실제 가리키는 객체의 모든 메모리에 접근하기 위해서는 레퍼런스 변수가 실제 가리키는 것이 무엇인지 명시적 형변환을 통해 알려주거나, 인스턴스의 원래 클래스 레퍼런스에 저장한 다음에 사용해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Car car = new Sedan();
// 상위 레퍼런스가 가리키고 있는 하위 인스턴스의 멤버를 사용하려고 하면
// 실제 가리키고 있는 것은 하위 클래스의 인스턴스라고 해도
// 레퍼런스 타입의 범위를 벗어나서 사용할 수 없다.
car.sunroof = true; // 컴파일 오류
// 해결책
// 1. 명시적 형변환: ((원래인스턴스타입)레퍼런스).멤버
((Sedan)car).sunroof = true; //OK!
// 2. 인스턴스의 원래 클래스 레퍼런스에 저장한 다음에 사용
Sedan sedan = (Sedan)car;
sedan.sunroof = true; //OK!
하위 클래스의 레퍼런스로 상위 클래스를 가리킬 수는 없다. 상위 클래스의 인스턴스에는 하위 클래스의 인스턴스 변수가 없을 수도 있기 때문이다. 개발자가 레퍼런스를 통해 존재하지 않는 인스턴스 멤버를 사용하는 것을 방지하기 위해 컴파일 단계에서 막는다. 명시적 형변환을 하면 컴파일 단계에서는 막지 못해도 런타임 에러가 발생한다. (java.lang.ClassCastException
: 맞지 않는 것을 던지지 마라!)
다형적 변수의 활용
다형적 변수를 사용하게 되면 동일한 코드를 갖고 있는 메서드를 한 개의 메서드로 통합할 수 있다. Sedan 객체와 Truck객체를 모두 가리킬 수 있는 상위 클래스의 레퍼런스(Car car)를 선언하면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void printCar(Car car) {
System.out.printf("모델명: %s\n", car.model);
System.out.printf("cc: %d\n", car.cc);
System.out.println("-------------------------");
}
public static void main(String[] args) {
Sedan car1 = new Sedan();
car1.model = "티코";
car1.cc = 800;
Truck car2 = new Truck();
car2.model = "타이탄II";
car2.cc = 10000;
printCar(car1); // OK! 왜? Sedan은 Car의 일종이다.
printCar(car2); // OK! 왜? Truck도 Car의 서브클래스이다.
}
다형적 변수에 대해 몰랐을 때는 Sedan 객체와 Truck객체를 각각 처리하는 두 개의 메서드를 만들어야 했다. 다형적 변수를 사용하면 Sedan과 Truck을 모두 처리하는 메서드를 만들어 사용할 수 있다!
instanceof 연산자
레퍼런스에 들어 있는 주소가 특정 클래스의 인스턴스인지 검사한다. 또는 그 하위 클래스의 인스턴스인지 검사한다.
1
2
3
4
5
6
Vehicle v = new Sedan();
System.out.println(v instanceof Sedan); // true
System.out.println(v instanceof Car); // true
System.out.println(v instanceof Object);// true
System.out.println(v instanceof Truck); // false
getClass()
instanceof 연산자는 상위 클래스를 피연산자로 주었을 때까지 모두 true가 나온다. 특정 클래스의 인스턴스인지 좁혀서 검사하기 위해서는 getClass()
메서드를 사용하면 된다.
1
2
3
4
System.out.println(v.getClass() == Sedan.class); //true
System.out.println(v.getClass() == Car.class); //false
System.out.println(v.getClass() == Object.class);//false
System.out.println(v.getClass() == Truck.class); //false
레퍼런스를 사용해서 클래스 정보를 알아낸 다음에 빌트인 클래스 변수로 비교해서 알아볼 수 있다. class변수가 내장되어 있다. 이 정보를 꺼내서 비교
예제
하나의 메서드로 Sedan, Truck, Bike의 모든 정보를 자세히 출력하는 프로그램 만들기
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
public static void print(Vehicle v) {
System.out.println("[기본정보]");
System.out.printf("모델명: %s\n", v.model);
System.out.printf("수용인원: %d\n", v.capacity);
if (v instanceof Bike) {
Bike bike = (Bike) v;
System.out.println("[바이크 정보]");
System.out.printf("엔진의 존재: %b\n", bike.engine);
} else if (v instanceof Car) {
Car car = (Car) v;
System.out.println("[자동차 기본정보]");
System.out.printf("cc: %d\n", car.cc);
System.out.printf("밸브: %d\n", car.valve);
if (v instanceof Sedan) {
Sedan sedan = (Sedan) v;
System.out.println("[승용차 기본정보]");
System.out.printf("썬루프: %b\n", sedan.sunroof);
System.out.printf("자동변속: %b\n", sedan.auto);
} else if (v instanceof Truck) {
Truck truck = (Truck) v;
System.out.println("[트럭 정보]");
System.out.printf("덤프가능: %b\n", truck.dump);
System.out.printf("중량: %.1f\n", truck.ton);
}
}
System.out.println("-------------------------");
}