추상화는 추상적이지 않다

Choi Geonu
9 min readAug 17, 2022

프로그래머라면 언젠가는 마주해보았을 단어가 “추상화(Abstraction)” 일것입니다. 대부분의 사람들은 객체지향의 개념에 대해 공부하며 처음 접해보았을것이고, 디자인패턴, 설계패턴등에 대해 공부할때 또한 접해보았을것입니다.

아무래도 추상화라는 단어와 일상생활에서 쓰는 어휘와의 거리감이 있기에 그 의미를 오해받기 쉽지 않나 생각합니다. 추상화에 깊은 애정을 가진 사람으로서 추상화에 대한 정확한 이해와 컴퓨터과학에서 왜 추상화가 중요한지에 대해 글을 몇자 적어보고자 합니다.

추상화의 정의

위키백과에 따른 추상화의 정의는 다음과 같습니다.

추상화(abstraction)는 복잡한 자료, 모듈, 시스템 등으로부터 핵심적인 개념 또는 기능을 간추려 내는 것을 말한다.

흠잡을 것 없이 정확한 정의이지만 우리들의 마음속에는 그닥 와닿지 않는 정의 일 것입니다. 더군다나 우리가 일상생활에서 사용하는 “추상”의 뉘앙스와는 많이 다르다는 느낌이 있습니다.

여러분은 일상생활에서 추상이라는 단어를 언제 사용하시나요? 저는 다음과 같이 사용할때가 많습니다.

“네 의견은 너무 추상적이야, 조금 더 구체적으로 말해줘”

이때 일반적으로 머리속에 떠오르는 추상이라는 단어의 의미는모호하고, 관념적이며 디테일이 없다 라는 뉘앙스를 담고있지요. 하지만 이는 추상이라는 단어가 들으면 억울해 할 일입니다. 추상이라는 단어가 가진 본질적인 의미는 불필요한 디테일을 걷어내고 핵심 가치만을 남겨 의미를 더욱 명확하게 하는것 이기 때문인데 말이죠.

이러한 일상과의 괴리감 때문에 컴퓨터과학에서 이야기하는 추상화라는 단어의 의미가 우리 마음속에 와닿지 않는건가 생각이 듭니다.

추상화에 대해 더 잘 이해하기 위해 잠시 다른 분야에서의 추상화에 대한 이야기로 넘어가 보겠습니다.

수학에서의 추상화

필자는 대학에서 수학을 전공하였고 추상화의 의미에 대해 가장 큰 각인을 새겨준 과목이 “추상대수학 (Abstract Algebra)” 이었습니다.

대수학이란 간단하게 이야기해서 미지수인 변수와 덧셈, 뺄셈, 곱셉 등의 연산을 이용해 계산하고 방정싱을 풀어나가는 수학의 한 분야입니다.

예를 들면

라는 방정식이 있을 때 우리는 x = 2 라는 것을 풀어낼 수 있죠. 이것이 바로 대수학입니다. 기호와 수식을 이용해 방정식을 푸는, 또는 그 구조를 이해하는 분야이지요.

제가 졸업한 학교에서 몇학기에 걸친 이 과목의 목표는 “5차 방정식 이상에 대한 근의공식은 존재하지 않는다” 를 증명하는것 이었습니다.

이 문제에 다다르기까지 수많은 추상화를 경험하게 되는데 여기서는 간단하게 한가지만 소개해 보겠습니다.

군 (Group)

군은 집합과 연산의 조합을 의미하며, 다음과 같은 조건을 만족합니다.

  • 결합법칙이 성립한다.
  • 항등원이 존재한다
  • 역원이 존재한다.

구체적인 예시를 들면 간단합니다.

정수와 덧셈의 조합은 군입니다

마찬가지로 정수,유리수,실수와 곱셉의 조합 또한 군 입니다.

이러한 군 이라는 구조로 시작해서 정수의 성질을 추상화한 환(Ring), 유리수,실수, 복소수를 추상화한 체(Field) 라는 개념으로 확장하며 최종 목표까지 다다릅니다.

여기서 이러한 추상 구조들이 중요한 이유는, 군에서 볼 수 있듯이 “덧셈” 그 자체에 집중하지 않고 덧셈의 특징에만 집중하여 구조를 만들었다는 것 입니다.
군이 가지는 특징, 정리(Theorem)등을 알아낸다면 그 특징은 고스란히 덧셈, 곱셈등에도 반영이 되지요. 이렇게 알아낸 특징들을 쌓아올려서 우리는 새로운 본질을 찾아낼 수 있는것입니다.

이처럼 수학에서의 추상은 대상의 성질을 명확하게 짚어내고 이를 통해 짚어낸 성질들을 일반화하여 본질적인 특징에 대해 논하게 합니다.

컴퓨터과학에서 이야기하는 추상화도 이것과 일맥상통합니다. 나타내려는 대상의 본질을 간결하고 정확하게 표현하는것을 의미하죠.

컴퓨터과학에서의 추상화

이제 다시 컴퓨터 이야기로 돌아와 보겠습니다. 여기 좌표평면에 나타낸 원이 하나 있습니다.

위와 같은 원을 자료구조로 표현하려면 어떤 특징들을 뽑아내야할까요? 다음과 같은 두가지 요소를 쉽게 생각하실 수 있을겁니다.

  • 원의 중심점의 좌표
  • 원의 반지름의 길이

이를 코드로 나타내면 다음과 같이 나타낼 수 있지요

type Point struct {
X float32
Y float32
}
type Circle struct {
Center Point
Radius float32
}
circle := Circle{
Center: Point{X: 2, Y: 3},
Radius: 3,
}

