이펙티브 자바 8장) 메서드

동동주·2025년 12월 11일

이펙티브 자바

목록 보기
9/13

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

1. 왜 매개변수 검사가 중요한가?

✔ 메서드는 잘못된 입력을 절대 받아들이면 안 된다.

매개변수가 유효하지 않으면 즉시(fail-fast) 예외를 던져야 한다. 유효성 검사를 하지 않으면 다음 문제가 발생할 수 있다:

1.중간 실행 중 모호한 예외 발생
2. 잘못된 반환값 생성
3. 객체 상태(invariant)가 깨져 미래의 예측 불가한 시점에 오류 발생
→ 이런 오류는 실패 원자성(Item 76)까지 깨뜨릴 수 있다.

따라서:
모든 public 메서드는 “메서드 본문이 실행되기 전에” 매개변수 유효성을 검사해야 한다.

2. 매개변수 제약은 문서화해야 한다

  • 매개변수가 어떤 조건을 만족해야 하는지 반드시 명시해야 한다.
  • public/protected 메서드라면 @throws 태그로 어떤 조건 위반 시 어떤 예외가 발생하는지 문서화할 것.
  • 예: null, 범위, 크기, 인덱스 등

✔ 표준적으로 많이 쓰는 예외

  • IllegalArgumentException
  • IndexOutOfBoundsException
  • NullPointerException

3. 유효성 검사 방법

3-1. null 검사 — Objects.requireNonNull

가장 간편하고 널리 쓰이는 null 체크 방식.

this.strategy = Objects.requireNonNull(strategy, "strategy must not be null");

3-2. 범위 검사 — Objects.checkIndex (Java 9+)

List/Array용 범위 체크 유틸리티.

int idx = Objects.checkIndex(index, list.size());

(※ 메시지는 지정할 수 없고 List/Array 전용이라는 제약이 있음)

3-3. assert 검사 — private 메서드용

  • 내부 메서드에서만 사용 (외부 입력이 불가능한 경우)
  • JVM 옵션 -ea 필요
private void sort(int[] arr, int offset, int length) {
    assert arr != null;
    assert offset >= 0 && length >= 0;
}

3-4. 저장해두는 매개변수는 더 철저히 검사

static List<Integer> intArrayAsList(int[] a) {
    Objects.requireNonNull(a);
    return new AbstractList<>() { ... };
}

→ null 검사가 없으면 List 뷰 생성은 되지만, 이후 사용하는 시점에 NPE 발생

4. 유효성 검사가 불필요한 경우

  • 검사 비용이 지나치게 큰 경우
  • 계산 과정에서 자동으로 유효성이 검증되는 경우

예: Collections.sort(list)
→ 비교 과정에서 자연스럽게 ClassCastException 발생하므로 사전 검증 불필요.

정리
1. 모든 public 메서드/생성자는 매개변수 유효성 검사를 메서드 시작 시점에 수행해야 한다.
2. 제약 조건과 위반 시 예외를 문서화(@throws)하고, null·범위·인덱스 등은 반드시 검증한다.
3. private 메서드는 assert 활용 가능하지만, public API는 반드시 명시적 검사(requireNonNull 등)를 사용한다.


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

1. 왜 방어적 복사가 필요한가?

Period 같은 불변 클래스를 만들어도, 가변 객체(Date 등)를 참조로 받으면 내부 불변식이 깨진다.

Period p = new Period(start, end);
end.setYear(78);   // p 내부가 수정됨 → 불변 아님

즉, 불변처럼 보이지만 실제로는 공격·버그에 취약하다.

2. 생성자에서 방어적 복사

✔ 핵심 원칙

“매개변수 유효성 검사 전에 복사하고, 복사본을 기준으로 검증해라.”

멀티스레드 환경에서

  1. 원본 검증
  2. 복사
    사이에 다른 스레드가 원본을 변경할 수 있기 때문.

올바른 작성 예

public Period(Date start, Date end) {
    this.start = new Date(start.getTime());
    this.end   = new Date(end.getTime());

    if (this.start.compareTo(this.end) > 0) {
        throw new IllegalArgumentException(start + " after " + end);
    }
}

