[Java] 소프트웨어의 품격 CH.9 재사용성 발표자료

naneun·2022년 7월 13일
0

Java

목록 보기
6/7
post-thumbnail

재사용성

  • 특정 문제를 해결하는 구체적인 클래스 -> 다양한 문제에 맞게 일반화
  • 하지만 ❓

    • 필수적인 부분과 부차적인 부분의 경계가 모호
  • 정확성, 성능, 적시성, 기능적, 비기능적 요구사항
  • 그래서 ❓

    • 어디까지 일반화해야 하는가? 🤔

일반적인 프레임워크

  • 제네릭 컨테이너가 지원해야 할 필수 기능

    1. 제네릭 컨테이너는 타입이 V 인 속성을 갖는다. (ex. 수조의 경우 V = Double)

    2. 클라이언트는 제네릭 컨테이너를 서로 영구적으로 연결할 수 있다.

      public interface ContainerLike<V> {
          V get(); // 'getAmount'의 일반화
          void update(V value); // 'addWater'의 일반화
          // void connectTo(T other); // 'connectTo'의 일반화
      }
  • 문제점

    • update: 컨테이너 그룹에 따라 구체적인 로직이 달라지지만, API 만으로 확인이 불가능하다. 🤦‍♂️

    • 2진 메서드 문제

      • 2진 메서드 - 같은 클래스의 객체를 인자로 받는 메서드

        1. Object 클래스의 equals (객체의 상등성)
        2. Comparable 인터페이스의 compareTo 메서드 (객체 사이의 순서를 비교)
      • 자바와 같은 객체지향 언어의 타입 시스템은 주어진 클래스나 인터페이스의 모든 서브클래스가 특정 형태의 2진 메서드를 갖도록 강제할 수 없다.

        public interface Comparable {
            int compareTo(specificType other); // specificType -> Comparable 인터페이스를 구현하는 클래스
        }
    • 해결방법

      • equals의 파라미터를 Object로 선언하고 인자의 타입이 적절한지는 서브클래스가 런타임에 확인한다.

      • Comparable 인터페이스의 경우 제네릭으로 해결한다.

        • 인터페이스의 타입 파라미터로 T를 지정한다.

        • compareTo 메서드의 파라미터를 타입 T로 선언한다.

        • 타입 안전성은 높아지지만 다음과 같은 유명한 오.남용 사례도 가능하다.

          class Apple implements Comparable<Orange> { ... }
  • 그렇다면 'connectTo' 시그니처는

    • void connectTo(Object other): 타입 안전성 포기, 컴파일러 도움 X, 런타임 타입 확인 필요 (instanceof) 0점

    • void connectTo(ContainerLike<V> other): ContainerLike 타입은 보장할 수 있지만 V 타입은 강제할 수 없다 20점

    • 'Comparable' 인터페이스에 적용한 제네릭을 사용하자

      class Employee implements Comparable<Employee> { ... } // Comparable 의 올바른 사용법
      class Employee implements Comparable<Apple> { ... } // ?
      public interface ContainerLike<V, T extends Container<V, T>> { // 같은 인터페이스를 구현한 타입으로 강제하자!
          V get();
          void update(V val);
          void connectTo(T other);
      }
    • 주의: 같은 인터페이스를 구현한 같은 클래스이기를 강제할 수는 없다.

