List
Search
1. 들어가기 앞서
개발자 독서 모임을 통해 알게 된 이선협님(Cobalt의 CTO)의 세션을 우연히 Devcon 2024에서 접하게 되었다. 세션 제목은 소프트웨어 파괴의 미학으로, 처음에는 어떤 내용을 다룰지 쉽게 가늠하기 어려웠다.
세션을 들어보니, 파괴 지향 개발이라는 신선한 관점을 바탕으로, 끊임없이 변화하는 비즈니스 환경에 적응하려면 우리가 개발한 소프트웨어는 결국 파괴될 수밖에 없다는 점을 강조한 점이 인상적이었다. 이를 염세적으로 받아들이는 대신, 어떻게 소프트웨어를 잘 파괴할 수 있을지 고민해야 한다는 시각의 전환이 흥미로웠다. 특히, 소프트웨어 개발을 니체의 사상과 접목시켜 설명한 부분과 더불어, “결국 어떻게 하면 파괴를 잘 할 수 있는가?” 라는 질문에 대한 구체적인 방법을 제시해 준 점이 매우 유익했다.
이하의 내용은 이선협님이 발표한 세션을 바탕으로, 나만의 관점에서 다시 정리한 것이다. 발표 내용을 선협님께서 블로그에 포스팅하신게 있는데 원본 내용이 궁금하신분은 아래 링크를 통해 확인할 수 있다.
2. 불확실성에 대항하기
2.1. 소프트웨어 개발이 어려운 이유
•
많은 것이 불확실함
•
[내부 요인] 지식과 경험의 부족
◦
리팩터링
▪
지식과 경험의 부족했기에 개발자가 스스로 만들어낸 기술부채
▪
리팩터링 가장 좋은 시기? 어떻게 리팩터링을 해야할지 정확히 알 때
◦
내부 요인은 개발자가 통제 가능. 극복하기 위한 노력들을 진행할 수 있음
•
[외부 요인] 급변하는 비즈니스
◦
카노 모델 - 제품을 제공하는 기능을 분류하는 모델
▪
제품을 사용하는 이유? 그 제품만 제공하는 기능을 쓰기 위해
▪
매력적 기능은 시간이 지나면 당연하고 흔한 기능이 됨. 영원하지 않음
→ 계속 바꿔야함. 비즈니스는 안정화되기 너무 어려움
◦
시간이 지나도 비즈니스 복잡성은 죽지 않음
•
은총알은 없음
◦
모든건 트레이드 오프
◦
완벽할 수 없음 → 확신 할 수 없음
◦
결국, 우리는 언젠가 삭제할 수 밖에 없는 코드를 만든다는 것을 의미함
2.2. 니체의 사상과 소프트웨어 개발
•
프리드리히 니체의 “비극의 탄생”
◦
경계를 파괴하는 힘
◦
경계를 인식하고 나누는 힘
▪
디오니소스에 대항하는 이성적인 방법들
•
분석, 분류, 검증
•
아폴론적인 것
▪
대부분 개발자는 매우 아폴론적인 존재들
•
소프트웨어 개발도 유사함
◦
혼돈과 예측 불가능성이 끊임없이 존재
◦
비즈니스 요구 사항, 사용자 피드백, 기술 변화 등이 지속적으로 변함
◦
소프트웨어는 완성된 후에도 계속 수정되고, 재구성되며, 그 과정에서 카오스적인 흐름이 발생
◦
이는 현실 세계에서의 불확실성, 변화의 속도 등을 반영하는 디오니소스적인 요소와 유사함
2.3. 디오니소스에 대항하기
•
소프트웨어 개발에 디오니소스에 대항하는 일반적인 과정
1.
코딩 기술 학습 & 논리적 사고 훈련
2.
단순 구현 한계 → 패턴 설계 공부
3.
어느순간 완벽한 설계는 없음을 깨달음. 유동적으로 움직일 수 있는 방법론 학습
•
예) 애자일, 칸반, TDD, DDD 등
4.
그럼에도 많은 실패를 겪음
•
디오니소스에 대항하는 방법
1.
염세주의적 태도
•
디오니소스적인 힘에 대항은 무의미함
◦
어차피 바뀔거 대충하자
◦
설계, 방법론 무의미 등등
•
편하고 안락한 길
2.
맞서 싸우기
•
디오니소스적인 힘을 받아들이는 것
◦
완벽한 것은 없다는걸 깨닫음. 그럼에도 불구하고 끊임없이 노력
•
어렵고 견뎌야 하는 길
•
니체의 사상에 맞는 길
•
혼돈에 대항한 소프트웨어 업계는 끊임 없이 노력을 해옴
◦
데이터 추상화, 프로그래밍 패러다임, 애자일, DDD, 리팩터링 등
2.4. 제안: 파괴 지향 개발
•
우리가 개발한 소프트웨어는 어차피 언젠가 제거된다.
•
염세주의적 생각에 빠지기 쉬운 현실
•
발상의 전환 → 차라리 잘 지울 수 있게 개발해보자!
◦
파괴에는 파괴로 대응
•
소프트웨어에서 발생하는 파괴의 종류
1.
기능 삭제 (Pivoting)
•
제품을 더 좋은 길로 나아갈 수 있는 기회
2.
기능 재구현 (Refactoring)
•
소프트웨어의 생명을 연장시키는 일
•
리팩터링은 경제성
•
3. 파괴 지향 개발
3.1. 개요
•
정의
◦
언젠가 코드가 파괴될 것이라는 것을 사실로 받아들이고 그것을 지향하는 개발 방법론
•
파괴 지향 개발의 3 원칙
◦
불확실성 유무 파악
◦
파괴하기 쉬운 방향 선택
◦
필요한 것만 유지. 필요없는 것은 전부 지움
•
프로세스
◦
분석 → 경계 분리 → 코드 구현 → 복잡성 제거 (반복)
3.2. [Step 1] 경계 분리
•
불확실성에 따라 경계 분리
◦
불확실성이 높은 것과 낮은 것을 분리하는 것이 중요
◦
불확실성은 변화율을 의미 (얼마나 자주 변하는지?)
•
변화율 측정 방법
◦
외부 요인
▪
기능이 실험적인가?
▪
기능이 릴리즈 되었는가?
▪
기능이 릴리즈 된 후 얼마나 지났는가?
▪
고객의 반응은 어떠한가?
◦
내부 요인
▪
적절한 지식과 경험이 있는 상태로 만들었는가?
▪
코드에 신뢰성이 있는가?
▪
코드를 이해할 수 있는가?
▪
목표 성능을 달성했는가?
•
무엇부터 분리할까?
◦
분리 단위
▪
애플리케이션, 모듈, 유즈케이스, 컴포넌트 등
▪
무엇을 기준으로 분리할지 추상화 레벨 결정 필요
▪
그 레벨안에서만 놀 수 있게 격리 필요
•
변화율이 높은 기능
◦
복잡한 비즈니스 로직
◦
릴리즈된지 오래된 기능
•
변화율이 낮은 기능
◦
지원 서브 도메인
◦
계정 관리, 이메일 발송, 로깅, 모니터링 등
3.3. [Step 2] 구현: 파괴하기 쉽게 만들기
파괴 가능성; 파괴하기 쉬운쪽을 선택
•
독립성 - 코드가 독립적일 수록 파괴하기 쉬움
◦
결합도와 응집도
◦
단일 책임 원칙
•
인지 가능성 - 개발자가 코드를 보고 이해할 수 있는 정도
◦
코드를 인지할 수 있는가?
◦
코드의 가독성, 문서화
•
통제 가능성 - 개발자가 통제할 수 있는 영역인지?
◦
내가 통제할 수 있는 영역인가?
◦
코드 오너쉽
◦
코드의 신뢰성
◦
코드의 사회성
▪
사회성이 떨어지는 코드
▪
예) 조직내 약속하지 않은 규칙을 사용하는 것
3.4. [Step 3] 복잡성 제거
•
사용하지 않는다면 코드베이스에서 제거
•
파괴 가능성이 낮은 코드는 리팩터링 하거나 다시 만들 것
•
최대한 단순성을 유지하는 것이 핵심
4. 코드 파괴의 기술
4.1. 변화율 기록하기
•
변화율이 높을 것으로 예상되는 코드엔 주석을 남김
•
추후 정리 및 다른 개발자가 확인할 때 편함
4.2. 코드 가독성 끌어올리기
•
성능이슈가 없다면 선형적, 선언적으로 작성
4.3. 단계 분리하기
•
전처리, 계산, 후처리 등의 단계에 맞게 로직을 분리 진행
function primeSum(n: number): number {
let sum = 0;
for (let i = 2; i <= n; i++) {
let isPrime = true;
// 소수 여부 체크와 동시에 합산
for (let j = 2; j < i; j++) {
if (i % j === 0) {
isPrime = false;
break;
}
}
if (isPrime) {
sum += i;
}
}
return sum;
}
function main() {
console.log(primeSum(10)); // 2 + 3 + 5 + 7 = 17
}
main();
TypeScript
복사
단계가 분리되어있지 않는 코드
// 전처리: 소수 리스트 생성
function makePrimeList(n: number): number[] {
const primes: number[] = [];
for (let i = 2; i <= n; i++) {
let isPrime = true;
// 소수 여부 체크
for (let j = 2; j < i; j++) {
if (i % j === 0) {
isPrime = false;
break;
}
}
if (isPrime) {
primes.push(i);
}
}
return primes;
}
// 계산: 리스트의 합 계산
function listSum(list: number[]): number {
return list.reduce((acc, cur) => acc + cur, 0);
}
// 후처리: 결과 출력
function main() {
const primes = makePrimeList(10); // 소수 리스트 생성
const sum = listSum(primes); // 소수의 합 계산
console.log(sum); // 17 출력
}
main();
TypeScript
복사
단계가 분리되어있는 코드
4.4. 참조 투명성 보장
•
순수함수, 불변 객체, 멱등성
•
코드 신뢰성의 측면
let globalValue = 5;
function addWithSideEffect(a: number): number {
globalValue += a;
return globalValue;
}
console.log(addWithSideEffect(3)); // 결과는 8
console.log(addWithSideEffect(3)); // 결과는 11, 외부 상태(globalValue)가 변경됨
TypeScript
복사
참조 투명성이 없는 함수
function add(a: number, b: number): number {
return a + b;
}
console.log(add(2, 3)); // 항상 5를 반환
console.log(add(2, 3)); // 항상 5를 반환
TypeScript
복사
참조 투명성이 있는 함수
4.5. 단일 책임 원칙
•
가장 이해하기 쉬우면서도 어려운 원칙
•
아래 예제에서 주소 관련된 속성들은 UserAddress로 분리할 수도 있고 안할 수도 있음
•
단일 책임의 기준은 무엇인가? 라는 질문에는 요구사항을 잘 이해하는게 중요
class UserInfo {
userId: number;
userName: string;
email: string;
telephone: string;
provinceAddress: string; // 도
cityAddress: string; // 시
regionAddress: string; // 구
detailAddress: string; // 상세 주소
avatarUrl: string;
constructor(
userId: number,
userName: string,
email: string,
telephone: string,
provinceAddress: string,
cityAddress: string,
regionAddress: string,
detailAddress: string,
avatarUrl: string,
) {
this.userId = userId;
this.userName = userName;
this.email = email;
this.telephone = telephone;
this.provinceAddress = provinceAddress;
this.cityAddress = cityAddress;
this.regionAddress = regionAddress;
this.detailAddress = detailAddress;
this.avatarUrl = avatarUrl;
}
}
TypeScript
복사
4.6. 인터페이스 분리 법칙
•
인터페이스를 잘 나누면 기능 제거시 인터페이스만 제거하면 됨
interface Appliance {
turnOn(): void;
turnOff(): void;
setTimer(minutes: number): void;
heatUp(temperature: number): void; // 전자레인지만 해당
brewCoffee(): void; // 커피 머신만 해당
}
class Microwave implements Appliance {
turnOn() {
console.log("Microwave is now on.");
}
turnOff() {
console.log("Microwave is now off.");
}
setTimer(minutes: number) {
console.log(`Microwave timer set for ${minutes} minutes.`);
}
heatUp(temperature: number) {
console.log(`Microwave heating up to ${temperature} degrees.`);
}
brewCoffee() {
// 구현되지 않음
}
}
class CoffeeMachine implements Appliance {
turnOn() {
console.log("Coffee Machine is now on.");
}
turnOff() {
console.log("Coffee Machine is now off.");
}
setTimer(minutes: number) {
// 구현되지 않음
}
heatUp(temperature: number) {
// 구현되지 않음
}
brewCoffee() {
console.log("Brewing coffee.");
}
}
TypeScript
복사
인터페이스가 잘 나눠지지 않은 코드
interface Switchable {
turnOn(): void;
turnOff(): void;
}
interface Timer {
setTimer(minutes: number): void;
}
interface Heatable {
heatUp(temperature: number): void;
}
interface CoffeeBrewer {
brewCoffee(): void;
}
class Microwave implements Switchable, Timer, Heatable {
turnOn() {
console.log("Microwave is now on.");
}
turnOff() {
console.log("Microwave is now off.");
}
setTimer(minutes: number) {
console.log(`Microwave timer set for ${minutes} minutes.`);
}
heatUp(temperature: number) {
console.log(`Microwave heating up to ${temperature} degrees.`);
}
}
class CoffeeMachine implements Switchable, CoffeeBrewer {
turnOn() {
console.log("Coffee Machine is now on.");
}
turnOff() {
console.log("Coffee Machine is now off.");
}
brewCoffee() {
console.log("Brewing coffee.");
}
}
TypeScript
복사
인터페이스가 잘 나눠진 코드
4.7. 잘 직동하는 코드 교체하기 (스트랭글러 무화과 패턴)
•
레거시 코드를 교체할 때 사용하는 패턴
◦
레거시 코드 일부를 새로운 코드로 교체
◦
안정화 기간이 지난후 대체된 레거시 코드 제거
◦
반복
•
기존 코드를 교체하는 것이 아니라 새로운 코드를 만든 다음, 기존 코드를 감싸는 식
•
예시)
class Object {
oldMethod1() {
// ...
}
oldMethod2() {
// ...
}
}
TypeScript
복사
1. 기존 코드
class LegacyObject {
oldMethod1() {
// ...
}
oldMethod2() {
// ...
}
}
class NewObject {
private legacyObject: LegacyObject;
constructor() {
this.legacyObject = new LegacyObject();
}
newMethod1() {
// ...
}
oldMethod2() {
this.legacyObject.oldMethod2();
}
}
TypeScript
복사
2. 새 코드를 추가 & 기존 코드를 감싸는 방식으로 교체
class NewObject2 {
newMethod1() {
// ...
}
newMethod2() {
// ...
}
}
TypeScript
복사
3. 최종적으로 모든 코드를 새 코드로 이전
4.8. 메서드 전문화
•
메서드의 일반화를 줄이고 더 특정한 목적을 가지도록 하는 것
•
개발자의 본능을 거스르는 일
•
분리되는 만큼 삭제하기 편함
4.9. 중복 코드 작성
•
변화율이 높은 곳에서는 일부러 중복 코드 작성
◦
둘 중 하나는 분명 변할 것이라는 확신이 있을 때
◦
경험과 직관에 따라 선택
•
개발자의 본능을 거스르는 일이라 어려움
5. 마치며
•
코드 파괴 기술에 열거한 내용들은 이미 널리 알려진 것이라고 볼 수 도 있음
•
그러나, 바라보는 관점이 다름
•
무엇이 중요한가
◦
잘 작성한 코드는 파괴하기 쉬움
◦
개발자 내면의 본능을 잠시 억누르자
▪
경계 나누기, 확장, 코드 애정 등
◦
중요한 것은 좋은 소프트웨어를 만드는 것
◦
염세주의는 소프트웨어의 독
◦
끊임없이 무엇이 좋은가를 생각하자