반응형

이 챕터에서는 우리가 자주 사용하는 extends(상속을 통한 확장) 보다 해당 클래스를 참조하는 private 필드를 선언해서 사용하는 것이 더 좋은 이유에 대해 설명한다.

 

(이 챕터 내용과 비슷하게 스프링 바이블인 '토비의 스프링' 책에서도 관심사의 분리, 제어의 역전, 콜백 패턴 등을 설명할 때 계승보다는 구성을 사용하는 것이 좋다고 설명한다.)

 

계승의 문제점

계승은 상위 클래스와 하위 클래스가 밀접한 관계를 가진다. 하위 클래스는 상위 클래스의 구현에 의존할 수밖에 없기 때문에 캡슐화 원칙을 위반한다. 즉 상위 클래스의 구현이 변경되면 하위 클래스는 기존에 구현한 코드가 원하는 방식으로 동작하지 않을 수 있다.

 

계승을 하기 위해서는 상위 클래스 구현에 대해 잘 알아야 한다. 그렇지 않을 경우 하위 클래스가 원하는 방식으로 동작하지 않을 수 있는데 예를 들어보자.

 

java.util에서 제공하는 HashSet 클래스를 상속하여 원소가 추가될 때마다 카운팅을 하는 클래스를 아래와 같이 구현했다.

public class CountHashSet<E> extends HashSet<E> {
	private int addCount = 0;

	public CountHashSet() {

	}

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

 

심플한 클래스로 잘 동작할 것 같지만 addAll()을 호출할 경우 문제가 발생한다. addAll()에서 super.addAll()은 java.util.AbstractCollection 클래스의 addAll()을 호출하는데 구현 코드는 아래와 같다.

    public boolean addAll(Collection<? extends E> c) {
        boolean modified = false;
        for (E e : c)
            if (add(e))
                modified = true;
        return modified;
    }

 

코드를 보면 알겠지만 addAll()에서 다시 add()를 호출한다. 따라서 위에서 구현한 코드는 중복 계산되므로 addAll()을 호출하게 되면 항상 원소의 두 배만큼 증가한다.

 

해당 버그를 fix 하기 위해 addAll() 재정의 메서드를 제거하더라도 나중에 addAll()에서 add()를 호출하지 않으면 문제가 다시 발생한다. 다른 방법으로 super.addAll()을 호출하지 않고 직접 구현하면 된다. 하지만 상위 클래스의 동작을 위해 직접 구현하기 어려울 뿐 아니라, 접근 불가능한 필드가 있을 경우 구현은 아예 불가능하다.

 

이외에도, 상위 클래스에 또 다른 원소를 추가하는 메서드가 생겨나게 된다면 하위 클래스에서는 매번 릴리즈 노트를 확인해야 할 것이다.

 

하위 클래스가 상위 클래스를 망가뜨릴 수 있다. 만약 HashSet에 특정 조건을 만족해야지만 add 할 수 있는 메서드가 있다면 하위 클래스도 add를 재정의할 때 해당 조건을 추가해서 구현해야지 상위 클래스를 만족한다. 하지만 잘못된 사용자는 하위 클래스를 이용하여 해당 조건을 넣지 않아 상위 클래스를 망가뜨릴 수 있다.

 

계승보다 구성

위와 같은 계승의 문제점은 구성(필드 선언)을 통해 해결할 수 있다. 소스는 아래와 같다.

public class CountHashSetByComposit<E> {
	private int addCount = 0;
	private Set<E> s;

	public CountHashSetByComposit(Set<E> s) {
		this.s = s;
	}

	public boolean add(E e) {
		addCount++;
		return s.add(e);
	}

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

 

(책에서는 Set 인터페이스를 구현하는 전달 클래스를 하나 만들어 Wrapper 클래스로 예를 들었다.)

 

위 클래스는 어떤 Set 객체를 인자로 받아 필요한 부분만 사용하고, 추가적인 기능은 직접 구현하였다. 계승 대신 구성을 하게 되면 사용하고자 하는 메서드에 대해서만 이해하면 되고, 해당 클래스의 API의 일부가 변경되어도 구성을 사용한 클래스를 알 필요 없다.

 

계승은 상위 클래스와 하위 클래스 사이에 IS-A 관계가 확실할 때만 사용하는 것이 좋다. IS-A 관계가 확실하다고 해도 내가 구현한 상위 클래스가 아니라면 계승보다는 구성이 낫다. 즉 계승은 상위 클래스와 하위 클래스 구현을 같은 개발자가 구현한다면 고려해도 된다. 하지만 구성을 하게 되면 하위 클래스보다 견고할 뿐 아니라, 더 강력하다.

반응형
반응형

Immutable(이하 불변) 클래스는 객체를 한 번 만들게 되면 상태(필드의 값)를 변경할 수 없는 클래스이다.

이 글에서는 불변 클래스의 구변 방법을 설명하고 특징을 설명한다.

 

불변 클래스 구현 방법

자바에서 객체 필드의 값을 변경하지 못하게 하는 final 키워드, 외부에서 접근 불가능하게 하는 접근 제어자 등을 사용하여 불변 클래스를 쉽게 구현할 수 있다.

 

먼저 자바에서 제공하는 클래스 중 불변 클래스인 Integer 클래스의 소스는 아래와 같다.

package java.lang;

...

// 2. final을 사용하여 다른 클래스에서 상속하지 못하도록 한다.
public final class Integer extends Number implements Comparable<Integer> {
	...
    

    // 1. 모든 필드를 final로 선언한다.
    // 4. 모든 필드를 private로 선언한다.
    private final int value;
    
    public int intValue() {
        return value;
    }
    
 	...
    
