Skip to main content
Summary

설계는 거창한 문서가 아니라 선택이다

소프트웨어 설계는 그림을 예쁘게 그리는 일이 아니다. 코드를 어디에 둘지, 어떤 이름으로 나눌지, 무엇이 무엇을 알아도 되는지 정하는 일이다. 좋은 설계는 지금 기능을 빨리 만드는 것뿐 아니라, 다음 변경이 왔을 때 덜 흔들리게 만든다.

예를 들어 쇼핑몰에 주문 취소 기능을 만든다고 하자. 처음에는 단순히 주문 상태를 CANCELED로 바꾸면 될 것 같다. 하지만 실제로는 결제 취소, 재고 복구, 쿠폰 복원, 알림 발송, 취소 가능 시간 확인이 함께 따라온다. 이 모든 로직을 하나의 함수에 몰아넣으면 처음에는 빠르다. 그러나 결제 방식이 추가되거나 일부 상품만 취소하는 기능이 생기면 작은 수정도 위험해진다.

설계의 핵심 질문은 다음과 같다.

  • 이 코드는 어떤 책임을 가져야 하는가?

  • 변경될 가능성이 높은 부분은 어디인가?

  • 서로 몰라도 되는 것들이 서로를 알고 있지는 않은가?

  • 테스트하려면 실제 데이터베이스나 외부 API가 꼭 필요한가?

이 질문에 답하며 코드를 나누는 과정이 설계다.

소프트웨어 설계 원칙과 아키텍처 사고AI 생성 이미지

응집도와 결합도로 모듈을 판단하기

모듈은 함께 이해하고, 함께 수정하고, 함께 테스트할 수 있는 코드의 묶음이다. 함수, 클래스, 패키지, 서비스 모두 모듈이 될 수 있다. 모듈을 나눌 때 가장 기본이 되는 기준은 응집도와 결합도다.

응집도는 한 모듈 안의 코드들이 같은 목적을 위해 모여 있는 정도다. 응집도가 높으면 모듈 이름만 보고도 하는 일을 짐작할 수 있다. OrderCancelService가 주문 취소 가능 여부 확인, 결제 취소 요청, 주문 상태 변경을 조율한다면 자연스럽다. 반대로 회원 조회, 파일 업로드, 주문 통계까지 함께 처리한다면 응집도가 낮다.

결합도는 한 모듈이 다른 모듈에 얼마나 강하게 의존하는지를 뜻한다. 결합도가 높으면 한쪽 수정이 다른 쪽 수정으로 쉽게 번진다. 예를 들어 주문 취소 코드가 특정 PG사의 HTTP 요청 형식, 데이터베이스 테이블 구조, 알림 문구까지 직접 알고 있다면 결합도가 높다. PG사를 바꾸거나 알림 정책을 수정할 때 주문 취소 로직까지 흔들린다.

좋은 방향은 높은 응집도와 낮은 결합도다. 관련 있는 것은 모으고, 몰라도 되는 것은 분리한다. 단, 무조건 작게 나누는 것이 좋은 설계는 아니다. 함께 바뀌는 코드를 지나치게 흩어 놓으면 오히려 변경 지점이 늘어난다. 모듈 분리의 기준은 파일 개수가 아니라 변경 이유다.

나쁜 신호
- 하나의 함수가 검증, 계산, 저장, 외부 API 호출, 응답 생성까지 모두 한다.
- 작은 요구사항 변경에 여러 폴더를 동시에 수정해야 한다.
- 테스트를 쓰려면 항상 실제 DB와 외부 서버가 필요하다.
- 모듈 이름은 넓은데 실제 책임은 계속 늘어난다. 예: CommonUtil, Manager, Helper

SOLID는 암기보다 변경 사례로 이해한다

SOLID는 객체지향 설계 원칙으로 알려져 있지만, 핵심은 객체지향 문법이 아니라 변경에 강한 의존 구조를 만드는 데 있다. 약어를 외우는 것보다 어떤 문제가 생길 때 필요한 원칙인지 이해하는 편이 낫다.

