프로그래밍 일기 — 객체를 지향하는 Java 2

배우는 자(Learner Of Life)
42 min readAug 17, 2023

--

정말 배울게 많다…

#Java, #객체지향, #클래스, #접근제어자, #오버라이딩, #final, #패키지

Java의 효과적 사용을 위해서는 알아야할 개념들이 많고 복잡하다.(1)

오늘은 Java의 객체지향 개념들을 공부하는 두 번째 날이다. Java의 가장 중요한 개념답게, 이 부분은 정말 배울 것이 많고, 그 것들이 한번에 이해가 잘 되지 않을 정도로 그렇게 간단한 개념이라는 생각이 들지 않았다. 역시 프로그래밍에 입문하려는, 그것도 이 일로 밥먹고 살아가려고 결정한 것에 대한 댓가는 작지않았다. 그만큼 노력해야하고, 시간을 많이 투자해야한다는 것을 느낀다.

오늘 역시 Java의 객체 지향 개념에 대해 학습한다. 오늘은 부디 배워야할 내용을 어제보다는 조금 더 원활하게 소화할 수 있기를 바란다.

접근 제어자

제어자는 클래스, 변수, 메서드 등의 선언부에 사용되어 부가적인 의미룰 부여한다.

  • 접근 제어자: public , protected , default , private
  • 그 외 제어자: static , final , abstract

하나의 대상에 여러 개의 제어자를 조합해서 사용할 수 있으나, 접근 제어자는 단 하나만 사용할 수 있다. 멤버 또는 클래스에 사용, 외부에서 접근하지 못하도록 제한한다. 클래스, 멤버변수, 메서드, 생성자에 사용되고, 지정되어 있지 않다면 default 이다.

  • public : 접근 제한이 전혀 없다
  • protected : 같은 패키지 내에서, 다른 패키지의 자손클래스에서 접근 가능
  • default : 같은 패키지 내에서만 접근 가능
  • private : 같은 클래스 내에서만 접근 가능

사용가능한 접근 제어자에는 아래가 있다. 지역변수에 사용되는 것은 없다.

  • 클래스: public , default
  • 메서드 & 멤버변수: public , protected , default , private

접근 제어자는 클래스 내부에 선언된 데이터를 보호하기 위해서 사용된다. 이를 다른 말로 캡슐화(Encapsulation)이라고 한다. 유효한 값을 유지하도록, 함부로 변경하지 못하도록 접근을 제한하는 것이 필요하다. 접근 제어자를 생성자에 사용하면 인스턴스의 생성을 제한할 수 있다. 일반적으로 생성자의 접근 제어자는 클래스의 접근 제어자와 일치한다.

Getter/Setter

접근 제어자는 객체의 변경이 없는 상태(무결성)를 유지하기 위해 사용한다. 이때 외부에서 필드에 직접 접근하는 것을 막기 위해 필드에 private , default 등의 접근 제어자를 사용할 수 있다. 객체의 private 필드를 읽어오거나 저장하기 위해 사용하는 것이 GetterSetter 이다.

Getter 는 외부에서 객체의 private 한 필드를 읽을 필요가 있을 때 사용한다. 예를 들어 자동차 클래스의 필드에 private 접근 제어자로 지정한 필드가 있다면, Getter 메서드로 값을 가져올 수 있다. 주로 get + [필드 이름] (필드 이름의 첫 글자는 대문자여야한다.)의 문법을 갖는다. 사용법은 인스턴스 메서드 호출과 동일하다.

// private 접근 제어자 필드
private double speed; // 자동차 속도 , km/h
private char gear = 'P'; // 기어의 상태, P,R,N,D
private boolean lights; // 자동차 조명의 상태

// getter 메서드
public String getModel() {
return model;
}
public String getColor() {
return color;
}
public double getPrice() {
return price;
}

Setter 매서드 역시 private 필드의 저장/수정이 필요할 때 활용한다. 예를 들어 자동차 클래스에 private 접근 제어자로 지정한 필드가 있다고한다면, Setter 매서드를 토해 값을 저장하거나 수정할 수 있다. 주로 set + [필드 이름] (필드 이름의 첫 글자는 대문자여야한다.)의 문법을 갖는다. 사용법은 인스턴스 메서드 호출과 동일하다.

private double speed; // 자동차 속도 , km/h
private char gear = 'P'; // 기어의 상태, P,R,N,D
private boolean lights; // 자동차 조명의 상태

public void setModel(String model) {
this.model = model;
}
public void setColor(String color) {
this.color = color;
}
public void setPrice(double price) {
this.price = price;
}

제어자 조합

  • 클래스: public , default , final , abstract
  • 메서드: public , protected , default , private , final , abstract , static
  • 멤버변수: public , protected , default , private , final , static
  • 지역변수: final

제어자 사용시 주의사항이 있다.

  • 메서드: staticabstract 는 함께 사용 불가
  • 클래스: abstractfinal 을 동시에 사용 불가
  • abstract 메서드의 접근 제어자는 private 일 수 없다.
  • 메서드에 privatefinal 을 같이 사용할 필요는 없다.

