지속 성장 가능한 소프트웨어를 만들어가는 방법

Gemini Kim
20 min readApr 18, 2021

--

스프링은 국내에서 정말 많이 쓰이고 있습니다, 개인적으로 많은 회사를 다녀보며 주니어/시니어를 막론하고 많은 분들이 스프링에 함몰되어 개발을 하고 있다는 느낌을 받을 때가 많았고 이 점이 항상 아쉬웠습니다.

최근 개발자들 교육을 위해 정리해놓은 자료가 있는데 일부 내용을 글로 옮겨 적어봅니다.

물론, 이 글 또한 완벽한 정답이 아니므로 한 방향성이라고봐주시면 감사하겠습니다.

수많은 회사와 시스템이 차세대라는 명목의 개발을 수없이 진행합니다.
그리고 특정 영역은 이 주기가 매우 빠릅니다. 차세대 -> 초기 장애 -> 안정화 -> 차세대 이런 패턴을 보여줍니다.

저는 이것이 많은 낭비를 일으킨다 생각합니다, 금전적인 낭비거나, 누군가의 인생이 낭비될 수도 있다고 생각합니다.

저는 지속 성장 가능한 소프트웨어를 개발하는데 관심이 정말 많습니다. 그래서 아직도 소프트웨어를 지속 성장시키기 위해 많은 노력과 수련을 하고 있습니다.

퇴사한 지 꽤 된 회사에서 아직도 제가 만든 코드들이 유지 보수되며 돌아가고 있다는 것을 듣게 될 때 정말 기분이 좋습니다.

어쨌든 지속 성장 가능한 소프트웨어를 위해 얘기해볼 핵심 주제는 3가지입니다.

첫 번째로 제가 추구하는 비즈니스 로직의 의미와 코드로 어떻게 표현하는지

두 번째로 소프트웨어의 레이어를 어떻게 관리하고 만들어나가고 있는지

마지막으로 어떤 관점으로 모듈화를 진행하고 어떤 방식으로 모듈을 확장시켜나가는지 얘기해 보려 합니다.

첫 번째 주제는 비즈니스 로직입니다 많이 들어본 말인데 대체 비즈니스 로직은 무엇을 의미하는 걸까요?

제가 생각하는 비즈니스 로직에 대해서 알아보기에 앞서서

홍길동이 만든 소프트웨어를 예제로 우리가 이미 보았거나 볼 수 있는 소프트웨어의 구조와 코드를 살펴보겠습니다.

홍길동은 스프링을 사용하여 API 서버를 구성했습니다.

코드의 호출 구조가 Controller, Service, Repository로 되어있네요

Controller가 요청을 받고, Service가 처리하면서, Repository를 통해서 데이터를 가져오는 흐름인 것 같습니다.

그렇다면 이 구조에서 비즈니스 로직은 어디에 작성될까요?

비즈니스 로직은 기존의 관습 또는 관성에 의해 Service 단에 작성됩니다.

이런 구조를 가지고 있다면 저희가 보게 되는 코드는 어떤 형태일까요?

정확히 어떤 기능을 하는지 아직 모르겠지만 이런 코드를 볼 수 있을 것 같습니다. 한 부분씩 살펴보겠습니다.

주황색 부분은 다양한 Repository들을 주입받고 있습니다,
빨간색 부분은 어떤 기능에 대해 로직을 가지고 있고요.
분홍색 부분빨간색에서 사용하는 여러 함수들로 구성되어 있습니다.

그렇다면 이 서비스가 가지는 비즈니스 로직의 의미는 어떤 것일까요?

좀 더 이해해 보기 위해 빨간 부분의 코드를 자세히 보겠습니다.

코드를 빠르게 훑어보면.. businessPay라는 함수의 인자로 리퀘스트 객체와 유저 아이디를 받고 있습니다.
그 밑에는 user를 가져오고, store도 가져와서 비교하여 검증을 진행합니다.
이어서 storeItem을 가져와서 request와 비교하는 로직이 있고 그 후 storeItem.type을 기준으로 개별 로직을 수행하는 것 같아 보입니다.
다음으로 user.id를 통해 point를 가져와서 차감하는 행위를 하고 있습니다.
조회된 point가 없으면 신규로 만드는 부분도 있네요.
마지막으로 payment를 생성해서 저장하고 해당 함수가 끝납니다.