단일 책임 원칙은 하나의 모듈이 하나의 변경 이유를 가져야 한다는 뜻이다. 주문서를 PDF로 만들고, 이메일로 보내고, 전송 기록까지 저장하는 클래스가 있다고 하자. PDF 양식 변경, 이메일 서버 변경, 기록 정책 변경은 서로 다른 이유다. 이들을 한 클래스에 묶으면 관련 없는 변경이 서로 영향을 준다. 생성, 발송, 기록을 나누면 각 변경을 독립적으로 다룰 수 있다.

개방-폐쇄 원칙은 기존 코드를 계속 수정하기보다 새 동작을 추가할 수 있게 만들라는 원칙이다. 결제 수단이 카드뿐일 때는 조건문 하나면 충분하다.

if payment.type == CARD:
    cancel_card_payment(payment)
elif payment.type == BANK_TRANSFER:
    cancel_bank_transfer(payment)
elif payment.type == POINT:
    cancel_point(payment)

결제 수단이 계속 늘어난다면 이 조건문은 매번 수정된다. 이때 결제 취소라는 공통 역할을 두고, 카드 취소기와 포인트 취소기를 각각 구현하면 새 결제 수단을 추가할 때 기존 흐름의 수정 범위를 줄일 수 있다. 중요한 것은 조건문을 무조건 없애는 것이 아니라 변경 지점을 좁히는 것이다. 결제 수단이 두세 개로 고정되어 있고 변경 가능성이 낮다면 단순한 조건문이 더 낫다. 원칙은 복잡하게 만들라는 명령이 아니다.

리스코프 치환 원칙은 하위 타입이 상위 타입을 대체해도 프로그램의 기대 동작이 깨지지 않아야 한다는 뜻이다. 조금 더 풀어 말하면, 상위 타입이 약속한 동작을 하위 타입이 깨뜨리면 안 된다는 말이다. Bird에 fly 메서드를 두고 Penguin이 이를 상속받는다면 문제가 생긴다. 펭귄은 새지만 날 수 없다. 이런 구조에서는 상속보다 Flyable 같은 능력 중심의 분리가 더 적절하다.

인터페이스 분리 원칙은 사용하지 않는 기능에 의존하지 말라는 뜻이다. 프린터 기능만 필요한 코드가 스캔, 팩스, 복사 메서드까지 가진 거대한 인터페이스에 의존하면 불필요한 변경 영향을 받는다. 클라이언트가 실제로 쓰는 기능 기준으로 인터페이스를 작게 나누는 편이 안전하다.

의존성 역전 원칙은 중요한 정책 코드가 세부 구현에 직접 의존하지 않도록 하라는 원칙이다. 주문 서비스가 특정 데이터베이스 라이브러리나 특정 메시지 큐 SDK를 직접 호출하면 테스트와 교체가 어려워진다. 대신 OrderRepository, NotificationSender 같은 추상 역할에 의존하고, 실제 구현을 바깥에서 연결하면 핵심 로직은 더 안정적이 된다.

계층형 아키텍처, MVC, 클린 아키텍처

아키텍처는 코드의 큰 배치 방식이다. 목적은 관심사를 분리하고 의존성 방향을 관리하는 데 있다.

계층형 아키텍처는 흔히 프레젠테이션, 애플리케이션, 도메인, 데이터 접근 계층으로 나눈다. 웹 API를 예로 들면 컨트롤러는 HTTP 요청과 응답을 다루고, 애플리케이션 서비스는 업무 흐름을 조율하며, 도메인은 핵심 규칙을 표현하고, 리포지토리는 데이터베이스 접근을 맡는다.

요청 흐름 예시
Controller -> Application Service -> Domain
                           └-> Repository -> Database

여기서 화살표는 사용자의 요청이 처리되는 대표적인 흐름을 단순화한 것이다. 실제 코드의 의존 방향은 팀의 설계 방식에 따라 달라질 수 있다. 중요한 점은 컨트롤러가 데이터베이스 세부 구현을 직접 다루거나, 도메인 규칙이 HTTP 요청 형식에 끌려다니지 않도록 경계를 세우는 것이다.