패키지(Package)

패키지는 클래스의 일부이면서 클래스를 식별해 주는 역할을 한다. 상위 패키지와 하위 패키지는 . 로 구분한다. 문법은 package {상위 패키지}.{하위 패키지} 이다. 예를 들어 oop.pk1 이라는 패키지와 oop.pk2 라는 패키지가 있다고 가정한다면, 두 패키지에 모두 Car 클래스가 존재하고 이 클래스를 활용하려면, Java는 패키지의 경로를 통해 구분한다.

아래 예제를 보자, oop.pk1oop.pk2 는 같은 이름의 클래스를 가지고 있지만, 각자 출력값은 다르다. 그렇다면 이 동명의 클래스를 동시에 활용하기 위해서는, 패키지의 이름과 클래스의 이름을 모두 넣어 인스턴스화 해주어야한다. 상위패키지는 oop , 하위패키지는 pk1pk2 그리고 각각의 클래스를 명기해주어 {상위패키지}.{하위패키지}.{클래스} 를 명기하여 인스턴스화하고, 이후 .horn() 메서드를 호출하여 각각의 클래스내 메서드에 정의된 출력값을 문제없이 출력할 수 있다.


// oop.pk1.Car 클래스
package oop.pk1;
public class Car {
public void horn() {
System.out.println("pk1 빵빵");
}
}

// oop.pk2.Car 클래스
package oop.pk2;
public class Car {
public void horn() {
System.out.println("pk2 빵빵");
}
}

// Main
package oop.main;
public class Main {
public static void main(String[] args) {
oop.pk1.Car car = new oop.pk1.Car();
car.horn(); // pk1 빵빵
oop.pk2.Car car2 = new oop.pk2.Car();
car2.horn(); // pk2 빵빵
}
}

import

다른 패키지에 있는 클래스를 사용하기 위해 명시하는 키워드이다. 예를 들어 위 oop.pk1 에 있는 클래스 Car 를 사용하려고한다면, import oop.pk1.Car 라는 문법으로 사용할 패키지와 클래스를 import 해 주어야한다. 만약 * 를 활용해 import oop.pk1.* 를 입력하면 패키지내 모든 클래스를 활용할 수 있다. 그러나 서로 다른 패키지에 존재하는 같은 이름의 클래스를 동시에 사용할 경우, 해당 클래스에 패키지 명을 전부 명시해야 프로그램이 헷갈리지 않는다.

아래 예제에서는 사용하는 패키지가 oop.main 내에 존재하므로 package oop.main 을 통해 패키지를 불러왔다. 둘다 동명의 Car 클래스를 가지는 oop.pk1oop.pk2import 했다.메인 프로그램에서 oop.pk1.Carimport 하고 Car 클래스를 인스턴화 하면 이 클래스의 .horn() 메서드는 pk1Car 클래스에 정의된대로 출력될 것이다. 그러나 pk2 에 정의된 .horn() 메서드를 호출하려면, oop.pk2.Car 로 사용할 상위와 하위 패키지 및 그 안의 클래스를 확실하게 명기하여 인스턴스화 해주어야한다.

// Car
package oop.pk1;
public class Car {
public void horn() {
System.out.println("pk1 빵빵");
}
}

package oop.pk2;
public class Car {
public void horn() {
System.out.println("pk2 빵빵");
}
}

// Main
package oop.main;
import oop.pk1.Car;
public class Main {
public static void main(String[] args) {
Car car = new Car();
car.horn(); // pk1 빵빵
oop.pk2.Car car2 = new oop.pk2.Car();
car2.horn(); // pk2 빵빵
}
}

상속

상속의 사전적 정의를 보자. 대게는 부모가 자식에게 무언가를 물려주는 행위를 말한다. 객체지향 프로그래밍에서는 이 상속의 개념을 활용해, 부모 클래스의 필드와 메서드를 자식 클래스가 이어 받을 수 있다. 상속을 활용하면 적은 양의 코드로도 새로운 클래스를 작성할 수도 있고, 공통된 코드를 관리하여 코드의 추가와 변경이 쉬워질 수 있다. 이러한 특성때문에 상속을 사용하면 코드의 중복이 제거되고 재사용성이 크게 증가하여 생산성과 유지보수성에 매우 유리하다.

클래스간 상속은 extends 키워드로 정의할 수 있다. 아래와 같은 문법을 따른다. 상속은 확장의 개념으로 이해하면 좋다. 자식 클래스는 부모 클래스의 속성(필드)이나 행위(메서드)를 물려받는다, 이때 주의해야할 것은 부모 클래스의 범위가 자녀 클래스의 범위보다 작다는 것이다. 조금 더생각해보면, 부모클래스의 특징을 물려받았지만, 거기서 조금 더 다양한 범위의 특징을 구현하기위해 자식 클래스를 활용하는 것이기 때문이다.

public class {자식클래스} extends {부모클래스} {
}

