3.5.2 - 좋은 의도, 나쁜 결과
- getter와 setter는 캡슐화 원칙을 위반하기 위해 설계되었다.
- Java에서 getter와 setter는 클래스를 자료구조로 바꾸기 위해 도입되었다.
- 자바는 public 프로퍼티를 추가해야하는 어색한 상황을 피하기 위해 프로퍼티를 private 으로 변경하고 모든 프로퍼티에 getter와 setter를 추가하는 기능을 제공한다.
- IDE 설계자들은 getter와 setter 이용해서 private 프로퍼티를 감싸는 방식을 권장한다.
- 하지만 getter와 setter를 사용하면 OOP의 캡슐화 원칙을 손쉽게 위반하게 된다.
- 겉으로는 메서드처럼 보이지만 실제로는 우리가 데이터에 직접 접근하고 있다는 불쾌한 현실을 가리고 있을 뿐이다..
그래서 record가 나오지 않았을까 ! ?
3.5.3 - 접두사에 관한 모든 것
- getter/setter 안티 패턴에서 유해한 부분은 두 접두사인 get과 set이라는 사실이 중요하다.
- 이러한 접두사는 객체가 어떤 존중도 받을 가치가 없는 자료구조라는 사실을 전달하게 된다.
- 이러한 접두사를 붙이면 객체가 데이터를 노출하는 바이트 집합으로 다뤄질 것이라고 기대를 하게 된다.
- 객체는 대화를 원하지 않는다. 그저 우리가 어떤 데이터를 객체 안에 넣어주거나 다시 꺼내주기를 원할 뿐이다.
- 대화를 원하지 않는다….?
- 어떤 데이터를 반환하는 메서드를 포함하는 것은 괜찮다
class Cash { private final int value public int dollars() { return this.value; } }
- 하지만 이 메서드의 이름을 다음과 같이 짓는 것은 올바르지 않다
class Cash { private final int value public int getDollars() { return this.value; } }
- 둘의 차이는 getDollars()는 데이터 중 dollars를 찾은 후 반환하세요 라고 이야기 하는 것이고 dollars()는 얼마나 많은 달러가 필요한가요? 라고 묻는 것이다.
- dollars는 객체를 데이터의 저장소로 취급하지 않고 객체를 존중한다. 사용자는 이 메서드를 통해 얼마나 많은 달러가 포함되어 있는지 알 수 없지만 이 값이 private 프로퍼티로 저장되어 있다고 가정하지는 않는다.
- 즉 내부 구조에 관해 어떤 것도 가정하지 않으며 결코 이 객체를 자료구조라고 생각하지 않는다.
결론은 getter와 setter가 OOP에서 안티패턴이라고 많이 하지만 과연 이런 규칙을 잘 지키면서 개발을 하는 곳이 있을까 하는 의문이 든다. getter와 setter를 편리하게 만들어주는 IDE나 롬복 같은 편리한 도구 덕분에 이 규칙은 계속 이어지지 않을까..? 생각이 드는 것 같습니다.
3.6 부 생성자 밖에서는 new를 사용하지 마세요
- 작고 구현한지 얼마 안된 애플리케이션에서는 현재 다루는 문제점이 명확하게 드러나지는 않지만 큰 규모의 시스템에서는 매우 중요하고 종종 치명적인 결과를 초래하기도 한다.
// 이 예제는 의존성에 문제가 있는 코드의 전형적인 모습을 보여주고 있다. class Cash { private final int dollars; public int euro() { return new Exchange().rate("USD", "EUR") * this.dollars; } }
- euro() 메서드 안에서는 new 연산자를 이용해 Exchange 인스턴스를 생성하고 있다.
- 이 경우 클래스가 작고, 단순하거나, 값비싼 자원을 사용하지 않는다면 전혀 문제가 되지 않는다.
- 문제는 바로 “하드코딩된 의존성"이다. Cash 클래스는 Exchange 클래스에 직접 연결되어 있기 때문에 의존성을 끊기 위해서는 Cash 클래스 내부코드를 변경하는 방법밖에 없다.
- 클래스자 작다면 큰 문제는 아니지만 큰 규모에서 하드코딩된 의존성이 소프트웨어를 테스트하고 유지보수하기 어렵게 만든다.
- 문제의 근본은 new 연산자이다.
- 만약 메서드 내부에서 new 연산자를 사용하지 못하도록 만든다면?
// 올바른 코드 class Cash { private final int dollars; private final Exchange exchange; Cash(int value, Exchange exch) { this.dollars = value; this.exchange = exch; } public int euro() { return this.exchange.rate("USD","EUR") * this.dollars; } }
- … ? 의존은 하지만 의존성을 제어하는 주체가 Cash가 아니라 우리 자신이라는 점에서 차이가 있다.
- 즉 우리의 결정에 의지하고 우리가 제공하는 객체와 협력하게 된다.
// 부 생성자를 여러 개 추가할 수도 있다. class Cash { private final int dollars; private final Exchange exchange; Cash() { // 부 생성자 this(0); } Cash(int value) { // 부 생성자 this(value, new NYSE()); } Cash(int value, Exchange exch) { this.dollars = value; this.exchange = exch; } public int euro() { return this.exchange.rate("USD","EUR") * this.dollars; } }
부 생성자를 제외한 어떤 곳에서도 new를 사용하지 말자 이러한 규칙만 지킨다면 객체들은 상호간에 분리되고 테스트 용이성과 유지보수성을 크게 향상시킬 수 있다.
읽었던 내용중 잘 이해되고 의존성 주입을 다른 방식으로 이해할 수 있었던 부분이었던 것 같아서 괜찮았다. 부 생성자 부분도 괜찮게 읽었던 내용이였는데 두 방법은 충분히 적용해볼만 하다고 생각이 들었다.
3.7 인트로스펙션과 캐스팅을 피하세요
public <T> int size(Iterable<T> items) { if (items instanceof Collection) { return Collection.class.cast(items).size(); } int size = 0; for( T item : items ) { ++size; } return size; }
- 리플렉션을 사용하면 메서드, 명령어, 구문, 클래스, 객체, 타입등을 변경할 수 있다.
- CPU가 이 요소들에 접근하기 전에 쉽고 간단하게 코드를 수정할 수 있게 된다.
- 리플렉션은 매우 강력한 기법이지만 동시에 코드를 유지보수하기 어렵게 만드는 기법이다.
- 코드가 런타임에 다른 코드에 의해 수정된다는 사실을 항상 기억해야 한다면 코드를 읽기 매우 어려울 것이다.
- 이 접근방법은 타입에 따라 객체를 차별하기 때문에 OOP의 기본 사상을 심각하게 훼손시킨다.
- 차별하지 말고 객체가 누구건 상관없이 자신의 일을 할 수 있도록 해야한다.
- 위의 예제에서 Iterable, Collection 이라는 두개의 인터페이스에 의존하고 있고 의존하는 대상이 늘어날수록 결합도는 높아지기 때문에 유지보스 측면에서 중요하지 않다.
instanceof 연산자를 사용하거나 클래스를 캐스팅하는 일은 안티패턴이기 때문에 사용해서는 안된다.
내용이 조금 어렵고 생소해서 이해가 명확하게 되지는 않았던 부분인 것 같습니다…