⚠ clone()을 쓰면 안 되는 이유

  • 매개변수가 제3자가 만든 하위 클래스일 수 있음
  • clone이 악의적으로 재정의되어 있으면 공격자가 내부를 조작할 수 있음
    → 생성자/팩터리를 이용한 복사가 안전하다.

3. 접근자(getter)에서도 방어적 복사 필요

다시 “가변 필드”를 그대로 넘기면 내부가 수정된다.

p.getEnd().setYear(78);   // 내부 필드 수정 가능 → 불변 아님

올바른 접근자

public Date getStart() { return new Date(start.getTime()); }
public Date getEnd()   { return new Date(end.getTime()); }

여기서는 clone 사용 가능

Period가 내부적으로 보유한 Date는 java.util.Date로 확정적이므로 clone도 안전.
하지만 Item13에 따라 생성자를 쓰는 방식이 일반적으로 더 권장됨.

4. 방어적 복사의 목적 = “불변 클래스 만들기”만이 아니다

  • 클라이언트가 준 객체를 내부에서 저장해야 하는 경우
  • 내부 객체를 외부로 전달해야 하는 경우
    가변 객체라면 항상 복사본 사용이 기본 룰.

핵심 원칙

“어떤 참조도 외부가 객체 내부를 바꿀 수 있게 해선 안 된다.”

5. 방어적 복사를 항상 할 수는 없다

방어적 복사에는 비용이 존재한다.

복사를 생략해도 되는 경우

  • 복사 비용이 매우 큰 경우
  • 클라이언트를 신뢰할 수 있는 내부 시스템
  • 불변식이 깨져도 오로지 호출자에게만 영향이 있는 경우

정리
1. 가변 객체를 매개변수로 받거나 반환할 때는 반드시 방어적 복사를 해야 한다.
2. 생성자에서는 유효성 검사보다 ‘복사’가 먼저이며, clone은 절대 쓰지 않는 것이 안전하다.
3. getter에서도 내부 가변 객체를 그대로 노출하지 말고 반드시 복사 후 반환한다.
4. 방어적 복사를 하면 불변식이 깨지는 공격/버그를 완전히 차단할 수 있다.
5. 비용이 크거나 신뢰 가능한 상황에서만 복사를 생략하며, 그 경우 반드시 문서화한다.


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

1. 메서드 이름은 신중하게

  • 표준 명명 규칙(Item68)을 반드시 따르고 읽는 사람이 의도를 바로 이해할 수 있어야 한다.
  • 같은 패키지/프로젝트 내 다른 이름들과 일관성을 유지하는 것이 가장 중요하다.
  • 너무 긴 이름은 피하고, 애매하면 자바 표준 라이브러리의 이름 패턴을 참고하자.

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

  • 편의 메서드(convenience method)는 사용자를 위해 복잡한 작업을 쉽게 해주는 메서드.
  • 하지만 편의 메서드를 남발하면 클래스에 메서드가 너무 많아지고,
    학습·사용·문서화·테스트·유지보수 모두 어려워진다.
  • 각 기능을 완벽히 수행하는 메서드만 두고, 정말 자주 쓰이는 경우에만 약칭 메서드를 제공하라.

3. 매개변수 목록은 짧게 유지하자

  • 일반적으로 4개 이하가 이상적이다.
  • 특히 동일 타입의 매개변수 여러 개가 연속되는 경우는 매우 위험하다.
    (순서를 바꿔 입력해도 컴파일되므로, 의도와 다르게 동작하기 쉬움)

4. 매개변수를 줄이는 실전 기법 3가지

① 여러 메서드로 기능을 쪼갠다

  • 원래 긴 매개변수 목록이 들어가는 한 메서드를
    → 의미 있는 기능 단위로 쪼개서
    → 더 적은 파라미터를 받도록 만드는 방식.
  • 이는 “직교성(orthogonality)”을 높여 오히려 메서드 수를 줄이는 효과도 있다.
  • 예) subList + indexOf 조합으로 3개 매개변수가 필요한 작업을 해결.