상속을 기억하는데 있어 아래 3가지 사항을 알면 좋다.

  • 부모 클래스에 새로운 필드와 메서드가 추가되면, 자식 클래스는 이를 상속받아 활용가능하다.
  • 자식 클래스에 새로운 필드와 메서드가 추가되어도 부모 클래스에게는 아무 영향이 없다.
  • 자식 클래스의 멤버 개수는 부모 클래스보다 항상 같거나 많다.

아래 코드 예제는 부모 클래스인 Car 와 이를 활용한 자녀 클래스 SportsCar 그리고 메인프로그램으로 구성된다. 먼저 메인프로그램에서 Car 클래스를 인스턴스화한다. 이 인스턴스를 통해 자녀클래스의 필드인 enginebooster에 접근하려고한다면, 이 둘은 Car 부모 클래스에 정의되어있지 않기 때문에 오류가 발생한다. 이에 접근하기 위해서는 자녀 클래스를 인스턴스화 해야한다. sportsCar 클래스를 인스턴스화하면, 그제서야 이 필드들에 접근이 가능하다.

이후에는 자식 클래스의 객체에서 부모 클래스 객체를 활용할 수 있다. 자녀 클래스 인스턴스인 sportsCar 에서 부모 클래스의 필드인 company.setModel() , .horn() , changeGear() 등의 메서드에 접근할 수 있다.

// Car.java
public class Car {
String company; // 자동차 회사
private String model; // 자동차 모델
private String color; // 자동차 색상
private double price; // 자동차 가격
double speed; // 자동차 속도 , km/h
char gear = 'P'; // 기어의 상태, P,R,N,D
boolean lights; // 자동차 조명의 상태
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public double gasPedal(double kmh, char type) {
changeGear(type);
speed = kmh;
return speed;
}
public double brakePedal() {
speed = 0;
return speed;
}
public char changeGear(char type) {
gear = type;
return gear;
}
public boolean onOffLights() {
lights = !lights;
return lights;
}
public void horn() {
System.out.println("빵빵");
}
}

// SportsCar.java
public class SportsCar extends Car{
String engine;
public void booster() {
System.out.println("엔진 " + engine + " 부앙~\n");
}
}

// Main.java
public class Main {
public static void main(String[] args) {
// 부모 클래스 객체에서 자식 클래스 멤버 사용
Car car = new Car();
// car.engine = "Orion"; // 오류
// car.booster(); // 오류
// 자식 클래스 객체 생성
SportsCar sportsCar = new SportsCar();
sportsCar.engine = "Orion";
sportsCar.booster();
// 자식 클래스 객체에서 부모 클래스 멤버 사용
sportsCar.company = "GENESIS";
sportsCar.setModel("GV80");
System.out.println("sportsCar.company = " + sportsCar.company);
System.out.println("sportsCar.getModel() = " + sportsCar.getModel());
System.out.println();
sportsCar.horn();
System.out.println(sportsCar.changeGear('D'));
}
}

// 출력값
엔진 Orion 부앙~

sportsCar.company = GENESIS
sportsCar.getModel() = GV80

빵빵
D

클래스간 관계

