본문 바로가기

프로그래밍(TA, AA)/개발방법론

[개발방법론] 개발방법론 관련하여 알아둘 팁

객체 지향 설계 단계적 접근 방법


단계1 : 모호성의 해소

객체 지향 설계 관련 문제들은 대개 고의적인 모호성을 띱니다. 이를 해소하기 위해 여러분 스스로 가정을 도입하는지, 면접관에게 질문을 던지는지 살펴보기 위해서입니다. 결국, 무엇을 개발해야 하는지 이해하지 못한 상태에서 코딩부터 시작하는 개발자는 회사의 시간과 돈을 낭비하며, 그보다 더 심각한 문제들을 만들어 내기도 합니다.


객체 지향 설계에 관한 질문을 받으며, '누가' 그것을 사용할 것이며 '어떻게' 사용할 것이ㅏㄴ지에 대한 질문을 던져야 합니다. 질문에 따라서는 육하원칙에 따른 질문을 던져야 할 때도 있습니다. 누가, 무엇을, 어디서, 언제, 어떻게, 왜.


가령 여러분이 커피 메이커에 대한 객체 지향적 설계를 내놓으라는 요구를 받았다 가정해보겠습니다. 간단해 보이지만 실제로는 그렇지 않습니다. 여러분이 다룰 커피 메이커는 시간당 수백 명의 고객을 상대하며 열 가지 이상의 제품을 만들어 내야 하는, 대규모 식당에 설치되는 기계일 수도 있고, 나이 드신 분들이 사용하는, 블랙 커피만 만드는 간단한 기계일 수도 있습니다. 어떤 용도로 쓰이느냐가 설계를 좌우하게 됩니다.


단계2 : 핵심 객체의 설계

이제 무엇을 설계하고 있는 것인지 파악했으니, 시스템에 넣을 '핵심 객체'가 무엇인지 고민해야 합니다. 가령, 식당을 객체 지향적으로 설계하라는 문제를 받았다 해보겠습니다. 핵심 객체로는 Table, Guest, Party, Order, Meal, Employee, Server, Host 등이 도출될 것입니다.


단계3 : 관계 분석

핵심 객체 식별이 어느 정도 끝났다면, 이제 객체 간 관계를 분석해야 합니다. 어떤 객체가 어떤 객체의 멤버인가? 다른 객체로부터 계승을 받아야 하는 객체는 있나? 관계는 다-대-다(many-to-many) 관꼐인가 아니면 일-대-다(one-to-many) 관계인가?


가령, 식당 문제의 경우 우리는 다음과 같은 관계성을 도출하게 됩니다.


- Party는 Guest의 배열을 가져야한다.

- Server와 Host는 Employee를 계승해야 한다.

- Table은 하나의 Party만 가질 수 있지만, 각 Party는 여러 개의 Table을 가질 수 있다.

- Restaurant에 Host는 하나뿐이다.


여기서 주의할 것은, 종종 잘못된 가정을 만들어 사용하는 경우가 있다는 것입니다. 가령 하나의 Table에 여러 Party가 앉는 경우도 있을 수 있습니다(요즘 많이 사용되고 있는 대형 공동 테이블의 경우). 여러분의 설계가 얼마나 범용적이어야 하는지에 관해서는 면접관과 상의해보기를 바랍니다.


단계4 : 행위 분석

여기까지 왔다면 여러분의 객체 지향 설계의 골격은 잡힌 상태일 것입니다. 남은 일은 객체가 행해야 하는 일들을 생각하고, 어떻게 상호작용해야 하는지 따져보는 것입니다. 그러다 보면 잊은 객체가 있음을 발견하게 될 수도 있고, 결국 설계를 변경해야 할 수도 있습니다. 

가령, 한 Party가 Restaurant에 입장하고, 한 Guest가 Host에게 Table을 부탁한 경우를 생각해 보자. Host는 Reservation을 살펴본 다음 자리가 있으면 해당 Party에게 Table을 배정할 것입니다. 자리가 없다면 Party는 Reservation 리스트 맨 마지막에 추가될 것입니다. 한 Party가 식사를 마치고 떠나면 한 Table이 비게 될 것이고, 리스트의 맨 위 Party에 할당될 것입니다.



디자인 패턴


면접관은 여러분의 지식이 아니라 능력을 테스트하므로 디자인 패턴은 보통 면접 범위 외로 칩니다. 하지만 Singleton이나 Factory Method와 같은 디자인 패턴을 알아두면 면접볼 때 특히 유용하므로, 여기서 다루도록 하겠ㅅ브니다.

디자인 패턴의 수는 이 책에서 논의할 수 있는 것 이상으로 엄청나게 많습니다. 소프트웨어 엔지니어링 기술을 향상시키는 환상적인 방법 하나는, 디자인 패턴에 관한 책을 하나 골라 공부하는 것입니다.