이 구조의 장점은 이해하기 쉽다는 점이다. 대부분의 웹 서비스에 적용하기 쉽고, 초보자도 요청 흐름을 따라가기 좋다. 단점은 시간이 지나며 서비스 계층이 너무 커지기 쉽다는 것이다. 모든 업무 로직이 서비스에 몰리면 결국 거대한 함수와 비슷한 문제가 생긴다.

MVC는 Model, View, Controller로 관심사를 나누는 방식이다. View는 화면 표현, Controller는 사용자 입력과 요청 흐름, Model은 데이터와 규칙을 담당한다. 서버 렌더링 웹 애플리케이션에서는 MVC가 특히 자연스럽다. API 서버에서도 Controller와 Model의 분리는 유용하지만, View 대신 JSON 응답 형식이 들어가는 경우가 많다.

클린 아키텍처는 핵심 업무 규칙을 프레임워크, 데이터베이스, UI 같은 외부 세부사항으로부터 보호하려는 사고방식이다. 중요한 방향은 안쪽이다. 도메인과 유스케이스는 바깥 기술을 몰라야 한다. 데이터베이스, 웹 프레임워크, 메시지 큐는 바깥쪽 구현으로 둔다.

의존성 방향
외부: Web, DB, Framework
        ↓
중간: Adapter, Repository 구현
        ↓
내부: UseCase, Domain Rule

이 그림에서 아래쪽으로 갈수록 더 핵심에 가까운 코드라고 보면 된다. 의존성은 핵심 규칙을 향해야 한다. 반대로 실제 실행 흐름은 사용자의 HTTP 요청처럼 바깥에서 시작해 안쪽 규칙을 호출한 뒤 다시 바깥으로 응답을 내보낼 수 있다. 의존 방향과 실행 흐름이 항상 같은 것은 아니다.

이렇게 하면 테스트 가능성이 좋아진다. 주문 취소 규칙을 테스트할 때 실제 HTTP 요청이나 실제 데이터베이스가 없어도 된다. 가짜 저장소와 가짜 결제 취소기를 넣어 핵심 규칙만 검증할 수 있다. 반면 구조가 복잡해지고 파일 수가 늘어날 수 있다. 작은 CRUD 서비스에 처음부터 과한 계층을 도입하면 개발 속도만 느려질 수 있다.

아키텍처 선택의 기준은 멋진 이름이 아니다. 변경 가능성, 팀 규모, 테스트 필요성, 도메인 복잡도를 보고 결정해야 한다.

디자인 패턴은 설계의 어휘다

디자인 패턴은 반복해서 등장하는 설계 문제에 붙은 이름이다. 패턴을 많이 아는 것이 곧 좋은 설계를 뜻하지는 않는다. 중요한 것은 지금 코드에 어떤 문제가 있고, 그 문제를 설명하는 데 패턴이 도움이 되는지다.

전략 패턴은 실행 중 선택되는 알고리즘이나 정책을 분리할 때 유용하다. 할인 정책이 정액 할인, 정률 할인, 회원 등급 할인으로 늘어날 때 각 할인 방식을 전략으로 분리할 수 있다. 주문 계산 코드는 할인 정책의 구체적인 조건을 모두 알 필요가 없다.

팩토리 패턴은 객체 생성 규칙이 복잡할 때 유용하다. 결제 타입에 따라 서로 다른 결제 처리기를 만들어야 한다면 생성 책임을 한곳으로 모을 수 있다. 단, 단순히 new 한 번이면 끝나는 상황에 팩토리를 만들면 불필요한 간접 계층이 된다.

어댑터 패턴은 외부 시스템의 인터페이스를 내부에서 쓰기 좋은 형태로 바꿀 때 유용하다. PG사마다 결제 취소 API의 요청 형식이 다르더라도 내부에서는 cancelPayment 같은 동일한 역할로 다루게 만들 수 있다.

패턴을 남용하면 코드가 실제 문제보다 더 어려워진다. 좋은 패턴 사용은 이름을 붙이기 전에 먼저 문제를 단순하게 설명할 수 있어야 한다. 반복되는 조건문이 늘어나는가, 생성 규칙이 흩어졌는가, 외부 API 형식이 내부 로직을 오염시키는가. 이런 문제가 보일 때 패턴을 도구로 사용한다.