② 도우미(helper) 클래스를 사용한다

  • 두 개 이상의 매개변수가 하나의 개념을 이룬다면 이를 묶는 클래스를 만든다.
  • 보통 정적 멤버 클래스(Item24)로 구현한다.
  • 예) 카드의 숫자(rank) + 무늬(suit)는 항상 함께 다니므로 Card라는 클래스로 묶기.

이렇게 하면:

  • 메서드는 Card 하나만 받으면 되고,
  • 내부 구현도 훨씬 깔끔해지고,
  • 의미도 명확해진다.

③ 메서드 호출에 ‘빌더 패턴’을 응용한다

  • 매개변수 개수가 많거나 일부만 선택적으로 설정하면 되는 경우 유용.
  • “설정 객체(파라미터 객체)”를 만들어 setter 형태로 필요한 인자만 채운 뒤
    마지막에 execute() 같은 메서드를 호출해서 실행.

예)

QueryBuilder qb = new QueryBuilder()
        .limit(10)
        .orderBy("name")
        .filter("age > 20");
qb.execute();

이는

  • 긴 시그니처를 피할 수 있고,
  • 선택적 매개변수 처리가 쉬워지며,
  • 최종 실행 전에 유효성 검사도 할 수 있다.

5. 매개변수 타입은 구체 클래스보다 인터페이스로 받자

  • 매개변수로 사용할 인터페이스가 있다면 반드시 인터페이스로 받자.
  • 예: HashMap을 받도록 설계하지 말고 Map을 받도록 하자.
    → 그럼 HashMap, TreeMap, ConcurrentHashMap 등 모든 Map 구현체 사용 가능
    → 클라이언트를 특정 구현체로 제한하지 않는다.
  • 클래스 타입을 받으면 클라이언트가 “형 변환” 또는 “복사 비용”을 치르게 된다.

6. boolean 파라미터 대신 두 값짜리 enum을 사용하라

  • boolean을 쓰면 호출 시 의미가 명확하지 않다.
  • 온도 단위 예시:
Thermometer.newInstance(true); // 의미 모호
Thermometer.newInstance(TemperatureScale.CELSIUS); // 명확
  • 확장성도 좋아서 이후 KELVIN 같은 값도 쉽게 추가할 수 있다.
  • 또한 enum 상수에 각 단위를 처리하는 메서드를 포함시킬 수도 있다.

정리
1. 메서드 이름은 일관성 있고 이해 가능하게 지어라.
2. 편의 메서드 남발은 해롭다 — 각 메서드는 자신의 역할만 하게 하라.
3. 매개변수는 4개 이하로, 특히 같은 타입 여러 개는 피하라.
4. 매개변수는 쪼개기·도우미 클래스·빌더 패턴으로 줄여라.
5. 매개변수 타입은 인터페이스로 받고, boolean보다 enum을 사용하라.


✅ Item 52. 다중정의(Overloading)는 신중히 사용하라

1. 왜 다중정의가 위험한가?

✔ 다중정의는 정적으로(compile-time) 선택된다

오버라이딩은 런타임 타입(runtime type) 이 기준이지만,
오버로딩은 컴파일 타입(compile-time type) 만 보고 어떤 메서드를 호출할지 결정한다.

즉:

whatIsThisClass(Collection<?> c);

여기서 실제 런타임 객체가 Set, List여도
컴파일 타입이 Collection이면 Collection 버전이 항상 호출된다.

그래서 아래 예제에서 3번 모두 "컬렉션"이 출력된다:

🧨 다중정의 실패 예시

public String whatIsThisClass(Set<?> set) { return "셋"; }
public String whatIsThisClass(List<?> list) { return "리스트"; }
public String whatIsThisClass(Collection<?> c) { return "컬렉션"; }

Collection<?>[] arr = { new HashSet<>(), new ArrayList<>(), map.values() };
for (Collection<?> c : arr) {
    System.out.println(whatIsThisClass(c)); 
}

