[JAVA] 일급 콜렉션을 이용하여 상태와 로직을 따로 관리하자!

2025. 10. 17. 15:10·Java & Kotlin

들어가며

우아한테크코스 코드컨벤션 문서를 보면 아래와 같은 내용이 있다.

콜렉션에 대해 일급 콜렉션 적용...? 뭐 일급비밀(?) 그런건가? 깔깔 처음 봤을 때 감이 하나도 안와서 수많은 기술블로그들을 보고 우테코 선배 기수님들의 코드를 염탐하였다. 보면 대부분 일급 콜렉션을 이용중이다..! 일급 콜렉션을 보면 하나의 도메인을 위해 두 개의 클래스를 만든다. 아래 사진을 보자.

사진처럼 Car라는 도메인을 위해 두개의 클래스 두고 있음(6기 기수님 코드 구조)

그래서 처음에는 에이 뭐야 왜 클래스를 두개나 만들지??? 별로네..! 했었는데, 다~ 의미가 있다. 나도 코드에 적극 적용해볼 생각이기에 이번 기회에 정리해보려고 한다! 가보자!

 

본론으로

Collection을 Wrapping하면서, Wrapping한 Collection 외 다른 멤버 변수가 없는 상태를 일급 콜렉션이라고 한다. 이해를 위해 아래의 간단한 Book 클래스를 보자! 

public class Book {
    private String name;
    private int tag; // 예시를 위해 간단하게 tag는 정수형으로!
}

위의 Book 클래스를 아래의 코드처럼 바꿔보자!

// List<Book> books를 Wrapping
// 일급 컬렉션
public class Books {
    // 멤버변수가 하나 밖에 없다!!
    private List<Book> books;
    // ...
}

코드를 보면 알겠지만 List<Book> books; 말고는 다른 멤버 변수가 없다! 이것이 일급 콜렉션이다.


그래서 이게 왜? 좋은건데?????? 더 자세히 알아보자  

아래 코드처럼 보통 도서관에서는 여러 책을 대여해준다.

// Library.class
public class Library {
    // 도서관에는 여러 책을 대여 해줄 것이다.
    private List<Book> books;
    
    public Library(List<Book> books) {
        this.books = books;
    }
    ...
}

 

도서관에서는 보통 책을 대여할 수 있는 수가 정해져있다! 5권까지만 대여 가능하다고 가정하자. 그러면 아래 코드와 같이 검증 코드가 들어갈 것이다.

// Library.class
public class Library {
    // 도서관에는 여러 책을 대여 해줄 것이다.
    private List<Book> books;
    
    public Library(List<Book> books) {
        this.books = books;
    }
    
    private void validateSize(List<Book> books) {
    	if (books.size() > 5) {
            new throw IllegalArgumentException("책은 5권까지만 대여 가능합니다.")
        }
    }
    // ...
}

 

음 근데 여기까지는 뭐,,, 도메인에서 검증하고 좋은데,, 뭐가 문제지???

 


1. 만약 책에 대한 검증이 늘어난다면??

private void validateSize();
private void validateQulity(); // 책의 파손 상태
private void validateExist(); // 책이 존재하는지?

 

2. 만약 도서관이 국립도서관, 별마당도서관(여기 이쁘던데..) 처럼 늘어난다면 그때마다 검증로직도 추가할 것인가!!!! 만약 그렇다면 도서관의 역할이 너무 무거워지지 않겠는가????

// 국립Library.class
public class 국립Library {
    private List<Book> books;
    
    public 국립Library(List<Book> books) {
        this.books = books;
    }
    
    private void validateSize(List<Book> books) {
    	if (books.size() > 5) {
            new throw IllegalArgumentException("책은 5권까지만 대여 가능합니다.")
        }
    }
   validateQuaity(List<Book> books)...
   validateExist(List<Book> books)...
}

// 별마당Library.class
public class 별마당Library {
    private List<Book> books;
    
    public 별마당Library(List<Book> books) {
        this.books = books;
    }
    
    private void validateSize(List<Book> books) {
    	if (books.size() > 5) {
            new throw IllegalArgumentException("책은 5권까지만 대여 가능합니다.")
        }
    }
   validateQuaity(List<Book> books)...
   validateExist(List<Book> books)...
}

 

3. 검증 로직뿐만이 아니다!! 새로운 메서드를 추가하려면 모든 도서관에 추가해줘야 할 것이다. (find()를 유틸 클래스라고 생각해서 정적 메서드로 따로 빼기도 애매하다.)

// 국립Library.class
public class 국립Library {
    private List<Book> books;
    
