다룰 내용
3.1 5개 이하의 public 메서드만 노출하세요
3.2 정적 메서드를 사용하지 마세요
3.3 인자의 값으로 NULL을 절대 허용하지 마세요
3.4 충성스러우면서 불변이거나, 아니면 상수이거나
위 내용에 대해서 말하기 전에 저자가 말하는 절차적인 프로그래밍과 OOP의 차이점에 대해서 언급하고 아래 이야기를 시작하고자 한다.
이 둘의 차이점은 책임을 지는 주체가 무엇인가? 가 다르다.
절차적인 프로그래밍에서는 문장(statement), 연산자(operator), 명령문(instruction)으로 구성된 코드가 가장 중요한 지위를 차지. 명령문이 데이터를 제어하고, 조작하고, 수정하고 읽는다. 데이터는 코드가 다가와서 자신을 수정해 주기를 기다리는 수동적인 존재일 뿐이다. 서브루틴(sub-routine)과 자료 구조(data structure)는 큰 문제를 더 작은 문제들로 분해하기 위해 사용할 수 있는 기본 도구.
객체지향에서는 데이터를 대체하는 객체가 가장 중요한 지위, 명령,문자,연산자는 더 이상 데이터를 책임지지 않는다. 오직 클래스와 인스턴스만을 포함해야 한다. OOP에서는 작은 객체들을 모아 전체 애플리케이션이라는 더 큰 객체로 조합한 후, 이 객체에게 작업을 수행하도록 위임한다.
3.1 5개 이하의 public 메서드만 노출하세요
저자가 강하게 주장하는 5개 public 메서드는 어떤 명확한 근거는 없다. 하지만 하나의 클래스 안에서 public 메서드가 많아지면 클래스가 커지고, 이는 유지보수 비용을 높여지는 쪽으로 흩어진다고 한다. 이는 나도 공감한다. 개발할 때 딱 5개만 노출할 수 있도록 해봐야 겠다. 저자가 말하는 5개가 넘어가는거라면 설계에 문제가 있을수 있다고 시사한다. 그 이유는, 클래스를 작게 만들수록 유지보수 하기 쉬워지며, 이는 에러, 수정이 용이해지는 효과를 일으키기 때문이라고 한다.
코드를 유지보수 또는 신기능을 개발할 때 이 규칙을 지키는 노력을 해봐야될 것 같다.
갑자기 드는 생각은 JPA를 활용해 생성한 Entity가 커지는 경우 어떻게 대처해야될까? 라는 질문이 떠올랐다. MappedClass? 임베디드클래스? 와 같은 어노테이션으로 클래스를 나눌수 있지만, 여전히 강한 결합이 유지되는건 사실이다. 심지어 프로덕트로 운영중인 프로젝트에서는 더욱 어떻게 할 것인가? 쉽사리 나눌수 있는 방법이 생각나지 않는다.
3.2 정적 메서드를 사용하지 마세요
이 절에서 읽으면서 들었던 생각은 나는 한번도 딥다이브하지 못했던 부분을 저자는 고민했었던것 같다.먼저 저자는 정적 메서드 사용을 철저하게 금지해야 한다고 주장하고 있다. OOP에서는 절대 정적 메서드를 사용하지 않아야 한다 라고 말하고 있다. 왜 그럴까? 저자에게도 나름의 이유가 명확히 있다.
먼저 이유를 설명하기 위해 절차지향 프로그래밍와 객체지향 프로그래밍에 대한 차이에 대해서 말한다.
절차지향 프로그래밍은 순차적인 사고방식으로 컴퓨터에게 할 일을 지시하는 것이지만, 객체지향 프로그래밍은 정의하는 것이다.
간단한 예로 Math.max(7,9)
는 컴퓨터에게 7과 9중 최대값은 무엇이냐? 라고 묻는 거라면, new Max(5,9)
는 정의하는 것이다. 안에 무엇을 하는지 관심이 없다.
또다른 예시를 살펴보겠다.
// 중간값을 구하는 2가지 종류의 코드
public void doIt(){
int x = Math.between(5, 9, 13);
if(/* x가 필요한가? */) {
System.out.println("x="+x);
}
}
public void doIt(){
int x = new Between(5, 9, 13);
if(/* x가 필요한가? */) {
System.out.println("x="+x);
}
}
첫번째는 최적화 관점. 절차지향 프로그래밍보다는 객체지향 프로그래밍이 더욱 최적화되어 있다. 이유는 객체지향은 필요한 부분에 대해서 즉시 반환이 필요없다는 점이다.
두번째는 다형성을 예시로 말하고 있습니다. 코드 블럭 사이의 의존성을 끊을 수 있는 능력을 말한다.
class Between implements Number {
private final Number num;
Between(int left, int right, int x){
this(new Min(new Max(left, x), right))
}
Between(Number number){
this.num = number;
}
}
---
Integer x = new Between(new IntegertWithOwnAlgo(5,9,13))
객체지향을 사용하면 위와같이 코드 사이의 의존성을 끊을 수 있다.
세번째로 표현력이라고 말한다. 선언형 방식은 결과를 이야기하는데 반해, 명령형 방식은 수행 가능한 한 가지 방법을 이야기한다. 명령형 방식에서 결과를 예상하기 위해서는 먼저 머릿속에서 코드를 ‘실행’해야 하기 때문에 명령형 방식이 선언형 방식보다 덜 직관적이다.
OOP는 알고리즘과 실행대신 객체와 행동의 관점에서 사고해야 한다.
// 절차적인 프로그래밍
Collection<Integer> evens = new LinkedList<>();
for (int number: numbers){
if ...
}
//
// 객체지향적인 프로그래밍
Collection<Integer> evens = new Filtered(numbers, new Predicate<Integer>() {...})
네번째 이유는 코드 응집도 이다. 위 코드에서 객체지향의 경우, 한 줄로 표현되는 것이 비해서 절차적인 프로그래밍은 긴 코드가 만들어진다.
그외 유틸리티 클래스와 싱글톤 패턴에 대해서 언급하는데, 싱글톤 패턴은 안티패턴이라 말한다. 싱글톤의 장점은 상태를 저장한다라고 하지만, 유틸리티 클래스도 상태를 저장하는 것이 가능하다. 차이점이라 할 수 있는 것은 싱글톤은 setInstance(xxx)
로서 분리가능한 의존성을 가지고 있다는 점이다. 싱글톤을 사용할 거라면 캡슐화를 조금더 고려하길 바란다고 저자는 말한다.
또, 정적메서드는 합성(composition)이 불가능하기 때문에 사용하면 안되는 또다른 이유라 말한다.
3.3 인자의 값으로 NULL을 절대 허용하지 마세요
OOP에서는 ‘존재하지 않는 인자(absent argument)’ 문제는 ‘널 객체(Null object)’를 이용해서 해결해야 합니다. 전달할 것이 없다면, 비어있는 것처럼 행동하는 객체를 전달하면 된다. 그러나 Java는 이를 if(a == null){}
이렇게 직접적으로 체크를 하기 때문에 객체가 존중받지 못한다고 한다.
null 체크를 한다는 것은 코드를 오염시키는 것과 유사하다. 아예 무시해버리거나, 방어적인 코드로 예외를 던져서 처리하는건 어떠한가? 라고 저자는 주장한다.
3.4 충성스러우면서 불변이거나, 아니면 상수이거나
우리는 클래스 하나를 이게 불변인지 가변인지 어떻게 판단할까? 글을 읽기 전에는 equals, hashcode를 구현하고, 클래스안에 객체들을 변경불가능하도록 final을 붙여주면 이게 불변 객체라고 부를 수 있는게 아닐까? 라고 생각했습니다.
class WebPage {
private final URI uri;
WebPage(URI path) {
this.uri = path;
}
public String content(){
...
}
}
위 객체는 불변일까? 가변일까? 나는 불변이라 확신하지만, 책의 저자는 많은 사람들이 이 객체를 보고 가변이라고 말하는 사람이 있다고 한다. 왜그럴까? 저자는 재미있는 표현을 쓰는데, 위 클래스는 결과가 변하기 때문에 상수는 아니지만, 객체가 대표하는 엔티티에 ‘충성하기(loyal)’ 때문에 불변 객체로 분류된다고 한다. 이는 상수와 다른 차이점이다.