인자의 유효성을 검사하라.
유효성 검사를 통해 인자가 유효하지 않을 때 적절한 예외를 발생시키면. 빠르고 깔끔하게 메소드를 종료할 수 있다.
Public 메소드의 경우 javadoc의 @throws 태그를 써서 인자값이 제약 조건을 어겼을 때 던지는 예외를 문서화 해야한다.
public class Big { /** * (this mod m)의 값을 가지는 BigInteger를 리턴한다. * 이 메소드는 항상 음수가 아닌 BigInteger를 리턴한다는 점이 * remainder 메소드와 다르다 * * @param m 나누는 수, 반드시 양수이다. * @return this BigInteger를 m으로 나눈 몫. 항상 양수이다. * @throws ArithmeticException m <=0 일 때 발생한다. */ public BigInteger mod(BigInteger m) { if(m.signum() <= 0) throw new ArithmeticException("Modulus not positive"); //...실제 계산 코드 return null; } }
필요한 경우 방어복사하라.
클래스의 모든 클라이언트는 항상 불변규칙을 깨뜨리기 위해 최선을 다한다고 가정하고, 프로그래밍할 때 항상 방어하는 자세를 가져야 한다.
다음 클래스는 두 날짜 사이의 기간을 표시한다.
//가짜 불변 클래스 public final class Period { private final Date start; private final Date end; /** * @param start 시작일. * @param end 종료일. 반드시 시작일 이후이다. * @throws IllegalArgumentException 시작일이 종료일보다 늦을 때 발생한다. * @throws NullPorinterException 시작일이나 종료일이 null일때 발생한다. */ public Period(Date start, Date end) { if(start.compareTo(end) > 0) throw new IllegalArgumentException(start + "after" + end); this.start = start; this.end = end; } public Date start() { return start; } public Date end() { return end; } //...이하 생략 }
Date 클래스가 가변 클래스이기 때문에 불변규칙은 쉽게 깨진다.
//Period 인스턴스의 내부를 공격하라. Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); end.setYear(78); //p를 수정한다.
이런 종류의 공격으로부터 Period 인스턴스 내부를 보호하려면, 생성자에 전달되는 변경가능 인자들을 방어복사(defensive copy)해야한다.
//인자를 방어복사한다. public Period(Date start, Date end) { this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); if(start.compareTo(end) > 0) throw new IllegalArgumentException(start + "after" + end); }
생성자를 이렇게 하면 앞에 나왔던 공격 패턴은 더 이상 먹히지 않는다. 인자의 유효성 검사를 하지 전에 먼저 복사하고 나서 원본이 아닌 복사본의 유효성을 검사한다는 것에 주목할 필요가 있다.
개선한 생성자가 모든 공격을 막아낼 수 있을 것 같지만, 아직도 Period 인스턴스를 공격할 수 있는 방법이 있다.
//Period 인스턴스에 대한 다른 공격 Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); p.end().setYear(78); //p를 수정한다.
접근자 메소드에서 가변 객체에 대한 참조를 방어복사하여 리턴하도록 고쳐야 이런 종류의 공격을 막아 낼 수 있다.
public Date start() { return (Date) start.clone(); } public Date end() { return (Date) end.clone(); }
Period의 내부 Date 객체는 java.util.Date 클래스의 인스턴스라는 것이 확실하기 때문에 clone 메소드를 써도 아무 문제가 없다.
메소드를 중복정의할 때는 신중하라.
다음 예제는 컬렉션을 종류에 따라 분류할 목적으로 만든 것이다.
//틀린 구현 - 메소드 중복정의를 잘못 쓰고 있다. public class CollectionClassifier { public static String classify(Set s) { return "Set"; } public static String classify(List l) { return "List"; } public static String classify(Collection c) { return "Uknow Collection"; } public static void main(String[] args) { Collection[] tests = new Collection[] { new HashSet(), //Set new ArrayList(), //List new HashMap().values() //Set도 List도 아닌 것 }; for(int i=0; i<tests.length; i++) System.out.println(classify(tests[i])); } }
아마 다음과 같은 출력을 예상했을 것이다.
Set List Unknown Collection
하지만, 실제로는 “Unknown Collection” 만 세 번 출력한다.
이 문제를 해결하려면 세개의 classify 메소드를 하나로 합치고 명시적으로 instanceof를 써서 타입을 검사해야 한다.
public static String classify(Collection c) { return (c instanceof Set ? "Set" : (c instanceof List ? "List" : "Unknow Collecion")); }
널( null)이 아닌 길이가 0인 (zero-length)배열을 리턴하라.
다음과 같이 메소드를 만드는 경우를 많이 볼 수 있다.
private List cheeseInStack = …; /** * 상점에 남아 있는 모든 치즈의 배열을 리턴하거나, * 팔 수 있는 치즈가 없으면 null을 리턴한다. */ public Cheese[] getCheeses() { if(cheeseInStack.size() == 0) return null; //...코드 생략 }
팔 수 있는 치즈가 남아 않으면 다음과 같이 처리 할 수 있다.
Cheese[] cheese = shop.getCheeses(); public void in() { if(Arrays.asList(shop.getCheeses()).contains(Cheese.STILTON)) System.out.println("Jolly good, just the thing."); }
길이가 0인 배열을 리턴할 수 있는 곳에서는 null을 처리하는 코드가 반복되어야 한다. 배열을 리턴하는 메소드에서 null을 리턴할 이유가 전혀 없다. Null을 리턴할 상황이라면 길이가 0인 배열을 리턴하면 된다.
다음 코드는 null대신에 길이가 0인 배열을 리턴한다.
Main.java
class Cheese { String name; Cheese(String name) { this.name = name; } public static final Cheese STILTON = new Cheese("Stilton"); public static final Cheese CHEDDAR = new Cheese("Cheddar"); } class CheeseShop { private static Cheese[] ECA = new Cheese[0]; private List cheesesInStock = Collections.singletonList(Cheese.STILTON); private final static Cheese[] NULL_CHEESE_ARRAY = new Cheese[0]; /** * @return Cheese[] 팔 수 있는 모든 치즈의 배열을 리턴한다. */ public Cheese[] getCheeses() { return (Cheese[]) cheesesInStock.toArray(NULL_CHEESE_ARRAY); } } public class Main { static CheeseShop shop = new CheeseShop(); public static void main(String[] args) { Cheese[] cheeses = shop.getCheeses(); if (Arrays.asList(cheeses).contains(Cheese.STILTON)) System.out.println("Jolly good, just the thing."); if (Arrays.asList(cheeses).contains(Cheese.CHEDDAR)) System.out.println("Oops, too bad."); } }
'개발자 센터 > Java' 카테고리의 다른 글
자바 프로그래밍 일반 (1) | 2009.12.14 |
---|---|
자바 클래스를 싱글 패턴으로 구현하기 (0) | 2009.12.14 |