문제의 핵심

  • 다형성처럼 보이지만 다형성을 제공하지 않는다.
  • 타입 계층에 따라 메서드가 선택되는 것이 아니라
    컴파일러가 정한 하나만 호출된다.

2. 반면 오버라이딩은 런타임에 동작이 결정된다

Burger -> ChickenBurger -> Whopper
Burger b = new Whopper();
b.print();  // "와퍼"

→ 여기서는 런타임 타입(실 객체 타입) 이 기준이므로 기대한 대로 동작한다.

✦ 결론:
오버라이딩은 다형성. 오버로딩은 단순한 이름 재사용 기능.

3. 다중정의가 헷갈리는 이유

다중정의는 아래 상황에서 특히 혼란을 일으킨다:

  • 서로 관련된 타입 (예: Object, Collection, List)
  • 매개변수 수가 같은 메서드
  • 오토박싱, varargs, 형변환 등이 끼어 있을 때
  • 컴파일러가 선택하는 메서드가 사람 기대와 다름

4. 안전하게 설계하는 방법

✔ 1) 매개변수 수가 같은 오버로딩은 피하라

특히 타입 계층이 얽혀 있는 경우(Set, List, Collection 등)는 더욱 금지.

✔ 2) 이름을 완전히 다르게 만들어라

가장 안전한 해결책.

readInt(), readLong()  

✔ 3) varargs(가변인수)와 오버로딩은 절대 같이 쓰지 마라

컴파일러가 어떤 메서드에 매칭할지 극도로 모호함.

✔ 4) 타입이 헷갈릴 수 있으면 아예 받지 마라

예: Integer용 오버로딩과 Object 오버로딩을 함께 두는 경우

5. 결론

다중정의(Overloading) — 정적 바인딩 → 컴파일타임 결정
재정의(Overriding) — 동적 바인딩 → 런타임 결정

🚫 위험한 다중정의는 피하고

  • 매개변수 수가 같거나
  • 타입 계층이 서로 관련되어 있고
  • varargs가 얽혀 있으면
    메서드 이름을 다르게 만들어라.

✔ 안전한 다중정의만 허용

  • 서로 전혀 다른 타입
  • 직관적으로 명확한 차이
  • 자동 형변환/오토박싱/상속 계층이 얽히지 않는 경우

✅ Item 53. 가변인수(varargs)는 신중히 사용하라

1. 가변인수(varargs)란?

  • 개수가 정해지지 않은 인자를 받을 수 있는 메서드 기능
    (int... numbers, String... args)
  • 호출 시 인수 개수만큼 배열을 새로 만들어 전달한다.
  • 인수를 하나도 넘기지 않아도 되고, 여러 개 넘겨도 된다.

✔ 내부적으로는 배열 생성 → 값 복사 → 메서드 호출의 오버헤드가 항상 존재한다.

2. 기본 사용 예시

✔ 간단한 가변인수 메서드 (정상 예시)

static int sum(int... args) {
    int sum = 0;
    for (int arg : args)
        sum += arg;
    return sum;
}

sum(1,2,3); // 6
sum();      // 0

3. 문제 상황: 인수가 1개 이상 필요한 경우

❌ 잘못된 구현

static int min(int... args) {
    if(args.length == 0)
        throw new IllegalArgumentException();
    int min = args[0];
    for(int i = 1; i < args.length; i++)
        if(args[i] < min)
            min = args[i];
    return min;
}

문제점

  • min() 호출 시 런타임 오류 발생
  • 매개변수 유효성 검사를 매번 해야 함
  • 첫 요소를 미리 뽑아 쓰므로 코드가 깔끔하지 않음

4. ✔ 올바른 설계: "필수 매개변수 + 가변인수"

static int min(int firstArg, int... remainingArgs) {
    int min = firstArg;
    for (int arg : remainingArgs)
        if (arg < min)
            min = arg;
    return min;
}

장점

  • 최소 하나는 반드시 전달됨
  • 가독성 증가
  • 유효성 체크 불필요

5. 성능 최적화가 필요한 경우

가변인수는 매 호출마다 배열 생성 비용이 든다 → 비용이 크다.