그럼 이제 이 함수가 의미하는 비즈니스 로직은 무엇일까요?

제가 느끼기엔 비즈니스 로직보다는 상세한 구현 로직에 가깝다고 느껴집니다.

Service가 데이터를 직접 조회해서 검증하고 payment를 만들거나 point를 만들고 차감하는 행위까지 갖고 있는 것을 보니 이 Service가 하는 일이 너무 많은 것 같습니다.

신규 입사자가 왔을 때 이 코드를 기준으로 비즈니스 로직을 설명하여 이해시키고 업무에 쉽게 적응하도록 도울 수 있을까요?

저는 아쉬운 부분이 있다고 생각합니다.

물론 해당 코드는 예시로 작성된 코드이기 때문에 근거가 약할 수 있습니다.
그러나 우리가 현실에서 보는 더 복잡한 비즈니스 로직이 이런 식으로 작성되어 있다면 많은 어려움을 가져다줄 것입니다.

그렇다면 제가 추구하고 말하고자 하는 비즈니스 로직은 무엇이고 그리고 어떤 식으로 개발하고 있을까요?

이제 제가 생각하는 비즈니스 로직에 대해 코드를 보면서 얘기해보겠습니다.

제가 생각하는 지속 성장 가능한 소프트웨어의 비즈니스 로직의 의미를 투영하는 코드는 이런 모습입니다.

방금 전 홍길동의 코드와 완벽히 동일한 동작을 하는 코드입니다.

이 코드 또한 상세 구현 로직을 알 수는 없지만 흐름에 대해서 약간의 추측이 가능할 것 같습니다.

한 줄씩 빠르게 살펴보겠습니다.

먼저로 함수 인자부터 바뀐 부분이 있습니다 requset를 받아서 사용하지 않고 더 목적이 뚜렷한 형태(targetStore, usePoint)로 받고 있는 부분이 있습니다.
첫 번째user를 가져오고
두 번째store를 가져옵니다. 이때 user.type으로 StoreGrade라는 객체를 만들어 넘기고 있습니다.
(아마도 유저 타입별로 접근 가능한 스토어의 권한 정보로 추측됩니다.)
세 번째user의 포인트를 usePoint 기준으로 사용합니다
마지막으로 userstore로 결제 정보를 추가합니다.

이렇게 추측을 동반하여 빠르게 처리 흐름을 살펴보았습니다,
물론 상세 구현 로직은 각 클래스에서 확인이 필요해 보입니다.

긍정적인 부분은 이 서비스가 가진 기능의 흐름을 유추할 수 있게 되었습니다. 또한 서비스 자체가 하는 일이 매우 줄어들었습니다

데이터를 직접 조회해서 검증하고 payment를 만들거나 point를 만들고 차감하는 행위를 하지 않고 로직도 갖고 있지 않습니다.

비즈니스 흐름 기준으로 각 역할을 가진 협력 도구 클래스들을 중계해 주는 역할을 하고 있습니다.

또한 각 협력 도구 클래스들이 명시적으로 한 가지 일을 담당하는 것이 잘 나타나고 있습니다.

제가 생각하는 지속 성장 가능한 소프트웨어의 올바른 비즈니스 로직을 한 문장으로 정리하자면 아래와 같습니다.

상세 구현 로직은 잘 모르더라도 비즈니스의 흐름은 이해 가능한 로직이어야 한다.

신규 개발자 외에도 사업 담당자나 영업 담당자에게까지 코드를 보면서 `대충 이런 흐름이다`라고 설명이 가능한 수준이면 가장 이상적일 것 같습니다.

이제 다음 주제를 살펴보겠습니다.

지속 성장 가능한 소프트웨어를 위해서 중요한 또 한 가지 바로 소프트웨어 레이어입니다.

팀 or 파트 단위로 각자 프로젝트 or 제품에 맞는 적절한 레이어를 규정짓고 개발을 진행하면서 확장 또는 축소해가면서 코드의 통제와 제어권을 갖게 된다면 더 빨리, 더 많이, 더 오래 가치를 만들 수 있다고 생각합니다.

위의 레이어는 제가 특이사항 없을 때 일반적으로 정의하는 기본 레이어입니다.
한 단계 씩 살펴보겠습니다.

