소프트웨어 패턴이 왜 중요할까?

모든 프로그래머들은 처음에는 시행착오에 빠진다.
어떤 문제든 처음 보고 척척해결하는 프로그래머는 거의 없다.
다만, 프로그래머계의 격언이 있듯이 'Google은 항상 답을 알고 있다.'
대부분의 신입 프로그래머들이 겪는 문제점들은 이미 선배 프로그래머들이 경험해본 경우가 대부분이고, 많은 경우 그 해결방안 또한 stackoverflow와 같은 기타 커뮤니티에 정리되어 있는 경우가 많다.

소프트웨어 패턴 또한 이런 부분에서 시작되었다고 할 수 있다.
코드의 동작 여부와 별개로 어떤 문제점이 존재할 때,(책에서는 이러한 코드를 '냄새나는 코드'로 이야기하였다.) 이를 해결하기 위한 좋은 방법을 선배프로그래머나 연구자들이 '패턴'이라는 형태로 그 답을 정해놓은 것이다.
그렇기 때문에, 소프트웨어 패턴이 대단한 점은 그로부터 설계에 대한 여러 유용한 아이디어를 얻을 수 있다는 데에 있는 것이다.

이런 생각으로 처음으로 여러 종류의 패턴을 배우면 훌륭한 프로그래머가 될 수 있다는 착각에 빠진다고 저자는 언급했다. 물론 패턴을 사용하면 견고하고 확장성 있는 시스템을 만드는 데에 큰 도움이 되지만, 때로는 '패턴 만능주의'에 빠져 일을 지나치게 복잡하게 만들기 때문이다. 따라서 저자는 패턴을 너무 일찍 코드에 도입하는 대신 꼭 필요한 시기에 리팩터링을 통해 도입하는 방법을 추천한다.

과도한 설계

항상 일을 할때도 계획만 번지르르하게 세우는 종류의 사람들이 있는데, 프로그래머 중에도 그런 프로그래머들이 있다.
물론 계획을 세우는 것은 중요한 일이지만, 일부 프로그래머들은 현재 시스템에 대한 미래의 요구사항을 모두 알고 있다고 착각한다.
그런 이유로 미래에 닥칠 설계의 변화에 대비하여 현재 설계를 융통성 있고 정교하게 해두는 것이다.
그럴싸하게 들리지만 저자는 이러한 설계를 비판한다.
결국 설계의 변화가 없다면 필요 이상의 귀중한 시간과 비용을 낭비한 꼴이 되기 때문이다.
과도한 설계에 시간을 쏟는 대신, 새로 필요한 기능을 추가하거나 결함을 고치는데에 시간을 쏟는 쪽이 훨씬 합리적인 것이다.

더 심각한 문제는 저런 코드들은 없어지지도 않는다는 것이다.
혹시 나중에 사용할 것을 대비하여, 혹은 귀찮아서 등, 여러가지 이유로 이러한 과도한 설계에 해당하는 코드들이 쌓이게 된다.
그렇게 되면 작업하는 사람들은 단순한 코드를 보는 대신 훨씬 복잡한 코드를 기반으로 작업해야하므로 불편해 지는 것이다.
생산력의 감소로 이어지지만, 이것이 과도한 설계 때문이라는 것은 알아채기 힘들다.
그렇기 때문에 저자는 모든 것을 패턴으로 해결하려는 생각을 버리고, 작고 단순하고 이해하기 쉬운 코드를 작성하는데 주의를 기울여야 한다고 주장한다.

미진한 설계

미진한 설계가 과도한 설계에 비해 자주 발생하게 된다. 저자는 다음과 같은 이유들로 발생한다고 설명한다.

  • 시간이 없을때
    • 절대적인 시간이 주어지지 않을 때
    • 기존 시스템에 새 기능을 추가할 때
    • 참여 중인 프로젝트가 많을 때
  • 훌륭한 설계를 모를때

지속적으로 미진한 설계가 쌓이면 처음엔 빠르게 진행되다가 점차 느려지는 양상이 된다.
엉망인 코드로 초기 버전을 일단 작성한후, 이 엉망인 코드 때문에 생산성도 떨어지고, 결국엔 완전히 재작성하게 되는 경우가 허다하다.

TDD와 리팩터링