✔ 해결 전략: 다중정의 + 마지막에만 varargs

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

효과:

  • 인수 0~3개 호출은 가변인수 배열을 생성하지 않음 → 빠름
  • 인수가 많을 때만 varargs 배열 생성
  • EnumSet의 정적 팩터리도 이 방식 사용(성능 최적화)

6. 가변인수 메서드 호출 과정 (중요)

  1. 인수 개수만큼 길이를 가진 배열 생성
  2. 전달된 인수들을 해당 배열에 복사
  3. 메서드가 이 배열을 매개변수로 받음

→ 호출할 때마다 “배열 생성 + 복사” 오버헤드 발생

정리
1. 인수 개수가 정해져 있지 않다면 가변인수를 사용한다.
2. 필수 인수는 가변인수 앞에 두어야 한다.
3. 1개 이상 전달해야 하는 경우는 firstArg + varargs 형태로 만들 것.
4. 성능 민감 코드라면 다중정의로 배열 생성을 최소화한다.
5. 내부적으로 가변인수 호출 시 항상 배열을 새로 만든다.


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

1) 절대 null 을 반환하지 마라

다음과 같은 코드처럼, “값이 없으니 null 반환”은 흔하지만 나쁜 API 설계다.

return cheesesInStock.isEmpty() ? null : new ArrayList<>(cheesesInStock);

이렇게 하면 클라이언트 코드가 매 호출마다 null 체크를 강제당한다.

List<Cheese> cheeses = shop.getCheeses();
if (cheeses != null && cheeses.contains(Cheese.Mozzarella)) {
    ...
}

API 사용성이 떨어지고, null 체크가 누락되면 NPE 발생 위험이 커진다.

2) 빈 컬렉션을 반환하는 것이 더 안전 & 더 편리하다

✔ 빈 컬렉션 할당 비용은 성능 문제를 일으키지 않는다

  • JVM 최적화 때문에 빈 리스트 생성 비용은 매우 낮다.
  • 성능 프로파일링을 해보면 문제 원인은 대부분 다른 데 있음.
  • "빈 컬렉션을 만든다고 성능이 떨어진다"는 잘못된 걱정.

3) 더 효율적인 방법 — 불변(empty) 컬렉션 반환

빈 컬렉션을 매번 새로 생성하지 않고 불변 싱글턴 컬렉션을 쓰면 된다.

return cheesesInStock.isEmpty() 
        ? Collections.emptyList() 
        : new ArrayList<>(cheesesInStock);

Collections.emptyList()

  • 불변 싱글턴
  • 재사용 가능 (새 객체 생성 X)
  • 안전하고 성능에도 유리

Set/Map도 동일하게 emptySet(), emptyMap() 제공.

4) 배열도 null 대신 길이 0짜리 배열을 반환하라

❌ 잘못된 방식

return null;

✔ 올바른 방식

private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];

public Cheese[] getCheeses() {
    return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);
}

new Cheese[0] 배열은 불변이기 때문에 재사용이 가능하다.

정리
null 대신 빈 컬렉션/배열을 반환하라
✔ API 사용성 ↑, NPE ↓
✔ 성능 문제 거의 없음
✔ 필요하면 Collections.emptyXxx() 같은 불변 빈 컬렉션 사용
✔ 배열도 0길이 배열 싱글턴 사용


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

1. 공개 API에는 반드시 Javadoc을 달아라

Javadoc = Java 코드에서 API 문서를 생성하는 공식 주석 시스템

  • 공개된 클래스, 인터페이스, 메서드, 필드에는 반드시 Javadoc 작성
  • 특히 직렬화 가능한 클래스는 직렬화 형태까지 문서화해야 함
  • 기본 생성자는 Javadoc을 달 수 없으므로 공개 API에서는 기본 생성자 사용 금지

2. 문서 주석은 “클라이언트와의 규약(계약)”을 정확히 기술해야 한다

메서드를 문서화할 때 반드시 아래 3개를 명시해야 한다.

✔ 1) 전제조건(preconditions) – @param, @throws

메서드 호출자가 만족해야 하는 조건
예: index >= 0, object != null