Presentation Layer는 외부 변화에 민감한, 외부 의존성이 높은 영역입니다.
외부 영역에 대한 처리를 담당하는 코드나 요청, 응답 클래스들도 이
레이어에 속합니다.

Business Layer비즈니스 로직을 투영하는 레이어입니다.
만약 코드가 계속 성장하여
비즈니스 로직이 너무 많아지거나 결합이 되어야 하는 경우 당연히 상위 레이어를 더 쌓아올립니다.

Implement Layer는 위의 예제에서 봤던 비즈니스 로직을 이루기 위해 도구로서 상세 구현 로직을 갖고 있는 클래스들이 있습니다.
이곳은 가장 많은 클래스들이 존재하고 있으면서 구현 로직을 담당하기 때문에 재사용성도 높은
핵심 레이어입니다.

Data Access Layer는 상세 구현 로직이 다양한 자원에 접근할 수 있는 기능을 제공하는 레이어입니다.
특징으로는 기술 의존성을 격리하여 구현 로직에게 순수한 인터페이스를 제공합니다.
이 점을 극대화하기 위해 일반적으로 별도의
모듈로 구성되어 제공됩니다.

레이어는 단순히 구성으로 끝나는 것이 아니라 적절한 제약을 통한 통제가 필요합니다.
제가 일반적으로 기본 레이어를 통제하기 위해 걸어 놓는 4가지 규칙을 살펴보겠습니다.

첫 번째 규칙
레이어는 위에서 아래로 순방향으로만 참조 되어야 한다

아주 단순하고 기본적이면서도 핵심적인 규칙입니다.

두 번째 규칙
레이어의 참조 방향이 역류 되지 않아야한다.

코드로 얘기해 보면 UserServiceUserController 를 갖고 있는 것은 당연히 일반적이지 않습니다.
Implement LayerUserReader라는 클래스와 Business LayerUserService가 있다면
UserReaderUserService를 알고 있으면 안 된다는 규칙입니다.

세 번째 규칙
레이어의 참조가 하위 레이어를 건너 뛰지 않아야 한다.

우리가 앞에서 예제로 본 홍길동의 코드에서도 Service에서 많은 Repository를 알고 있습니다.

이는 Business Layer상세 구현 로직구현 기술에 대해 너무 자세히 알고 있는 형태가 돼버립니다.
이러한 형태는 소프트웨어를 오랫동안 유지 및 확장 시키려 할 때 어느 순간 걸림돌이 됩니다.

그런 문제를 방지하기 위해 Implement LayerData Access Layer를 이용하여 상세 구현 로직을 풀어내고, Business LayerImplement Layer를 사용하여 비즈니스 로직을 풀어가는 형태가 됩니다.

비즈니스를 담당하는 영역이 구현 기술이나 구현 로직을 모르고 있는 형태로 유지하고 레이어의 오염을 막기 위한 규칙입니다.

네 번째 규칙
동일 레이어 간에는 서로 참조하지 않아야 한다.
(다만, Implement Layer는 예외적으로 서로 참조가 가능합니다.)

이 규칙은 Implement Layer 클래스들의 재사용성을 늘리고
협력이 가능한 높은 완결성의 도구 클래스들을 더 많이 만들게 합니다.

또한, 비즈니스 로직의 오염을 막기 위한 규칙이기도 하고 잘 만들어진 구현체의 재사용을 유도하기 위해 이런 규칙을 가지고 있습니다.

여기까지 지속 성장 가능한 소프트웨어를 위해 제가 일반적으로 레이어에 걸어놓는 몇 가지 핵심 규칙을 알아봤습니다.

개인적으로 가장 중요한 핵심 규칙은 내부 개발자들의 창의성과 생각의 자유를 뺏지 않고 개방적인 표준을 권장하는 것입니다.

제가 근무하고 있는 곳에서는 표준 레이어를 정의하고, 표준에서 변경이나 확장을 한 팀은 README.md에 작성하여 관리하는 것을 권장하고 있습니다.

이를 통해 신규 개발자들이 빠르게 적응하고 업무에 녹아들 수 있다는 것을 여러 번 경험해봤고, 이런 내용을 기반으로 자유롭게 토론해가며 더욱 탄탄하게 정리해가고 있습니다.

이제 소프트웨어 레이어와 밀접한 관계가 있는 모듈에 대해서 얘기해보겠습니다.

