Skip to content

Commit

Permalink
GITBOOK-203: item 53 : 가변인수는 신중히 사용하라.
Browse files Browse the repository at this point in the history
  • Loading branch information
GoldenPearls authored and gitbook-bot committed Nov 26, 2024
1 parent 07fe3f3 commit bfa1bc1
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 15 deletions.
1 change: 1 addition & 0 deletions developLog/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
* [item 50 : 적시에 방어적 복사본을 만들라](programming-lanuage/java/effective-java/8/item-50.md)
* [item 51 : 메서드 시그니처를 신중히 설계하라](programming-lanuage/java/effective-java/8/item-51.md)
* [item 52 : 다중정의는 신중히 사용하라](programming-lanuage/java/effective-java/8/item-52.md)
* [item 53 : 가변인수는 신중히 사용하라.](programming-lanuage/java/effective-java/8/item-53-..md)
* [스터디에서 알아가는 것](programming-lanuage/java/effective-java/undefined.md)
* [모던 자바 인 액션](programming-lanuage/java/modern-java-in-action/README.md)
* [chaper 1. 자바 8, 9, 10, 11 : 무슨 일이 일어나고 있는가?](programming-lanuage/java/modern-java-in-action/chaper-1.-8-9-10-11.md)
Expand Down
31 changes: 16 additions & 15 deletions developLog/programming-lanuage/java/effective-java/8/item-52.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,24 +236,25 @@ list = [-2, 0, 2]

### 2) 다중 정의의 함정 2: 람다와 메서드 참조

* **문제 코드**:
**문제 코드**:

```java
@Test
public void lambdaTest1() {
new Thread(System.out::println).start();
}
```java
@Test
public void lambdaTest1() {
new Thread(System.out::println).start();
}

@Test
public void lambdaTest2() {
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println); // 컴파일 오류 발생 가능
}
```
@Test
public void lambdaTest2() {
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println); // 컴파일 오류 발생 가능
}
```

* `lambdaTest1()`은 `Runnable`을 요구하기 때문에 문제없이 컴파일된다.
* `lambdaTest2()`에서는 `submit()` 메서드가 `Runnable`과 `Callable<T>`를 모두 받을 수 있어 **컴파일러가 어떤 메서드를 선택해야 할지 모호해** 컴파일 오류가 발생한다.
* **서로 다른 함수형 인터페이스**라도 **같은 위치의 인수**로 받을 경우, 다중 정의는 혼란을 초래할 수 있다.
* `lambdaTest1()``Runnable`을 요구하기 때문에 문제없이 컴파일된다.
* `lambdaTest2()`에서는 `submit()` 메서드가 `Runnable``Callable<T>`를 모두 받을 수 있어 **컴파일러가 어떤 메서드를 선택해야 할지 모호해** 컴파일 오류가 발생한다.
* **서로 다른 함수형 인터페이스**라도 **같은 위치의 인수**로 받을 경우, 다중 정의는 혼란을 초래할 수 있다.
* 이 말은 서로 다른 함수형 인터페이스라도 서로 근본적으로 다르지 않다는 뜻이다. 컴파일할 때 명령줄 스위치로 -Xlint:overloads를 지정하면 이런 종류의 다중정의를 경고해줄 것이다.

{% hint style="danger" %}
자바 4와 그 이후에 대해서
Expand Down
138 changes: 138 additions & 0 deletions developLog/programming-lanuage/java/effective-java/8/item-53-..md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# item 53 : 가변인수는 신중히 사용하라.

## 1. 잘못 구현된 가변 인수 예시

**문제 코드**:

```java
public static int min(int... args) {
if (args.length == 0) {
throw new IllegalArgumentException("인수가 1개 이상 필요합니다.");
}

int min = args[0];
for (int i = 0; i < args.length; i++) {
if (args[i] < min) {
min = args[i];
}
}

return min;
}

@Test
public void minTest(){
int min = min(); // 인수를 넣지 않고 호출
System.out.println("min = " + min);
}
```

* 이 메서드는 **가변 인수로 0개 이상의 인수**를 받을 수 있다. 만약 **아무 인수도 전달되지 않는 경우**, 런타임에 예외(`IllegalArgumentException`)가 발생한다.
* **런타임 예외**는 코드를 실행하기 전까지 문제를 발견할 수 없으므로, **컴파일 타임**에 이 문제가 감지되지 않는다.
* 또한, **for-each**문 대신 일반 **for**문이 사용되어 코드가 불필요하게 복잡하고 덜 깔끔하다.

