Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Item 49,50,51 매개변수 유효성 & 방어적 복사 & API설계 요령 #31

Open
olrlobt opened this issue Dec 27, 2023 · 0 comments
Assignees

Comments

@olrlobt
Copy link
Contributor

olrlobt commented Dec 27, 2023

section: (8장 Item 49,50,51 )


매개변수가 유효한지 검사하라

매개변수 검사를 제대로 하지 못하면, 메서드가 수행되는 중간에 모호한 예외를 던지거나 어떤 객체를 이상한 상태로
만들어 놓아서 이 메서드와는 관련 없는 오류를 내기도 한다.

다시 말해 매개변수 검사에 실패하면 실패 원자성을 어기는 결과를 낳는다.

실패 원자성

특정 연산이 실패했을 때 시스템의 상태를 연산이 시작되기 전의 상태로 되돌리는 성질로 추후 아이템76에서 다룬다.

예외 문서화

public과 protected 메서드는 매개변수 값이 잘못됐을 때 던지는 예외를 문서화해야 한다.

보통은 IllegalArgumentException , IndexOutOfBounds Exception, NullPointerException
중 하나를 던진다. 매개변수의 제약을 문서화 한다면 그 제약을 어겼을 때 발생하는 예외도 함께 기술해야 한다.

이러한 방식은 API 사용자가 제약을 지킬 가능성을 크게 높여준다.

/**
 * (현재  값 mod m) 값을 반환한다. 이 메서드는
 * 항상 음이 아닌 BigInteger를 반환한다는 점에서 remainder 메서드와 다르다.
 *
 * @param m 계수 (양수여야 한다.)
 * @return 현재 값 mod m
 * @throws ArithmeticException m이 0보다 작거나 같으면 발생한다.
 */

public BigInteger mod(BigInteger m){
        if(m.signum()<=0){ // signum() 은 부호를 반환한다.
            throw new ArithmeticException("계수(m)는 양수여야 합니다"+m);
        }
}

위 예시는 BigInterger 클래스 수준에서 기술했기 때문에, 각 메서드에 적용되는 것들은 각 메서드에
일일이 주석하지 않고 클래스 수준 주석을 이용한다.

클래스 수준 주석은 그 클래스의 모든 public 메서드에 적용되므로 각 메서드에
일일이 기술하는 것보다 훨씬 깔끔한 방법이다.

@nullable

@Nullable과 비슷한 어노테이션을 사용해 특정 매개변수가 null이 될 수 있다고 알려 줄 수 있지만 표준적인 방법은 아니고,
일반적인 방법이다.
그리고 이와 같은 목적으로 사용할 수 있는 애너테이션도 여러가지다. ( @Optional, @MayBeNull )

requireNonNull

requireNonNull 메서드는 유연하고 사용하기도 편하니, 더 이상 null 검사를 수동으로 하지 않아도 된다.

this.say = Objects.requireNonNull(say, "할 말이 없네"); // say가 Null 이면 "할 말이 없네"를 출력

반환값은 무시하고 어디서든 null 검사 목적만으로 사용해도 된다.

자바 9에서는 Objects에 범위 검사 기능도 더해졌다.

checkIndex(int index, int length);
checkFromToIndex(int fromIndex, int toIndex, int length); // toIndex는 포함하지 않는다.
checkFromIndexSize(int fromIndex, int size, int length);

위와 같은 메서드들인데 null 검사 만큼이나 유연하지는 않다.
예외 메시지 지정이 불가능하고, 리스트와 배열 전용으로 설계 되었다.
또한 닫힌 범위는 다루지 못한다.

assert

public이 아닌 메서드라면 단언문(assert)를 사용해 매개변수 유효성을 검증할 수 있다.

assert 문 (단언문)은 주로 개발 및 테스트 단계에서 매개변수의 유효성을 검증하거나 코드의
특정 조건이 충족되는지 확인하는 데 사용할 수 있다.

assert 문은 조건이 true일 때 아무런 동작을 하지 않지만, false일 경우 AssertionError 를 발생시킨다.

private void haveAnithingToSay(Object say) {
    assert say != null : "할 말이 없네";
    // 메서드의 나머지 부분
}

하지만, assert문은 운영 환경에서 비활성화 되기 때문에, assert에만 의존해서는 안 된다.

생성자

생성자에서 매개변수의 유효성을 검사하는 것은 클래스의 불변식(invariants)을 유지하는 데 중요하다.

public class Doctor {
    private final String say;

    public Doctor(String say) {
        if (say == null || say.isEmpty()) {
            throw new IllegalArgumentException("할 말이 없으면 홍박사가 될 수 없습니다.");
        }
    }

    // ... getter 메서드 및 기타 메서드 ...
}

암묵적 유효성 검사

개발자가 직접 조건을 검사하는 코드를 작성하지 않아도, 시스템이나 컴파일러, 런타임 환경이 기본적으로 제공하는 기능을 통해 수행

int say;
say = "할 말은 있긴 한데 .."; // 컴파일 오류 발생

