22. 인터페이스는 타입을 재정의하는 용도로만 사용하라

인터페이스는 타입을 정의하는 용도로만 사용해야 합니다.
상수 공개용 수단으로 사용해서는 안됩니다

안티패턴 - 상수 인터페이스

메서드 없이, 상수를 뜻하는 static final 필드로만 가득찬 인터페이스

public interface PhysicalConstants {

    // 아보가드로 수
    static final double AVOGADROS_NUMBER = 6.022_140_857e23;
    // 볼츠만 상수
    static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;
    // 전자 질량
    static final double ELECTRON_MASS = 9.109_383_56e-31;
}
  • 클래스 내부에서 사용하는 상수는 외부 인터페이스가 아닌 내부 구현입니다
    • 내부 구현을 클래스의 API로 노출하는 행위가 됩니다
    • 또한, 사용자에게 혼란과 오해의 소지를 줄 뿐입니다

대안

클래스의 상수로 선언하여 사용합니다

public class PhysicalConstants {

    // 인스턴스화 방지
    private PhysicalConstants(){}

    static final double AVOGADROS_NUMBER = 6.022_140_857e23;
    static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;
    static final double ELECTRON_MASS = 9.109_383_56e-31;
}

또는 enum의 값으로 사용합니다

public enum PhysicalConstants {

    AVOGADROS_NUMBER(6.002_140_857e23),
    BOLTZMANN_CONSTANT(1.380_648_52e-23),
    ELECTRON_MASS(9.109_383_56e-31);

    private double value;

    PhysicalConstants(double value) {
        this.value = value;
    }

    public double getValue(){
        return value;
    }
}

참조

effective java 3/e - 조슈아 블로크

블로그 이미지

yhmane

댓글을 달아 주세요

Item18

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

상속은 코드를 재사용하는 강력한 수단이지만, 항상 최선은 아니다.

잘못 사용하면 오류를 내기 쉬운 소프트웨어를 만들게 된다.


메서드 호출과 달리 상속은 캡슐화를 깨뜨린다.

  • 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있다.
    • 상위 클래스는 릴리스마다 내부 구현이 달라질 수 있으며, 그 여파로 코드 한줄 건드리지 않은 하위 클래스가 오작동할 수 있다.
public class InstrumentedHashSet<E> extends HashSet<E> {
    private int addCount = 0;

    public InstrumentedHashSet(){}

    public InstrumentedHashSet(int initCap, float loadFactor){
        super(initCap, loadFactor);
    }

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }
}

public class Item18 {
    public static void main(String[] args) {
        InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
        s.addAll(List.of("틱", "탁탁", "펑"));

        System.out.println(s.getAddCount());
    }
}
  • getAddCount()의 결과가 3을 반환하리라 생각하겠지만 6을 반환한다. HashSet의 addAll 메서드가 add 메서드를 사용해 구현된 데 있다.
  • 하위 클래스에서 addAll 메서드를 재정의하지 않으면 문제를 고칠 수 있다.
    • 하지만 이처럼 자신의 다른 부분을 사용하는 '자기사용' 여부는 해당 클래스의 내부 구현방식에 해당하며, 자바 플랫폼 전반적인 정책인지, 그 다음 릴리즈에도 유지될지 알 수 없다.
  • 그렇다면 재정의 대신 새로운 메서드를 추가하면 괜찮을까?
    • 괜찮은 방법이라 생각할수도 있지만, 위험이 전혀 없는 것은 아니다.
    • 다음 릴리스에 상위 클래스에 새 메서드가 추가 됐는데, 운 없게도 하필 추가된 메서드와 시그니처가 같고 반환 타입이 다를 수도 있다. 컴파일 문제가 바로 발생한다.

상속대신 컴포지시션을 이용

기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 한다.
컴포지션을 통해 새 클래스의 인스턴스 메서드들은 기존 클래스에 대응하는 메서드를 호출해 그 결과를 반환하게 한다.
새로운 클래스는 기존 클래스의 내부 구현 방식의 영향에서 벗어나며, 심지어 기존 클래스에 새로운 메서드가 추가되더라도 전혀 영향을 받지 않는다.

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

public class InstrumentedSet<E> extends ForwardingSet<E> {
  private int addCount = 0;

  public InstrumentedSet(Set<E> s) {
    super(s);
  }

  @Override
  public boolean add(E e) {
    addCount++;
    return super.add(e);
  }