## 2. 올바른 가변 인수 구현

* **올바른 구현 예**:

```java
public static int min2(int firstArg, int... args) {
int min = firstArg;

for (int arg : args) {
if (min > arg) {
min = arg;
}
}

return min;
}
```

* 이 방법에서는 첫 번째 매개변수를 **일반 매개변수**로 두고, 나머지는 **가변 인수**로 받는다. 이로 인해 최소 **하나 이상의 인수**가 필요함을 **컴파일 타임에 보장**할 수 있다.
* 즉, 아무런 인수를 주지 않으면 컴파일러가 오류를 표시하므로 **런타임에 실패할 가능성을 줄여든**.
* 또한, **for-each**문을 사용하여 코드가 더 간결하고 가독성이 높다.

## 3. 가변 인수와 성능 최적화

{% hint style="info" %}
가변인수란?&#x20;
{% endhint %}

* **가변 인수(Varargs)**:
* 가변 인수는 **매개변수의 개수가 유동적인 경우**에 유용하며, 자바에서는 `int... args`와 같이 선언하여 사용한다.
* 가변 인수를 사용하는 메서드는 호출 시 **배열을 생성**하고 그 배열에 인수를 저장하기 때문에, 반복적인 호출 시 **성능 저하**가 발생할 수 있다.
* **성능 최적화와 코드 안정성**:
* 올바르게 가변 인수를 사용하는 것은 코드의 안정성을 높이는 데 기여한다. 필수 매개변수를 일반 매개변수로 정의하면 컴파일 타임에 필수 조건이 보장된다.
* 성능 최적화도 중요하다. 특히, 자주 호출되는 메서드에서는 **불필요한 배열 할당을 최소화**함으로써 성능을 개선할 수 있다.

1. **가변 인수의 성능 문제**:

* **가변 인수**는 메서드가 호출될 때마다 **배열을 할당하고 초기화**한다. 이 과정은 **큰 비용이 아닐 수도 있지만**, 성능 최적화가 필요한 경우에는 주의가 필요하다.

2. **성능 최적화 방법**:

```java
public void foo() { }
public void foo(int a1) { }
public void foo(int a1, int a2) { }
public void foo(int a1, int a2, int a3) { }
public void foo(int a1, int a2, int a3, int... rest) { }
```

* 위와 같은 방식으로 **다양한 매개변수 개수에 맞는 메서드를 오버로딩**한다.
* 대부분의 호출이 **3개 이하의 인수**를 사용하는 경우라면, **인수 3개까지는 배열을 생성하지 않도록** 함으로써 **성능을 최적화**할 수 있다.
* **가변 인수****4개 이상의 인수**를 받을 때만 배열을 생성하도록 하여, 배열 생성의 오버헤드를 줄인다.
* **실제 사용 예시 - `EnumSet`**:
* **`EnumSet`의 정적 팩터리 메서드**도 이와 같은 방식으로 최적화되어 있다. 예를 들어, 4개 이상의 인수를 받는 경우에만 추가적인 처리를 하여 불필요한 리소스 사용을 줄인다.

{% hint style="danger" %}
근데.. .앞장에서 다중 정의는 신중히 쓰라고 하지 않았나? 성능 최적화를 위해 메서드 오버로딩?
{% endhint %}

맞습니다, 이 부분은 다소 모순적일 수 있습니다. 앞에서 다중 정의(오버로딩, Overloading)는 혼란을 초래할 수 있으므로 사용을 신중히 해야 한다고 했지만, 여기에서는 성능 최적화를 위해 **매개변수 개수에 따른 메서드 오버로딩**을 제안하고 있습니다.

> 이 모순은 특정 상황에서의 트레이드오프(trade-off)로 이해할 수 있습니다. 각각의 상황에서 **무엇이 더 중요한지**에 따라 접근 방식이 달라지기 때문입니다.
#### 오버로딩을 피해야 하는 이유

