27. 비검사 경고를 제거하라

제네릭을 사용하기 시작하면 수많은 컴파일러 경고들을 마주치게 됩니다
이러한 경고들을 가능한 많이 제거하는 것이 습니다
경고들을 모두 제거한다면, 그 코드는 타입 안정성이 보장되기 때문입니다

대부분의 비검사 경고는 쉽게 제거할 수 있습니다

Set<Car> cars = new HashSet();
Venery.java:4: warning: [unchecked] unchecked conversion
                Set<Car> cars = new HashSet();
                                ^
  required: Set<Car>
  found:    HashSet

컴파일러가 알려준 대로 수정하면 경고가 사라집니다

자바7 이후 제공하는 다이아몬드 연산자<>로 해결이 가능합니다

Set<Car> cars = new HashSet<>();

다만, 제거하기 훨씬 어려운 경고들도 있습니다.

곧바로 해결되지 않는 경고가 나타나더라도 할 수 있는한 모든 비검사 경고를 제거하도록 합니다

경고를 제거할 수 없지만 타입이 안전하다고 확신할 수 있으면,  @SuppressWarnings(“unchecked”)를 달아 경고를 숨기도록 합니다

@SuppressWarnings

  • 지역변수 ~ 클래스 전체까지 모두 선언 할 수 있습니다
    • 가능한 가장 좁은 범위에 적용하도록 합니다
    • 또한, 클래스 전체에 적용하지 않도록 합니다
  • 한줄이 넘는 메서드나 생성자에 달린 @SuppressWarnings은 지역변수 선언 쪽으로 옮기도록 합니다
    • 이를 위해 지역변수를 새로 선언하는 수고를 해야할 수 있지만, 그만한 값어치가 있습니다
  • 또한 @ SuppressWarnings 사용시 안전한 이유를 항상 주석으로 남겨두도록 합니다 
  • public <T> T[] toArray(T[] a) { if (a.length < size) { // 생성한 배열과 매개변수로 받은 배열의 타입이 모두 T[]로 같으므로 올바른 형변환입니다 @SuppressWarnings("unchecked") T[] result = (T[]) Arrays.copyOf(elements, size, a.getClass()); return result; } System.arraycopy(elements, 0, a, 0, size); if (a.length > size) a[size] = null; return a; }

참조

effective java 3/e

블로그 이미지

yhmane

댓글을 달아 주세요

25. 톱레벨 클래스는 한 파일에 하나만 담아라

우리가 사용하는 파일 내에 하나만 존재하는 클래스를 톱레벨 클래스라고 합니다.
(중첩클래스는 top 레벨 클래스가 아닙니다)

하지만, 소스파일 하나에 여러개의 톱레벨 클래스가 있다면 심각한 위험을 감수해야 합니다

문제

Utensil.java

class UtenSil {
    static final String NAME = "pan";
}

class Dessert {
    static final String NAME = "cake";
}

Dessert.java

class UtenSil {
    static final String NAME = "pot";
}

class Dessert {
    static final String NAME = "pie";
}

여기서 아래의 main 함수를 실행해보자

class Main {
    public static void main(String[] args) {
        System.out.println(Utensil.NAME + Dessert.NAME);
    }
}

일반적으론 중복 클래스가 존재한다고 컴파일 에러가 나지만,
javac Main.java Utensil.java의 명령으로 컴파일 한다면 ‘pancake’가
Javac Main.java Dessert.java의 명령으로 컴파일 한다면 ‘potpie’가 출력될 것입니다

해결방안

정적멤버 클래스 또는 톱레벨 클래스를 서로 다른 파일로 분리

정적멤버클래스

class Main {
    public static void main(String[] args) {
        System.out.println(Utensil.NAME + Dessert.NAME);
    }

    static class Utensil {
        static final String NAME = "pan";
    }

    static class Dessert {
        static final String NAME = "cake";
    }
}

톱레벨 클래스로 분리

class Utensil {
    static final String NAME = "pan";
}
class Dessert {
    static final String NAME = "cake";
}

참조

effective java 3/e

블로그 이미지

yhmane

댓글을 달아 주세요

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

중첩클래스란

중첩 클래스(nested class) 다른 클래스 안에 정의된 클래스를 말합니다.
중첩 클래스는 자신을 감싼 바깥 클래스에서만 쓰여야 하며, 그 외의 쓰임새가 있다면 톱 레벨 클래스로 만들어야 합니다

  • 정적 멤버 클래스
  • (비정적) 멤버 클래스
  • 익명 클래스
  • 지역클래스

이 중에서 정적 멤버 클래스를 제외한 나머지를 내부 클래스 (inner class)라고 합니다

정적 멤버 클래스

정적 멤버 클래스는 바깥 클래스의 private 멤버에도 접근할 수 있다는 점을 제외하고
일반 클래스와 쓰임새는 동일합니다

public class OuterWithStaticClass {

    private String name;

    static class StaticClass {
        void hello() {
            OuterWithStaticClass outerWithStaticClass = new OuterWithStaticClass();
            outerWithStaticClass.name = "홍길동";
            System.out.println(outerWithStaticClass.name);
        }
    }
}
public class Item24App {

    public static void main(String[] args) {

        OuterWithStaticClass.StaticClass staticClass = new StaticClass();
        staticClass.hello();
    }
}

비정적 멤버 클래스

구문상으로는 정적 클래스와 static이 붙어있고 없고의 차이입니다.
하지만 의미상으로 비정적 멤버 클래스는 바깥 클래스의 인스턴스와 암묵적으로 연결됩니다. 따라서 정규화된 this를 사용해 바깥 인스턴스의 메서드를 호출하거나 참조를 가져올 수 있습니다

public class OuterWithNoneStaticClass {

    private final String name;

    public OuterWithNoneStaticClass(String name) {
        this.name = name;
    }

    public String getName() {
        NonStaticClass nonStaticClass = new NonStaticClass("noneStatic-class : ");
        return nonStaticClass.getNameWithOuter();
    }

    private class NonStaticClass {
        private final String noneStaticName;

        public NonStaticClass(String noneStaticName) {
            this.noneStaticName = noneStaticName;
        }

        public String getNameWithOuter() {
            return noneStaticName + OuterWithNoneStaticClass.this.name;
        }
    }
}
public class Item24App {

    public static void main(String[] args) {
        OuterWithNoneStaticClass noneStaticClass = new OuterWithNoneStaticClass("고길동");
        System.out.println(noneStaticClass.getName());
    }
}

드물게 직접 바깥 인스턴스의 클래스.new Member Class(args)를 호출해 수동으로
만들기도 합니다. 하지만 이 관계 정보는 비정적 멤버 클래스의 인스턴스 안에 만들어져 메모리 공간을 차지하며, 생성시간도 더 걸립니다.

따라서, 중첩 클래스의 인스턴스가 바깥 클래스의 인스턴스와 독립적으로 존재해야 한다면 정적 멤버 클래스로 만드는게 좋습니다.

익명 클래스

  • 이름이 없고 바깥 클래스의 멤버도 아닙니다
  • 쓰이는 시점에 선언과 동시에 인스턴스가 만들어집니다
  • 비정적인 문맥에서 사용될 때만 바깥 클래스의 인스턴스 참조가 가능하다
  • 상수 정적변수 (static final) 외에는 정적 변수를 가질 수 없다.
public class Anonymouss {

    private String name;

    public void hello() {
        HelloBot helloBot = new HelloBot() {
            @Override
            public void hello() {
                System.out.println("안녕하세요. 헬로우 봇입니다");
            }
        };
        helloBot.hello();
    }
}

interface HelloBot {
    void hello();
}
public class Item24App {

    public static void main(String[] args) {
        Anonymouss anonymouss = new Anonymouss();
        anonymouss.hello();
    }
}

익명 클래스의 제약사항

  • 선언한 지점에서만 인스턴스를 만들수 있습니다
  • 여러 인터페이스를 구현할 수 없고, 구현과 동시에 다른클래스 상속도 불가능합니다
  • 익명 클래스 사용 클라이언트는 사용하는 익명 클래스가 상위타입에서 상속한 멤버외에는 호출이 불가능합니다
  • 람다(자바7) 등장 이전에는 작은 함수 객체나 처리 객체 구현에 사용되었지만 java8 부터는 람다를 제공합니다

지역 클래스

네 가지 중첩 클래스 중 가장 드물게 사용됩니다.
지역 클래스는 지역 변수를 선언할 수 있는 곳이면 어디서든 선언할 수 있고, 유효 범위도 지역 변수와 같습니다.

public class LocalClass {

    public void hello() {
        class LocalExample {
            private String name;

            public LocalExample(String name) {
                this.name = name;
            }

            public String getName() {
                return name;
            }
        }

        LocalExample localExample = new LocalExample("윤호호");
        System.out.println(localExample.getName());
    }
}
public class Item24App {

    public static void main(String[] args) {
        LocalClass localClass = new LocalClass();
        localClass.hello();
    }
}

지역 클래스는 다른 중첩 클래스들의 공통점을 하나씩 가지고 있다.

  • 멤버 클래스처럼 이름을 가질수 있고 반복해서 사용할 수 있습니다
  • 비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있습니다
  • 정적 멤버는 가질 수 없으며, 가독성을 위해 짧게 작성되어야 합니다
블로그 이미지

yhmane

댓글을 달아 주세요

23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라

태그 달린 클래스

태그 클래스란, 두가지 이상의 의미를 표현할 때 그 중 현재 표현하는 의미를 태그값으로 알려주는 클래스 입니다.

책에 나온 예제로 살펴보도록 하겠습니다.

class Figure {

    enum Shape {
        RECTANGLE, CIRCLE
    }

    // 태그 필드
    final Shape shape;

    // RECTANGLE 용 필드
    double length;
    double width;

    // CIRCLE 용 필드
    double radius;

    // RECTANGLE
    Figure(double length, double width) {
        shape = Shape.RECTANGLE;
        this.length = length;
        this.width = width;
    }

    // CIRCLE
    Figure(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    }

    double area() {
        switch(shape) {
            case RECTANGLE:
                return length * width;
            case CIRCLE:
                return Math.PI * (radius * radius);
            default:
                throw new AssertionError(shape);
        }
    }
}

태그 클래스의 단점

  1. Enum switch, field 등 여러 구현이 혼합 되어 있습니다
  2. 이로 인해 가독성이 떨어집니다
  3. 사용하지 않는 필드로 인해 메모리도 추가적으로 사용합니다
  4. 또 다른 타입을 추가하게 되면 switch 문의 수정이 일어납니다
  5. 인스턴스 타입만으로 객체의 의미를 알 수 없습니다

태그가 달린 클래스는 코드가 길어지고 확장성에 취약합니다.
또한, 태그가 추가 될 때마다 사용하지 않는 필드들이 추가 될 수도 있습니다.


계층 클래스

계층 구조의 클래스를 만드는 방법

  1. 계층구조의 최상위(root)가 될 추상클래스를 정의합니다
  2. 태그값에 따라 달라지는 동작(메서드)들을 최상위 클래스의 추상 메서드로 선언합니다
  3. 태그값에 상관없이 동작이 일정한 메서드는 최상위 클래스에 일반 메서드로 정의합니다.
  4. 모든 하위 클래스에 공통으로 사용하는 상태값(필드)들은 루크 클래스에 정의합니다.

계층구조로 변환

abstract class Figure {
    abstract double area();
}

class Rectangle extends Figure {

    final double length;
    final double width;

    Rectangle(double length, double width) {
        this.length = length;
        this.width  = width;
    }

    @Override
    double area() {
        return length * width;
    }
}


class Circle extends Figure {

    final double radius;

    Circle(double radius) {
        this.radius = radius;
    }

    @Override
    double area() {
        return Math.PI * (radius * radius);
    }
}

클래스 계층구조의 장점

  1. 독립된 의미를 가지는 상태값(필드)들이 제거 되어 각 구현 클래스는 간결해집니다
  2. 살아남은 field는 모두 final이므로 정의할 수 있습니다
  3. 실수로 빼먹은 case 문으로 인해 runTime 에러를 방지할 수 있습니다
  4. 최상위 클래스를 수정하지 않고 타입을 확장 할 수 있습니다
  5. 타입사이의 자연스러운 계층 관계를 반영할 수 있습니다
블로그 이미지

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

댓글을 달아 주세요

Item15

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

잘 설계된 컴포넌트는 모든 내부 구현을 완벽히 숨겨, 구현과 API를 깔끔히 분리합니다. 정보 은닉, 혹은 캡슐화라고 하는 이 개념은 소프트웨어의 근간이 되는 원리입니다.

정보 은닉의 장점

  • 시스템 개발 속도를 높여줍니다. 여러 컴포넌트를 병렬로 개발할 수 있습니다.
  • 시스템 관리 비용을 낮춰줍니다.
  • 소프트웨어의 재사용성을 높여줍니다. 외부에 거의 의존하지 않고 독자적으로 동작하는 컴포넌트라면 낯선 환경에서도 유용하게 쓰일 가능성이 높습니다.
  • 큰 시스템을 제작하는 난이도를 낮춰줍니다.

Java의 접근제한자

  • private
    • 멤버를 선언한 톱레벨 클래스에서만 접근할 수 있습니다.
  • package-private
    • 멤버가 소속된 패키지 안의 모든 클래스에서 접근 할 수 있습니다.
  • protected
    • package-private의 접근 범위를 포함하며 선언한 클래스의 하위 클래스에서도 접근할 수 있습니다.
  • public
    • 어디에서나 접근 가능합니다.

클래스 레벨

  • 톱레벨 수준이 같은 수준에서의 접근제한자는 public과 package-private만 사용 할 수 있습니다.
    • public으로 선언하는 경우 - 공개 API로 사용하고 하위호환을 평생 신경써야 합니다.
    • package-private로 사용하는 경우 - 해당 패키지 안에서만 사용 가능하여 다음 릴리즈에서도 변경이 가능합니다.

public 클래스의 인스턴스 필드는 되도록 public이 아니어야 합니다.

필드가 가변객체를 참조하거나, final이 아닌 인스턴스 필드를 public으로 선언하면 불변식을 보장할 수 없습니다. public 가변 필드를 갖는 클래스는 일반적으로 thread safe 하지 않습니다.

  • 상수라면 관례대로 public static final 필드로 공개해도 좋습니다.
  • 하지만 클래스에서 public static final 배열 필드를 두면 안됩니다.
    • 배열을 private로 만들고 public 불변 리스트를 추가하거나 public 메서드를 추가하여 줍니다.

결론

  • 프로그램 요소의 접근성은 가능한 최소한으로 설계합니다.
  • public API는 필요한 것만 골라 최소한으로 설계합니다.
  • 상수용 public static final 필드 외에는 어떠한 public 필드도 가져서는 안됩니다. static final field 필드가 참조하는 객체가 불변인지도 확인해야 합니다.

참조

[effective java 3/e]
yhmane github

블로그 이미지

yhmane

댓글을 달아 주세요

14. Comparable을 구현할지 고려하라

Comparable의 compareTo() 단순 동치성 비교에 더해 순서까지 비교할  있으며 제네릭 합니다.

compareTo() 일반규약

  • 이 객체와 주어진 객체의 순서를 비교합니다.
  • 이 객체가 주어진 객체보다 작으면 음수를, 같으면 0을, 크면 양수를 반환합니다.
  • 이 객체와 비교할 수 없는 타입의 객체가 주어지면 ClassCastException을 던집니다.
  • x.compareTo(y) = y.compareTo(x) * -1 입니다. 또한 예외가 발생한다면 양쪽 표현식 모두 발생합니다.
  • 추이성을 보장합니다.
    • x.compareTo(y) > 0이고 y.compareTo(z) > 0 이면 x.compareTo(z) > 0도 성립합니다.
  • (x.compareTo(y) == 0) == (x.equals(y)) 이어야 합니다.
    • 이 권고를 지키지 않는 모든 클래스는 아래의 사실을 명시해야 합니다
    • “이 클래스의 순서는 equals 메서드와 일관되지 않다”
    • Collection, Set, Map 인터페이스들은 동치성을 비교할 때 equals 대신 compareTo를 사용하기 때문에 마지막 규약을 꼭 지키는 것이 좋습니다.

사용예제

  • 기본 타입 필드가 여럿일 때의 비교자

Comparable 이용

@Override
public int compareTo(StudentComparable o) {
    if (this.age > o.age) {
		return 1;
	} else if (this.age == o.age) {
		return 0;
	} else {
		return -1;
	}
}

Comparable, 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드

public int compareTo(StudentComparable o) {
    return Integer.compare(this.age, o.age);
}

Comparator 이용

Comparator anti-pattern

Collections.sort(list, (Comparator<Student>) (o1, o2) -> {
	if (o1.getAge() > o2.getAge()) {
		return 1;
	} else if (o1.getAge() == o2.getAge()) {
		return 0;
	} else {
		return -1;
	}
});

Comparator, 인터페이스가 제공하는 비교자 생성 메서드

Collections.sort(list, Comparator.comparingInt(Student::getAge));

 

overflow 문제로, Comparale, Compartor 에 함수 재정의시 anti-pattern을 피하도록 합니다!!

 


결론

  • 순서를 고려해야 하는 값 클래스를 작성할 경우 꼭 Comparable 인터페이스를 구현합니다.
  • 그 인스턴스들은 쉽게 정렬하고, 검색하고, 비교 기능을 제공하는 컬렉션과 어우러져야 합니다.
  • compareTo()는 필드의 값을 비교할 때 ‘<‘와 ‘>’ 연산자는 쓰지 말도록 합니다. overflow 문제로 인해
  • 대신 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용합니다.
블로그 이미지

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

댓글을 달아 주세요

03. Private 생성자나 열거 타입으로 싱글턴임을 보증하라

싱글턴(singleton)이란? 인스턴스를 오직 하나만 생성할 수 있느 클래스

싱글턴이란?

  • 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말합니다. (Application내에서 단 1개의 인스턴스만 생성할 수 있는 클래스)
  • 한번의 객체 생성으로 재사용이 가능하기 때문에 메모리 낭비를 방지할 수 있습니다.
  • 싱글톤으로 생성된 객체는 전역성을 띄기에 다른 객체와 공유가 용이합니다.

싱글턴을 언제 언제 쓸까요?

  • Stateless 객체일 때 (상태값이 없는 객체, 함수처럼)
  • 유일한 시스템 컴포넌트일 때

싱글턴의 단점

  • 싱글턴의 역할을 복잡하게 부여할 경우, 객체간의 결합도가 높아지는 문제가 발생살 수 있습니다.
  • 멀티 쓰레드 환경에서 동기화 처리 문제가 있습니다.
  • 인터페이스를 구현한 싱글턴 객체가 아니라면 mock 객체를 만들 수 없기에 테스트가 어렵습니다.
    • 그 싱글턴이 어떤 interface를 implement 하는게 아니라면 mock으로 바꿔치기 어렵습니다

싱글턴을 만드는 방법

  1. public final field 사용
  2. static factory 사용
  3. enum 사용 (가장 좋은 방법)

 

1. 필드 방식의 싱글턴

public class Elvis {

    public static final Elvis INSTANCE = new Elvis();

    private Elvis() { }

    public void leaveTheBuilding() {
        System.out.println("Whoa baby, I'm outta here!");
    }

    public static void main(String[] args) {
        Elvis elvis = Elvis.INSTANCE;
        elvis.leaveTheBuilding();
    }
}

장점

  • API를 보면 Singleton임이 분명하고, 간단합니다

단점

  • reflection을 사용하여 private 생성자를 호출할 수 있습니다

 

2. 정적 팩터리 방식의 싱글턴

public class Elvis {
    
    private static final Elvis INSTANCE = new Elvis();
    
    private Elvis() { }
    public static Elvis getInstance() { return INSTANCE; }

    public void leaveTheBuilding() {
        System.out.println("Whoa baby, I'm outta here!");
    }

    public static void main(String[] args) {
        Elvis elvis = Elvis.getInstance();
        elvis.leaveTheBuilding();
    }
}

장점

  • Singleton이 아니게 바꾸고 싶을 때, API를 바꾸지 않고 할 수 있습니다
  • getInstance()는 항상 같은 객체의 참조를 반환하므로 다른 Elvis 인스턴스가 만들어지지 않습니다
  • 또한 정적 팩터리를 제네릭 싱글턴 팩토리로 만들 수 있습니다.
  • 메서드 참조를 공급자로 사용할 수 있습니다. Elvis::getInstance

단점

  • reflection을 사용하여 private 생성자를 호출할 수 있습니다.
  • 위의 장점들이 필요 없다면 방법1이 낫습니다.

3. Enum 방식의 싱글턴

public enum Elvis {

    INSTANCE;

    public void leaveTheBuilding() {
        System.out.println("leave the building");
    }

    // 이 메서드는 보통 클래스 바깥(다른 클래스)에 작성해야 한다!
    public static void main(String[] args) {
        Elvis elvis = Elvis.INSTANCE;
        elvis.leaveTheBuilding();
    }
}

가장 이상적인 방법입니다

장점

  • 간결하고 추가 노력 없이 직렬화할 수 있습니다.
  • 리플렉션의 경우에도 막아줄 수 있습니다. 따라서, 가장 이상적인 방법이라 할 수 있습니다

단점

  • 단, 만드려는 싱글턴이 Enum 외에 클래스를 상속해야 한다면 이 방법은 쓸 수 없습니다

 

주의할 점

필드, 정적팩터리 중의 하나의 방식으로 싱글턴 클래스를 직렬화할 경우, Serializable 만으로는 부족합니다.
모든 인스턴스 필드를 일시적으로 선언하고, resolve 메서들 제공해야 합니다. (ITEM 89)
이렇게 하지 않으면 역지렬화할때 마다 새로운 인스턴스가 만들어집니다.

 

참조

- effective-java 3판

블로그 이미지

yhmane

댓글을 달아 주세요