일급 컬렉션이라는 단어는 소트웍스 앤솔러지에서 언급되었습니다.
Collection
을 Wrapping
하면서 Wrapping
한 Collection
외에 다른 멤버 변수가 없는 상태를 말합니다.
Collections을 Wrapping 한다는 의미는 아래 코드와 같습니다.
class AClass {
private List<Student> students;
}
class Student {
private String name;
private int age;
public String getName() {
return name;
}
}
위의 코드를 아래 코드로 바꾸는 걸 일급 컬렉션이라고 합니다.
class AClass {
private Students students;
}
class Student {
private String name;
private int age;
public String getName() {
return name;
}
}
class Students {
private List<Student> students;
}
코드를 보면 기존 AClass
의 List<Student>
를 Students
클래스로 바꿔주었습니다. Students
클래스를 살펴보면 List<Student> students
말고는 다른 멤버변수는 존재하지 않습니다. 이를 일급컬렉션이라고 칭합니다.
일급 컬렉션을 사용하면 뭐가 좋을까요?
만약에 학급 수를 검증하는 로직이 필요하다고 했을때 일급컬렉션으로 구현되지 않은 AClass
에서는 아래와 같이 코드를 작성할 것입니다.
class AClass {
private List<Student> students;
public void validateSize(List<Student> students) {
if (students.size() > 30) {
throw new IllegalArgumentException("학생 수가 30명이 넘으면 안됩니다!");
}
}
}
위 코드엔 문제가 발생할 수 있는데 뭐가 문제일까요?
만약 요구사항이 수정되어 AClass
가 아니라 BClass, CClass...
이렇게 N개의 클래스가 생긴다고 하면 아래와 같이 만들 수 있을 것 입니다.
class AClass {
private List<Student> students;
public void validateSize(List<Student> students) {
if (students.size() > 30) {
throw new IllegalArgumentException("학생 수가 30명이 넘으면 안됩니다!");
}
}
}
class BClass {
private List<Student> students;
public void validateSize(List<Student> students) {
if (students.size() > 30) {
throw new IllegalArgumentException("학생 수가 30명이 넘으면 안됩니다!");
}
}
}
AClass, BClass...
를 생성할 때마다 로직을 추가해줘야 합니다.30명
이 아니라 20명
으로 수정해줘야 한다면 N개
의 클래스의 조건을 수정해줘야 합니다.뿐만 아니라 학생 이름을 가지고 학생을 찾는 로직이 필요하다면 아래와 같이 코드를 짤 것입니다.
class AClass {
private List<Student> students;
public void validateSize(List<Student> students) {
if (students.size() > 30) {
throw new IllegalArgumentException("학생 수가 30명이 넘으면 안됩니다!");
}
}
public Student findStudentByName(String name) {
return students.stream()
.filter(student -> student.getName().equals(name))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("이름을 찾을 수 없습니다."));
}
}
class BClass {
private List<Student> students;
public void validateSize(List<Student> students) {
if (students.size() > 30) {
throw new IllegalArgumentException("학생 수가 30명이 넘으면 안됩니다!");
}
}
public Student findStudentByName(String name) {
return students.stream()
.filter(student -> student.getName().equals(name))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("이름을 찾을 수 없습니다."));
}
}
만약 이걸 일급컬렉션으로 만든 코드로 구현하게 된다면 어떻게 될까요?
class AClass {
private Students students;
public Student findStudentByName(String name) {
return students.findStudentByName(name);
}
}
class BClass {
private Students students;
public Student findStudentByName(String name) {
return students.findStudentByName(name);
}
}
class Student {
private String name;
private int age;
public String getName() {
return name;
}
}
class Students {
private List<Student> students;
public Students(List<Student> students) {
validateSize(students);
this.students = students;
}
public void validateSize(List<Student> students) {
if (students.size() > 30) {
throw new IllegalArgumentException("학생 수가 30명이 넘으면 안됩니다!");
}
}
public Student findStudentByName(String name) {
return students.stream()
.filter(student -> student.getName().equals(name))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("이름을 찾을 수 없습니다."));
}
}
차이를 보자면 검증 로직이 일급 컬렉션
으로 모아져 중복이 사라졌습니다.
이는 기존에 AClass
에서 학생 수를 검증하는 역할을 일급 컬렉션
으로 위임하면서 역할과 책임이 분리가 잘되었다고 할 수 있습니다. 만약에 DClass, EClass...
와 같이 학급이 늘어나게 되어도 일급 컬렉션
에서 검증을 수행할 것입니다.
따라서 일급 컬렉션을 사용하면
1. 상태와 로직을 따로 관리할 수 있기에 클래스의 부담을 줄여줄 수 있습니다.
2. 중복코드를 줄일 수 있습니다.
3. 컬렉션에 이름을 붙여줄 수 있습니다.
일급 컬렉션을 구성할때 아래 코드와 같이 setter를 구현해주지 않으면 불변이 보장됩니다.
class Students {
private List<Student> students;
public Students(List<Student> students) {
this.students = students;
}
public List<Student> getStudents() {
return students;
}
}
하지만 setter
가 아니더라도 일급컬렉션
에 영향을 줄 수 있습니다.
@Test
void 일급컬렉션은_불변이_아니다() {
// given
List<Student> list = new ArrayList<>();
list.add(new Student("A", 12));
Students students = new Students(list);
int size1 = students.getStudents().size();
// when
list.add(new Student("B", 12));
int size2 = students.getStudents().size();
// then
System.out.println("size1 = " + size1);
System.out.println("size2 = " + size2);
assertNotEquals(size1, size2);
}
위의 테스트 코드를 실행해보면 size1
과 size2
는 값이 다릅니다.
이유는 List<Student> list
의 주소와 Students
클래스의 멤버변수인 List<Student> students
의 주소값이 동일하기 때문입니다.
파라미터로 인한 멤버변수의 값이 변하게 하고 싶지 않다면 아래와 같이 코드를 작성하면 됩니다.
class Students {
private List<Student> students;
public Students(List<Student> students) {
this.students = new ArrayList<>(students);
}
}
이렇게 하면 생성자 파라미터를 통해 멤버 변수에 값을 할당할때 주소 값을 재할당하기 때문에 영향을 받지 않습니다.
하지만 이것도 테스트 코드로 불변을 깨뜨릴 수 있습니다.
@Test
void 일급컬렉션은_불변이_아니다2() {
// given
List<Student> list = new ArrayList<>();
list.add(new Student("A", 12));
Students students = new Students(list);
int size1 = students.getStudents().size();
// when
students.getStudents().add(new Student("B", 12));
int size2 = students.getStudents().size();
// then
System.out.println("size1 = " + size1);
System.out.println("size2 = " + size2);
assertNotEquals(size1, size2);
}
이를 해결하기 위해서는 Collections
의 unmodifiableList
를 사용해줘야 합니다.
class Students {
private List<Student> students;
public Students(List<Student> students) {
this.students = new ArrayList<>(students);
}
public List<Student> getStudents() {
return Collections.unmodifiableList(students);
}
}
unmodifiableList
를 사용하면 일급컬렉션은 불변이 되고, getter를 통해 가져온 컬렉션은 수정할 수 없습니다.
참고