일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 객체지향 쿼리 언어
- 교육기획팀
- 자동처리
- springboot
- 자바 ORM 표준 JPA 프로그래밍
- scheduling messages with rabbitmq
- java
- JPQL
- 이펙티브자바
- kusitms
- 교육기획팀원
- Spring Batch
- rabbitmq-delayed-message-exchange
- GitHub Actions
- 최범균
- jdbc
- 큐시즘
- 영속성
- JPA
- 30기
- delayed message plugin
- Domain Driven Design
- 밋업프로젝트
- RESTClient
- reactive operaton
- 한국대학생it경영학회
- Spring
- 도메인 주도 개발 시작하기
- ddd
- cicd
- Today
- Total
코딩은 마라톤
[이펙티브자바] 아이템 16, 17, 18 요약 정리 본문
아이템 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라
class Point {
public double x;
public double y;
}
위와 같은 클래스는 데이터 필드에 직접 접근할 수 있으니 캡슐화의 이점을 제공하지 못한다.
또한 API 수정 없이 내부 표현을 바꿀 수 없으며, 불변식 보장 못하고, 외부에서 필드에 접근할 때 부수 작업을 수행할 수도 없다.
따라서 필드들을 private으로 바꾸고 public 접근자 (getter)를 추가한다.
class Point {
private double x;
private double y;
public double getX() { return x; }
public double getY() { return y; }
public void setX(double x) { this.x = x; }
public void setY(double y) { this.y = y; }
}
패키지 바깥에서 접근할 수 있는 클래스라면(public) 접근자를 제공함으로써 클래스 내부 표현 방식을 바꿀 수 있는 유연성을 얻을 수 있다.
별개로 package-private(default) 클래스 혹은 private 중첩 클래스라면 데이터 필드를 노출해도 문제가 없다.
package-private는 클래스를 포함하는 패키지 내부에서만 사용되고,
private 중첩 클래스는 수정 범위가 더 좁아져서 이 클래스를 포함하는 외부 클래스까지만 수정 범위가 제한되기 때문이다.
결론 : public 클래스의 필드가 불변이라 할지라도 노출하지 말자. (가변 필드는 절대 노출하지 말자)
오히려 package-private 클래스나 private 중첩 클래스에선 종종 노출해도 괜찮다.
public 클래스에서는 필드 노출하지 말자!
아이템 17. 변경 가능성을 최소화하라
불변 클래스 : 그 인스턴스의 내부 값을 수정할 수 없는 클래스
불변 인스턴스의 정보는 고정되어 객체가 파괴되는 순간까지 절대 달라지지 않는다.
장점
- 가변 클래스보다 설계하고 구현하고 사용하기 쉽다.
- 오류가 생길 여지가 적고 훨씬 안전하다.
불변 클래스 만드는 다섯가지 규칙
- 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다. ex) setter
- 클래스를 확장할 수 없도록 한다.
- 상속이 가능하면 하위 클래스에서 객체의 상태를 변하게 할 수 있다.
- 모든 필드를 final로 선언한다.
- 설계자의 의도를 명확히 드러낸다.
- 다른 쓰레드로 새로 생성된 인스턴스를 건네도 문제없이 동작한다.
- 모든 필드를 private으로 선언한다.
- 가변 객체를 클라이언트에서 직접 접근해 수정하는 일을 막아준다.
- 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
또한 함수형 프로그래밍을 이용하는 것도 불변이 되는 영역의 비율을 높일 수 있다.
함수형 프로그래밍 : 피연산자에 함수를 적용해 그 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴
public final class Complex {
private final double re;
private final double im;
public Complex(double re, double im) {
this.re = re;
this.im = im;
}
// 함수형 프로그래밍
public Complex plus(Complex c) {
return new Complex(re + c.re, im + c.im);
}
}
위와 같이 함수형 프로그래밍을 통해 피연산자 자체는 변하지 않을 수 있다.
불변 객체 장점
- 단순하다.
- 불변 객체는 생성된 시점의 상태를 파괴될 때까지 그대로 간직한다.
- 근본적으로 스레드 안전하여 따로 동기화할 필요 없다. (안심하고 공유 가능)
- 여러 스레드가 동시에 사용해도 절대 훼손되지 않는다.
- 자주 쓰이는 값을 상수(public static final)로 제공하여 재활용할 수 있다.
- 자주 사용되는 인스턴스를 캐싱하여 같은 인스턴스를 중복 생성하지 않게 해주는 정적 팩터리를 제공한다.
- 여러 클라이언트가 인스턴스를 공유하여 메모리 사용량과 가비지 컬렉션 비용이 줄어든다.
- 방어적 복사도 필요 없다. (복사해봤자 원본과 똑같으니 복사의 의미가 없다 -> clone 메서드나 복사 생성자 제공하지 않는게 좋다.)
- 자유롭게 공유할 수 있으며 불변 객체끼리는 내부 데이터를 공유할 수 있다.
public class BigInteger extends Number implements Comparable<BigInteger> {
final int signum;
final int[] mag;
/**
* Returns a BigInteger whose value is {@code (-this)}.
*
* @return {@code -this}
*/
public BigInteger negate() {
return new BigInteger(this.mag, -this.signum);
}
mag배열은 비록 가변이지만 복사하지 않고 원본 인스턴스와 공유해도 된다.
그 결과 새로 만든 BigInteger 인스턴스도 원본 인스턴스가 가리키는 내부 배열을 그대로 가리킨다.
- 객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많다.
- 불변식을 유지하기 훨씬 수월하다.
- 그 자체로 실패 원자성을 제공한다.
- 실패 원자성 : 메서드에서 예외가 발생한 후에도 그 객체는 여전히 호출 전과 똑같은 유효한 상태여야 한다.
- 불변 객체 메서드는 내부 상태를 바꾸지 않으므로 실패 원자성을 만족한다.
불변 객체 단점
- 값이 다르면 반드시 독립된 객체로 만들어야 한다.
- BigInteger instance = ...;
instance = instance.flipBit(0);
백만 비트 짜리 BigInteger에서 비트 하나를 바꿔야할 때 새로운 BigInteger 인스턴스를 생성해야 한다.
또한 새로운 instance는 가변이기 때문이다.
즉, 객체 생성 비용이 높고 상태가 다른 객체를 자주 만들어야 한다면 큰 비용이 들게 된다. - 해결책 :
- 다단계 연산을 기본 기능으로 제공한다.
- ex) String의 가변 동반 클래스 : StringBuilder(Buffer)
- 다단계 연산을 기본 기능으로 제공한다.
- BigInteger instance = ...;
불변 클래스를 만드는 또 다른 설계 방법 중에 자신을 상속하지 못하게 하는 방법이 있다.
상속 못하게 하는 가장 쉬운 방법은 final 클래스로 선언하는 것이다.
하지만 더 유연한 방법이 있는데 바로 모든 생성자를 private 혹은 default로 만들고 public 정적 팩터리를 제공하는 것이다.
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);
}
}
결론
- 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.
- 게터가 있다고 무조건 세터를 만들지는 말자
- String과 BigInteger처럼 무거운 값 객체도 불변으로 만들 수 있는지 고심하고, 성능때문에 어쩔 수 없다면 가변 동반 클래스를 public으로 제공하자.
- 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분은 최소한으로 줄이자.
- 변경할 필드 제외 모든 필드는 final로 선언하자.
- 모든 필드는 private final이어야 한다.
- 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
- 생성자, 정적 팩터리 외에는 어떤 초기화 메서드도 public으로 제공해서는 안된다.
아이템 18. 상속보다는 컴포지션을 사용하라
상속은 캡슐화를 깨뜨린다.
(= 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작 이상이 생길 수 있다.)
이러한 이상 문제를 모두 피해갈 수 있는 방식이 있다.
기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하면 된다.
기존 클래스가 새로운 클래스의 구성 요소로 쓰인다는 뜻에서 "컴포지션"이라 한다.
- private 필드로 기존 클래스의 인스턴스 참조
- 새 클래스의 인스턴스 메서드들은 기존 클래스의 대응하는 메서드를 호출해 그 결과 반환 : 전달(forwarding)
- 새 클래스의 메서드들은 전달 메서드(forwarding method)라 부른다.
- 새로운 클래스는 기존 클래스의 구현 방식의 영향에서 벗어나며 기존 클래스에 새로운 메서드가 추가되어도 전혀 영향받지 않는다.
- 래퍼 클래스 :InstrumentedHashSet
public class InstrumentedHashSet<E> extends ForwardingSet<E> {
// 추가된 원소의 수
private int addCount = 0;
public InstrumentedHashSet(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
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) { this.s= s;}
public void clear() {s.clear();}
public boolean contains(Object o) { return s.contains(o);}
public boolean isEmpty() { return s.isEmpty();}
public int size() { return s.size();}
public Iterator<E> iterator() { return s.iterator(); }
public boolean add(E e) { return s.add(e); }
public boolean addAll(Collection<? extends E> c) { return s.addAll(c); }
{ ... }
}
InstrumentedSet 같은 클래스는 다른 Set 인스턴스를 감싸고 있다는 뜻에서 래퍼 클래스라고 한다.
또한 다른 Set에 계측 기능을 덧씌운다는 점에서 데코레이터 패턴이라고 한다.
위 코드들을 토대로 컴포지션을 정리해보면,
새로운 클래스를 만들고 = InstrumentedSet<E> (전체 틀)
private 필드로 기존 클래스의 인스턴스를 참조한다. = ForwardingSet의 s (부분 틀)
즉, ForwardingSet 클래스의 메서드들은 기존 인터페이스(Set)의 메서드에 대응하는 메서드를 호출한다.
그리고 그 메서드들을 이용하는 InstrumentedSet이 되는 것이다.
결론
- 상속은 반드시 하위 클래스가 상위 클래스의 '진짜' 하위 타입인 상황에서만 사용하자
- 클래스 B가 클래스 A와 is-a 관계일 때만 클래스 A를 사용해야 한다.
- is-a 관계라도 안심할 수 없기 때문에 컴포지션과 전달을 사용하자.
- 래퍼 클래스는 하위 클래스보다 견고하고 강력하다.
컴포지션, 래퍼 클래스, 전달 클래스 너무 어렵다. 데코레이터 패턴 너무 어렵다.
좀 더 알아봐야 겠다.....
'Language > Java' 카테고리의 다른 글
[이펙티브자바] 아이템 22, 23, 24 요약 정리 (1) | 2024.06.06 |
---|---|
[이펙티브자바] 아이템 19, 20, 21 요약 정리 (2) | 2024.06.05 |
[이펙티브자바] 아이템 13, 14, 15 요약 정리 (0) | 2024.06.02 |
[이펙티브자바] 아이템 10, 11, 12 요약 정리 (0) | 2024.06.01 |
[이펙티브자바] 아이템 7, 8, 9 요약 정리 (0) | 2024.05.30 |