클래스와 인터페이스 - 아주 조금 더 깊게

문지은·2022년 9월 22일
0

클래스와 접근 권한을 최소화하라

public 일 필요가 없는 클래스의 접근 수준을 package-private 톱레벨 클래스로 좁혀야 한다.

그렇다면 테스트는 어떻게 할까?
책에서는 다음과 같이 하라고 한다.

테스트 코드를 테스트 대상과 같은 패키지에 두어 package-private 한 요소에 접근할 수 있게 한다.
https://stackoverflow.com/questions/18507311/private-class-unit-tests
private 메서드는 src/main 하위에 있고 test 코드는 src/test 밑에 두는데 어떻게 접근하는걸까?
-> 클래스의 디폴트 생성자는 protected 이므로 package-private이 적용되어 접근 가능해진다!

추가적으로 자바9부터 제공하는 모듈을 사용하는 방법이 있다.
모듈이 뭘까?
패키지들의 묶음으로 이를 통해서 두가지 암묵적 접근 수준을 더 제공한다.
모듈은 자신이 속하는 패키지 중 공개할 것들을 선언한다. 클래스를 외부에 공개하지 않으면서 같은 모듈을 이루는 패키지 사이에서는 자유롭게 공유할 수 있다. 여기에서 모듈 내부 한정 public, protected 수준이 추가된다고 볼 수 있다.

이에 관련된 더 자세한 설명 및 사용 예시 :
https://www.concretepage.com/java/java-9/java-module#Create

하지만 모듈을 사용하기 위해선 패키지를 모듈 단위로 묶고 이에 대한 의존성을 명시하고 모듈 안으로부터 일반 패키지로의 모든 접근에 특별한 조치를 취해야 하므로 많은 준비가 필요하다. 꼭 필요한 경우가 아니라면 사용하지 않는 것을 추천한다고 한다.

오라클 공식 홈페이지에서의 모듈에 대한 설명 : https://www.oracle.com/kr/corporate/features/understanding-java-9-modules.html

오라클 공식 홈페이지에 따르면 강력한 은닉을 통해 코드 내부에 대한 의존성을 줄이고 확장성을 높여준다고 하는데 패키지 레벨에서의 접근성 설정을 통해서도 이를 충분히 구현할 수 있지 않을까..? 생각이 든다. 한번 사용한 코드를 접해보고 싶다!

변경 가능성을 최소화하라

불변 클래스를 사용하자. 설계와 구현, 사용이 쉬우며 오류가 생길 여지도 적고 훨씬 안전하다.
이 때 변경점이 많은 클래스라면 성능 문제가 생길 수 있다. 이 때 해결법이 있다.

  1. 흔히 쓰일 다단계 연산을 미리 예측하여 기본 기능으로 제공하기
  2. 가변 동반 클래스(companion class) 사용하기

대표적인 예가 StringStringBuilder
companion class가 뭘까? 불변 객체의 내용 수정을 위해 제공되는 클래스. 검색해도 나오는 게 Effective Java 밖에 없다. 흔하게 쓰는 말은 아닌듯하다.

상속보다는 컴포지션을 사용하라

상속은 캡슐화를 깨뜨린다. 상위 클래스에 문제가 있을 경우 해당 문제가 하위 클래스로 전파되며 상위 클래스의 변화에 따라 하위 클래스도 계속 수정해야 하는 문제가 있다. 상위 클래스에서 메서드를 수정하지 않고 새로 만들면 이런 문제를 줄일 수는 있지만 하위 클래스에 이미 같은 이름의 메서드가 정의되어 있는 경우 문제가 된다.

상속 대신 컴포지션을 사용해서 클래스를 정의하면 다음 코드처럼 만들 수 있다.

public class ForwardingSet<E> implements Set<E> {
    private final Set<E> s;
    public ForwardingSet(Set<E> s) { this.s = s; }

    public void clear()               { s.clear();            }
    public boolean contains(Object o) { return s.contains(o); }
    public boolean isEmpty()          { return s.isEmpty();   }
    public int size()                 { return s.size();      }
    public Iterator<E> iterator()     { return s.iterator();  }
    public boolean add(E e)           { return s.add(e);      }
    public boolean remove(Object o)   { return s.remove(o);   }
    public boolean containsAll(Collection<?> c)
                                   { return s.containsAll(c); }
    public boolean addAll(Collection<? extends E> c)
                                   { return s.addAll(c);      }
    public boolean removeAll(Collection<?> c)
                                   { return s.removeAll(c);   }
    public boolean retainAll(Collection<?> c)
                                   { return s.retainAll(c);   }
    public Object[] toArray()          { return s.toArray();  }
    public <T> T[] toArray(T[] a)      { return s.toArray(a); }
    @Override public boolean equals(Object o)
                                       { return s.equals(o);  }
    @Override public int hashCode()    { return s.hashCode(); }
    @Override public String toString() { return s.toString(); }
}

대표적인 예시는 Google에서 만든 라이브러리인 Guava.
https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ForwardingList.java

// ForwardingList 클래스
public abstract class ForwardingList<E extends @Nullable Object> extends ForwardingCollection<E>
    implements List<E> {
  // TODO(lowasser): identify places where thread safety is actually lost

  /** Constructor for use by subclasses. */
  protected ForwardingList() {}

  @Override
  protected abstract List<E> delegate();

  @Override
  public void add(int index, @ParametricNullness E element) {
    delegate().add(index, element);
    // super로 호출 안하고 delegate 용 객체를 만들어서 호출
  }
  .
  .
  .

멤버 클래스는 되도록 static으로 만들라

non-static 멤버 클래스는 암묵적으로 바깥 클래스의 인스턴스와 연결된다. 즉 non-static 멤버 클래스의 인스턴스와 바깥 인스턴스 사이의 관계는 멤버 클래스가 인스턴스화될 때 확립되며 이 관계 정보는 non-static 멤버 클래스의 인스턴스 안에 만들어져 메모리 공간을 차지하고 생성 시간도 더 걸린다. 더 심각한 문제는 가비지 컬렉션이 바깥 클래스의 인스턴스를 수거하지 못하는 메모리 누수가 발생할 수 있다.


public class OuterClass {

	private List<InnerClass> innerClassList;
    
    public static class InnerClass {
    	int i;
    }
}

위와 같은 클래스의 인스턴스를 생성하면 메모리 구조가 어떻게 될까? 궁금해졌다.
왜냐하면 클래스의 인스턴스를 생성하면 jvm에서 heap에 자료를 저장하고 있는데 static 변수나 클래스는 method area에 저장되기 때문이다.

관련해서 내 생각을 정리하면 다음과 같다.

inner static class는 외부 클래스가 인스턴스화되지 않아도 접근 및 인스턴스화가 가능하다. 곧, 이 말은 inner static class는 외부 클래스의 필드(코드)와 같은 개념으로 볼 수 있다고 생각했다. 클래스의 코드에 대한 정보는 어차피 method area에 저장된다. method area는 static 클래스나 변수들이 저장되는 곳이기도 하기 때문에 일반 클래스의 코드와 같은 개념으로 볼 수 있을 것이다!

oracle docs 에서의 method area에 대한 설명 참조 : https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5

profile
백엔드 개발자입니다.

0개의 댓글