지속 가능한 소프트웨어를 만들기 위해서 적절한 모듈화는 정말 중요한 역할을 합니다.
그리고 모듈 간의 격리를 이용하여 소프트웨어를 강력하게 통제하고 제어권을 얻기도 합니다.

모듈 관련해서도 홍길동예제 소프트웨어를 보면서 얘기해보겠습니다.

홍길동은 처음에 단순한 소프트웨어를 만들어서 서비스를 시작합니다,
WEB 과 JPA 의존성으로 API Server 모듈을 구성한 것 같습니다.

그러나 점점 고객도 많아지고 사업의 요구 사항도 늘어납니다 그에 따라 소프트웨어도 같이 확장될 것입니다.

홍길동성장한 소프트웨어는 이런 모습을 가지게 됩니다.

여러 요구 사항이 많아지다 보니 다수의 의존성이 추가되었고 API Server 모듈 이 꽤 커졌습니다.
(외부 의존성 때문에 모듈이 오염되지 않았기를 바라봅니다.)

오염 사례로는 비즈니스나 상세 구현 로직에서 외부 의존성의 클래스를 범용적으로 재사용 하거나, enum class를 직접적으로 사용하는 등 다양한 사례가 있을 것 같습니다.

이런 상황에서 Batch를 구성하여 개발해야 하는 요구 사항이 생겼습니다 그렇게 된다면 홍길동은 어떤 형태로 확장을 시킬까요?

홍길동은 가장 쉬운 방법을 선택한 것 같습니다. 일단 의존성을 그대로 가져가서 Batch 모듈을 구성한 것 같네요

당장의 요구 사항과 사업 확장에 대응하였으니 핵심 목적은 이뤘다고 볼 수도 있습니다.

그런데 시간이 흘러서 어느 날 더 좋은 성능을 내기 위해 기술을 변경해야 하는 상황이 생겼습니다.

예를 들어 JPA를 들어내고 더 강력한 것을 도입하려 합니다.

그런데 만약 홍길동API Server 모듈 이 이미 JPA 와 강력하게 결합되어 있다면 쉽게 JPA를 걷어 낼 수 없을 것입니다.

물론 천천히 작업하면 가능도 하겠지만 그 과정 속에서 비즈니스 로직에 영향이 있거나 상세 구현에 영향을 끼칠 수 있습니다.

한 모듈이 너무 많은 의존성 덩어리들에 파묻히게 된 상황입니다. 그렇다고 그대로 두면 점점 더 커질 것이고, 어느 순간 감당이 안 돼서 결국 차세대 시스템을 개발해야 할 수도 있습니다.

말 그대로 의존성에 발목이 잡혀버린 상황입니다 예제로 JPA를 들었지만 어떤 의존성이든 이런 상황은 올수 있습니다.

그렇다면 지속 성장 가능한 소프트웨어를 위해서 저는 어떤 식으로 모듈을 구성하는지 얘기해보겠습니다.

제가 모듈을 구성하는 과정입니다. 우선 초기 모듈 구성을 위해 Cloud Config, Logging 같은 기본적인 의존성도 모듈로 만들어 의존하도록 구성합니다.

기본 구성이 완료되었고, 이제 개발 요구 사항이 생겼습니다.

첫 요구 사항은 국세청 API 연동입니다. 요구 사항에 따라 모듈이 어떻게 확장되는지 보겠습니다.

국세청과 통신하기 위해서 NTS-API Client라는 모듈을 구성하여 외부 통신 부분별도 모듈로 추가한 형태가 되었습니다.

다음으로 국세청 API 결과 데이터를 저장해야 하는 요구 사항이 생겼습니다. 어떻게 확장되는지 보겠습니다.

데이터베이스에 접근하는 것도 별도의 DB 모듈로 구성했습니다.

1차 요구 사항 구현이 끝났습니다. 그럼에도 Payments API 모듈의 크기는 그대로입니다.

이쯤에서 모듈 구성도 및 의존성을 보겠습니다.

우선 예제를 쉽게 보기 위해서 실행 불가능한 모듈modules 폴더에 모았습니다.
(실제로는 좀 더 의미 중심으로 모듈을 그룹으로 구성합니다.)

의존성 구성을 보면 JPA를 쓰는지 다른 기술을 쓰는지 실제 구현 기술에 대한 정보를 Payments API 모듈이 신경 쓰지 않고 있습니다.