  @Override
  public boolean addAll(Collection<? extends E> c) {
    addCount += c.size();
    return super.addAll(c);
  }

  public int getAddCount() {
    return addCount;
  }
}

public class Item18 {
  public static void main(String[] args) {
    InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
    s.addAll(List.of("틱", "탁탁", "펑"));
    System.out.println(s.getAddCount());

    InstrumentedSet<String> s2 = new InstrumentedSet<>(new HashSet<String>());
    s2.addAll(List.of("틱", "탁탁", "펑"));
    System.out.println(s2.getAddCount());
  }
}

결론

  • 상속은 강렬하지만 캡술화를 해친다는 문제가 있다.
  • 상속은 상위 클래스와 하위 클래스가 is-a 관계일 때만 써야 한다.
  • 상위 클래스와 하위 클래스의 패키지가 다를 경우에는 is-a 관계라도 문제가 발생할 수 있다.
  • 상속의 취약점을 피하려면 상속 대신 컴포지션 전달을 사용하자.

참조

블로그 이미지

yhmane

댓글을 달아 주세요

불변 클래스(Immutable Class)란 ?

  • 인스턴스의 내부 값을 수정할 수 없는 클래스를 말합니다.
  • 인스턴스의 저장된 정보가 객체가 파괴되기 전까지 바뀌지 않습니다
  • 대표적으로 String, Boolean, Integer, Float, Long 등등이 있습니다
  • Immutable Class들은 heap영역에서 변경불가능 한 것이지 재할당을 못하는 것은 아닙니다

 

불변 클래스 사용 이유는 무엇일까요?

  • 설계, 구현, 사용이 쉽습니다
  • thread-safe하고 에러를 만들 가능성이 적습니다

 

클래스를 불변으로 만들시 지켜야 할 규칙

객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.

  • setter나 필드의 정보를 변경하는 메서드를 제공하지 않는다

클래스를 확장할 수 없도록 한다.

  • 객체의 상태를 변하게 만드는 사태를 막아준다. 상속을 막는 대표적인 방법은 클래스를 final로 선언하는 것이다.

모든 필드를 final로 선언한다.

  • 필드의 수정을 막겠다는 설계자의 의도를 드러내는 방법이다.
  • 생성된 인스턴스를 동기화 없이 다른 스레드로 건네도 문제없이 동작하게끔 보장하여 준다.

모든 필드를 private으로 선언한다.

  • 필드가 참조하는 가변 객체를 클라이언트에서 직접 접근하여 수정하는 일을 막아 준다.

자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.

  • 클래스에 가변 객체를 참조하는 필드가 하나라도 있다면 클라이언트에서 인스턴스 내에 가변 객체의 참조를 얻을 수 없게 해야한다.
  • 생성자, 접근자(getter), readObject 메서드 모두에서 방어적 복사를 수행한다.

 


불변 복소수 클래스 Complex

public final class Complex {

    private final double re;
    private final double im;

    public Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }

    public double realPart() {
        return re;
    }

    public double imaginaryPart() {
        return im;
    }

    public Complex plus(Complex c) {
        return new Complex(re + c.re,im + c.im);
    }

    public Complex minus(Complex c) {
        return new Complex(re - c.re,im - c.im);
    }

    public Complex times(Complex c) {
        return new Complex(re * c.re - im * c.im,re * c.im + im + c.re);
    }

    public Complex dividedBy(Complex c) {
        double tmp = c.re * c.re + c.im * c.im;
        return new Complex((re * c.re + im * c.im) / tmp,
                (im * c.re - re + c.im) / tmp);
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }

        if (!(o instanceof Complex)) {
            return false;
        }

        Complex c = (Complex) o;
        return Double.compare(c.re, re) == 0
                && Double.compare(c.im, im) == 0;
    }

    @Override
    public int hashCode() {
        return 31 * Double.hashCode(re) + Double.hashCode(im);
    }

    @Override
    public String toString() {
        return "(" + re + " + " + im + "i)";
    }

}
  • 실수부와 허수부 값을 반환하는 접근자(realPart, imaginaryPart)와 사칙연산 메서드(plus, minusm times, dividedBy)를 정희하였다.
  • 이 사칙연산 매서드들은 인스턴스 자신은 수정하지 않고 새로운 Complex 인스턴스를 만들어 반환한다.
  • 이처럼 피연산자에 함수를 적용해 그 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴을 함수형 프로그래밍이라 한다.
  • 이처럼 함수형 프로그래밍 기법을 적용하면 코드의 불변이 영역이 되는 비율이 높아져 안전합니다.불변 객체의 장점
  • 불변 객체는 근본적으로 스레드 안전하여 따로 동기화 할 필요가 없다
  • 불변 객체는 안심하고 공유 할 수 있다
  • 불변 객체는 방어적 복사본이 필요없다
  • 불변 객체는 자유롭게 공유할 수 있음은 물론, 불변 객체끼리는 내부 데이터를 공유할 수 있다.
  • 불변 객체를 key로 하면 이점이 많다.
    • Map의 key
    • Set의 원소
  • 불변 객체는 그 자체로 실패 원자성을 제공한다