    // 1. 값을 변경하는 메서드를 제공하지 않는다.
    // 5. 변경 가능한 객체에 대한 참조가 있다면, 외부에서는 해당 참조를 얻을 수 없도록 해야한다.
}

먼저 Integer에서 실제 값을 저장하는 value 필드는 한 번 값이 초기화되면 더 이상 수정할 수 없도록 1. final 키워드를 사용하였고, 외부에서 해당 필드를 접근하지 못하도록 4. private로 선언하였다. 또한 만약 value를 1. 수정할 수 있는 메서드가 제공된 다면 외부에서 해당 메서드를 이용하여 수정 가능하므로 제공하지 않았다.

 

또한 규칙 13에서 설명한 내용과 비슷하게 5. 변경 가능한 객체에 대한 참조가 있다면 외부에서는 해당 참조를 얻을 수 없도록 해야 한다. 만약 아래와 같이 Array 필드인 data를 외부에서 접근 가능하도록 getter 메서드를 제공한다면 참조 값은 변경할 수 없지만, 참조에 대한 객체는 수정이 가능하므로 불변 클래스가 깨지게 된다.

private static final Thing[] data = { ... };

 

또한 2. 클래스에 final을 선언하여 외부에서 Integer를 상속할 수 없도록 하였다. 상속을 허용한다면 하위 클래스에서는 메서드를 재정의할 수 있게 된다. 이 것이 문제가 될 수 있는데 예를 들어 intValue() 메서드는 변경 불가능한 값인 value를 리턴하는 메서드인데, 하위 클래스가 악의적으로 다른 값을 반환하게 재정의 한다면 마치 변경된 것처럼 보인다. 이처럼 잘못 작성되는 하위 클래스를 방지하기 위해 상속할 수 없도록 한다.

 

사실 상속할 수 없도록 하는 방법에는 클래스에 final로 선언하는 방법 말고 한 가지 더 유연한 방법이 있다. 바로 private나 package-private로 된 생성자만을 제공하고, 외부에는 규칙 1에서 설명한 정적 팩토리 메서드를 제공하는 것이다. public이나 protected로 선언된 생성자가 없기 때문에 외부에서는 상속을 할 수 없다. 또한 그렇기 때문에 외부에서는 객체를 생성자로 생성할 수 없으므로 정적 팩토리 메서드를 제공한다.

 

 

불변 클래스 특징

불변 클래스의 장점을 설명하기 위해 이번엔 자바에서 제공하는 또 다른 불변 클래스인 BigInteger에 대해 먼저 설명한다. 소스는 다음과 같다.

package java.math;

...

public class BigInteger extends Number implements Comparable<BigInteger> {

    final int signum;
    final int[] mag;
    
    ...

    // 1. 불변 클래스의 객체는 자유롭게 공유될 수 있다.
    public static final BigInteger ZERO = new BigInteger(new int[0], 0);
    public static final BigInteger ONE = valueOf(1);

    ...
    
    public BigInteger negate() {
        // 2. 불변 클래스안에 필드들은 서로 공유 가능하다.
        return new BigInteger(this.mag, -this.signum);
    }
    
    public BigInteger flipBit(int n) {
        if (n < 0)
            throw new ArithmeticException("Negative bit address");

        int intNum = n >>> 5;
        int[] result = new int[Math.max(intLength(), intNum+2)];

        for (int i=0; i < result.length; i++)
            result[result.length-i-1] = getInt(i);

        result[result.length-intNum-1] ^= (1 << (n & 31));

        return valueOf(result);
    }
    
    private static BigInteger valueOf(int val[]) {
        // 3. 불변 클래스는 값을 변경할 수 없기때문에 새로운 객체를 만든다. 
        return (val[0] > 0 ? new BigInteger(val, 1) : new BigInteger(val));
    }

