이번 글에서는 객체지향을 지향하는 개발자라면 한번쯤은 들어봤을 단어인 추상화에 대해서 다뤄보고자합니다.
오늘 오전에 팀 단위 스크럼을 진행하며 이러한 대화가 오고갔습니다.
A: MVP 아키텍처를 구성하면서 View에서 Presenter로 데이터를 넘기면서 abstract class가 아니라 interface를 쓰던데 혹시 왜 그런거야? 뭔 특별한 이유라도 있어? Android 공식문서에서는 abstract class로 구현하라고 권장하던데...
B: 그냥요. 저렇게 해도 되던데요? 그리고 저렇게 해도 상관없을 것 같다고 생각했어요
저는 저런 대화 중간에 끼어들어 아래와 같은 답을 해주었는데, 제가 해당 대화에 내놓았던 답은 아래와 같았습니다.
Me: 어차피 abstract class나 interface는 객체를 추상화한다는 목적은 동일한 요소들이야. 다만 차이점은 abstract class는 모든 메소드 요소가 abstract일 필요는 없지만, interface는 모든 메소드는 abstract로 취급되어야한다는 점에 차이가 있을 뿐이지. 결국에 Hilt를 사용하는 관점에서는 interface로 사용하여도 다형성에 의해 의존성이 잘 주입되기 때문에 interface를 사용해도 상관이 없을거야.
원래는 클린아키텍처에 관해서 글을 쓰려고 하였으나, 객체지향에서의 추상화에 대해서 이해도가 부족하면 클린아키텍처 또한 이해가 되지않을 것 같아 추상화부터 다뤄보기로 하였습니다.
1. 우리가 흔히 알고있는 추상화
우리가 흔하게 알고있는 객체지향 관점의 추상화의 정의는 아마도 아래와 같을겁니다. (출처)
여러 클래스에서 사용하는 공통된 메소드, 혹은 요소들을 공통으로 묶어내서 규칙만 빼내는 행위를 객체지향 관점의 추상화라고 부른다. 여러 클래스를 일반화 하는 개념이라고 생각하면된다.
이러한 관점에서 일반적인 사람들은 인터페이스가 거의 유일한 객체지향에서의 추상화 기법이며, 그냥 공통된 부분을 빼내기 위해서 인터페이스를 쓰는거구나! 라고 생각을 하게될겁니다. 하지만...
2. 다른 관점에서 바라보는 객체지향의 추상화
우아한 객체지향 - 조영호 실장님(우아한형제들 개발실장) 를 보면 객체지향에서의 추상화를 아래와 같이 정의합니다.
객체지향에서의 추상화는 객체를 잘 변하지 않게 하는 모든 일련의 방법을 의미한다
하지만, 이러한 정의만으로 와닿지 않는 몇 가지의 사항들이 있습니다. 첫째로, 도대체 왜 추상화가 필요하고, 둘째로 도대체 이러한 추상화를 이용해서 얻을 수 있는 장점은 뭐가 있는지에 대해서 해결이 되지않습니다. 이제 이러한 의문점에 대해서 설명해드리겠습니다.
3. 추상화, 왜 필요한 것일까?
추상화의 필요성에 논하기 이전에, 좋은 코드가 도대체 뭔지에 대해서 논할 필요가 있다고 생각합니다. 제가 생각하는 좋은 코드란 아래와 같다고 생각합니다.
- 모든 코드는 가독성이 좋아야한다 (i.e. 사람이 이해하는 코드여야한다)
- 코드는 예측 가능성이 높아야한다 (i.e. 유지보수하기 좋은 코드여야한다)
- 코드는 확장에 열려있고 변경에는 닫혀있어야한다 (물론 과도하게 확장에 열려있거나, 혹은 너무 과도한 확장을 염려한 설게는 독이라고도 생각합니다)
저는 열거한 요구사항들 중에서 두번째, 세번째 요소가 추상화의 존재 이유, 필요성이라고 생각합니다. 사실 객체를 추상화하면 두번째와 세번째의 요구사항은 딸려오게되는데요, 사례를 통해서 분석해보도록 하겠습니다.
// Interface
interface BoardRepository {
fun findByUsername(username: String): Board?
}
// Dao
@Repository
class BoardDao(dbTemplate: AnyDatabaseTemplate) : BoardRepository {
override fun findByUsername(username: String): Board? {
return dbTemplate.find(username)
}
}
// Service
@Service
class BoardService(private val boardRepository: BoardRepository) {
fun findByUsername(username: String): BoardQuery.SimpleResponse {
val result = boardRepository.findByUsername(username)
return result.toSimpleResponse()
}
}
// Service Test
fun `username을 통해서 simpleResponse 하나를 가져온다`(): Unit {
// given
val username = "doccimann"
// when
every { boardRepository.findByUsername(username) } returns getMockBoard(username=username)
// then
val result = boardService.findByUsername(username)
verify(exactly = 1) { boardRepository.findByUsername(username) }
verify(exactly = 1) { boardService.findByUsername(username) }
assertNotNull(result)
assertEquals(result.username, username)
}
위의 코드는, BoardDao를 BoardRepository로 추상화하여 BoardService 클래스의 생성자 파라미터로 던지는 코드입니다. 위의 코드 방식으로 작성함으로써 얻을 수 있는 장점은 아래의 것들이 있다고 생각합니다.
- BoardService에게 제공되는 BoardDao의 정보가 제한됩니다. 이에 따라서 BoardDao의 변화가 BoardService에 미치는 영향이 줄어들기 때문에 BoardService는 자신의 로직에 집중할 수 있게됩니다.
- 이 외에도 interface 타입으로 생성자 파라미터를 통해 의존성을 주입하게 되면 확장에 잘 열려있는 형태가 됩니다.
- 이에 따라서 추상화된 타입을 사용함으로써 코드의 예측 가능성또한 높아지는 효과를 불러옵니다.
이러한 장점들로 인해서 Spring을 구현하는 모범사례로 각 상위 계층에 하위 계층의 객체를 주입할 때는 인터페이스를 통해서 주입하는 형태가 꼽히게 되는것입니다.
4. 추상화로 얻을 수 있는 또다른 장점
위와 같이 interface를 통한 추상화를 해두면 DIP라는 혜택 또한 받으실 수 있습니다. 즉, DAO를 직접 Service에 주입하지 않고 domain에 존재하는 repository를 주입하는 형태이기 때문에 계층화의 필요성 에서 언급한 단방향으로 흐르는 의존성 과 두 단게를 건너뛰지 않는 의존성 을 지켜줄 수 있게됩니다.
5. 추상화에 관한 오해
흔히 추상화에 대해선 아래의 오해들을 하는 분들을 많이 보았습니다.
추상화는 무조건 abstract class 아니면 interface로만 하는거야!
반은 맞고 반은 틀린 이야기라고 생각합니다. 제가 위에서 언급드렸듯이, 변경 가능성을 차단하는 모든 일련의 행위들은 추상화라고 볼수가 있기 때문에 변경 가능성이 없는 일반 클래스를 만들더라도 추상화라고 해석할 수 있습니다.
이 글에서 다룬 내용들은 모두 제가 다음에 다룰 주제인 클린아키텍처 를 이해하기 위해선 기본에 깔려있어야하는 내용이므로, 객체지향에서의 추상화 개념에 대해 깊게 이해하고 넘어가는 것을 저는 매우 추천하는 바이며, 그런 목적이 아니더라도 객체지향의 추상화에 대해서 이해하고 넘어가는게 저는 나쁘지 않다고 생각합니다.
'Architecture' 카테고리의 다른 글
Clean Architecture, for sustainable software development (0) | 2023.01.13 |
---|---|
계층화의 필요성 (0) | 2023.01.03 |