싱글톤 클래스

싱글톤 패턴은 어떤 클래스가 오직 하나의 객체만을 갖도록 하며, 프로그램 전반에 그 객체 하나만 사용되도록 보장합니다. 정확히 하나만 생성되어야 하는 전역적 객체를 구현해야 하는 경우에 특히 유용합니다. 가령, Restaurant와 같은 클래스는 정확히 하나의 객체만 갖도록 구현하면 좋을 것입니다.


class Restaurant {
    private static Restaurant _instance = null;
    public static Restaurant getInstance() {
        if(_instance == null ){
            _instance = new Restaurant();
        }
        return _instance;
    }
}


팩토리 메서드

팩토리 메서드 패턴은 어떤 클래스의 객체를 생성하기 위한 인터페이스를 제공하되, 하위 클래스에서 어떤 클래스를 생성할지 결정할 수 있도록 합니다. 팩토리 메서드 패턴을 구현하는 한 가지 방법은 객체 생성을 처리하는 클래스를 abstract로 선언하여, 팩토리 메서드를 구현하지 않고 놔두는 것입니다. 다른 한 가지 방법은, 객체 생성을 처리하는 클래스를 concrete 클래스로 만들어 팩토리 메서드를 구현하고, 생성해야 할 클래스를 나타내는 값을 팩토리 메서드의 인자로 받는 것입니다.


interface GameType {
    public static CardGame Poker = new PokerGame();
    public static CardGame BlackJack = new BlackJackGame();
}

class PokerGame extends CardGame {}
class BlackJackGame extends CardGame {}

class CardGame {
    public static CardGame createCardGame(GameType type) {
        if(type == GameType.Poker) {
            return new PokerGame();
        } else if (type == GameType.BlackJack) {
            return new BlackJackGame();
        }
        return null;
    }
}



규모확장성과 메모리 제한


규모확장성에 관한 문제는 가장 쉬운 종류의 문제입니다. 분산 시스템 강의를 들을 필요도 없고, 시스템 설계 분야의 경험을 쌓을 필요도 없습니다. 적절한 수준의 연습만 하면, 소프트웨어 엔지니어라 자부하는 사람이라면 쉽게 풀수 있는 문제들입니다.


단계적 접근법


면접관들은 여러분이 시스템 설계에 관해 무엇을 알고 있는지 테스트하려 하지 않습니다. 사실 면접관들은 가장 기본적인 컴퓨터 과학 개념들을 제외하고는, 여러분이 런저런 지식을 가졌는지 테스트해 보려 하지 않습니다. 대신, 여러분이 까다로운 문제를 쪼개어 아는 사실들을 바탕으로 풀어나갈 능력이 있는지 살핍니다. 시스템 설계와 관련된 문제들에는 아래의 접근법들을 적용하면 좋습니다.


단계1: 간단하게 시작하라

모든 데이터가 한 기계에 보관될 수 있고, 메모리 제한도 없다고 가정하라. 그런 상황에서는 문제를 푼다면 어떻게 되겠는가? 이 질문에 대한 답이 여러분이 만들 해답의 일반적 골격이 되어 줄 것이다.


단계2: 현실로 돌아가라

이제 원래 문제로 돌아간다. 한 기계에 담을 수 있는 데이터의 양은? 데이터를 나누면 발생하는 문제들은? 데이터를 논리적으로 분할하는 방법이나, 여러 대의 기계에 퍼져 있는 데이터 가운데 특정한 데이터의 위치를 찾는 방법등은 자주 출제되는 문제들이다.


단계3: 문제를 풀라

마지막으로 단계2에서 찾은 문제들을 어떻게 풀 것인지 생각해 보라. 어떤 문제를 풀면 정말로 그 문제가 사라질 수도 있겠지만, 단순히 문제가 완화되는 것으로 그칠 수 있음을 명심하라. 보통 단계1에서 구상한 접근법을 수정해 가면서 계속 사용할 수도 있지만, 근본적으로 뜯어고쳐야 하는 경우도 때로 발생한다.


통상적으로는 순환적인 접근법(iterative approach)을 사용하면 좋습니다. 단계2에서 등장한 문제를 풀면 또 다른 새로운 문제가 등장할 수 있는데, 그런 문제들도 공략해야 한다는 뜻이다. 여러분의 목표는 회사가 몇백만 달러를 들여 만든 복잡한 시스템을 재설계하는 것이 아니라, 여러분이 문제를 분석하고 풀 능력이 있다는 것을 보이는 것입니다. 여러분 자신이 설계한 해법의 약점을 돌파해 나가는 것은, 그런 능력이 있음을 입증하는 환상적인 방법입니다.



