Java/Effective Java

[Effective Java] Chapter 8. 메서드

댕주 2024. 10. 27. 23:13

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


"오류는 가능한 한 빨리 (발생한 곳에서) 잡아야 한다"

메서드 몸체가 실행되기 전에 매개변수를 확인한다면 잘못된 값이 넘어왔을 때 즉각적이고 깔끔한 방식으로 예외를 던질 수 있다.

 

public과 protected 메서드는 매개변수 유효성 검사를 통해 던질 수 있는 예외를 반드시 문서화하여 API 사용자가 메서드의 동작을 명확히 이해하고 예외 처리를 설계할 수 있도록 한다

Javadoc 의 @throws 태그를 사용해 예외 유형과 발생 조건을 설명하면, API의 신뢰성과 사용 편의성을 높일 수 있다.

 

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

그렇다면 왜 단언문을 사용할까?

주로 개발 중에만 활성화되어 디버깅과 테스트 시에만 유효성 검사를 수행할 수 있기 때문에, public이 아닌 비공개 메서드의 유효성 검증을 간편하게 처리하는 데 유용하다. 단언문이 활성화 되지 않은 프로덕션 환경에서는 무시되므로, 성능에 영향을 주지 않는다.

 

생성자는 "나중에 쓰려고 저장하는 매개변수의 유효성을 검사하라"는 원칙의 특수한 사례다

생성자는 객체가 처음 생성될 때, 잘못된 상태로 초기화되는 것을 방지하기 위해 매개변수의 유효성을 검사하는 것이 중요하다

왜 생성자에서 매개변수 유효성 검사가 필요할까?

1. 객체의 불변성 보장: 생성자는 객체가 생성될 때 단 한 번만 호출되므로, 생성 시점에 잘못된 매개변수가 들어오지 않도록 유효성을 검증하면 객체의 불변성을 보장할 수 있다.

2. 잘못된 상태의 객체 생성 방지: 매개변수를 검증하지 않으면 잘못된 상태의 객체가 생성되어 이후에 발생할 수 있는 오류를 야기할 수 있다.

3. 문제 조기 발견: 생성자에서 유효성을 검사하여 문제를 가능한 빨리 발견하고, 잘못된 매개변수를 명확하게 파악할 수 있다.

 

생성자에서 유효성을 검사하지 않으면 객체가 불완전하거나 유효하지 않은 상태로 초기화될 수 있으며, 이러한 객체를 사용할 때 런타임 오류나 예상치 못한 동작이 발생할 가능성이 높다.

 

유효성 검사 비용이 지나치게 높거나 실용적이지 않을 때, 혹은 계산 과정에서 암묵적으로 검사가 수행될 때는 메서드 몸체가 실행되기 전 유효성 검증을 하는 것이 비효율적일 수 있다.

 

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


객체의 불변성을 유지하고 외부 변경으로부터 객체를 보호하기 위해 적시에 방어적 복사본을 만드는 것을 다루고 있는 아이템이다.

주로 객체가 외부에서 전달받은 가변 객체를 멤버 변수로 가질 때 발생할 수 있는 위험을 방지하기 위해 복사본을 만들어서 사용하는 방법에 대해서 적혀있다.

예를 들어, 생성자나 setter 메서드에 가변 객체가 전달될 때, 해당 객체의 참조를 직접 할당하는 대신 방어적 복사를 만들어 내부에서만 사용하게 하는 것이다. 이 방식으로 외부에서 객체가 변경되더라도 원본은 안전하게 유지된다.

 

매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사해야 한다. 특히 외부에서 전달받은 가변 객체가 있는 경우 유용하다. 이 과정이 필요한 이유는, 외부 코드가 전달한 객체가 유효성 검사 이후에 변경될 가능성이 있기 때문이다.

 

Item 51. 메서드 시그니처를 신중히 설계하라


1. 메서드 이름을 신중히 짓자

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

3. 매개변수 목록은 짧게 유지하자 (되도록 4개 이하!)

- 같은 타입의 매개변수가 여러 개가 연달아 나오는 것을 지양

 

과하게 긴 매개변수 목록 짧게 쪼개기

1. 여러 메서드로 쪼갠다

- 쪼개진 메서드 각각은 원래 매개변수 목록의 부분집합을 받는다

 

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

- 일반적으로 도우미 클래스는 정적 멤버 클래스로 둔다

 

3. 1+2 = 객체 생성에 사용한 빌더 패턴을 메서드 호출에 응용한다

- 모든 매개변수를 하나의 추상화한 객체를 정의한다.

- 클라이언트에서 이 객체의 세터(setter) 메서드를 호출해 필요한 값을 설정한다

- 이때 각 세터 메서드는 매개변수 하나 혹은 서로 연관된 몇 개만 설정하게 한다

- 클라이언트는 필요한 매개변수를 다 설정한 다음, execute 메서드를 호출해 앞서 설정한 매개변수들의 유효성을 검사한다

- 설정이 완료된 객체를 넘겨 원하는 계산을 수행한다.

 

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

public void example(Collections<String> stringCollection) {
    // 인터페이스를 사용해 list, set 등 다양한 컬렉션을 인자로 받을 수 있다
}

 

boolean보다는 원소 2개짜리 열거 타입이 낫다.

public void exampleMethod(boolean isActive) {
    // isActive true, false 의미가 명확하지 않음
}

public enum ExEnum {
    ACTIVE,
    INACTIVE
}

public void exampleMethod2(ExEnum exEnum) {
    if (status == ExEnum.ACTIVE) {
    }
}

 

Item 52. 다중정의는 신중히 사용하라