모놀리스와 마이크로서비스는 우열이 아니라 선택이다

모놀리스는 하나의 애플리케이션으로 여러 기능을 함께 배포하는 구조다. 회원, 주문, 결제, 관리자 기능이 하나의 코드베이스와 배포 단위에 들어갈 수 있다. 장점은 단순함이다. 로컬 개발, 테스트, 배포, 장애 추적이 비교적 쉽다. 트랜잭션 처리도 단일 데이터베이스 안에서 해결하기 쉽다.

단점은 규모가 커질수록 변경 영향 범위가 넓어진다는 점이다. 작은 기능 수정도 전체 애플리케이션 배포로 이어질 수 있고, 코드 경계가 약하면 여러 팀이 같은 영역을 계속 건드리게 된다. 하지만 이것은 모놀리스 자체의 실패라기보다 내부 모듈 경계가 무너진 결과인 경우가 많다. 잘 나눈 모듈식 모놀리스는 많은 서비스에서 현실적인 선택이다.

마이크로서비스는 기능을 여러 독립 서비스로 나누고, 각 서비스를 독립적으로 개발·배포·확장하는 구조다. 주문 서비스, 결제 서비스, 알림 서비스가 별도 프로세스와 배포 단위로 존재할 수 있다. 장점은 팀별 소유권과 배포 독립성이다. 결제 서비스만 따로 확장하거나 배포할 수 있다.

하지만 운영 복잡도가 크게 늘어난다. 서비스 간 네트워크 호출은 실패할 수 있고, 데이터 정합성을 단일 트랜잭션으로 보장하기 어렵다. 로그, 모니터링, 인증, 배포 파이프라인, 장애 대응 체계가 성숙하지 않으면 마이크로서비스는 문제를 해결하기보다 문제를 분산시킨다.

현실적인 판단 기준은 다음과 같다.

모놀리스가 적합한 경우
- 팀 규모가 작고 기능 변화가 빠르다.
- 도메인 경계가 아직 명확하지 않다.
- 운영 자동화와 모니터링 체계가 충분하지 않다.
- 강한 데이터 정합성이 중요하다.

마이크로서비스를 검토할 수 있는 경우
- 팀별로 명확한 업무 경계와 소유권이 있다.
- 특정 기능만 독립적으로 배포하거나 확장해야 한다.
- 장애 격리와 서비스별 기술 선택의 이점이 크다.
- 관측, 배포, 장애 대응 체계가 준비되어 있다.

먼저 좋은 모놀리스를 만들고, 실제로 분리할 이유가 생겼을 때 나누는 접근이 대체로 안전하다. 처음부터 서비스 이름만 많이 나눈다고 좋은 아키텍처가 되지는 않는다.

좋은 설계는 경계를 계속 조정한다

설계는 한 번에 완성되지 않는다. 처음에는 단순하게 만들고, 변경이 반복되는 지점을 관찰하며 경계를 조정해야 한다. 요구사항이 거의 바뀌지 않는 영역은 단순한 코드가 좋다. 반대로 정책이 자주 바뀌고 테스트가 중요한 영역은 인터페이스와 계층을 둬서 변경 비용을 낮추는 편이 좋다.

실무에서 좋은 설계를 판단하는 기준은 의외로 단순하다.

  • 새 요구사항을 넣을 때 수정 범위가 예측되는가?

  • 핵심 규칙을 독립적으로 테스트할 수 있는가?

  • 모듈 이름이 책임을 잘 설명하는가?

  • 외부 기술 변경이 업무 로직까지 흔들지 않는가?

  • 팀원이 코드를 읽고 변경 이유를 설명할 수 있는가?

설계는 미래를 정확히 맞히는 일이 아니다. 바뀔 수밖에 없는 현실을 받아들이고, 바뀌어도 덜 아프게 만드는 일이다. 응집도, 결합도, SOLID, 아키텍처, 디자인 패턴은 모두 이 목적을 위한 도구다. 도구보다 중요한 것은 책임과 경계를 의식하며 코드를 작성하는 습관이다.

책 목록

개발자에게 필요한 필수 지식 정리

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기