[Java] 상속과 인터페이스
시작하며…
다음 과제까지 어떤걸 할까 고민하다가 자바의 스트림, 상속이나 인터페이스같은 걸 공부해보기로했다.
클린하게 코드를 작성하려면 좀더 자바의 기능들을 사용해야 복잡한 코드를 자바의 기능하나로 대체 할 수 있다는 생각에,
다음과제에서 사용할 수 있는 여러가지 기능들을 미리 익히기로 했다.
먼저 공부 할 것은 상속과 인터페이스이다.
공부 했던 내용들이지만 또 막상 실제 구현에서 사용하려니 확 와닿지가 않아서, 다시 복습해보려한다.
상속
상속이란?
상속은 객체지향 프로그래밍의 중요한 특징으로, 우리가 아는 상속의 의미와 동일하게 자식에게 물려주는걸 말한다.
예를들어 B클래스가 A클래스를 상속받으면, A클래스의 멤버변수와 메서드를 사용할 수 있게 되는 것이다.
객체 지향프로그램의 장점은 유지보수가 간편한 것인데, 이것의 기반이 되는 기술이 바로 상속이다.
이전에 공부했던 내용
상속이 어떤 기능인지는 공부했지만, 정확히 어떤 상황에서 상속을 통해 클린코드를 작성할 수 있는지 생각해보지 않았다.
이런 상속이라는 기능을 어떻게 사용하면 코드를 줄일 수 있는지에 대한 예제가 넥스트 스텝 클린코드에 있어서 한번 따라해보았다.
커피와 차라는 두개의 객체를 각각 Coffee, Tea로 구현해보자.
상속 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Coffee {
void prepareRecipe() {
boilWater();
brewCoffeeGrinds();
pourInCup();
addSugarAndMilk();
}
public void boilWater() {
System.out.println("물을 끓인다.");
}
public void brewCoffeeGrinds() {
System.out.println("필터를 활용해 커피를 내린다.");
}
public void pourInCup() {
System.out.println("컵에 붓는다.");
}
public void addSugarAndMilk() {
System.out.println("설탕과 우유를 추가한다.");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Tea {
void prepareRecipe() {
boilWater();
steepTeaBag();
pourInCup();
addLemon();
}
public void boilWater() {
System.out.println("물을 끓인다.");
}
public void steepTeaBag() {
System.out.println("티백을 담근다.");
}
public void pourInCup() {
System.out.println("컵에 붓는다.");
}
public void addLemon() {
System.out.println("레몬을 추가한다.");
}
}
이 클래스들은 prepareRecipe() 메서드를 사용하면 각각의 레시피대로 음료를 만들기 시작한다.
지금 코드를 보면 중복된 메서드가 두개 있는데, boilWater()와 PourInCup() 두 메서드는 이름과 기능이 모두 동일하다.
바로 이런경우에 상속을 사용해서 중복을 제거해 줄 수 있다.
두 음료 모두 카페인을 가진 음료이므로, CaffeineBeverage라는 클래스를 만들어, 두 클래스에 상속해주도록 해보자.
1
2
3
4
5
6
7
8
9
public class CaffeineBeverage {
protected void boilWater() {
System.out.println("물을 끓인다.");
}
protected void pourInCup() {
System.out.println("컵에 붓는다.");
}
}
두 메서드를 묶어 CaffeieBeverage라는 클래스로 만들어 주었으니, 두 클래스의 중복된 메서드를 지우고
새로만든 클래스를 상속해주자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Coffee extends CaffeineBeverage {
void prepareRecipe() {
boilWater();
brewCoffeeGrinds();
pourInCup();
addSugarAndMilk();
}
public void brewCoffeeGrinds() {
System.out.println("필터를 활용해 커피를 내린다.");
}
public void addSugarAndMilk() {
System.out.println("설탕과 우유를 추가한다.");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Tea extends CaffeineBeverage {
void prepareRecipe() {
boilWater();
steepTeaBag();
pourInCup();
addLemon();
}
public void steepTeaBag() {
System.out.println("티백을 담근다.");
}
public void addLemon() {
System.out.println("레몬을 추가한다.");
}
}
두 클래스 모두 extends를 사용하여 새로만든 클래스를 상속해주었다.
이제 CaffeineBeverage라는 큰 범주안에 Coffee와 Tea가 존재하게 된다.
이 두클래스는 따로 작성을 하지 않아도 상위클래스의 멤버변수와 메서드를 사용할 수 있다.
그럼 이제 또 중복되는 부분을 제거해주어야 하는데, prepareRecipe()가 두 클래스모두 비슷한 기능을 갖고있지만
두 메서드가 서로 이름이 달라 prepareRecipe()를 CaffeineBeverage로 옮겨 줄 수는 없다.
이럴때 필요한게 바로 추상화인데, 두기능이 비슷한 역할을 한다면 기능을 조금 더 추상화 해서 같은 이름의 메서드로 관리해주면,
상속을 통해 중복을 더 제거 할 수 있다.
두 메서드는 각각 물을 끓인 후, brewCoffeeGrind()와 steepTeaBag()의 과정을 거치는데, 두 행위 모두 재료가 다를 뿐 물에 무언가를 우려낸다는 기능은 같다.
따라서 이 메서드의 이름을 brew()로 통일 해줄 수 있을 것이다.
또, 마지막 부분에 있는 addSugarAndMink()와 addLemon() 메서드도 위와 동일하게 재료만 다를 뿐 추가한다는 기능은 같으므로,
이 메서드는 addCondiments()로 통일 해 줄 수 있다.
이렇게 하면 이제 prepareRecipe() 메서드가 두 클래스 모두 완전히 동일해졌으므로, 이 메서드를 CaffeineBeverage에 상속해 줄 수 있게 되었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public abstract class CaffeineBeverage {
void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
protected void boilWater() {
System.out.println("물을 끓인다.");
}
protected void pourInCup() {
System.out.println("컵에 붓는다.");
}
}
하지만 이렇게 코드를 작성하면 오류가 발생한다. prepareRecipe() 안에 있는 brew()와 addCondiments()메서드를 이 클래스는 갖고있지 않기 떄문에
두 메서드가 오류를 일으킨다. 하지만 이 두 메서드는 이름이 같을 뿐 메서드의 기능은 다르기 떄문에 이 두 메서드를 추가 해줄 수도 없다.
이럴 때 사용하는 것이 바로 추상 클래스 이다.
CaffeineBeverage를 추상클래스로 선언해주게 되면, 몸통이 없는 메서드인 추상 메서드 를 만들어 줄 수 있다.
아래의 예시를 보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public abstract class CaffeineBeverage {
abstract void brew();
abstract void addCondiments();
void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
protected void boilWater() {
System.out.println("물을 끓인다.");
}
protected void pourInCup() {
System.out.println("컵에 붓는다.");
}
}
abstract를 클래스 선언부에 선언 해준뒤, abstract로 메서드를 선언해주면, 위와같이 이름만 있는 함수를 사용할 수 있다.
이렇게 abstract 메서드를 선언해주면, 상속을 받는 클래스는 무조건 abstract 메서드와 동일한 이름의 메서드를 구현해주어야 한다.
1
2
3
4
5
6
7
8
9
10
public class Coffee extends CaffeineBeverage{
public void brew() {
System.out.println("필터를 활용해 커피를 내린다.");
}
public void addCondiments() {
System.out.println("설탕과 우유를 추가한다.");
}
}
1
2
3
4
5
6
7
8
9
10
public class Tea extends CaffeineBeverage{
public void brew() {
System.out.println("티백을 담근다.");
}
public void addCondiments() {
System.out.println("레몬을 추가한다.");
}
}
이렇게 추상화 클래스를 사용하면, 중복을 최대한 제거 할 수 있다.
인터페이스
인터페이스란??
###이전에 공부 했던 내용
하나의 시스템을 구성하는 2개의 구성 요소 (하드웨어, 소프트웨어) 또는 2개의 시스템이 상호 작용할 수 있도록
접속되는 경계 (Boundary), 이 경계에서 상호 접속하기 위한 하드웨어, 소프트웨어, 조건, 규약 등을 포괄적으로 가리키는말이다.
말이 너무 어렵다…
예제를 통해 알아보자.
인터페이스를 이해하기 위한 예제
만약 우리가 MSSQL DB로 서비스를 시작했는데, 갑자기 사용자가 너무 늘어 비용을 감당할 수 없게 되었다고 가정해보자.
그래서 우리가 사용하는 DB를 무료 데이터베이스인 MySQL로 바꾸고 싶다.
이러한 경우, MSSQL에서만 작동하게 코드를 짜놨다면, MySQL로 DB를 변경하는데 많은 시간과 비용이 소모될 것이다.
또한 갑자기 많은 소스 코드가 변경 되면 서비스도 불안정해 질 수 밖에 없을 것이다.
따라서 이러한 문제를 사전에 방지 하도록 공통 부분을 추상화 해서 표준으로 만들어 놓은것을 “인터페이스” 라고 한다.
위의 경우는 DB연결이나 SQL쿼리 생성, 쿼리에 인자 전달과 같은 공통 기능들을 인터페이스로 만들어놓으면, 사용자가 이 인터페이스만 지켜준다면
추후에 DB가 변경되더라도 서로 호환이 되므로 소스 코드를 수정하지 않아도 된다.
위의 예시에는 다음과 같은 인터페이스들이 존재 할 것이다.
한마디로 어떠한 기능은 반드시 이런 모양의 클래스로 구현해야 한다고 강제해주는 가이드라인이자 설명서 라고 할 수 있다.
아래의 소스코드를 보자.
1
2
3
4
5
6
7
8
9
public interface Calc {
double PI = 3.14;
int ERROR = -99999999;
int add(int num1,int num2);
int substract(int num1, int num2);
int times(int num1, int num2);
int divide(int num1,int num2);
}
위 코드는 계산기 프로그램에 대한 interface이다. interface 예약어를 사용한 것을 볼 수 있는데,
이렇게 하면 예약어를 명시하지 않아도 컴파일 과정에서 자동으로 추상메서드로 변환되고, 변수는 상수가 된다.
interface를 따르는 클래스는 implements 예약어를 사용하는데, 아래와 같이 사용한다.
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
public class completeCalc implements Calc{
@Override
public int add(int num1, int num2) {
return num1 + num2;
}
@Override
public int substract(int num1, int num2) {
return num1 - num2;
}
@Override
public int times(int num1, int num2) {
return num1 * num2;
}
@Override
public int divide(int num1, int num2) {
if(num2 != 0)
return num1 / num2;
else
return Calc.ERROR;
}
public void showInfo(){
System.out.println("Calc 인터페이스 구현 완료");
}
}
implements를 사용해주고, interface에서 명시된 메서드 규칙에 따라서 메서드를 만들어주었다.
이렇게 하면 인터페이스 하나를 보고 모두 공통된 입출력을 갖는 메서드로 소스코드를 구성하게 되기 때문에, 호환성이 올라간다.
만약, 메서드를 전부 구현하지 않고 일부만 구현한다면 어떻게 될까?
1
2
3
4
5
6
7
8
9
10
11
public class Calculator implements Calc{
@Override
public int add(int num1, int num2) {
return num1 + num2;
}
@Override
public int substract(int num1, int num2) {
return num1 - num2;
}
}
이 코드는 오류를 출력하는데, 인터페이스를 따랐지만 모든 메서드를 구현하지 않아서이다.
전부 구현하지 않았음으로 추상클래스가 되어 예약어 abstract를 추가해주어야 오류를 출력하지 않는다.
1
2
3
4
5
6
7
8
9
10
11
public abstract class Calculator implements Calc{
@Override
public int add(int num1, int num2) {
return num1 + num2;
}
@Override
public int substract(int num1, int num2) {
return num1 - num2;
}
}
이러한 클래스는 다른 추상클래스와 마찬가지로 상속해서 사용할 수 있다.
이 클래스를 상속해서 위에서 구현한 CompleteCal 클래스를 만들어보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class CompleteCalc extends Calculator{
@Override
public int times(int num1, int num2) {
return num1 * num2;
}
@Override
public int divide(int num1, int num2) {
if(num2 != 0)
return num1 / num2;
else
return Calc.ERROR;
}
public void showInfo(){
System.out.println("Calc 인터페이스 구현 완료");
}
}
위의 클래스에서 인터페이스의 일부만 구현한 Calculator를 상속하고 나머지 부분을 구현해주면,
올바른 인터페이스 규칙을 따른것이므로 오류가 발생하지 않게된다.
만약 뭇시적 형변환을 사용하면 어떻게 될까?? 인터페이스 calc 형으로 CompletCalc를 형변환하여 선언해보자.
1
Calc calc = new CompleteCalc();
이렇게 되면 CompleteCalc는 Calc에 없는 메소드인 showInfo() 를 사용 할 수 없고 오직 인터페이스에서 선언한 메서드만 사용할 수 있게된다.
이것이 바로 인터페이스의 중요한 역할이다.
댓글남기기