int[] say = new int[3];
int say = say[5]; // ArrayIndexOutOfBoundsException 발생

List<String> 자동차에서시원한곳차운데 = Collections.unmodifiableList(new ArrayList<>(Arrays.asList("이승헌", "조다민", "홍박사")));
자동차에서시원한곳차운데.add("김낄껴규현"); // UnsupportedOperationException 발생

결론

"매개변수에 제약을 두는게 좋다" 라고 해석해서는 안 된다.
메서드는 최대한 범용적으로 설계해야 한다.





적시에 방어적 복사본을 만들어라

데이터의 직접적인 참조를 통한 접근을 피하고, 대신 복사본을 사용하는 것.

이렇게 함으로써 객체의 상태가 외부 요소에 의해 예기치 않게 변경되는 것을 방지한다.

public class Period {
    private Date start;
    private Date end;

    public Period(Date start, Date start) {
        // 방어적 복사: 외부에서 제공된 Date 객체의 복사본을 저장
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());

        if (this.start.compareTo(this.end) > 0) {
            throw new IllegalArgumentException( this.start + "가 "+ this. end + "보다 늦다.");
        }
        
    }

    public Date getStart() {
        // 방어적 복사: 내부 Date 객체의 복사본을 반환
        return new Date(start.getTime());
    }
}

clone을 사용한 복사본은 기대했던 Date 객체가 아닌 하위 클래스 타입의 객체가 될 수 있기 때문에,
새 객체를 반환하는 것이 좋다.

Date는 낡은 API이니 새로운 코드를 작성할 때는 Instant나, LocalDateTime, ZonedDateTime을 사용하자.

Set과 Map은 외부에서 컬렉션의 참조를 통해 내부 상태를 변경할 수 있는 경우이므로
방어적 복사를 사용해야한다.

또한, 방어적 복사 비용이 너무 크거나 클라이언트가 요소를 잘못 수정할 일이 없음을 신뢰한다면,
방어적 복사를 수행하는 대신 해당 구성요소를 수정했을 때의 책임이 클라이언트에 있음을
문서에 명시하도록하자.





메서드 시그니처를 신중히 설계하라 (API 설계 요령)

1. 메서드 이름을 신중히

2. 편의 메서드를 너무 많이 만들지 말자

메서드가 너무 많은 클래스는 익히고, 사용하고, 문서화하고, 테스트하고, 유지보수하기 어렵다.
아주 자주 쓰일 경우에만 별도의 약칭 메서드를 두자.

확신이 서지 않는다면 그냥 만들지 말자.

3. 매개변수 목록은 짧게

4개 이하가 좋다. 같은 타입의 매개변수가 여러 개가 연달아 나오는 경우 특히 해롭다.
사용자가 실수로 순서를 바꿔 입력해도 그대로 컴파일 되고 실행된다. 단지 의도와 다르게 동작할 뿐.

과하게 긴 매개변수 목록을 짧게 줄여주는 기술 세 가지

1. 여러 매서드로 쪼갠다.

2. 매개변수 여러 개를 묶어주는 도우미 클래스를 만든다.

// 도우미 클래스
public class Address {
    private final String city;
    private final String street;
    private final String zipCode;

    public Address(String city, String street, String zipCode) {
        this.city = city;
        this.street = street;
        this.zipCode = zipCode;
    }

    // getter 메서드 등...
}

// 사용 예
public class User {
    private final Address address;
    // 추가 필드

    public User(Address address) {
        this.address = address;
        // 추가 필드
    }

    // User 클래스의 다른 메서드들...
}

3. 두 기법을 합친 빌더패턴

public class Address {
    private final String city;
    private final String street;
    private final String zipCode;

    // Address 클래스의 생성자 및 메서드들...

    public static class Builder {
        
        // 생략..

        public Address build() {
            return new Address(city, street, zipCode);
        }
    }
}

public class User {
    private final Address address;
    private final String name;
    private final int age;

    // User 클래스의 생성자 및 메서드들...

    public static class Builder {
        
        // 생략..

        public User build() {
            return new User(address, name, age);
        }
    }
}

    // 사용 예
    User user = new User.Builder()
            .name("John Doe")
            .age(30)
            .address(new Address.Builder()
                    .city("New York")
                    .street("5th Avenue")
                    .zipCode("10001")
                    .build())
            .build();



매개변수의 타입으로는 클래스보다 인터페이스

매서드에 HashMap을 넘길 일은 전혀 없다.
인터페이스 대신 클래스를 사용하면, 특정 구현체만 사용하도록 제한하는 꼴이며,
혹시라도 입력 데이터가 다른 형태로 존재한다면 비싼 복사 비용을 치러야 한다.

Boolean보다는 원소 2개짜리 열거 타입

(메서드 이름상 boolean을 받아야 더 명확할때는 예외)

public enum Status {
    GOOD,
    NODAB
}

public class YoonGi {
    private Status status;

    public YoonGi() {
        this.status = Status.NODAB;
    }
    
//    private boolean isNODAB;
//    
//    public YoonGi() {
//        this.isNODAB = false;
//    }
}

reference

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant