콜렉션을 포함한 클래스는 반드시 다른 맴버 변수가 없어야 한다.
— The ThourghtWorks Anthology
객체지향 생활체조 파트 규칙 8에서 언급
as-is
public class User {
	private List<Email> emails;
}

public class Email {
	private String local;
	private String domain;
}
to-be
public class User {
	private Emails emails;
}

/* first class collection */
public class Emails {
	private List<Email> emails;
}

publc class Email {
	private String local;
	private String domain;
}

이점

비지니스에 종속적인 자료구조

이메일에 대해서 아래와 같은 조건이 있을 경우 구현은 다음과 같다.

  • 이메일은 중복 값이 없어야 한다.

  • 이메일은 최대 10개만 저장할 수 있다.

as-is
public class User {
	private static final int MAX_EMAIL = 10;
	private List<Email> emails;

	private void addEmail(Email email) {
		if (emails.size() < MAX_EMAIL) {
			throw new InvaliedException("A maximum of " + MAX_EMAIL + " Email addresses are allowed");
		}
		if (members.stream().anyMatch(email::equals)) {
			throw new InvalidException(email.getContent() + " is already in ter emails");
		}
		emails.add(email);
	}
}

이처럼 이메일 관련 검증 로직과 최대 갯수에 대한 비지니스 요구사항까지 User 가 가지게 된다.

일급 컬렉션을 사용하면 이메일 관련 요구사항은 Emails 에서만 관리하게 되면서 응집도(Cohesion)를 높히고 UserEmail 에 대한 커플링(Coupling)을 낮출 수 있다.

to-be
public class User {
	private Emails emails;
}

public class Emails {
	private static final int MAX_EMAIL = 10;
	private List<Email> emails;

	private void addEmail(Email email) {
		if (emails.size() < MAX_EMAIL) {
			throw new InvaliedException("A maximum of " + MAX_EMAIL + " Email addresses are allowed");
		}
		if (members.stream().anyMatch(email::equals)) {
			throw new InvalidException(email.getContent() + " is already in ter emails");
		}
		emails.add(email);
	}
}

Collection의 불변성을 보장

일급 컬렉션은 컬렉션의 불변을 보장하는데, 단순히 final 을 사용하는 것이 아니라 캡슐화를 통해 이뤄진다. final 은 재할당만 금지할 뿐이다.

Emails 클래스에 생성자와 getter 외에 다른 메소드가 없다. 즉, 아래와 같이 setter를 구현하지 않으면 불변 컬렉션이 된다.

public class Emails {
	private final List<Email> emails;

	public Emails(List<Email> emails) {
		this.emails = emails;
	}

	public Emails getEmail() {
		return new Email(emails.stream()...);
	}

	private Optional<Email> getRepresentEmail() {
		return new Email(emails.stream().filter(Email::isRepresent).findFirst());
	}
}

상태와 행위를 한 곳에서 관리

일급 컬렉션은 값과 로직이 함께 존재하기 때문에 응집도가 높아진다. 즉, Emails 컬렉션을 사용하면 똑같은 기능을 중복 생성하지 않고, 히스토리를 한곳에서 관리할 수 있다.

as-is
public class User {
	private List<Email> emails;

	private Optional<Email> getRepresentEmail() {
		return emails.stream().filter(Email::isRepresent).findFirst();
	}
}
to-be
public class User {
	private Emails emails;
}
public class Group {
	private Emails emails;
}

public class Emails {
	private List<Email> emails;

	private Optional<Email> getRepresentEmail() {
		return emails.stream().filter(Email::isRepresent).findFirst();
	}
}

이름이 있는 컬렉션

예를 들어, NAVER Emails에 대한 요구사항을 검색하거나 선언할 경우 아래와 같은 문제점을 겪을 수 있다.

  • 담당자마다 변수명이 다르다.

  • 중요한 값이지만 명확하게 표현해둔 단어/변수명이 없다.

일급 컬렉션을 사용한다면 NAVER Email에 대한 요구사항이 바뀌었을 경우 NaverEmails 만 검색하면 사용 코드를 모두 찾을 수 있다.

as-is
@Test
public void 이름이_있는_컬렉션() {
	List<Email> googleEmails = createGoogleEmails();
	List<Email> naverEmails = createNaverEmails();
}
to-be
@Test
public void 이름이_있는_컬렉션() {
	private GoogleEmails googleEmails = new GoogleEmails(createGoogleEmails());
	private NaverEmails naverEmails = new NaverEmails(createNaverEmails());
}

참고