-
추상 클래스, 인터페이스, 상속, 합성, 프록시/데코레이터 패턴 그리고 Spring AOPDEV 2024. 1. 20. 12:08
추상클래스와 인터페이스는 어떻게 다른가? 왜 상속보다는 합성(조합)을 사용하는가?
데코레이터 패턴, 프록시 패턴은 이들과 어떤 관계가 있는가?
추상 클래스
public abstract class Logger { private String name; private boolean enabled; private Level minPermittedLevel; //생성자 public void log(Level level, String msg) { ... doLog(level, message); } protected abstract void doLog(Level level, String msg); } public class FileLogger extends Logger { private Writer fileWriter; //생성자 @Override public void doLog(Level level, String msg) { fileWriter.write(...); } } public class MsgQueueLogger extends Logger { private MessageQueueClient msgQueueClient; //생성자 @Override public void doLog(Level level, String msg) { msgQueueClient.send(...); } }
- Logger = 추상클래스
- FileLogger, MessageQueueLogger → Logger class 상속
- FileLogger, MessageQueueLogger는 Logger class의 name, enabled, minPermittedLevel 속성, log() 메서드 재사용하지만, 로그 출력 방식이 다르기 때문에 Logger의 doLog()를 재정의
추상 클래스 특성
- 추상 클래스는 인스턴스화할 수 없고, 상속만 가능, 객체 생성 시 컴파일 에러 발생
Logger logger = new Logger(..);
- 추상 클래스는 속성, 메서드 포함, 메서드를 구현할 수 있고, 구현하지 않을 수도 있다. 코드 구현이 포함되지 않은 메서드 → 추상 메서드
- 하위 클래스는 추상 클래스를 상속할 때 추상 클래스의 모든 추상 메서드를 실제로 구현해야 한다. doLog()
추상 클래스의 의미
- 추상 클래스는 코드 재사용을 위해 사용, 여러 하위 클래스가 추상 클래스에 정의된 속성, 메서드를 상속 가능하므로 하위클래스에서 동일한 코드를 재작성하지 않아도 된다.
- name, enabled, minPermittedLevel 속성, log() 메서드 - 상속을 통해 코드 재사용의 목적을 달성할 수는 있지만, 굳이 추상 클래스일 필요는 없다.
- 추상 클래스를 사용하지 않아도 상속을 통한 코드 재사용은 가능한데, 이거 외에 다른 어떤 의미가 있을까?
public class Logger { private String name; private boolean enabled; private Level minPermittedLevel; //생성자 protected boolean isLoggable() { ... } } public class FileLogger extends Logger { private Writer fileWriter; //생성자 public void log(Level level, String msg) { if(!isLoggable()) return; fileWriter.write(...); } } public class MsgQueueLogger extends Logger { private MessageQueueClient msgQueueClient; //생성자 public void log(Level level, String msg) { if(!isLoggable()) return; msgQueueClient.send(...); } }
- 코드를 재사용할 수는 있지만 다형성을 사용할 수 없게 된다.
- Logger에 log() 메서드가 정의 x → 컴파일 에러 발생
Logger looger = new FileLogger(....); logger.log(Level.ERROR, "test");
- Logger에 빈 log()를 추가하고, 하위 클래스가 Logger의 log()를 재정의 하게 할 수도 있지만..
- 추상 클래스가 있는데 굳이 다른 방식으로 해야 할 이유는 없다.
- Logger의 log()가 비어있는 채로 정의된 이유를 알기 어려울 수 있다.
- 새로운 Logger의 하위 클래스 생성 시 log()를 재구현 하는 것을 잊어버릴 가능성
- Logger가 인스턴스화될 가능성, 비어있는 Logger의 log()를 호출할 가능성
pubilc class Logger { ... public void log(){} } public class FileLogger extends Logger { ... @Override public void log(Level level, String msg) { if(!isLoggable()) return; filewritre.write(...); } }
추상 클래스와 인터페이스
- 추상 클래스는 코드 재사용에 초점, 인터페이스는 디커플링에 초점
- 추상 클래스는 상향식 설계방식
- 하위 클래스의 코드를 반복한 다음 상위 클래스를 추상화 - 인터페이스는 하향식 설계방식
- 인터페이스를 먼저 설계 후 다음 특정 구현을 고려
상속보다 합성
객체지향 프로그래밍의 고전적인 설계원칙 → 합성이 상속보다 낫다.
상속의 문제는 무엇인가?
- 상속은 객체지향 프로그래밍의 4대 특성(캡슐화, 추상화, 상속, 다형성) 중 하나, 코드의 재사용 문제 해결
- 하지만 상속 단계가 너무 깊고 복잡해지면 코드의 유지관리 가능성에 영향을 미친다
- 상속을 사용하는 것이 옳은지에 대해선 많은 논쟁이 있기도 하며, 사람들은 상속을 드물게 사용하거나 전혀 사용하지 않아야 하는 안티 패턴으로 간주하기도 한다.
public class AbstractBird { ... public void fly() {...} } public class Ostrich extends AbstractBird { ... public void fly() { throw new UnSupportedException("I can't fly."); } }
- AbstractBird 클래스를 추상 클래스로 새의 개념을 정의, 참새, 비둘기, 까치등 새의 하위 클래스는 모두 추상 클래스에 상속
- 대부분의 새가 날 수 있기 때문에, AbstractBird 추상 클래스에 fly() 메서드를 정의했지만,
- 타조 같은 예외가 발생한다면.. 위 코드처럼 fly() 메서드를 재정의하고 UnSupportedException을 발생시킬 수도 있겠지만
- 펭귄, 도도 같은 새들이 추가된다면..
- 이를 해결하기 위해 AbstractBird 클래스에서 날 수 있는 새(AbstractFlyableBird), 날지 못하는 새(AbstractUnFlyableBird)로 추상 클래스를 세분화
- 상속 관계가 3단계로 늘어남. 그래도 아직까지는 받아들일 수 있을 것 같기도 하다.
- 하지만 만약 노래를 부를 수 있는가에 대해서도 구분이 필요하다면..
- 이제 더 이상은 안될 것 같은 느낌.. 알을 낳는가에 대해서도 고려가 필요하다면 조합의 수는 기하급수적으로 증가
- 클래스의 상속 계층은 점점 더 깊어지고, 상속 관계는 더 복잡해질 것이다.
합성은 어떻게 문제를 해결하는가?
- 위 상속의 문제는 합성(composition), 인터페이스, 위임(delegation)이라는 세 가지 기술적 방법을 통해 해결
- '날아다닌다'는 기능에 대해 Flyable 인터페이스
- '노래 부른다'는 기능에 대해 Tweetagle 인터페이스
- '알을 낳는다'는 기능에 대해 EggLayable 인터페이스 정의
public interface Flyable { void fly(); } public interface Tweetable { void tweet(); } public interface EggLayable { void layEgg(); } public class Ostrich implements Tweetable, EggLayable { .... @Override public void tweet() {...} @Override public void layEgg() {...} } public class Sparrow implements Flyable, Tweetable, EggLayable { ... @Override public void fly() {...} @Override public void tweet() {...} @Override public void layEgg() {...} }
- 하지만 인터페이스는 메서드를 선언할 뿐 구현하진 않기 때문에, 알을 낳는 모든 새는 layEgg() 메서드를 구현해야 함
- 이는 또다시 코드 중복의 문제 발생
- 이러한 코드 중복 문제를 해결하려면 3가지 인터페이스를 구현하는 클래스 추가
- fly() 구현 → FlyAbility class
- tweet() 구현 → TweetAbility class
- lagEgg() 구현 → EggLayAbility class - 합성, 위임을 통해 코드 중복을 제거
public interface Flyable { void fly(); } public class FlyAbility implements Flyable { @Override public void fly() {...} } public interface Tweetable { void tweet(); } public class TweetAbility implements Tweetable { @Override public void tweet() {...} } public interface EggLayable { void layEgg(); } public class EggLayAbility implements EggLayable { @Override public void layEgg() {...} } public class Ostrich implements Tweetable, EggLayable { private TweetAbility tweetAbility = new TweetAbility(); //합성 private EggLayAbility eggLayAbility = new EggLayAbility(); //합성 ... @Override public void tweet() { tweetAbility.tweet(); //위임 } @Override public void layEgg() { eggLayAbility.layEgg(); //위임 } }
프록시 패턴, 데코레이터 패턴
- 만약 독수리 클래스를 추가할 때 tweet() 메서드에 기능을 추가하고 싶다면
public class Eagle implements Tweetable, EggLayable { private TweetAbility tweetAbility = new TweetAbility(); //합성 private EggLayAbility eggLayAbility = new EggLayAbility(); //합성 ... @Override public void tweet() { //기능 추가 tweetAbility.tweet(); //위임 //기능 추가 } ... }
- 원본 클래스와 연관 없는 기능(로깅, 인증, 모니터링..)을 추가하면 → 프록시 패턴
- 원본 클래스와 관련 있거나 향상된 기능을 추가하면 → 데코레이터 패턴
Spring AOP
- 이러한 프록시패턴을 스프링에서 동적으로 만들어 주는 것 → Spring framework AOP
- JDK Dynamic Proxy
- java.lang.reflect.Proxy class
- 대상이 하나 이상의 인터페이스를 implements 하고 있을 경우 - CGLIB Proxy
- CGLIB (Code Generation Library) third-party libary
- 대상이 인터페이스를 implement 하지 않을 경우
언제나 합성은 상속보다 나은가?
- 합성을 더 많이 사용하고, 상속을 덜 사용하도록 권장하지만, 합성이 언제나 완벽하게 동작하는 것은 아니고 상속이 언제가 쓸모없는 것도 아니다.
- 더 많은 합성, 더 적은 상속을 권장하는 이유는 오랫동안 많은 프로그래머가 상속을 남용했기 때문
- 다양한 상황에서 상속이나 합성을 적절하게 사용하는 것이 좋다는 너무나 당연한 이야기로 마무리..🫤
728x90'DEV' 카테고리의 다른 글
블록체인과 해쉬 (0) 2024.07.21 미술과 객체지향 프로그래밍의 추상 (0) 2024.01.27 심리적 안전과 지식공유, 강백호와 중경삼림 (0) 2024.01.12 Dependency-Structure-Matrix & DDD & Layered-Architecture (0) 2024.01.08 빈약한 도메인 모델, 풍부한 도메인 모델, DDD 그리고 캡슐화 (0) 2023.12.31