결론

  • 모든 클래스를 불면으로 만들수는 없다.
  • 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄여야 한다.
  • 다른 합당한 이유가 없다면 모든 필드는 private final 이어야 한다.
  • 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.

참조

  • effetive java 3/e
블로그 이미지

yhmane

댓글을 달아 주세요

Item16

public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라

public 필드 사용

class Point {
    public double x;
    public double y;
}

public class Execution {
  public static void main(String[] args) {
    Point p = new Point();
    p.x = 123;
    System.out.println(p.x);
  }
}
  • 위와 같은 클래스 Point는 데이터 필드에 직접 접근할 수 있으니 캡슐화의 이점을 제공하지 못합니다.
  • API를 수정하지 않고는 내부 표현을 바꿀 수 없습니다.
  • 불변식을 보장할 수 없습니다.

접근자와 변경자 설정을 통해 데이터를 캡슐화

class Point2 {
  private double x;
  private double y;

  public Point2(double x, double y) {
    this.x = x;
    this.y = y;
  }

  public double getX() {
    return x;
  }

  public double getY() {
    return y;
  }

  public void setX(double x) {
    this.x = x;
  }
  public void setY(double y) {
    this.y = y;
  }
}

public class Execution {
  public static void main(String[] args) {
    Point2 p2 = new Point2(12, 34);
    p2.setX(123);
    System.out.println(p2.getX());
  }
}
  • private field를 사용하여 직접적인 접근을 막습니다.
  • 접근자(getter)와 수정자(setter)를 통해 내부 표현 방식의 유연성을 얻습니다.
  • package-private(default class) 혹은 private 중첩 클래스라면 데이터 필드를 노출한다 해도 문제가 없습니다.

결론

  • public 클래스는 절대 가변 필드를 직접 노출해서는 안됩니다.
  • 불변 필드라면 노출해도 덜 위험하지만 완전히 안심할 수는 없습니다. (item 15의 배열의 경우 안심할 수 없음)
  • package-private 클래스나 중첩 클래스에서는 종종 필드를 노출하는 편이 나을때도 있습니다.

참조

블로그 이미지

yhmane

댓글을 달아 주세요

Item12

toString을 항상 재정의하라

Object의 기본 toString 메서드가 우리가 작성한 클래스에 적합한 문자열을 반환하는 경우는 거의 없습니다.
이 메서드는 PhoneNumber@adbbd처럼 단순히 클래스_이름@16진수로_표시한_해시코드를 반환합니다.
toString의 일반 규약에 따르면 '간결하고 사람이 읽기 쉬운 형태의 유익한 정보'를 반환해야 합니다.
toString의 규약은 '모든 하위 클래스에서 이 메서드를 재정의하라'고 합니다.


toString 재정의

  • toString을 잘 구현한 클래스는 사용하기 편하고 디버깅하기 쉽습니다.
    • {Jenny=PhoneNumber@adbbd} 보다는 {Jenney=707-867-5309}라는 메세지가 유용합니다.
  • 실전에서 toString 그 객체가 가진 주요 정보 모두를 반환하는게 좋습니다.
  • toString을 구현할 때면 반환값의 포맷을 문서화할지 정해야 합니다.
    • 포맷을 명시하면 그 객체는 표준적이고, 명확하고, 사람이 읽을 수 있게 됩니다.
    • 단, 포맷을 한번 명시하면, 평생 그 포맷에 얽매이게 됩니다.
  • 포맷을 명시하든 아니든 개발자의 의도는 명확히 밝혀야 합니다.
  • 포맷 명이 여부와 상관없이 toString이 반환한 값에 대해 포함된 정보를 얻어올 수 있는 API를 제공해야 합니다.

