일급 컬렉션을 왜 사용할까요?

청포도봉봉이·2024년 3월 27일
1

java

목록 보기
18/20
post-thumbnail

일급컬렉션이란?

일급 컬렉션이라는 단어는 소트웍스 앤솔러지에서 언급되었습니다.

CollectionWrapping 하면서 WrappingCollection외에 다른 멤버 변수가 없는 상태를 말합니다.

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;
}

코드를 보면 기존 AClassList<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명이 넘으면 안됩니다!");
        }
    }
}
  1. 학생 수를 검증하는 로직을 AClass, BClass...를 생성할 때마다 로직을 추가해줘야 합니다.
  2. 학생 수 검증하는 로직의 조건이 학생 수가 줄어들어 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("이름을 찾을 수 없습니다."));
    }
}
  1. 똑같은 이름의 메서드와 로직이 불필요하게 증가한다. (중복 코드 발생) 따라서 class의 역할이 무거워진다고 할 수 있습니다.

만약 이걸 일급컬렉션으로 만든 코드로 구현하게 된다면 어떻게 될까요?

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);
}

위의 테스트 코드를 실행해보면 size1size2는 값이 다릅니다.

이유는 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);
}

이를 해결하기 위해서는 CollectionsunmodifiableList를 사용해줘야 합니다.

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를 통해 가져온 컬렉션은 수정할 수 없습니다.

참고

일급 컬렉션 (First Class Collection)의 소개와 써야할 이유

일급 컬렉션을 사용하는 이유

profile
서버 백엔드 개발자

0개의 댓글