   ...
}

BigInteger 클래스는 불변 클래스이지만 Integer 클래스에서 설명했던 내용과 다른 부분이 존재한다. 먼저 필드를 private로 선언하지 않고 package-private로 선언하였다. 사실 위에서 설명한 원칙들은 다소 과하기 때문에 필요하다면 일부 완화할 수 있다. package-private도 높은 수준의 접근 제어자이기 때문에 어느 정도 수용 가능하다.

 

하지만 private로 선언하지 않은 것과는 다르게 BigInteger를 개발할 때 큰 실수를 하였는데, 바로 상속을 못하도록 방어하지 않았다. 불행히도 하위 호환성 때문에 이는 고쳐지지 않고 사용되고 있다. 그러나 나머지 구현 부분은 불변 클래스의 규칙을 따른다.

 

 

1. 불변 클래스의 객체는 자유롭게 공유할 수 있다.

불변 클래스의 객체는 절대 변하지 않는다. 따라서 외부에 공유해도 해당 객체를 수정할 수 있는 방법이 없기 때문에 문제가 없다. BigInteger는 자주 사용되는 값(ZERO, ONE 등)을 미리 선언하여 재사용하는데, 이 필드들은 public으로 선언되어 있다. public으로 선언되면 외부에서 수정하는 문제가 발생할 수 있겠지만 BigInteger는 불변 클래스이므로 수정이 불가능하므로 자유롭게 공유할 수 있다.

 

2. 불변 클래스 안에 필드들은 서로 공유 가능하다.

불변 클래스 안에 필드들은 당연하게도 절대 변하지 않는다. 따라서 객체들끼리 필드를 공유하여도 변경되지 않으므로 서로 공유가 가능하다. 예를 들어 BigInteger에서 negate() 메서드는 양수이면 음수를, 음수이면 양수로 변환하여 BigInteger 객체를 생성하는 메서드이다. 이때 msg 필드는 배열이기 때문에 서로 공유하면 문제가 될 수 있다. 하지만 BigiInteger는 불변 클래스이기 때문에 msg 배열은 절대 변경되지 않으므로 새로운 객체와 원래 객체가 같은 배열을 참조해도 어떠한 문제도 발생하지 않는다.

 

3. 불변 클래스는 값을 변경할 수 없기 때문에 새로운 객체를 만든다. 

불변 클래스는 값을 변경할 수 없기 때문에 대부분 새로운 객체를 만들어 반환되도록 작성한다. 이는 불변 클래스의 유일한 단점인데, 만약 객체를 생성하는 비용이 크다면 문제가 발생한다. BigInteger의 flipBit() 메서드는 특정 위치의 bit 값을 반대로 변경하는 메서드이다. 만약 BigInteger의 값이 매우 큰 값이어서 bit 배열이 엄청 크다면, 새로운 BigInteger의 값을 생성하기 위해서 동일한 크기의 bit 배열을 하나 새롭게 만들고, 특정 위치의 bit 값을 변경해주고 객체를 생성해야 한다. 이렇게 되면 bit 배열만큼의 시간과 공간이 소요되기 때문에 문제가 발생한다.

 

이외에 불변 클래스는 단순하며 스레드에 안전하다. 불변 클래스는 값을 변경할 수 없다. 따라서 값이 혹시 null인지, 비정상 범위의 값인지에 대해서 객체를 생성할 때 미리 검사한다면, 클라이언트는 두려워하지 않고 사용하기만 하면 된다. 또한 값이 변경되지 않는다는 것은 스레드에 안전하다.

 

또한 불변 클래스는 다른 불변 클래스의 필드로 사용될 수 있다. 따라서 새로운 불변 클래스를 작성할 때 기조 불변 클래스에 변경 가능성에 대해 걱정할 필요 없다.    

 

 

요약

- 변경 가능한 클래스로 만들 이유가 없다면 불변 클래스로 먼저 만들어 보자.

- 변경 가능한 클래스를 만들어야 한다고 해도, 최대한 변경 가능성을 제한하자.

 

  

 

 

 

 

 

 

 

 

반응형
반응형

규칙 13에서 객체 필드 변수는 절대로 public으로 선언하지 말고 getter와 같은 접근자 메서드를 사용하라고 설명했다.

규칙 14에서는 위 내용에 대해 간단하게 설명한다.

 

객체 필드 변수를 public으로 선언하게 되면 외부에서 데이터를 쉽게 조작할 수 있어 캡슐화의 이점을 누릴 수 없다.

 

예를 들어 양수만 표현하는 Point 클래스를 아래와 같이 선언한다면 생성자에서는 음수를 넣을 수 없지만 정상적으로 객체 생성 후 클라이언트가 음수를 직접 세팅할 수 있다.

public class PositivePointWithPublicField {
	public double x;
	public double y;

	public PositivePointWithPublicField(double x, double y) {
		if (x < 0 || y < 0) {
			throw new IllegalArgumentException();
		}

		this.x = x;
		this.y = y;
	}
}

public class Client {
	public static void main(String[] args) {
		PositivePointWithPublicField point = new PositivePointWithPublicField(0, 0);
		point.x = -5;
		point.y = -10;

	}
}

 

하지만 필드를 private로 선언하고 getter를 사용하게 되면 위를 방지할 수 있다.

public class PositivePointWithPrivateField {
	private double x;
	private double y;

	public PositivePointWithPrivateField(double x, double y) {
		if (x < 0 || y < 0) {
			throw new IllegalArgumentException();
		}

		this.x = x;
		this.y = y;
	}

	public double getX() {
		return x;
	}

	public double getY() {
		return y;
	}
}


public class Client {
	public static void main(String[] args) {
		PositivePointWithPrivateField point = new PositivePointWithPrivateField(0, 0);

		// 컴파일 에러
		point.x = -5;
		point.y = -10;

	}
}

 

getter 메서드(또한 setter)는 너무나 많이 사용하기 때문에 IDE에서 자동으로 생성해주는 기능이다. 뿐만 아니라 lombok과 같은 라이브러리에서는 어노테이션을 사용하여 자동으로 생성할 수 있다.

@Getter
public class PositivePointWithPrivateField {
	private double x;
	private double y;

	public PositivePointWithPrivateField(double x, double y) {
		if (x < 0 || y < 0) {
			throw new IllegalArgumentException();
		}

		this.x = x;
		this.y = y;
	}

}

 

다만 Immutable 필드(final 선언)라면 초기에 한 번만 설정할 수 있으므로 public으로 선언하여도 아래와 같이 클라이언트가 조작하지 못하므로 public으로 선언해도 된다. 하지만 굳이 이럴 바에는 getter를 사용하는 게 더 맞다. 또한 public 필드를 두게 되면 후에 호환성을 위해 항상 해당 필드를 유지해야 한다. 하지만 getter를 제공한다면 이미 작성된 클라이언트 코드는 깨트리지 않을 수 있다.

public class PositivePoinWithFinalField {
	public final double x;
	public final double y;

	public PositivePoinWithFinalField(double x, double y) {
		if (x < 0 || y < 0) {
			throw new IllegalArgumentException();
		}

		this.x = x;
		this.y = y;
	}
}

public class Client {
	public static void main(String[] args) {
		PositivePoinWithFinalField point = new PositivePoinWithFinalField(0, 0);

		// final이서 컴파일 에러
		point.x = -5;
		point.y = -10;

	}
}

 

참고

자바에서 제공하는 클래스 중에 규칙 14를 깨트린 클래스들이 존재한다. 따라서 이런 클래스는 코딩할 때 참고하지 않는 것이 좋다.

 

// java.awk.Point
public class Point extends Point2D implements java.io.Serializable {
    public int x;
    public int y;
     
    // ...
}

// java.awk.Dimension
public class Dimension extends Dimension2D implements java.io.Serializable {
    public int width;
    public int height;
    