toString 재정의가 필요치 않는 경우

  • 정적 유틸리티 클래스
  • enum Type
  • 대다수의 컬렉션 구현체
  • 구글의 @Autovalue, Lombok의 @ToString

결론

  • 재정의가 필요치 않는 경우를 제외하곤 모든 클래스에서 toString을 재정의 하는게 좋습니다.
  • toString은 해당 객체에 관한 명확하고 유용한 정보를 읽기 좋은 형태로 반환해야 합니다.

참조

yhmane github
이펙티브 자바 3/e [조슈아블로크]

블로그 이미지

yhmane

댓글을 달아 주세요

Item10

equals는 일반 규약을 지켜 재정의하라

  • equals 메서드는 여러 조건들을 충족해야 하기에 재정의하기 쉽지 않습니다.
  • 문제를 회피하는 가장 쉬운 방법은 아예 재정의 하지 않는 것입니다.
  • 아래의 사항에 해당하는 것이 있다면 재정의 하지 않는 것이 최선

equals를 재정의 하지 않아도 되는 경우

각 인스턴스가 본질적으로 고유할 경우

  • 값을 표현하는게 아닌 동작하는 개체를 표현할 경우. 대표적으로 Thread

인스턴스의 '논리적 동치성(logical equality)'을 검사할 일이 없을 경우

  • java.utils.regex.Pattern은 equals를 재정의해서 두 Pattern의 인스턴스가 같은 정규표현식을 나타내는지 검사합니다.

상위 클래스에서 재정의한 equals가 하위 클래스에서도 딱 들어맞을 경우

  • AbstractSet -> Set, AbstractList -> List, AbstractMap -> Map 들은 상위 Abstract 클래스로부터 equals를 구현 받아 사용합니다.

클래스가 private이거나, package-private여서 equals를 호출할 일이 없는 경우

  • equals가 실수로라도 호출되는 걸 막고 싶다면 다음처럼 구현을 하면 됩니다.
    @Override public boolean equals(Object o) {
      throw new AssertionError();
    }

싱글턴을 보장하는 클래스'인스턴스 통제, Enum'인 경우


equals를 재정의 할 경우 지켜야 할 규약

반사성 (reflexivity)

  • null이 아닌 모든 참조 값 x에 대해, 'x.equals(x) = true' 이어야 합니다. (자기 자신에 대해서 true)

대칭성 (symmetry)

  • null이 아닌 모든 참조 값 x, y에 대해 'x.equals(y) = true'면 'y.equals(x) = true'를 만족해야 합니다.

    public final class CaseInsensitiveString {
        private final String s;
    
        public CaseInsensitiveString(String s) {
          this.s = Objects.requireNonNull(s);
        }
    
        @Override
        public boolean equals(Object o) {
          if(o instanceof CaseInsensitiveString) {
                return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
          }
    
          if(o instanceof String) {
                return s.equalsIgnoreCase((String) o);
          }
          return false;
        }
    }
    CaseInsensitiveString cis = new CaseInsensitiveString("hello?");
    String hello = "hello?";
    System.out.println(cis.equals(hello)); //true
    System.out.println(hello.equals(cis)); //false

cis.equals(hello)는 true를 반환하지만, hello.equals(cis)는 false를 반환한다. String의 equals는 일반 String을 알고 있지만 CaseInsenitiveString의 존재를 알수 없다.

추이성

  • null이 아닌 모든 참조 값 x, y, z에 대해 'x.equals(y) = true'면 'y.equals(z) = true'도 만족하면 'x.equals(z) = true'도 만족해야 합니다.

    class Point {
        private final int x;
        private final int y;
    
        public Point(int x, int y) {
          this.x = x;
          this.y = y;
        }
    
        @Override
        public boolean equals(Object o) {
          if(!(o instanceof Point)) return false;
          Point p = (Point) o;
          return this.x == p.x && this.y == p.y;
        }
    }
    class ColorPoint extends Point {
    
        private final Color color;
    
        @Override
        public boolean equals(Object o) {
          if(!(o instanceof Point)) {
              return false;
          }
    
          if(!(o instanceof ColorPoint)) {
              return o.equals(this);
          }
          return super.equals(o) && this.color == ((ColorPoint) o).color;
        }
    }
    ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
    Point p2 = new Point(1, 2);
    ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);

    p1 -> p2, p2 -> p3를 만족하지만 p1 -> p3는 색상도 비교해야 하기에 만족하지 않습니다. 따라서 추이성 규약을 위반합니다.