국세청 API에 대해서도 단순히 Apache Http Component 같은 의존성을 참조하여 Payments API 모듈이 구현체를 갖고 있는 게 아니라, client-nts-api 모듈을 통해서 사용하는 구성을 보여주고 있습니다.

사실, 외부 API 통신에 대해서 HttpClientFeign을 쓰는지는 Payments API 모듈의 관심사가 아닙니다.

결과적으로 모듈의존성 설정을 보면 이 모듈이 사용하는 기술보다는 사용하는 개념에 초점이 맞춰져 있습니다.

이렇게 구성된 상태에서, 저도 Batch를 추가해야 하는 요구 사항이 생겼습니다.

어떻게 구성될까요?

Batch에 대해서도 모듈로 구성됩니다. Payments Batch 모듈 또한 기본적으로 사용해야 하는 Cloud Config, Logging에 대해서 의존하는 형태가 됩니다.

전반적으로 추가 의존성이 중복해서 늘어나지 않고 기존 모듈재사용 되는 것을 볼 수 있습니다.

다음 요구 사항으로 Payments Batch 모듈국세청 SFTP와 붙어야 하는 요구 사항이 생겼습니다.

Payments Batch 모듈비즈니스 로직에 집중하고 국세청 SFTP와 데이터를 주고받는 상세 로직이나 구현 기술은 NTS-SFTP Client 모듈에게 위임하는 구조가 되었습니다.

즉, Payments Batch 모듈은 정확히 어떤 기술로 어떻게 SFTP 데이터를 주고받는지 모르고 있고, 관심도 없고, 신경 쓰지 않아도 됩니다.

SFTP 데이터를 주고받는 것은 NTS-SFTP Client책임지고해주고 있는 것이죠.

좀 더 자세하게 알아보기 위해 이 구조에 대한 모듈 구성도 및 의존성을 보겠습니다.

client-nts-sftp 모듈이 추가되었고, Payments Batch 모듈이 이를 의존하도록 구성되어 있습니다.

이번에도 의존성 구성을 봐도 client-nts-sftp 모듈이 어떤 구현체로 되어 있는지 알 수가 없습니다.

여기서 잠깐 알아보고 갈 내용이 있습니다, 모듈 간 의존성을 어떤 방식으로 구성하고 있기에 구현기술의 상세 내용을 모를 수 있을까요?

Gradle의 의존성 구성 키워드 중 apiimplementation 이 어떤 차이가 있는지 현재 상황을 기준으로 알아보겠습니다.

Payments Batch 모듈NTS-SFTP Client 모듈을 의존하고 있습니다.

그리고 NTS-SFTP Client 모듈Spring Integration SFTP를 의존하고 있습니다.

이때 implementation 통해서 의존하고 있습니다.
현재 상황을 기준으로 implementation 키워드를 설명해보면

SFTP Client 모듈Spring Integration SFTPimplementation으로 의존하고 있기 때문에 Batch 모듈Spring Integration SFTP의 존재를 모르고 있고 관련 클래스 참조도할 수 없습니다.

Payments Batch 모듈상세 구현 기술에 접근할 수 없다는 의미이고 NTS-SFTP Client 모듈을 이용해서만 SFTP 사용이 가능하다는 것입니다.

그렇다면 api 키워드는 어떻게 동작할까요?

만약 이렇게 NTS-SFTP Client 모듈Spring Integration SFTPapi로 의존하도록 구성한다면

Batch 모듈Spring Integration SFTP클래스하위 의존성까지 참조 할수 있게 됩니다.
의존성 참조 범위가 늘어나고 구현 기술에 대한 의존성이 모두 Batch 모듈에게 전파되는 것입니다.

의존성 전파가 된 후에는 개발자의 실수로 상위 모듈 오염이 발생할 수 있습니다.
즉, api 키워드로 인해 한순간에 레이어의 오염 또한 가능해지고, 다양한 실수의 가능성이 매우 높아진 것입니다.

그래서 저의 경우는 최대한 implementation을 사용하는 방식으로 모듈을 확장하고 있습니다.

요구 사항에 따라 성장하여 이런 구성이 완성되었습니다.

물론 예제 기반이라서 상세한 부분을 모두 설명할 순 없지만 홍길동의 API 모듈과 비교해 보면 구성이 확실히 달라 보입니다.