TDD와 리팩터링은 XP(extreme programming)의 핵심 실행지침중 하나라고 한다. 프로그래밍 과정을 다음과 같은 interactive 과정으로 변경하여, 코드를 효율적으로 작성하고 외부 변화에 민감할 수 있도록 만든 것이다.

  • 질문. 테스트를 작성함으로써 시스템에 질문한다.
    바로 다음 작성할 코드가 해야 할 일을 예상하고 이것을 나타내는 테스트를 작성한다. 아직 코드를 작성하기 전이기 때문에 테스트는 당연히 실패한다.
  • 대답. 테스트를 통과하는 코드를 작성해 질문에 대답한다.
    테스트를 통과하도록 임시방편으로라도 프로그램을 작성한다. 이 때는 코드 반복, 설계같은것을 고민할 필요 없이 일단 돌아가게 작성한다.
  • 정제. 아이디어를 통합하고, 불필요한 것은 제거하고, 모호한 것은 명확히 해서 대답을 정제한다.
    테스트가 통과한다면, 더 좋은 설계를 맘편하게 테스트 할수 있다. 코드의 반복제거, 명확하고 단순한 코드, 좋은 설계는 이 단계에서 고려한다.
  • 반복. 다음 질문을 물어 대화를 반복한다.
    완성될때까지 계속 반복한다. XP와 같은 환경에서는 사용자가 계속 피드백을 주는 경우가 많기 때문에, 이 피드백을 지속적으로 반영한다고 생각해도 좋을 것이다.

TDD는 얼핏 보기에는 비효율적이고 무계획한 소프트웨어 개발 방법으로 보일 수 있지만, 실제로는 이와 정 반대로, 이는 생산성을 극대화하는 간결하고도 반복적이며 통제된 개발방식이다. 이러한 방식은 두려움 없이 리팩터링을 할수 있게 해주고, 이를 통해 더 단순하고 훌륭한 코드를 생산하는 데 도움을 준다. 또한, 단순한 과정의 반복으로 스트레스 없이 프로그램을 작성할 수 있게 된다.

발전적 설계

저자는 더 나은 소프트웨어 엔지니어가 되기 위해서, 훌륭한 소프트웨어 설계를 공부하는 것보다 그것이 발전해온 과정을 공부하는 것이 더 가치있다고 말한다. 그 발전 과정속에 선배 프로그래머들의 지혜가 담겨있다고 얘기한다. 왜 그런식으로 발전했는지를 이해하지 못한다면, 다른 프로젝트에서 선배 프로그래머들이 겪었던 과오를 반복해서 경험할 가능성이 커진다. 설계를 발전시켜 나가면, 과도하거나 미진한 설계를 줄일 수 있고, 그 핵심에 있는게 바로 TDD와 리팩터링이다. 패턴을 이용한 리팩터링을 습득하게 된다면 훌륭한 설계를 발전시킬수 있는 장비를 가진 것과 다름이 없는 것이다.

'패턴 공부' 카테고리의 다른 글

Move Creation Knowledge to Factory  (0) 2021.04.07
Replace Constructors with Creation Methods  (0) 2021.04.07
리팩터링이란?  (0) 2021.04.06
서론  (0) 2021.04.06

본 게시판은 Joshua Kerievsky의 'Refactoring to Patterns'의 번역서인 '패턴을 위한 리팩토링'을 공부하며 스스로 배운점들을 요약 정리하는 게시판입니다.

'패턴 공부' 카테고리의 다른 글

Move Creation Knowledge to Factory  (0) 2021.04.07
Replace Constructors with Creation Methods  (0) 2021.04.07
리팩터링이란?  (0) 2021.04.06
소프트웨어 패턴과 TDD  (0) 2021.04.06
#define MAX 30000
#define QSIZE 10
using namespace std;

