자바는 대표적인 객체지향 프로그래밍 언어이다.
그렇다면 객체지향 프로그래밍이 도대체 뭘까?
객체지향이 뭐길래 프로그램 변경이 쉽고 재사용성이 좋아지는걸까?
객체지향을 이해하기 위해서는 객체지향의 특성인 캡슐화, 상속, 추상화, 다형성을 알아야 한다.
계산기 공장을 만들면서 객체지향 특성을 살려서 프로그래밍을 구현해보자!
공통 요구사항 발생
구현 기능이 조금씩 다른 A, B, C 타입의 계산기를 만들려고 한다.
각각 CalculatorA
, CalculatorB
, CalculatorC
로 클래스를 만들어보자.
public class CalculatorA {
final int WIDTH;
final int HEIGHT;
public CalculatorA() {
WIDTH = 30;
HEIGHT = 30;
}
void display(int num1 , String operator, int num2, double result) {
System.out.println(num1 + " " + operator + " " + num2 + " = " + result);
}
double operate(int num1, String operator, int num2) {
double result = -99999;
switch (operator) {
case "+":
result = num1 + num2;
break;
case "-":
result = num1 - num2;
break;
case "*":
result = num1 * num2;
break;
case "/":
if (num2 > 0) {
result = num1 / num2;
}
break;
}
display(num1, operator, num2, result);
return result;
}
}
public class CalculatorB {
final int WIDTH;
final int HEIGHT;
public CalculatorB() {
WIDTH = 30;
HEIGHT = 30;
}
void display(int num1 , String operator, int num2, double result) {
System.out.println(num1 + " " + operator + " " + num2 + " = " + result);
}
double operate(int num1, String operator, int num2) {
double result = -99999;
switch (operator) {
case "*":
result = num1 * num2;
break;
case "/":
if (num2 > 0) {
result = num1 / num2;
}
break;
}
display(num1, operator, num2, result);
return result;
}
}
public class CalculatorC {
final int WIDTH;
final int HEIGHT;
public CalculatorC() {
WIDTH = 30;
HEIGHT = 30;
}
void display(int num1 , String operator, int num2, double result) {
System.out.println(num1 + " " + operator + " " + num2 + " = " + result);
}
double operate(int num1, String operator, int num2) {
double result = -99999;
switch (operator) {
case "+":
result = num1 + num2;
break;
case "-":
result = num1 - num2;
break;
case "*":
result = num1 * num2;
break;
case "/":
if (num2 > 0) {
result = num1 / num2;
}
break;
case "%":
result = num1 % num2;
break;
}
display(num1, operator, num2, result);
return result;
}
}
public class Factory {
public static void main(String[] args) {
CalculatorA calculatorA = new CalculatorA();
calculatorA.operate(100 ,"/",20);
System.out.println();
CalculatorB calculatorB = new CalculatorB();
calculatorB.operate(100 ,"-",20);
System.out.println();
CalculatorC calculatorC = new CalculatorC();
calculatorC.operate(100 ,"%",21);
}
}
A, B, C 타입에 맞게 각각의 클래스를 구현했다. 이 때, +, - 기능만 탑재한 D 타입의 계산기를 추가해달라는 요청이 발생했다. CalculatorA, B, C 를 만들었듯이 D에는 +, - 기능만 넣고 클래스를 구현하면 다음과 같다.
public class CalculatorD {
final int WIDTH;
final int HEIGHT;
public CalculatorD() {
WIDTH = 30;
HEIGHT = 30;
}
void display(int num1 , String operator, int num2, double result) {
System.out.println(num1 + " " + operator + " " + num2 + " = " + result);
}
double operate(int num1, String operator, int num2) {
double result = -99999;
switch (operator) {
case "+":
result = num1 + num2;
break;
case "-":
result = num1 - num2;
break;
}
display(num1, operator, num2, result);
return result;
}
}
공통 요구사항 발생
A, B, C, D 타입의 계산기를 만들어놨더니 또 다른 요구사항이 발생했다. 연산 결과 값이 -99999인 경우, “Error” 라는 메시지가 뜨도록 변경해달라고 한다.
CalculaterA 부터 CalculaterD 까지 일일이 출력 부분을 고치려고 하니 모든 클래스를 다 수정해야하는 번거로움이 생긴다. 어떻게 하면 좀 더 쉽게 변경사항을 수정할 수 있을까?
여기서 사용할 객체지향의 특성이 바로 “추상화”이다.
CalculaterA ~ CalculaterD 까지 모든 클래스에서 공통으로 사용하는 변수와 기능을 뽑아 추상클래스(Abstract class)로 만들어보자.
public abstract class AbstractCalculator {
final int WIDTH = 30; // 공통으로 사용하는 변수도 추상 클래스에 선언해준다.
final int HEIGHT = 30;
abstract double operate(int num1, String operator, int num2);
void display(int num1 , String operator, int num2, double result) {
if (result == -99999)
System.out.println("Error!");
else
System.out.println(num1 + " " + operator + " " + num2 + " = " + result);
}
}
모든 계산기 타입에서 공통으로 사용하는 기능은 연산(operate
)과 출력(display
)이다.operate
(연산)display
(출력)잠깐! 여기서 추상화의 편리함을 찾을 수 있다.
우리는 모든 타입의 계산기에서 result 값이 -99999 일 때, “Error!” 라는 메시지를 출력해주기로 했다.
추상 클래스가 없었다면, CalculatorA ~ CalculatorD까지 일일이 display
메서드를 수정해야 했을 것이다.
하지만, 추상 클래스를 만들어 놓은 덕분에 모든 클래스가 아닌 추상 클래스에 있는 display
메서드만 수정해주면 된다.
Calculator 클래스들은 AbstractCalculator
클래스를 상속받아서 추상메서드인 operate
를 타입에 맞게 기능 구현해주면 되고, display
함수는 호출만 해주면 된다.
이렇게 클래스가 상속을 받으면, 부모가 되는 클래스의 함수를 호출해서 바로 쓸 수 있다. 코드가 간결해진다!
public class CalculatorA extends AbstractCalculator {
double operate(int num1, String operator, int num2) {
double result = -99999;
switch (operator) {
case "+":
result = num1 + num2;
break;
case "-":
result = num1 - num2;
break;
case "*":
result = num1 * num2;
break;
case "/":
if (num2 > 0) {
result = num1 / num2;
}
break;
}
display(num1, operator, num2, result);
return result;
}
}
public class CalculatorB extends AbstractCalculator {
double operate(int num1, String operator, int num2) {
double result = -99999;
switch (operator) {
case "*":
result = num1 * num2;
break;
case "/":
if (num2 > 0) {
result = num1 / num2;
}
break;
}
display(num1, operator, num2, result);
return result;
}
}
public class CalculatorC extends AbstractCalculator {
double operate(int num1, String operator, int num2) {
double result = -99999;
switch (operator) {
case "+":
result = num1 + num2;
break;
case "-":
result = num1 - num2;
break;
case "*":
result = num1 * num2;
break;
case "/":
if (num2 > 0) {
result = num1 / num2;
}
break;
case "%":
result = num1 % num2;
break;
}
display(num1, operator, num2, result);
return result;
}
}
public class CalculatorD extends AbstractCalculator {
double operate(int num1, String operator, int num2) {
double result = -99999;
switch (operator) {
case "+":
result = num1 + num2;
break;
case "-":
result = num1 - num2;
break;
}
display(num1, operator, num2, result);
return result;
}
}
공통 요구사항 발생
또 다른 요구사항이 발생했다. 전원 기능과 스위치 기능이 필요하다. 추상 클래스를 만들어 기능들을 추상화하지 않았다면, 또 모든 타입의 계산기 클래스를 수정해야 했을 것이다.
하지만 우리는 추상 클래스를 만들었으므로 걱정할 필요 없다.
AbstractCalculator
에 새로 추가할 기능들을 만들어보자!
public abstract class AbstractCalculator {
final int WIDTH = 30;
final int HEIGHT = 30;
boolean power = false; // 전원 기능에 필요한 변수 power
int num1; // 스위칭 기능으로 연산할 때 필요한 변수 num1, num2, operator
int num2;
String operator;
abstract double operate(int num1, String operator, int num2);
// 스위치 기능
void switchingNumber() {
operate(num2, operator, num1);
}
// 전원 기능
void powerOnOff() {
power = !power;
System.out.println("power = " + power);
}
void display(int num1 , String operator, int num2, double result) {
if (result == -99999)
System.out.println("Error!");
else
System.out.println(num1 + " " + operator + " " + num2 + " = " + result);
}
}
switchingNumber
(스위치 기능)operate
함수를 호출할텐데, 함수 호출 시 넣을 매개변수가 필요하다. 그래서 이 추상 클래스에서 num1, num2, operator 변수를 선언해줘야 한다.powerOnOff
(전원 기능)이제 Calculator 클래스로 넘어가기 전에 생각해보자.
CalculatorA
~ CalculatorD
클래스에서는 operate
에서 계산을 위해 매개변수로 받은 num1, num2 와 operator 를, 부모인 AbstractCalculator
클래스의 변수 num1, num2, operator에 넣어줘야 한다.
public class CalculatorA extends AbstractCalculator {
double operate(int num1, String operator, int num2) {
**this.num1 = num1;
this.num2 = num2;
this.operator = operator;**
// ..
}
}
넣어주지 않는다면 어떤 문제가 발생할까?
public class Factory {
public static void main(String[] args) {
CalculatorA calculatorA = new CalculatorA();
calculatorA.operate(100, "*", 5);
System.out.println("switching");
calculatorA.switchingNumber(); // Error!
System.out.println();
}
}
public abstract class AbstractCalculator {
// ...
int num1;
int num2;
String operator;
void switchingNumber() {
operate(num2, operator, num1);
}
}
CalculatorA
에서 {this.num1 = num;}으로 부모 클래스에 있는 변수의 값을 정해주지 않았다면, switchingNumber()
를 호출했을 때, NullPointerException
이 발생한다. AbstractCalculator
에서 switchingNumber
를 실행시키려고 할 때, num1, num2, operator 값이 지정되지 않았기 때문이다.
그래서 CalculatorA
~ CalculatorD
클래스의 operate
메서드 내에서 {this.num1 = num;} 으로 값을 넣어줘야 한다.
그런데 잠깐, 계산기 B와 D는 스위치 기능이 제외되었다. 하지만 지금 상태라면 CalculatorB
와 CalculatorD
의 인스턴스에서 switchingNumber()
메서드를 호출하면 AbstractCalculator
에서 선언한 메서드가 실행되고만다.
switchingNumber()
메서드가 실행되지 않도록 @Override 를 통해 “Error!” 메시지가 나오도록 구현해주자.
public class CalculatorA extends AbstractCalculator {
double operate(int num1, String operator, int num2) {
this.num1 = num1;
this.num2 = num2;
this.operator = operator;
double result = -99999;
switch (operator) {
// ...
}
display(num1, operator, num2, result);
return result;
}
}
public class CalculatorB extends AbstractCalculator {
@Override
void switchingNumber() {
System.out.println("Error! - no switching function");
}
double operate(int num1, String operator, int num2) {
this.num1 = num1;
this.num2 = num2;
this.operator = operator;
double result = -99999;
switch (operator) {
// ...
}
display(num1, operator, num2, result);
return result;
}
}
public class CalculatorC extends AbstractCalculator {
double operate(int num1, String operator, int num2) {
this.num1 = num1;
this.num2 = num2;
this.operator = operator;
double result = -99999;
switch (operator) {
// ...
}
display(num1, operator, num2, result);
return result;
}
}
public class CalculatorD extends AbstractCalculator {
@Override
void switchingNumber() {
System.out.println("Error! - no switching function");
}
double operate(int num1, String operator, int num2) {
this.num1 = num1;
this.num2 = num2;
this.operator = operator;
double result = -99999;
switch (operator) {
// ...
}
display(num1, operator, num2, result);
return result;
}
}
여기까지 구현하고 보니, CalculatorB
와 CalculatorD
가 마음에 걸린다. 계산기 B, D 는 스위치 기능이 없는 계산기인데, switchingNumber()
라는 메서드가 아예 호출되지 않도록 만들 수는 없을까?
지금은 계산기 B, D 에서도 스위치 기능이 호출 가능하기 때문에, 스위치 기능이 호출되었을 경우 “Error!”라고 띄워주도록 코드를 작성했다. 그런데 이 코드는 불필요하지 않은가?
이 불필요함을 해소하기 위해 우리는 공통 기능을 모아 인터페이스를 만들 것이다.
A, B, C, D 타입의 모든 계산기에서 사용하는 기능 따로, A, C 타입 계산기에서만 사용하는 기능 따로 구현해 볼 것이다.
A, B, C, D 공통 기능 → CommonFunction
operate
: 연산 기능powerOnOff
: 전원 기능display
: 출력 기능A, C 공통 기능 -> SwitchingFunction
switchingNumber
: 스위칭 기능CommonFunction
public interface CommonFunction {
abstract double operate(int num1, String operator, int num2);
void powerOnOff();
void display(int num1 , String operator, int num2, double result);
}
SwitchingFunction
public interface SwitchingFunction {
void switchingNumber();
}
이제 인터페이스 구현과 추상 클래스 상속을 통해 계산기 클래스를 수정해보자!
- abstract class
AbstractCalculator
<- InterfaceCommonFunction
- class
CalculatorA
,CalculatorC
<- abstract classAbstractCalculator
- class
CalculatorA
,CalculatorC
<- InterfaceSwitchingFunction
- class
CalculatorB
,CalculatorD
<- abstract classAbstractCalculator
인터페이스에 있는 메서드는 public
이므로, 계산기 클래스에서 구현할 때도 public
으로 해줘야 됨을 기억하자!
public class CalculatorA extends AbstractCalculator implements SwitchingFunction {
public double operate(int num1, String operator, int num2) {
this.num1 = num1;
this.num2 = num2;
this.operator = operator;
double result = -99999;
switch (operator) {
// ...
}
display(num1, operator, num2, result);
return result;
}
@Override
public void switchingNumber() {
operate(num2, operator, num1);
}
}
public class CalculatorB extends AbstractCalculator {
public double operate(int num1, String operator, int num2) {
this.num1 = num1;
this.num2 = num2;
this.operator = operator;
double result = -99999;
switch (operator) {
// ...
}
display(num1, operator, num2, result);
return result;
}
}
public class CalculatorC extends AbstractCalculator implements SwitchingFunction {
public double operate(int num1, String operator, int num2) {
this.num1 = num1;
this.num2 = num2;
this.operator = operator;
double result = -99999;
switch (operator) {
// ...
}
display(num1, operator, num2, result);
return result;
}
@Override
public void switchingNumber() {
operate(num2, operator, num1);
}
}
public class CalculatorD extends AbstractCalculator {
public double operate(int num1, String operator, int num2) {
this.num1 = num1;
this.num2 = num2;
this.operator = operator;
double result = -99999;
switch (operator) {
// ...
}
display(num1, operator, num2, result);
return result;
}
}
public class Factory {
public static void main(String[] args) {
CalculatorA calculatorA = new CalculatorA();
calculatorA.operate(100, "*", 5);
calculatorA.operate(100, "/", 3);
calculatorA.display(100, "-", 5, 95);
calculatorA.powerOnOff();
System.out.println("switching");
calculatorA.switchingNumber();
System.out.println();
CalculatorB calculatorB = new CalculatorB();
calculatorB.operate(100, "*", 2);
calculatorB.operate(100, "-", 3);
calculatorB.display(100, "/", 5, 20);
calculatorB.powerOnOff();
// calculatorB.switchingNumber(); // error!
System.out.println();
CalculatorC calculatorC = new CalculatorC();
calculatorC.operate(100, "*", 3);
calculatorC.operate(100, "%", 5);
calculatorC.display(100, "+", 5, 0);
calculatorC.powerOnOff();
System.out.println("switching");
calculatorC.switchingNumber();
System.out.println();
CalculatorD calculatorD = new CalculatorD();
calculatorD.operate(100, "%", 3);
calculatorD.operate(100, "-", 5);
calculatorD.display(100, "+", 5, 105);
calculatorD.powerOnOff();
// calculatorD.switchingNumber(); // error!
}
}
공통 기능과 스위치 기능을 나누어 인터페이스를 구현했기 때문에, 이제 CalculatorB
와 CalculatorD
에서는 switchingNumber()
메서드를 부를 수 없다. 기능을 사용 안한다고 굳이 오버라이딩해서 에러 메시지를 띄우지 않아도 된다.
이번에는 추상 클래스 AbstractCalculator
에 있는 num1, num2, operator 변수를 외부에서 쉽게 바꾸지 못하도록 캡슐화할 것이다.
캡슐화를 위해 AbstractCalculator
클래스의 변수 num1, num2, operator 를 private
으로 만들자.
그리고 변수의 초기값을 만들어주는 메서드들을 만들 것이다.
setOperateVar
: 위 변수들 값을 초기화 하는 메서드 (값을 초기화할 때 꼭 생성자만 사용되는 것은 아니다.)
getNum1
, getNum2
, getOperator
: private 변수들의 값을 얻는 getter
setNum1
, setNum2
, setOperator
: private 변수들의 값을 설정하는 setter
AbstractCalculator
public abstract class AbstractCalculator implements CommonFunction {
final int WIDTH = 30;
final int HEIGHT = 30;
boolean power = false;
private int num1;
private int num2;
private String operator;
public AbstractCalculator() {
}
public AbstractCalculator(int num1, String operator, int num2) {
this.num1 = num1;
this.operator = operator;
this.num2 = num2;
}
public void setOperateVar(int num1, String operator, int num2) {
this.num1 = num1;
this.num2 = num2;
this.operator = operator;
}
public int getNum1() {
return num1;
}
public void setNum1(int num1) {
this.num1 = num1;
}
public int getNum2() {
return num2;
}
public void setNum2(int num2) {
this.num2 = num2;
}
public String getOperator() {
return operator;
}
public void setOperator(String operator) {
this.operator = operator;
}
public void powerOnOff() {
power = !power;
System.out.println("power = " + power);
}
public void display(int num1 , String operator, int num2, double result) {
if (result == -99999)
System.out.println("Error!");
else
System.out.println(num1 + " " + operator + " " + num2 + " = " + result);
}
}
이제 계산기 클래스에서도 {this.num1 = num1;} 이런 식으로 부모 클래스의 변수에 접근하지 못한다.
setOperateVar
을 이용해 모두 초기화해주자.
public class CalculatorA extends AbstractCalculator implements SwitchingFunction {
public CalculatorA() {
}
public CalculatorA(int num1, String operator, int num2) {
super(num1, operator, num2);
}
public double operate(int num1, String operator, int num2) {
// num1, num2, operate에 해당하는 동일한 변수들이 없기 때문에 super, this를 이용해 다양한 방식으로 접근이 가능하다.
// setOperateVar 사용해도 된다.
super.setNum1(num1);
this.setNum2(num2);
setOperator(operator);
double result = -99999;
switch (operator) {
case "+":
result = num1 + num2;
break;
case "-":
result = num1 - num2;
break;
case "*":
result = num1 * num2;
break;
case "/":
if (num2 > 0) {
result = num1 / num2;
}
break;
}
display(num1, operator, num2, result);
return result;
}
@Override
public void switchingNumber() {
operate(super.getNum2(), this.getOperator(), getNum1()); // 변수 가져올때도 getter 사용
}
}
public class CalculatorB extends AbstractCalculator {
public double operate(int num1, String operator, int num2) {
setOperateVar(num1, operator, num2); // 변수 값 초기화를 위해 setOperateVar 사용
double result = -99999;
// ...
}
display(num1, operator, num2, result);
return result;
}
}
public class CalculatorC extends AbstractCalculator implements SwitchingFunction {
public double operate(int num1, String operator, int num2) {
setOperateVar(num1, operator, num2);
double result = -99999;
// ...
}
@Override
public void switchingNumber() {
operate(super.getNum2(), this.getOperator(), getNum1());
}
}
public class CalculatorD extends AbstractCalculator {
public double operate(int num1, String operator, int num2) {
setOperateVar(num1, operator, num2);
double result = -99999;
// ...
}
}
이제 테스트 코드를 만들어보자. 매개변수 타입이 다음과 같은 코드를 만들 것이다.
CommonFunction
InterfaceSwitchingFunction
InterfaceAbstractCalculator
abstract class
// import는 개발한 경로에 맞춰서 작성
import javaprac.calculatorfactory.step3.change2private.AbstractCalculator;
import javaprac.calculatorfactory.step3.change2private.CommonFunction;
import javaprac.calculatorfactory.step3.change2private.SwitchingFunction;
public class Test {
void calculatorTestCommonFunction(CommonFunction calculator) {
calculator.operate(100, "*", 5);
calculator.display(100, "-", 5, 95);
calculator.powerOnOff();;
}
void calculatorTestSwitching(SwitchingFunction calculator) {
calculator.switchingNumber();
}
void calculatorTestAbstract(AbstractCalculator calculator) {
calculator.operate(100, "%", 5);
calculator.display(100, "+", 5, 105);
calculator.powerOnOff();
}
}
import javaprac.calculatorfactory.step3.change2private.*;
public class Factory {
public static void main(String[] args) {
// AbstractCalculator calculator; 이것도 사용 가능!
CommonFunction calculator;
calculator = new CalculatorA();
calculator.operate(100, "/", 5);
calculator.display(100, "/", 5, 20);
calculator.powerOnOff();
System.out.println("switching");
// calculator.switchingNumber(); // 다형성 원리에 의해서 해당 스위칭 기능은 추상화된 클래스 즉, 부모 클래스에 없는 기능이기 때문에 사용 불가능
System.out.println();
calculator = new CalculatorB();
calculator.operate(100, "%", 5);
calculator.display(100, "*", 5, 500);
calculator.powerOnOff();
System.out.println();
calculator = new CalculatorC();
calculator.operate(100, "/", 5);
calculator.display(100, "%", 5, 0);
calculator.powerOnOff();
System.out.println();
calculator = new CalculatorD();
calculator.operate(100, "/", 5);
calculator.display(100, "-", 5, 95);
calculator.powerOnOff();
System.out.println();
// SwitchingFunction function2 = new CalculatorB(); // Error! CalculatorB 는 SwitchingFunction 인터페이스를 구현하지 않음
SwitchingFunction function = new CalculatorA(30, "*", 50);
function.switchingNumber();
System.out.println();
// 매개변수 다형성
Test test = new Test();
test.calculatorTestCommonFunction(new CalculatorB());
System.out.println();
test.calculatorTestSwitching(new CalculatorA(30, "*", 50));
System.out.println();
test.calculatorTestAbstract(new CalculatorC());
}
}
CommonFunction
타입의 변수 calculator를 만들었지만, AbstractCalculator
타입으로 선언해도 된다.operate
메서드는 AbstractCalculator
에 없지만 CommonFunction
인터페이스에 정의되어 있었고 이를 AbstractCalculator
가 구현했기 때문에 사용 가능하다SwitchingFunction
을 사용하면 operate
메서드를 사용할 수 없기 때문에 테스트를 위해 만들어둔 생성자를 사용해서 강제로 num1, num2, operator 에 값을 넣는다