본질 기반 해석(Essence-Based Interpretation, EBI)
1. 소개
나는 좋은 코드에 대해서 많은 노력을 해왔다. 좋은 코드를 작성하기 위해서 리팩토링과 디자인 패턴은 물론이고 여러 아키텍처와 개발 방법을 학습했다. 내가 작성한 코드가 잘 동작함에도 불구하고 더 나은 구조나 이름에 대해서 한참 고민하기도 했다.
그럼에도 불구하고 선택의 순간에 명확히 답을 내리지 못하고 오늘은 A방법으로 구현했다가 내일은 B방법으로 구현하는 일이 오랜시간 지속됐다. 왜냐하면 두 방법 모두 장단점이 명확했기 때문에 어느 한 방법을 선택해도 아쉬움이 남기 때문이다. 여기서 방법은 디자인 패턴이 될 수도 있고 함수나 변수의 이름일 수도 있다.
그 때의 나는 개발자로서 어느 정도 역량을 갖추고 있다고 생각했지만, 이런 선택의 고민은 아무리 많은 경험과 노력을 쌓아도 좀처럼 사라지지 않을 것만 같았다. 이 부분 만큼은 정답이 존재하지 않고 개인의 성향에 따른 선택이 있을 뿐인, 예술의 영역은 아닐까? 하는 생각도 했다. 그 만큼 나에게는 넘기 힘든 벽처럼 보였다. 마치 디자인 패턴을 이해하지 못하는 개발자들이 디자인 패턴은 실무에서 쓸모없다고 치부하는 것과 같은 방어기재였을지도 모르겠다.
그러다 문득 내가 놓치고 있던 것을 깨달았다. 먼저 몇 가지 사례를 살펴보고 그것이 무엇인지 알아보려고 한다.
2. 방향키의 구현
사용자에게 ‘상/하/좌/우’를 선택할 수 있는 방향키를 제공해야 한다. 4개 화살표의 모양은 동일하고 방향만 다르다. 이 때 방향키를 구현하는 방법이 두 가지 있다.
[그림 2-1] 동일한 모양의 방향키를 가진 게임패드
2.1. 방법#1 - 한 개의 이미지를 회전시켜서 재사용
방법#1
은 한 개의 화살표 이미지(arrow.png)를 회전시켜서 구현하는 것이다. 화살표의 모양은 동일하기 때문에 쉽게 구현할 수 있다.
이 방법은 저장 공간을 적게 차지하는 장점이 있다. 그러나 코드 가독성이 상대적으로 떨어진다는 단점이 있다.
<Image src="arrow.png" rotate="0" />
<Image src="arrow.png" rotate="180" />
<Image src="arrow.png" rotate="-90" />
<Image src="arrow.png" rotate="90" />
[그림 2-2] 한 개의 이미지를 회전시켜서 구현한 방향키
2.2. 방법#2 - 4개의 이미지를 사용
방법#2
는 상/하/좌/우에 해당하는 4개의 이미지를 사용하는 것이다.
이 방법은 더 많은 이미지 리소스를 관리하는 번거로움과 저장 공간도 더 많이 차지한다는 단점이 있다. 반면 코드의 가독성은 비교적 좋다.
<Image src="up.png" />
<Image src="down.png" />
<Image src="let.png" />
<Image src="right.png" />
[그림 2-3] 상/하/좌/우 4개의 이미지로 구현한 방향키
2.3. 옳은 방법은 무엇일까?
무엇이 옳은 것일까? 혹은 이 선택에 있어서 옳고 그름이 존재하긴 하는 걸까? 그저 개인의 철학에 따른 선택만이 있는 것은 아닐까? 효율성을 추구한다면 방법#1
이지만 읽기 쉬운 코드를 추구한다면 방법#2
일 것이다.
읽기 쉬운 코드가 옳은가? 성능 효율적인 코드가 옳은가? 대체로 과거에는 성능 효율을 우선했겠지만 요즘처럼 하드웨어의 성능이 충분한 경우에는 읽기 쉬운 코드를 선호한다. 그런 이유로 읽기 쉬운 코드를 선택해야 할까?
다양한 생각들이 있겠지만 우선적으로 고려해야 하는 것은 화살표의 의미다. 만약 화살표가 [그림 2-4] 처럼 특정한 물체를 가리키는 용도라면 방법#1
처럼 화살표를 회전시켜서 구현하는 것이 맞다.
[그림 2-4] 무언가를 가리키는 용도의 화살표
그러나 사용자가 생각한 방향키는 [그림 2-5] 와 같이 키보드 구석에 고정되어 있는 4개의 방향키였을 것이다. 그리고 그런 사용자의 생각과 유사한 것은 방법#2
와 같이 4개의 이미지를 사용하는 것이다.
[그림 2-5] 서로 다른 모양의 화살표를 가지는 방향키
사용자에게 보여지는 결과물은 같기 때문에 어떤 방법을 선택해도 큰 차이는 없다고 생각할지도 모르겠다. 사용자의 생각을 무시한 채 단순히 구현 편의성만을 추구하면 어떻게 될까?
사용자는 언제든지 화살표의 모양을 쉽게 변경할 수 있다고 생각한다. 왜냐하면 사용자는 당연히 4개의 이미지로 구성된 방향키라고 생각하기 때문이다. 이미지를 회전시켜서 성능을 최적화 하는 것은 지극히 개발자의 입장일 뿐이다. 그리고 어느날 사용자는 방향키의 모양을 [그림 2-4] 처럼 변경해 달라고 가볍게 요구할 수 있다. 키의 이미지를 변경하기만 하면 되니까 쉬운 작업이라고 생각할 것이다. 그러나 개발자는 구현 방법을 바꿔야 하는 큰 일이 된다.
화살표의 모양이 동일한 것은 그저 우연일 뿐이다. 이런 우연으로 생긴 상황을 코드에 반영해서 구현하면 사용자의 생각이나 기대와 점점 멀어지게 된다. 다시 얘기하자면 사용자의 의도를 무시한 채 구현 편의성만을 추구하면 유지보수가 점점 더 어려워 지게 된다.
2.4. 해석의 어려움
요구사항의 본질을 통찰하는 과정에서 여러 방법을 두고 고민하게 되는 이유 중에 하나는 당연한 정보는 누락하기 때문이다.
사용자가 방향키를 요구사항으로 언급했을 때는 키보드의 그 방향키라고 구체적으로 설명하지는 않았을 것이다. 사용자의 입장에서는 방향키라고 하면 당연히 키보드의 그것이라고 생각하기 때문이다.
그러나 개발자 입장에서는 방향키에 대한 추가 정보가 없기 때문에 구현 방법을 선택하는 데 있어서 조금 더 고민하게 되는 것이다.
이것이 해석의 어려운 부분인데 당연하다고 여겨 구체적 정의를 생략한 부분은, 개발 단계에서 개발자가 스스로 채워넣어야 한다. 그리고 개발자가 누락된 부분을 채우려면 요구사항이 그렇게 정의된 이유와 과정까지 모두 고려해야 하기 때문에 많은 경험과 통찰력이 필요하다.
만약 지금 상황에서 사용자의 의도를 정확히 파악할 수 없다면 어떻게 해야 할까? 혹은 어떻게 변경될지 예측할 수 없다면 어떻게 해야 할까?
아래처럼 Up, Down, Left, Right
클래스로 정의해서 화살표의 요구사항이 어떻게 변경 되더라도 다른 곳에 영향이 없도록 하면 된다.
<script>>
const Up = () => <Image src="arrow.png" rotate="0" />
const Down = () => <Image src="arrow.png" rotate="180" />
const Left = () => <Image src="arrow.png" rotate="-90" />
const Right = () => <Image src="arrow.png" rotate="90" />
</script>
<body>
<Up />
<Down />
<Left />
<Right />
</body>
3. REST API의 Shallow Routing vs Nested Routing
[그림 3-1]은 사용자가 영화 예매 서비스에서 상영 중인 영화/극장/시간을 선택하는 시퀀스 다이어그램이다. 여기에서 REST API의 라우팅을 어떻게 디자인 해야 할까?
[그림 3-1]
3.1. Shallow Routing
Shallow Routing
형식으로 디자인 한다면 아래와 비슷한 형태가 될 것이다.
# 상영 영화 목록 요청
/movies?status=showing
# 상영 극장 목록 요청
/theaters?movieId={movidId}
# 상영일 목록 요청
/showdates?movieId={movieId}&theaterId={theaterId}
Shallow Routing
은 각 리소스를 독립적으로 관리할 수 있으므로 확장성이 좋다. 그러나 리소스 간의 관계를 명확하게 표현하지 않기 때문에 복잡한 계층 구조의 데이터를 표현하는데 어려움이 있다.
3.2. Nested Routing
Nested Routing
형식으로 디자인 한다면 아래와 비슷한 형태가 될 것이다.
# 상영 영화 목록 요청
/showing/movies
# 상영 극장 목록 요청
/showing/movies/{movieId}/theaters
# 상영일 목록 요청
/showing/movies/{movieId}/theaters/{theaterId}/showdates
Nested Routing
은 리소스 간의 관계를 URL에서 명확하게 표현할 수 있으므로, 복잡한 리소스 구조를 표현하는데 적합하다. 그러나 중첩된 리소스 구조가 변경될 경우, URL도 함께 변경되어야 하므로 유연성이 제한된다.
3.3. 옳은 방법은 무엇일까?
두 REST API의 라우팅 디자인 방식의 장단점을 간단하게 살펴봤다. 그렇다면 Shallow Routing
의 유연성과 Nested Routing
의 명확함 사이에서 어떤 방법을 선택해야 할까?
두 방식 중에서 무엇을 선택할 것인지는 개념적인 관점에서 영화 예매 프로세스를 더 잘 표현하는 것이 무엇인지를 봐야 한다.
그런 면에서 Nested Routing
은 티켓 구매 프로세스를 그대로 반영하고 있다.
티켓 구매 프로세스가 영화 선택 후 극장을 선택해야 하듯이, Nested Routing
도 영화를 지정하지 않으면 극장을 지정할 수 없다.
즉, Nested Routing
의 REST API가 티켓 구매 프로세스와 유사한 구조를 표현하고 있다.
이 정도면 별도의 문서가 없어도 티켓 구매 프로세스를 알 수 있을 것이다.
Shallow Routing
과 Nested Routing
중에서 무엇이 좋은가에 대한 논쟁을 종종 보게된다.
그러나 그런 논쟁은 무의미하다. 중요한 것은 요구사항을 보다 정확히 반영하는 것이 무엇이냐인 것이다.
기술적 관점에서 보면 답이 없는 문제를 가지고 논쟁을 하니 논쟁이 끝나지 않는 것이다.
“한참 고민해도 답이 보이지 않는다면, 답이 거기에 없는 것이다.”
3.4. 클래스의 상속(Inheritance)과 합성(Composition)
Shallow Routing
과 Nested Routing
의 논쟁과 유사한 다른 논쟁으로 클래스의 상속(Inheritance)과 합성(Composition)이 있다.
유연성이 주는 장점 때문에 Shallow Routing
이 기술적으로 우월하다는 대체적인 공감대와 마찬가지로 클래스도 가능하면 상속을 피하고 합성을 사용하는 것이 좋은 재사용 방법이라고 한다. 그러나 이것도 마찬가지로 도메인 개념을 더 잘 표현하는 것이 무엇인지를 고민해야 하는 것이지 기술적 우월성을 우선해서 고려하면 안 된다.
위의 다이어그램에서 Dog는 Animal의 한 종류다. 이것은 상속으로 표현하는 것이 자연스럽다. 반면에 Engine은 Car를 구성하는 부품 중 하나이다. 이것은 포함으로 표현하는 것이 자연스럽다.
4. 유사한 형식의 문서 구현 방법
소득증명서와 같은 국내에서 발행되는 문서의 국외 사용을 위한 인증 방식이 두 가지 있는데 ‘아포스티유’와 ‘영사확인’이다. 영사확인이 일반적인 절차이고 아포스티유는 협약에 따라 영사확인 절차를 보다 보다 간소화 한 것이다.
프로젝트의 목표는 이 두 문서를 암호화 하고 변조 여부를 확인할 수 있는 시스템을 구축하는 것이었다.
아포스티유와 영사확인 문서는 항목이나 구조가 유사했기 때문에 기존에 구축된 서비스도 하나의 테이블을 공유하고 있었다.
4.1. 초기 설계
기존 시스템을 분석하는 과정에서 나는 영사확인과 아포스티유가 비슷해 보이는 것은 우연일 뿐이며 동일한 문서로 취급하면 안 될 것처럼 보였다. 만약 같은 문서라면 프로젝트 이름이 ‘아포스티유 & 영사확인’은 아니었을 것이다.
그에 반해서 back-end 담당자는 두 개로 분리할 필요가 없다는 주장을 하고 있었다. 결국 타협점으로 REST API만 두 개로 분리하고 테이블 등은 하나로 구현하기로 했다.
4.2. 설계 변경
그런데 프로젝트가 진행되면서 두 문서의 차이가 구체화 되기 시작했다. 아포스티유와 영사확인의 문서번호가 중복될 수 있어서 문서번호 체계가 달라졌다. 그리고 서비스 기능이 확장 되면서 두 문서의 인터페이스는 점점 달라졌다.
결국 테이블을 둘로 나누고 내부 구조도 분리하기로 결정했다. 다행스럽게도 외부에 노출되는 API는 두 개로 분리되어 있었기 때문에 내부 구조를 변경하는 것은 비교적 수월했다. 만약 분리하는 것이 부담스러워서 리팩토링을 피하려고 했다면 코드 곳곳에 if-else가 넘쳐나고 지옥으로 가는 문이 열렸을 것이다.
4.3. 이런 일이 발생한 이유
이 사례에서 두 문서의 형식이 같았던 것은 그저 우연일 뿐이었다. 사용자의 요구에 따라서 얼마든지 달라질 가능성이 있었다. 애초에 다른 문서이기 때문에 다른 이름이 붙은 것이라는 사실을 간과한 것이 문제였다.
프로그래머는 종종 구현 편의성을 우선하는 경향이 있다. 그 습관을 버리기가 쉽지 않을 것이다. 그러나 철저하게 도메인의 개념을 따라야 한다.
5. 인코딩된 파일명의 저장
사용자가 웹 브라우저로 한글.txt
파일을 업로드 하려고 한다.
사용자가 업로드 하는 파일명에 특수문자가 포함되어 있어서 URL encoding을 해서 서버에 전송해야 한다. 마찬가지로 사용자가 파일을 다운로드 받으려면 파일명을 URL encoding 해야 한다.
그렇다면 서버는 DB에 인코딩된 문자열(%ED%95%9C%EA%B8%80.txt
)을 그대로 저장하는 것이 좋을까? 아니면 이것을 다시 디코딩해서 한글.txt
로 저장하는 것이 좋을까?
한글.txt
로 저장하면 사용자에게 파일을 전송할 때 다시 인코딩 해야 한다. 그렇다면 그냥 받은 그대로 %ED%95%9C%EA%B8%80.txt
으로 저장하는 것이 효율적이지 않을까?
본질이 무엇인지 알기 위해서 사용자의 생각을 살펴봐야 한다. 사용자가 업로드 한 파일명은 한글.txt
이다. 사용자는 이것이 변환된다고 생각하지 않은다. 그러니까 저장할 때도 사용자의 생각에 맞춰서 한글.txt
으로 저장하는 것이 옳다.
애초에 URL Encoding은 ASCII 문자 집합의 제한 때문에 필요한 것이지, 사용자의 요구사항이 아니다. 특정 기술의 한계 혹은 특성이 다른 영역에 영향을 주는 것은 좋은 구조가 아니다. 그러니까 HTML의 전송 과정에서 발생하는 기술적인 문제는 그 과정에서 해결해야 하는 것이지 그것을 DB까지 가져오면 두 개의 큰 영역이 서로 강하게 결합되는 안티-패턴이 된다. 사용자의 의도를 더 정확하게 반영하는 것이 우선이고 최적화는 그 다음이다.
다운로드 기능만을 고려한다면 받은 그대로 저장하는 것이 최선의 선택일 것이다. 그러나 기능이 확장되면서 파일 목록을 보여주거나 검색을 허용할 때는 원본 문자열(한글.txt
)이 필요할 것이다. 왜냐하면 사용자는 파일명을 한글.txt
으로 생각하기 때문이다. 그런데 저장을 %ED%95%9C%EA%B8%80.txt
으로 한다면 조회나 검색 기능을 구현할 때 어려움을 겪을 것이다.
구현 편의성을 추구하면 이렇게 작은 변화에도 쉽게 흔들리게 되는 것이다. 반대로 본질을 파악해서 구현하면 예상하지 못한 변화에도 보다 쉽게 대응할 수 있다.
// 인코딩 된 파일명을 보여줄 것인가?
%ED%95%9C%EA%B8%80.txt
%0A%0A%ED%85%8C%EC%8A%A4%ED%8A%B8.jpg
%0A%0A%ED%8C%8C%EC%9D%BC.json
// 원본 파일명을 보여줄 것인가?
한글.txt
테스트.jpg
파일.json
6. 본질에 기반한 기능 정의
영화 예매 서비스를 개발한다고 할 때 여기에 장바구니 기능을 구현해야 할까? 전자상거래 서비스에서는 장바구니가 필수적인 기능이지만, 영화 예매 서비스에서도 꼭 필요할까?
이 질문에 답하기 위해서는 실제 영화 예매 프로세스에서의 사용자 경험을 생각해 봐야 한다. 일반적으로 사용자가 영화를 예매할 때는 다음과 같은 순서로 진행된다.
- 영화 선택
- 상영 시간 선택
- 좌석 선택
- 결제
이 과정에서 우리는 “장바구니에 담기”와 같은 중간 단계를 발견할 수 없다. 사용자는 영화, 시간, 좌석을 선택하고 바로 결제를 진행한다.
그렇다면 온라인 영화 예매 서비스에 장바구니 기능을 추가하는 것은 사용자의 실제 경험과는 거리가 먼 결정이 될 수 있다. 오히려 불필요한 복잡성을 야기하고, 온라인과 오프라인에서의 경험 간 일관성을 해칠 수 있다.
물론 “나중에 결제하기 위해 선택한 영화를 저장해 두는 기능이 있으면 좋겠다”라는 사용자 요구사항이 있을 수 있다. 하지만 이 경우에도 “장바구니”라는 개념을 그대로 가져오기보다는, 영화 예매 서비스의 맥락에 맞는 개념으로 재해석할 필요가 있다. 예를 들어, “관심 영화 저장” 또는 “예매 내역 저장” 등의 기능으로 제공하는 것이 더 적절할 수 있다.
7. 결론
지금까지 살펴본 사례들이 보여준 공통점은 ‘무엇(what)’이 아니라 ‘왜(why)’에 집중한다는 것이다. ‘무엇(what)’은 ‘왜(why)’에 도달하기 위한 방법 중에 하나일 뿐이다. 목적(why)은 쉽게 바뀌지 않지만 방법(what)은 여러 상황에 따라서 얼마든지 바뀔 수 있다.
‘왜(why)’에 집중해야 하는 다른 중요한 이유는 분석 단계에서 사용자의 모든 생각을 문서로 정리할 수 없기 때문이다. 이것은 설계도 마찬가지인데 설계 단계에서 설계자의 모든 생각을 정리할 수 없다. 모든 요구사항과 설계를 최대한 반영한 것이 코드이기 때문이다. 어느 정도 빈틈이 있을 수 밖에 없고 그 빈틈은 굳이 말하지 않아도 알 것이라고 생각하는 것들이다. 문제는 사용자가 당연하게 생각하는 것들을 개발자는 전혀 다르게 받아들일 수 있다는 것이다.
그런데 ‘왜(why)’에 집중해서 사고하면 결국 같은 곳을 바라보기 때문에 의사소통에 다소의 빈틈이 있더라도 그 오차가 크지 않다. 이렇게 사용자와 개발자 간에 발생할 수 있는 생각의 차이를 줄이는 것이 본질 기반 해석(EBI)
의 중요한 역할 중 하나이다.
본질 기반 해석(Essence-Based Interpretation, EBI)
은 너무 당연하고 원론적인 개념이라서 대상 범위나 구체적인 실천 방법을 정의하기가 어렵다. 그리고 개발에만 국한되는 것도 아니다.
본질 기반 해석(EBI)
은 도메인에 기반해야 한다는 점에서 도메인 주도 설계(DDD)
과 비슷한 맥락을 가진다. 그러나 DDD는 보다 체계적이고 구체적인 설계 방법론으로 복잡하거나 변화가 잦은 도메인에 대응하는 것이 주 목적이다. 반면에 본질 기반 해석(EBI)은 구체적인 방법론이라기보다는, 소프트웨어 개발을 포함한 다양한 분야에 적용할 수 있는 일반적인 사고방식이라 할 수 있다. 정리하면 도메인 주도 설계(DDD)
이 요구사항이 무엇인지를 파악하는 것에 주목하는 것이라면 본질 기반 해석(EBI)
은 요구사항을 정의한 이유가 무엇인지를 통찰하려는 노력이다.
본질 기반 해석(EBI)
은 알고보면 당연한 이야기처럼 들린다. 그래서 굳이 본질 기반 해석(EBI)
이라는 거창한 이름으로 정의하려니 좀 창피하기도 하다. 그러나 나를 포함한 많은 개발자들이 이 개념의 존재를 보다 분명하게 인지하는데 도움이 되기를 바라는 마음으로 본질 기반 해석(EBI)
을 정의하고자 한다.
좋은 코드에 대한 고민은 사고방식의 변화를 가져오며, 이는 본질을 통찰하여 예측 불가능한 변화에 대응하는 전략으로 이어진다. 이는 소프트웨어 개발 특유의 소중한 도전이다.