일관성

  • null이 아닌 모든 참조 값 x, y에 대해 'x.equals(y)'는 true/false 등 항상 일관된 값을 반환해야 합니다.

null-아님

  • null이 아닌 모든 참조 값 x에 대해 'x.equals(null) = false'입니다.
    @Override 
    public boolean equals(Object o) {
        if (o == null) {
            return false; 
        }
        ...
    }
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof MyType)) {
            return false; 
        } 
        ...
    }

equals 구현 절차

  • == 연산자를 사용해 입력이 자기 자신의 참조인지 확인합니다.
    • 성능 최적화용으로 equals가 복잡할 때 같은 참조를 가진 객체에 대한 비교를 안하기 위함입니다.
  • instanceof 연산자로 입력이 올바린 타입인지 확인합니다.
    • 올바르지 않다면 false를 반환합니다.
    • equals중에서는 같은 interface를 구현한 클래스끼리 비교하는 경우도 있습니다.
  • 입력을 올바른 타입으로 형변환합니다.
    • 앞서 instanceof 연산을 수행했기 때문에 100% 성공합니다.
  • 입력 객체와 자기 자신의 대응되는 '핵삼' 필드들이 모두 일치하는지 하나씩 검사합니다.
    • 모두 일치하면 true, 하나라도 다르면 false를 반환합니다.
    • interface 비교시 필드정보를 가져오는 메서드가 interface에 정의되어있어야하고, 구현체에서 메서드를 재정의 해야합니다.
  • float, double을 제외한 기본타입은 ==을 통해 비교하고 참조(reference) 타입은 equals를 통해 비교합니다.
    • float, double은 Float.compare(float, float)와 Double.compare(double, double)로 비교합니다.
    • 배열의 모든 원소가 핵심 필드라면 Arrays.equals를 사용합니다.
  • null도 정상값으로 취급하는 참조타입 필드도 있습니다.
    • Objects.equals(obj, obj)를 이용해 NullPointerException 발생을 예방합니다.
  • 최상의 성능을 내기 위해 다음과 같은 방법이 있습니다.
    • 다를 확률이 높은 필드부터 비교합니다.
    • 비교하는 비용이 작은 것을 먼저 수행합니다.

결론

  • 꼭 필요한 경우가 아니면 equals를 재정희 하지 말자.
  • 많은 경우에 Object의 equals가 정확한 비교를 수행하여 줍니다.
  • 재정의할때는 위 다섯 가지 규약을 확실히 지켜가며 정의하도록 합니다.

참조

yhmane github

블로그 이미지

yhmane

댓글을 달아 주세요

09. try-finally보다는 try-with-resources를 사용하라

InputStream, OutputStream, Connection  close() 직접 호출해 닫아줘야 하는 자원이 많이 있습니다.
자원 닫기는 클라이언트가 놓칠수 있기 때문에 예측할  없는 성능문제로 이어지기도 합니다.
이러한 자원중 상당수가 안전망으로 finalizer(item8) 사용하고 있지만, finalizer는 그리 믿을만하지 못합니다.

전통적인 방식 (try-finally)

public String getTryFinallyRead(String path) throws IOException {
    BufferedReader br = null;

    try {
        br = new BufferedReader(new FileReader(path));
        return br.readLine();
    } catch (Exception e) {
        e.getMessage();
    } finally {
       br.close();
    }

    return path;
}
  • 난해한 코드가 작성 되었습니다.
  • 예외는 try 블록과 finally 블록 모두에서 발생할 수 있습니다.
  • 물리적인 문제가 발생한다면 try, finally에서 모두 예외가 발생합니다.
  • 하지만, 이런 경우 두번째 예외가 첫번째 문제를 삼켜서 실제 시스템에서 버그 추적이 어려워 질 수도 있습니다.

대안점은?

  • 자바7이 투척한 try-with-resource를 사용
  • AutoCloseable를 구현하고 close method()를 사용할 것