✔ 2) 사후조건(postconditions) – @return

메서드가 성공적으로 수행된 후 반드시 만족해야 하는 조건
예: “정렬된 리스트를 반환한다”

✔ 3) 부작용(side effects)

배경 스레드 생성, 전역 상태 변경 등
→ 문서화하지 않으면 API 오용이 발생할 수 있음
(예: Thread.stop() API 문서처럼 상세히 설명해야 함)

📌 3. Javadoc 태그 정리

@param

매개변수 설명

  • 관례상 문장 끝에 마침표 생략

@return

반환 값 설명

  • Java 16부터 {@return} 사용 가능 → 중복 설명 줄어듬

@throws

체크/언체크 예외 기술

4. 중요한 Javadoc 태그들

{@code ...}

  • 코드 폰트로 렌더링
  • HTML 태그/다른 Javadoc 태그 무시 → 안전함

{@literal ...}

  • HTML 메타문자(<, > 등)를 escape해서 그대로 출력
  • Mrs. 같은 문자열의 문장 종료 오해 문제 해결

@implSpec

  • 상속용 클래스의 “하위 클래스와의 계약(implementation requirements)”을 설명
  • 활성화하려면 컴파일 옵션 필요
    -tag "implSpec:a:Implementation Requirements:"

@summary (JDK 10+)

문서 첫 문장 생성 시 마침표 문제 해결하는 용도

{@index ...} (JDK 9+)

검색 색인 기능 추가

5. 제네릭 / Enum / Annotation 문서화 규칙

✔ 제네릭 타입

모든 타입 매개변수에 반드시 @param 문서 작성

@param <K> 키 타입
@param <V> 값 타입

✔ Enum

각 상수까지 모두 문서화해야 하는 거의 유일한 타입

/** 플루트, 오보 등 목관악기 */
WOODWIND

✔ 애너테이션

애너테이션 자체 + 멤버 모두 문서화해야 함

  • 필드 설명 = 명사구
  • 애너테이션을 적용하면 어떤 동작이 발생하는지 = 동사구

6. API 단위 문서화 파일

✔ package-info.java

패키지 설명을 여기에 작성 (package 선언 포함)

✔ module-info.java

모듈 설명을 여기에 작성

7. 스레드 안정성 / 직렬화 문서화는 필수

  • API에서 스레드 안전성을 반드시 명시
    ("불변", "thread-safe", "not thread-safe", "조건부 thread-safe" 등)

  • 직렬화 가능 클래스는 직렬화 형태(documented serialized form) 명시
    @serial, @serialField, @serialData 태그 사용 가능

8. Javadoc 상속 규칙

Javadoc이 문서를 자동 상속하는 우선순위:

  1. 인터페이스 → 클래스 순으로 탐색
  2. 존재하지 않으면 상위 인터페이스/상위 클래스 체인 따라감
  3. 가장 먼저 찾은 문서를 사용

→ 즉, 인터페이스 문서가 가장 강력한 API 계약이 된다.

9. 링크 활용

복잡한 API라면 문서에 관련 클래스, 패키지 링크를 제공하는 것이 좋다.

See {@link java.util.Collections}

10. Javadoc 품질 검사

IDE와 JDK 자체에 Javadoc 검증 기능이 있음
(문서 누락, 태그 오류 등을 자동 검사)

정리
✔ 공개된 API요소에는 반드시 Javadoc 작성
✔ 메서드는 "계약(전제조건·사후조건·부작용)"을 정확히 문서화
✔ 코드/HTML 안전성을 위해 {@code}, {@literal} 적극 활용
✔ 상속용 API는 반드시 @implSpec 사용
✔ 제네릭/Enum/Annotation 모두 문서화해야 함
✔ thread-safety / serialization 도 문서에서 필수
✔ Javadoc 상속 규칙 준수 (인터페이스 문서 = 최우선 계약)


참고 블로그:
https://github.com/back-end-study/effective-java/tree/main/8%EC%9E%A5_%EB%A9%94%EC%84%9C%EB%93%9C

0개의 댓글