diff --git "a/\354\240\204\353\221\220\354\235\264/4\354\236\245/item23.md" "b/\354\240\204\353\221\220\354\235\264/4\354\236\245/item23.md" new file mode 100644 index 0000000..3318280 --- /dev/null +++ "b/\354\240\204\353\221\220\354\235\264/4\354\236\245/item23.md" @@ -0,0 +1,24 @@ +# 태그 달린 클래스보다는 클래스 계층구조를 활용하라 + +열거 타입 선언, 태그 필드, switch 문 등 여러 구현이 한 클래스에 혼합되어 있는 코드는 가독성이 나쁘다. + +1. **태그 달린 클래스의 문제점**: + - 불필요한 코드와 데이터 필드가 추가되어 코드가 복잡하고 장황해짐 + - 타입별로 조건문(`switch`, `if-else`)을 사용해야 하며, 새로운 타입을 추가할 때 실수로 조건문을 빼먹으면 런타임 오류가 발생할 가능성이 있음 + - 확장성과 유연성이 떨어짐. 다른 개발자가 새로운 타입을 추가하거나 유지보수하기 어려움 +2. **클래스 계층구조의 장점**: + - **간결하고 명확함**: 각 의미를 독립된 클래스로 분리해 관련 없는 데이터 필드를 제거 + - **타입 안전성**: 각 클래스의 생성자가 모든 필드를 초기화하고, 추상 메서드를 구현하도록 컴파일러가 보장 + - **유연성과 확장성**: 루트 클래스의 코드를 변경하지 않고도 계층구조를 확장할 수 있음 + - **컴파일 타임 타입 검사**: 특정 타입(예: `Rectangle`, `Circle`)만 매개변수로 제한하거나 명시적으로 다룰 수 있음 + - **자연스러운 계층 관계 반영**: 예를 들어, 정사각형(`Square`)이 사각형(`Rectangle`)의 특별한 형태임을 간단히 표현 가능 +3. **리팩터링 권장**: + - 새로운 클래스를 작성하는데 태그 필드가 등장한다면, 태그를 없애고 **계층구조**로 대체하는 방법을 고려하라 + - 기존 클래스에 태그 필드가 있다면, **계층구조로 리팩터링**하는 것을 고민하라 + +--- + +### **결론** + +> 태그 달린 클래스는 코드가 복잡하고 유지보수가 어렵기 때문에, **클래스 계층구조로 설계**하여 간결하고 확장 가능한 구조를 만들어야 한다. +> \ No newline at end of file diff --git "a/\354\240\204\353\221\220\354\235\264/4\354\236\245/item24.md" "b/\354\240\204\353\221\220\354\235\264/4\354\236\245/item24.md" new file mode 100644 index 0000000..4a46623 --- /dev/null +++ "b/\354\240\204\353\221\220\354\235\264/4\354\236\245/item24.md" @@ -0,0 +1,47 @@ +# 멤버 클래스는 되도록 static으로 만들어라 + +## 중첩 클래스란? + +다른 클래스 안에 정의된 클래스 + +- 정적 멤버 클래스 +- (비정적) 멤버 클래스 +- 익명 클래스 +- 지역 클래스 + +### 정적 멤버 클래스 + +- `static` 키워드가 붙어있으며, 바깥 클래스의 **private 멤버**에도 접근 가능 +- **바깥 클래스와 독립적으로 존재할 수 있다면 정적 멤버 클래스로 사용** +- 메모리와 생성 시간이 절약된다 +- **주 용도**: 바깥 클래스와 함께 사용되는 **public 도우미 클래스**로 활용 + - 예: 계산기의 연산(Operation)을 정의하는 열거 타입 + +### 비정적 멤버 클래스 + +- `static`이 없으며, **바깥 클래스의 인스턴스와 암묵적으로 연결**됨 +- 바깥 클래스의 **인스턴스 없이는 생성할 수 없음** +- 인스턴스 간 연결 정보가 메모리를 차지하며 생성 비용이 더 듦 +- **주 용도**: 바깥 클래스의 인스턴스에 접근해야 할 때 + - 예: 컬렉션의 뷰(Map의 keySet, entrySet 등)나 반복자(iterator) 구현 + +### 익명 클래스와 지역 클래스 + +- **익명 클래스**: 메서드 안에서 사용하며, **짧고 간결하게 표현할 때** 적합 +- **지역 클래스**: 한 메서드 안에서만 쓰이며, 익명 클래스로 표현하기 어려울 때 사용 + +### 권장 사항 + +외부 클래스의 인스턴스에 접근할 필요가 없다면 무조건 `static` 멤버 클래스로 사용 + +- 메서드 밖에서도 사용되거나 길다면 **멤버 클래스**로 만든다. +- 바깥 클래스의 인스턴스를 참조하면 **비정적**, 참조하지 않으면 **정적**으로 만든다. +- 한 메서드 안에서만 쓰이고, 생성 지점이 단 한 곳이라면 **익명 클래스**. +- 익명 클래스로 적합하지 않다면 **지역 클래스**로 만든다. + +--- + +## 결론 + +> 중첩 클래스를 사용할 때는 쓰임새를 명확히 구분하고, **바깥 클래스의 인스턴스를 참조할 필요가 없는 경우 반드시 `static`을 붙여 정적 멤버 클래스로 사용**하라. +> \ No newline at end of file diff --git "a/\354\240\204\353\221\220\354\235\264/4\354\236\245/item25.md" "b/\354\240\204\353\221\220\354\235\264/4\354\236\245/item25.md" new file mode 100644 index 0000000..3442cb7 --- /dev/null +++ "b/\354\240\204\353\221\220\354\235\264/4\354\236\245/item25.md" @@ -0,0 +1,10 @@ +# 톱레벨 클래스는 한 파일에 하나만 담으라 + +**소스 파일 하나에 톱레벨 클래스(또는 인터페이스)를 하나만 담아야 한다.** + +이 규칙을 지키면 다음과 같은 문제가 방지된다: + +1. **컴파일러가 클래스 정의를 여러 개 생성**하는 문제 +2. **컴파일 순서에 따라 프로그램 동작이 달라지는 문제** + +이 규칙을 따르면 프로그램의 동작이 일관되며, 유지보수성과 안정성이 높아진다. \ No newline at end of file diff --git "a/\354\240\204\353\221\220\354\235\264/5\354\236\245/item26.md" "b/\354\240\204\353\221\220\354\235\264/5\354\236\245/item26.md" new file mode 100644 index 0000000..6cfb39b --- /dev/null +++ "b/\354\240\204\353\221\220\354\235\264/5\354\236\245/item26.md" @@ -0,0 +1,29 @@ +# 로 타입은 사용하지 말라 + +## 로 타입이란 + +- **로 타입(raw type)**은 **제네릭 타입의 타입 매개변수를 지정하지 않은 상태**를 말한다. +- 예를 들어, 제네릭 타입 `Set`의 **로 타입**은 그냥 `Set`이다. +- 로 타입은 **제네릭이 도입되기 이전 코드와의 호환성**을 위해 제공되었지만, 사용하면 여러 문제가 발생할 수 있다. + +### **왜 로 타입을 사용하면 안 될까?** + +1. **타입 안전성이 없다** + - `Set`나 `Set`은 타입 시스템의 보호를 받지만, 로 타입인 `Set`은 그렇지 않음 + - 잘못된 타입의 객체를 저장하거나 가져올 때 **런타임 오류**가 발생할 수 있다. +2. **제네릭의 이점을 잃는다** + - 타입 검사를 컴파일 시점에 하지 않으므로 코드의 안정성과 가독성이 떨어진다. + + +### **대안** + +- `Set`: 어떤 타입의 객체도 저장할 수 있는 **매개변수화 타입** +- `Set`: 특정 타입이지만 **구체적으로 알 수 없는 타입**만 저장할 수 있는 **와일드카드 타입** + +--- + +## 결론 + +- **로 타입은 사용하지 말아야 한다**. +- 제네릭을 사용하여 **컴파일 시점에 타입 검사를 받고** 런타임 오류를 방지하자. +- 로 타입은 과거 코드와의 호환성을 위해 존재할 뿐, 현대 코드에서는 사용하지 않는 것이 좋다. \ No newline at end of file diff --git "a/\354\240\204\353\221\220\354\235\264/5\354\236\245/item27.md" "b/\354\240\204\353\221\220\354\235\264/5\354\236\245/item27.md" new file mode 100644 index 0000000..d472f33 --- /dev/null +++ "b/\354\240\204\353\221\220\354\235\264/5\354\236\245/item27.md" @@ -0,0 +1,47 @@ +# 비검사 경고를 제거하라 + +## 비검사 경고(Unchecked Warning)란 + +- **비검사 경고**는 Java에서 **제네릭 사용 시 타입 안전성(type safety)이 보장되지 않을 때** 발생하는 컴파일 경고이다. +- 컴파일러가 "이 코드가 올바르게 작동할지 확신할 수 없다"고 판단할 때 경고를 표시한다. +- **런타임에 ClassCastException이 발생할 가능성**이 있는 코드를 경고하는 것이다. + +### **왜 비검사 경고를 제거해야 할까?** + +1. **타입 안전성을 위반할 가능성** + - 비검사 경고가 있는 코드는 런타임에 **ClassCastException**을 발생시킬 위험이 있다. +2. **코드의 신뢰성과 유지보수성 저하** + - 경고를 무시하면 코드가 제대로 작동하지 않을 가능성이 높아지고, 추후 문제를 찾기가 어려워진다. + +--- + +### **비검사 경고를 없애는 방법** + +1. **경고를 완전히 제거하기** + - 제네릭 타입을 제대로 사용하여 **타입 안전성을 보장한다.** + - 예: + + ```java + List rawList = new ArrayList(); // 비검사 경고 발생 + List safeList = new ArrayList<>(); // 타입 안전 + ``` + +2. **경고를 제거할 수 없는 경우** + - 해당 코드가 타입 안전함을 **직접 증명한다.** + - 경고를 숨기기 위해 `@SuppressWarnings("unchecked")` 애너테이션을 사용한다. + - 반드시 코드에 **주석을 남겨 근거를 기록하자** + - 예: + + ```java + @SuppressWarnings("unchecked") // 이 캐스트는 타입 안전성을 검증했음 + List list = (List) someMethod(); + ``` + + +--- + +## 결론 + +- 비검사 경고는 **무시하지 말고 제거하라** +- **타입 안전성을 보장**할 수 있도록 코드를 수정하거나 제네릭을 올바르게 사용하라 +- 경고를 없앨 방법이 없을 경우, **안전성을 증명한 후** `@SuppressWarnings("unchecked")`를 사용해 숨기고, **주석으로 근거를 남겨라** \ No newline at end of file diff --git "a/\354\240\204\353\221\220\354\235\264/5\354\236\245/item28.md" "b/\354\240\204\353\221\220\354\235\264/5\354\236\245/item28.md" new file mode 100644 index 0000000..14b136a --- /dev/null +++ "b/\354\240\204\353\221\220\354\235\264/5\354\236\245/item28.md" @@ -0,0 +1,133 @@ +# 배열보다는 리스트를 사용하라 + +배열과 제네릭 타입의 차이점 2가지 + +## 1. 공변(Covariant) / 불공변(Invariant) + +공변성(Covariance)이란 + +**타입의 상속 관계에서 상위 타입 변수에 하위 타입 객체를 할당할 수 있는 특성을 뜻한다.** + +→ 즉, 자식 클래스의 객체는 부모 클래스의 타입 변수에 할당될 수 있을 때, 공변성을 가진다고 말한다. + +### 배열은 공변적이다 + +```java +Object[] objectArray = new Long[1]; // 정상 +objectArray[0] = "String"; // 런타임 오류 (ArrayStoreException) +``` + +상위 타입 Object[] 배열에 하위 클래스인 Long[] 배열을 할당할 수 있다. + +하지만, objectArray에 문자열 데이터를 넣으려고 하면 `ArrayStoreException`이 **런타임 시점에 발생**한다. + +### 제네릭은 불공변이다 (Invariant) + +제네릭 타입은 부모 타입과 자식 타입을 바꿀 수 없다는 의미이다. + +```java +List ol = new ArrayList(); // 호환되지 않는 타입이다. +ol.add("String"); // 컴파일 오류! +``` + +List와 List은 호환되지 않기 때문에, **컴파일 시점에 타입 오류가 발생**한다. + +### 런타임 에러와 컴파일 에러 + +- `런타임 에러`: 프로그램 실행 시점에 발생 (코드의 논리적 오류) + + 예시: null 참조, 배열의 인덱스를 잘못 참고, 잘못된 타입의 객체를 처리하려고 할 때 + +- `컴파일 에러`: 소스 코드가 컴파일될 때 발생 + + 예시: 문법 오류, 변수나 메서드의 호출의 오류, 컴파일러가 코드를 분석하면서 발견할 수 있는 오류들 + + +컴파일 오류는 개발 시점에 미리 발견할 수 있어서 빠르게 수정할 수 있지만, 런타임 오류는 실행 중에 발생하므로 더 큰 문제를 일으킬 수 있다. + +컴파일 시 오류를 줄이는 것이 더 안전하고 효율적인 개발을 가능하게 한다. + +--- + +## 2. 실체화(reify) / 소거(erasure) + +### 실체화(Reification) + +- 타입 정보가 런타임까지 유지되는 것을 의미한다. + +배열은 실체화된 타입이다. 즉, 배열은 런타임에서 타입 정보를 유지한다. 예를 들어 + +```java +String[] strings = new String[10]; +Object[] objects = strings; +objects[0] = 1; // 런타임 오류 발생 +``` + +위 코드에서 `objects`는 `Object[]`로 생성되었지만, 실제 배열 타입은 `String[]` 이다. 이로 인해 런타임 검사에서 정수 타입 1을 저장하려고 할 때 `ArrayStoreException`이 발생한다. + +이처럼 배열은 타입이 일치하지 않을 경우 런타임 에러를 발생시킨다. + +### 타입 소거(Type erasure) + +- 제네릭 타입의 정보가 컴파일 시점에서 제거되어 런타임 시점에서는 `원시 타입(raw type)`으로 처리되는 Java의 제네릭 처리 방식이다. +- 제네릭을 도입하기 전에, Java는 원시 타입만을 사용했으므로, 제네릭을 사용하더라도 이전 코드와의 호환성을 유지하기 위해 필요하다. + +제네릭은 컴파일 시점에만 타입 검사를 수행하고, 런타임에는 타입 정보가 존재하지 않는다. + +```java +// 컴파일 시에는 타입이 다르지만, 런타임에서는 둘 다 List로 취급된다. +List list1 = new ArrayList<>(); +List list2 = new ArrayList<>(); +``` + +위에서 `List`은 컴파일 시점에는 `List`으로 다루어지지만, 런타임에서는 그냥 `List`로 취급된다. + +그래서 `List` 와 `List`는 컴파일 시점에는 서로 다른 타입이지만, 런타임에서는 모두 `List`로 처리된다. + +### 제네릭 배열을 만들 수 없는 이유 + +이와 같이 제네릭은 타입 소거가 적용되기 때문에 런타임 시점에는 타입을 구별할 수 없다. + +반면, 배열은 런타입 시점에 타입 정보를 검사하므로 만약 제네릭 배열을 허용하게 되면 배열의 런타임 타입 검사와 제네릭의 타입 소거가 충돌하여 타입 안전성이 깨질 수 있기 때문에, 컴파일 시점에 에러가 발생한다. + +원본 코드(제네릭 타입 배열 생성 문제) → 컴파일 에러 + +```java +public class Chooser { + private final T[] choiceArray; + + public Chooser(Collection choices) { + choiceArray = choices.toArray(); // 컴파일 에러 + } +} +``` + +- `choices.toArray()` 의 반환 타입은 Object[] 이다. 따라서 T[]에 직접 할당하려고 할 때 타입 불일치 오류가 발생한다. + +수정된 코드(배열을 List로 변경) → 타입 안전성 확보 + +```java +public class Chooser { + private final List choiceList; + + public Chooser(Collection choices) { + choiceList = new ArrayList<>(choices); + } + + public T choose() { + Random rnd = ThreadLocalRandom.current(); + return choiceList.get(rnd.nextInt(choiceList.size())); + } +} +``` + +- 배열 대신 List를 사용하면 제네릭 타입의 요소들을 유연하게 저장할 수 있게 된다. +- `new ArrayList<>(choices)`를 사용하면 `Collection`의 `choices`를 타입 안전하게 리스트로 복사할 수 있다. + +--- + +## 결론 + +- 배열은 공변이고 실체화 되는 반면, 제네릭은 불공변이고 타입 정보가 소거된다. +- 이로 인해 배열은 타입 안전성을 보장해줄 수 없어 제네릭 배열을 생성할 수 없다. +- 만약 이 둘을 섞어 쓰다가 오류를 만나면, 가장 먼저 배열을 리스트로 대체하는 방법을 적용해보자. \ No newline at end of file diff --git "a/\354\240\204\353\221\220\354\235\264/5\354\236\245/item29.md" "b/\354\240\204\353\221\220\354\235\264/5\354\236\245/item29.md" new file mode 100644 index 0000000..dced79e --- /dev/null +++ "b/\354\240\204\353\221\220\354\235\264/5\354\236\245/item29.md" @@ -0,0 +1,15 @@ +# 이왕이면 제네릭 타입으로 만들라 + +## 왜 제네릭 타입을 사용하는 것이 좋은가? + +- **타입 안전성 보장:** 제네릭을 사용하면 **컴파일 시점**에 타입을 확인하므로 **런타임 오류**를 방지할 수 있다. +- **코드 가독성과 유지보수성 향상:** 제네릭을 사용하면 **형변환이 필요 없으므로** 코드가 간결하고 읽기 쉬워진다. +- **재사용성 증가:** 제네릭 타입은 **다양한 타입**에 대해 하나의 코드로 처리할 수 있어 **재사용성**이 높아진다. + +--- + +## 결론 + +- **제네릭 타입**은 형변환이 필요 없어서 더 안전하고 사용하기 쉽다. +- **새로운 타입 설계 시** 형변환 없이 사용할 수 있도록 **제네릭 타입을 적극 활용**하라 +- 기존 타입도 제네릭이 더 적합하다면 **제네릭 타입으로 변경**해 클라이언트 코드를 편리하게 만들어라 \ No newline at end of file diff --git "a/\354\240\204\353\221\220\354\235\264/5\354\236\245/item30.md" "b/\354\240\204\353\221\220\354\235\264/5\354\236\245/item30.md" new file mode 100644 index 0000000..b3ebc97 --- /dev/null +++ "b/\354\240\204\353\221\220\354\235\264/5\354\236\245/item30.md" @@ -0,0 +1,46 @@ +# 이왕이면 제네릭 메서드로 만들라 + +### **왜 제네릭 메서드를 사용하는 것이 좋은가?** + +- **타입 안정성 보장:** 입력과 출력 타입을 컴파일 시점에 명확히 지정하므로 **런타임 오류**를 방지할 수 있다. +- **형변환 필요 없음:** 제네릭 메서드는 **자동으로 타입이 지정되므로** 클라이언트가 `형변환(casting)`을 하지 않아도 된다. +- **가독성과 유지보수성 향상:** 코드에서 타입이 명확히 드러나므로 **가독성이 좋고**, 잘못된 타입 사용을 컴파일 시점에 잡아준다. + +### 제네릭 활용 예시 + +**두 집합의 합집합을 반환하는 메서드** + +```java +public static Set union(Set s1, Set s2) { + Set result = new HashSet<>(); + result.addAll(s2); + return result; +} +``` + +**제네릭 메서드로 변경한 코드** + +```java +public static Set union(Set s1, Set s2) { + Set result = new HashSet<>(s1); + result.addAll(s2); + return result; +} + +public static void main(String[] args) { + Set guys = Set.of("톰", "딕", "해리"); + Set stooges = Set.of("래리", "모에", "컬리"); + Set aflCio = union(guys, stooges); + System.out.println(aflCio); +} +``` + +- 기존 union 메서드에서 제네릭으로 변경함으로써 이는 타입 안전하고 직접 형변환 하지 않아도 경고 없이 컴파일 된다. + +--- + +## 결론 + +- 제네릭 타입처럼, 제네릭 메서드도 형변환 없이 사용할 수 있어 더 안전하고 편리하다. +- 새로운 메서드를 설계할 때는 형변환 없이 사용할 수 있도록 제네릭 메서드로 구현하라. +- 기존 메서드도 제네릭하게 개선하면, 기존 코드는 유지하면서 새로운 사용자를 더 편리하게 지원 가능하다. \ No newline at end of file