diff --git "a/\354\240\204\353\221\220\354\235\264/4\354\236\245/item15.md" "b/\354\240\204\353\221\220\354\235\264/4\354\236\245/item15.md" new file mode 100644 index 0000000..8f138ac --- /dev/null +++ "b/\354\240\204\353\221\220\354\235\264/4\354\236\245/item15.md" @@ -0,0 +1,29 @@ +# 클래스와 멤버의 접근 권한을 최소화하라 + +### 정보 은닉(캡슐화)의 중요성 + +- 잘 설계된 컴포넌트는 내부 구현을 완벽히 숨겨 외부에서 접근할 수 없게 하고, API를 통해서만 소통하게 한다. +- 장점: + - 개발 속도 향상: 병렬로 개발이 가능 + - 유지보수 비용 절감: 컴포넌트를 빠르게 이해하고 교체 부담이 적음 + - 성능 최적화 용이: 특정 컴포넌트만 독립적으로 최적화 가능 + - 재사용성 향상: 독립적으로 동작하는 컴포넌트는 다양한 환경에서도 재사용 가능 + +2. 접근 제한자 및 접근성 원칙 +- 접근 제한자는 클래스의 내부 정보 노출을 줄이는 데 필수적 + - `private`: 해당 클래스 내부에서만 접근 가능 + - `package-private` (기본 접근 수준): 같은 패키지 내에서 접근 가능 + - `protected`: 패키지 내와 하위 클래스에서 접근 가능 + - `public`: 모든 클래스에서 접근 가능 +- 상속 시 접근 제한 규칙: + - 하위 클래스는 상위 클래스에서 정의한 메서드의 접근 수준을 좁힐 수 없음. 이는 리스코프 치환 원칙을 따르기 위함이다. + +3. 테스트 시 접근 범위 + +- 테스트 목적으로 접근 범위를 넓히는 것은 가급적 필요한 최소한의 수준으로 한다. + +4. Public 필드 사용에 대한 주의사항 + +- public 인스턴스 필드는 피하는 것이 좋음 + - 이유: 불변성을 해치고, 필드 변경 시 동기화 등의 추가 작업이 불가능하여 스레드 안전을 확보하기 어려움 +- 예외: public static final 상수 필드는 가능하나, 참조하는 객체가 불변인지 확인이 필요 \ No newline at end of file diff --git "a/\354\240\204\353\221\220\354\235\264/4\354\236\245/item16.md" "b/\354\240\204\353\221\220\354\235\264/4\354\236\245/item16.md" new file mode 100644 index 0000000..b241f67 --- /dev/null +++ "b/\354\240\204\353\221\220\354\235\264/4\354\236\245/item16.md" @@ -0,0 +1,24 @@ +# public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 + +**1. public 필드를 그대로 사용할 때의 문제점** + +- **내부 표현을 바꾸기 어렵다** + - `public` 필드를 외부에서 직접 접근할 수 있게 하면, 클래스를 사용하는 모든 코드에서 해당 필드에 의존하게 되어, 그 필드의 구조나 이름을 바꾸기 어렵게 된다. +- **데이터의 무결성 보장**이 힘들다 + - 필드를 `public`으로 두면 외부에서 값을 수정할 수 있어 데이터의 일관성을 유지하기가 어려움 +- **필드 접근 시 부수 작업을 추가할 수 없다** + - 필드에 접근할 때 필요한 로직(예: 값 검증, 로그 기록 등)을 추가할 수 없기 때문에, 객체 지향적인 방식으로 확장성을 갖추기 어렵다. + +**2. 해결책: 필드는 `private`으로 두고 `getter`를 사용** + +- 필드를 `private`으로 선언하고, `getter` 메서드를 통해 간접적으로 접근하게 만드는 것이 좋다. +- `getter` 메서드에서는 필드의 값을 제공하면서, 필요한 부수 작업을 추가하거나, 추후 내부 구조를 변경할 여지도 남길 수 있다. + +**3. 불변 필드라면 공개해도 괜찮지만 주의가 필요** + +- `public`으로 노출되는 필드가 `불변(fianl)`이라면 상대적으로 안전하다. 그래도 완전한 안전을 보장하는 것은 아니므로 되도록 권장되지 않는다. + +**4. 예외: package-private 클래스나 private 중첩 클래스** + +- `package-private` 클래스나 `private` 중첩 클래스에서만 사용할 때는 필요한 경우 `public` 필드를 사용할 수 있다. +- 이 경우 외부에서 접근할 수 없고, 클래스 내부에서만 사용할 것이기 때문에 필드 공개로 인한 위험성이 줄어든다. \ No newline at end of file diff --git "a/\354\240\204\353\221\220\354\235\264/4\354\236\245/item18.md" "b/\354\240\204\353\221\220\354\235\264/4\354\236\245/item18.md" new file mode 100644 index 0000000..38d387d --- /dev/null +++ "b/\354\240\204\353\221\220\354\235\264/4\354\236\245/item18.md" @@ -0,0 +1,158 @@ +# 상속보다는 컴포지션을 사용하라 + +## 상속이란 + +상속은 자식 클래스가 부모 클래스의 메서드와 필드를 그대로 물려받아 사용할 수 있게 하는 강력한 도구이다. +하지만 이 과정에서 `캡슐화(Encapsulation)`가 깨질 수 있다. + +- 캡슐화: 객체의 내부 구현을 외부에 숨기고, 필요한 부분만 노출하는 원리 + +상속을 사용하면 자식 클래스가 부모 클래스의 내부 구현에 의존하게 되어, 부모 클래스가 변경될 때 자식 클래스의 동작에도 예상치 못한 오류가 생길 수 있다. + +### 상속으로 인한 의도치 않은 동작의 예 + +- `HashSet` 이라는 클래스의 기능을 확장해, 추가된 원소의 개수를 기록하는 `InstrumentedHashSet` 클래스 +- `add` 메서드를 재정의해, 원소가 추가될 때마다 추가된 횟수를 세도록 함 + +```java +public class InstrumentedHashSet extends HashSet { + private int addCount = 0; + + @Override + public boolean add(E e) { + addCount++; + return super.add(e); + } + + @Override + public boolean addAll(Collection c) { + addCount += c.size(); + return super.addAll(c); + } + + public int getAddCount() { + return addCount; + } +} +``` + +- 이때 `addAll` 메서드를 호출하여 원소 3개를 추가했을 때, addCount의 값이 3을 반환할 것이라고 기대하겠지만, 실제로는 6이 반환되는 문제가 생긴다. +- 이는 `HashSet`의 `addAll` 메서드가 `add` 메서드를 호출해 구현되기 때문에 발생하는 문제이다. `addAll`이 각 원소를 `add`로 추가하면서 `addCount`가 중복으로 증가하게 된 것이다. + +위의 예시를 바탕으로 상속의 문제점을 정리하자면 다음과 같다. + +### 상속의 문제점 + +- **상위 클래스의 변경에 따른 위험**: 상위 클래스가 릴리스마다 내부 구현을 변경할 수 있고, 그 여파로 하위 클래스가 예상치 못한 동작을 할 수 있다. +- **상위 클래스의 메서드 추가**: 만약 상위 클래스에 새로운 메서드가 추가되면, 자식 클래스가 그 메서드를 재정의하지 못한 상태에서 의도치 않은 행동을 할 수 있다. +- **메서드 시그니처 충돌**: 다음 릴리스에서 상위 클래스에 하필 자식 클래스의 메서드와 같은 이름을 가진 메서드가 추가된다면 컴파일 오류가 발생하거나, 자식 클래스의 메서드가 상위 클래스의 새로운 규약을 충족하지 못하게 될 수 있다. + +## 상속의 대안: Composition, forwarding + +**컴포지션**은 기존 클래스를 새로운 클래스의 필드로 포함하는 방식으로, 새로운 기능을 추가하거나 기존 기능을 변경할 때 사용된다. + +컴포지션은 내부 객체를 통해 원본 객체의 기능을 사용하며, 필요에 따라 추가 로직을 덧붙일 수 있다. + +포워딩은 포함된 객체(컴포지션된 객체)에게 메서드 호출을 전달하는 방식이다. 새로운 클래스에서 메서드를 호출하면 내부에 포함된 객체가 해당 메서드를 실행하는 구조로, 상속보다 유연하게 기존 객체의 기능을 활용할 수 있다. + +### 래퍼 클래스 (Wrapper Class) + +- 컴포지션과 포워딩 방식을 결합하여 기존 클래스의 기능을 확장하는 방법 + +래퍼 클래스는 내부에 원본 객체를 필드로 포함하고, 원본 객체의 기능을 활용하면서도 추가적인 기능을 덧붙일 수 있다. 상속과 달리 기존 객체의 인터페이스에 영향을 미치지 않으므로, **안정적이고 유연하게 기능을 확장**할 수 있다. + +예시: `ForwardingSet`과 `InstrumentedSet`이라는 래퍼 클래스를 사용해 기존 `Set` 인터페이스를 확장 + +여기서는 `Set`의 요소 추가 시, 총 추가된 요소 개수를 기록하는 기능을 덧붙였다. + +```java +public class ForwardingSet implements Set { + private final Set s; + + public ForwardingSet(Set s) { + this.s = s; + } + + @Override + public boolean add(E e) { + return s.add(e); + } + + @Override + public boolean remove(Object o) { + return s.remove(o); + } + + @Override + public boolean contains(Object o) { + return s.contains(o); + } + + @Override + public int size() { + return s.size(); + } + + @Override + public void clear() { + s.clear(); + } + + // Set의 다른 메서드들도 같은 방식으로 구현... +} +``` + +```java +public class InstrumentedSet extends ForwardingSet { + private int addCount = 0; + + public InstrumentedSet(Set s) { + super(s); + } + + @Override + public boolean add(E e) { + addCount++; + return super.add(e); // 원래 기능 호출 + } + + @Override + public boolean addAll(Collection c) { + addCount += c.size(); + return super.addAll(c); // 원래 기능 호출 + } + + public int getAddCount() { + return addCount; + } +} +``` + +- `ForwardingSet` 클래스는 주어진 `Set`의 메서드를 전달하는 역할을 한다. 이 클래스는 다른 `Set` 구현체에 대해 유연하게 사용될 수 있다. + + → 유연하다 의미: `Set` 기능을 모두 유지하면서도 `addCount`라는 추가적인 기능을 제공할 수 있음 + +- `InstrumentedSet` 클래스는 `ForwardingSet`을 상속하여, `add`와 `addAll` 메서드를 오버라이드함으로써 요소가 추가될 때마다 카운트를 증가시킨다. + +래퍼 클래스는 **기존 클래스의 기능을 수정하거나 추가할 때** 유용하다. + +하지만 콜백 프레임워크를 함께 사용할 때는 주의해야 한다. + +- 콜백 프레임워크: 특정 이벤트나 조건이 발생했을 때 미리 정의된 메서드를 호출하는 방식을 의미 +- 이 패턴은 객체가 특정 작업을 완료한 후 다른 객체에 알려줄 수 있게 해준다. + (예시: 버튼 클릭 이벤트, 비동기 작업 처리 등) + +### SELF 문제 + +래퍼 클래스가 내부 객체를 감싸고 있고, 내부 객체가 콜백 메서드를 호출할 때 자신의 참조를 넘긴다면, 콜백 메서드에서 원본 객체의 기능만 호출하게 된다. + +이로 인해 래퍼 클래스에서 추가한 기능이 무시되거나 작동하지 않을 수 있다. + +--- + +## 결론 + +- 상속은 강력하지만, 상위 클래스에 대한 의존성 때문에 캡슐화를 해칠 수 있다. 또한, 상속은 순수한 is-a 관계일 때만 사용하는 것이 안전하다. +- 상속의 취약점을 피하려면, 상속 대신 컴포지션과 포워딩 방식을 사용하는 것이 좋다. +- **래퍼 클래스**는 상속보다 유연하며, 기존 객체의 기능을 수정하거나 추가하는 데 적합하다. +- **데코레이터 패턴**은 래퍼 클래스를 이용해 기존 객체에 동적으로 다양한 기능을 덧붙일 수 있는 패턴으로, 기능 조합을 유연하게 구성할 수 있다. \ No newline at end of file diff --git "a/\354\240\204\353\221\220\354\235\264/4\354\236\245/item19.md" "b/\354\240\204\353\221\220\354\235\264/4\354\236\245/item19.md" new file mode 100644 index 0000000..16faf1a --- /dev/null +++ "b/\354\240\204\353\221\220\354\235\264/4\354\236\245/item19.md" @@ -0,0 +1,26 @@ +# 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라 + +## **상속을 지원하려면 설계를 철저히 해야 한다** + +- 상속용 클래스를 설계하려면, 클래스 내부적으로 메서드가 서로 어떻게 사용하는지(자기 사용 패턴)를 명확히 문서화해야 한다. + - 자기 사용 패턴: 클래스 내부에서 한 메서드가 다른 메서드를 호출하는 방식. + - 상속 시 문제가 생길 수 있음: 상위 클래스가 호출한 메서드가 하위 클래스에서 오버라이드된 메서드로 동작할 수 있음. +- **훅 메서드(hook)** + + 하위 클래스가 상위 클래스의 동작을 중간에 변경하거나 확장할 수 있도록 설계된 `protected` 메서드 + + - 상위 클래스의 알고리즘 흐름은 유지하되, 하위 클래스에서 원하는 동작을 끼워 넣을 수 있다. → 유연성과 재사용성 증가, 선택적 확장 제공 + +## **상속 설계는 복잡하고 신중해야 한다** + +- 어떤 메서드를 `protected`로 노출할지 잘 판단해야 하고, 실제로 하위 클래스를 만들어 검증해야 한다. +- 상속 설계는 클래스에 많은 제약을 부여하고 유지보수 비용을 높인다. +- 상속용으로 설계하지 않은 클래스는 **상속을 금지**하는 것이 안전하다. + - 클래스에 `final`을 선언하거나, 생성자를 외부에서 호출할 수 없게 설정 + +--- + +## 결론 + +- 상속 설계는 단순히 메서드를 재정의하는 문제를 넘어선다. 개발자로서 "상속을 허용할 것인가, 금지할 것인가"를 명확히 결정하고, 필요하다면 상속을 대신할 다른 설계 방식을 적극적으로 탐색해야 한다. +- 상속용 클래스를 설계하려면 문서화, 테스트, 그리고 내부 동작의 철저한 이해가 필수적이다. **상속은 신중히 사용하되, 필요하지 않다면 피하라.** \ No newline at end of file diff --git "a/\354\240\204\353\221\220\354\235\264/4\354\236\245/item20.md" "b/\354\240\204\353\221\220\354\235\264/4\354\236\245/item20.md" new file mode 100644 index 0000000..399cf34 --- /dev/null +++ "b/\354\240\204\353\221\220\354\235\264/4\354\236\245/item20.md" @@ -0,0 +1,57 @@ +# 추상 클래스보다는 인터페이스를 우선하라 + +자바는 다중 구현을 두 가지 방식으로 제공한다: **인터페이스**와 **추상 클래스**. (다중 구현 매커니즘) + +하지만 둘 사이에는 중요한 차이점이 있다. + +- **추상 클래스:** + - 상속받는 클래스는 반드시 **추상 클래스의 하위 클래스**여야 한다. + - 자바는 단일 상속만 지원하므로, 한 클래스가 여러 추상 클래스를 상속받을 수는 없다. + - 따라서 새로운 타입 정의에 큰 제약이 생긴다. +- **인터페이스**: + - 한 클래스가 여러 인터페이스를 **구현**할 수 있다. + - 어떤 클래스를 상속받고 있든 상관없이 인터페이스만 구현하면 새로운 타입을 추가할 수 있다. + +## **인터페이스의 장점** + +- **다중 구현 가능**: + + 클래스는 여러 개의 인터페이스를 동시에 구현할 수 있다. + + 이를 통해 **유연한 설계**가 가능해진다. + +- **기존 클래스에도 쉽게 적용**: + + 이미 작성된 클래스에도 새로운 인터페이스를 추가 구현할 수 있다. + + 예를 들어, 기존 클래스가 특정 인터페이스를 지원하지 않는다면, 해당 인터페이스를 구현하도록 쉽게 수정할 수 있다. + +- **계층구조 없는 타입 정의 가능**: + + 상속을 사용하면 계층구조가 생기지만, 인터페이스는 **유연하게 관계를 정의**할 수 있어 더 깔끔한 설계가 가능하다. + +- **디폴트 메서드 제공 가능**: + + 자바 8부터 인터페이스에서 디폴트 메서드(default method)를 지원한다. + + 이를 활용하면 인터페이스에서도 일부 기본 구현을 제공할 수 있어, 추상 클래스처럼 활용할 수 있다. + + +## **골격 구현(Skeletal Implementation)** + +- **골격 구현**은 인터페이스의 기본 구현을 제공하여 개발자가 구현해야 할 작업을 줄여준다. +- 가능한 경우, 인터페이스에 **디폴트 메서드**를 사용해 골격 구현을 제공하자. +- 하지만 디폴트 메서드로는 해결할 수 없는 제약이 있을 때는 추상 클래스로 골격 구현을 제공하는 경우도 있다. + +--- + +## 결론 + +- 새로운 타입을 정의할 때는 **추상 클래스보다 인터페이스를 우선**하라. +- 인터페이스를 사용하면 설계가 더 유연하고 확장성이 높아진다. +- 복잡한 인터페이스를 구현해야 한다면, **골격 구현**을 함께 제공해 개발자가 쉽게 사용할 수 있도록 하자. + +> **인터페이스와 추상 클래스를 언제 사용할지 명확히 하자.** +> +- **인터페이스:** 다중 구현이 필요하거나 유연한 설계를 원할 때 +- **추상 클래스:** 공통 로직을 재사용하거나 디폴트 메서드로 해결할 수 없는 제약이 있을 때 \ No newline at end of file diff --git "a/\354\240\204\353\221\220\354\235\264/4\354\236\245/item21.md" "b/\354\240\204\353\221\220\354\235\264/4\354\236\245/item21.md" new file mode 100644 index 0000000..99e8ca4 --- /dev/null +++ "b/\354\240\204\353\221\220\354\235\264/4\354\236\245/item21.md" @@ -0,0 +1,22 @@ +# 인터페이스는 구현하는 쪽을 생각해 설계하라 + +## **디폴트 메서드** + +- 디폴트 메서드는 인터페이스 내에서 구현 코드가 있는 메서드 +- 과거에는 인터페이스에 메서드의 구현을 넣을 수 없었으나, Java 8 이후 디폴트 메서드를 사용하면 인터페이스에서 메서드를 구현할 수 있게 되었다. + +### **장점과 단점** + +- 장점: 기존 인터페이스에 새로운 메서드를 추가할 때, 기존 구현체에 영향을 주지 않으므로 **하위 호환성**을 유지할 수 있다. +- 단점: 디폴트 메서드가 너무 범용적이거나 복잡하게 작성되면, **불변식**을 깨뜨릴 수 있기 때문에 신중하게 사용해야 한다. +- **디폴트 메서드를 사용할 때 주의사항** + - 디폴트 메서드를 추가하는 일이 필요한 경우가 아니라면, **기존 인터페이스를 수정하는 일**을 피하는 것이 좋다. + - 또한, 디폴트 메서드는 기존 메서드의 시그니처를 변경하거나 메서드를 제거하는 용도로 사용해서는 안 된다. + - 디폴트 메서드를 추가하기 전에 **기존 구현체들과 충돌이 없는지** 확인해야 하며, 새로운 인터페이스를 설계할 때는 **테스트**를 반드시 거쳐야 한다. + + +--- + +## **결론** + +- 디폴트 메서드는 매우 유용하지만, **인터페이스를 설계할 때는 신중함**이 필요하고, 꼭 필요한 경우에만 사용해야 한다. \ No newline at end of file diff --git "a/\354\240\204\353\221\220\354\235\264/4\354\236\245/item22.md" "b/\354\240\204\353\221\220\354\235\264/4\354\236\245/item22.md" new file mode 100644 index 0000000..ee3dee7 --- /dev/null +++ "b/\354\240\204\353\221\220\354\235\264/4\354\236\245/item22.md" @@ -0,0 +1,17 @@ +# 인터페이스는 타입을 정의하는 용도로만 사용하라 + +인터페이스는 **타입을 정의**하는 용도로만 사용해야 한다. + +즉, 인터페이스가 어떤 메서드를 정의하느냐는 그 구현체가 무엇을 할 수 있는지를 나타내는 것임 + +- **상수 인터페이스는 피하라 (상수 인터페이스 안티패턴)** + - 상수 인터페이스란 메서드 없이 `static final` 상수만 포함된 인터페이스이다. +- **상수 공개는 다른 방법으로 하라** + - 상수는 `열거형(Enum)`이나 **클래스 내부의 상수**로 정의하는 것이 더 좋은 방법이다. + +--- + +## 결론 + +- 인터페이스는 **타입을 정의하는 용도**로만 사용해야 하며, 상수 인터페이스와 같은 패턴은 피해야 한다. +- 상수는 열거형(Enum)이나 별도의 유틸리티 클래스를 사용해 정의하는 것이 좋다. \ No newline at end of file