알아야할 것: 정보, 전략 그리고 문제


일반적인 시스템

슈퍼컴퓨터라는 물건이 아직도 사용되고 있긴 하지만, 대부분의 웹 기반 회사들은 서버들을 엮어 만든 대형 시스템을 사용합니다. 따라서 거의 항상 여러분도 그런 시스템을 사용한다고 가정할 수 있습니다.


면접 전에 다음 표를 채워봐야 합니다. 컴퓨터가 얼마나 많은 데이터를 보관할 수 있는지 어림잡을 수 있도록 도와줄 것입니다.


 구성요소

 통상적 용량 / 가격

 하드 디스크

 HDD : 1GB당 100원

 메모리

 DDR4 : 1GB 812원

 인터넷 전송 지연

 ?


대량 데이터 분할

때로 고용량 하드디스크를 사면 문제가 해결되기도 하지만, 결국에는 데이터를 여러 기계로 분할해야 하는 때가 옵니다. 그때는 이런 질문을 던져봐야 합니다. 어떤 기계에 어떤 데이터를 둘 것인가? 몇가지 전략을 적용할 수 있습니다.


- 등장 순서에 따라: 데이터의 출현 순서에 따라 분할할 수 있다. 다시 말해, 새로운 데이터 접수 결과로 현재 사용 중인 기계가 꽉 찬 경우에 새로운 기계를 추가하는 것이다. 이렇게 하면 필요한 것 이상의 기계를 사용하지 않아도 되는 장점이 있다. 하지만 이렇게 하면 문제나 데이터 종류에 따라서는 데이터를 검색하기 위해 참조해야 하는 조회 테이블이 복잡해지고, 매우 커질 수도 있다.


- 해시 값에 따라: 또 다른 방법은, 데이터를 해시 함수에 통과시켜 얻은 결과 값에 따라 데이터를 저장할 기계를 결정하는 것이다. 좀 더 구체적으로 설명하자면 (1)데이터에 관계된 키를 고른 다음에, (2)그 키에 해시 함수를 적용하여, (3)결과로 얻은 값 v에 mod(v, N)을 적용하여 v'를 구하고(여기서 N은 기계의 수) (4)v'가 가리키는 기계에 그 값을 저장하는 것이다. 다시 말해, 데이터를 #[mod(hash(key), N)]번 기계에 저장한다는 것이다.

  이 접근법의 좋은 점은 조회 테이블 같은 별도 자료구조를 둘 필요가 없다는 것이다. 모든 기계가 데이터 위치를 자동적으로 파악할 수 있기 때문이다. 문제는 어떤 기계는 다른 기계보다 많은 데이터를 받아들여 결국에는 그 용량 한계를 넘어서게 될 수도 있다는 것이다. 그런 일이 발생하면 데이터를 다른 기계로 옮겨 부하를 균등화하는 전략을 취하거나(비용이 많이 든다) 해당 기계의 데이터를 두 기계에 분할하는 방법을 사용해야 할 것이다(기계들을 트리tree 형태로 조직해야만 한다)


- 실제 값에 따라: 해시 값에 따라 데이터를 나누는 것은 사실 임의적이다. 데이터가 표현하는 내용과 데이터의 위치 사이에 아무런 연관성이 없기 때문. 어떤 경우에는 데이터가 무엇을 표현하느냐를 이용해서 시스템 지연을 줄일 수 있다.

  가령 여러분이 소셜 네트워크를 디자인한다고 하자. 전 세계의 많은 사람이 모이지만, 실제로는 멕시코에 사는 사람은 러시아 친구보다는 멕시코 친구가 더 많게 마련이다. 그러니 아마도 유사한 데이터를 같은 기계에 모아두면 데이터를 찾기 위해 뒤져야 하는 기계의 수가 줄어들게 될 것이다.


- 임의로: 데이터가 그냥 임의로 쪼개지는 탓에, 데이터의 위치 정보를 저장하기 위한 조회 테이블을 별도로 구현해야 하는 경우도 자주 있다. 조회 테이블이 굉장히 커질 수 있긴하지만, 시스템 설계상의 어떤 부분은 단순화 시켜줄 것이고, 부하를 더 균등하게 배분할 수 있을 것이다.



예제: 특정한 단어 리스트를 포함하는 모든 문서를 찾아라

수백만 개의 문서가 있다고 하자. 어떤 단어 리스트를 포함하는 모든 문서를 찾으려면 어떻게 해야 겠는가? 단어가 등장하는 순서는 고려할 필요가 없지만, 단어가 문자열의 일부로 등장하는 경우는 검색 대상에서 제외합니다. 다시 말해, 'bookkeeper'라는 단어가 등장했다고 해서 'book'이 등장한 것으로 간주해 버리면 안된다는 뜻입니다.


