|
| 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<BigInteger> |
| 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