그럼 여기서 아까 홍길동의 API 모듈에서 발생했던 기술 변경 문제를 다시 얘기해보겠습니다.

더 좋은 성능을 내기 위해 데이터베이스에 접근하는 기술 변경해야 하는 상황이 생겼습니다. 이번에는 어떻게 진행될까요?

우선 모듈 구조를 더 자세히 보겠습니다.

확인해보니 다행히 DB 모듈은 JPA를 implementation 키워드로 의존하고 있습니다.

다행히 JPA 의존성이 Payments API 모듈까지 전파되지 않았다는 것입니다.

Payments API 모듈은 애초에 자기가 JPA를 사용하는지 모르는 구조로 개발된 것입니다.

이를 통해서 우리는 JPA 와 Payments API 모듈이 결합되어 있지 않다는 것을 확인하였고, JPA를 걷어내는 주요 작업은 DB 모듈에서만 진행하면 되는 것을 알게 되었습니다.

이렇게 JPA에서 new-super-persistence-api로 변경이 됩니다.
물론 DB 모듈 자체의 코드 변경 가능성은 충분히 있습니다.

그러나 DB 모듈이 제공하고 있던 Interface 들에는 전혀 변화가 없을 것입니다. 온전히 내부 구현변경되었다는 얘기입니다.

작업 이후에도 Payments API 모듈은 데이터베이스 접근 기술에 의존성이 없는 상태로 유지되고 있습니다.
또한, 계속 어떤 기술을 쓰는지 알 필요도 없고 코드 수정 없이 자기가 가진 비즈니스 로직을 수행할 것입니다.

결과적으로 이런 구조를 통하여 하위 기술 모듈이 변경되더라도 상위 모듈에게는 변경이나 영향을 주지 않는 상태를 지속할 수 있는 것입니다.

앞으로도 새로운 기술 또는 기술의 전환이 필요할 때 비교적 쉽고 빠르게 기존 비즈니스에 가장 적은 영향을 끼치며 모듈 단위로 변경이 가능해진 것입니다.

여기까지가 제가 지속 성장 가능한 소프트웨어를 위해 생각하고 추구하는 모듈 설계 및 확장에 대한 이야기였습니다.

모든 주제에 대해서 알아보았는데요, 그렇다면 저는 왜 이런 생각규칙을 추구하는 걸까요?

그것은 제가 지속 성장 가능한 소프트웨어를 만들어가기 위해 두 가지 핵심 키워드에 기반하여 생각하고 계속 고민해나가고 있어서 그렇습니다.

첫 번째는 통제입니다. 저는 소프트웨어를 완벽히 통제하고 싶습니다.
이것은 언제나 회사와 저에게 가장 큰 가치를 가져와 줍니다.
이 글의 핵심 주제였던 비즈니스로 로직, 레이어, 모듈 내용 속에 숨겨서 가장 얘기하고 싶었던 핵심 키워드입니다.

저는 비즈니스 로직을 가시화하고, 레이어를 탄탄히 다지며, 제약을 걸고 적절한 모듈화로 기술을 격리하고
이를 통해 다양한 변화에 대응할 수 있는 통제된 상태유기적인 소프트웨어를 만드는 것을 언제나 중요하게 생각합니다.

이러한 통제를 기반으로 저는 소프트웨어를 예측 가능하게 하고 자유자재로 제어하기를 원합니다.

회사 or 프로젝트에 따라 다르겠지만 우리가 만들어가는 소프트웨어는 기본적으로 지속 성장하면서 운영 가능해야 한다고 생각합니다.

지금 우리가 만들고 있는 소프트웨어가 매우 긴 수명을 갖고 있을 수도 있습니다.

소프트웨어가 언제 어떻게 커지거나 죽을지 모르기 때문에, 항상 지속 성장시켜 나가기 위한 준비를 하고 있어야 한다고 생각합니다.
그러기 위해서 저는 언제나 소프트웨어의 통제권을 얻어서 제어할 수 있어야 한다고 생각합니다.

길고 장황한 글 여기까지 읽어주셔서 감사드립니다. :D

지금도 소프트웨어를 성장시키기 위해 많은 고민과 고생을 하고 계신 모든 분들에게 이 글을 바칩니다.

--

--

Responses (4)