public String getTryWithResourcesRead(String path) throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader(path))) {
       return br.readLine();
    }
}

  • 코드가 간결해졌습니다. (AutoClosable을 구현하여 짧게 사용이 가능합니다)
  • try-with-resources를 이용하면 첫번째 예외부터 추적이 가능해집니다.
  • 뿐만아니라 스택 추적 영역으로 예외를 추적할 수 있습니다.

사용방법

try() 내부에 AutoCloseable를 구현한 클래스를 사용하면 됩니다


결론

  • try-finally보다는 try-with-resources를 사용하는 것이 좋습니다.
  • 코드는 짧고 명확해지고, 예외 정보도 더욱 유용합니다.

 

참조

effective java 3rd

블로그 이미지

yhmane

댓글을 달아 주세요

05. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라

많은 클래스가 하나 이상의 자원에 의존합니다. 

맞춤법 검사기는 사전(dictionay) 의존하는데, 이러한 클래스를 정적 유틸리티 클래스로 구현하는 것을 볼수 있습니다

 

  • 정적 유틸리티 클래스
public class SpellChecker {
	private static final Lexicon dictionary = ...; // 사전
	private SpellChecker() {} 
    	public static boolean isValid(String word) { ... } 
    	public static List<String> suggestions(String typo) { ... } 
} 

SpellChecker.isValid("word")

비슷하게 싱글턴으로 구현한 것도 볼수 있습니다

  • 싱글턴
public class SpellChecker { 
	private final Lexicon dictionary = ...; 
    	private SpellChecker(...) {} 
    	public static SpellChecker INSTANCE = new SpellChecker(...); 
    	public static boolean isValid(String word) { ... } 
    	public static List<String> suggestions(String typo) { ... } 
} 

SpellChecker.INSTANCE.isValidisValid("word")

 방법 모두 확장이 어렵고 테스트하기 어렵습니다. 

사전의 경우 국어사전, 영어사전, 일어사전  다양한 종류의 사전이 존재하는데  하나만 사용하는 가정은 좋아 보이지 않습니다

여러 사전을 쓸수 있도록 만들어 보자 Final을 제거하고 다른 사전으로 교체할  있도록 메서드를 추가해 보겠습니다

public class SpellChecker {
	private static Lexicon dictionary = ...;
    
    ...
    public static void changeDictionary(Lexicon newDictionary) {
    	dictionary = newDictionary;
    }
    ...
}

SpellChecker.changeDictionary(newDictionary);

 방법은 오류를 내기 쉬우며 멀티스레드 환경에서는 사용할  없습니다

 

의존객체 주입방법

인스턴스를 생성할  생성자에 필요한 자원을 넘겨주는 방식입니다.

public class SpellChecker {
    private final Lexicon dictionary;
    
    public SpellChecker(Lexicon dictionary){
    	this.dictionary = Objects.requireNotNull(dictionary);
    }
    
    public static boolean isVaild(String word) {...}
	  ...
}

// 인터페이스
interface Lexicon {}
public class EnglishDicionary implements Lexicon {
	...
}

// 사용
Lexicon englishDictionary = new EnglishDicionary();
SpellChecker spellChecker = new SpellChecker(englishDictionary);

spellChecker.isVaild(word);

이방법은 유연성과 테스트 용이성을 높여줍니다. 

또한, 불변을 보장하여 여러 클라이언트가 의존 객체들을 안심하고 공유할  있습니다

의존성이 많아지게 되면 다소 불편함감이 없지 않지만, 이러한 문제는 프레임워크 레벨에서 다룰 수 있습니다

 

참조

effective-java 3rd 

블로그 이미지

yhmane

댓글을 달아 주세요

04. 인스턴스화를 막으려거든 private 생성자를 사용하라

정적 메서드와 정적 필드만을 담은 클래스

  • 객체 지향적 방식과는 거리가 멀지만, 나름의 쓰임새가 존재합니다.
    • ex) java.lang.Math, java.util.Arrays, java.util.Collections
  • 특정 메서드들을 모아놓은 유틸성 클래스들이 그러합니다.
public class Item4App { 
    public static void main(String[] args) {
      Math.abs(5);
      Arrays.sort(new int[]{5, 3});
    }
}

하지만 생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자를 생성

  • public 생성자가 만들어지며, 사용자가 의도한 것인지 자동생성된 것인지 알 수 없습니다.
  • 실제로 공개된 API 에서도 의도치 않게 인스턴스화할 수 있는 클래스가 종종 목격됩니다.
  • 추상 클래스로 만들어도 상속을 하게 되면 소용이 없습니다.

