소프트웨어는 코드를 작성한다고 끝나는 작업이 아니다.
기능이 많아지고 사용자가 늘어나고 여러 명이 같은 코드를 수정하기 시작하면 다른 문제가 생긴다. 요구사항이 바뀌고, 버그가 발생하고, 기존 코드에 기능 하나를 추가했더니 전혀 관계없어 보이는 곳에서 문제가 발생하기도 한다.
소프트웨어공학은 이런 문제를 다루기 위한 방법론에 가깝다.
좋은 코드를 작성하는 것만이 아니라 어떻게 요구사항을 정의하고, 설계하고, 구현하고, 테스트하고, 배포하고, 유지보수할 것인가까지 포함한다.
소프트웨어 개발의 전체 흐름
소프트웨어 개발은 보통 다음과 같은 단계로 볼 수 있다.

실제 프로젝트에서는 이 과정이 한 번만 진행되지 않는다.
기능을 배포하고 나면 새로운 요구사항이 생기고, 기존 기능을 수정하고, 다시 테스트하고 배포한다.
그래서 실제 개발은 선형적인 과정이라기보다 반복적인 과정에 가깝다.

요구사항
소프트웨어 개발에서 가장 먼저 해결해야 하는 문제는 “무엇을 만들 것인가”다.
이것을 정의하는 것이 요구사항(Requirement)이다.
요구사항은 크게 기능 요구사항과 비기능 요구사항으로 나눌 수 있다.
기능 요구사항
시스템이 무엇을 해야 하는지에 대한 요구사항이다.
예를 들면
플레이어는 회원가입을 할 수 있어야 한다.
플레이어는 게임을 저장할 수 있어야 한다.
관리자는 유저를 차단할 수 있어야 한다.
같은 것들이다.
비기능 요구사항
시스템이 어떤 특성을 가져야 하는지를 정의한다.
로그인 요청은 1초 이내에 처리되어야 한다.
서버는 동시에 10만 명의 사용자를 처리할 수 있어야 한다.
서비스는 장애 발생 시 데이터를 복구할 수 있어야 한다.
성능, 보안, 확장성, 가용성, 유지보수성 등이 여기에 포함된다.
실제 프로젝트에서는 기능 자체보다 이런 비기능 요구사항 때문에 아키텍처가 크게 달라지기도 한다.
요구사항은 계속 바뀐다
현실에서 요구사항이 처음부터 완벽하게 정의되는 경우는 드물다.
개발을 진행하다 보면 기획이 바뀌고, 사용자의 요구가 달라지고, 기술적인 제약이 발견된다.
그래서 소프트웨어 개발에서 변경을 어떻게 다룰 것인가가 상당히 중요하다.
변경 자체를 막는 것은 현실적으로 어렵다.
대신 변경의 영향을 제한하고 빠르게 대응할 수 있는 구조를 만드는 것이 중요하다.
이것이 이후에 나오는 모듈화, 추상화, 테스트, 버전 관리와 연결된다.
소프트웨어 설계
요구사항을 구현하기 전에 시스템을 어떤 구조로 만들지 결정해야 한다.
예를 들어 게임 서버라면 하나의 클래스에 로그인, 결제, 인벤토리, 전투, 저장 기능을 전부 넣는 것보다 책임을 나누는 편이 관리하기 쉽다.