클래스간에도 관계를 분석하여 관계설정을 할 수 있다.

  • 상속관계: is — a (“~는 ~이다")
  • 포함관계: has — a (“~은 ~를 가진다.”)

조금 더 쉽게 이해하기위한 예제를 들 수 있다. 상속관계는 “인간은 포유류다.”라고 말 할 수 있지만, “인간은 포유류를 가진다.”고 말하진 않는다. 포함관계는 오히려 “자동차는 핸들, 타이어, 차문 등을 갖는다.”라고 할 때 더 적절하다.

// Tire.java
public class Tire {
String company; // 타이어 회사
double price; // 타이어 가격
public Tire(String company, double price) {
this.company = company;
this.price = price;
}
}

// Door.java
public class Door {
String company; // 차문 회사
String location; // 차문 위치, FL, FR, BL, BR
public Door(String company, String location) {
this.company = company;
this.location = location;
}
}

// Handle.java
public class Handle {
String company; // 핸들 회사
String type; // 핸들 타입
public Handle(String company, String type) {
this.company = company;
this.type = type;
}
public void turnHandle(String direction) {
System.out.println(direction + " 방향으로 핸들을 돌립니다.");
}
}

// Car.java
public class Car {
static final String company = "GENESIS"; // 자동차 회사
String model; // 자동차 모델
String color; // 자동차 색상
double price; // 자동차 가격
double speed; // 자동차 속도 , km/h
char gear = 'P'; // 기어의 상태, P,R,N,D
boolean lights; // 자동차 조명의 상태
Tire[] tire;
Door[] door;
Handle handle;
public Car(String model, String color, double price) {
this.model = model;
this.color = color;
this.price = price;
}
public void setTire(Tire ... tire) {
this.tire = tire;
}
public void setDoor(Door ... door) {
this.door = door;
}
public void setHandle(Handle handle) {
this.handle = handle;
}
public double gasPedal(double kmh, char type) {
changeGear(type);
speed = kmh;
return speed;
}
public double brakePedal() {
speed = 0;
return speed;
}
public char changeGear(char type) {
gear = type;
return gear;
}
public boolean onOffLights() {
lights = !lights;
return lights;
}
public void horn() {
System.out.println("빵빵");
}
}

// Main.java
public class Main {
public static void main(String[] args) {
// 자동차 객체 생성
Car car = new Car("GV80", "Black", 50000000);
// 자동차 부품 : 타이어, 차문, 핸들 선언
Tire[] tires = new Tire[]{
new Tire("KIA", 150000), new Tire("금호", 150000),
new Tire("Samsung", 150000), new Tire("LG", 150000)
};
Door[] doors = new Door[]{
new Door("LG", "FL"), new Door("KIA", "FR"),
new Door("Samsung", "BL"), new Door("LG", "BR")
};
Handle handle = new Handle("Samsung", "S");
// 자동차 객체에 부품 등록
car.setTire(tires);
car.setDoor(doors);
car.setHandle(handle);
// 등록된 부품 확인하기
for (Tire tire : car.tire) {
System.out.println("tire.company = " + tire.company);
}
System.out.println();
for (Door door : car.door) {
System.out.println("door.company = " + door.company);
System.out.println("door.location = " + door.location);
System.out.println();
}
System.out.println();
// 자동차 핸들 인스턴스 참조형 변수에 저장
Handle carHandle = car.handle;
System.out.println("carHandle.company = " + carHandle.company);
System.out.println("carHandle.type = " + carHandle.type + "\n");
// 자동차 핸들 조작해보기
carHandle.turnHandle("Right");
carHandle.turnHandle("Left");
}
}

// 출력값
tire.company = KIA
tire.company = 금호
tire.company = Samsung
tire.company = LG

door.company = LG
door.location = FL

door.company = KIA
door.location = FR

door.company = Samsung
door.location = BL

door.company = LG
door.location = BR


carHandle.company = Samsung
carHandle.type = S

Right 방향으로 핸들을 돌립니다.
Left 방향으로 핸들을 돌립니다.

단일 상속 및 다중 상속

Java의 특징 중 하나는 다중 상속을 허용하지 않는다는 것이다. 그 이유는 다중상속을 허용하면 클래스간 관계가 점점 복잡해질 수 있기 때문이다. 자식 클래스에서 상속받는 서로 다른 부모 클래스가 같은 이름의 멤버를 가진다면, 자식 클래스는 그 멤버들을 구분할 방법이 없기 때문이다.

Final클래스 및 메서드

final 키워드를 클래스와 메서드에 선언한다면 어떨까? 먼저 final 을 선언한 클래스를 상속 받으려하면 에러가 발생한다. 클래스에 final 키워드를 지정하여 선언하면 최종적인 클래스가 되기 때문에 더 이상 상속할 수 없는 클래스가 되기 때문이다. 또한, 더 이상 오버라이딩을 할 수 없는 메서드가 된다.

// final 클래스 사용 잘못된 예
public final class Car {}
...
public class SportsCar extends Car{} // 오류가 발생합니다.

// final 메서드 사용 잘못된 예
public class Car {
public final void horn() {
System.out.println("빵빵");
}
}
...

// 위 클래스의 final 매서드와 같은 이름의 메서드를 선언하는 것은 불가능하다.
public class SportsCar extends Car{
public void horn() { // 오류가 발생합니다.
super.horn();
}
}

객체(Object)

객체 클래스(Object Class)를 말하며, Java내 모든 클래스 중 최상위 부모 클래스이다. 모든 클래스는 객체 클래스의 메서드를 활용가능하며, 부모 클래스가 없는 자식 클래스는 컴파일러에 의해 자동으로 객체 클래스를 상속 받는다.

객체 클래스의 메서드의 예는 아래와 같다.

  • Object clone(): 해당 객체의 복제본을 생성해 반환
  • boolean equals(Object object) : 해당 객체와 전달받은 객체가 같은지 여부를 반환
  • Class getClass() : 해당 객체의 클래스 타입을 반환
  • int hashCode() : Java에서 객체를 식별하는 정수값인 해시 코드 반환
  • String toString() : 해당 객체의 정보를 문자열로 반환(Object 클래스에서는 {클래스 이름}@{hashcode값} 리턴

오버라이딩(Overriding)

부모 클래스로부터 상속받은 메서드의 내용을 재정의하는 것을 말한다. 부모 클래스의 메서드를 그대로 사용할 수도 있지만, 자식 클래스는 다른 용도에 맞게 변경하는 것이 용이한 경우에 활용되는 개념이다. 단, 이를 위해서는 아래 조건이 만족해야한다.

  • 선언부가 부모 클래스의 메서드와 일치해야한다.
  • 접근 제어자를 부모 클래스의 메서드 보다 좁은 범위로 변경은 불가능하다.
  • 예외는 부모 클래스의 메서드 보다 많이 선언할 수 없다.

아래는 오버라이딩의 예제이다. SportsCar 클래스는 부모 클래스인 Car 로부터 상속을 받는다. 여기서 @Override 라는 머릿말을 가지게된다. 이 것은 오버라이딩의 문법인데, 부모 클래스로부터 상속받은 자식 클래스의 멤버를 다른 용도로 변경하는 것이다. 이 자식 클래스의 brakePedal() 메서드는 이제 부모 클래스의 동명 메서드와는 다른 행위를 하도록 꾸밀 수 있다. horn() 메서드 역시 마찬가지다. 이 자식 클래스의 horn() 메서드는 스스로 내에서 정의하였으나 상속받지는 않은 booster() 메서드를 호출하도록 되어있다.

// Car.java
public class Car {
String company; // 자동차 회사
private String model; // 자동차 모델
private String color; // 자동차 색상
private double price; // 자동차 가격
double speed; // 자동차 속도 , km/h
char gear = 'P'; // 기어의 상태, P,R,N,D
boolean lights; // 자동차 조명의 상태
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public double gasPedal(double kmh, char type) {
changeGear(type);
speed = kmh;
return speed;
}
public double brakePedal() {
speed = 0;
return speed;
}
public char changeGear(char type) {
gear = type;
return gear;
}
public boolean onOffLights() {
lights = !lights;
return lights;
}
public void horn() {
System.out.println("빵빵");
}
}

// SportsCar.java
public class SportsCar extends Car{
String engine;
public void booster() {
System.out.println("엔진 " + engine + " 부앙~\n");
}
public SportsCar(String engine) {
this.engine = engine;
}
@Override
public double brakePedal() {
speed = 100;
System.out.println("스포츠카에 브레이크란 없다");
return speed;
}
@Override
public void horn() {
booster();
}
}

// Main.java
public class Main {
public static void main(String[] args) {
// 부모 클래스 자동차 객체 생성
Car car = new Car();
car.horn(); // 경적
System.out.println();
// 자식 클래스 스포츠카 객체 생성
SportsCar sportsCar = new SportsCar("Orion");
// 오버라이딩한 brakePedal(), horn() 메서드 호출
sportsCar.brakePedal();
sportsCar.horn();
}
}

// 출력결과
빵빵

스포츠카에 브레이크란 없다
엔진 Orion 부앙~

Super/Super()

부모 클래스의 멤버를 참조할 수 있는 키워드이다. 객체 내부 생성자 및 메서드에서 부모 클래스의 멤버에 접근하기 위해 사용할 수 있다. 자식 클래스 내부에서 선언한 멤버와 부모 클래스에서 상속받은 멤버와 이름이 같다면, super 로 이를 구분할 수 있다.

아래는 super 의 예제이다. 자식 클래스의 메서드를 호출하면 super 키워드로 접근한 부모 클래스의 model , color 필드에 매개변수 값이 저장된다. this 키워드로 접근한 자식 클래스의 price 필드에는 매개변수의 값이 바로 저장된다.

// 부모 클래스 Car 속성
String model; // 자동차 모델
String color; // 자동차 색상
double price; // 자동차 가격

// 자식 클래스 SportsCar 속성
String model = "Ferrari"; // 자동차 모델
String color = "Red"; // 자동차 색상
double price = 300000000; // 자동차 가격

// 자식 클래스의 메서드
public void setCarInfo(String model, String color, double price) {
super.model = model; // model은 부모 필드에 set
super.color = color; // color는 부모 필드에 set
this.price = price; // price는 자식 필드에 set
}

위 예제를 바탕으로 실제 코드 예제를 보자. 자녀 클래스인 SportsCarsetCarInfo() 메서드에서 부모 클래스의 필드인 modelcolorsuper 키워드를 통해 참조하는 것을 볼 수 있다. 그러나 price 필드는 부모의 것이 아닌 자신의 것을 참조하고있다. 이렇게되면 modelcolor 에 대한 매개변수값은 부모의 것에 저장되고, price 필드의 매개변수값은 자식 클래스의 것에 저장된다.

이 메서드를 매개변수값을 넣어 호출하면 modelcolor 에 대한 값은 부모의 것이 되며, 자식에게 지정한 값은 변하지 않는다. 오직 자식내 필드인price 의 값만 변할 뿐이다.

부모 클래스의 필드 값이 변경되었음을 확인하기위해, 자식 클래스의 객체에서 setModel()setColor() 메서드를 호출하여 modelcolor 의 값이 변경되었음을 확인할 수 있다.

// 부모 클래스
public class Car {
String company; // 자동차 회사
String model; // 자동차 모델
String color; // 자동차 색상
double price; // 자동차 가격
double speed; // 자동차 속도 , km/h
char gear = 'P'; // 기어의 상태, P,R,N,D
boolean lights; // 자동차 조명의 상태
public String getModel() {
return model;
}
public String getColor() {
return color;
}
public double gasPedal(double kmh, char type) {
changeGear(type);
speed = kmh;
return speed;
}
public double brakePedal() {
speed = 0;
return speed;
}
public char changeGear(char type) {
gear = type;
return gear;
}
public boolean onOffLights() {
lights = !lights;
return lights;
}
public void horn() {
System.out.println("빵빵");
}
}

// 자녀 클래스 SportsCar
public class SportsCar extends Car{
String engine;
String model = "Ferrari"; // 자동차 모델
String color = "Red"; // 자동차 색상
double price = 300000000; // 자동차 가격
public SportsCar(String engine) {
this.engine = engine;
}
public void booster() {
System.out.println("엔진 " + engine + " 부앙~\n");
}
public void setCarInfo(String model, String color, double price) {
super.model = model; // model은 부모 필드에 set
super.color = color; // color는 부모 필드에 set
this.price = price; // price는 자식 필드에 set
}
@Override
public double brakePedal() {
speed = 100;
System.out.println("스포츠카에 브레이크란 없다");
return speed;
}
@Override
public void horn() {
booster();
}
}

// Main.java
public class Main {
public static void main(String[] args) {
// 자식 클래스 스포츠카 객체 생성
SportsCar sportsCar = new SportsCar("Orion");
// 자식 객체의 model, color, price 초기값 확인
System.out.println("sportsCar.model = " + sportsCar.model); // Ferrari
System.out.println("sportsCar.color = " + sportsCar.color); // Red
System.out.println("sportsCar.price = " + sportsCar.price); // 3.0E8
System.out.println();
// setCarInfo 메서드 호출해서 부모 및 자식 필드 값 저장
sportsCar.setCarInfo("GV80", "Black", 50000000);
// this.price = price; 결과 확인
System.out.println("sportsCar.price = " + sportsCar.price); // 5.0E7
System.out.println();
// super.model = model; super.color = color;
// 결과 확인을 위해 자식 클래스 필드 model, color 확인 & 부모 클래스 메서드인 getModel(), getColor() 호출
// 자식 클래스 필드 값은 변화 없음.
System.out.println("sportsCar.model = " + sportsCar.model); // Ferrari
System.out.println("sportsCar.color = " + sportsCar.color); // Red
System.out.println();
// 부모 클래스 필드 값 저장됨.
System.out.println("sportsCar.getModel() = " + sportsCar.getModel()); // GV80
System.out.println("sportsCar.getColor() = " + sportsCar.getColor()); // Black
}
}

>>>

// 출력 값
sportsCar.model = Ferrari
sportsCar.color = Red
sportsCar.price = 3.0E8

sportsCar.price = 5.0E7

sportsCar.model = Ferrari
sportsCar.color = Red

sportsCar.getModel() = GV80
sportsCar.getColor() = Black

Super()

부모 클래스의 생성자를 호출할 수 있는 키워드로써, 객체 내부 생성자 및 메서드에서 해당 객체의 부모 클래스 생성자를 호출하기 위해 사용가능하다. 자식 클래스의 객체가 생성될 때 부모 클래스들이 모두 합쳐져서 하나의 인스턴스가 생성되는데, 이때 부모 클래스의 멤버들이 먼저 초기화되어야한다. 따라서, 자식 클래스의 생성자에서는 부모 클래스의 생성자가 호출되며, 부모 클래스의 생성자는 가장 첫 줄에 호출되어야한다.

자식 클래스 객체의 생성시, 생성자 매개변수에 매개값을 받아와서 Super() 를 사용해 부모 생성자의 매게변수에 매개값을 전달해 호출하면서 부모 클래스의 멤버를 먼저 초기화한다. 오버로딩된 부모 클래스의 생성자가 없어도, 부모 클래스의 기본 생성자는 호출해야한다. 눈에 보이지 않지만 컴파일러가 super(); 를 자식 클래스 생성자 첫 줄에 자동으로 추가해 준다.

// 부모 클래스 Car 생성자
public Car(String model, String color, double price) {
this.model = model;
this.color = color;
this.price = price;
}

// 자식 클래스 SportsCar 생성자
public SportsCar(String model, String color, double price, String engine) {
// this.engine = engine; // 오류 발생
super(model, color, price);
this.engine = engine;
}

아래 예제를 보자. SportsCar 자식 클래스 내 SportsCar() 메서드는 model , color , price 매개변수를 받는 super() 메서드를 포함한다. 이 메서드는 곧 입력되는 매개변수값을 부모 클래스인 Car 의 매개변수값으로 저장한다는 의미가된다. 이 값들이 저장되었는지 확인하기위해 자식 클래스의 객체를 통해 .getModel() , .getColor() , .getPrice() 메서드를 호출할 수 있다. 결과값은 SportsCar() 메서드를 호출할 때 활용한 매개변수값을 출력할 것이다.

// Car.java
public class Car {
String company; // 자동차 회사
String model; // 자동차 모델
String color; // 자동차 색상
double price; // 자동차 가격
double speed; // 자동차 속도 , km/h
char gear = 'P'; // 기어의 상태, P,R,N,D
boolean lights; // 자동차 조명의 상태
public Car(String model, String color, double price) {
this.model = model;
this.color = color;
this.price = price;
}
public String getModel() {
return model;
}
public String getColor() {
return color;
}
public double getPrice() {
return price;
}
public double gasPedal(double kmh, char type) {
changeGear(type);
speed = kmh;
return speed;
}
public double brakePedal() {
speed = 0;
return speed;
}
public char changeGear(char type) {
gear = type;
return gear;
}
public boolean onOffLights() {
lights = !lights;
return lights;
}
public void horn() {
System.out.println("빵빵");
}
}

// SportsCar.java
ppublic class SportsCar extends Car{
String engine;
public SportsCar(String model, String color, double price, String engine) {
// this.engine = engine; // 오류 발생
super(model, color, price);
this.engine = engine;
}
public void booster() {
System.out.println("엔진 " + engine + " 부앙~\n");
}
@Override
public double brakePedal() {
speed = 100;
System.out.println("스포츠카에 브레이크란 없다");
return speed;
}
@Override
public void horn() {
booster();
}
}

// Main.java
public class Main {
public static void main(String[] args) {
// 자식 클래스 스포츠카 객체를 생성합니다.
SportsCar sportsCar = new SportsCar("Lamborghini", "Red", 400000000, "V12");
sportsCar.brakePedal();
sportsCar.horn();
// 자식 클래스의 생성자를 통해 부모 클래스의 생성자가 호출되어 필드값이 초기화 되었는지 확인
System.out.println("sportsCar.getModel() = " + sportsCar.getModel()); // Lamborghini
System.out.println("sportsCar.getColor() = " + sportsCar.getColor()); // Red
System.out.println("sportsCar.getPrice() = " + sportsCar.getPrice()); // 4.0E8
}
}

// 출력 값
스포츠카에 브레이크란 없다
엔진 V12 부앙~

sportsCar.getModel() = Lamborghini
sportsCar.getColor() = Red
sportsCar.getPrice() = 4.0E8

다형성

참조변수의 타입변환

{부모타입 변수} = {자식타입객체}; 로 선언하면 자동으로 부모타입으로 변환이 일어난다. 자식 객체는 부모 객체의 멤버를 상속받으므로 부모와 동일하게 취급될 수 있다. 예를 들어 포유류 클래스를 상속받은 고래 클래스가 있다면 포유류 고래 = 고래 객체; 가 성립된다. 그 이유는 예를 들어 고래 객체는 포유류의 특징인 모유수유 행위를 가지기 때문이다. 주의할 것은, 부모타입 변수로 자식 객체의 멤버에 접근할 때는 부모 클래스에 선언된 즉, 상속받은 멤버만 접근이 가능하다.

아래 예제를 보자 Mammal 부모 클래스에서 상속받는 자녀 클래스 Whale 이 있다. 이를 선언할 때 왼쪽 선언부에서는 Mammal 부모 클래스를 선언하는데, 오른쪽 생성부에서는 Whale 자녀 클래스를 생성하여 인스턴스화한다. 이는 자녀 클래스가 부모 클래스의 속성과 행위를 물려받았다는 가정하에 자식 타입의 객체를 부모타입으로 변환해 생성하는 것이다.

여기서 feeding() 메서드를 오버라이딩하여 부모 클래스의 동명 메서드와는 다른 출력을 하도록 한다. 메인 프로그램에서 Whale() 클래스를 객체화하고, feeding() 메서드를 호출하면 부모의 것이 아닌 자식 클래스의 오버라이딩된 멤버를 호출한다.

단, 주의할 것은 이렇게 선언할 경우, 자녀 클래스의 속성이나 행위에 직접 접근할 수 없다( mammal.swimming 은 에러를 출력한다.). 부모 클래스에는 정의되지 않은 것이기 때문이다.

// Mammal.java
class Mammal {
// 포유류는 새끼를 낳고 모유수유를 한다.
public void feeding() {
System.out.println("모유수유를 합니다.");
}
}

// Whale.java
class Whale extends Mammal {
// 고래는 포유류 이면서 바다에 살며 수영이 가능하다.
public void swimming() {
System.out.println("수영하다.");
}
@Override
public void feeding() {
System.out.println("고래는 모유수유를 합니다.");
}
}

// Main.java
public class Main {
public static void main(String[] args) {
// 고래는 포유류이기 때문에 포유류 타입으로 변환될 수 있습니다.
Mammal mammal = new Whale();
// 하지만 포유류 전부가 바다에 살고 수영을 할 수 있는 것은 아니기 때문에
// 수영 하다 메서드는 실행 불가
// 즉, 부모 클래스에 swimming이 선언되어있지 않아서 사용 불가능합니다.
// mammal.swimming(); // 오류 발생
// 반대로 모든 포유류가 전부 고래 처럼 수영이 가능한 것이 아니기 때문에 타입변환이 불가능합니다.
// 즉, 부모타입의 객체는 자식타입의 변수로 변환될 수 없습니다.
// Whale whale = new Mammal(); // 오류 발생
mammal.feeding();
}
}

// 결과 값
고래는 모유수유를 합니다.

강제 타입변환

부모타입객체는 자식타입 변수로 자동으로 타입변환되지 않는다. 이럴때는 자식타입 즉, 타입변환 연산자를 사용하여 강제로 자식타입으로 변환할 수 있다.

// 자식타입객체가 자동 타입변환된 부모타입의 변수
Mammal mammal = new Whale();
mammal.feeding();

// 자식객체 고래의 수영 기능을 사용하고 싶다면
// 다시 자식타입으로 강제 타입변환을 하면된다. (Whale)
Whale whale = (Whale) mammal;
whale.swimming();

그러나 무조건 강제 타입변환을 할 수 있는 것은 아니다. 자식타입객체가 부모타입으로 자동 타입변환된 후 다시 자식타입으로 변환될 때 만 강제 타입변환이 가능하다. 부모타입 변수로는 자식타입객체의 고유한 멤버를 사용할 수 없으므로 사용할 필요가 있을때는 강제 타입변환을 사용한다. 이렇게 자동 타입변환된 부모타입 변수가 아닌 부모객체를 자식타입의 변수로 강제 타입변환을 시도하면 오류가 발생한다.

Mammal newMammal = new Mammal();
Whale newWhale = (Whale) newMammal; // ClassCastException 발생

아래 코드예제를 보자. 이전에 보았던 MammalWhale 클래스는 그대로다. Mammal mammal = new Whale(); 을 통해 자식타입객체를 부모타입객체로 변환한다. 다음으로 mammal 인스턴스의 feeding() 을 호출한다. 이는 자녀 클래스 Whale 의 메서드이다.

이후 자식객체 고래의 swimming() 메서드를 활용하기위해 자식타입으로 강제 타입변환시킨다. 이후 swimming() 메서드를 출력가능하다.

// Mammal.java
class Mammal {
// 포유류는 새끼를 낳고 모유수유를 한다.
public void feeding() {
System.out.println("모유수유를 합니다.");
}
}

// Whale.java
class Whale extends Mammal {
// 고래는 포유류 이면서 바다에 살며 수영이 가능하다.
public void swimming() {
System.out.println("수영하다.");
}
@Override
public void feeding() {
System.out.println("고래는 모유수유를 합니다.");
}
}

// Main.java
public class Main {
public static void main(String[] args) {
// 자동 타입변환된 부모타입의 변수의 자식 객체
Mammal mammal = new Whale();
mammal.feeding();
// 자식객체 고래의 수영 기능을 사용하고 싶다면
// 다시 자식타입으로 강제 타입변환을 하면된다.
Whale whale = (Whale) mammal;
whale.swimming();
Mammal newMammal = new Mammal();
// Whale newWhale = (Whale) newMammal;
}
}

// 결과 값
고래는 모유수유를 합니다.
수영하다.

오늘의 느낌/생각/다짐

오늘은 3주차 과정 그 두 번째를 보고 있었다. 아침에는 1주차 시험이 있었고, 그전에 주어졌던 과제가 쉬운 편은 아니라고 느껴서 걱정을 많이했었다. 그러나 막상 뚜껑을 열어보니 과제보다 훨씬 쉬웠고, 대부분 30분도 안되어서 시험을 마쳤다. 나는 조금 넘기는 했지만.

그리고 나서 어제 다 보지 못한 3주차 과정을 다시 이어나갔다. 내 학습 속도가 그렇게 빠르지 않은 것인지, 나는 동영상이아닌 텍스트를 보면서 학습해 나갔는데, 이제 3분의 2 정도 왔다. 아마 주말에도 조금 시간을 투자해야 끝낼 수 있을 것 같다.

오늘은 1주차에 짜여진 조인원들과의 마지막 날이었다. 다들 조용한 성격이라 나이가 비교적 많은 내가 먼저 다가가야겠다는 생각이들었고, 나는 그들의 말을 이끌어내려고 많은 것을 물어보았다. 하지만 내가 너무 말을 많이 한 것같은 생각이들었다. 그래도 나름 잘 맞아 나와 짝지어진 분과는 과제를 성공적으로 제출했고, 다들 열심히 공부하려는 열의가 느껴졌다. 다들 관심 분야도 있어서 생각한대로 잘 갔으면 좋겠다고 바라게되었다. 물론 이 과정이 끝났을 때 생각해 볼 문제겠지만.

일주일 동안 달리느라 수고했다고 말해주고싶다. 아직 우리에게는 약 95일 정도의 여정이 남았다. 이제 한 고비 넘어선 것은 매우 작은 발걸음처럼 보일지라도 하루 하루 많은 것을 배우면서 모두 많이 성장했다고 생각한다. 그리고 앞으로도 쉽지 않겠지만 이 여정을 잘 견뎌내서 모두 표류하는 사람없이 잘 목적지에 도착할 수 있는 항해가 되었으면한다.

100일 후에 달라져있을 우리의 모습, 그리고 개발자가 될 준비가 된 우리의 모습을 상상하면서 내일부터 또다시 힘차게 달리자고 다짐해본다.

참조:

(1) https://pixabay.com/illustrations/security-security-concept-eyes-1163108/

--

--

배우는 자(Learner Of Life)
배우는 자(Learner Of Life)

Written by 배우는 자(Learner Of Life)

배움은 죽을 때까지 끝이 없다. 어쩌면 그게 우리가 살아있다는 증거일지도 모른다. 배움을 멈추는 순간, 혹은 배움의 기회가 더 이상 존재하지 않는 순간, 우리의 삶은 어쩌면 거기서 끝나는 것은 아닐까? 나는 배운다 그러므로 나는 존재한다. 배울 수 있음에, 그래서 살아 있음에 감사한다.

No responses yet