개요
마틴 파울러의 소트웍스 앤솔로지에 나오는 객체지향 생활 체조 원칙 (Object Calisthenics) 중 이런 항목이 있다. '일급 컬렉션(first-class)을 쓴다'. 객체지향과 클린 코드를 논할때면 단골 손님처럼 등장하는 일급 컬렉션에 대해 알아보자.
일급 컬렉션이란?
일급 컬렉션(First Class Collection)은 컬렉션을 Wrapping하면서, 해당 컬렉션 이외의 다른 멤버 변수를 가지지 않는 클래스를 의미한다. 컬렉션을 단순 데이터로 보지 않고, 하나의 논리 (도메인) 객체로 취급하여 데이터와 데이터를 조작하는 로직을 하나의 클래스로 캡슐화 하는 것이라고 이해하면 된다.
List<Member> members = new ArrayList<>();
이렇게 만든 코드를
class Members {
private final List<Member> members;
Members() {
members = new ArrayList<Member>();
}
Members add(final Member newMember) {
List<Member> adding = new ArrayList<>(members);
adding.add(newMember);
return new Members(adding);
}
}
관련 로직과 함께 이렇게 클래스로 묶어준다는 의미!
왜 이런 개념이 등장한걸까? 일급 컬렉션이 처음 언급된 객체지향 생활 체조 원칙에선 일급 콜렉션에 대해 다음과 같이 언급한다.
콜렉션을 포함한 클래스는 반드시 다른 멤버 변수가 없어야 한다.
각 콜렉션은 그 자체로 포장돼 있으므로 이제 콜렉션과 관련된 동작은 근거지가 마련된셈이다.
필터가 이 새 클래스의 일부가 됨을 알 수 있다. 필터는 또한 스스로 함수 객체가 될 수 있다.
또한 새 클래스는 두 그룹을 같이 묶는다든가 그룹의 각 원소에 규칙을 적용하는 등의 동작을 처리할 수 있다.
이는 인스턴스 변수에 대한 규칙의 확실한 확장이지만 그 자체를 위해서도 중요하다.
콜렉션은 실로 매우 유용한 원시 타입이다.
정리하자면, 컬렉션(데이터)와 컬렉션을 다루는 행위(로직)을 하나의 클래스로 묶어 하나의 개념으로 확장하기 위함으로 이해할 수 있다. 밑줄 친 부분에 따르면 컬렉션과 관련된 코드를 일급 컬렉션에 집합시켜, 컬렉션과 관련된 동작 (=코드)의 중복을 막을 수도 있을 것이다.
일급 컬렉션의 특징
간단히 일급 컬렉션의 개념에 대해 알아봤다. 대체 이 일급 컬렉션의 장점이 뭐기에 각종 클린 코드와 객체지향 원칙에서 이 친구를 언급하는 걸까? 일급 컬렉션의 장단점은 캡슐화와 관련이 있다.
1. 높은 응집도
우리는 for 반목문이나 Stream 등을 통해 컬렉션을 가공 또는 데이터를 뽑아 쓸 수 있다. 단, 이런 컬렉션과 관련된 작업을 처리하는 코드가 시스템 전체에 퍼져 있기에 응집도가 낮아질 수 있다. 위에서 작성한 일급 컬렉션의 예시를 다시 보자.
class Members {
private final List<Member> members;
Members() {
members = new ArrayList<Member>();
}
Members add(final Member newMember) {
List<Member> adding = new ArrayList<>(members);
adding.add(newMember);
return new Members(adding);
}
}
위 일급 컬렉션 Members는 컬렉션 자료형의 인스턴스 변수 외에도, 컬렉션 자료형의 인스턴스 변수에 값을 추가하는 메서드가 존재한다.
예시와 동일하게, 우리는 일급 컬렉션 내부에 컬렉션 변수를 조작하는 (쓰고, 수정하고, 삭제하고, 가져다 읽고) 로직을 위치하여, 관련 도메인 컨텍스트를 녹여낼 수 있다. 만약 members에 30살 이상의 Member만 추가되어야 한다는 조건이 추가되는 상황을 가정해보자. 일급 컬렉션이 없다면 members에 add하는 모든 코드를 찾아 가드를 추가해야 할 것이다. 일급 컬렉션을 사용했다면 add 멤버 메서드만 수정해주면 된다.
이와 같이, 일급 컬렉션 변수에 정상적인 값만 추가되도록 조작하고, 또 변수를 조작하는 로직도 한 클래스에 응집시켜 요구사항 변경이 생겼을 때 적은 수정으로 유연하게 대응할 수 있다는 장점을 갖는다.
이제 우리는 컬렉션이 사용되는 로직, 그리고 컬렉션이 가지는 의미를 파악할 때 일급 컬렉션 클래스 하나만 보면 된다. 시스템 전체에 발생한 코드 중복도 줄이고, 가독성을 높여 코드 의도를 파악하기도 쉬워졌으니 전체 시스템 유지보수성도 좋아진다. 역시 응집도는 높고 볼 일이다👍
2. 불변 보장
또 하나의 장점은 외부로 컬렉션 데이터를 전달할 때 컬렉션의 변경을 막을 수 있다는 점이다.
class Members {
List<Member> members() {
return members;
}
}
위와 같이, 인스턴스 변수를 그대로 외부에 전달하면 외부에서 마음대로 변수를 추가하고 제거할 수 있다. 초기화 한 이후 members에 어떤 값이 더이상 추가되지 않기를 바라면 어떻게 해야 할까?
final은 안전하지 않다. 원시 타입(primitive type)에 사용된 final은 변경을 막는 불변의 의미를 갖지만, 객체(referenced type)에 대해서는 조금 다르게 동작한다. 값을 변경하지 못하게 만드는 것이 아니라, 객체의 참조를 변경하지 못하게 만드는 것!
final이 객체에 대한 참조를 보유한 경우, 객체에 대한 연산으로 객체의 상태가 변경될 수는 있으나 변수는 항상 동일한 객체를 참조한다.
즉 객체 상태는 변경할 수 있으나, 객체를 새로 만들어 새로운 참조값을 할당하는 것만 막아준다고 이해할 수 있다. 따라서 HashMap에 새로운 값은 put하거나, List에서 원소를 remove하는 것 모두가 허용되어 컬렉션의 값 변경을 막을 수 없다 🤔
따라서 우리는 일급 컬렉션을 이용해, 컬렉션 변수를 직접 참조하는 것을 막아 불변을 구현할 수 있다.
class Members {
private final List<Member> members;
Members() {
members = new ArrayList<Member>();
}
List<Member> getMembers() {
return members.unmodifiableList();
}
}
먼저 외부에서 컬렉션을 직접 참조하지 못하게 & 새로운 참조를 할당하지 못하게 private final로 설정한다. 이를 통해 외부 클라이언트는 members를 사용하려면 무조건 getter 등의 메서드를 사용해 클래스에 데이터를 요청해야 한다.
이때 get 메서드가 멤버 변수를 바로 리턴하는게 아니라, unmodifiableList로 값을 넘겨주게 만들자. unmodifiableList (또는 unmodifiableMap, unmodifiableSet 등...) 는 직역 그대로, 컬렉션을 수정할 수 없는 read-Only로 만드는 메서드이다. 이렇게 외부로 컬렉션을 전달할 때 unmodifiable한 상태로로 넘겨주면, 값을 받은 클라이언트는 컬렉션 요소를 변경할 수 없기에 한바퀴 돌아서 불변을 유지할 수 있게 된다 ✨
레퍼런스
- 일급 컬렉션의 소개와 써야할 이유 : https://jojoldu.tistory.com/412
- 일급 컬렉션을 사용하는 이유 : https://tecoble.techcourse.co.kr/post/2020-05-08-First-Class-Collection/
- 객체지향 생활 체조 : https://github.com/iamkyu/TIL/blob/master/.bak/object-calisthenics/object-calisthenics.md
'Develop > Etc' 카테고리의 다른 글
[Cache/Spring] EhCache vs Cache2k vs Caffeine 캐시 비교하기 (0) | 2024.08.12 |
---|---|
[클린 코드] 클래스 응집도의 중요성, 그리고 완전 생성자와 값 객체 (0) | 2023.11.05 |
[Design Pattern] Facade 패턴과 이모저모 (2) | 2023.07.12 |