From d20e0f9906884a0d2e746837ff2aecb4c3745ae8 Mon Sep 17 00:00:00 2001 From: Seoyoon Tak Date: Mon, 30 Jun 2025 02:17:54 +0900 Subject: [PATCH 01/14] item10 --- peng255/src/main/java/item10.md | 85 +++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 peng255/src/main/java/item10.md diff --git a/peng255/src/main/java/item10.md b/peng255/src/main/java/item10.md new file mode 100644 index 0000000..9cb27eb --- /dev/null +++ b/peng255/src/main/java/item10.md @@ -0,0 +1,85 @@ +## 10. equals는 일반 규약을 지켜 재정의하라 +### **언제 equals를 재정의해야 하는가?** + +- 인스턴스마다 고유한 객체(예: Thread 등)는 equals를 재정의하지 않는다. +- 논리적 동등성(값이 같으면 같다고 판단)이 필요한 값 객체(예: Integer, String 등)는 equals를 재정의해야 한다. +- 상위 클래스가 이미 equals를 적절히 재정의했다면 굳이 다시 재정의할 필요 없다. +- private 또는 package-private 클래스이고 equals가 호출될 일이 없다면 재정의하지 않는다. + +- equals를 재정의할 땐 5가지 규약(반사성, 대칭성, 추이성, 일관성, null-불일치)을 반드시 지켜야 한다. +- 잘못 구현하면 컬렉션 등에서 예측 불가능한 버그가 발생한다. +- 값 객체라면 equals와 hashCode를 함께 재정의한다. + +### **equals 일반 규약** + +equals 메서드를 재정의할 때 반드시 다음 5가지 규약을 지켜야 한다 + +| **규약** | **설명** | +| --- | --- | +| 반사성 | x.equals(x)는 항상 true여야 한다. | +| 대칭성 | x.equals(y)가 true면 y.equals(x)도 true여야 한다. | +| 추이성 | x.equals(y)와 y.equals(z)가 true면 x.equals(z)도 true여야 한다. | +| 일관성 | x.equals(y)는 객체 상태가 변하지 않는 한 여러 번 호출해도 항상 같은 결과를 반환해야 한다. | +| null-불일치 | x.equals(null)은 항상 false여야 한다. | + +### **잘못된 예시** + +**대칭성 위반 예시** + +```java +public final class CaseInsensitiveString { + private final String s; + public CaseInsensitiveString(String s) { this.s = s; } + + @Override + public boolean equals(Object o) { + if (o instanceof CaseInsensitiveString) + return s.equalsIgnoreCase(((CaseInsensitiveString) o).s); + if (o instanceof String) // String과의 비교도 true로 처리 + return s.equalsIgnoreCase((String) o); + return false; + } +} +``` + +위 코드에서`cis.equals("abc")`는 true지만,`"abc".equals(cis)`는 false가 되어 대칭성을 위반한다. + +**추이성 위반 예시** + +```java +public class Point { + private final int x, y; + // equals: x, y만 비교 +} + +public class ColorPoint extends Point { + private final Color color; + // equals: x, y, color까지 비교 +} +``` + +Point와 ColorPoint를 섞어서 비교하면 추이성이 깨질 수 있다. + +### **올바른 equals 구현 예시** + +```java +@Override +public boolean equals(Object o) { + if (this == o) return true; // 반사성 + if (!(o instanceof Book)) return false; // 타입 체크 및 null-불일치 + Book other = (Book) o; + return this.name.equals(other.name) + && this.published == other.published + && this.content.equals(other.content); // 중요한 필드 비교 +} +``` + +- 반드시 Object 타입을 파라미터로 받아야 한다. +- instanceof로 타입 체크를 하고, 필드별 값 비교를 한다. +- null 체크는 instanceof가 대신 처리한다. + +### **equals 재정의 시 주의점** + +- equals를 재정의하면 반드시 hashCode도 재정의한다. +- 필드 비교는 논리적으로 중요한 필드만 대상으로 한다. +- 성능상, 먼저 다를 확률이 높은 필드부터 비교하는 것이 좋다. \ No newline at end of file From c34e8d4180d1ac22543cd412d8deea1fefee9f55 Mon Sep 17 00:00:00 2001 From: Seoyoon Tak Date: Mon, 30 Jun 2025 02:18:48 +0900 Subject: [PATCH 02/14] item11 --- peng255/src/main/java/item11.md | 73 +++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 peng255/src/main/java/item11.md diff --git a/peng255/src/main/java/item11.md b/peng255/src/main/java/item11.md new file mode 100644 index 0000000..fa39769 --- /dev/null +++ b/peng255/src/main/java/item11.md @@ -0,0 +1,73 @@ +## 11. equals를 재정의하려거든 hashCode도 재정의하라 +### **왜 hashCode를 재정의해야 하는가?** + +- equals만 재정의하고 hashCode를 재정의하지 않으면, HashMap, HashSet 등 컬렉션에서 객체가 제대로 동작하지 않는다. +- hashCode 규약은 다음과 같다: + - 같은 객체에 대해 hashCode를 여러 번 호출해도 값이 변하지 않아야 한다(equals에 사용되는 정보가 변하지 않는 한). + - equals로 비교해 같다고 판단되는 두 객체는 반드시 같은 hashCode 값을 가져야 한다. + - equals로 다르다고 판단되는 두 객체는 hashCode가 다를 수도 같을 수도 있지만, 다르면 성능이 좋아진다. + +### **잘못된 예시** + +```java +@Override public int hashCode() { return 42; } +``` + +- 모든 객체가 같은 hashCode를 반환하므로, HashMap/HashSet이 내부적으로 리스트처럼 동작해서 성능이 크게 저하된다. + +### **좋은 hashCode 구현법** + +1. 의미 있는 필드(=equals 비교에 쓰는 필드)들의 값을 이용해 hashCode를 만든다. +2. 각 필드의 hashCode 값을 31로 곱해가며 누적한다. +3. 최종적으로 result를 반환한다. + +### **예시** + +```java +@Override +public int hashCode() { + int result = Short.hashCode(areaCode); + result = 31 * result + Short.hashCode(prefix); + result = 31 * result + Short.hashCode(lineNum); + return result; +} +``` + +- 의미 있는 필드가 여러 개라면, 순서대로 31을 곱해가며 합산한다. + +### **간단한 방법 (성능이 중요하지 않을 때)** + +```java +@Override +public int hashCode() { + return Objects.hash(lineNum, prefix, areaCode); +} +``` + +- 이 방식은 코드가 짧지만, 내부적으로 배열 생성 등 오버헤드가 있어 성능이 아주 중요할 때는 권장하지 않는다. + +### **캐싱 기법** + +- 불변 객체에서 hashCode 계산 비용이 크면, hashCode를 필드로 저장해두고 처음 한 번만 계산하는 방법도 있다. + +```java +private int hashCode; // 기본값 0 + +@Override +public int hashCode() { + int result = hashCode; + if (result == 0) { + result = Short.hashCode(areaCode); + result = 31 * result + Short.hashCode(prefix); + result = 31 * result + Short.hashCode(lineNum); + hashCode = result; + } + return result; +} +``` + +### **주의점** + +- equals에 사용하지 않는 필드는 hashCode 계산에 포함시키지 않는다. +- 성능을 위해 중요한 필드를 빼면 해시 품질이 떨어져서 HashMap/HashSet의 성능이 나빠질 수 있다. +- hashCode 구현 결과값의 구체적인 스펙을 문서로 명시하지 않는다(유연성 확보) \ No newline at end of file From 8229f95c94281badded94e7105475d8731e4b80d Mon Sep 17 00:00:00 2001 From: Seoyoon Tak Date: Mon, 30 Jun 2025 02:18:56 +0900 Subject: [PATCH 03/14] item12 --- peng255/src/main/java/item12.md | 61 +++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 peng255/src/main/java/item12.md diff --git a/peng255/src/main/java/item12.md b/peng255/src/main/java/item12.md new file mode 100644 index 0000000..b41a74c --- /dev/null +++ b/peng255/src/main/java/item12.md @@ -0,0 +1,61 @@ +## 12. toString을 항상 재정의하라 +- Object의 기본 toString은 클래스명@해시코드(16진수) 형태로, 사람이 보기엔 거의 쓸모가 없다. +- toString의 일반 규약은 “**간결하면서도 사람이 읽기 쉬운, 유용한 정보**”를 반환해야 한다. +- toString을 재정의하면 디버깅, 로그, 출력, 에러 메시지 등에서 객체 정보를 쉽게 확인할 수 있다. +- 컬렉션이나 Map에 객체가 들어갈 때도 toString이 잘 구현되어 있으면 전체 구조를 한눈에 파악하기 쉽다. +- toString 구현 시, 객체의 중요한 정보를 모두 포함하는 것이 좋다. + + 단, 객체가 너무 크거나 복잡하면 요약 정보를 반환한다. + + +### **예시: PhoneNumber 클래스** + +```java +@Override +public String toString() { + return String.format("%03d-%03d-%04d", areaCode, prefix, lineNum); +} +``` + +- 010-123-4567처럼 사람이 직관적으로 이해할 수 있는 형태로 반환한다. + +### **포맷 명시의 장단점** + +- **포맷을 명시하면:** + - 표준화된 문자열 표현을 만들 수 있다. + - CSV, 로그, 파일 저장 등에 활용하기 좋다. + - 반대로, 포맷을 바꾸면 기존 사용자 코드가 깨질 수 있으니 신중해야 한다. +- **포맷을 명시하지 않으면:** + - 향후 포맷 변경이 자유롭다. + - 대신, 외부에서 toString 결과를 파싱해서 쓰면 안 된다. + +### **포맷 명시 예시 (JavaDoc)** + +```java +// "XXX-YYY-ZZZZ" 형태의 전화번호 문자열을 반환한다. +// 각 부분이 자릿수보다 작으면 앞에 0을 붙인다. +@Override +public String toString() { + return String.format("%03d-%03d-%04d", areaCode, prefix, lineNum); +} +``` + +### **포맷 미명시 예시 (JavaDoc)** + +```java +// 이 포션의 간단한 설명을 반환한다. +// 구체적 포맷은 바뀔 수 있다. +// 예: "[Potion #9: type=love, smell=turpentine, look=india ink]" +@Override +public String toString() { ... } +``` + +### **추가 팁** + +- toString이 반환하는 정보는 반드시 getter 등으로도 접근할 수 있게 만든다. + + 그렇지 않으면, 외부에서 toString 결과를 파싱해서 쓰는 불안정한 코드가 생길 수 있다. + +- static 유틸리티 클래스나 대부분의 enum 타입에는 toString을 따로 구현하지 않는다. +- IDE 자동 생성 toString도 쓸 수 있지만, 의미 있는 값 객체라면 직접 포맷을 정의하는 것이 더 좋다. +- toString을 재정의하지 않으면, 로그나 에러 메시지에서 객체 정보를 알 수 없어 디버깅이 불편해진다. \ No newline at end of file From 240b283e5340d50af2c79454835db8c18c8a35aa Mon Sep 17 00:00:00 2001 From: Seoyoon Tak Date: Mon, 30 Jun 2025 02:19:06 +0900 Subject: [PATCH 04/14] item13 --- peng255/src/main/java/item13.md | 112 ++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 peng255/src/main/java/item13.md diff --git a/peng255/src/main/java/item13.md b/peng255/src/main/java/item13.md new file mode 100644 index 0000000..7c57b84 --- /dev/null +++ b/peng255/src/main/java/item13.md @@ -0,0 +1,112 @@ +## 13. clone 재정의는 주의해서 진행하라 +### **Cloneable 인터페이스란** + +- Cloneable은 "이 객체는 복제(clone)할 수 있다"는 의도를 나타내는 마커 인터페이스다. +- 메서드는 없고, 단지 Object의 protected clone() 메서드가 예외를 던지지 않게 해준다 +- Cloneable을 구현하지 않으면 clone() 호출 시 CloneNotSupportedException이 발생한다. + +### **clone 메서드의 동작** + +- Cloneable을 구현한 객체에서 Object의 clone()을 호출하면 **필드 단위로 복사한 새 객체**가 만들어진다(기본적으로 shallow copy) +- 하지만, Cloneable 인터페이스 자체에는 clone 메서드가 없으므로 **직접 public clone()을 오버라이드**해야 한다 + +### **clone 메서드의 일반 규약** + +- x.clone() != x (복제 객체는 원본과 다르다) +- x.clone().getClass() == x.getClass() (보통 복제 객체의 타입은 원본과 같다) +- x.clone().equals(x) == true (보통 값이 같지만, 필수는 아니다) +- 복제본은 원본과 독립적이어야 한다. 즉, 내부 상태가 서로 영향을 주지 않아야 한다 + +### **shallow copy vs deep copy** + +| **Shallow Copy** | **Deep Copy** | +| --- | --- | +| 필드 값만 복사, 참조 타입은 같은 객체를 가리킴 | 모든 필드와 참조 객체까지 재귀적으로 복사 | +| 내부에 가변 객체가 있으면 원본/복제본이 서로 영향을 준다 | 완전히 독립적인 객체가 된다 | +| 빠르고 메모리 효율적 | 느리지만 안전하게 분리됨 | +| 불변 객체, 간단한 구조에 적합 | 복잡하거나 가변 객체가 있을 때 필요 | +- clone()의 기본 동작은 shallow copy다. +- 내부에 가변 객체(예: 배열, 컬렉션 등)가 있으면, clone() 오버라이드 시 deep copy를 직접 구현해야 한다 + +### **잘못된 예시: shallow copy만 하는 경우** + +```java +@Override +public Stack clone() { + try { + return (Stack) super.clone(); // elements 배열이 공유됨! + } catch (CloneNotSupportedException e) { + throw new AssertionError(); + } +} +``` + +- 이 경우 복제본과 원본이 같은 배열을 공유해서, 한쪽을 변경하면 다른 쪽에도 영향이 간다. + +### **올바른 예시: deep copy 구현** + +```java +@Override +public Stack clone() { + try { + Stack result = (Stack) super.clone(); + result.elements = elements.clone(); // 배열 복제 + return result; + } catch (CloneNotSupportedException e) { + throw new AssertionError(); + } +} +``` + +- elements 배열을 새로 복사해서 원본과 복제본이 완전히 분리된다 + +### **복잡한 구조(예: 연결리스트)의 deep copy** + +```java +private static class Entry { + final Object key; + Object value; + Entry next; + + Entry deepCopy() { + return new Entry(key, value, next == null ? null : next.deepCopy()); + } +} + +@Override +public HashTable clone() { + try { + HashTable result = (HashTable) super.clone(); + result.buckets = new Entry[buckets.length]; + for (int i = 0; i < buckets.length; i++) + if (buckets[i] != null) + result.buckets[i] = buckets[i].deepCopy(); + return result; + } catch (CloneNotSupportedException e) { + throw new AssertionError(); + } +} +``` + +- 각 버킷의 연결리스트까지 재귀적으로 복제한다. + +### **Cloneable/clone의 문제점** + +- clone은 생성자를 호출하지 않으므로, 객체 불변성/초기화 보장이 어렵다. +- final 필드와 호환이 안 좋다. +- 복잡한 객체에서는 deep copy를 직접 구현해야 해서 실수하기 쉽다. +- 인터페이스임에도 메서드가 없어, 일관된 사용이 어렵다. +- checked exception(throws CloneNotSupportedException) 처리도 번거롭다 + +### **대안: 복사 생성자/복사 팩토리** + +```java +// 복사 생성자 +public Stack(Stack original) { ... } + +// 복사 팩토리 +public static Stack newInstance(Stack original) { ... } +``` + +- 명확하고, final 필드, 불변성, 상속 등과의 호환성이 좋다. +- 복사 과정이 명시적이라 실수할 여지가 적다 \ No newline at end of file From 74aca192d07c7b6dc5b8df3371cba4090d1d562a Mon Sep 17 00:00:00 2001 From: Seoyoon Tak Date: Mon, 30 Jun 2025 02:20:27 +0900 Subject: [PATCH 05/14] item13 --- peng255/src/main/java/item13.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peng255/src/main/java/item13.md b/peng255/src/main/java/item13.md index 7c57b84..18acf4d 100644 --- a/peng255/src/main/java/item13.md +++ b/peng255/src/main/java/item13.md @@ -86,7 +86,7 @@ public HashTable clone() { throw new AssertionError(); } } -``` +``` - 각 버킷의 연결리스트까지 재귀적으로 복제한다. From 97e28c573f5502e3721bde135911ab4bdb98b267 Mon Sep 17 00:00:00 2001 From: Seoyoon Tak Date: Mon, 18 Aug 2025 17:31:10 +0900 Subject: [PATCH 06/14] =?UTF-8?q?14,=2015,=2016=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- peng255/src/main/java/item14.md | 96 +++++++++++++++++++++++++++++++++ peng255/src/main/java/item15.md | 73 +++++++++++++++++++++++++ peng255/src/main/java/item16.md | 64 ++++++++++++++++++++++ 3 files changed, 233 insertions(+) create mode 100644 peng255/src/main/java/item14.md create mode 100644 peng255/src/main/java/item15.md create mode 100644 peng255/src/main/java/item16.md diff --git a/peng255/src/main/java/item14.md b/peng255/src/main/java/item14.md new file mode 100644 index 0000000..31e461b --- /dev/null +++ b/peng255/src/main/java/item14.md @@ -0,0 +1,96 @@ +## 14. Comparable을 구현할지 고려하라 + +### 요점 정리 + +- 자바의 `Comparable` 인터페이스는 객체 간의 자연 순서(natural ordering)를 정의하기 위해 존재한다. `equals`가 동등성만 비교한다면, `compareTo`는 순서 비교까지 가능하게 한다. +- 자연 순서(natural ordering)가 명확한 값 클래스라면 반드시 Comparable을 구현해보자. +- compareTo 구현 시 `<`,`>` 연산자 대신 `Integer.compare`, `Comparator.comparingInt` 같은 정적 비교 메서드를 사용하자. +- equals와 compareTo의 결과를 일치시키는 것이 좋다. +- 잘못된 compareTo 구현은 **`TreeSet`**, **`TreeMap`** 같은 정렬 기반 컬렉션에서 예기치 못한 버그를 일으킬 수 있다. + +### Comparable을 잘못된 방식으로 구현하는 예 + +*비교를 뺄셈으로 구현하면 안 된다!* + +```java +static Comparator hashCodeOrder = (o1, o2) -> o1.hashCode() - o2.hashCode(); +``` + +문제점: + +- **오버플로우** 발생 가능 +- **부동소수점**에서는 IEEE 754 특성 때문에 **잘못된 비교 결과** 유발 + +올바른 방법: + +```java +// 안전한 구현 +static Comparator hashCodeOrder = + Comparator.comparingInt(Object::hashCode); +``` + +--- + +**1. Comparable 구현이 필요한 경우인가?** + +- 정렬이 필요한 값 클래스인가? (**`String`**,**`Integer`**,`LocalDate`같은 경우) +- 특정 기준으로 "자연스러운 순서"가 명확한가? + - 예: 이름 → 알파벳 순서 + - 숫자 → 크기 순서 + - 날짜/시간 → 시간순 + +👉 명확하다면 **`Comparable`** 구현해보자 + +**2. compareTo 계약 지키기** + +- **`sgn(x.compareTo(y)) == -sgn(y.compareTo(x))`** (대칭성) +- **`(x > y && y > z) ⇒ x > z`** (추이성) +- **`x == y ⇒ x와 y를 비교한 결과가 항상 같은 부호`** +- 가급적 **`compareTo == 0 ⇔ equals == true`** 지키기 + +**3. 구현 패턴** + +- **기본 원칙**:**`<`**,**`>`**대신**`Integer.compare`**,**`Comparator.comparing`**사용 +- 🚫 차이(b - a) 연산 이용 금지 (오버플로우 위험 있음!) + +**4. 단일 필드일 경우** + +```java +@Override +public int compareTo(Person p) { + return name.compareTo(p.name); +} +``` + +**5. 다중 필드일 경우** + +**(전통 방식 버전! 불가피할 때만 쓰기)** + +```java +@Override +public int compareTo(PhoneNumber pn) { + int result = Integer.compare(areaCode, pn.areaCode); + if (result == 0) { + result = Integer.compare(prefix, pn.prefix); + if (result == 0) + result = Integer.compare(lineNum, pn.lineNum); + } + return result; +} +``` + +**(실무에서 권장, Java 8+)** + +```java +private static final Comparator COMPARATOR = + Comparator.comparingInt((PhoneNumber pn) -> pn.areaCode) + .thenComparingInt(pn -> pn.prefix) + .thenComparingInt(pn -> pn.lineNum); + +@Override +public int compareTo(PhoneNumber pn) { + return COMPARATOR.compare(this, pn); +} +``` + +Comparator.comparingInt를 이용한다 \ No newline at end of file diff --git a/peng255/src/main/java/item15.md b/peng255/src/main/java/item15.md new file mode 100644 index 0000000..1378abb --- /dev/null +++ b/peng255/src/main/java/item15.md @@ -0,0 +1,73 @@ +## 15. 클래스와 멤버의 접근 권한을 최소화하라 + +잘 설계된 컴포넌트는 자신의 내부 구현을 철저히 감춘다. 이를 **정보 은닉(Encapsulation)**이라고 하며, 소프트웨어 설계의 핵심 원칙이다! + +접근 권한을 최소화하면 컴포넌트 간 결합도가 낮아져 개발, 테스트, 유지보수, 최적화, 재사용에서 큰 장점이 생긴다. + +### 접근 제어자 선택 기준 + +상황에 맞게 접근 제어자를 정해야 한다. + +최대한 닫고, 꼭 필요할 때만 점진적으로 여는 것을 기본으로 한다. + +**1. 클래스/인터페이스 (Top-level)** + +| `public` | 외부 API로 공개해야 할 클래스 | 최소한으로만 | +| --- | --- | --- | +| `default`(package-private) | 같은 패키지 안에서만 사용 | ✅ 기본 선택 | + +**2. 멤버(필드, 메서드, 중첩 클래스)** + +| **접근 제어자** | **사용 범위** | **실무 권장도** | **비고** | +| --- | --- | --- | --- | +| `private` | 해당 클래스 내부 | 최우선 선택 | 기본값으로 시작 | +| `default`(package-private) | 같은 패키지 내부 | 필요한 경우만 | 테스트 용도 or 패키지 내부 협력 | +| `protected` | 같은 패키지 + 하위 클래스 | 신중히 사용 | 사실상 public API, 영구 유지 부담 | +| `public` | 어디서든 | 최소한만 사용 | 진짜 공개 API만 | + +**3. 필드(field)** + +- **인스턴스 필드(public) → 절대 금지** + - 불변식 깨짐, 스레드 안정성 붕괴할 수 있기 때문! +- **상수(public static final)** → 허용 + - 단, 반드시 **기본 타입** 또는 **불변 객체**만 사용 + - 배열 직접 공개 금지 → **`Collections.unmodifiableList()`** 또는 **`clone()`**으로 대체하자 + +1. **테스트 목적 접근 제어** +- private → package-private 확대는 OK +- public까지 확대는 절대 금지 +- 같은 패키지에 테스트 클래스를 두면 package-private 멤버 접근 가능 + +> 정리 +> 1. 클래스는 기본적으로 package-private +> 2. 멤버는 기본적으로 private +> 3. public은 진짜 공개 API만, protected는 특별한 경우만 +> 4. 필드 공개 금지 (상수 제외, 배열은 특히 주의) +> 5. 테스트용 접근 확장은 package-private까지만 + + +### 주의해야할 원칙들 + +- **public static final 배열 금지** + - 배열은 항상 가변적이기 때문에 보안 구멍이 생길 수 있다 + + ```java + public static final Thing[] VALUES = { ... }; // ❌ 위험 + ``` + + - 해결책: + 1. 불변 리스트로 감싸기 (Collections.unmodifiableList) + + ```java + private static final Thing[] PRIVATE_VALUES = { ... }; + public static final List VALUES = + Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES)); + ``` + + 2. 복제본 반환하기 + ```java + private static final Thing[] PRIVATE_VALUES = { ... }; + public static Thing[] values() { + return PRIVATE_VALUES.clone(); + } + ``` \ No newline at end of file diff --git a/peng255/src/main/java/item16.md b/peng255/src/main/java/item16.md new file mode 100644 index 0000000..fe3a1ee --- /dev/null +++ b/peng255/src/main/java/item16.md @@ -0,0 +1,64 @@ +## 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 + +### public 클래스에 public 필드를 두면 생기는 문제들 + +**1. 캡슐화(정보은닉) 깨짐** + +- (권장) **`private + getter/setter`** 설계 시: 내부 표현 변경 시 외부 API 그대로 유지 가능하다 +- (권장x) **`public field`**: 내부 구현 상세가 API 계약으로 굳어져, 표현 방식을 바꿀 수 없게 된다 + +--- + +**2. 불변식(invariant) 강제 불가** + +- 예: 좌표(Point)에서 **`x,y ≥ 0`** 조건을 강제하고 싶음 → 불가능 +- setter 메서드가 없으므로 검증할 통로가 없음 +- 필드 직접 접근이 가능해 **잘못된 상태의 객체**가 생길 수 있음 + +--- + +**3. 필드 접근 시 부수작용(로깅, 검증, 캐싱 등) 불가** + +- 예: **`setBalance()`** 호출 시 로그 남기기 +- **필드 직접 접근**하면 이런 **후처리 불가능 →** 나중에 요구사항 생기면 API 전체를 바꿔야 한다 + +--- + +**4. API 변경 불가 → 유지보수가 너무 어려워진다** + +- `public double x;`를 `private int x;`로 바꾸는 순간 외부 모든 클라이언트가 컴파일 에러 + +--- + +**5. 멀티스레드 환경에서 안전성 깨짐** + +- 필드가 `public`이라 동기화 처리 불가 +- 예: **`x`** 값을 읽거나 쓸 때 동시성 문제가 발생해도 제어 불가 + +--- + +**6. 배열 같은 참조 타입은 특히 치명적** + +- **`public static final Type[] arr;`** → 모듈 사용자가 **배열 안 값 변경 가능** +- 보안 취약점 및 심각한 **버그를 유발** + +```java +// 캡슐화를 잘 적용한 예시 +public class Point { + private double x; + private double y; + + public Point(double x, double y) { + this.x = x; + this.y = y; + } + + // getter + public double getX() { return x; } + public double getY() { return y; } + + // setter (클래스가 불변이라면 생략 가능) + public void setX(double x) { this.x = x; } + public void setY(double y) { this.y = y; } +} +``` \ No newline at end of file From 14f5a287c811c9653f91a455d54b00ae43de3930 Mon Sep 17 00:00:00 2001 From: Seoyoon Tak Date: Tue, 26 Aug 2025 00:22:29 +0900 Subject: [PATCH 07/14] =?UTF-8?q?17,=2018,=2019=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- peng255/src/main/java/item17.md | 83 +++++++++++++++++++++++ peng255/src/main/java/item18.md | 113 ++++++++++++++++++++++++++++++++ peng255/src/main/java/item19.md | 81 +++++++++++++++++++++++ 3 files changed, 277 insertions(+) create mode 100644 peng255/src/main/java/item17.md create mode 100644 peng255/src/main/java/item18.md create mode 100644 peng255/src/main/java/item19.md diff --git a/peng255/src/main/java/item17.md b/peng255/src/main/java/item17.md new file mode 100644 index 0000000..ff8c956 --- /dev/null +++ b/peng255/src/main/java/item17.md @@ -0,0 +1,83 @@ +## 17. 변경 가능성을 최소화하라 + +### 요점 정리 + +- 객체는 가능하다면 immutable (변경 불가능)하게 설계하자. + + 왜?? 불변 클래스를 쓰면 설계, 구현이 단순하다. 오류도 줄어들고 보안과 스레드 안정성도 얻을 수 있음!! + +- public 생성자 대신 static factory를 쓰면 캐싱 최적화를 할 수 있다 + +### 불변 클래스를 만들기 위한 규칙들 + +1. 상태를 변경하는 메서드들을 제공하지 않기 (setter, mutator 등) +2. 클래스를 확장할 수 없게 만들어야 한다 + - final로 선언하기 + - 생성자를 private / package-private으로 막고 static factory만 제공하는 방식도 가능하다 +3. 모든 필드를 final로 선언하기 +4. 모든 필드를 private로 선언하기. + - 내부 표현을 외부에서 직접 변경하기 못하게 보호하기 위함 +5. mutable 객체를 참조하는 필드가 있다면 defensive copy를 사용한다 + +### 불변 객체의 장점 + +- **단순성 :** 상태가 생성시점부터 변하지 않는다! +- **안전성 :** + - 스레드 안전하다 → 추가 동기화가 필요 없다 + - 공유 가능 → 여러 쓰레드가 동시에 사용해도 문제가 없다 +- **재사용성 :** + - 자주 쓰이는 값은 `public static final` 상수로 제공할 수 있다 + + ```java + // ex. 불변 클래스 Complex가 있을 때 + public static final Complex ZERO = new Complex(0, 0); + public static final Complex ONE = new Complex(1, 0); + public static final Complex I = new Complex(0, 1); + ``` + + +### 정적 팩토리 방식으로 불변 클래스 만들기 + +```java +public class Complex { + private final double re; + private final double im; + + private Complex(double re, double im) { + this.re = re; + this.im = im; + } + + public static Complex valueOf(double re, double im) { + return new Complex(re, im); + } +} +``` + +valueOf과 같은 방식으로 만들 수도 있다 + +- public 생성자 대신 static factory를 쓰면 캐싱 최적화를 할 수 있다 + + → valueOf로 호출할 때 자주 쓰이는 객체를 미리 만들어두고 돌려줄 수 있다 + + ```java + public static final Complex ZERO = new Complex(0,0); + public static final Complex ONE = new Complex(1,0); + + public static Complex valueOf(double re, double im) { + if (re == 0 && im == 0) return ZERO; + if (re == 1 && im == 0) return ONE; + return new Complex(re, im); + } + ``` + +- 외부에서 상속 불가 → 불변성 안전 + + 사실상 final과 같은 효과. 왜냐하면 생성자가 private니까 …extends ImmutableClass 이런 식으로 상속하는 게 불가능하기 때문 + + +- **비유 :** + + public 생성자는 그냥 “객체 만드는 기계 버튼”이고 + + static factory는 “객체를 제공하는 창구”라서 내부 사정에 따라 새 걸 줄지, 기존 걸 줄지 / 어떤 구조로 만들어진 구현체를 줄지 내부에서 결정할 수 있는 자유가 생긴다. \ No newline at end of file diff --git a/peng255/src/main/java/item18.md b/peng255/src/main/java/item18.md new file mode 100644 index 0000000..146cd10 --- /dev/null +++ b/peng255/src/main/java/item18.md @@ -0,0 +1,113 @@ +## 18. 상속보다는 컴포지션을 사용하라 + +### 요점 정리 + +- 상속은 코드 재사용을 위한 강력한 방법이지만… 설계/유지보수 면에서 위험할 수 있다 + - 특히 다른 사람이 만든 일반 concrete class를 상속하면, 부모 클래스의 구현에 의존하게 돼서 자식 클래스가 취약해진다. + - 그러니 특별한 경우를 제외하면 상속 대신 composition과 전달을 활용하는 것이 더 안전하다 +- B is A가 진짜로 성립할 때만 extends를 사용하자 +- B is A가 성립하지 않는다면 composition으로 감싸서 원하는 기능을 추가하자 +- composition을 사용하면 더 유연하고 안정적이고 확장 가능한 API를 만들 수 있다 + +### 상속의 문제점 + +부모 클래스의 내부 구현이 변경되면 자식 클래스가 깨질 수 있다. + +```java +// 부모 클래스 +class Parent { + public void doSomething() { + System.out.println("기본 동작"); + } + + // v1 버전에서는 이 메서드가 없음 + // → 나중에 v2 버전에서 추가됨 + public void newFunction() { + doSomething(); // 내부적으로 doSomething 호출 + } +} + +// 자식 클래스 +class Child extends Parent { + @Override + public void doSomething() { + System.out.println("자식 클래스 동작"); + } +} + +public class InheritanceBreakDemo { + public static void main(String[] args) { + Child child = new Child(); + + // 1. 원래 의도 + child.doSomething(); // "자식 클래스 동작" + + // 2. 부모가 나중에 추가한 메서드 + child.newFunction(); + // 의도: "기본 동작" + // 실제: "자식 클래스 동작" (== 부모 의도 깨짐!) + } +} +``` + +- Parent 클래스에 새 메서드 newFunction()을 추가했다고 생각해보자. 이 메서드에서는 내부적으로 doSomething()을 호출한다 + + 그런데 Child 클래스가 doSomething()을 오버라이드했기 때문에 `child.newFunction()`을 실행했을 때 newFunction 내부에서 부모의 doSomething이 아닌 Child의 soSomething이 실행된다 + → 예상치 못한 동작을 하게 됨 +

