프로그래밍 일기 — 객체를 지향하는 Java 3
Java, 너에게로 언제든지 돌아오겠어
#Java, #다형성, #인터페이스, #추상클래스, #상속, #default, #static, #타입변환
알고리즘 공부 4일차가 이어졌다. 알고리즘 문제가 35개 주어졌는데 난이도가 올라갈수록 그것을 풀어내는데 시간이 많이 걸린다는 것을 느낀다. 이제 6개 정도 남았지만, 이 것들을 남은 2일간 다 풀어낼 수 있을지 모르겠다.
그 와중에 Java의 개념을 최대한 다음 Spring과정전까지 반드시 시작해야한다는 조언을 들어서, 시간을 내서 Java의 개념을 다시 학습하기로했다. 그리고 마침 알고리즘 문제에 지쳐있던 사이에, 잠시 숨을 고르기 위해서 Java 기본기 학습으로 다시 되돌아왔다. Java를 배우는 것은 Spring이라는 세상에서 안정적으로 운전을 하기위한 기본 운전면허를 획득하는 것과 다를바 없기 때문이다.
다형성
여러가지 형태를 가질 수 있는 능력을 말한다. 예를 들어서, 자동차의 핸들을 바꾸면 핸들링이 부드러워지거나 바퀴를 바꾸면 승차감이 개선되는 것과 같다. 소프트웨어도 마찬가지로 구성부를 교체했을 때 실행 성능 및 결과물이 바뀌기도한다. 참조변수 타입변환을 이용하면 다형성을 구현할 수 있다.
아래와 같이 {부모타이어 변수} = {자식타이어객체};
를 선언하면 자동 타입변환된 변수를 사용하여 각각의 자식타이어 객체에 재정의 된 메서드를 통해 다양한 승차감을 가진 자동차를 생성가능하다. 매개변수에도 다형성이 적용된다. Car
생성자에서 매개변수의 타입이 부모 타이어이기 때문에, 자식 타이어 객체들을 매개값으로 전달할 수 있다.
Tire tire = new HankookTire("HANKOOK");
Tire tire = new KiaTire("KIA");
// 타이어 객체 교체 (자식타이어 객체를 부모타입으로 변환)
public Car(Tire tire) {
this.tire = tire;
}
...
// 자식 타이어의 객체를 매개변수로 전달
Car car1 = new Car(new KiaTire("KIA"));
Car car2 = new Car(new HankookTire("HANKOOK"));
또한 반환타입에도 다형성이 적용되는데, 반환타입이 부모타이어이면, 자식 타이어 객체를 반환 값으로 지정할 수 있다. 자동 타입변환된 반환값인 자식 타이어 객체를 강제로 타입변환 할 수도 있다.
// 변수를 확인하는 매서드
Tire getHankookTire() {
return new HankookTire("HANKOOK");
}
Tire getKiaTire() {
return new KiaTire("KIA");
}
...
// 자식타이어 객체를 부모 타이어 객체로 강제타입 변환
Tire hankookTire = car1.getHankookTire();
KiaTire kiaTire = (KiaTire) car2.getKiaTire();
아래는 위 언급된 내용을 포괄적으로 설명하기위한 예제이다.
// Car 클래스
public class Car {
Tire tire;
public Car(KiaTire tire) {
this.tire = tire;
}
public Car(HankookTire hankook) {
}
Tire getHankookTire() {
return new HankookTire("HANKOOK");
}
KiaTire getKiaTire() {
return new KiaTire("KIA");
}
}
// Tire 클래스
public class Tire {
String company; // 타이어 회사
public Tire(String company) {
this.company = company;
}
public Tire() {
}
public void rideComfort() {
System.out.println(company + " 타이어 승차감은?");
}
}
// KiaTire 클래스
public class KiaTire extends Tire{
public KiaTire(String company) {
super(company);
}
@Override
public void rideComfort() {
System.out.println(super.company + " 타이어 승차감은 " + 60);
}
}
// HankookTire 클래스
public class HankookTire extends Tire {
public HankookTire(String company) {
super(company);
}
@Override
public void rideComfort() {
System.out.println(super.company + " 타이어 승차감은 " + 100);
}
}
// Main.java
public class Main {
public static void main(String[] args) {
// 매개변수 다형성 확인!
Car car1 = new Car(new KiaTire("KIA"));
Car car2 = new Car(new HankookTire("HANKOOK"));
// 반환타입 다형성 확인!
Tire hankookTire = car1.getHankookTire();
KiaTire kiaTire = (KiaTire) car2.getKiaTire();
// 오버라이딩된 메서드 호출
car1.tire.rideComfort(); // KIA 타이어 승차감은 60
car2.tire.rideComfort(); // HANKOOK 타이어 승차감은 100
}
}
instanceof
instance of
란 다형성 기능을 통해 해당 클래스 객체의 원래 클래스명을 체크하는 것이 필요한데, 이때 사용할 수 있는 명령어가 instance of
이다.
- 이 명령어를 통해 해당 객체가 내가 의도하는 클래스의 객체인지 확인 가능하다.
{대상 객체} instance of {클래스 이름}
과 같은 형태로 사용하며, 응답값은boolean
이다.
// instance of 예제
class Parent { }
class Child extends Parent { }
class Brother extends Parent { }
public class Main {
public static void main(String[] args) {
Parent pc = new Child(); // 다형성 허용 (자식 -> 부모)
Parent p = new Parent();
System.out.println(p instanceof Object); // true 출력
System.out.println(p instanceof Parent); // true 출력
System.out.println(p instanceof Child); // false 출력
Parent c = new Child();
System.out.println(c instanceof Object); // true 출력
System.out.println(c instanceof Parent); // true 출력
System.out.println(c instanceof Child); // true 출력
}
}
추상 클래스
클래스가 설계도라면 추상 클래스는 미완성된 설계도이다. abstract 키워드를 사용하여 추상 클래스를 선언가능하며, 추상 클래스는 추상 메서드를 포함할 수 있다. 추상 클래스는 자식 클래스에 상속되어 자식 클래스에 의해서만 완성가능하다. 추상 클래스는 여러개의 자식 클래스들에서 공통적인 필드나 메서드를 추출해서 만들 수 있다.
// 추상 클래스 문법
public abstract class 추상클래스명 {
}
추상 메서드
아직 구현되지 않은 미완성된 메서드이다. 역시 abstract 키워드를 통해 선언가능하며, 일반적인 메서드와는 다르게 블록{}이 없다. 정의만 할 뿐, 실행 내용은 가지고 있지 않다.
추상 클래스의 상속
추상 메서드는 extends 키워드를 통해 클래스에서 상속된다. 상속받은 클래스에서 추상 클래스의 추상 메서드는 반드시 오버라이딩 되어야한다.
// 추상클래스 상속 문법
public class 클래스명 extends 추상클래스명 {
@Override
public 리턴타입 메서드이름(매개변수, ...) {
// 실행문
}
}
추상 클래스 사용법
클래스의 공통적인 필드와 메서드를 추출한다. 아래 예제를 보면 Car
클래스의 모든 자녀 클래스들이 horn()
이라는 메서드를 공유하지만, 서로 다른 행위를 수행한다. 리턴하는 문자열값이 다르기 때문이다.
// Car 클래스
public abstract class Car {
String company; // 자동차 회사 : GENESIS
String color; // 자동차 색상
double speed; // 자동차 속도 , km/h
public double gasPedal(double kmh) {
speed = kmh;
return speed;
}
public double brakePedal() {
speed = 0;
return speed;
}
public abstract void horn();
}
// BenzCar 클래스
public class BenzCar extends Car {
@Override
public void horn() {
System.out.println("Benz 빵빵");
}
}
// AudiCar 클래스
public class AudiCar extends Car {
@Override
public void horn() {
System.out.println("Audi 빵빵");
}
}
// ZenesisCar 클래스
public class ZenesisCar extends Car {
@Override
public void horn() {
System.out.println("Zenesis 빵빵");
}
}
// Main.java
public class Main {
public static void main(String[] args) {
Car car1 = new BenzCar();
car1.horn();
System.out.println();
Car car2 = new AudiCar();
car2.horn();
System.out.println();
Car car3 = new ZenesisCar();
car3.horn();
}
}
// 출력값
Benz 빵빵
Audi 빵빵
Zenesis 빵빵
인터페이스
인터페이스는 두 객체를 연결하는 역할을 한다. 예를 들어 사람, 삼성티비, LG티비가 존재한다고 가정한다면, 사람 객체는 멀티 리모컨 인터페이스를 통해 삼성티비 객체의 채널을 변경한다. 삼성티비가 아니라 엘지티비로 객체가 교체된다고해도 채널을 변경할 수 있는 것이다.
상속 관계가 없는 다른 클래스들이 서로 동일한 행위 즉, 메서드를 구현해야할 때 인터페이스는 구현 클래스들의 동일한 사용 방법과 행위를 보장한다. 이러한 특징은 인터페이스에 다형성을 적용할 수 있게한다. 인터페이스는 클래스처럼 public
및 default
접근 제어자를 지정가능하다.
// 인터페이스 문법
public interface 인터페이스명 {
}
인터페이스의 멤버
인터페이스의 모든 멤버의 특징은 아래와 같다.
- 멤버변수:
public static final
이어야한다(생략 가능). - 멤버메서드:
public abstract
이어야한다(생략 가능). - 생략되는 제어자는 컴파일러가 자동으로 추가한다.
public interface 인터페이스명 {
public static final char A = 'A';
static char B = 'B';
final char C = 'C';
char D = 'D';
void turnOn(); // public abstract void turnOn();
}
인터페이스는 추상 클래스처럼 직접 인스턴스를 생성할 수 없으므로, 클래스에 구현되어 생성된다. implements
키워드를 사용하여 인터페이스를 구현할 수 있다. 인터페이스의 추상 메서드는 구현될 때 반드시 오버라이딩이 되어야한다. 만약 인터페이스의 추상 메서드를 일부만 구현한다면, 해당 클래스를 추상 클래스로 변경한다.
public class 클래스명 implements 인터페이스명 {
// 추상 메서드 오버라이딩
@Override
public 리턴타입 메서드이름(매개변수, ...) {
// 실행문 (여기에 입력)
}
}
인터페이스 상속
인터페이스간 상속은 implements
가 아닌 extends
키워드를 사용한다. 클래스와 다르게 다중 상속이 가능하다. 아래 예제에서 인터페이스 C는 아무것도 선언 되어있지 않지만 인터페이스 A, B를 다중 상속 받았으므로 추상 메서드 a()
와 b()
를 갖는 상태이다. 따라서 Main
클래스에서 인터페이스 C가 구현되면, a, b 추상 메서드가 오버라이딩된다.
public class Main implements C {
@Override
public void a() {
System.out.println("A");
}
@Override
public void b() {
System.out.println("B");
}
}
interface A {
void a();
}
interface B {
void b();
}
interface C extends A, B { }
인터페이스의 구현은 상속과 함께 사용될수도 있다.
// 인터페이스와 상속의 혼용
public class Main extends D implements C {
@Override
public void a() {
System.out.println("A");
}
@Override
public void b() {
System.out.println("B");
}
// 클래스 D에서 d()메서드 상속하여 활용
@Override
void d() {
super.d();
}
public static void main(String[] args) {
Main main = new Main();
main.a();
main.b();
main.d();
}
}
interface A {
void a();
}
interface B {
void b();
}
interface C extends A, B {
}
class D {
void d() {
System.out.println("D");
}
}
// 출력값
>>>
A
B
D
디폴트 메서드
추상 메서드의 기본적인 구현을 제공하는 메서드이다. 메서드 앞에 default
키워드를 붙이고 {}
가 있어야한다. default 메서드는 기본적인 접근 제어자가 public
이며 생략이 가능하다.
추상 메서드가 아니므로, 인터페이스의 구현체들에서 필수로 재정의할 필요는 없다.
// default 메서드 예제
public class Main implements A {
@Override
public void a() {
System.out.println("A")
}
public static void main(String[] args) {
Main main = new Main();
main.a();
// 디폴트 메서드 재정의 없이 바로 사용가능합니다.
main.aa();
}
}
interface A {
void a();
default void aa() {
System.out.println("AA");
}
}
// 출력값
>>>
A
AA
static 메서드
인터페이스에서 선언가능한 메서드로써, static
특성을 그대로 받아 객체 없이 호출이 가능하다. 선언 방법 및 호출 방법은 클래스의 static
메서드와 동일하다. 접근 제어자 생략시, 컴파일러가 public
을 추가한다.
// static 메서드 예제
public class Main implements A {
@Override
public void a() {
System.out.println("A");
}
public static void main(String[] args) {
Main main = new Main();
main.a();
main.aa();
System.out.println();
// static 메서드 aaa() 호출
A.aaa();
}
}
interface A {
void a();
default void aa() {
System.out.println("AA");
}
static void aaa() {
System.out.println("static method");
}
}
인터페이스 타입변환
인터페이스의 타입 변환은 어떻게 할까? 먼저 자동 타입변환에 대해 알아보자.
자동 타입변환
{인터페이스 변수} = 구현객체;
의 문법을 쓰면 자동으로 타입 변환이 일어난다.
public class Main {
public static void main(String[] args) {
// A 인터페이스에 구현체 B 대입
A a1 = new B();
// A 인터페이스에 구편체 B를 상속받은 C 대입
A a2 = new C();
}
}
interface A { }
class B implements A {}
class C extends B {}
강제타입 변환
Casting과 같은 문법으로, {구현객체타입 변수} = {구현객체타입} 인터페이스변수;
의 문법을 통해 변환한다.
// 강제타입 변환 예제
public class Main {
public static void main(String[] args) {
// A 인터페이스에 구현체 B 대입
A a1 = new B();
a1.a();
// a1.b(); // 불가능
System.out.println("\nB 강제 타입변환");
B b = (B) a1;
b.a();
b.b(); // 강제 타입변환으로 사용 가능
System.out.println();
// A 인터페이스에 구편체 B를 상속받은 C 대입
A a2 = new C();
a2.a();
//a2.b(); // 불가능
//a2.c(); // 불가능
System.out.println("\nC 강제 타입변환");
C c = (C) a2;
c.a();
c.b(); // 강제 타입변환으로 사용 가능
c.c(); // 강제 타입변환으로 사용 가능
}
}
interface A {
void a();
}
class B implements A {
@Override
public void a() {
System.out.println("B.a()");
}
public void b() {
System.out.println("B.b()");
}
}
class C extends B {
public void c() {
System.out.println("C.c()");
}
}
// 출력값
B.a()
B 강제 타입변환
B.a()
B.b()
B.a()
C 강제 타입변환
B.a()
B.b()
C.c()
인터페이스의 다형성
인터페이스를 활용하여 다형성을 구현할 수도 있다. 예를 들어 {멀티리모컨인터페이스 변수} = {TV 구현객체};
를 선언해 자동 타입변환된 인터페이스 변수를 사용하여 TV구현객체의 기능을 조작할 수 있다. TV구현객체를 교체해도 멀티리모컨인터페이스 변수는 전혀 수정작업 없이 그대로 기능을 호출한다.
다형성이 “여러가지 형태를 가질 수 있는 능력"이라고 정의를 했다. 사용 방법은 같지만, 다양한 특징과 결과를 가질 수 있는 것을 말하는 것이다. 즉, 멀티리모컨으로 티비를 사용하는 방법은 같지만, 어떤 TV구현객체가 대입되었느냐에 따라 실행결과가 다르게 나오는 것이고, 이를 통해 다형성이 적용되었음을 확인할 수 있다.
// LG TV 구현체를 조작
MultiRemoteController mrc = new LgTv("LG");
mrc.turnOnOff();
mrc.volumeUp();
// 조작 대상을 Samsung TV로 교체
mrc = new SamsungTv("Samsung");
mrc.turnOnOff();
mrc.channelUp();
인터페이스도 마찬가지로 매개변수와 반환타입에서 다형성이 적용될 수 있다. 아래 예제는 반환타입에는 인터페이스, 매개변수에는 추상클래스로 다형성이 적용되어있다.
// 매개변수와 반환타입 다형성 확인 메서드
default MultiRemoteController getTV(Tv tv) {
if(tv instanceof SamsungTv) {
return (SamsungTv) tv;
} else if(tv instanceof LgTv){
return (LgTv) tv;
} else {
throw new NullPointerException("일치하는 Tv 없음");
}
}
위 다형성 개념을 전체적으로 담은 예제를 아래에서 확인할 수 있다.
// Tv 클래스
public abstract class Tv {
private String company; // 티비 회사
private int channel = 1; // 현재 채널 상태
private int volume = 0; // 현재 볼륨 상태
private boolean power = false; // 현재 전원 상태
public Tv(String company) {
this.company = company;
}
public void displayPower(String company, boolean power) {
if(power) {
System.out.println(company + " Tv 전원이 켜졌습니다.");
} else {
System.out.println(company + " Tv 전원이 종료되었습니다.");
}
}
public void displayChannel(int channel) {
System.out.println("현재 채널은 " + channel);
}
public void displayVolume(int volume) {
System.out.println("현재 볼륨은 " + volume);
}
public String getCompany() {
return company;
}
public int getChannel() {
return channel;
}
public int getVolume() {
return volume;
}
public boolean isPower() {
return power;
}
public void setChannel(int channel) {
this.channel = Math.max(channel, 0);
}
public void setVolume(int volume) {
this.volume = Math.max(volume, 0);
}
public void setPower(boolean power) {
this.power = power;
}
}
// SamsungTv 클래스
public class SamsungTv extends Tv implements MultiRemoteController{
public SamsungTv(String company) {
super(company);
}
@Override
public void turnOnOff() {
setPower(!isPower());
displayPower(getCompany(), isPower());
}
@Override
public void channelUp() {
setChannel(getChannel() + 1);
displayChannel(getChannel());
}
@Override
public void channelDown() {
setChannel(getChannel() - 1);
displayChannel(getChannel());
}
@Override
public void volumeUp() {
setVolume(getVolume() + 1);
displayVolume(getVolume());
}
@Override
public void volumeDown() {
setVolume(getVolume() - 1);
displayVolume(getVolume());
}
}
// LgTv 클래스
public class LgTv extends Tv implements MultiRemoteController {
public LgTv(String company) {
super(company);
}
@Override
public void turnOnOff() {
setPower(!isPower());
displayPower(getCompany(), isPower());
}
@Override
public void channelUp() {
setChannel(getChannel() + 1);
displayChannel(getChannel());
}
@Override
public void channelDown() {
setChannel(getChannel() - 1);
displayChannel(getChannel());
}
@Override
public void volumeUp() {
setVolume(getVolume() + 1);
displayVolume(getVolume());
}
@Override
public void volumeDown() {
setVolume(getVolume() - 1);
displayVolume(getVolume());
}
}
// MultiRemoteController
public interface MultiRemoteController {
void turnOnOff();
void channelUp();
void channelDown();
void volumeUp();
void volumeDown();
// 매개변수와 반환타입 다형성 확인 메서드
default MultiRemoteController getTV(Tv tv) {
if(tv instanceof SamsungTv) {
return (SamsungTv) tv;
} else if(tv instanceof LgTv){
return (LgTv) tv;
} else {
throw new NullPointerException("일치하는 Tv 없음");
}
}
}
// Main.java
public class Main {
public static void main(String[] args) {
// LG TV 구현체를 조작
MultiRemoteController mrc = new LgTv("LG");
mrc.turnOnOff();
mrc.volumeUp();
mrc.channelDown();
mrc.channelUp();
mrc.turnOnOff();
// 조작 대상을 Samsung TV로 교체
System.out.println("\n<Samsung TV로 교체>");
mrc = new SamsungTv("Samsung");
mrc.turnOnOff();
mrc.channelUp();
mrc.volumeDown();
mrc.volumeUp();
mrc.turnOnOff();
// 매개변수, 반환타입 다형성 체크
System.out.println("\n<매개변수, 반환타입 다형성 체크>");
MultiRemoteController samsung = mrc.getTV(new SamsungTv("Samsung"));
samsung.turnOnOff();
SamsungTv samsungTv = (SamsungTv) samsung;
samsungTv.turnOnOff();
System.out.println();
MultiRemoteController lg = mrc.getTV(new LgTv("LG"));
lg.turnOnOff();
LgTv lgTv = (LgTv) lg;
lgTv.turnOnOff();
}
}
// 출력값
LG Tv 전원이 켜졌습니다.
현재 볼륨은 1
현재 채널은 0
현재 채널은 1
LG Tv 전원이 종료되었습니다.
<Samsung TV로 교체>
Samsung Tv 전원이 켜졌습니다.
현재 채널은 2
현재 볼륨은 0
현재 볼륨은 1
Samsung Tv 전원이 종료되었습니다.
<매개변수, 반환타입 다형성 체크>
Samsung Tv 전원이 켜졌습니다.
Samsung Tv 전원이 종료되었습니다.
LG Tv 전원이 켜졌습니다.
LG Tv 전원이 종료되었습니다.
오늘의 느낌/다짐/생각
오늘도 알고리즘 문제를 풀었다. 알고리즘 문제를 푸는데 하루 반 정도의 시간을 쓰고, 나머지 반은 Java 객체지향 개념을 다시 학습하기 위해 Java로 돌아왔다. 오늘 알고리즘 문제를 풀면서 느낀 것은, 내가 쓰고자하는 데이터 타입, 특히 기본형 변수의 범위를 제대로 알고 사용해야겠다는 생각이들었다. long
타입의 변수 연산을 하면서 int
를 같이 활용하니, long
에서 int
로 변환이 안되었다. int
보다는 long
의 범위가 현저히 더 크기 때문이다. 그래서 이 부분에 대해서 유의하여 내가 반환해야하는 변수의 타입이 무엇인지 정확하게 인지한 후에 사용을 해야겠다는 생각을했다.
또한 Java개념을 학습하면서 오늘 동료의 도움을 받았다. 추상 클래스에 대해 정확하게 이해가가지않아 그의 도움을 구했는데, 생각보다 많은 것을 알려주었다. 덕분에 추상 클래스가 “같은 코드를 반복해서 쓰는 것을 줄여주기 위한 방법"이라는 것을 알 수 있었다. 추상 클래스의 멤버로 선언해 놓으면, 그것을 상속한 자녀 메서드를 선언할 때, 굳이 추상 클래스의 멤버를 선언할 필요없이, 원하는 멤버만 @Override
키워드를 입혀 의도한 행위를 하도록 수정해주면 된다는 것을 알았다. 팀이 짜여지는게 정말 감사하게 느껴졌다. 내가 모르는 것을 물어볼 수도 있고, 내가 도움을 줄 수 있는 사람에게 손을 내밀 수 있어서다. 사람은 다른 사람을 가르쳐줄 때 가장 많이 배운다고했다. 내가 도움을 받을 때도 많이 배우지만, 내가 도움을 줄 때 더 많이 배운다. 내가 알려줌으로써, 내가 그 것을 확실히 설명할 수 있는지 테스트할 수 있고, 다른 사람을 이해시킴으로써 뿌듯함까지 느낄 수 있다. 아인슈타인이 말했던 것처럼, “내가 무엇을 다른 사람에게 설명할 수 있지 않다면, 그것을 충분히 이해했다고 말할 수 없다.”
나도 최대한 도움이 필요한 사람에게 도움을 주고 싶다. 동시에 내가 도움이 필요할때도 적극적으로 도움을 청하는 사람이 되고싶다. 혼자서 너무 오래 끙끙앓면서 문제를 해결하지 못하는 것보다는, 나 보다 더 잘 아는 사람에게 적극적으로 도움을 구하는 것이 더 문제를 빨리 해결하고 더 효율적으로 배우는 방법이다. 또한 다른 사람에게 도움을 받고, 도움을 주면서 서로 더 많이 교류하게되고, 관계도 만들어 갈 수 있는 것 같다. 개발자역시 혼자서 일하는 사람은 이제 지구상에 없다고 할 수 있는 만큼, 같이 일하고 싶은사람이 되려면 관계를 만들 수 있는 사람이 되어야한다고 생각한다. 그 사람이 나보다 어리다해도 내가 확실히 배울 수 있는게 있다면 배워야한다. 빨리 변화하는 세상에서 적응하기 위해서는 내가 항상 틀릴 수 있고, 다른 사람이 옳을 수 있다는 생각을 가져야한다고 생각한다. 그 사람의 배경이 어떻든, 배울 점이 있다면 기꺼이 가르침을 청하는 사람이 되고싶다.
참조:
(1) https://pixabay.com/vectors/boomerang-aboriginal-weapons-25796/