이런 식으로 기능과 책임을 적절하게 분리하는 것이 설계의 기본적인 방향이다.
설계에서 중요한 것은 클래스나 폴더를 많이 만드는 것이 아니다.
변경될 가능성이 높은 부분과 그렇지 않은 부분을 어떻게 분리할 것인가를 보는 것이 중요하다.
응집도와 결합도
소프트웨어 구조를 평가할 때 자주 사용하는 개념이 응집도(Cohesion)와 결합도(Coupling)다.
응집도는 하나의 모듈 안에 서로 관련된 책임이 얼마나 잘 모여 있는지를 의미한다.
예를 들어
Inventory-> 아이템 추가-> 아이템 제거-> 아이템 검색-> 아이템 정렬
처럼 인벤토리와 관련된 기능이 모여 있다면 응집도가 높다고 볼 수 있다.
반대로 전투 시스템 안에 로그인 처리나 데이터베이스 연결 코드가 같이 들어 있다면 책임이 섞여 있다.
결합도는 모듈 사이의 의존 정도다.
A -> B -> C -> D
처럼 여러 모듈이 강하게 연결되어 있으면 하나를 수정할 때 다른 부분까지 영향을 받을 가능성이 커진다.
일반적으로는
높은 응집도 + 낮은 결합도
를 지향한다.
다만 이것도 절대적인 규칙은 아니다. 지나친 분리는 오히려 구조를 복잡하게 만들 수 있다.
추상화
추상화는 복잡한 내부 구현을 숨기고 필요한 인터페이스만 노출하는 것이다.
예를 들어 게임 코드에서
renderer.Draw(mesh);
라고 호출한다고 해보자.
호출하는 쪽에서는 GPU 버퍼를 어떻게 관리하는지, 어떤 그래픽 API를 사용하는지 알 필요가 없다.
Game Code -> Renderer Interface -> Graphics API -> GPU
이런 구조를 만들면 구현 세부사항이 바뀌더라도 상위 코드의 변경을 줄일 수 있다.
추상화의 목적은 코드를 무조건 복잡하게 만드는 것이 아니다.
변경되는 구현과 그것을 사용하는 코드를 분리하는 것에 가깝다.
캡슐화
캡슐화는 객체 내부의 상태와 구현을 외부에서 직접 조작하지 못하도록 제한하고 정해진 인터페이스를 통해 접근하게 하는 방식이다.
예를 들어
class Player
{
private:
int health;
public:
void TakeDamage(int damage);
int GetHealth() const;
};
외부에서 health를 직접 변경하지 못하게 하고 TakeDamage() 같은 메서드를 통해 상태를 변경하도록 만들 수 있다.
이렇게 하면 객체 내부의 상태가 어떤 규칙을 통해 변경되는지 관리하기 쉬워진다.
캡슐화는 단순히 private을 사용하는 문법적 특징이라기보다 변경 권한과 책임을 어디에 둘 것인가의 문제로 보는 편이 좋다.
SOLID
객체지향 설계에서 자주 언급되는 원칙이 SOLID다.
Single Responsibility Principle
하나의 클래스가 너무 많은 책임을 가지지 않도록 한다.
Player -> 이동 -> 렌더링 -> 저장 -> 네트워크 -> 결제
이런 구조는 변경의 이유가 너무 많다.
가능하면 책임을 적절하게 나눈다.
Open/Closed Principle
기존 코드를 계속 수정하기보다 새로운 기능을 추가하는 방식으로 확장할 수 있도록 설계한다.
다만 이 원칙을 억지로 적용해서 모든 것을 인터페이스로 만들 필요는 없다.
Liskov Substitution Principle
상위 타입을 사용하는 코드에서 하위 타입을 사용해도 프로그램의 의미가 깨지지 않아야 한다.
상속 관계를 만들었다면 단순히 “문법적으로 상속된다”가 아니라 정말 같은 종류의 객체로 취급할 수 있는가를 봐야 한다.
Interface Segregation Principle
사용하지 않는 기능까지 포함된 거대한 인터페이스를 강제로 의존하게 만들지 않는다.
Dependency Inversion Principle
상위 수준의 정책이 구체적인 구현에 직접 의존하지 않도록 한다.
Game Logic -> Interface -> Concrete Implementation
이렇게 의존 방향을 분리하면 구현 교체와 테스트가 쉬워질 수 있다.
SOLID는 법칙이라기보다 설계할 때 사용할 수 있는 판단 기준에 가깝다.
디자인 패턴
소프트웨어 개발에서는 반복적으로 등장하는 설계 문제들이 있다.
이런 문제에 대한 일반적인 해결 구조를 정리한 것이 디자인 패턴이다.
대표적으로
- Singleton
- Factory
- Observer
- Strategy
- State
- Command
- Adapter
- Decorator
등이 있다.
예를 들어 Strategy 패턴은 같은 목적을 수행하지만 알고리즘을 교체해야 하는 경우에 사용할 수 있다.