+**- 컴포지션으로 바꿔보자** + +```java +// 부모 역할 클래스 +class Parent { + public void doSomething() { + System.out.println("기본 동작"); + } + + public void newFunction() { + doSomething(); // 안전하게 기본 doSomething 실행 + } +} + +// 자식은 "상속하지 않고 포함(Composition)" 함 +class Child { + private final Parent parent = new Parent(); + + // Child만의 독립적인 동작 + public void customDoSomething() { + System.out.println("자식 클래스 동작"); + } + + // 부모 기능을 그대로 쓰고 싶으면 위임(forwarding) + public void parentDoSomething() { + parent.doSomething(); + } + + public void parentNewFunction() { + parent.newFunction(); + } +} + +public class CompositionDemo { + public static void main(String[] args) { + Child child = new Child(); + + // 자식만의 동작: 부모와 충돌 없음 + child.customDoSomething(); // "자식 클래스 동작" + + // 부모 동작도 그대로 안전히 사용 가능 + child.parentDoSomething(); // "기본 동작" + child.parentNewFunction(); // "기본 동작" + } +} +``` + +- Child가 Parent를 상속하지 않고 내부에 `private final Parent parent` 인스턴스로 두고, 클래스 메서드에서는 parent.doSomething()으로 호출한다 +- 이러면 Parent의 변경이 있어도 충돌이 없다 +- Child가 customDoSomething()으로 자기만의 기능을 안전하게 추가ㅎ하여 부모 기능과 독립적일 수 있다 +- API를 노출할지 말지를 Child가 직접 통제하니 안정적이다 + +### 그럼 언제 상속을 써도 되나? + +- 상속하려는 클래스가 같은 패키지 내부에서 관리되는 경우 +- 상속하려는 클래스가 상속을 고려해서 설계/문서화 되어있는 경우 \ No newline at end of file diff --git a/peng255/src/main/java/item19.md b/peng255/src/main/java/item19.md new file mode 100644 index 0000000..6ba4e96 --- /dev/null +++ b/peng255/src/main/java/item19.md @@ -0,0 +1,81 @@ +## 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라 + +### 요점 정리 + +- item 18에서 봤듯이 상속은 부모클래스 구현에 종속돼서 깨지기 쉽다 +- 그러니 상속을 안전하게 허용하려면 처음부터!! 확장까지 고려해서 설계하고 문서화하는 것이 필요하다 +- 상속을 허용하려면 + - `@implSpec`으로 self-use 상세히 문서화하기 + - 생성자에서 오버라이드 가능한 메서드 호출 절대 금지 +- 상속을 막으려면 + - 클래스에 final 붙이기 + - private 생성자 + static factory로 설계 + +### 상속을 고려해서 설계하는 게 어떻게 하는 건데? + +**1. self-use를 문서화하기** + + 이 메서드가 내부적으로 어떤 overridable 메서드를 언제 호출하는지를 Javadoc에 명시해야 한다. + + ex. `remove()` → 내부적으로 iterator().remove()를 사용합니다 + +**2. 생성자에서 오버라이드 가능 메서드를 호출하지 말기!!** + + ```java + class Super { + public Super() { + overrideMe(); // ❌ 위험 + } + public void overrideMe() {} + } + + class Sub extends Super { + private final Instant instant; + Sub() { instant = Instant.now(); } + @Override public void overrideMe() { + System.out.println(instant); // instant는 아직 초기화 전 + } + } + + public static void main(String[] args) { + new Sub(); // null 출력 -> 깨짐 + } + ``` + + 위 예시에서 Super 클래스의 생성자가 overrideMe()라는 overridable 메서드를 호출하고 있다. overrideMe() 메서드는 Sub 클래스에서 `@Override` 되고 있다 + + - 생성자는 자식의 초기화보다 먼저 실행된다! 즉 자식의 필드는 초기화되지 않은 시점이다. + + → 생성자에서는 private, final, static 메서드만 호출해야 안전하다 + +- 어떤 메서드를 protected로 제공해야 할지 신중히 선택 + - 너무 많이 열면: 내부 구현을 외부에 드러내는 것 + - 너무 적게 열면: 자식 클래스가 제대로 동작하기 힘듦 + + → 항상 직접 서브클래스를 작성해 테스트해야 적절한 공개 수준 확인 가능 + + +### 상속을 막는 법과 언제 막아야 하는지 + +**1. final 선언하기** + + ```java + public final class Complex { ... } + ``` + +**2. 모든 생성자를 private 또는 package-private 처리하고 static factory를 제공하기** + + ```java + public class Complex { + private Complex(...) { ... } + public static Complex valueOf(...) { ... } + } + ``` + + +**- 언제 상속을 금지해야 하냐?** + - 불변 클래스 (예: String, BigInteger…) → 반드시 금지해야 한다 + - 일반 구체 클래스는 대부분 상속 필요없으니 `final`을 붙이자 + - 인터페이스 기반 컴포넌트 (Set, List, Map 등) + - 상속 금지해도 문제없음 + - 기능 확장을 하고 싶다면 wrapper, Decorator등의 컴포지션으로 대체하자 \ No newline at end of file From f4e3e8c07466f94be3b67b110aa352559519cc45 Mon Sep 17 00:00:00 2001 From: Seoyoon Tak Date: Sun, 31 Aug 2025 01:27:47 +0900 Subject: [PATCH 08/14] =?UTF-8?q?20~25=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- peng255/src/main/java/item20.md | 55 +++++++++++++++ peng255/src/main/java/item21.md | 38 +++++++++++ peng255/src/main/java/item22.md | 57 ++++++++++++++++ peng255/src/main/java/item23.md | 114 ++++++++++++++++++++++++++++++++ peng255/src/main/java/item24.md | 83 +++++++++++++++++++++++ peng255/src/main/java/item25.md | 74 +++++++++++++++++++++ 6 files changed, 421 insertions(+) create mode 100644 peng255/src/main/java/item20.md create mode 100644 peng255/src/main/java/item21.md create mode 100644 peng255/src/main/java/item22.md create mode 100644 peng255/src/main/java/item23.md create mode 100644 peng255/src/main/java/item24.md create mode 100644 peng255/src/main/java/item25.md diff --git a/peng255/src/main/java/item20.md b/peng255/src/main/java/item20.md new file mode 100644 index 0000000..2185768 --- /dev/null +++ b/peng255/src/main/java/item20.md @@ -0,0 +1,55 @@ +## 20. 추상 클래스보다는 인터페이스를 우선하라 + +### 요점 정리 + +- 다형성을 구현할 때의 유연성, 안정성 : 인터페이스 > 추상 클래스 +
→ 왜?? 인터페이스는 다중구현 (다중 상속), 유연한 구조 설계, 기존 클래스 확장성 등 장점이 있기 때문!! +- 새로운 타입/역할/기능은 반드시 인터페이스로 구현하는 걸 우선한다 +- 표준 인터페이스로 설계했다면 구현 보조를 위해 추상 클래스를 함께 제공하자 + +### 왜?? 인터페이스를 우선해야 되냐 + +1. **다중 상속이 가능하다** + + 클래스는 딱 하나의 클래스만 상속할 수 있다. + + 반면 인터페이스는 여러 개를 동시에 구현 (implement)할 수 있다. 그러니 기존 타입 위에 Comparable, Serializable 등 새로운 개념을 덧붙일 때 매우 쉽다 +

