2022. 8. 27. 15:44ㆍ학습일지
'실용주의 프로그래머'를 읽던 중, 결합도에 관한 이야기를 일게 되었다. 그중에서 인상 깊었던 점은, '상속은 결합을 늘린다'라는 항목을 보게 되었다.
처음엔 조금 의아했지만, 몇몇 국내 자바 기본서에는 확장(extends)을 상속이라고 정의하는 경우가 종종 있다는 점을 생각해보면, 클래스의 확장(extends)과 인터페이스의 주입(implements)을 구분해서 생각하는 게 좋을 것 같다.
따라서, 같은 기능을 하는 코드를 추상 클래스와 인터페이스를 활용한 예제를 통해, '상속세' 개념을 활용해보면 좋을 것 같다.
0. 예제 준비
추상 클래스나, 인터페이스로 추상화할 기능은 다음과 같다.
- 인삿말을 빌드하는 메서드
- 인사말을 출력하는 메서드
이 두 기능을 2가지 방식으로 추상화하여, 그 차이를 파악해보자.
1. 추상 클래스 + 템플릿 패턴
이 코드는 추상 클래스에 템플릿 패턴을 적용하여, 구현 클래스가 해당 추상 클래스를 확장하여 buildPhraseAndPrint()라는
메서드를 사용하면, 인삿말 문자열 빌드와 출력을 같이 할 수 있는 방식으로 설계해 보았다.
AbstractHelloA
public abstract class AbstractHelloA {
abstract void sayHello();
abstract void niceTo();
abstract void meetYou();
abstract void printPhrase();
public void buildPhraseAndPrint() {
sayHello();
niceTo();
meetYou();
printPhrase();
}
}
HelloA
public class HelloA extends AbstractHelloA {
private String phrase;
public HelloA() {
this.phrase = "";
}
@Override
protected void sayHello() {
phrase += "Hello from " + this.getClass().getSimpleName();
}
@Override
protected void niceTo() {
phrase += " and say nice to";
}
@Override
protected void meetYou() {
phrase += " meet you.";
}
@Override
protected void printPhrase() {
System.out.println(phrase);
}
}
2. 인터페이스
Buildable
public interface Buildable {
void build();
String getPhrase();
}
Printable
public interface Printable {
void print(String phrase);
}
HelloB
public class HelloB implements Buildable, Printable {
private String phrase;
public HelloB() {
this.phrase = "";
}
public void sayHello() {
phrase += "Hello from " + this.getClass().getSimpleName();
}
public void niceTo() {
phrase += " and say nice to";
}
public void meetYou() {
phrase += " meet you.";
}
@Override
public void build() {
sayHello();
niceTo();
meetYou();
}
@Override
public String getPhrase() {
return phrase;
}
@Override
public void print(String phrase) {
System.out.println(phrase);
}
}
Client
public class Client {
public static void main(String[] args) {
/**
* abstract
*/
AbstractHelloA helloA = new HelloA();
((HelloA) helloA).buildPhraseAndPrint();
/**
* interface
*/
HelloB helloB = new HelloB();
helloB.print();
}
}
위의 client 코드를 보면, 추상 클래스의 경우, 기존 클래스의 제약을 그대로 따르기 때문에, 메인 클래스로 서브 클래스의 메소드를 사용하려면, 다음과 같이 '명시적인 형 변환'이 필요하지만, 인터페이스의 경우, 형 변환을 적용할 필요가 없다.
그런데, 과연 인터페이스를 활용할 경우 얻을 수 있는 이점이 과연 '형 변환' 부분만 있을까?
Solid 원칙 중에 ISP에 대해 잠시 살펴보면서, 좀 더 예제 코드를 정리해보자.
3. ISP
Interface segregation principle (인터페이스 분리 원칙)란, 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다는 원칙입니다.
ISP를 적용할 수 있는 경우를 만들기 위해, 좀 더 다양한 상황을 부여해보자. 현재 인터페이스를 구현한 클래스는 오직 '영어'로만 인사를 한다. 그런데, '한국말'로 인사하고 싶은 경우에는 어떻게 할까? 그리고, 개발자가 출력하는 메서드를 외부 라이브러리나 프레임워크를 활용하고 싶다면 어떻게 해야 할까? 즉, 인사말 빌드와 출력을 따로 써야 하는 상황이 왔을 경우, 위의 두 가지 코드 중 하나를 선택하게 되면 어떤 일이 발생할까?
- 한국말의 인사말만 구현하고 싶을 경우, 출력 메서드를 억지로 구현해야 한다.
- 반대로 출력만 구현하고 싶을 경우, 인사말 메서드를 억지로 구현해야 한다.
- 위의 두 가지 경우 중 하나를 구현하고, 출력이나 인사말 빌드를 외부 라이브러리로 구현하고 싶을 때는 또 다른 클래스 혹은 다른 메서드의 네이밍으로 같은 기능의 메서드를 또 구현하는 이상한 상황이 발생한다.
위와 같은 상황을 해결해야 할 경우, 추상 클래스는 한계가 명확하다. 왜냐면, 자바는 오직 하나의 클래스만 확장할 수밖에 없는 제약이 있기 때문이다.
그러나, 인터페이스의 경우, 하나의 클래스에 여러 개의 인터페이스를 주입할 수 있다.
3.1. ISP 예제
약간의 재미(?)를 위해, 기존의 인터페이스를 가지고 뭔가 다른 예제를 시도해보자.
기존의 HelloA
와 HelloB
는 그냥 영어로 된, 인사말을 빌드하고 프린트하는 기능밖에 없다. 이번에는 고양이의 인삿말과 무미건조한 미국인의 인삿말을 같이 써야 하는 상황을 가정하고 진행해보자. 그럼 이제 todo 리스트를 짜 보고 예제를 진행해보자.
TODO list
- Printer와 Buildable을 따로 구현.
- 미국인과 고양이 Builder 구현.
- Printer 구현.
- 미국인과 고양이 Builder 실행.
1. Builder
EnglishHelloBuilder
public class EnglishHelloBuilder implements Buildable {
private String phrase;
public EnglishHelloBuilder() {
this.phrase = "";
build();
}
public void sayHello() {
phrase += "Hello from " + this.getClass().getSimpleName();
}
public void niceTo() {
phrase += " and say nice to";
}
public void meetYou() {
phrase += " meet you.";
}
@Override
public void build() {
sayHello();
niceTo();
meetYou();
}
@Override
public String getPhrase() {
return phrase;
}
}
KittyHelloBuilder
public class KittyHelloBuilder implements Buildable {
private final String name;
private String phrase = "";
public KittyHelloBuilder(String name) {
this.name = name;
build();
}
private void sayHello() {
phrase += "안냥, \"" + name + "\" 이다냥.";
}
private void niceTo() {
phrase += " 반갑다냥.";
}
private void meetYou() {
phrase += " 또보자냥.";
}
@Override
public void build() {
sayHello();
niceTo();
meetYou();
}
@Override
public String getPhrase() {
return phrase;
}
}
2. Printer
Printer
public class Printer implements Printable {
private String phrase;
public Printer(String phrase) {
this.phrase = phrase;
}
@Override
public void print() {
System.out.println(phrase);
}
}
3. Client
Client
public class Client {
public static void main(String[] args) {
Buildable englishBuilder = new EnglishHelloBuilder();
Buildable kittyBuilder = new KittyHelloBuilder("김고양");
Printer printer = new Printer();
printer.print(kittyBuilder.getPhrase());
printer.print(englishBuilder.getPhrase());
}
}
인터페이스를 활용하여, 기능을 세분화하여, 구현하면, 위처럼, print 기능만을 가진 printer 하나로, 다양한 종류의 Builder를 출력할 수 있다.
지금은 간단한 기능을 예시로 들어 티가 많이 안 날 수 있지만, 스프링을 활용했을 때, 추상 클래스를 확장해서 사용하는 경우보다, 기능을 세분화해서 인터페이스를 활용했을 때, 훨씬 더 유연한 코드를 구성할 수 있는 경우가 많을 수 있다.
물론 이 부분은 개발자의 역량과 기능 개발의 요구사항의 성격에 따라, 달라질 수 있는 부분이니, 이 글을 읽으시는 분은 꼭 비판적인 시각으로 받아주시길 바란다.
마지막으로, '상속세'에 대한 실용주의 프로그래머의 격언을 차용하며, 해당 게시글을 마무리하도록 하겠다.
당신이 원한 것은 바나나 하나였지만, 당신이 받은 것은 바나나를 들고 있는 고릴라와 정글 전체다.
해당 코드는 https://github.com/wanniDev/inheritance_tax 에서 살펴보실 수 있습니다.
'학습일지' 카테고리의 다른 글
02. Spring security Features 요약 : Protection Against Exploits (0) | 2022.11.04 |
---|---|
01. Spring security Features 요약 : Password Storage (0) | 2022.11.04 |
RabbitMQ - @RabbitListener를 사용할 때 주의할 점 (0) | 2022.07.17 |
'클린코드'에서 이야기하는 깨끗한 코드 (0) | 2022.06.17 |
RabbitMQ -1- AMQP 0-9-1 프로토콜 (0) | 2022.05.19 |