Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions 전두이/4장/item15.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# 클래스와 멤버의 접근 권한을 최소화하라

### 정보 은닉(캡슐화)의 중요성

- 잘 설계된 컴포넌트는 내부 구현을 완벽히 숨겨 외부에서 접근할 수 없게 하고, API를 통해서만 소통하게 한다.
- 장점:
- 개발 속도 향상: 병렬로 개발이 가능
- 유지보수 비용 절감: 컴포넌트를 빠르게 이해하고 교체 부담이 적음
- 성능 최적화 용이: 특정 컴포넌트만 독립적으로 최적화 가능
- 재사용성 향상: 독립적으로 동작하는 컴포넌트는 다양한 환경에서도 재사용 가능

2. 접근 제한자 및 접근성 원칙
- 접근 제한자는 클래스의 내부 정보 노출을 줄이는 데 필수적
- `private`: 해당 클래스 내부에서만 접근 가능
- `package-private` (기본 접근 수준): 같은 패키지 내에서 접근 가능
- `protected`: 패키지 내와 하위 클래스에서 접근 가능
- `public`: 모든 클래스에서 접근 가능
- 상속 시 접근 제한 규칙:
- 하위 클래스는 상위 클래스에서 정의한 메서드의 접근 수준을 좁힐 수 없음. 이는 리스코프 치환 원칙을 따르기 위함이다.

3. 테스트 시 접근 범위

- 테스트 목적으로 접근 범위를 넓히는 것은 가급적 필요한 최소한의 수준으로 한다.

4. Public 필드 사용에 대한 주의사항

- public 인스턴스 필드는 피하는 것이 좋음
- 이유: 불변성을 해치고, 필드 변경 시 동기화 등의 추가 작업이 불가능하여 스레드 안전을 확보하기 어려움
- 예외: public static final 상수 필드는 가능하나, 참조하는 객체가 불변인지 확인이 필요
24 changes: 24 additions & 0 deletions 전두이/4장/item16.md
Original file line number Diff line number Diff line change
@@ -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` 필드를 사용할 수 있다.
- 이 경우 외부에서 접근할 수 없고, 클래스 내부에서만 사용할 것이기 때문에 필드 공개로 인한 위험성이 줄어든다.
158 changes: 158 additions & 0 deletions 전두이/4장/item18.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# 상속보다는 컴포지션을 사용하라

## 상속이란

상속은 자식 클래스가 부모 클래스의 메서드와 필드를 그대로 물려받아 사용할 수 있게 하는 강력한 도구이다.
하지만 이 과정에서 `캡슐화(Encapsulation)`가 깨질 수 있다.

- 캡슐화: 객체의 내부 구현을 외부에 숨기고, 필요한 부분만 노출하는 원리

상속을 사용하면 자식 클래스가 부모 클래스의 내부 구현에 의존하게 되어, 부모 클래스가 변경될 때 자식 클래스의 동작에도 예상치 못한 오류가 생길 수 있다.

### 상속으로 인한 의도치 않은 동작의 예

- `HashSet` 이라는 클래스의 기능을 확장해, 추가된 원소의 개수를 기록하는 `InstrumentedHashSet` 클래스
- `add` 메서드를 재정의해, 원소가 추가될 때마다 추가된 횟수를 세도록 함

```java
public class InstrumentedHashSet<E> extends HashSet<E> {
private int addCount = 0;

@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}