자바의 제네릭 구현

  • Attribute Interface - 속성 관리 API

    • 모든 컨테이너에서 성립해야 할 속성의 동작

      1. 그룹 요약 값(그룹을 대표하는 값)과 주어진 지역 값으로 새로운 요약 값을 계산할 수 있다.
      2. 두 그룹을 병합할 때 이전 두 그룹의 요약 값만 이용해 새로운 그룹의 요약 값을 얻을 수 있다.
      • 지역적인 값의 타입 V, 그룹 요약 값의 타입 S -> (ContainerSummary, PostSummary ...)

        • seed 메서드 - 새로운 제네릭 컨테이너는 그룹의 요약 값을 초기화할 수 있다

        • report 메서드 - 제네릭 컨테이너의 get 메서드에서는 요약 값을 V 타입의 지역적인 값으로 바꿔주는 메서드가 필요하다

        • update 메서드 - 제네릭 컨테이너의 update 메서드는 요약 값을 수정해야 한다

        • merge 메서드 - connectTo 메서드에서는 두 요약 값을 병합하는 메서드가 필요하다

          public interface Attribute<V, S> {
            S seed();                           // 요약 값을 초기화
            V report(S summary);                // 요약 값을 해석
            void update(S summary, V value);    // 주어진 값을 이용해 요약 값을 갱신
            S merge(S summary1, S summary2);    // 두 요약 값을 병합
          }
          // 속성 값 포함 X, 무상태
    • 제네릭 컨테이너의 메서드와 Attribute 인터페이스의 메서드 사이의 의존성

      제네릭 컨테이너의 메서드속성(Attribute)의 메서드
      생성자S seed()
      V getV report(S summary)
      void update(V val)void update(S summary, V value)
      void connectTo(T other)S merge(S summary1, S summary2)
    • Attribute 객체는 속성 값을 포함하지 않으므로 무상태라는 점에 주목하자

    • S 타입의 객체(그룹 요약)와 V 타입의 객체(캐싱된 지역 값)는 제네릭 컨테이너 안에 별도로 저장된다.

    • Attribute 인터페이스는 자바 8에서 스트림 연산 결과를 하나의 값으로 수집할 때 사용하는 인터페이스와 매우 비슷하다.

      • 일반적인 컬렉션을 하나의 가변적인 결과로 수집하는 과정을 생각해보자

      • 일종의 summary 객체를 초깃값으로 초기화한 후 컬렉션의 모든 요소에 대해 갱신할 것이다.

      • 모든 요소를 스캔한 후 다른 타입 즉 결과 타입으로 변환한다.

        Collection<V> collection = ...
        for (V value: collection) {             // 1. 요약 값을 초기화
            summary.update(value);               // 2. 주어진 값을 이용해 요약 값을 갱신
        }
        Result result = summary.toResult();     // 3. 요약 값을 결과로 변환
                                                 // ...
                                                 // 4. 병렬 컬렉터에 의한 병합 연산
      • 요약 값의 타입을 S, 최종 결과의 타입을 R이라고 하면 Collector 인터페이스에는 다음과 같은 메서드가 필요하다.

        S supply();                             // 1. 요약 
        void accumulate(S summary, V value);    // 2. 주어진 값을 이용해 요약 값을 갱신
        S combine(S summary1, S summary2);      // 3. 두 요약 값을 병합
        R finish(S summary);                    // 4. 요약 값을 결과로 반환
        인터페이스추상 메서드의 타입역할
        Supplier\<S>void -> S요약 초깃값을 제공
        BiConsumer<S, V>(S, V) -> void주어진 값을 이용해 요약 값을 갱신
        BinaryOperator\<S>(S, S) -> S두 요약 값을 병합
        Function<S, R>S -> R요약 값을 결과로 변환
        public interface Collector<V, S, R> {
            Supplier<S> supplier();           // 1. 요약 초깃값을 제공
            BiConsumer<S, V> accumulator();   // 2. 주어진 값을 이용해 요약 값을 갱싱
            BinaryOperator<S> combiner();     // 3. 두 요약 값을 병합
            Function<S, R> finisher();        // 4. 요약 값을 결과로 반환
        }
        Collector<String, StringBuilder, String> concatenator = 
            new Collector<>() { // 1. 외부 익명 클래스
                @Override
                public Supplier<StringBuilder> supplier() { // 2. 요약 초깃값을 제공
                    return new Supplier<>() { // 3. 첫 번째 내부 익명 클래스
                        @Override
                        public StringBuilder get() {
                            return new StringBuilder();
                        }
    • 메서드 참조

      • 이미 존재하는 메서드나 생성자를 함수형 인터페이스의 인스턴스로 변환하는 새로운 표현식 '::'으로 표기

      • 메서드 참조를 이용해 인스턴스 메서드를 적당한 인터페이스로 바꿀 수 있다.

        ToIntFunction<Object> hasher = Object::hashCode;
      • ToIntFunction는 다음과 같은 유일한 메서드를 포함하는 함수형 인터페이스

        int applyAsInt(T item)
      • 메서드 참조는 특정 객체의 메서드도 참조할 수 있다.

        Consumer<String> printer = System.out::println;
      • 정적 메서드와 생성자에도 메서드 참조를 적용할 수 있다.

      • StringBuilder 생성자의 참조를 이용해 제공자 Supplier 를 만들 수 있다.

      • 컴파일러는 생성자를 Supplier 타입으로 세심하게 감싼다.

        Collector<String, StrinbBuilder, String> concatenator = new Collector<>() {
            @Override
            public Supplier<StringBuilder> supplier() {
                return StringBuilder::new; // 생성자를 참조
            }
        }
    • Collector 클래스에서 제공하는 정적 메서드 of 를 이용하면 외부 익명 클래스도 필요 없이 다음과 같은 간단한 코드를 작성할 수 있다.

      Collector<String, StringBuilder, String> concatenator = 
          Collector.of(StringBuilder::new,        // 1. 제공자(생성자를 참조)
                        StringBuilder::append,    // 2. 갱신 함수
                        StringBuilder::append,    // 3. 병합 함수(또 다른 append 메서드)
                        StringBuilder::toString); // 4. 종결자
    • 메서드 참조에서는 메서드 시그니처는 지정할 수 없고 메서드 이름만 지정한다.

    • 컴파일러는 메서드 참조가 수행된 곳의 문맥을 바탕으로 메서드 시그니처를 추론하여 함수형 인터페이스를 지목해야 한다.

    • 예를 들어 위의 코드 조각에서 갱신 함수의 참조는 StringBuilder의 메서드 중에서 다음과 같은 메서드를 선택한다.

      public StringBuilder append(String s)
    • 문맥에 비춰보면 BiConsumer<StringBuilder, String> 이 필요하기 때문이다.

    • 여기서 append는 값을 리턴하지만 BiConsumer는 void를 리턴하는 불일치가 존재한다.

    • 값을 리턴하는 메서드를 호출한 후 리턴 값을 무시할 수 있듯이 컴파일러가 이를 알아서 처리한다.

      메서드목표 함수형 인터페이스
      시그니처SB append(String s)BiConsumer<SB, String>
      타입(SB, String) -> SB(SB, String) -> void
    • 값을 리턴하는 메서드의 참조를 void 함수형 인터페이스의 참조에 대입할 수 있다.

    • 이제 병합 함수의 메서드 참조를 살펴보자

    • 문맥에 따르면 BinaryOperator 즉 (this를 포함해) 두 StringBuilder를 인자로 받아 StringBuilder를 리턴하는 메서드가 필요하다.

    • StringBuilder 클래스의 또 다른 append 메서드가 이러한 역할을 한다.

      public StrinbBuilder append(CharSequence seq)
    • 여기서 목표로 하는 함수형 인터페이스는 인자 타입으로 StringBuilder를 기대하지만 append 메서드는 CharSequence를 인자로 받으므로 타입 변환이 필요한데 CharSequence가 StringBuilder의 슈퍼 타입이므로 이러한 변환이 가능하다.

      메서드목표 함수형 인터페이스
      시그니처SB append(CharSequence seq)BinaryOperator\<SB|
      타입(SB, CharSequence seq) -> SB(SB, String) -> SB
    • 타입 T를 인자로 받는 메서드의 참조를 T의 서브 타입을 인자로 받는 메서드에 상응하는 함수형 인터페이스의 참조에 대입할 수 있다.

    • 여기서 다룬 concatenator는 JDK의 정적 메서드 Collectors.joining()이 리턴하는 컬렉터 객체와 매우 비슷하다.

      public static Collector<CharSequence, ?, String> joining() {
          return new CollectorImpl<CharSequence, StringBuilder, String>(
                  StringBuilder::new, StringBuilder::append,
                  (r1, r2) -> { r1.append(r2); return r1; },
                  StringBuilder::toString, CH_NOID);
      }
    • Attribute 타입의 객체를 리턴하는 어댑터 정적 메서드

      public static Attribute<V, S> of(Supplier<S> supplier,
                                       BiConsumer<S, V> updater,
                                       BinaryOperator<S> combiner,
                                       Function<S, V> finisher) {
           
      	return new Attribute<>() { // 1. 익명 클래스
      	
               @Override
               public S seed() {
                   return supplier.get();
               }
      	
               @Override
               public void update(S summary, V value) {
                   updater.accept(summary, value);
               }
      
               @Override
               public S merge(S summary1, S summary2) {
                   return combiner.apply(summary1, summary2);
               }
      
               @Override
               public V report(S summary) {
                  return finisher.apply(summary);
              }
          };  // 2. 익명 클래스 
      }

제네릭 컨테이너 구현

  • 연결 상태와 그룹을 관리하는 ContainerLike의 제네릭 구현

    public class Container {
        private Container parent = this;
        private double amount;
        private int size = 1;
    }
  • 제네릭 버전

    • 그룹 요약 값을 저장하는 S 타입의 객체와 요약 값, 지역적인 값을 조작하는 메서드로 이뤄진 Attribute 타입의 객체를 필드로 포함

    • 이 두 필드가 amount 필드를 대체한다.

    public class UnionFindNode<V, S> implements ContainerLike<V, UnionFindNode<V, S>> {
        private UnionFindNode<V, S> parent = this;
        private int groupSize = 1;
        private final Attribute<V, S> attribute; // 속성을 조작하는 메서드를 포함한다.
        private S summary;
    
        public UnionFindNode(Attribute<V, S> dom) {
            attribute = dom;
            summary = dom.seed();
        }
        @Override
        public V get() { // 현재 속성을 리턴
            UnionFindNode<V, S> root = findRootAndCompress();
            return attribute.report(root.summary, value);
        }
    
        @Override
        public void update(V value) { // 속성을 갱신
            UnionFindNode<V, S> root = findRootAndCompress();
            attribute.update(root.summary, value);
        }
    
        @Override
        public void connectTo(UnionFindNode<V, S> other) {
            UnionFindNode<V, S> root1 = findRootAndCompress();
            UnionFindNode<V, S> root2 = findRootAndCompress();
            
            if (root1 == root2) return;
    
            ...
    
            S newSummary = attribute.merge(root1.summary, root2.summary);
    
            ... // 크기에 따른 병합 정책
        }
    }
  • 수조의 기능 재현하기

    • 추상화 레벨이 추가된다.

    • getAmount와 addWater 메서드 대신 ContainerLike 인터페이스가 제공하는 제네릭 메서드인 get과 update를 사용한다.

      Container a = new Container();
      Container b = new Container();
      Container c = new Container();
      Container d = new Container();
      
      a.update(12.0);
      d.update(8.0);
      a.connectTo(b);
      System.out.println(a.get() + " " + b.get() + " " + c.get + " " + d.get());
    • UnionFindNode를 구현하는 모든 구체 클래스는 타입 V와 S를 지정하고 Attribute<V, S> 타입의 객체를 지정해야 한다.

    • Attribute 객체는 자신이 속한 UnionFindNode 객체에 접근할 수 없고 groupSize 필드에도 접근할 수 없다.

    • 따라서 그룹 크기 관련 정보를 요약 값 안에 별도로 복사해 저장해야한다.

    • 결국 단순하게 S = Double 로 하지 않고 그룹의 요약 값 역할을 할 클래스를 따로 만들어야 하는데 이 클래스를 ContainerSummary 라고 하자.

    • 이 요약 정보는 그룹 전체에 담긴 물의 양과 수조 개수를 포함한다.

      class ContainerSummary {
          private double amount;
          private int groupSize;
      
          public ContainerSummary(double amount, int groupSize) {
              this.amount = amount;
              this.groupSize = groupSize;
          }
          public ContainerSummary() {
              this(0, 1);
          }
      
          public void update(double increment) {
              this.amount += increment;
          }
      
          public ContainerSummary merge(ContainerSummary other) {
              return new ContainerSummary(amount + other.amount, groupSize + other.groupSize);
          }
      
          public double getAmount() {
              return amount / groupSize;
          }
      
          public static final Attribute<Double, ContainerSummary> ops 
              = Attribute.of(ContainerSummary::new, ContainerSummary::update, ContainerSummary::merge, ContainerSummary::getAmount);
          }
      }

    • update, merge, report 메서드의 첫 번째 인자는 그에 상응하는 ContainerSummary 메서드에서 this로 연결된다.

    • 구체적인 요약 값 타입과 그 지원 메서드를 정의했다면 단 세 줄의 코드로 지금까지 다룬 전형적인 수조의 기능을 재현할 수 있다.

    • 다음 코드처럼 UnionFindNode를 상속받고 그 생성자에 적절한 Attribute 객체를 넘겨주면 된다.

      public class Container extends UnionFindNode<Double, ContainerSummary> {
          public Container() {
              super(ContainerSummary.ops);
          }
      }
    • 자바 제네릭의 한계점 - 모든 UnionFindNode 객체가 같은 Attribute 객체 참조를 포함한다.

      • 제네릭 타입 정보가 삭제되기 때문에 정적 필드로 만들 수 없다.
      public class UnionFindNode<V, S>
            implements ContainerLike<V, UnionFindNode<V,S>> {
      
          private UnionFindNode<V, S> parent = this;
          private int groupSize = 1;
      
          private static final Attribute<V,S> attribute; // compile error
          private S summary;
    • 소셜 네트워크 포스트 (위와 동일)

      public class Post extends UnionFindNode<Integer,PostSummary> {
          public Post() {
              super(PostSummary.ops);
          }
      }
      
      class PostSummary {
          private int likeCount;
          public PostSummary(int likeCount) {
              this.likeCount = likeCount;
          }
          public PostSummary() {}
          public void update(int likes) {
              likeCount += likes;
          }
          public PostSummary merge(PostSummary summary) {
              return new PostSummary(likeCount + summary.likeCount);
          }
          public int getCount() {
              return likeCount;
          }
          public static final Attribute<Integer,PostSummary> ops =
              Attribute.of(PostSummary::new,
                           PostSummary::update,
                           PostSummary::merge,
                           PostSummary::getCount);
      }
  • MVC

    • 컨트롤러: 사용자 입력에 대응하는 컴포넌트

    • 모델: 응용 프로그램 관련 데이터를 저장하는 컴포넌트

    • 뷰: 사용자에게 데이터를 보여주는 컴포넌트

      public interface ParametricFunction {
      
          int getNParams();
          String getParamName(int i);
          double getParam(int i);
          void setParam(int i, double val);
          double eval(double x);
      }
      public abstract class AbstractFunction implements ParametricFunction {
      
          private final int n;
          protected final double[] a;
      
          public AbstractFunction(int n) {
              this.n = n;
              this.a = new double[n];
          }
      
          public int getNParams() { return n; }
      
          public String getParamName(int i) {
              final int firstLetter = 97; // a
              return Character.toString(firstLetter + i);
          }
      
          public double getParam(int i) {
              return a[i];
          }
      
          public void setParam(int i, double val) {
              a[i] = val;
          }
      }

      데코레이터 디자인 패턴

      public class ObservableFunction implements ParametricFunction {
      
          private final ParametricFunction f;
          private final List<ActionListener> listeners = new ArrayList<>();
          private final ActionEvent dummyEvent = new ActionEvent(this, ActionEvent.ACTION_FIRST, "update");
      
          public ObservableFunction(ParametricFunction f) { this.f = f; }
      
          public int getNParams()           { return f.getNParams();    }
          public double getParam(int i)     { return f.getParam(i);     }
          public String getParamName(int i) { return f.getParamName(i); }
          public double eval(double x)      { return f.eval(x);         }
      
          public void addActionListener(ActionListener listener) {
              listeners.add(listener);
          }
      
          public void setParam(int i, double val) {
              f.setParam(i, val);
              for (ActionListener listener: listeners)
                  listener.actionPerformed(dummyEvent);
          }
      }

      Pub, Sub 메시징 패턴

      Executorservice 를 사용한 카페 주문 기능 구현에서 해당 패턴을 사용해봤습니다. Publisher, Subscriber Pattern Sample Code

profile
riako

0개의 댓글