Skip to content

Commit 0f795eb

Browse files
committed
feat: add post Local Variable Type Inference Style Guidelines
1 parent cfdc878 commit 0f795eb

File tree

1 file changed

+381
-0
lines changed

1 file changed

+381
-0
lines changed
Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
---
2+
title: "Local Variable Type Inference Style Guidelines"
3+
date: "2024-06-07"
4+
tags: ["Clean Code"]
5+
summary: "Local Variable Type Inference Style Guidelines"
6+
description: "지역 변수 유형 추론 타입인 'var' 를 사용하는 방법"
7+
---
8+
9+
10+
:::info
11+
<a href = "https://openjdk.org/projects/amber/guides/lvti-style-guide" target="_blank">Local Variable Type Inference Style Guidelines</a> 에 대한 내용 정리
12+
Java SE 10 에는 <a href = "https://openjdk.org/jeps/286" target="_blank"> 지역 변수에 대한 유형 추론 </a>이 도입되었다.
13+
중복 코드를 줄여서 가독성을 향상 시킬 수 있고, 코드가 간결하다는 점에서는 좋은 점이 있지만,
14+
오히려 중요한 정보를 제거해서 가독성을 낮출 수 있다는 점에서 어느 정도 논란이 있다.
15+
var 를 언저 사용하는 것이 좋은지에 대한 포괄적인 규칙은 없지만, 효과적인 사용을 위한 지침을 제공하고 있다.
16+
:::
17+
18+
---
19+
20+
21+
## Principles
22+
23+
---
24+
25+
### G1. 유용한 정보를 제공하는 변수 이름을 선택한다.
26+
**Choose variable names that provide useful information.**
27+
28+
일반적으로 좋은 습관이지만, 변수의 맥락에서는 훨씬 더 중요하다. 변수 선언에서 의미와 용도에 대한 정볼르 전달 할 수 있고, `var` 로 대체할 때는 변수 이름을 개선하는 작업이 동반되어야 한다.
29+
30+
```java
31+
// ORIGINAL
32+
List<Customer> x = dbconn.executeQuery(query);
33+
34+
// GOOD
35+
var custList = dbconn.executeQuery(query);
36+
37+
// 쓸모 없는 변수 이름은 var 와 할께 변수 유형을 포함된 이름으로 대체 되었다.
38+
```
39+
40+
변수의 유형을 이름에 인코딩하여 사용하면 [Hungarian notation](https://en.wikipedia.org/wiki/Hungarian_notation) 이 되는데, 명시적 Type 과 마찬가지로 도움이 되긴 하지만, 복잡해지기도 한다.
41+
위의 예시에서 `custList` 이라는 리스트를 반환하는 것을 의미하는데, 이는 중요하지 않다. 정확한 유형 대신 변수의 이름에 `customers` 와 같이 변수의 역할이나 특성을 표현하는 것이 더 좋을 수 있다.
42+
43+
```java
44+
// ORIGINAL
45+
try (Stream<Customer> result = dbconn.executeQuery(query)) {
46+
return result.map(...)
47+
.filter(...)
48+
.findAny();
49+
}
50+
51+
// GOOD
52+
try (var customers = dbconn.executeQuery(query)) {
53+
return customers.map(...)
54+
.filter(...)
55+
.findAny();
56+
}
57+
```
58+
59+
---
60+
61+
### G2. 로컬 변수의 범위를 최소화한다.
62+
**Minimize the scope of local variables.**
63+
64+
일반적으로 지역 변수의 범위를 제한하는 것이 좋다. Effective Java 의 Item 57 항목에 설명되어 있다. 변수가 사용 중인 경우 강력하게 적용된다.
65+
다음 예에서 `add` 메서드는 특수 항목을 마지막 목록 요소로 명확하게 추가하므로 예상대로 마지막에 처리된다.
66+
67+
```java
68+
var items = new ArrayList<Item>(...);
69+
items.add(MUST_BE_PROCESSED_LAST);
70+
for (var item : items) ...
71+
```
72+
73+
중복 항목을 제거하기 위해 `ArrayList` 대신 `HashSet` 를 사용한다고 가정해보자.
74+
75+
```java
76+
var items = new HashSet<Item>(...);
77+
items.add(MUST_BE_PROCESSED_LAST);
78+
for (var item : items) ...
79+
```
80+
81+
집합에 정의된 반복 순서가 없기 때문에 버그가 생긴다. 그러나 `items` 변수의 용도가 선언에 인접해 있어서 버그를 즉시 수정할 가능성이 높다.
82+
83+
```java
84+
var items = new HashSet<Item>(...);
85+
86+
// ... 100 lines of code ...
87+
88+
items.add(MUST_BE_PROCESSED_LAST);
89+
for (var item : items) ...
90+
```
91+
92+
위의 경우는 `items` 이 멀리 떨어진 곳에 정의 되어 있기 때문에 버그는 훨씬 더 오래 지속될 수 있다.
93+
`item` 이 명시적으로 `List<item>` 으로 선언된 경우 `Set<String>` 으로 변경되어야 한다.
94+
개발자는 이러한 변경으로 인해 영향을 받을 수 있는 코드가 있는지 나머지 메서드를 검사 해야 할 수도 있다. (그렇지 않을 수도 있다.)
95+
`var` 를 사용하면 이러한 메시지가 제거 되므로, 버그가 발생할 윟머이 높아진다.
96+
97+
이것은 `var` 사용을 반대하는 것처럼 보일 수 있지만, `var` 를 사용할 때는 로컬 변수의 범위를 줄인 다음 사용하라는 것이다.
98+
99+
100+
### G3. 이니셜라이저가 충분한 정보를 제공하는 경우는 var 를 고려해라.
101+
**Consider var when the initializer provides sufficient information to the reader.**
102+
103+
로컬 변수는 생성자를 통해 초기화 되는 경우가 많다. 생성되는 클래스의 이름은 왼쪽에 명시적 유형으로 반복된다. Type 이름이 긴 경우 `var` 를 사용하면 정보 손실 없이 간결하게 표현할 수 있다.
104+
105+
```java
106+
// ORIGINAL
107+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
108+
109+
// GOOD
110+
var outputStream = new ByteArrayOutputStream();
111+
```
112+
113+
초기화가 생성자 대신 정적 팩토리 메서드와 같이 메서드 호출인 경우, 그리고 그 이름에 충분한 Type 정보가 포함되어 있는 경우에도 `var` 를 사용 하는 것이 합리적이다.
114+
115+
```java
116+
// ORIGINAL
117+
BufferedReader reader = Files.newBufferedReader(...);
118+
List<String> stringList = List.of("a", "b", "c");
119+
120+
// GOOD
121+
var reader = Files.newBufferedReader(...);
122+
var stringList = List.of("a", "b", "c");
123+
```
124+
125+
이러한 경우 메서더의 이름은 특정 반환 유형을 강력하게 암시하고 변수 Type 을 유추하는데 사용된다.
126+
127+
### G4. 연속적으로 로컬 변수가 있는 곳과 중첩된 표현식을 분리하려면 var 를 사용한다.
128+
**Use var to break up chained or nested expressions with local variables.**
129+
130+
문자열 컬렉션을 가져와 가장 자주 발생하는 문자열을 찾는 코들르 생각해보면,
131+
132+
```java
133+
return strings.stream()
134+
.collect(groupingBy(s -> s, counting()))
135+
.entrySet()
136+
.stream()
137+
.max(Map.Entry.comparingByValue())
138+
.map(Map.Entry::getKey);
139+
```
140+
141+
위의 코드는 정확하지만, 단일 스트림 파이프라인처럼 보이기 때문에 혼동 할 수 있다. 실제로는 짧은 스트림에 이어서 첫 번째 스트림의 결과에 대한 두 번째 스트림,
142+
그리고 두 번째 스트림의 선택적 결과에 대한 매핑이 이어진다. 이 코드를 가장 읽기 쉽게 표현하는 방법은 아래와 같이 Map 으로 그룹화 한 이후에 key 추출 하는 것이 좋았을 것이다.
143+
144+
145+
```java
146+
Map<String, Long> freqMap = strings.stream()
147+
.collect(groupingBy(s -> s, counting()));
148+
149+
Optional<Map.Entry<String, Long>> maxEntryOpt = freqMap.entrySet()
150+
.stream()
151+
.max(Map.Entry.comparingByValue());
152+
return maxEntryOpt.map(Map.Entry::getKey);
153+
```
154+
155+
그러나, 중간 변수의 유형을 작성하는 것이 부담스웠을 것이고, 그 대신에 제어 흐름이 왜곡되었을 것이다.
156+
이 때 `var` 를 사용하면 중간 변수 유형을 명시적으로 선언하는데 드는 높은 비용을 지불하지 않고도 더 자연스럽게 표현할 수 있다.
157+
158+
```java
159+
var freqMap = strings.stream()
160+
.collect(groupingBy(s -> s, counting()));
161+
162+
var maxEntryOpt = freqMap.entrySet()
163+
.stream()
164+
.max(Map.Entry.comparingByValue());
165+
166+
return maxEntryOpt.map(Map.Entry::getKey);
167+
```
168+
169+
하나의 긴 메서드 호출 체인이 있는 것을 선호 할 수 있다. 하지만 긴 메서드 체인을 분리하는 것이 가독성에 더 좋다. 이 과정에서 `var` 를 사용하는 것은 중간 변수에 Type 을 작성하는 것보다 좋은 대안이 될 수 있다.
170+
다른 많은 상황과 마찬가지로, `var` 를 사용하려면 무언가를 빼는 것(명시적 유형)과 다시 추가하는 것(더 나은 변수 이름, 더 나은 코드 구조화)가 모두 포함 될
171+
172+
173+
### G5. 로컬 변수를 사용한 "인터페이스 프로그래밍" 에 너무 걱정하지 말아라.
174+
**Don’t worry too much about “programming to the interface” with local variables.**
175+
176+
Java 프로그래밍은 일반적으로 구체적인 유형의 인스턴스를 구성하되, 이를 인터페이스 유형의 변수에 할당하는 것이다.
177+
178+
```java
179+
// ORIGINAL
180+
List<String> list = new ArrayList<>();
181+
182+
// var 를 사용하면 인터페이스 대신 구체적인 유형이 추론된다.
183+
// Inferred type of list is ArrayList<String>
184+
var list = new ArrayList<String>();
185+
```
186+
187+
다시 강조 하지만, `var` 는 지역 변수에만 사용 할 수 있다. 필드 유형, 메서드 매개변수 유형, 메서드 반환 유형을 유추하는데 사용할 수 없다.
188+
"인터페이스 프로그래밍" 이라는 원칙은 이러한 상황에서도 여전히 중요하다.
189+
190+
가장 큰 문제는 변수를 사용하는 코드가 구체적인 구현에 종속성을 형성할 수 있다는 것이다. 변수의 이니셜라이저가 나중에 변경되면 유추된 유형이 변경되어 변수를 사용하는 후속 코드에서 오류나 버그가 발생할 수 있다.
191+
192+
G2 에서 권장하는 대로 로컬 변수의 범위가 작으면 후속 코드에 영향을 줄 수 있는 구체적인 구현의 "누수"로 인한 위험이 제한된다.
193+
변수가 몇 줄 떨어진 코드에서만 사용된느 경우 문제를 피하거나 문제가 발생하더라도 쉽게 해결할 수 있다.
194+
195+
이 특별한 경우, `ArrayList` 에는 `List` 에 없는 두 가지 메서드, 즉 `ensureCapacity`, `trimToSize` 만 포함된다. 이러한 메서드는 list 의 내용에 영향을 미치지 않으므로 정확성에 영향을 미치지 않는다.
196+
이렇게 하면 추론된 유형이 인터페이스가 아닌 구체적인 구현인 경우 영향이 더욱 줄어든다.
197+
198+
199+
### G6. 다이아몬드 또는 일반 메서드와 함께 var 를 사용할 때 주의해라.
200+
**Take care when using var with diamond or generic methods.**
201+
202+
`var``<>` 기능 모두 이미 존재하는 정보에서 파생할 수 있는 경우 명시적 유형 정보를 생략할 수 있다. 동일한 선언에서 두 가지 모두 사용할 수 있을까?
203+
204+
다음을 고려해라.
205+
206+
```java
207+
PriorityQueue<Item> itemQueue = new PriorityQueue<Item>();
208+
209+
// 유형 정보를 잃지 않고 다이아몬드 또는 var 를 사용해서 다시 작성할 수 있다.
210+
211+
// OK: both declare variables of type PriorityQueue<Item>
212+
PriorityQueue<Item> itemQueue = new PriorityQueue<>();
213+
var itemQueue = new PriorityQueue<Item>();
214+
215+
// var 와 다이아몬드 모두 사용하는 것은 합법적이지만 추론된 유형은 변경된다.
216+
// DANGEROUS: infers as PriorityQueue<Object>
217+
var itemQueue = new PriorityQueue<>();
218+
```
219+
220+
추론을 위해 다이아몬드에서는 대항 유형 또는 생성자 인수의 유형을 사용할 수 있다. 둘다 존재 하지 않는 경우 `Object` 가 된다. 하지만 일반적으로 의도하는 것은 아니다.
221+
222+
제네릭 메서드는 타입 추론을 매우 성공적으로 사용했기 때문에 개발자가 명시적인 타입 인수를 제공하는 경우는 드물다.
223+
제네릭 메서드에 대한 추론은 충분한 타입 정볼르 제공하지는 실제 메서드 인수가 없는 경우 대상 타입에 의존한다.
224+
`var` 선언에서는 대상 유형이 없으므로 다이아몬드와 비슷한 문제가 발생할 수 있다.
225+
226+
227+
```java
228+
// DANGEROUS: infers as List<Object>
229+
var list = List.of();
230+
```
231+
232+
다이아몬드 메서드와 일반 메서드 모두 생성자나 메서드에 실제 인자를 추가 형 정보를 제공하여 의도한 유형을 유추할 수 있다.
233+
234+
```java
235+
// OK: itemQueue infers as PriorityQueue<String>
236+
Comparator<String> comp = ... ;
237+
var itemQueue = new PriorityQueue<>(comp);
238+
239+
// OK: infers as List&#60;BigInteger&#62;
240+
var list = List.of(BigInteger.ZERO);
241+
```
242+
243+
다이아몬드 또는 일반 메서드와 함께 `var`를 사용하기로 결정한 경우
244+
메서드 또는 생성자 인수가 유추된 유형이 의도와 일치하도록 충분한 유형 정보를 제공하는지 확인해야 한다.
245+
그렇지 않으면 동일한 선언에서 다이아몬드 또는 일반 메서드와 함께 `var` 를 모두 사용하지 말아라.
246+
247+
### G7. 리터럴과 함게 var 를 사용할 때는 주의해라.
248+
**Take care when using var with literals.**
249+
250+
원시 리터럴은 `var` 선언의 이니셜라이저로 사용 할 수 있다. 일반적으로 유형 이름이 짧아서 `var` 를 사용하는 것이 큰 이점을 제공하지 않는다.
251+
하지만, 변수 이름을 정렬할 때와 같이 변수가 유용할 때 가 있다.
252+
`boolean`, `char`, `long`, `string` 과 같이 리터럴에서 유추되는 유형은 정확해서 `var` 의 의미가 모호하지 않다.
253+
254+
255+
```java
256+
// ORIGINAL
257+
boolean ready = true;
258+
char ch = '\ufffd';
259+
long sum = 0L;
260+
String label = "wombat";
261+
262+
// GOOD
263+
var ready = true;
264+
var ch = '\ufffd';
265+
var sum = 0L;
266+
var label = "wombat";
267+
```
268+
269+
이니셜라이저가 숫자 값, 특히 정수 리터럴인 경우 특히 주의해야한다.
270+
271+
```java
272+
// ORIGINAL
273+
byte flags = 0;
274+
short mask = 0x7fff;
275+
long base = 17;
276+
277+
// DANGEROUS: all infer as int
278+
var flags = 0;
279+
var mask = 0x7fff;
280+
var base = 17;
281+
```
282+
283+
`float`은 대부분 모호하지 않다.
284+
285+
```java
286+
// ORIGINAL
287+
float f = 1.0f;
288+
double d = 2.0;
289+
290+
// GOOD
291+
var f = 1.0f;
292+
var d = 2.0;
293+
```
294+
295+
부동 소수점 리터럴은 자동으로 `double`로 확장될 수 있다. `var` 를 사용할 때는 다음과 같은 주의가 필요하다.
296+
297+
```java
298+
// ORIGINAL
299+
static final float INITIAL = 3.0f;
300+
...
301+
double temp = INITIAL;
302+
303+
// DANGEROUS: now infers as float
304+
var temp = INITIAL;
305+
```
306+
307+
(실제로 위의 예는 이니셜라이저에 유형을 볼 수 있는 정보가 충분하지 않기 때문에 G3 를 위반한다.)
308+
309+
---
310+
311+
### Examples
312+
313+
`var` 를 사용할 때 가장 큰 이점을 얻을 수 있는 위치에 대한 예는 아래와 같다.
314+
이터레이터 유형이 중첩된 와이드카드일 때, `var` 를 사용하고, for 문에서 사용하면 간결하게 쓸 수 있다.
315+
316+
```java
317+
// ORIGINAL
318+
void removeMatches(Map<? extends String, ? extends Number> map, int max) {
319+
for (Iterator<? extends Map.Entry<? extends String, ? extends Number>> iterator =
320+
map.entrySet().iterator(); iterator.hasNext();) {
321+
Map.Entry<? extends String, ? extends Number> entry = iterator.next();
322+
if (max > 0 && matches(entry)) {
323+
iterator.remove();
324+
max--;
325+
}
326+
}
327+
}
328+
329+
// GOOD
330+
void removeMatches(Map<? extends String, ? extends Number> map, int max) {
331+
for (var iterator = map.entrySet().iterator(); iterator.hasNext();) {
332+
var entry = iterator.next();
333+
if (max > 0 && matches(entry)) {
334+
iterator.remove();
335+
max--;
336+
}
337+
}
338+
}
339+
```
340+
341+
`try-with-resources` 문을 사용할 때도 간단하게 사용할 수 있다.
342+
343+
```java
344+
// ORIGINAL
345+
try (InputStream is = socket.getInputStream();
346+
InputStreamReader isr = new InputStreamReader(is, charsetName);
347+
BufferedReader buf = new BufferedReader(isr)) {
348+
return buf.readLine();
349+
}
350+
351+
// GOOD
352+
try (var inputStream = socket.getInputStream();
353+
var reader = new InputStreamReader(inputStream, charsetName);
354+
var bufReader = new BufferedReader(reader)) {
355+
return bufReader.readLine();
356+
}
357+
```
358+
359+
---
360+
361+
### Result
362+
`var` 를 사용하면 복잡함이 줄어들고, 더 중요한 정보를 돋보이게 하며 코드를 개선할 수 있다.
363+
반면, 무분별한 `var` 를 사용하면 반대가 될 수 있다. 적절하게 사용한다면 코드 개선에 도움이 되고 코드를 더 짧고 명확하게 만들 수 있다.
364+
365+
366+
---
367+
368+
:::success
369+
<b>개인적인 생각</b>
370+
✔ 무분별한 <b> var </b> 를 사용하는 것은 오히려 가독성에 저해가 될 수 있다.
371+
✔ 결국엔 <b> var </b> 를 사용하기 위해서는 <b> 의미 전달</b> 이 될 수 있는 네이밍을 사용 해야 한다.
372+
✔ 새로운 기술이나 문법이 나왔다고 무지성으로 쓰는 것보다는 역시 알고 쓰는 것이 중요한 것 같다.
373+
:::
374+
375+
376+
---
377+
378+
### 📚 Reference
379+
380+
* [Local Variable Type Inference Style Guidelines](https://openjdk.org/projects/amber/guides/lvti-style-guide)
381+
* [Java 10 LocalVariable Type-Inference](https://www.baeldung.com/java-10-local-variable-type-inference)

0 commit comments

Comments
 (0)