+2. **기존 클래스에 타입을 추가할 수 있다** + + 이미 존재하는 클래스를 수정하는 일 없이 새로운 인터페이스로 확장이 가능하다! 여러 개의 인터페이스를 구현할 수 있기 때문. + + 예를 들어, JDK 8 이후에 수많은 기존 라이브러리 클래스들이 `Comparable`, `Iterable` 등 implements 형태로 보강됐다! +

+3. **여러 특성을 조합할 때 인터페이스로 깔끔하게 표현 가능하다** + + ```java + public interface Singer { void sing(); } + public interface Songwriter { void compose(); } + public interface SingerSongwriter extends Singer, Songwriter { void perform(); } + ``` + + 클래스로만 설계하면 조합 수마다 별도의 클래스가 필요하다..(조합 폭발) + + +### 구현 예시 + +인터페이스로 “타입”을 정의하고.. 필요하다면 기본 구현 (목업)은 “추상 클래스”로 보조하는 느낌으로 뼈대를 잡아도 된다. + +```java +public interface SimpleMap { + int size(); + V get(K k); + // ... +} + +public abstract class AbstractSimpleMap implements SimpleMap { + // 기본 구현을 이곳에 모음 +} +``` + +### 추상 클래스의 한계점은? + +- 다중 상속이 불가능하다 +- 이미 클래스 계층 구조가 짜여 있다면 기존 코드에 추상 클래스를 추가하거나 적용이 불가하다 +- 클라이언트가 기존 타입과 혼용/확장이 굉장히 어렵다 \ No newline at end of file diff --git a/peng255/src/main/java/item21.md b/peng255/src/main/java/item21.md new file mode 100644 index 0000000..b7a525d --- /dev/null +++ b/peng255/src/main/java/item21.md @@ -0,0 +1,38 @@ +## 21. 인터페이스는 구현하는 쪽을 생각해 설계하라 +### 요점 정리 + +- 한 번 인터페이스가 공개되면 수정이 매우 어렵다! +- 인터페이스를 설계할 때는 장기정 호환성, 다양한 구현을 반드시 고려해서 신중하게 “최소, 필수적인 메서드만” 설계해야 한다 +- default method로 보완해도 기존 그현체가 의도와 다르게 동작할 수 있다 + +### 인터페이스 설계시 생길 수 있는 위험 + +- 기존에는 인터페이스에 메서드를 하나 추가하면 모든 구현체가 컴파일 에러 +- Java 8부터는`default`메서드로 새 기능을 추가할 수 있지만, + - 예전 구현체들이 이 동작을 **원하지 않더라도 무조건 주입**됨 + - 예제:`Collection.removeIf`는 기본적으로 iterator 기반 구현을 넣었지만, + + 동기화 컬렉션(synchronized collection)에선 lock 없이 removeIf가 작동 → **동시성 버그 발생** + + ```java + // Java 8 Collection 인터페이스에 추가된 default 메서드 + default boolean removeIf(Predicate filter) { + Objects.requireNonNull(filter); + boolean removed = false; + for (Iterator it = iterator(); it.hasNext(); ) { + if (filter.test(it.next())) { + it.remove(); + removed = true; + } + } + return removed; + } + ``` + + +### 좋은 인터페이스 설계를 위해 실무에서 지킬 것 + +- 미리 여러 가지(최소 3개 이상)의 구현체와 다양한 클라이언트 코드로 테스트 +- 필수적이고, 다양한 구현체에서 문제 없이 쓸 수 있는 "최소 기능"만 명확하게 정의 +- 되도록이면 불변식, 동기화, 실패 처리 등이 구현마다 달라질 수 있다는 점을 고려 +- 변동 가능성이 있는 기능은 처음부터 빼거나, interface 추가로 확장 \ No newline at end of file diff --git a/peng255/src/main/java/item22.md b/peng255/src/main/java/item22.md new file mode 100644 index 0000000..04bb975 --- /dev/null +++ b/peng255/src/main/java/item22.md @@ -0,0 +1,57 @@ +## 22. 인터페이스는 타입을 정의하는 용도로만 사용하라 + +### 요점 정리 + +- 인터페이스는 “이 객체가 무엇을 할 수 있는지” 타입으로서 역할을 정의하는 용도여야 한다! +- 인터페이스를 만들 때 상수를 모아 두기만을 위한 목적으로 만들면 안된다 +- 상수를 담기 위해서 유틸리티 클래스나 enum을 이용하자 + +### 잘못된 상수 인터페이스(Constant Interface) 패턴 + +```java +// ❌ 상수를 위해 만든 인터페이스 (지양) +public interface PhysicalConstants { + static final double AVOGADROS_NUMBER = 6.022_140_857e23; + static final double BOLTZMANN_CONST = 1.380_648_52e-23; + static final double ELECTRON_MASS = 9.109_383_56e-31; +} +``` + +- **왜 이렇게 쓰면 안됨?** + + 이 인터페이스를 구현하면 하위 클래스까지 모든 상수 이름이 scope에 포함되는 일이 생긴다 + + "이 타입을 구현한다"는 인터페이스의 의미를 왜곡한다 + + 내부 구현 디테일이 외부 공개 API로 ‘새나감’ (정보은닉 원칙 위반) + + 상수가 필요 없어져도 호환성 때문에 구현을 유지해야 한다 +