    public 국립Library(List<Book> books) {
        this.books = books;
    }
    public Book find(String name) {
        return books.stream()
            .filter(books::isSameName)
            .findFirst()
            .orElseThrow(RuntimeException::new)
    }
}

// 별마당Library.class
public class 별마당Library {
    private List<Book> books;
    
    public 별마당Library(List<Book> books) {
        this.books = books;
    }
    public Book find(String name) {
        return books.stream()
            .filter(books::isSameName)
            .findFirst()
            .orElseThrow(RuntimeException::new)
    }

}

 

결론적으로 Library는 새로운 기능, 검증이 추가될 때마다 매우 무거워지고, 중복코드도 많아질 것이다.

여기서 나온 것이 일급 콜렉션이다. 일급 콜렉션을 이용하면 상태와 행위를 각각 관리할 수 있기에 위의 문제점이 해결된다. 그럼 아래 예제를 통해 문제점을 하나씩 해결해보자.


먼저 책을 일급 콜렉션으로 만들어 보겠다. Books로 만들었다.

// Books.class
public class Books {
    private List<Book> books;
    
	public Books(List<Book> books) {
        validateSize(books) // 생성 시점에 사이즈 검증 중!
        this.books = books
    }
    
    private void validateSize(List<Book> books) {
    	if (books.size() > 5) {
            new throw IllegalArgumentException("책은 5권까지만 대여 가능합니다.")
        }
    
    public Book find(String name) {
        return books.stream()
            .filter(books::isSameName)
            .findFirst()
            .orElseThrow(RuntimeException::new)
    }
}

 

그렇다면 아까 국립도서관과, 별마당 도서관의 코드는 어떻게 달라질까? 아래 코드를 보자!

// 국립Library.class
public class 국립Library {
    private Books books // 일급 콜렉션 적용!!
    
    public 국립Library(List<Book> books) {
        this.books = books;
    }
    public Book find(String name) {
        return books.find(name)
    }
}

//별마당Library.class
public class 별마당Library {
    private List<Book> books; // 일급 콜렉션 적용!!
    
    public 별마당Library(List<Book> books) {
        this.books = books;
    }
    public Book find(String name) {
        return books.find(name)
    }

}

 

Wow!! 각 도서관마다 find()의 로직을 일급 콜렉션에 정의해놓은 find()를 이용해 리턴해주면 된다!! 그리고 책의 size도 Books가 생성되는 시점에 validation하고 있기에 검증도 안에서 몰래 진행중이다!!

 

즉 Library에서 했던 역할을 일급 콜렉션에게 위임하여 상태와 역할을 수행하게 한 것이다. 정말 좋지 않은가?

 

그렇다면 도서관에 Books뿐만 아니라 Pencils, Erasers 도 생긴다면 각 도메인에 어울리는 검증과 메서드들은 도서관은 어떻게 구현되어있는지 알 필요도 없이 쉽게 사용할 수 있을 것이다.

 

정리하자면, 일급 콜렉션이 상태와 로직을 부담해주기에 클래스가 가벼워지고 로직도 깔끔해질 수 있는 것이다!! 정말 좋다잉

일급 콜렉션! 너 참 좋다!

 

일급 콜렉션은 불변성을 보장할까?

일급 콜렉션과 관련된 글을 많이 보다보면, 값을 변경할 수 있는 메서드가 없기에 불변성을 보장해준다. 라며 불변성 보장이라는 키워드가 많이 언급된다. 아래의 Books 일급 콜렉션을 보자. 얼핏보면 setter도 없어 일급 콜렉션은 불변을 보장해 줄 것 같은 느낌이 든다.

public class Books {
    private final List<BookNumber> books;

    public Books(List<BookNumber> books) {
        this.books = books;
    }

    public List<BookNumber> getBooks() {
        return books;
    }
}

public class BookNumber {
    private final int bookNumber;

    public BookNumber (int bookNumber) {
        this.bookNumber = bookNumber;
    }
    
}

 

흠 그럼 아래의 테스트 코드에서 Book에 get(1), get(2)를 하게되면 어떻게 될까?  각각 1과 2가 들어가 있다.

@Test
public void Books_변화_테스트() {
    List<BookNumber> bookNumbers = new ArrayList<>();
    bookNumbers.add(new bookNumbers(1));
    Book book = new Book(bookNumbers);
    bookNumbers.add(new BookNumber(2));
}

 

그렇다 Books의 생성자를 보면 알 수 있듯이, BookNumber와 Book클래스의 멤버변수의 주소값이 같으므로 영향을 받게 된다. 즉 불변이 아니란 소리다! 그렇다면 새로운 객체를 만들어서 반환해주면 되지 않을까?

public class Books {
    private final List<BookNumber> books;

