1. 일급 컬렉션?
일급 컬렉션은 Collection객체를 감싸면서 다른 필드가 없는 클래스를 의미합니다.
특정 클래스에 List나 Set 같은 Collection 필드로 가지고 있을 때, 이들을 하나의 클래스로 만들어서 사용할 수 있습니다!
일급 컬렉션이라는 단어는 소트웍스 엔소롤지의 객체지향 생활체조 파트에서 언급이 되었습니다.
규칙 8: 일급 콜렉션 사용
이 규칙의 적용은 간단하다.
컬렉션을 포함한 클래스는 반드시 다른 멤버 변수가 없어야 한다.
각 콜렉션은 그 자체로 포장돼 있으므로 이제 컬렉션과 관련된 동작은 근거지가 마련된 셈이다.
필터가 이 새 클래스의 일부가 됨을 알 수 있다. 필터는 또한 스스로 함수 객체가 될 수 있다.
또한 새 클래스는 두 그룹을 같이 묶는다든가 그룹의 각 원소에 규칙을 적용하는 등의 동작을 처리할 수 있다.
이는 인스턴스 변수에 대한 규칙의 확실한 확장이지만 그 자체를 위해서도 중요하다.
컬렉션은 실로 매우 유용한 원시 타입이다.
더 자세한 글은 향로님의 글 을 읽어보길 권장합니다.
요약하면, 일급 컬렉션을 사용하면 다음과 같은 장점들이 생기게 됩니다.
- 비즈니스에 종속적인 자료구조
- Collection의 불변성 보장
- 상태와 행위를 한 곳에서 관리
- 이름이 있는 컬렉션
2. 사용 예시
2-1) 해결해야 하는 상황
우리는 다음과 같은 도메인이 있다고 가정해 봅시다.
우리의 목표는 마지막 구간(건대역 - 성수역)을 삭제하는 것이 목표입니다.
이때 다음과 같은 조건을 가지게 됩니다.
- 지하철 노선에 등록된 역(하행 종점역)만 제거할 수 있다. 즉, 마지막 구간만 제거할 수 있다.
- 지하철 노선에 상행 종점역과 하행 종점역만 있는 경우(구간이 1개인 경우) 역을 삭제할 수 없다.
이를 기반으로 테스트 코드를 먼저 작성하면 다음과 같습니다.
1) 2호선을 만들고
2) 구간을 2개 등록한 다음 (강남역 - 건대입구역), (건대입구역 - 성수역)
3) 마지막 구간을 삭제하면, 즉 (건대입구역 - 성수역) 구간을 삭제하면
4) 남은 구간의 사이즈가 1이 될 것입니다. (강남역 - 건대입구역)만 남음
이제 Line(노선), Section(구간), Station(역)이라는 class를 만들어보도록 하겠습니다.
▶ Line.class
@Entity
public class Line {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String color;
@OneToMany(mappedBy = "line", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval = true)
private List<Section> sections = new ArrayList<>();
public Line() {
}
public Line(Long id, String name, String color) {
this.id = id;
this.name = name;
this.color = color;
}
public void addSection(Section section) { sections.add(section); }
// 마지막 구간 삭제 메서드
public void deleteLastSection(Station station) {
if (!isValidSize()) {
throw new SectionException(ErrorCode.CAN_NOT_DELETE_SECTION_EXCEPTION);
}
Section lastSection = sections.get(getLastIndex());
if (!lastSection.hasDownStation(station)) {
throw new IllegalArgumentException();
}
sections.remove(lastSection);
}
// 일부 생략...
}
Line에는 Section을 추가하는 메서드와, 마지막 Section을 삭제하는 메서드가 존재합니다.
▶ Section.class
@Entity
public class Section {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(cascade = CascadeType.PERSIST)
@JoinColumn(name = "line_id")
private Line line;
@ManyToOne(cascade = CascadeType.PERSIST)
@JoinColumn(name = "up_station_id")
private Station upStation;
@ManyToOne(cascade = CascadeType.PERSIST)
@JoinColumn(name = "down_station_id")
private Station downStation;
private int distance;
public Section() {
}
public Section(Line line, Station upStation, Station downStation, int distance) {
this.line = line;
this.upStation = upStation;
this.downStation = downStation;
this.distance = distance;
}
// 일부 생략...
}
Section은 자신의 상행구간 역(upStaion)과 하행 구간 역(downStation)에 대하여 저장하고 있습니다.
▶ Station.class
@Entity
public class Station {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
public Station() {
}
public Station(String name) {
this.name = name;
}
public Station(Long id, String name) {
this.id = id;
this.name = name;
}
// 일부 생략...
}
Station은 단순하게 역과 관련된 정보만을 저장하고 있습니다.
Line은 여러 개의 Section을 가질 수 있습니다. 따라서 List<Section>이라는 자료구조를 통해 Section을 저장하는 상황입니다.
또한 하나의 Section에서는 상행 역, 하행 역 2개를 저장할 수 있습니다.
Line class 코드를 보면, 내부에서 검증하는 비즈니스 로직을 처리한 후, 마지막 Section을 삭제하고 있습니다.
public void deleteLastSection(Station station) {
// 사이즈 검증
if (!isValidSize()) {
throw new SectionException(ErrorCode.CAN_NOT_DELETE_SECTION_EXCEPTION);
}
Section lastSection = sections.get(getLastIndex());
// 마지막 하행종점역이 인자로 받은 역과 일치하는지 검증
if (!lastSection.hasDownStation(station)) {
throw new IllegalArgumentException();
}
// 구간 삭제
sections.remove(lastSection);
}
여기서 몇 가지 문제점이 있다고 생각됩니다.
1. 마지막 구간을 삭제하기 위한 검증이 과연 Line에서 수행할 일인가?
2. 만약 다른 곳에서 Section을 삭제하는 조금은 다른 메서드가 있다면, 검증 로직이 중복되어 들어가야 한다.
2-2) 해결 과정
아래와 같이 관련된 로직을 포함하고 있는 자료구조를 만들면 위에서 언급한 문제들이 모두 해결됩니다.
그리고 이런 클래스를 우린 일급 컬렉션이라고 부릅니다.
List<Section>을 Sections라는 일급객체로 만들어보겠습니다.
@Embeddable
public class Sections {
private static final int FIRST_SECTION_INDEX = 0;
private static final String NOT_EXIST_SECTIONS_EXCEPTION = "삭제할 Sections이 존재하지 않습니다.";
private static final String NOT_SAME_DOWN_STATION_EXCEPTION = "마지막 구간의 하행종점역이 삭제할 하행종점역과 일치하지 않습니다";
@OneToMany(mappedBy = "line", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval = true)
private List<Section> sections = new ArrayList<>();
public void add(Section section) {
this.sections.add(section);
}
public void deleteLastSection(Station station) {
if (isInValidSize()) {
throw new IllegalStateException(NOT_EXIST_SECTIONS_EXCEPTION);
}
Section lastSection = getLastSection();
if (lastSection.dontHasDownStation(station)) {
throw new IllegalStateException(NOT_SAME_DOWN_STATION_EXCEPTION);
}
sections.remove(lastSection);
}
private boolean isInValidSize() { return sections.size() <= 1; }
private Section getLastSection() { return sections.get(getLastIndex()); }
private int getLastIndex() { return sections.size() - 1; }
}
또한 일급컬렉션을 만들 때 @Embeddable을 사용한 것을 확인할 수 있습니다.
이를 사용하는 Line은 다음과 같이 코드가 변경되었습니다.
일급컬렉션을 생성하는 것이 전부입니다. 이때 사용하는 쪽 에서는 @Embedded를 사용하면 됩니다.
이렇게 @Embeddable, @Embedded를 사용한다고 해서 테이블의 구조가 변경되지는 않습니다.
즉, @Embedded를 통해서 동일한 테이블 구조를 가지는 동시에 객체를 분리할 수 있게 됩니다.
@Embeddable을 가진 클래스(Sections)의 멤버는 @OneToMany 등의 어노테이션과 관계없이 @Embbeded에 의해 값 타입이 됩니다.
@OneToMany(mappedBy = "line", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval = true)
private List<Section> sections = new ArrayList<>();
즉 테이블의 관점에서는 사실상 아래와 동일하다 생각할 것입니다.
@Entity
public class Line {
@OneToMany(mappedBy = "line", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval = true)
private List<Section> sections = new ArrayList<>();
}
기존에 Line에서 마지막 Section삭제를 위한 모든 검증 로직이 Sections(일급 컬렉션) 내부로 이동하게 되었습니다.
따라서 Line에서의 삭제는 다음과 같이 일급 컬렉션에게 메시지만 전달하면 됩니다.
Sections라는 일급컬렉션을 만들게되면서 List<Section>에 관한 모든 로직들을 Sections Class로 이동시킬 수 있게 되었습니다.
SOLID원칙 중 하나인 단일책임원칙을 만족할 수가 있습니다.
이제 Section 리스트와 관련된 모든 로직은 Sections에서 관리하게 된 것이죠!
또한 위에서 만든 테스트를 실행할 경우 정상적으로 삭제되는 것을 확인할 수 있습니다.
여기까지 오면 다음 3가지 조건을 충족시켰다고 할 수 있습니다.
- 비즈니스에 종속적인 자료구조
- 상태와 행위를 한 곳에서 관리
- 이름이 있는 컬렉션
단순 List<Section>이 아닌 Sections이라는 이름이 있는 컬렉션이 되었으며,
Sections 안에서 상태와 행위를 전부 관리하게 되었고,
Section의 비즈니스와 관련된 로직들을 전부 모아서 비즈니스에 종속적인 자료구조를 만들었습니다.
마지막으로 불변객체인지만 확인하면 될 것 같습니다.
우리의 Sections를 다시 한번 살펴봅시다.
이 클래스는 add()와 deleteLastSection() 외에 다른 메서드가 없습니다.
즉, 이 클래스의 사용법은 구간을 추가하거나, 구간을 삭제할 뿐인 거죠.
(private 메서드들은 어차피 외부에서 사용이 불가능하니까 상관없습니다)
직접적으로 List<Section> sections에 접근할 수 있는 방법이 없게되었습니다!!
List라는 컬렉션에 접근할 수 있는 방법이 없기 때문에 값을 변경/추가가 안됩니다.
이렇게 일급 컬렉션을 사용하면, 불변 컬렉션을 만들 수 있습니다.
'BackEnd > JPA' 카테고리의 다른 글
[JPA] Lazy 로딩으로 인한 JSON 반환 오류 (No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer) (1) | 2022.09.07 |
---|---|
[JPA] JPA metamodel must not be empty! (0) | 2022.07.28 |
[JPA] SpringBoot 2.5 이후부터 data.sql 초기화 시점 (0) | 2022.06.06 |
[JPA] JSON 직렬화 순환 참조 해결하기 (0) | 2022.06.05 |
[JPA] Spring Data JPA가 제공하는 QueryDsl 기능 (0) | 2022.05.16 |
댓글