생성자 인스턴스화를 막는 방법

private으로 생성자를 선언하면 인스턴스화가 불가능합니다
또한, private을 사용하 상속이 불가능하여 원치 않는 인스턴스화를 막을 수 있습니다.
public class Item4 {

  private Item4() {
     throw new AssertionError(); 
  }

  public static void play() {
      System.out.println(“just play”);
  }
}

public class Item4Main {  
    public static void main(String[] args) {  
        // 인스턴스화 불가능  
        //Item4 item4 = new Item4();  
        Item4.play();  
    }  
}

주의하면 좋은것

클래스 바깥에서는 private 생성자에 접근할  없으니 Error를  던질 필요는 없습니다.
다만 에러 설정을 해둔다면 의도를 명확히 드러낼  있습니다.

 

참조

effective java 3rd

블로그 이미지

yhmane

댓글을 달아 주세요

Item2

생성자에 매개변수가 많다면 빌더를 고려하라

  • 점층적 생성자 패턴도 쓸 수 있지만, 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵습니다.
public class Reservation {
    private String name;
    private String phone;
    private String reservingTime;
    private String startTime;
    private String endTime;
    
    public Reservation(String name, String phone) {
        this.name = name;
        this.phone = phone;
    }

    public Reservation(String name, String phone, String reservingTime) {
        this.name = name;
        this.phone = phone;
        this.reservingTime = reservingTime;
    }

    public Reservation(String name, String phone, String reservingTime, String startTime) {
        this.name = name;
        this.phone = phone;
        this.reservingTime = reservingTime;
        this.startTime = startTime;
    }

    public Reservation(String name, String phone, String reservingTime, String startTime, String endTime) {
        this.name = name;
        this.phone = phone;
        this.reservingTime = reservingTime;
        this.startTime = startTime;
        this.endTime = endTime;
    }
}
  • 자바빈즈 패턴(setter)를 이용해 이러한 단점을 보완할 수 있지만, 객체의 일관성이 무너진 상태에 놓이게 됩니다.
public static void main() {
    Reservation reservation = new Reservation();
    reservation.setName("황윤호");
    reservation.setPhone("010-1234-5678");
    reservation.setReservingTime("2020-11-01");
    reservation.setStartTime("2020-11-18");
    reservation.setEndTime("2020-11-20");
}
  • 점층적 생성자 패턴의 안전성과 자바빈즈 패턴의 가독성을 겸비한 빌더 패턴이 있습니다.
public class Reservation {
    private String name;
    private String phone;
    private String reservingTime;
    private String startTime;
    private String endTime;
    
    public static class Builder {
        // 필수 매개변수
        private String name;
        private String phone;
        
        // 선택 매개변수
        private String reservingTime = "2020-01-01";
        private String startTime     = "2020-01-02";
        private String endTime       = "2020-01-03";
        
        public Builder(String name, String phone) {
            this.name = name;
            this.phone = phone;
        }

        public Builder reservingTime(String val) {
            reservingTime = val;
            return this;
        }

        public Builder startTime(String val) {
            startTime = val;
            return this;
        }

        public Builder endTime(String val) {
            endTime = val;
            return this;
        }
        
        public Reservation build() {
            return new Reservation(this);
        }
    }
    
    private Reservation(Builder builder) {
        name = builder.name;
        phone = builder.phone;
        reservingTime = builder.reservingTime;
        startTime = builder.startTime;
        endTime = builder.endTime;
    }
}

public class Item2 {
    public static void main() {
        Reservation reservation = new Reservation
                .Builder("황윤호","010-1234-5678")
                .reservingTime("2020-11-13")
                .startTime("2020-11-20")
                .endTime("2020-11-22")
                .build();
    }
}

정리

  • 코드가 읽고 쓰기 쉬워집니다.
  • 객체의 일관성을 부여할 수 있습니다.
  • 빌더 패턴은 빌더를 만들어 주어야 하기 때문에 매개변수가 많지 않다면 필수는 아닙니다.
  • 점층적 생성자 패턴이나 자바빈즈 패턴의 장점을 모아두었고, 생성자는 시간이 지날수록 매개변수가 늘어나니 빌더패턴을 적용하는 것이 좋습니다.



참고

- effective java 3/e [조슈아 블로크]


블로그 이미지

yhmane

댓글을 달아 주세요