* **혼란 가능성**: 오버로딩은 메서드가 컴파일타임에 선택되기 때문에, 어떤 메서드가 호출될지 **명확하지 않은 경우** 혼란을 초래할 수 있습니다. 특히, 매개변수의 타입이 **근본적으로 다르지 않거나** 오토박싱, 제네릭 등의 요소가 개입되면 개발자가 의도한 대로 동작하지 않을 수 있습니다.
* **사용자 혼란**: API 사용자가 어떤 메서드가 호출될지 예상하기 어려운 경우, 개발자가 그 동작을 잘못 이해하고 사용하는 경우가 생길 수 있습니다.

#### 오버로딩을 사용하는 경우 - 성능 최적화

* **성능 최적화의 필요성**: 가변 인수는 메서드가 호출될 때마다 **배열을 생성하고 초기화**하기 때문에, 성능이 중요한 경우 이 비용을 줄이는 것이 필요합니다.
* 이때, 자주 호출되는 메서드의 경우 **매개변수의 개수별로 오버로딩된 메서드를 제공**함으로써 **불필요한 배열 생성**을 피할 수 있습니다. 대부분의 호출이 인수가 적은 경우라면 이러한 최적화는 성능 개선에 크게 기여할 수 있습니다.

#### EnumSet의 최적화 예시

* `EnumSet`**정적 팩터리 메서드**도 이러한 방식으로 최적화되어 있습니다. **인수가 적은 경우에는 별도의 처리를 통해 최적화**하고, **4개 이상의 인수**에 대해서만 배열을 생성하거나 추가적인 처리를 합니다.
* 이는 성능 최적화와 코드의 효율성 때문에 도입된 예시로, 오버로딩을 사용하여 특정 성능 이슈를 해결하려는 것입니다.

#### 요약 및 트레이드오프 설명

* **오버로딩을 피해야 할 때**:
* 메서드 간의 선택이 **명확하지 않은 경우**: 예를 들어, 서로 다른 매개변수 타입이 있지만 이들 사이에 **오토박싱이나 제네릭**과 같은 요소가 개입되면 혼란을 일으킬 수 있으므로, 오버로딩을 피하는 것이 좋습니다.
* **API의 가독성과 직관성**이 중요한 경우: API 사용자에게 **명확하게 어떤 메서드가 호출될지 보장할 수 없는 경우** 오버로딩은 지양해야 합니다.
* **오버로딩을 사용해도 좋은 경우**:
* **성능 최적화가 중요한 경우**: 예를 들어, **가변 인수의 배열 생성**이 반복될 때의 **성능 오버헤드**를 줄이기 위해서는 **매개변수 개수에 따른 오버로딩**이 도움이 될 수 있습니다.
* 이런 경우, 오버로딩을 통해 자주 호출되는 메서드에서 **최적화된 코드 경로**를 제공하여 성능을 개선할 수 있습니다.

결론적으로, **오버로딩****상황에 따라 장단점이 있으며, 어떤 상황에서는 피해야 하지만** 성능 최적화와 같이 특정한 이유가 있다면 **신중하게 사용할 수 있습니다**. 중요한 것은 오버로딩으로 인해 발생할 수 있는 **혼란과 장점을 잘 저울질하여 사용 여부를 결정하는 것**입니다.

## 📚 핵심 정리

1. **가변 인수를 사용하자**:
* 인수 개수가 일정하지 않은 메서드를 정의할 때 **가변 인수**를 사용하는 것이 좋다.
2. **필수 매개변수가 있다면**:
* **필수 매개변수**가 있다면, **가변 인수 앞에 일반 매개변수**로 두어야 한다. 이를 통해 **컴파일 타임에 오류를 감지**할 수 있고, 잘못된 호출을 방지할 수 있다.
3. **성능 고려**:
* 가변 인수를 사용할 때는 성능 문제도 고려해야 한다. 필요한 경우, **매개변수의 개수에 따라 다른 메서드를 오버로딩**하여 배열 할당 비용을 줄일 수 있다.

> 결론적으로, **가변 인수를 사용할 때는 필수 매개변수를 앞에 두어 컴파일 타임에 오류를 잡도록** 하고, **성능 최적화가 필요하다면 오버로딩**을 통해 배열 생성의 오버헤드를 줄이는 방식을 사용하는 것이 좋다. 이렇게 하면 코드의 **가독성과 안정성**, 그리고 **성능**을 모두 개선할 수 있다.


> 참고 및 출처
>
> * [https://jake-seo-dev.tistory.com/526?category=906605](https://jake-seo-dev.tistory.com/526?category=906605)

0 comments on commit bfa1bc1

Please sign in to comment.