    // ...
}
반응형
반응형

규칙 13에서는 소프트웨어 설계의 기본적인 원칙 가운데 하나인 캡슐화에 관해서 설명한다.

 

캡슐화는 다른 모듈 간 커플링을 최소화하기 위하여 구현 세부사항을 전부 API 뒤쪽에 숨긴다. 그리고 모듈들은 이 API를 통해서만 서로를 호출한다. 디커플링을 하게 되면 서로 간에 의존성이 낮아지기 때문에 한쪽이 변경되어도 다른 모듈에 영향을 끼칠 걱정이 없다. 또한 다른 개발에서도 유용하게 쓰일 수 있다는 장점이 있다. 자바는 이런 장점을 사용하기 위해 캡슐화를 지원하며 그중 대표적으로 접근 제어(Access Control) 기능을 제공한다.

 

원칙은 단순하다. 가능한 한 다른 곳에서 접근 불가능하도록 만들어라. 클라이언트에게 제공하는 API만 public 또는 protected로 하라.

 

 

1. 클래스와 인터페이스에 public을 붙이지 않을 수 있는지 확인해보자.

public으로 한번 릴리스한 클래스와 인터페이스는 호환성을 보장하기 위해 계속 제공해야 한다. (대부분 지키지 않지만)

 

예를 들어 자바에서는 Enum에 특화된 Set을 제공하는 EnumSet을 제공하는데, EnumSet을 public으로 제공하여 누구나 사용할 수 있다. 하지만 실제로 정적 팩토리 메서드가 제공하는 객체는 RegularEnumSet, JumboEnumSet이 있는데 이 둘은 직접적으로 사용할 수 없도록 package private 접근 제어자를 사용한다. 즉 jdk 개발자는 RegularEnumSet 클래스를 변경하던, 삭제하던 호환성에 대해 걱정할 필요 없다. 

// EnumSet은 public으로 추상 클래스 제공
public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
    implements Cloneable, java.io.Serializable
{
	...
    public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        Enum<?>[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");

        if (universe.length <= 64)
            return new RegularEnumSet<>(elementType, universe);
        else
            return new JumboEnumSet<>(elementType, universe);
    }
	...
}

// package private
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> {
	...
}

 

2. 클래스의 멤버 변수들은 최대한 package-private까지만 지원하자. protected도 광범위하다.

객체 필드변수는 절대로 public으로 선언하면 안 된다. 필드를 public으로 선언하면 필드에 저장될 값을 제한할 수 없게 되어 그 필드에 관계된 불변식을 강제할 수 없다. 그러므로 getter와같은 접근자 메서드를 사용하는 게 좋다.

 

단, 상수는 public static final로 선언될 수 있다. 아래는 BigDeimal의 상수 예시이다.

    // Cache of common small BigDecimal values.
    private static final BigDecimal ZERO_THROUGH_TEN[] = {
        new BigDecimal(BigInteger.ZERO,       0,  0, 1),
        new BigDecimal(BigInteger.ONE,        1,  0, 1),
        new BigDecimal(BigInteger.TWO,        2,  0, 1),
        new BigDecimal(BigInteger.valueOf(3), 3,  0, 1),
        new BigDecimal(BigInteger.valueOf(4), 4,  0, 1),
        new BigDecimal(BigInteger.valueOf(5), 5,  0, 1),
        new BigDecimal(BigInteger.valueOf(6), 6,  0, 1),
        new BigDecimal(BigInteger.valueOf(7), 7,  0, 1),
        new BigDecimal(BigInteger.valueOf(8), 8,  0, 1),
        new BigDecimal(BigInteger.valueOf(9), 9,  0, 1),
        new BigDecimal(BigInteger.TEN,        10, 0, 2),
    };

	public static final BigDecimal ZERO = zeroThroughTen[0];
	public static final BigDecimal ONE = zeroThroughTen[1];
	public static final BigDecimal TEN = zeroThroughTen[10];

 

하지만 상수라도 기본 자료형 값들을 갖거나, 변경 불가능 객체를 참조하게 해야 한다.

public static final Thing[] VALUES = { ... };

위 코드와 같이 길이가 0이 아닌 배열 상수 필드는 배열 레퍼런스는 변경하지 못하지만 배열 레퍼런스 안에 존재하는 값은 변경할 수 있다. 따라서 public static final 배열 필드를 두거나, 배열 필드를 반환하는 접근자를 정의하면 안 된다.

 

따라서 클라이언트에게는 배열 필드의 상수 값을 복사해서 넘겨주는 방식으로 이 문제를 해결한다.

방법 1.

	private static final String[] UNMODIFY_NUM_LIST = { "1", "2", "3", "4" };
	public static final List<String> NUM_LIST_2 = Collections.unmodifiableList(Arrays.asList(UNMODIFY_NUM_LIST));

방법 2.

	private static final String[] CLONE_NUM_LISE = { "1", "2", "3", "4" };
	public static final String[] NUM_LIST_3() {
		return CLONE_NUM_LISE.clone();
	}

 

3. 클래스의 메서드도 최대한 접근 권한을 줄이자.

클래스의 메서드도 최대한 접근 권한을 줄여야 한다. 하지만 접근 권한을 줄일 수 없는 경우가 있다. 상위 클래스 메서드를 오버라이딩할 때는 원래 메서드의 접근 권한보다 낮은 권한을 설정할 수 없다.

 

 

결론은 접근 권한은 가능한 낮춰라.

 

반응형
반응형

최상위 객체인 Object에 선언되어 있는 메서드는 아니지만 자주 사용되는 compareTo 메서드에 대해 설명한다.

 

int java.lang.String.compareTo(String anotherString)

자바에서 기본적으로 제공하는 값 클래스(value class)는 compareTo 메서드가 구현되어 있으며 또한 String 클래스에도 compareTo가 구현되어 있다. String.compareTo 메서드는 문자열을 비교하여 같으면 0을, 사전순으로 작으면 음수를, 사전순으로 크면 양수를 리턴한다.

	public static void main(String[] args) {
		String str1 = "apple";

		System.out.println(str1.compareTo("banana")); // -1
		System.out.println(str1.compareTo("apple")); // 0
		System.out.println(str1.compareTo("abc")); // 14
	}

 

String.compareTo 메서드의 소스코드는 아래와 같다.