호출하는 쪽은 PathFinder라는 추상화에 의존하고 실제 알고리즘은 상황에 따라 교체할 수 있다.
하지만 패턴을 적용하는 것 자체가 좋은 설계는 아니다.
패턴은 문제를 해결하기 위한 도구이지 사용해야 하는 목표가 아니다.
버전 관리
여러 명이 소프트웨어를 개발한다면 코드 변경을 추적할 수 있어야 한다.
가장 대표적인 도구가 Git이다.
Git을 사용하면 코드의 변경 이력을 관리하고 여러 개발자가 서로 다른 작업을 진행한 뒤 변경사항을 통합할 수 있다.
기본적인 흐름은 다음과 같다.
작업 -> Commit -> Push -> Review -> Merge
버전 관리의 중요한 점은 단순히 코드를 백업하는 것이 아니다.
누가, 언제, 무엇을, 왜 변경했는지 추적할 수 있다는 것이다.
문제가 발생했을 때 특정 변경을 찾아 되돌릴 수 있다는 것도 큰 장점이다.
브랜치와 코드 리뷰
팀 개발에서는 기능별로 브랜치를 나누어 작업할 수 있다.

각 작업이 끝나면 코드 리뷰를 거쳐 메인 브랜치에 통합한다.
코드 리뷰의 목적은 단순히 문법 오류를 찾는 것이 아니다.
- 요구사항을 제대로 구현했는가
- 설계가 적절한가
- 다른 코드에 문제가 생기지 않는가
- 유지보수하기 어려운 구조는 아닌가
- 테스트가 충분한가
같은 부분을 확인한다.
코드 리뷰는 개발자의 실수를 줄이는 장치이면서 팀 전체의 코드 품질을 일정하게 유지하는 장치이기도 하다.
테스트
코드가 컴파일된다고 제대로 동작하는 것은 아니다.
테스트는 프로그램이 의도한 대로 동작하는지 확인하는 과정이다.
대표적으로
Unit Test
Integration Test
System Test
등이 있다.
Unit Test
작은 단위의 기능을 독립적으로 테스트한다.
TEST(PlayerTest, Damage)
{
Player player(100);
player.TakeDamage(30);
EXPECT_EQ(player.GetHealth(), 70);
}
Integration Test
여러 컴포넌트가 연결된 상태에서 제대로 동작하는지 확인한다.
예를 들어 로그인 시스템과 데이터베이스가 정상적으로 연결되는지를 테스트할 수 있다.
System Test
전체 시스템을 하나의 시스템으로 보고 요구사항대로 동작하는지 확인한다.
테스트의 핵심은 테스트 개수를 많이 만드는 것이 아니다.
변경했을 때 무엇이 깨졌는지 빠르게 발견할 수 있는 구조를 만드는 것이 중요하다.
테스트와 유지보수
소프트웨어는 한 번 만들고 끝나는 경우보다 계속 수정되는 경우가 많다.
처음에는 간단했던 코드도 시간이 지나면서 기능이 추가되고 예외 처리가 늘어나면서 복잡해진다.
이때 테스트가 있으면 기존 기능을 수정하면서 다른 기능이 깨졌는지 빠르게 확인할 수 있다.
기능 수정 -> 테스트 실행 -> 기존 기능 정상 -> 배포
테스트가 없다면 변경 이후 모든 기능을 사람이 직접 확인해야 할 수도 있다.
따라서 테스트는 개발 속도를 느리게 만드는 작업이라기보다 장기적으로 변경 비용을 낮추기 위한 안전장치로 볼 수 있다.
리팩토링
리팩토링은 외부에서 보이는 동작은 유지하면서 내부 구조를 개선하는 작업이다.
예를 들어
Before
긴 함수
복잡한 조건문
중복 코드
강한 의존성
을
After
작은 책임
명확한 이름
중복 제거
의존성 분리
같은 구조로 바꾸는 것이다.
리팩토링에서 중요한 것은 기능 변경과 섞지 않는 것이다.
기능도 바꾸고 구조도 바꾸면 문제가 생겼을 때 원인을 찾기 어려워진다.
가능하면
기존 동작 확인 -> 리팩토링 -> 테스트 -> 기능 변경 -> 테스트
처럼 변경 범위를 분리하는 것이 관리하기 쉽다.
기술 부채
빠르게 기능을 만들기 위해 임시적인 구현을 선택할 수 있다.
문제는 이런 선택이 계속 쌓이면 나중에 수정 비용이 커진다는 것이다.
이것을 기술 부채(Technical Debt)라고 한다.
예를 들어
"일단 여기서 처리하고 나중에 분리하자"
라는 코드가 계속 쌓이면 나중에는 구조를 바꾸는 것 자체가 어려워질 수 있다.
기술 부채가 항상 나쁜 것은 아니다.
시간이 부족하거나 요구사항이 확정되지 않은 초기 단계에서는 의도적으로 단순한 구현을 선택할 수도 있다.
중요한 것은 부채를 만든 사실을 인식하고, 언제 갚을 것인지 판단하는 것이다.
소프트웨어 품질
소프트웨어의 품질은 “버그가 없는가”만으로 판단하기 어렵다.
대표적으로 다음과 같은 특성을 생각할 수 있다.
Correctness
Performance
Reliability
Security
Maintainability
Scalability
Usability
어떤 특성을 얼마나 중요하게 볼지는 소프트웨어의 목적에 따라 달라진다.
예를 들어 게임에서는 프레임 성능과 응답성이 중요할 수 있고, 금융 시스템에서는 정확성과 안정성이 훨씬 중요할 수 있다.
모든 것을 최고 수준으로 만들려고 하면 비용이 너무 커진다.
그래서 소프트웨어 설계에서는 무엇을 얼마나 보장할 것인지를 결정하는 것도 중요한 일이다.
애자일
소프트웨어 개발 방법론에서 자주 등장하는 개념이 Agile이다.
애자일의 핵심적인 방향은 처음부터 모든 것을 완벽하게 계획하고 마지막에 한 번에 완성하는 것보다, 작은 단위로 개발하고 피드백을 받아 계속 수정하는 것이다.
계획 -> 개발 -> 테스트 -> 배포 -> 피드백 -> 다시 계획
대표적인 개발 방식으로 Scrum, Kanban 등이 있다.
애자일이 “계획을 하지 않는다”는 의미는 아니다.
오히려 변화가 발생한다는 전제에서 계획을 짧은 주기로 갱신하는 방식에 가깝다.
CI/CD
코드가 변경될 때마다 사람이 직접 빌드하고 테스트하고 배포하는 과정은 규모가 커질수록 부담이 된다.
이를 자동화하는 것이 CI/CD다.
CI(Continuous Integration)는 코드 변경사항을 지속적으로 통합하고 빌드와 테스트 등을 자동으로 수행하는 방식이다.
CD(Continuous Delivery / Deployment)는 검증된 소프트웨어를 배포 가능한 상태로 유지하거나 자동으로 배포하는 과정이다.
Code -> Commit -> Build -> Test -> Deploy
자동화된 파이프라인을 구축하면 사람이 반복적으로 수행하던 작업을 줄이고 코드 변경에 대한 피드백을 빠르게 받을 수 있다.
소프트웨어 개발에서 결국 관리해야 하는 것
소프트웨어공학을 여러 개념으로 나눠서 공부하면 각각 다른 분야처럼 보인다.
하지만 연결해서 보면 결국 관리해야 하는 대상은 크게 몇 가지다.
요구사항 -> 무엇을 만들 것인가
설계 -> 어떻게 만들 것인가
구현 -> 실제로 만들기
테스트 -> 제대로 만들어졌는가
버전 관리 -> 무엇이 어떻게 바뀌었는가
배포 -> 사용자에게 어떻게 전달할 것인가
유지보수 -> 변경을 어떻게 감당할 것인가
그리고 이 모든 과정에서 가장 큰 문제는 변경이다.
처음에는 잘 동작하는 코드를 만드는 것이 어렵지 않을 수 있다.
문제는 몇 달 뒤에도 수정할 수 있는 코드인가, 새로운 개발자가 이해할 수 있는가, 기능 하나를 추가했을 때 다른 기능이 망가지지 않는가, 문제가 발생했을 때 원인을 찾을 수 있는가다.
소프트웨어공학은 결국 이 문제를 다루는 분야라고 생각한다.
좋은 소프트웨어는 단순히 지금 동작하는 코드가 아니다.
변경할 수 있고, 테스트할 수 있고, 문제를 추적할 수 있고, 시간이 지나도 유지할 수 있는 코드와 개발 프로세스가 함께 있어야 한다.