재정의한 메서드(오버라이딩)는 동적으로 선택되고, 다중정의한 메서드(오버로딩)는 정적으로 선택되기 때문이다.

- 재정의(오버라이딩): 서브클래스가 슈퍼클래스의 메서드를 같은 이름, 반환형, 매개변수로 다시 정의하는 것.

- 다중 정의(오버로딩): 같은 클래스 내에서 같은 이름의 메서드를 서로 다른 매개변수로 정의하는 것.

 

재정의(오버라이딩): 동적 선택

메서드 재정의는 동적 디스패치(dynamic dispatch)라고 불리는 메커니즘을 통해 실행 시점에 메서드가 결정 된다. 객체가 슈퍼클래스 타입으로 선언되었더라도, 실제 객체의 타입에 따라 메서드가 호출된다.

class Animal {	
    void sound() {
        // Animal Sound Logic
    }
}

class Dog extends Animal {
    @Overriding
    void sound() {
        // dog sound
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myDog = new Dog(); // Animal 타입이지만 실제 객체는 Dog
        animal.sound(); // dog sound logic 수행
    }
}

이 경우 myDog 의 타입은 Animal 이지만 실제로 Dog 객체를 참조하고 있기 때문에 실행 시점에 Dog 클래스의 sound() 메서드가 선택된다. 이렇게 재정의된 메서드는 동적(런타임)으로 선택되며, 이를 통해 다형성을 구현할 수 있다.

 

다중 정의(오버로딩): 정적 선택

메서드 다중 정의는 정적 디스패치(static dispatch)에 의해 컴파일 시점에 어떤 메서드가 호출될지 결정된다. 즉, 메서드 호출 시점에 컴파일러가 메서드 시그니처(매개변수의 개수, 타입)를 보고 어떤 메서드를 호출할지 결정한다.

class Cooking {
    void cook(String receipe) {
        // 로직1
    }
    
    void cook(int minutes) {
        // 로직2
    }
}

public class Main {
    public static void main(String[] args) {
        Cooking cooking = new Cooking();
        cooking.cook("Pasta"); // 로직1
        cooking.cook(15); // 로직2
    }
}

여기서 cook(String receipe) 와 cook(int minutes) 는 다중 정의된 메서드이다. 메서드의 선택은 컴파일 시점에 이루어지므로 정적 선택이라고 한다. 따라서 어떤 메서드가 호출될지는 매개변수의 타입에 따라 컴파일 시점에 결정된다.

 

메서드를 다중정의할 때, 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안된다.

=> 람다 표현식이나 메서드 참조를 사용할 때 컴파일어가 어떤 메서드를 호출해야 할지 결정할 수 없는 모호성을 일으키기 때문이다.

 

Item 53. 가변인수는 신중히 사용하라


가변인수 메서드는 호출될 때마다 배열을 새로 하나 할당하고 초기화한다. => 비효율적

1. 가변인수와 고정 인수를 함께 사용할 경우, 가변인수를 마지막에 배치하기

2. 최소한의 인수 개수 강제하기

3. 가변인수 대신 컬렉션 사용고려하기

public void ex() {}
public void ex(int a1) {}
public void ex(int a1, int a2) {}
public void ex(int a1, int a2, int a3) {}
public void ex(int a1, int a2, int... rest) {}

 

EnumSet의 정적 팩토리도 이 기법을 사용해 열거 타입 집합 생성 비용을 최소화한다.

EnumSet: 특정 열거형(enum) 상수 집합을 효율적으로 표현할 수 있도록 설계된 컬렉션으로, 정적 팩토리 메서드를 통해 가변인수를 안전하게 사용하는 방법을 제공한다.

- EnumSet.of(E first, E... rest): 하나 이상의 열거형 값을 받아 EnumSet을 생성한다.

 

Item 54. null이 아닌, 빈 컬렉션이나 배열을 반환하라


null을 반환하면 메서드 호출자가 결과를 받을 때 null 여부를 항상 검사하지 않으면 NullPointerException이 발생할 수 있다.

빈 컬렉션: Collections.emptyList(), Collections.emptySet(), Collections.emptyMap()

빈 배열: new String[0]

 

Item 55. 옵셔널 반환은 신중히 하라


옵셔널: 원소를 최대 1개 가질 수 있는 '불변' 컬렉션이다. 그렇다고 Option<T>가 Collection<T>를 구현했다는 게 아니라 원칙적으로 그렇다는 것이다.

옵셔널을 반환하는 메서드에서는 절대 null을 반환하지 말자. => 옵셔널을 도입한 취지를 완전히 무시하는 것!

옵셔널은 검사 예외와 취지가 비슷한데, 반환 값이 없을 수도 있음을 API 사용자에게 명확히 알려준다.

 

컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안된다. 빈 Optional<List<T>> 보다는 빈 List<T>를 반환하는 게 좋다.

 

메서드 반환 타입을 T 대신에 Optional<T>로 선언해야할 때

- 결과가 없을 수 있으며, 클라이언트가 이 상황을 특별하게 처리해야 한다면 Optional<T>를 반환한다.

 

int, long, double -> OptionalInt, OptionalLong, OptionalDouble

박싱된 기본 타입을 담은 옵셔널은 기본 타입 자체보다 무거우니 이 것들을 반환하는 일은 지양하자.

 

옵셔널을 맵의 값으로 사용하면, 맵 안에 키가 없다는 사실을 나타내는 방법이 2가지나 되기 때문에 혼란과 오류 가능성을 키운다.

 

 

Item 56. 공개된 API 요소에는 항상 문서화 주석을 작성하라


 

'Java > Effective Java' 카테고리의 다른 글

[Effective Java] Chapter 7. 람다와 스트림  (0) 2024.10.27