방금 우리가 한것이 바로 컴퓨터과학에서의 추상화입니다.

이라는 대상을 명확히 표현하기 위해 대상의 핵심적인 특징을 중심점과 반지름으로 정의했고 이것을 코드로 표현했습니다. 수학에서의 추상화와 크게 다를것이 없지요.

이제 조금 더 깊게 들어가보겠습니다.

두개의 서버가 데이터를 송수신할 때, 우리는 보낼 자료구조를 직렬화(Serialize)하게 됩니다. 만약 우리가 직렬화 포맷을 JSON으로 선택했다면 다음과 같이 직렬화 코드가 작성될것입니다.

type JSONSerializer struct {}func (s JSONSerializer) Serialize(data any) []byte {
switch data.(type) {
case string:
return s.WriteString(data)
...
}
...
}
func (s JSONSerializer) WriteString(s string) []byte {
...
}
func (s JSONSerializer) WriteInteger(i int) []byte {
...
}

만약 우리가 JSON이 아닌 MessagePack을 직렬화 포맷으로 선택하면 어떨까요? 위와 비슷하게 다음과 같은 코드가 쓰일겁니다.

type MessagePackSerializer struct {}func (s MessagePackSerializer) Serialize(data any) []byte {
...
}
func (s MessagePackSerializer) WriteString(s String) []byte {
...
}
func (s MessagePackSerializer) WriteInteger(i int) []byte {
...
}

위 두가지 종류의 Serializer에서 공통된 핵심 메소드를 고르라면 무엇을 고르시겠습니까? 두말 할 것 없이 Serialize() 입니다. 여러분은 직관적으로 해당 메소드가 중요하다는것을 알겠지만, 굳이 이유를 정리해보면 다음과 같을 것 입니다.

  • Serializer가 하는 핵심적인 일은 직렬화(Serialize)이다.
  • Serializer를 사용하는 입장에서 관심이 있는것은 직렬화를 수행하는 메소드 뿐이다.

이 또한 추상화의 과정과 닮아있습니다. 두개의 Serializer에서 핵심 가치를 찾아내는 과정이었죠.
이러한 추상화 과정을 끝내기 위해선 위의 의 사례에서 처럼 코드로 나타낼 수 있어야 할 것 입니다. Go 언어에선 interface가 매우 적절한 표현법이 될것입니다.

type Serializer interface {
Serialize(data any) []byte
}

다양한 언어에서 같은 시맨틱을 나타낼 수 있는 문법을 가지고 있습니다. 어떤 언어에서는 protocol이라고 부를테고, 어떤 언어에서는 trait, 어떤 언어에선 abstract class로 같은 목적을 달성할 수 있을겁니다.

어떤 이름을 가지고 어떤 특징을 가졌든 그들이 공통적으로 가지는 한가지 목적은 다음과 같을겁니다.

그것을 상속받는 (혹은 구현하는) 타입이 표현하고자 하는 대상에 대한 추상화 일것입니다.

추상화 사례

이제 실제로 추상화를 이끌어내는 사례를 한가지 보여드리겠습니다.

흔히 웹서비스를 개발할 때 비즈니스 로직을 작성하는 서비스(Service)와 데이터 영속화를 담당라는 레포지토리(Repository)를 많이 만들게 됩니다.

우리가 블로그 게시글을 조회하는 서비스를 작성한다고 생각해보겠습니다. 그렇다면 서비스는 다음과 같이 구현될것입니다.

type BlogPostService struct {
repository BlogPostRepository
}
func (s BlogPostService) GetPost(postID string) Post {
found := s.repository.FindByPostID(postID)
... // Some business logic
... // Some converting logic
return post
}

그렇다면 BlogPostService 에서 관심있는 레포지토리의 형태는 다음과 같을것입니다.

type BlogPostRepository interface {
FindByPostID(postID string) PostModel
}

BlogPostRepositoryFindByPostID() 라는 메소드를 가질것입니다.

자 우리는 여기서 또 추상화를 했습니다. 우리는 BlogPostRepository 가 어떤 종류의 DBMS에서 어떤 알고리즘을 통해 게시글을 찾아올 것 인지에 논하지 않았습니다. 단지 “게시글의 ID를 통해 해당하는 게시글을 찾아낼 수 있다.” 라는 특징만 열거했지요.

이 또한 훌륭한 추상화의 예 입니다. 서비스 오브젝트가 의존하고 있는 레포지토리의 핵심 가치는 무엇인지에 대해 interface로 정의를 한 셈이죠. 이 예제가 Spring 이었다면, Service bean이 Repository class가 아닌 Repository interface에 의존하고 있는 모양새를 의미하기도 합니다.

사족

이 게시글에선 일부러 객체지향 언어가 아닌 Go를 기반으로 의사코드(Pseudocode)를 작성했습니다. 컴퓨터과학에서의 추상화라는 개념이 객체지향의 전유물이 아니라는것 나타내고 싶었고 어떤 언어에서든 추상화라는 것은 중요하다는 이야기를 하고싶었습니다. 함수형 언어인 LISP, Haskell 등의 언어에서도, C언어에서도 추상화는 가장 깊은곳 자리잡은 핵심 가치입니다.

마무리

좋은 추상화를 하기란 언제나 어려운 일입니다. 이 글이 좋은 추상화를 만들어내기 위한 정답을 제시해줄 수는 없지만 좋은 추상화를 첫걸음인 정의에 대한 이해가 되길 바랍니다 🙂

--

--