+- **그러면 상수를 어떻게 공개해야함?** + - 관련 클래스나 Enum에 넣거나.. 별도의 static 유틸리티 클래스에서 관리할 수 있다. + - **utility 클래스 예시** + + ```java + public class PhysicalConstants { + private PhysicalConstants() {} // 인스턴스화 방지 + public static final double AVOGADROS_NUMBER = 6.022_140_857e23; + public static final double BOLTZMANN_CONST = 1.380_648_52e-23; + public static final double ELECTRON_MASS = 9.109_383_56e-31; + } + ``` + + ```java + 사용할 때는 이렇게 사용한다 + + import static com.example.PhysicalConstants.*; + + public double atoms(double mols) { + return AVOGADROS_NUMBER * mols; + } + ``` + + static import를 하면 클래스 이름이 없어도 상수를 쓸 수 있다! + + - **그러면 Enum을 쓰는 경우는?** + + 상수들 각각이 의미 있는 값이면 enum을 쓰는 게 더 적절하다. (ex. 단위, 요일, 방향 등) \ No newline at end of file diff --git a/peng255/src/main/java/item23.md b/peng255/src/main/java/item23.md new file mode 100644 index 0000000..b9c262e --- /dev/null +++ b/peng255/src/main/java/item23.md @@ -0,0 +1,114 @@ +## 23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라 + +### 요점 정리 + +- “태그 필드(tag field)”로 여러 타입/상태를 한 클래스에 몰아넣지 말자!! + + → 서브클래스 계층 구조로 분리하는 게 더 깔끔하고 안전하다. + +- 클래스 인스턴스에 “종류를 구분하는 tag 필드”가 있으면 계층 구조로 바꿔라 + + 계층 구조는 가독성, 안전성, 유지보수, 확장성 면에서 더 좋다 + +- 타입에 따라 동작(메서드)이나 값이 다르면, 추상 타입 + 서브클래스 구조를 적극 활용한다 + +### 태그 달린 클래스가 뭔데?? + +```java +// 학생(Student) 객체가 "학생" 또는 "선생님" 역할이 둘 다 가능한 구조 (태그 필드로 구분) +class Person { + enum Type { STUDENT, TEACHER } + final Type type; + int studentId; // 학생일 때만 사용 + String subject; // 선생님일 때만 사용 + + // 학생 생성자 + Person(int studentId) { + type = Type.STUDENT; + this.studentId = studentId; + } + // 선생님 생성자 + Person(String subject) { + type = Type.TEACHER; + this.subject = subject; + } + + void printRole() { + switch (type) { + case STUDENT: + System.out.println("나는 학생! 학번: " + studentId); + break; + case TEACHER: + System.out.println("나는 선생님! 과목: " + subject); + break; + } + } +} +``` + +→ 위 예시를 보면 Person이라는 한 클래스가 학생이기도 하고, 선생님이기도 한 두가지 역할을 갖고 있다 + +type이라는 **태그 필드**로 현태 인스턴스가 **학생인지, 선생님인지 구분**한다. + +여기서 문제점! + +학생이든 선생님이든 각자 필요 없는 필드를 가지고 있고, 분기마다 switch(type)이나 if문으로 구분해야 한다. +
그리고 종류가 추가되면 모든 곳에서 switch/case문을 일일이 수정해야한다.. +
타입의 안정성을 컴파일러가 전혀 보장하지 않는다는 문제도 있다. + +이걸 계층 구조로 변경하자! + +### 태그 달린 클래스 → 계층구조로 바꿔보면? + +```java +// 1) 추상 클래스: 공통 타입 및 추상 동작 정의 +abstract class Person { + abstract void printRole(); +} + +// 2) 학생 클래스: 학생 역할 구현 +class Student extends Person { + private final int studentId; + + Student(int studentId) { + this.studentId = studentId; + } + + @Override + void printRole() { + System.out.println("나는 학생! 학번: " + studentId); + } +} + +// 3) 선생님 클래스: 선생님 역할 구현 +class Teacher extends Person { + private final String subject; + + Teacher(String subject) { + this.subject = subject; + } + + @Override + void printRole() { + System.out.println("나는 선생님! 과목: " + subject); + } +} +``` + +```java +사용 예시 +public class Main { + public static void main(String[] args) { + Person p1 = new Student(12345); + Person p2 = new Teacher("수학"); + + p1.printRole(); // 나는 학생! 학번: 12345 + p2.printRole(); // 나는 선생님! 과목: 수학 + } +} +``` + +- 태그 값(`type`) 대신, 역할별로 서브클래스를 따로 만들어 역할별 필드와 동작을 구분한다 +- switch문, 태그 필드 불필요 → 가독성, 유지보수성, 안전성 대폭 향상 +- 각 인스턴스는 정확한 데이터만 갖고 있어 메모리 낭비가 없다 +- 타입이 곧 역할/행위 의미 → 클라이언트 코드를 더욱 명확하고 엄격하게 만든다 \ No newline at end of file diff --git a/peng255/src/main/java/item24.md b/peng255/src/main/java/item24.md new file mode 100644 index 0000000..2247ee0 --- /dev/null +++ b/peng255/src/main/java/item24.md @@ -0,0 +1,83 @@ +## 24. 멤버 클래스는 되도록 static으로 만들라 + +### 요점 정리 + +- 가능한 한 멤버 클래스는 `static`으로 만든다. +- **외부 인스턴스 참조가 꼭 필요한 경우**에만 inner class로 한다. +- static 멤버 클래스는 외부 클래스에 종속적인 독립 클래스로 생각하면 된다. + +### 멤버 클래스가 뭔데? + +- 멤버 클래스 : 다른 클래스 내부에 정의된 클래스! +- 크게 두 가지가 있다 + 1. `static` 멤버 클래스 (static nested class) + 2. `non static` 멤버 클래스 (inner class) + +### 왜 static 멤버 클래스를 선호해야 하는데? + +- **외부 인스턴스 참조 부재로 인한 공간 및 시간 절감** + + inner 클래스는 내부적으로 **외부 클래스의 인스턴스를 참조하는 필드**를 가진다. 이 참조 때문에 생성 비용, 메모리 차지가 증가한다.. + + 외부 객체가 GC되더라도 inner 클래스 인스턴스 때문에 메모리 누수가 발생할 가능성이 있다 + +- **불필요한 종속성 제거** + + static 멤버 클래스는 외부 클래스 인스턴스와 독립적이어서 + + 분리와 재사용이 용이하고 명확한 역할 구분에 도움이 된다. + +- **API 설계 명확화 및 유지보수성 증가** + + 외부 객체 참조를 명시적으로 하지 않아도 된다 → 코드 이해도가 높아진다. + + +### non static 멤버 클래스 (inner class) 예시 + +```java +public class Outer { + private int outerValue = 10; + + class Inner { + void print() { + System.out.println("Outer value: " + outerValue); + } + } + + public Inner createInner() { + return new Inner(); + } +} +``` + +- Inner는 Outer 객체에 암묵적 연결 +- Inner 인스턴스는 외부 Outer 인스턴스를 반드시 참조해야 생성 가능 +- 메모리와 성능 부담, 불필요한 강한 결합 + +### static 멤버 클래스 예시 + +```java +public class Outer { + private static int staticValue = 20; + + static class Nested { + void print() { + System.out.println("Static value: " + staticValue); + } + } + + public static Nested createNested() { + return new Nested(); + } +} +``` + +- `Nested`는 외부 인스턴스가 필요하지 않다 +- 무거운 외부 참조 없고 가볍고 독립적 + +### 어떨 때 뭘 써야하나? + +해당 멤버 클래스가 외부 클래스 객체 상태에 의존하는가? + + 예 → non-static 멤버 클래스(inner class) + 아니오→ static 멤버 클래스 \ No newline at end of file diff --git a/peng255/src/main/java/item25.md b/peng255/src/main/java/item25.md new file mode 100644 index 0000000..8bc47b4 --- /dev/null +++ b/peng255/src/main/java/item25.md @@ -0,0 +1,74 @@ +## 25. 톱레벨 클래스는 한 파일에 하나만 담으라 + +### 요점 정리 + +- 자바 컴파일러는 한 소스 파일에 여러 톱레벨 클래스를 정의하는 걸 허용하지만… + + 이렇게 하면 여러 정의가 중복될 수 있고, 컴파일 순서에 따라 프로그램 동작이 달라지는 치명적 문제가 발생한다. + + → 따라서 톱레벨 클래스(또는 인터페이스)는 반드시 한 파일에 하나씩 정의해야 한다. + + +### 문제 상황 코드 예시 + +```java +// Main.java +public class Main { + public static void main(String[] args) { + System.out.println(Utensil.NAME + Dessert.NAME); + } +} +``` + +```java +// Utensil.java (한 파일에 Utensil과 Dessert를 같이 정의한 경우 - 나쁜 예) +class Utensil { + static final String NAME = "pan"; +} + +class Dessert { + static final String NAME = "cake"; +} +``` + +```java +// Dessert.java (다른 파일에 동일한 두 클래스를 중복 정의한 경우) +class Utensil { + static final String NAME = "pot"; +} + +class Dessert { + static final String NAME = "pie"; +} +``` + +이러면… + +- 컴파일 순서에 따라 프로그램 결과가 달라지고, 컴파일 에러가 날 수도 있다. +- 예)`javac Main.java Dessert.java` → 컴파일 에러(중복 정의) +- 예)`javac Dessert.java Main.java`→ `"potpie"` 출력 → 의도와 전혀 다름 +- 즉, 컴파일러가 어느 파일에서 클래스를 찾느냐에 따라 동작이 달라지는 매우 위험한 상황 + +### 해결법: + +- 각 톱레벨 클래스는 **각자 별도의 소스 파일에 분리**한다. +- 예를 들어,`Utensil.java`는 Utensil만, `Dessert.java`는 Dessert만 포함한다. + +- 여러 톱레벨 클래스를 한 파일에 넣고 싶다면 static 멤버 클래스로 바꾸자… (item 24 참고) + + ```java + // Test.java + public class Test { + public static void main(String[] args) { + System.out.println(Utensil.NAME + Dessert.NAME); + } + + private static class Utensil { + static final String NAME = "pan"; + } + + private static class Dessert { + static final String NAME = "cake"; + } + } + ``` \ No newline at end of file From e5f2d393df28d441fa4b75c9cdbfd7748af934b5 Mon Sep 17 00:00:00 2001 From: Seoyoon Tak Date: Sun, 7 Sep 2025 23:19:45 +0900 Subject: [PATCH 09/14] =?UTF-8?q?26~27=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- peng255/src/main/java/item25.md | 4 ++- peng255/src/main/java/item26.md | 47 ++++++++++++++++++++++++ peng255/src/main/java/item27.md | 63 +++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 peng255/src/main/java/item26.md create mode 100644 peng255/src/main/java/item27.md diff --git a/peng255/src/main/java/item25.md b/peng255/src/main/java/item25.md index 8bc47b4..a819d72 100644 --- a/peng255/src/main/java/item25.md +++ b/peng255/src/main/java/item25.md @@ -71,4 +71,6 @@ class Dessert { static final String NAME = "cake"; } } - ``` \ No newline at end of file + ``` + + diff --git a/peng255/src/main/java/item26.md b/peng255/src/main/java/item26.md new file mode 100644 index 0000000..17d7a12 --- /dev/null +++ b/peng255/src/main/java/item26.md @@ -0,0 +1,47 @@ +## 26. raw 타입은 사용하지 말라 + +### 요약 정리 + +- raw type : 제네릭 클래스나 인터페이스를 타입 파라미터 없이 사용하는 것! + - 예: `List list = new ArrayList();` (`List list`과 다르다) +- **왜 쓰지 말아야 하나?** + - 타입 안정성 사라짐 → 아무 타입이나 넣을 수 있음 + + → 컴파일러가 타입오류를 잡아줄 수 없음 + + → 잘못된 타입이 들어가면 나중에 꺼내서 casting 할 때 런타임 에러가 발생한다.. + +- **Generic을 써야 하는 이유는?** + - 컴파일 시점에 타입체크 → 버그 미리 방지 가능 + - 명확하게 어떤 타입을 담는지 코드를 문서화할 수 있다 + +- **예외적으로 raw type을 써도 되는 때가 있다** + - 클래스 literal 같은 거! (ex. `List.class`) + - `if (o instanceof List)` 이런 식으로 검사할 떄 + - 하지만 이런 경우 외에는 항상 타입 파라미터를 명시하자 + +### 잘못된 예시 (raw type 사용) + +```java +List list = new ArrayList(); // 로 타입 사용 +list.add("hello"); +list.add(42); + +// 꺼낼 때 뭔지 알 수 없어서 캐스팅 필요 +String str = (String) list.get(0); // 괜찮다 +String str2 = (String) list.get(1); // 런타임에 ClassCastException 발생! +``` + +- **문제점**: 컴파일은 통과하지만 실행시 예외 터짐, 코드의 의미도 불명확 + +### 올바른 예시 (Generic 사용) + +```java +List list = new ArrayList<>(); // 타입 명시 +list.add("hello"); +// list.add(42); // 컴파일 에러 → 미리 방지 + +String str = list.get(0); // 안전, 캐스팅 불필요 +``` + +- **장점**: 컴파일러가 타입을 체크해주고, 잘못된 사용을 사전에 막아줌 \ No newline at end of file diff --git a/peng255/src/main/java/item27.md b/peng255/src/main/java/item27.md new file mode 100644 index 0000000..3c7c35f --- /dev/null +++ b/peng255/src/main/java/item27.md @@ -0,0 +1,63 @@ +## 27. 비검사(unchecked) 경고를 제거하라 + +### 요점 정리 + +- Generic 코드를 작성하다 보면 unchecked 경고가 발생하는 일 일어남! + + → 언제 이런게 일어나냐? + + 1. 타입 파라미터를 명확히 쓰지 않았을 때 (`List list = new ArrayList();` 등) + 2. 실제 타입이 뭔지 알 수 없는데 강제 형변환(casting) 시킬 때 + 3. 불가피하게 제네릭 배열을 만들어야 할 때 (예: `List[] arr = new ArrayList;`) + +- 그럼 어떻게 경고를 없앰? + - 대부분… 제네릭 타입을 명확히 지정하면 없어짐 + - 논리적으로 완전히 타입 안전이 보장되는 상황이고 꼭 제네릭을 유지해야 하면 `@SuppressWarnings("unchecked")` 어노테이션을 쓰자. + + → 경고를 최소 범위(지역 변수·짧은 메서드)로만 억제하기 + + 클래스 전체나 긴 메서드에 붙이지 말고!! **꼭 필요한 좁은 범위**에만 붙이고 반드시 주석도 남기자 + + + +### 코드 예시 + +- 경고 없이 안전한 코드 예시 + +```java +List list = new ArrayList<>(); +list.add("hello"); + +// 안전: 컴파일러가 타입체크, 강제 변환 불필요 +String str = list.get(0); +``` + +- 이런 코드는 unchecked 경고가 발생한다 + +```java +Object obj = "hello"; +List list = new ArrayList<>(); +list.add((String)obj); // 안전하긴 하지만, 컴파일러는 타입 확신X +``` + +```java + +// 아래처럼 복잡한 상황일 때 주로 경고 발생 +List unknownList = new ArrayList(); + +@SuppressWarnings("unchecked") // result가 실제로 T[]임을 개발자가 100% 확신하는 경우만 허용 + T[] toArray(List list, T[] a) { + + // 실제로는 타입이 항상 맞음 (T[]로 복사), 하지만 컴파일러가 알 방법이 없음 + return (T[]) list.toArray(a); +} +``` + +- 이렇게는 절대 하지 말자!!! 전체 클래스에 Suppress 어노테이션 xx + +```java +// 무턱대고 전체 클래스/@SuppressWarnings("unchecked") 쓰기 +@SuppressWarnings("unchecked") +public class Danger { ... } +// -> 이렇게 하면 진짜 오류가 섞여도 경고를 못 보고 놓침! +``` \ No newline at end of file From ed1b44ad27c4cc52e1722bb70d3b899318ed1d62 Mon Sep 17 00:00:00 2001 From: Seoyoon Tak Date: Sat, 13 Sep 2025 23:53:55 +0900 Subject: [PATCH 10/14] =?UTF-8?q?28~29=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- peng255/src/main/java/item28.md | 65 +++++++++++++++++++++++++++++++++ peng255/src/main/java/item29.md | 62 +++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 peng255/src/main/java/item28.md create mode 100644 peng255/src/main/java/item29.md diff --git a/peng255/src/main/java/item28.md b/peng255/src/main/java/item28.md new file mode 100644 index 0000000..1aca77a --- /dev/null +++ b/peng255/src/main/java/item28.md @@ -0,0 +1,65 @@ +## 28. 배열보다는 리스트를 사용하라 +### 요약 정리 + +- **배열:** "Animal[]에 Dog[] 넣기 가능 → 위험, 타입 정보가 살아 있으니 잘못 넣으면 실행중 터진다." + + 런타임에서야 잘못된 타입 넣으면 에러, 실전에서 위험! + +- **리스트:** "List와 List은 아예 별개 → 컴파일 단계에서 오류, 실행중 타입 정보는 없다." + + 타입체크를 강하게 하니까 애초에 코딩 단계에서 막아준다. 훨씬 안전! + + +- 배열과 리스트를 혼용하지 말고 리스트를 우선 사용하자 +- 리스트를 쓰면 타입 에러를 컴파일 단계에서 막을 수 있다 +- 배열이 꼭 필요한 특수 상황(성능상 인덱스 접근 / 최저단 자료구조 등) 외에는 List<>가 디폴트 + +### 1. 배열은 공변(covariant)이다 + +공변성은 **타입 상속 관계가 배열에도 같이 적용된다**는 뜻이다!! + +- 예시: + + ```java + class Animal {} + class Dog extends Animal {} + + Dog[] dogs = new Dog[2]; + Animal[] animals = dogs; // 배열은 공변성이라 업캐스팅 가능 + ``` + +- 단점: 상위 타입 배열(Animal[])에 하위 타입 배열(Dog[])을 넣었을 때 + + ```java + animals[0] = new Animal(); // 컴파일 성공, 런타임 ArrayStoreException 발생! + ``` + + 실제 내부는 Dog[]인데 Animal을 넣으니 타입이 깨져서 런타임 에러가 발생한다. + + +### 2. 배열은 실체화(reified)된다 + +**런타임에도 배열 타입 정보(예: Dog[], Animal[], String[])가 남아있다.** + +- 타입을 속여서 집어넣으면 실행중 에러가 난다. +- 타입 정보가 직접 남아 있음. + +--- + +### 3. 제네릭 리스트(List)는 불공변(invariant)이고 소거(erased)된다 + +**불공변**이란, 타입 간 상속 관계가 리스트에는 적용되지 않는다는 것. + +- 예시: + + ```java + List dogList = new ArrayList<>(); + List animalList = dogList; // 컴파일 에러! 불공변 + ``` + +- 서로 완전히 다른 타입으로 취급함. + +**소거**란, 컴파일 시에는 List처럼 타입을 알고 있지만 + +- 실제 실행(runtime)에서는 타입 정보가 사라져 그냥 List로 동작한다. +- 강제로(잘못해서) 이상한 타입을 넣으면 컴파일에서 잡아주고 런타임에서는 체크를 안 한다. \ No newline at end of file diff --git a/peng255/src/main/java/item29.md b/peng255/src/main/java/item29.md new file mode 100644 index 0000000..ff2ccf6 --- /dev/null +++ b/peng255/src/main/java/item29.md @@ -0,0 +1,62 @@ +## 29. 이왕이면 제네릭 타입으로 만들라 +### 요약 정리 + +- 제네릭이 아닌 타입 (Object 기반 타입)은 안전성 문제 // 매번 캐스팅해야 한다는 문제 / 런타임오류 발생가능 등의 문제가 있다 +- 제네릭 타입으로 만들면 + + 컴파일 시점에 타입을 보장할 수 있고 + + 불필요한 캐스팅이 사라지며 + + 코드 가독성과 재사용성이 높아진다. + +- 이미 Object 기반 타입이 있다면, 소스 호환성도 거의 깨지지 않으니 기존 타입도 언제든 제네릭으로 변경하는 것이 좋다. + +### Object (제네릭 x) 로 만든 예시 + +```java +public class Stack { + private Object[] elements; + private int size = 0; + + public void push(Object e) { elements[size++] = e; } + public Object pop() { return elements[--size]; } +} + +// 사용 예 +Stack s = new Stack(); +s.push("hello"); +s.push(42); + +String str = (String) s.pop(); // 매번 캐스팅 필요 +``` + +예시처럼 String, Integer 등이 들어갈 수 있는데… + +혹시나 잘못된 캐스팅이 수행되면 런타임에 ClassCastException이 발생할 수 있다. + +→ 되도록 쓰지 말자… + +### 제네릭 타입으로 개선한 예시 + +```java +public class Stack { + private Object[] elements; + private int size = 0; + + public void push(E e) { elements[size++] = e; } + @SuppressWarnings("unchecked") + public E pop() { return (E) elements[--size]; } +} + +// 사용 예 +Stack s = new Stack<>(); +s.push("hello"); +// s.push(42); // 컴파일 에러! + +String str = s.pop(); // 캐스팅 필요 없음, 안전함 +``` + +Stack 객체를 만드는 시점에 타입을 하나로 고정하기 때문에 의도한 타입이 아닌 다른 타입이 들어갈 걱정 xㅌ + +하나의 타입으로 통일 → 값을 꺼낼때 처음부터 (E)로 캐스팅해서 돌려줄 수 있다 → 런타임에러 x \ No newline at end of file From 2631b36041837ac9ce4ab092e69e5fee0e1c1adf Mon Sep 17 00:00:00 2001 From: Seoyoon Tak Date: Thu, 18 Sep 2025 22:55:53 +0900 Subject: [PATCH 11/14] =?UTF-8?q?30,31=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- peng255/src/main/java/item30.md | 65 +++++++++++++++++++++++++++++++++ peng255/src/main/java/item31.md | 65 +++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 peng255/src/main/java/item30.md create mode 100644 peng255/src/main/java/item31.md diff --git a/peng255/src/main/java/item30.md b/peng255/src/main/java/item30.md new file mode 100644 index 0000000..f6462d8 --- /dev/null +++ b/peng255/src/main/java/item30.md @@ -0,0 +1,65 @@ +## 30. 이왕이면 제네릭 메서드로 만들라 +### 요점 정리 + +- 클래스뿐만 아니라 메서드도 제네릭으로 선언하면 + - 코드 재사용성, 타입 안전성, 가독성, 유지보수성 ↑ + - 직접 쓸 때 타입 캐스팅이 필요 없다 + - 컴파일러가 타입체크도 해준다 + +### 제네릭 메서드가 뭔데? + +- 일반적인 메서드 : 타입별로 따로 만들거나… Object로 받아서 캐스팅하는 형식 +- 제네릭 메서드 : 타입을 parameter로 받는다! 그 후 컴파일러가 알아서 타입을 결정해주는 형식 + +### 일반메서드 vs 제네릭 메서드 + +제네릭을 쓰지 않고 Object로 받고 캐스팅한다면? + +```java +// 제네릭 없는 버전 (Object 상자) +class Box { + private Object item; + + public void set(Object obj) { item = obj; } + public Object get() { return item; } +} + +Box b = new Box(); // Box 생성 + +b.set("커피"); // String넣기 +String drink = (String) b.get(); // 매번 형변환 필요, 실수하면 에러! +``` + +Box에 Object로 String도 들어갈 수 있고 다른 타입 E도 들어갈 수 있는데.. + +Box1의 item에는 String이 들어가고 + +Box2의 item에는 다른 타입 E가 들어갔을 때 + +(String)Box2.item → 이렇게 불가능한 캐스팅을 했을 때 에러가 발생할 수 있다. + +근데 각 Box 객체에 어떤 타입이 들어있는지 모르니 에러가 발생하기 쉽다. + +그러니 제네릭 메서드로 구현하자 + +```java +class Box { + private T item; + + public void set(T obj) { item = obj; } + public T get() { return item; } +} + +Box b = new Box<>(); // 이 상자엔 String만! +b.set("커피"); // 넣기 +String drink = b.get(); // 바로 꺼내기 (캐스팅 필요 없음! 타입 안전) + +Box b2 = new Box<>(); +b2.set(123); // 정수만 반환된다 + +int num = b2.get(); // 캐스팅 없이 안전하게 사용 +``` + +이 예시에서는 Object가 아니라 제네릭 T로 값을 받고, get()에서 값을 돌려줄 때는 캐스팅 필요없이 그 타입 그대로 돌려준다. + +잘못된 타입을 넣으려고 하면 컴파일 에러로 미리 확인할 수 있다는 장점도 있다. \ No newline at end of file diff --git a/peng255/src/main/java/item31.md b/peng255/src/main/java/item31.md new file mode 100644 index 0000000..e770e4d --- /dev/null +++ b/peng255/src/main/java/item31.md @@ -0,0 +1,65 @@ +## 31. 한정적 와일드카드를 사용해 API 유연성을 높이라 + +### 요점 정리 + +- 제네릭 타입은 기본적으로 불공변(invariant)이어서 List와 List는 서로 호환되지 않는다 + - **불공변**이란, 타입 간 상속 관계가 리스트에는 적용되지 않는다는 것. +- 그런데 실전에서 Number의 모든 하위 타입을 받고 싶다면? + + 아니면 아무 타입의 Collection을 받아서 스택을 그 Collection에 pop하고 싶다면? + + → **와일드 카드 `?` 를 써보자** + + - 생산자(아무 타입을 리스트에 담음)에는 `? extends`를 사용 + - 소비자(아무 타입의 리스트를 받음)에는 `? super` 를 사용 +- 단순히 타입을 한번만 쓰는 메서드 : 와일드 카드 이용 +- 타입을 여러 번 쓰고, 타입끼리 맞아야 하는 메서드 : 제네릭 타입 파라미터로 + +※ 와일드카드는 기능을 쉽게 확장해주지만.. 리턴타입에 쓰면 복잡해지니 조심해서 쓰자 + +### 생산자 ? extends 예시 + +List → Stack에 넣기 + +List → Stack에 넣기 + +```java +Stack stack = new Stack<>(); +List ints = List.of(1,2,3); +List doubles = List.of(2.5, 3.5); + +// 받아서 스택에 올리는 함수 +public void pushAll(Iterable src) { + for(Number n : src) push(n); +} + +stack.pushAll(ints); // 정수도 넣을 수 있음 +stack.pushAll(doubles); // 실수도 넣을 수 있음 +``` + +`? extends Number` → Integer, Double, Long 등 모든 Number 하위 타입을 받을 수 있다 + +### 소비자 ? super 예시 + +Stack → List에 넣어주기 + +Stack → List에 넣어주기 + +```java +Stack stack = new Stack<>(); +stack.push(1); +stack.push(2); + +List objs = new ArrayList<>(); +List nums = new ArrayList<>(); + +// 스택에서 꺼내서 Collection에 추가 +public void popAll(Collection dst) { + while(!isEmpty()) dst.add(pop()); +} + +stack.popAll(objs); // Object도 받아줌 +stack.popAll(nums); // Number도 받아줌 +``` + +`? super Integer` → Number, Object 등 Integer의 상위타입을 받을 수 있다 \ No newline at end of file From b67b6c23e0910471baad9a9d15c30829ba5870aa Mon Sep 17 00:00:00 2001 From: peng255 <149145441+peng255@users.noreply.github.com> Date: Mon, 29 Sep 2025 10:57:43 +0900 Subject: [PATCH 12/14] Create item32.md --- peng255/src/main/java/item32.md | 66 +++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 peng255/src/main/java/item32.md diff --git a/peng255/src/main/java/item32.md b/peng255/src/main/java/item32.md new file mode 100644 index 0000000..7cf2db4 --- /dev/null +++ b/peng255/src/main/java/item32.md @@ -0,0 +1,66 @@ +### 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라 + +제네릭 메서드와 가변 인수(...)를 같이 쓰면 타입 안전성 문제가 생길 수 있다. +가변 인수를 쓰면 내부적으로는 배열이 만들어지는데, 이 배열이 제네릭 타입일 때 힙 오염(heap pollution) 위험이 생긴다. +잘못 만든 코드는 경고 없이도 런타임 에러(ClassCastException)를 낼 수 있다. +그래서 자바 라이브러리의 중요한 제네릭 가변인수 메서드는 내부적으로 정말 조심해서 안전하게 만들었고, 이걸 보장할 땐 @SafeVarargs 어노테이션을 붙인다. + +기본 원칙 +제네릭 가변인수 배열에 값을 새로 저장(덮어쓰기)하지 않는다. +이 배열의 참조를 외부(신뢰할 수 없는 코드)에 넘기지 않는다. +완전히 안전하지 않으면, 가변인수 대신 List 파라미터를 쓴다. + +실생활/쉬운 코드 예시 +1. 위험한 코드 +```java +// 제네릭 가변인수 메서드 - 아주 위험! +static void dangerous(List... stringLists) { + List intList = List.of(42); + Object[] arr = stringLists; + arr[0] = intList; // 엉뚱한 타입으로 덮어씀 (힙 오염) + String s = stringLists[0].get(0); // 런타임 실패! ClassCastException +} +``` +이런 오류는 컴파일러가 못 찾아주고, 실행 중에만 터진다. + +2. 안전한 코드 - @SafeVarargs 활용 +```java +@SafeVarargs +static List flatten(List... lists) { + List result = new ArrayList<>(); + for (List l : lists) result.addAll(l); + return result; +} +``` + +// 사용 예시 +`List all = flatten(List.of("a", "b"), List.of("c", "d"));` +// 안전하게 여러 리스트 합치기 +배열에 값을 새로 덮어쓰거나 밖에 노출하지 않으므로 안전 + +@SafeVarargs는 "내가 보장하니 경고 띄우지 마"라는 의미 + +3. 제네릭 + 가변인수 대신 List 파라미터를 쓸 수도 있음 +```java +static List flatten(List> lists) { + List result = new ArrayList<>(); + for (List l : lists) result.addAll(l); + return result; +} +``` + +// 사용 예시 +`List all = flatten(List.of(List.of("a", "b"), List.of("c")));` +// 조금 더 코드가 길고 복잡해도 안전성은 확실하게 보장 + +제네릭 가변인수(예: T... args)는 매우 신중하게 써야 하며, +정말 안전할 때만 @SafeVarargs를 붙여서 경고를 없애고, +안전하지 않으면 List 파라미터로 변경하는 게 좋다. + +안전 기준은 "배열 수정/외부 노출 없음" + +실전에서 자주 쓰는 라이브러리 예시(Arrays.asList, List.of, Collections.addAll)는 모두 이 원칙으로 만들어져 있음 + +- 제네릭과 가변인수를 함께 쓸 땐 무심코 만들지 말고, 안전성 원칙을 반드시 지켜야 한다! + +- 실전에서 flatten, merge 등 여러 값 합칠 때 주로 등장하는 패턴이니, 무의식적으로 ...쓸 때 한 번 더 의심하고 쓰면 된다. From a0c6658f72681630d458cc9b70ac04b468088a31 Mon Sep 17 00:00:00 2001 From: peng255 <149145441+peng255@users.noreply.github.com> Date: Mon, 29 Sep 2025 11:00:20 +0900 Subject: [PATCH 13/14] 33 --- peng255/src/main/java/item33.md | 77 +++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 peng255/src/main/java/item33.md diff --git a/peng255/src/main/java/item33.md b/peng255/src/main/java/item33.md new file mode 100644 index 0000000..f028464 --- /dev/null +++ b/peng255/src/main/java/item33.md @@ -0,0 +1,77 @@ +### 33. 타입 안전 이종 컨테이너를 고려하라 +타입 안전 이종 컨테이너란? + +여러 가지 서로 다른 타입의 값(예: 이름은 String, 나이는 Integer, 학생 객체 등등)을 하나의 컨테이너에 안전하게 저장하고 꺼낼 수 있는 방식 + +보통의 컬렉션은 한 컨테이너에 한 타입만 넣을 수 있는데, 이 패턴은 여러 타입을 안전하게 다룰 수 있다. + +1. 일반적인 컬렉션의 한계 +```java +Set names = ...; // String만! +Set ages = ...; // Integer만! + +// 이름과 나이 둘 다 담고 싶은데? 분리해서 관리해야 함 +하나의 컨테이너에서 다양한 타입 값을 다루고 싶을 때 불편 +``` +2. 타입 안전 이종 컨테이너 패턴(특정 키에 따라 타입이 연결됨) +```java +public class Favorites { + private Map, Object> favorites = new HashMap<>(); + + public void putFavorite(Class type, T instance) { + favorites.put(type, instance); + } + + public T getFavorite(Class type) { + return type.cast(favorites.get(type)); + } +} +``` +사용 예시 +```java +Favorites f = new Favorites(); +f.putFavorite(String.class, "커피"); +f.putFavorite(Integer.class, 300); +f.putFavorite(Class.class, Favorites.class); + +String favDrink = f.getFavorite(String.class); // 자동으로 String 반환 +int favNum = f.getFavorite(Integer.class); // 자동으로 int 반환 +Class favClass = f.getFavorite(Class.class); // 클래스 반환 + +System.out.println(favDrink); // "커피" +System.out.println(favNum); // 300 +System.out.println(favClass); // "Favorites" +``` +여러 타입을 한 컨테이너에 안전하게 담고, 꺼낼 땐 타입까지 맞게 반환받음 + +실수로 타입이 안 맞으면 컴파일러 경고+런타임 오류가 나니 안정적 + +3. 일상 예시 비유
+여러 타입의 "즐겨찾기"를 하나의 큰 박스에 넣는데, + +"이 이름의 favorite을 String으로 꺼내줘" + +"이 값의 favorite을 Integer로 꺼내줘" + +"이 객체의 favorite을 Class 객체로 꺼내줘" + +"키"는 보통 Class 객체(예: String.class, Integer.class)가 되고 + +값을 넣고 뺄 때 항상 타입까지 함께 전달 → 타입이 맞지 않으면 오류 + +4. 유익한 점
+기존 컬렉션처럼 여러 개의 Set, Map을 각각 만들 필요 없음 + +원하는 만큼 다양한 타입을 한 곳에 안전하게 다룰 수 있다 + +실전에서는 설정값, 즐겨찾기, DB row 등 여러 컬럼 값을 다룰 때, +혹은 애노테이션 모듈에서 타입별 처리할 때 매우 효과적 + +- 전체 요약 +컬렉션 타입(Set, Map 등)은 보통 한 가지 타입만 안전하게 다룸 + +타입 안전 이종 컨테이너는 "키"의 타입(Class)에 따라 여러 다른 타입의 값을 안전하게 저장/조회할 수 있게 해준다 + +구현은 Class를 키, Object를 값으로 두고, get할 때 타입을 안전하게 캐스팅함 + +일상적으로는 여러 종류 값을 한 곳에 안전하게 관리하고 싶을 때 가장 잘 쓰는 패턴 From aa0e1dbc771c84633e7bea484bc7100338d30bbd Mon Sep 17 00:00:00 2001 From: Seoyoon Tak Date: Sun, 19 Oct 2025 23:44:20 +0900 Subject: [PATCH 14/14] item34 --- peng255/src/main/java/item34.md | 118 ++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 peng255/src/main/java/item34.md diff --git a/peng255/src/main/java/item34.md b/peng255/src/main/java/item34.md new file mode 100644 index 0000000..7fe8e7e --- /dev/null +++ b/peng255/src/main/java/item34.md @@ -0,0 +1,118 @@ +## 정수 열거 패턴의 단점 + +과거에는 **int enum pattern** 이라 해서 **public static final int APPLE_FUJI = 0;** 같은 상수를 사용했다. 이 방식은 여러 단점이 있다. + +1. 타입 안전성이 없다. + + 서로 다른 그룹의 상수(예: **APPLE_FUJI** , **ORANGE_NAVEL** )를 비교하거나 잘못된 값을 전달해도 컴파일 오류가 발생하지 않는다. + +2. 의미 없는 숫자 출력 + + 디버깅하거나 출력할 때 숫자(예: 0, 1)만 보여서 어떤 상수인지 식별하기 어렵다. + +3. 순회 불가 및 하드코딩 문제 + + **values()** 같은 기능이 없으므로 상수 그룹 전체를 순회하거나 크기를 알기 어렵다. + +4. 클라이언트 코드 재컴파일 문제 + + 상수의 값이 바뀌면 해당 값을 사용하는 모든 코드를 다시 컴파일해야 하며, 그렇지 않으면 잘못된 동작이 발생한다. + +5. 문자열 상수 패턴은 더 나쁨 + + 오타를 컴파일 타임에 잡을 수 없고 비교 비용도 크다. + + +--- + +## Enum 타입의 특징 + +자바의 **enum** 은 단순한 상수가 아니라 **클래스**다. + +각 열거 상수는 단 하나의 인스턴스로 존재하며, **public static final** 필드로 노출된다. + +사용자가 직접 생성할 수 없으므로 사실상 **Singleton** 패턴의 집합과 같다. + +- 서로 다른 enum 간 비교 불가 → 타입 안전 보장 + + 예: **Apple.FUJI == Orange.NAVEL** 은 컴파일 오류. + +- 각 enum은 고유한 네임스페이스를 가진다. + + 같은 이름의 상수도 서로 다른 enum 안에서는 충돌하지 않는다. + +- 상수 순서나 개수를 변경해도 클라이언트 재컴파일 불필요. + +--- + +## 데이터와 메서드를 가진 Enum + +**enum** 은 필드, 생성자, 메서드를 가질 수 있다. + +예를 들어 행성 정보를 나타내는 **Planet** 열거형은 질량(mass), 반지름(radius)을 속성으로 가지고, 이 두 값을 이용해 표면중력(surfaceGravity)을 계산할 수 있다. + +```java +public enum Planet { + EARTH(5.975e+24, 6.378e6); + private final double mass; + private final double radius; + Planet(double mass, double radius) { + this.mass = mass; + this.radius = radius; + } + public double surfaceGravity() { + final double G = 6.67300E-11; + return G * mass / (radius * radius); + } +} +``` + +이런 식으로 데이터를 상수와 함께 묶어두면 객체지향적으로 표현할 수 있다. + +--- + +## 상수별 메서드 구현 + +각 enum 상수마다 다른 동작이 필요할 때는 **상수별 메서드 구현(Constant-specific method implementation)** 을 사용한다. + +예를 들어 사칙연산을 표현하는 **Operation** 열거형은 다음과 같이 정의할 수 있다. + +```java +public enum Operation { + PLUS { double apply(double x, double y){ return x + y; } }, + MINUS { double apply(double x, double y){ return x - y; } }; + abstract double apply(double x, double y); +} +``` + +이렇게 하면 새로운 상수를 추가할 때 반드시 **apply** 를 구현해야 하므로 실수를 막을 수 있다. + +--- + +## 전략적 열거 타입 패턴 + +상수별 메서드 구현은 중복 코드가 늘어날 수 있다. + +이 경우 **전략적 열거 타입 패턴(Strategy Enum Pattern)** 을 사용한다. + +예를 들어 **PayrollDay** 열거형에서 평일/주말 근무 방식을 **PayType** 이라는 내부 enum에 위임하면, 각 상수의 급여 계산 방식을 깔끔하게 분리할 수 있다 : + +```java +enum PayrollDay { + MONDAY(PayType.WEEKDAY), SATURDAY(PayType.WEEKEND); + private final PayType payType; + enum PayType { + WEEKDAY { int overtimePay(...){...} }, + WEEKEND { int overtimePay(...){...} }; + } +} +``` + +--- + +## 요약 + +- **int enum pattern** 대신 **enum** 을 사용한다. +- **enum** 은 타입 안전하며, 의미 있는 문자열 표현과 순회 기능(**values()**)을 제공한다. +- 상수별 데이터나 행위를 추가할 수 있고, 객체지향적 설계가 가능하다. +- 상수마다 다른 동작이 필요할 경우 상수별 메서드를 쓰고, 일부 상수만 다른 경우에는 전략적 열거 타입 패턴을 사용한다. \ No newline at end of file