    public int compareTo(String anotherString) {
        int len1 = value.length;
        int len2 = anotherString.value.length;
        int lim = Math.min(len1, len2);
        char v1[] = value;
        char v2[] = anotherString.value;

        int k = 0;
        while (k < lim) {
            char c1 = v1[k];
            char c2 = v2[k];
            if (c1 != c2) {
                return c1 - c2;
            }
            k++;
        }
        return len1 - len2;
    }

1. 문자를 하나씩 비교하여 다를 경우 차를 구해 바로 리턴한다.

2. 길이가 작은 문자열의 길이 만큼 비교하여도 같다면 문자열의 길이가 작은게 사전순으로 더 앞에 있다고 판단하고 차를 구해 리턴한다.

 

이렇게 compareTo 메서드는 해당 클래스 객체들의 자연적 순서를 비교할 수 있도록 기능을 제공한다. 그리고 다들 알겠지만 compareTo는 일반적인 메서드가 아닌 Comparable 인터페이스에 정의되어 있는 유일한 메서드이다.

 

Comparable 인터페이스

Comparable 인터페이스는 클래스 객체에 순서를 정의하도록 한다. 이 순서를 클래스의 자연 순서라고 하며 클래스의 compareTo 메소드는 자연 순서를 계산할 수 있도록 기능을 구현해야 한다. 그래서 위와 같이 String 클래스는 Compareable 인터페이스를 구현하였고, 자연 순서를 계산할 수 있도록 compareTo 메서드를 구현한 것이다.

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
	...
}

 

규칙9에서 설명했던 hashCode는 규약을 따르지 않으면 hashCode를 사용하는 자바 API(ex : HashMap) 동작에 문제가 발생한다고 설명했다. 마찬가지로 Comparable의 compareTo 메서드를 사용하는 자바 API도 여러 개 존재하는데 compareTo 규약을 준수하지 않을 경우 API를 오동작시킬 수 있다. 아래는 compareTo를 사용하는 API의 예시이다.

 

- void java.util.Arrays.sort(Object[] a)

	public static void arraySort() {
		String arr[] = { "banana", "apple", "google", "aws" };

		Arrays.sort(arr);
		System.out.printf("%s\n", Arrays.toString(arr)); // [apple, aws, banana, google]

		Arrays.sort(arr, Collections.reverseOrder());

		System.out.printf("%s\n", Arrays.toString(arr)); // [google, banana, aws, apple]
	}

 

- java.util.TreeSet<E>

	public static void treeSet() {
		String arr[] = { "banana", "apple", "google", "aws" };

		Set<String> s = new TreeSet<String>();
		s.addAll(Arrays.asList(arr));

		System.out.println(s); // [apple, aws, banana, google]
	}

 

그렇다면 이제 compareTo를 잘 구현해야한다. 규약을 요약하자면 아래와 같다.

 

- Symmetry(대칭성)

X가 Y보다 크다면 Y는 X보다 작아야 한다. 반대로 X가 Y보다 작다면 Y는 X보다 커야 한다. 또한 X가 Y랑 같다면 Y도 X랑 같아야 한다.

 

- Transitivity(추이성)

X가 Y보다 크고, Y가 Z보다 크다면 X는 Z보다 커야한다.

 

- Consistent(일관성)

X와 Y가 같다면 X와 Z와의 비교 결과와 Y와 Z의 비교 결과는 항상 같다.

 

(이펙티브 자바 책에서는 위 순서를 반사성, 대칭성, 추이성이라고 표현하지만, 필자는 잘못되었다고 생각한다.)

 

- (강력히 추천하지만 절대적으로 요구되는 것은 아닌) X와 Y의 compareTo의 결과가 0이라면 equlas의 결과는 true여야 한다.

네번째 규약은 특별한 경우가 아니라면 지키는게 좋다. 자바에서 제공하는 대부분의 컬렉션 구현체들은 마지막 규약이 지켜졌다고 생각하고 동작한다. 만약 규약을 지키지 않으면 엄청난 재앙을 가져오진 않지만, 그래도 약간의 문제가 발생할 수 있다.

 

네번째 규약을 지키지 않은 대표적인 클래스가 있는데 예시는 아래와 같다.

	public static void main(String[] args) {
		BigDecimal a = new BigDecimal("1.0");
		BigDecimal b = new BigDecimal("1.00");

		System.out.println(a.equals(b)); // false
		System.out.println(a.compareTo(b)); // 0
		System.out.println((a.compareTo(b) == 0) == a.equals(b)); // false

		Set<BigDecimal> hashSet = new HashSet<BigDecimal>();
		hashSet.add(a);
		hashSet.add(b);

		System.out.println(hashSet.size()); // 2

		Set<BigDecimal> treeSet = new TreeSet<BigDecimal>();
		treeSet.add(a);
		treeSet.add(b);

		System.out.println(treeSet.size()); // 1
	}

BigDecimal은 소수점까지 모두 동일해야지 동일한 것으로 판단한다. 하지만 compare는 크기를 비교하므로 0을 리턴한다. 따라서 네번 째 규약을 지키지 않는 클래스이다. 이렇기 때문에 equals(hashCode)로 동일성을 판단하는 HashSet에서는 두 원소를 넣어도 다르다고 판단하여 두 개의 객체가 존재한다. 하지만 TreeSet은 compareTo로 비교하기 때문에 두 객체는 동일하다고 판단하여 하나의 객체만 존재하게 된다.

 

근데 만약 이미 구현되어 있는 클래스의 compareTo 순서 비교 방식이 아닌 새로운 순서 비교 방식을 사용하고 싶다면 어떻게 해야할까? 이럴 경우 Comparator 인터페이스를 구현하여 사용하면 된다.

 

Comparator 인터페이스

일반적으로 Comparable.compareTo를 사용하는 메서드는 Comparator의 compare 메서드를 사용할 수 있도록 구현되어있다. (ex : Collections.sort 또는 Arrays.sort) 따라서 미리 구현되어 있는 compareTo 방식이 아닌 새로운 방식으로 순서를 지정하고 싶다면 Comparator를 구현하면 된다.

 

아래는 예시이다.

