우테코에서는 객체 지향 프로그래밍
에 대해 굉장히 강조하고 있고, 이를 실천하기 위해 소트웍스 엔톨로지 의 객체지향 생활 체조 원칙
을 지키면서 미션을 수행하도록 가이드하고 있다.
지난 포스팅에서 원시값 포장
에 대해서 정리를 하였고, 이번에는 일급 컬렉션
에 대해 정리하겠다!!
스포를 하자면 일급 컬렉션
은 원시값 포장과 거의 비슷하다.
kotlin 혹은 java에서 제공하는 Collection (List, Map, ...)
을 class 로 wrapping하여 의미 있는 객체(Entity)로 만드는 것이다.
지난 원시값 포장에서 사용했던 Member
의 멤버변수인 subways
를 일급 컬렉션를 활용해 개선해나간 경험을 공유하려 한다.
data class Member(
...
val subways: Set<SubwayStation>,
) {
...
fun matchSubwayLine(other: Member): SubwayLine? {...}
fun matchSubwayStation(other: Member): SubwayStation? {...}
fun matchSubwayStations(other: Member): List<SubwayStation> {...}
...
현재 Member
는 다른 Member 와 일치하는 지하철 역
혹은 지하철 호선
을 가져오고 있다.
fun matchSubwayLine(other: Member): SubwayLine? {
subways.forEach { subway ->
val matchedLine = other.subways.find { it.matchSubwayLine(subway) != null }
if (matchedLine != null) return subway.matchSubwayLine(matchedLine)
}
return null
}
그중에 다른 사용자와 공통된 지하철 노선을 찾아 반환해주는 matchSubwayLine
함수다.
❌ 현재 matchSubwayLine
는 Set<Subway>
의 자율성을 침해하고 있다 😱
일치하는 지하철 노선을 찾는 로직을 Set<Subway>
스스로가 하고 있지 않고, getter()를 통해
Subway
들을 가져와 Member
에서 수행하고 있다.
이는 객체 지향 생활 체조 원칙 9 : getter/setter/프로퍼티 사용 금지
에 위반되는 행위이다.
이에 관련해서 2기 오렌지 센빠이가 잘 정리해주신 글이 있다.
이 원칙은 getter를 사용해서 객체의 상태를 외부로 꺼내와서 조건에 따라 값을 변경한다거나, 직접 로직을 수행하는 것이 아닌 객체가 스스로 수행할 수 있어야 한다
를 강조한다.
matchSubwayLine()
와 같이 무분별하게 Set<Subway> 의 상태
를 외부에서 꺼내 로직을 처리하게되면, Set<Subway>
의 세부사항이 변경되게되면 Member
또한 변경되어야 한다.
Member 뿐 만 아니라 다른 class에서도 Set<Subway>
의상태를 getter() 로 가져와서 로직을 처리하면 결국 변경해야하는 코드의 범위가 넓어지게 된다. 결국 Set<Subway>
와 다른 class의 이는 결합도가 높기 때문에 확장성과 유지보수성이 떨어지게 된다.
(저번에 포스트한 원시값 포장 글을 잘 이해했다면 이해하기 쉬울 것이다 😸)
따라서, Set<Subway>
가 matchSubwayLine()
를 수행할 수 있도록 하고 싶지만 Set
은 코틀린에서 제공하는 기본 api 이다.
따라서, class 로 Set<Subway>
를 wrapping 하여 Subways
를 만들어 책임을 부여했고 이를 일급컬렉션이라 한다.
🤔 확장함수로도 책임을 부여할 수 있는거 아닌가요??
확장 함수로 책임을 부여할 수는 있으나, public한 확장함수는 오히려 동료 개발자가 stdlib 에서 제공하는 함수인줄 오인할 수 있고, 수많은 함수들 때문에 찾기도 힘들다.. 따라서, 비추함!😅
확장 함수 형태로 만들었다고 가정해보자!
fun Set<SubwayStation>.matchSubwayLine(other: SubwayStations): SubwayLine? {...}
@JvmInline
value class SubwayStations(val stations: Set<SubwayStation>) {
fun matchSubwayLine(other: SubwayStations): SubwayLine? {
stations.forEach { subway ->
other.stations.forEach { otherSubway ->
val matchedLine = subway.matchFirstLine(otherSubway)
if (matchedLine != null) return matchedLine
}
}
return null
}
fun matchSubwayStation(other: SubwayStations): SubwayStation? {...}
fun matchSubwayStations(other: SubwayStations):List<SubwayStation {...}
}
Member
에 있었던 Set<SubwayStation>
가 수행할 책임들은 이제 SubwayStations
이 담당한다!
따라서, 외부에서 Set<SubwayStation>
를 직접 getter로 요소들을 뽑아와서 로직 처리를 하지 않고, SubwayStations
에게 메세지를 보내 요청만 하면 된다.(응집도 🆙)
상대방과 만나기 좋은 지하철역들을 추천해주는 기능
이 생겼다고 가정해보자!
만약, SubwayStations
가 없었다면 recommend
관련 모듈에서 Set<SubwayStation>
의 상태를 getter()로 가져와 로직을 수행할 것이다.
그러면, Set<SubwayStation>
와 recommend
의 결합도가 올라갔을 것이고, Set<SubwayStation>
의 응집도는 내려가 추후 변경사항에 유지보수하기 힘들었을 것이다.
// recommend 모듈 - 예시
fun recommendSubwayStation(subways: Set<SubwayStation>, otherMemberLocation: location): List<SubwayStation> {}
그러나, 현재 우리는 SubwayStations(일급컬렉션)이 이에 대한 로직을 수행할 수 있다 😸
@JvmInline
value class SubwayStations(val stations: Set<SubwayStation>) {
...
fun findCloseSubwayStations(otherMemberLocation: Location): List<SubwayStation> {...}
...
}
일급컬렉션은 불명확한 의미를 갖은 일반 Collection 을 class 로 감싸 Entity로 만들어 관련된 상태와 행위를 한 곳에서 관리한다.
일급컬렉션을 잘 활용한다면 유연한 어플리케이션을 만들 수 있는 기반이 될 것이다!! 끗