    public Books(List<BookNumber> books) {
        this.books = new Arraylist<>(books);
    }

    public List<BookNumber> getBooks() {
        return books;
    }
}

위와 같이 수정하면 Books가 생성될 때 멤버변수인 books가 새로운 주소가 할당되므로 영향을 받지 않을 것이다!!!!!!

 

하지만?? 아래의 테스트 코드를 보자..

@Test
public void Books_변화_테스트() {
    List<BookNumber> bookNumbers = new ArrayList<>();
    bookNumbers.add(new bookNumbers(1));
    Book book = new Book(bookNumbers); // 1을 넣어둔 bookNumber List로 만든 Book 클래스를 만든다.
    book.getBooks.add(new BookNumber(2)); // 그 클래스에 접근해서 books에 add한다면...? 주소 참조가 가능하다.. 크아악
}

주석으로 주소 참조하는 방법을 써놨는데.. 결론은 어떤 방법으로든 Collection에 주소 참조가 가능해서 불변이 무너진다는 것이다. 흑흑

 

이를 해결하는 방법으로는 unmodifiableList를 이용하면 된다! -> 방어적 복사 -> 방어적 복사 부분은 Collection의 복사 방법이라는 주제로 따로 다룰 예정이다. unmodifiableList는 이름 그대로 수정 불가능 List정도로 이해하면 된다. 코드는 아래와 같다. (getter 뿐만 아니라 생성자 private으로 막고 팩토리메서드 써도 좋을듯!)

public class Books {
    private final List<BookNumber> books;

    public Book(List<BookNumber> books) {
        this.books = new ArrayList<>(books);
    }

    public List<BookNumber> getBook() {
        return Collections.unmodifiableList(books);
    }
}

 

마무리하며

좋은 자바 코드들을 보면.. 요 일급 콜렉션... 다 쓰길래 한번 찐득하게 찍어먹어봤다. 이제는 도메인 계층에서 특수한 상황이 필요하다면(대부분 그렇지 않을까..) 일급 콜렉션이 먼저 떠오를 것 같다. 사실 이것만 써버릴까 무섭다... 하지만 자바의 Collection을 활용한 너무 매력적인 방법!! 나의 우테코 프리코스 1주차 코드도 일급 콜렉션으로 도메인을 구성해봤다. 다른 우테코 프리코스 팀원들은 어떻게 볼까? 얼른 코드리뷰 받고 싶다. 파이팅!

'Java & Kotlin' 카테고리의 다른 글

[JAVA] split() 메서드 정복하기 (나는 왜 split(delimiter, -1)을 썻을까?)  (1) 2025.10.17
[JAVA] 정규 표현식, Pattern 클래스로 쉽게 이용해보자! (Pattern.quote()를 중점적으로)  (0) 2025.10.17
[JAVA] final 키워드 정복하기!  (0) 2025.10.16
[JAVA] 정적 중첩 클래스를 활용하여 계층간 독립적인 Validation을 적용해보자  (0) 2025.10.16
[JAVA] 정적 팩토리 메서드(Static Factory Method)란? (버거 먹고싶은 작성자와 스프링의 반환 방식을 곁들인)  (0) 2025.10.16
'Java & Kotlin' 카테고리의 다른 글
  • [JAVA] split() 메서드 정복하기 (나는 왜 split(delimiter, -1)을 썻을까?)
  • [JAVA] 정규 표현식, Pattern 클래스로 쉽게 이용해보자! (Pattern.quote()를 중점적으로)
  • [JAVA] final 키워드 정복하기!
  • [JAVA] 정적 중첩 클래스를 활용하여 계층간 독립적인 Validation을 적용해보자
노을을
노을을
진인사대천명
  • 노을을
    노을의 개발일기장
    노을을
  • 전체
    오늘
    어제
    • All (61) N
      • Java & Kotlin (16)
      • Spring (3) N
      • Problem Solve (13) N
      • Computer Science (0)
      • Infra (1)
      • DB (2)
      • Various Dev (23)
        • 우아한테크코스 (9)
        • Git&Github (2)
        • Unity (12)
      • Book (1)
      • Writing (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    개발자
    코딩
    알고리즘
    백준
    github
    코테
    티스토리챌린지
    게임개발
    스프링
    오픈미션
    유니티
    프리코스
    개발
    우아한테크코스
    우테코
    코딩테스트
    자바
    8기
    합격
    java
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
노을을
[JAVA] 일급 콜렉션을 이용하여 상태와 로직을 따로 관리하자!
상단으로

티스토리툴바