	public static void main(String[] args) {
		String arr[] = { "banana", "apple", "google", "aws" };

		Arrays.sort(arr, new Comparator<String>() {
			public int compare(String o1, String o2) {
				return o2.compareTo(o1); // o1을 o2와 비교한 것이 아닌 o2를 o1과 비교
			}
		});

		System.out.printf("%s\n", Arrays.toString(arr)); // [google, banana, aws, apple]

	}

 

Arrays.sort에 새롭게 구현한 Comparator 구현체를 사용하였다. 자세히 보면 기존 o1.compareTo(o2)가 아닌 o2.compareTo(o1)을 사용하여 사전순이 아닌 역사전순으로 비교하도록 하였다.

 

이렇게 자신이 원하는 순서를 다시 재정의할 수 있는데, 자바에서는 자주 사용하는 비교 방식의 Comparator를 미리 제공한다. 위에 예시였던 arraySort() 메서드에서 보면 Collections.reverseOrder()를 사용하였는데, 이는 우리가 지금 구현한 Comparator와 동일하게 구현되어 있는 구현체이다. 이 외에도 String 클래스에서는 대소문자를 무시하여 비교하는 구현체를 제공한다. 아래는 해당 소스이다.

    public static final Comparator<String> CASE_INSENSITIVE_ORDER
                                         = new CaseInsensitiveComparator();
    private static class CaseInsensitiveComparator
            implements Comparator<String>, java.io.Serializable {
        // use serialVersionUID from JDK 1.2.2 for interoperability
        private static final long serialVersionUID = 8575799808933029326L;

        public int compare(String s1, String s2) {
            int n1 = s1.length();
            int n2 = s2.length();
            int min = Math.min(n1, n2);
            for (int i = 0; i < min; i++) {
                char c1 = s1.charAt(i);
                char c2 = s2.charAt(i);
                if (c1 != c2) {
                    c1 = Character.toUpperCase(c1);
                    c2 = Character.toUpperCase(c2);
                    if (c1 != c2) {
                        c1 = Character.toLowerCase(c1);
                        c2 = Character.toLowerCase(c2);
                        if (c1 != c2) {
                            // No overflow because of numeric promotion
                            return c1 - c2;
                        }
                    }
                }
            }
            return n1 - n2;
        }

        /** Replaces the de-serialized object. */
        private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
    }

 

반응형
반응형

자바에서 최상위 객체인 Object는 하위 객체들이 오버라이딩하여 사용하도록 설계된 메서드들이 있다. (equals, hashCode, toString, clone, finalize) 그리고 이 메서드들은 일반 규약이 존재하는데 이를 따르지 않으면 자바에서 제공하는 클래스와 함께 사용할 때 제대로 동작하지 않는다.

 

이번 장에서는 clone 메서드에 대해 설명한다.

 

Cloneable 인터페이스

clone 메서드는 Object 클래스에 정의되어 있고, 하위 객체가 오버라이딩하여 재정의하여 사용한다. 하지만 clone 메서드를 정상적으로 사용하기 위해서는 추가적으로 해당 클래스가 Cloneable 인터페이스를 implements 해야한다.

 

사실 이런 구현방식은 정말 기괴한 방식이다. Cloneable 인터페이스에 clone 메서드가 정의만 되어 있었어도 Cloneable 인터페이스를 구현하는 클래스는 강제적으로 clone 메서드를 구현해야 하기때문에 아주 상식적인 방식으로 clone의 동작 방식을 이해할 수 있었을 것이다.

 

하지만 자바는 Cloneable을 Marker Interface로 구현하였고, natvie 메서드인 Object.clone()에서 instanceof를 사용하여 Cloneable을 implement한 클래스인 경우 clone 메서드를 호출하고, Cloneable를 implement하지 않은 클래스인 경우 CloneNotSupportedException 예외를 던지도록 구현되어 있다.

 

아래는 Cloneable의 소스이다.

package java.lang;

/**
 * A class implements the <code>Cloneable</code> interface to
 * indicate to the {@link java.lang.Object#clone()} method that it
 * is legal for that method to make a
 * field-for-field copy of instances of that class.
 * <p>
 * Invoking Object's clone method on an instance that does not implement the
 * <code>Cloneable</code> interface results in the exception
 * <code>CloneNotSupportedException</code> being thrown.
 * <p>
 * By convention, classes that implement this interface should override
 * <tt>Object.clone</tt> (which is protected) with a public method.
 * See {@link java.lang.Object#clone()} for details on overriding this
 * method.
 * <p>
 * Note that this interface does <i>not</i> contain the <tt>clone</tt> method.
 * Therefore, it is not possible to clone an object merely by virtue of the
 * fact that it implements this interface.  Even if the clone method is invoked
 * reflectively, there is no guarantee that it will succeed.
 *
 * @author  unascribed
 * @see     java.lang.CloneNotSupportedException
 * @see     java.lang.Object#clone()
 * @since   JDK1.0
 */
public interface Cloneable {
}

 

Object.clone() 메서드

위에서 언급한 것처럼 Object.clone의 동작방식은 기괴하지만 널리 사용되고 있으므로 알아둬야 한다. 먼저 Object.clone() 메서드는 아래와 같다.