문제를 풀기 전에, 이것이 단 한 번만 수행되면 되는 작업인지, 아니면 findWords와 같은 메서드로 구현되어 반복적으로 실행될 수 있어야 하는 작업진지를 살펴야 합니다. 여기서는 같은 문서들에 대해 findWords를 여러번 호출할 수 있다고 가정하겠습니다. 그렇게 하면 전처리(pre-processing)에 드는 비용은 받아들일 수 있습니다.


단계 1)

첫 번째 단계에서는 수백만 개의 문서가 존재할 수 있다는 사실은 잊어버리고, 수십 개 정도의 문서만 있다고 가정하고 시작합니다. 이런 경우네느 findWords를 어떻게 구현하겠는가?


한 가지 방법은 모든 문서를 전처리 하여 해시 테이블 인덱스를 만드는 것입니다. 이 해시 테이블은 어떤 단어와 그 단어가 포함된 문서 간의 대응 관계를 저장합니다.


"books" -> {doc2, doc3, doc6, doc8}

"many" -> {doc1, doc3, doc7, doc8, doc9}


many와 books가 포함된 문서를 찾으려면, books에 대응되는 문서 집합과 many에 대응되는 문서 집합의 교집합을 구하면 됩니다. 그리하면 {doc3, doc8}을 구할 수 있습니다.


단계 2)

이제 원래로 돌아가 보자. 문서가 백만 개 이상이 되면 무슨 문제가 발생하나? 아마도 문서를 여러 기계에 나눠 보관해야 할 것이다. 그리고 여러 가지 다른 요인들에 따라서는 (단어의 수나 출현 빈도 등) 해시 테이블조차도 한 기계에 온전히 보관할 수 없을 수도 있다. 그런 일이 실제로 벌어진다고 가정해보자.


데이터를 나누려면, 다음과 같은 사항들을 고민해야 한다.


1. 해시 테이블은 어떻게 분할할 것인가? 키워드에 따라 나눌 수도 있다. 어떤 단어에 대한 문서 목록은 한 기계 안에 온전히 보관되록 하는 것이다. 문서에 따라 나눌 수도 있다. 전체 문서 집합 가운데 특정한 부분집합에 대한 해시 테이블만 한 기계에 두는 것이다.

2. 데이터를 분할하기로 결정하고 나면, 어떤 기계에서는 문서를 처리하고 그 처리 결과를 다른 기계로 옮겨야 할 수 있다. 이 절차는 어떻게 정의해야겠는가? (주의: 해시 테이블을 문서에 따라 나누기로 했다면 이 절차는 불필요할 수도 있다.)

3. 어떤 기계에 어떤 데이터가 보관되어 있는지 알 수 있도록 하는 방법이 필요하다. 이 조회 테이블의 형태는? 조회 테이블은 어디 두어야 하겠는가?


단지 세 가지 고려사항만 언급했을 뿐이다. 이보다 더 많을 수도 있다.



단계3)

이 단계에서는 앞서 언급한 문제들 각각에 대한 해결 방법을 찾는다. 한 가지 방법은 키워드를 알파벳 순서에 따라 분할하는 것입니다. 즉, 한 기계가 특정한 범위의 단어들만 통제하도록 하는 것이다.

키워드를 알파벳 순서로 순회하면서 가능한 한 많은 데이터를 한 기계에 저장하는 알고리즘을 고안할 수 있다. 그 기계가 다 차면, 다른 기계로 옮겨 간다.


이 접근법의 장점은 조회 테이블을 작고 단순하게 만들 수 있다는 점이다. (값이 범위만 명시하면 되기 때문이다) 각 기계 조회 테이블의 복사본을 저장할 수도 있다. 단점은 새로운 문서나 단어가 추가되면 키워드를 굉장히 많이 이동시켜야 할 수도 있다는 것이다.


특정한 문자열 집합을 포함하는 문서를 찾기 위해서는 우선 해당 문자열 리스트를 정렬한 다음에 각 기계에 그중 일부에 해당하는 문자열들을 찾으라는 조회 요청을 보내는 것이다. 가령 문자열 리스트가 "after builds boat amaze banana"와 같이 주어졌다면, 기계 1에는 {"after", "amaze"}에 대한 조회 요청을 보내는 것이다.


기계1은 "after"와 "amaze"를 포함하는 문서 집합들을 구한 다음 그 교집합을 구하여 반환한다. 기계3은 {"banana", "boat", "builds"}에 대해 같은 작업을 수행한다. 마지막으로, 조회 요청을 보냈던 기계는 기계1과 기계3으로부터 받은 결과의 교집합을 구한다.