class Status
{
private:
	int num[4], ind, mod[4][4], high;
	int f(int x)
	{
		return 81*81*81*num[x] + 81*81*num[(x+1)&3] + 81*num[(x+2)&3] + num[(x+3)&3];
	}
public:
	Status(){}
	Status(int m[4][4])
	{	
		high = -1;
		for(int i=0;i<4;i++) for(int j=0;j<4;j++)
		{
			mod[i][j]=m[i][j];
			if(mod[i][j]>high) high=mod[i][j];
		}
		num[0] = 27*m[0][0]+9*m[0][1]+3*m[1][0]+m[1][1];
		num[1] = 27*m[0][3]+9*m[1][3]+3*m[0][2]+m[1][2];
		num[2] = 27*m[3][3]+9*m[3][2]+3*m[2][3]+m[2][2];
		num[3] = 27*m[3][0]+9*m[2][0]+3*m[3][1]+m[2][1];
		
		ind=0;
		for(int i=1;i<4;i++)
		{
			int find = f(ind), fi = f(i);
			if(find>fi) ind=i;
		}
	}
	int get(int x){ return num[(ind+x)&3]; }
	bool operator==(Status& x)
	{
		for(int i=0;i<4;i++)
		{
			if(get(i)!=x.get(i)) return false;
		}
		return true;
	}
	bool operator<(Status& x)
	{
		for(int i=0;i<4;i++)
		{
			if(get(i)!=x.get(i)) return get(i)<x.get(i);
		}
		return false;
	}

	int getHigh(){return high;}
	Status flipComplement()
	{
		int t[4][4];
		for(int i=0;i<4;i++) for(int j=0;j<4;j++) t[i][j] = high - mod[i][3-j];
		return Status(t);
	}
};
struct Block
{
	Status st;
	int base;
	Block(){}
	Block(int _d[4][4], int _b)
	{
		st = Status(_d);
		base = _b;
	}
	bool operator<(Block& x)
	{
		if(st==x.st)return base>x.base;
		return st<x.st;
	}
};
class Queue{
private:
	int data[QSIZE];
	int f,b,s;
public:
	Queue()
	{
		f=b=s=0;
	}
	void push(int x)
	{
		data[b]=x;
		++b;
		++s;
		if(b==QSIZE) b=0;
	}	
	void pop()
	{
		++f;
		--s;
		if(f==QSIZE) f=0;
	}
	int front(){return data[f];}
	int size(){return s;}
};

class Data
{
public:
	Status status;
	Queue q;
	Data(){}
	Data(Status& _st)
	{
		status=_st;
		q=Queue();
	}
};
void swap(Block& x,Block& y)
{
	Block t=x;
	x=y;
	y=t;
}

void quickSort(int l,int r, Block* b)
{
	if(l+1>=r)return;
	
	int pivot = l;
	int j=pivot;
	
	for(int i=l+1;i<r;i++)
	{
		if(!(b[i]<b[pivot])) continue;
		j++;
		swap(b[i], b[j]);
	}

	swap(b[l], b[j]);

	pivot=j;
	quickSort(l, pivot, b);
	quickSort(pivot+1,r, b);
}

int binary_search(int l, int r, Status& v, Data* d)
{
	while(l<r)
	{
		int mid = (l+r)>>1;
		if(d[mid].status==v)return mid;
		
		if(d[mid].status<v) l=mid+1;
		else r=mid;
	}
	return -1;
}

Block b[MAX];
Data d[MAX];

int normalize(int module[4][4])
{
    int base=module[0][0];
    for(int i=0;i<4;i++)
        for(int j=0;j<4;j++)
            if(base>module[i][j])
                base = module[i][j];
    
    for(int i=0;i<4;i++)
        for(int j=0;j<4;j++)
            module[i][j] -= base;
    
    return base;
}

int makeBlock(int module[][4][4]) {
	for(int i=0;i<MAX;i++)
	{
		int base = normalize(module[i]);
		b[i] = Block(module[i], base);
	}
	quickSort(0, MAX, b);

	int dnum=0;
	for(int i=0;i<MAX;i++)
	{
		d[dnum]=Data(b[i].st);
		d[dnum].q.push(b[i].base);
		while(b[i].st == b[i+1].st)
		{
			d[dnum].q.push(b[i+1].base);
			++i;
		}
		++dnum;
	}

	int ans=0;
	for(int i=0;i<dnum;i++)
	{	
		if(d[i].q.size()==0)continue;
		Status target = d[i].status.flipComplement();
		int ind = binary_search(0, dnum, target, d);
		if(ind==-1) continue;

		if(i==ind)
		{
			while(d[i].q.size()>1)
			{
				ans += d[i].q.front();
				d[i].q.pop();
				ans += d[i].q.front();
				d[i].q.pop();
				ans += d[i].status.getHigh();
			}
			continue;
		}
		while(d[i].q.size()>0 && d[ind].q.size()>0)
		{
			ans += d[i].q.front() + d[ind].q.front() + d[i].status.getHigh();
			d[i].q.pop();
			d[ind].q.pop();
		}
	}
	return ans;
}

+ Recent posts