    /**
     * Creates and returns a copy of this object.  The precise meaning
     * of "copy" may depend on the class of the object. The general
     * intent is that, for any object {@code x}, the expression:
     * <blockquote>
     * <pre>
     * x.clone() != x</pre></blockquote>
     * will be true, and that the expression:
     * <blockquote>
     * <pre>
     * x.clone().getClass() == x.getClass()</pre></blockquote>
     * will be {@code true}, but these are not absolute requirements.
     * While it is typically the case that:
     * <blockquote>
     * <pre>
     * x.clone().equals(x)</pre></blockquote>
     * will be {@code true}, this is not an absolute requirement.
     * <p>
     * By convention, the returned object should be obtained by calling
     * {@code super.clone}.  If a class and all of its superclasses (except
     * {@code Object}) obey this convention, it will be the case that
     * {@code x.clone().getClass() == x.getClass()}.
     * <p>
     * By convention, the object returned by this method should be independent
     * of this object (which is being cloned).  To achieve this independence,
     * it may be necessary to modify one or more fields of the object returned
     * by {@code super.clone} before returning it.  Typically, this means
     * copying any mutable objects that comprise the internal "deep structure"
     * of the object being cloned and replacing the references to these
     * objects with references to the copies.  If a class contains only
     * primitive fields or references to immutable objects, then it is usually
     * the case that no fields in the object returned by {@code super.clone}
     * need to be modified.
     * <p>
     * The method {@code clone} for class {@code Object} performs a
     * specific cloning operation. First, if the class of this object does
     * not implement the interface {@code Cloneable}, then a
     * {@code CloneNotSupportedException} is thrown. Note that all arrays
     * are considered to implement the interface {@code Cloneable} and that
     * the return type of the {@code clone} method of an array type {@code T[]}
     * is {@code T[]} where T is any reference or primitive type.
     * Otherwise, this method creates a new instance of the class of this
     * object and initializes all its fields with exactly the contents of
     * the corresponding fields of this object, as if by assignment; the
     * contents of the fields are not themselves cloned. Thus, this method
     * performs a "shallow copy" of this object, not a "deep copy" operation.
     * <p>
     * The class {@code Object} does not itself implement the interface
     * {@code Cloneable}, so calling the {@code clone} method on an object
     * whose class is {@code Object} will result in throwing an
     * exception at run time.
     *
     * @return     a clone of this instance.
     * @throws  CloneNotSupportedException  if the object's class does not
     *               support the {@code Cloneable} interface. Subclasses
     *               that override the {@code clone} method can also
     *               throw this exception to indicate that an instance cannot
     *               be cloned.
     * @see java.lang.Cloneable
     */
    protected native Object clone() throws CloneNotSupportedException;

 

clone 메서드는 native 메서드로 작성되어 있으며 Cloneable 인터페이스를 구현하지 않은 클래스가 해당 메서드를 사용할 경우 CloneNotSupportedException 예외를 던지도록 정의되어 있다.

 

clone 메서드도 마찬가지로 규약이 존재하지만 다른 메서드의 비해 규약이 느슨한다.

 

1. x.clone() != x will be true, and that the expression.

객체의 복사본을 만들어서 반환하는 것이기 때문에 원본과 복사본은 동일할 수는 없다.

 

2. x.clone().getClass() == x.getClass() will be true, but these are not absolute requirements.

원본과 복사본의 클래스 타입은 동일해야하지만, 반드시 그래야 하는 것은 아니다. 하지만 이 규약은 너무 느슨하다. clone을 사용하는 입장에서는 원본과 복사본의 타입이 동일 할 것이라고 생각한다

 

3.  x.clone().equals(x) will be true, this is not an absolute requirement.

 

By convention, the returned object should be obtained by calling super.clone. If a class and all of its superclasses (except Object) obey this convention, it will be the case that x.clone().getClass() == x.getClass().

 

 

--- 추후 추가 작성 

 

반응형
반응형

이전 블로그에서 2017년 7월 21일에 작성한 글


Constant Interface를 위키에서 보다가 Constant Interface는 런타임 시에는 사용할 목적이 없지만, Marker Interface는 런타임 시에 사용할 목적이 있다고 표현되어 있었다. 고로 Marker Interface도 인터페이스이지만 메서드 선언이 없는 인터페이스라고는 추측은 되지만 그렇다면 어떤 용도로 사용하는지 자료를 찾아보았다.

 

Marker Interface

Marker Interface란 아무것도, 즉 변수와 메서드를 정의하지 않은 인터페이스이다. 먼저 대표적인 Marker Interface에는 ​자바에서 종종 보는 Serializable, Cloneable​ 인터페이스가 있다. 대학원을 다녔을 때, 오브젝트 정보를 파일로 저장하고 불러올 때 사용을 해봤지만 안에 어떤 것이 들어있는지 확인은 하지 않았다. 아래는 ​Cloneable​의 코드이다.

package java.lang;

/**
 * A class implements the <code>Cloneable</code> interface to
 * indicate to the {@link java.lang.Object#clone()} method that it
 * is legal for that method to make a
 * field-for-field copy of instances of that class.
 * <p>
 * Invoking Object's clone method on an instance that does not implement the
 * <code>Cloneable</code> interface results in the exception
 * <code>CloneNotSupportedException</code> being thrown.
 * <p>
 * By convention, classes that implement this interface should override
 * <tt>Object.clone</tt> (which is protected) with a public method.
 * See {@link java.lang.Object#clone()} for details on overriding this
 * method.
 * <p>
 * Note that this interface does <i>not</i> contain the <tt>clone</tt> method.
 * Therefore, it is not possible to clone an object merely by virtue of the
 * fact that it implements this interface.  Even if the clone method is invoked
 * reflectively, there is no guarantee that it will succeed.
 *
 * @author  unascribed
 * @see     java.lang.CloneNotSupportedException
 * @see     java.lang.Object#clone()
 * @since   JDK1.0
 */
public interface Cloneable {
}

 

그렇다면 Marker Interface는 어디에 사용될까? instanceof 연산자를 사용하여 런타임에서 객체에 대한 타입을 확인하여 프로그램의 흐름을 제어할 수 있다. 즉 Maker Interface는 특별한 기능을 하는 것이 아닌 단순히 해당 객체의 타입을 구분하는 정도로 사용한다.

 

예를 들어 오브젝트를 입력받아 스트림에 쓰는 ObjectOutputStream.writeObject(Object obj)는 Object를 인자로 받는다. 이 매소드는 writeObject0(Object obj, ...)​을 호출한다. 이 매소드는 Object의 타입에 따라 다시 알맞은 매소드를 호출한다. 아래는 writeObject0(Object obj, ...)​의 코드 일부이다. (String, Enum도 Serializable이지만 처리를 다르게 하려고 구분한 것으로 보임)

public class ObjectOutputStream extends OutputStream implements ObjectOutput, ObjectStreamConstants {