@Override
public boolean addAll(Collection<? extends E> 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<E> implements Set<E> {
private final Set<E> s;

public ForwardingSet(Set<E> 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<E> extends ForwardingSet<E> {
private int addCount = 0;

public InstrumentedSet(Set<E> s) {
super(s);
}

@Override
public boolean add(E e) {
addCount++;
return super.add(e); // 원래 기능 호출
}

@Override
public boolean addAll(Collection<? extends E> 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 관계일 때만 사용하는 것이 안전하다.
- 상속의 취약점을 피하려면, 상속 대신 컴포지션과 포워딩 방식을 사용하는 것이 좋다.
- **래퍼 클래스**는 상속보다 유연하며, 기존 객체의 기능을 수정하거나 추가하는 데 적합하다.
- **데코레이터 패턴**은 래퍼 클래스를 이용해 기존 객체에 동적으로 다양한 기능을 덧붙일 수 있는 패턴으로, 기능 조합을 유연하게 구성할 수 있다.
26 changes: 26 additions & 0 deletions 전두이/4장/item19.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라

## **상속을 지원하려면 설계를 철저히 해야 한다**

- 상속용 클래스를 설계하려면, 클래스 내부적으로 메서드가 서로 어떻게 사용하는지(자기 사용 패턴)를 명확히 문서화해야 한다.
- 자기 사용 패턴: 클래스 내부에서 한 메서드가 다른 메서드를 호출하는 방식.
- 상속 시 문제가 생길 수 있음: 상위 클래스가 호출한 메서드가 하위 클래스에서 오버라이드된 메서드로 동작할 수 있음.
- **훅 메서드(hook)**

하위 클래스가 상위 클래스의 동작을 중간에 변경하거나 확장할 수 있도록 설계된 `protected` 메서드

- 상위 클래스의 알고리즘 흐름은 유지하되, 하위 클래스에서 원하는 동작을 끼워 넣을 수 있다. → 유연성과 재사용성 증가, 선택적 확장 제공

## **상속 설계는 복잡하고 신중해야 한다**

- 어떤 메서드를 `protected`로 노출할지 잘 판단해야 하고, 실제로 하위 클래스를 만들어 검증해야 한다.
- 상속 설계는 클래스에 많은 제약을 부여하고 유지보수 비용을 높인다.
- 상속용으로 설계하지 않은 클래스는 **상속을 금지**하는 것이 안전하다.
- 클래스에 `final`을 선언하거나, 생성자를 외부에서 호출할 수 없게 설정

---

## 결론

- 상속 설계는 단순히 메서드를 재정의하는 문제를 넘어선다. 개발자로서 "상속을 허용할 것인가, 금지할 것인가"를 명확히 결정하고, 필요하다면 상속을 대신할 다른 설계 방식을 적극적으로 탐색해야 한다.
- 상속용 클래스를 설계하려면 문서화, 테스트, 그리고 내부 동작의 철저한 이해가 필수적이다. **상속은 신중히 사용하되, 필요하지 않다면 피하라.**
57 changes: 57 additions & 0 deletions 전두이/4장/item20.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# 추상 클래스보다는 인터페이스를 우선하라

자바는 다중 구현을 두 가지 방식으로 제공한다: **인터페이스**와 **추상 클래스**. (다중 구현 매커니즘)

하지만 둘 사이에는 중요한 차이점이 있다.

- **추상 클래스:**
- 상속받는 클래스는 반드시 **추상 클래스의 하위 클래스**여야 한다.
- 자바는 단일 상속만 지원하므로, 한 클래스가 여러 추상 클래스를 상속받을 수는 없다.
- 따라서 새로운 타입 정의에 큰 제약이 생긴다.
- **인터페이스**:
- 한 클래스가 여러 인터페이스를 **구현**할 수 있다.
- 어떤 클래스를 상속받고 있든 상관없이 인터페이스만 구현하면 새로운 타입을 추가할 수 있다.

## **인터페이스의 장점**

- **다중 구현 가능**:

클래스는 여러 개의 인터페이스를 동시에 구현할 수 있다.

이를 통해 **유연한 설계**가 가능해진다.

- **기존 클래스에도 쉽게 적용**:

이미 작성된 클래스에도 새로운 인터페이스를 추가 구현할 수 있다.

예를 들어, 기존 클래스가 특정 인터페이스를 지원하지 않는다면, 해당 인터페이스를 구현하도록 쉽게 수정할 수 있다.

- **계층구조 없는 타입 정의 가능**:

상속을 사용하면 계층구조가 생기지만, 인터페이스는 **유연하게 관계를 정의**할 수 있어 더 깔끔한 설계가 가능하다.

- **디폴트 메서드 제공 가능**:

자바 8부터 인터페이스에서 디폴트 메서드(default method)를 지원한다.

이를 활용하면 인터페이스에서도 일부 기본 구현을 제공할 수 있어, 추상 클래스처럼 활용할 수 있다.


## **골격 구현(Skeletal Implementation)**

- **골격 구현**은 인터페이스의 기본 구현을 제공하여 개발자가 구현해야 할 작업을 줄여준다.
- 가능한 경우, 인터페이스에 **디폴트 메서드**를 사용해 골격 구현을 제공하자.
- 하지만 디폴트 메서드로는 해결할 수 없는 제약이 있을 때는 추상 클래스로 골격 구현을 제공하는 경우도 있다.

---

## 결론

- 새로운 타입을 정의할 때는 **추상 클래스보다 인터페이스를 우선**하라.
- 인터페이스를 사용하면 설계가 더 유연하고 확장성이 높아진다.
- 복잡한 인터페이스를 구현해야 한다면, **골격 구현**을 함께 제공해 개발자가 쉽게 사용할 수 있도록 하자.

> **인터페이스와 추상 클래스를 언제 사용할지 명확히 하자.**
>
- **인터페이스:** 다중 구현이 필요하거나 유연한 설계를 원할 때
- **추상 클래스:** 공통 로직을 재사용하거나 디폴트 메서드로 해결할 수 없는 제약이 있을 때
22 changes: 22 additions & 0 deletions 전두이/4장/item21.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# 인터페이스는 구현하는 쪽을 생각해 설계하라

## **디폴트 메서드**

- 디폴트 메서드는 인터페이스 내에서 구현 코드가 있는 메서드
- 과거에는 인터페이스에 메서드의 구현을 넣을 수 없었으나, Java 8 이후 디폴트 메서드를 사용하면 인터페이스에서 메서드를 구현할 수 있게 되었다.

### **장점과 단점**

- 장점: 기존 인터페이스에 새로운 메서드를 추가할 때, 기존 구현체에 영향을 주지 않으므로 **하위 호환성**을 유지할 수 있다.
- 단점: 디폴트 메서드가 너무 범용적이거나 복잡하게 작성되면, **불변식**을 깨뜨릴 수 있기 때문에 신중하게 사용해야 한다.
- **디폴트 메서드를 사용할 때 주의사항**
- 디폴트 메서드를 추가하는 일이 필요한 경우가 아니라면, **기존 인터페이스를 수정하는 일**을 피하는 것이 좋다.
- 또한, 디폴트 메서드는 기존 메서드의 시그니처를 변경하거나 메서드를 제거하는 용도로 사용해서는 안 된다.
- 디폴트 메서드를 추가하기 전에 **기존 구현체들과 충돌이 없는지** 확인해야 하며, 새로운 인터페이스를 설계할 때는 **테스트**를 반드시 거쳐야 한다.


---

## **결론**

- 디폴트 메서드는 매우 유용하지만, **인터페이스를 설계할 때는 신중함**이 필요하고, 꼭 필요한 경우에만 사용해야 한다.
17 changes: 17 additions & 0 deletions 전두이/4장/item22.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# 인터페이스는 타입을 정의하는 용도로만 사용하라

인터페이스는 **타입을 정의**하는 용도로만 사용해야 한다.

즉, 인터페이스가 어떤 메서드를 정의하느냐는 그 구현체가 무엇을 할 수 있는지를 나타내는 것임

- **상수 인터페이스는 피하라 (상수 인터페이스 안티패턴)**
- 상수 인터페이스란 메서드 없이 `static final` 상수만 포함된 인터페이스이다.
- **상수 공개는 다른 방법으로 하라**
- 상수는 `열거형(Enum)`이나 **클래스 내부의 상수**로 정의하는 것이 더 좋은 방법이다.

---

## 결론

- 인터페이스는 **타입을 정의하는 용도**로만 사용해야 하며, 상수 인터페이스와 같은 패턴은 피해야 한다.
- 상수는 열거형(Enum)이나 별도의 유틸리티 클래스를 사용해 정의하는 것이 좋다.
Loading