• 추상 클래스, 인터페이스, 상속, 합성, 프록시/데코레이터 패턴 그리고 Spring AOP
    DEV 2024. 1. 20. 12:08

    decoration

    추상클래스와 인터페이스는 어떻게 다른가? 왜 상속보다는 합성(조합)을 사용하는가?

    데코레이터 패턴, 프록시 패턴은 이들과 어떤 관계가 있는가?

    추상 클래스

    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(...);
      }
    }

    추상 클래스와 인터페이스

    • 추상 클래스는 코드 재사용에 초점, 인터페이스는 디커플링에 초점
    • 추상 클래스는 상향식 설계방식
      - 하위 클래스의 코드를 반복한 다음 상위 클래스를 추상화
    • 인터페이스는 하향식 설계방식
      - 인터페이스를 먼저 설계 후 다음 특정 구현을 고려

    easy

    상속보다 합성

    객체지향 프로그래밍의 고전적인 설계원칙 → 합성이 상속보다 낫다.

    상속의 문제는 무엇인가?

    • 상속은 객체지향 프로그래밍의 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)로 추상 클래스를 세분화

    새 클래스 상속 구조 1

    • 상속 관계가 3단계로 늘어남. 그래도 아직까지는 받아들일 수 있을 것 같기도 하다.
    • 하지만 만약 노래를 부를 수 있는가에 대해서도 구분이 필요하다면..

    새 클래스 상속 구조 2

    • 이제 더 이상은 안될 것 같은 느낌.. 알을 낳는가에 대해서도 고려가 필요하다면 조합의 수는 기하급수적으로 증가
    • 클래스의 상속 계층은 점점 더 깊어지고, 상속 관계는 더 복잡해질 것이다.

    합성은 어떻게 문제를 해결하는가?

    • 위 상속의 문제는 합성(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(); //위임
        //기능 추가
      }
      ...
    }
    • 원본 클래스와 연관 없는 기능(로깅, 인증, 모니터링..)을 추가하면 → 프록시 패턴
    • 원본 클래스와 관련 있거나 향상된 기능을 추가하면 → 데코레이터 패턴

    proxy pattern
    proxy pattern

    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
go.