 // ...
 private void writeObject0(Object obj, boolean unshared) throws IOException {

  // ...
  if (obj instanceof String) {
   writeString((String) obj, unshared);
  } else if (cl.isArray()) {
   writeArray(obj, desc, unshared);
  } else if (obj instanceof Enum) {
   writeEnum((Enum<?>) obj, desc, unshared);
  } else if (obj instanceof Serializable) {
   writeOrdinaryObject(obj, desc, unshared);
  } else {
   if (extendedDebugInfo) {
    throw new NotSerializableException(cl.getName() + "\n" + debugInfoStack.toString());
   } else {
    throw new NotSerializableException(cl.getName());
   }
  }

  // ...
 }
}
반응형
반응형

이전 블로그에서 2017년 7월 20일에 작성한 글


 

프로젝트 소스를 보다가 상수(Constant)를 인터페이스(Interface)에 정의한 것을 발견했다. 학교 수업과 책으로 자바를 배웠을 때, 항상 상수는 클래스에 정의한 것만 봐왔던 나는 생소했다. 당연히 초보 개발자로서, 특별한 장점이 있으므로 실무에서는 인터페이스를 사용하지 않을까 생각해 자료를 찾아보았다. 결론부터 말하면 사용을 추천하지 않는 Anti 패턴이다.

 

Constant Interface

Constant Interface란 오직 상수만 정의한 인터페이스이다. 인터페이스의 경우, ​변수를 등록할 때 자동으로 public static final​이 붙는다. 따라서 상수처럼 어디에서나 접근할 수 있다. 그 뿐만 아니라 하나의 클래스에 여러 개의 인터페이스를 Implement 할 수 있는데, Constant Interface를 Implement 할 경우, ​인터페이스의 클래스 명을 네임스페이스로 붙이지 않고 바로 사용할 수 있다. 이러한 편리성 때문에 Constant Interface를 사용한다. 아래는 간단한 예이다. 

public interface Constants {
 double PI = 3.14159;
 double PLANCK_CONSTANT = 6.62606896e-34;
}

public class Calculations implements Constants {

 public double getReducedPlanckConstant() {
  return PLANCK_CONSTANT / (2 * PI);
 }
}

 

Constant Interface 문제점

Constant Interface를 써도 컴파일이 안 되는 것도 아니고 그렇게 잘못된 것 같지는 않아 보인다. 하지만 위키 Effective Java (규칙19) 책을 보면 다음과 같은 이유로 Anti 패턴으로 간주한다.

 

1. Implement 할 경우 사용하지 않을 수도 있는 상수를 포함하여 모두 가져오기 때문에 계속 가지고 있어야 한다.

 

2. 컴파일할 때 사용되겠지만, 런타임에는 사용할 용도가 없다. (Marker Interface는 런타임에 사용할 목적이 있으므로 다름)

 

3. Binary Code Compatibility (이진 호환성)을 필요로 하는 프로그램일 경우, 새로운 라이브러리를 연결하더라도, 상수 인터페이스는 프로그램이 종료되기 전까지 이진 호환성을 보장하기 위해 계속 유지되어야 한다.

 

4. IDE가 없으면, 상수 인터페이스를 Implement 한 클래스에서는 상수를 사용할 때 네임스페이스를 사용하지 않으므로, 해당 상수의 출처를 쉽게 알 수 없다. 또한 상수 인터페이스를 구현한 클래스의 하위 클래스들의 네임스페이스도 인터페이스의 상수들로 오염된다.

 

5.  인터페이스를 구현해 클래스를 만든다는 것은, 해당 클래스의 객체로 어떤 일을 할 수 있는지 클라이언트에게 알리는 행위이다. 따라서 상수 인터페이스를 구현한다는 사실은 클라이언트에게는 중요한 정보가 아니다. 다만, 클라이언트들을 혼동시킬 뿐이다.

 

6. 상수 인터페이스를 Implement 한 클래스에 같은 상수를 가질 경우, 클래스에 정의한 상수가 사용되므로 사용자가 의도한 흐름으로 프로그램이 돌아가지 않을 수 있다. 아래는 간단한 예제이다.

public interface Constants {
 public static final int CONSTANT = 1;
}

public class Class1 implements Constants {

 public static final int CONSTANT = 2; // *

 public static void main(String args[]) throws Exception {
  System.out.println(CONSTANT);
 }
}

 

Constant Interface 대안

자바문서에서 Constant Interface를 Anti 패턴으로 명시하였고 이 방안으로 ​import static 구문​ 사용을 권장한다.​ Constant Interface와 동일한 기능과 편리성을 제공한다. 아래는 간단한 예제이다.

public final class Constants {
 private Constants() {
  // restrict instantiation
 }

 public static final double PI = 3.14159;
 public static final double PLANCK_CONSTANT = 6.62606896e-34;
}



import static Constants.PLANCK_CONSTANT;
import static Constants.PI;

public class Calculations {

 public double getReducedPlanckConstant() {
  return PLANCK_CONSTANT / (2 * PI);
 }
}

 

반응형

+ Recent posts