[리팩토링]팩토리패턴과 데코레이터 패턴을 활용한 스타 프로리그 앱 리팩토링 하기

5월 17일, 최범균님과 함께 예전에 안드로이드로 만들었던 “스타 프로리그“앱을

오랜만에 꺼내서 리팩토링을 진행했다.

약 2시간 반동안 함께 코드리뷰를 하고 리팩토링을 하면서 얻은 가장 큰 성과는 바로

팩토리패턴“과 “데코레이터패턴“을 활용하여 기존에 쓸데없이 여기저기

흩어져있던 파일관련한 유틸클래스를 파일을 다루는 책임을 가진 한 곳으로 모아두어

코드의 가독성을 높이고 유지보수 효율을 좋게 만든 것이다.

===========================================================

우선, 리팩토링 이전에 전체적인 구조를 UML로 살펴보자.

20130517_리팩토링이전_UML

UML에서 보다시피, 각 3개의 액티비티들은 비동기로 데이터(파일 혹은 HTML)를 가져와

서 보여주는 추상클래스 AsyncDataViewer에게 FileUtils를 전달한다.

그리고, AsyncDataViewer를 구현한 3개의 콘크리트 클래스들은 액티비티에서 받은

FileUtils 객체를 이용해 상황에 맞게 File 데이터를 읽어오거나 저장하는 역할을 한다.

여기서의 가장 큰 문제는 바로 파일처리와 관련된 부분과 직접적으로 연관이 있지 않은

AsyncDataViewer에서 FileUtils 객체를 이용해 파일 관련된 처리를 하고 있다는 것이었다.

AsyncDataViewer는 단지 데이터를 가져와 보여주는 책임과 역할을 하는 것인데도 말이다.

그래서, 각 객체의 책임과 역할을 명확히 분리하고 쓸데없이 여기저기 흩어져있던  FileUtils

관련 코드를 제거하기 위해 “팩토리패턴“과 “데코레이터패턴“을 활용해 리팩토링을

진행했다.

===========================================================

먼저, 리팩토링 후의 UML을 다시 한번 살펴보자.

20130517_리팩토링이후1_UML

UML이 좀 복잡한데 하나씩 뜯어보면,

  • 데이터를 제공하는 인터페이스 StarLeagueDataProvider를 두고 각각 파일데이터와HTML데이터를 제공하는 2개의 구현체를 두었다.
  • AsyncDataViewer는 StarLeagueDataProviderFactory를 통해 StarLeagueDataProvider 구현체인 FileCacheStarLeagueDataProvider를 데이터의 파서로 이용한다. 이때, StarLeagueDataProvider의 구현체를 생성하는 곳에  “팩토리패턴”을 사용하여 한개의 객체만 생성되도록 구현하였다.
  • 그리고, StarLeagueDataProviderFactory에서 실질적으로 만들어진 FileCacheStarLeagueDataProvider객체에는 파일에 데이터가 없을 것을 대비해 실제 HTML페이지에서 데이터를 읽어오는 EsportSiteHtmlDataProvider 객체가 참조변수로 저장되어 있다.
  • 리팩토링 전에는, FileUtils 객체를 각기 액티비티에서 생성해 데이터를 가져와 보여주는 AsyncDataViewer에게 전달하여 그 안에서 파일을 읽고 저장하였다면, 리팩토링후에는 AsyncDataViewer에서 팩토리클래스를 만들어 원래 데이터 파서로 사용하던 EsportSiteHtmlDataProvider 대신 FileCacheStarLeagueDataProvider 객체를 생성해 사용하고 있다.
  • 이는, 기존에 HTML페이지를 파싱하기 전에 FileCacheStarLeagueDataProvider로 한번 감싸서 파일관련된 처리를 진행하고, 파일이 없으면 원래 HTML 파싱을 담당하던 EsportSiteHtmlDataProvider가 데이터를 가져오는 역할을 담당하게 된다.  곧, EsportSiteHtmlDataProvider전에 FileCacheStarLeagueDataProvider로 한번 감싸는 “데코레이터패턴”을 활용한 예이다.

이렇게 리팩토링을 한 덕분에,  각 액티비티들은 FileUtils객체를 생성해

AsyncDataViewer에게 전달하지 않아도 되고, AsyncDataViewer 또한 원래 역할인

데이터를 가져와 보여주는 것에 충실하게 되었다.

이 리팩토링에는 아래의 추상화도 한몫했다.

“데이터를 가져와 보여준다”

저 간단한 추상화 덕분에 StarLeagueDataProvider 인터페이스가 나왔고,

이를 구현한 두개의 콘크리트 클래스를 데코레이터 패턴을 활용해 중구난방 흩어져있던

파일과 관련된 처리를 하는 FileUtils 객체를 한곳에 모아 처리할 수 있게 되었다.

Advertisements

[베타리딩]객체 지향 디자인 패턴 – 전반부 서평

좋은 기회에 최범균님의 “객체 지향 디자인 패턴“책 출간 전 베타리딩을 하게 되어,

그 전반부에 대한 서평을 쓰게 되었다.

우선, 글을 읽으면서 내내 들었던 생각은

과연 자바언어를 사용하면서 클래스를 만들 때 얼마나 많은 생각을 하고 만들었나

하는 것이었다.

대학교에서 처음 자바언어를 접하고, 또 여러 책등을 통해 자바 언어를 습득하면서

단 한번도 저 문제에 대해 진지하게 생각해 본 적이 없었던 것 같다.

실제 사회생활을 하며 실무를 익힐때에도 마찬가지였다.

눈 앞에 보이는 문제를 해결하기 위해 API사용법이나 문법에 대한 부분은 공부 혹은

검색을 통해 근시안적으로 해결해왔지만, 이 책을 한장 한장 읽으면서 그러한 행동들을

반성하게 되는 계기가 되었다.

=============================================================

서론이 길었지만, 내가 이 책의 전반부를 보며 내린 결론은 결국,

좋은 객체지향 설계에서  좋은 코드가 나오고, 또 그것이 사용자에게

 좋은 품질의 서비스를 제공해준다

라는 것이다.

특히나 객체 지향의 장점은 요구사항의 지속적인 변화에 따른 “유지보수“에서 더 많은

장점을 발휘한다는 측면에서 1장을 시작하고 있다.

우리는 흔히 기존의 소프트웨어를 유지보수하면서, 무수한 if-else문의 향연을 적어도

한번쯤은 보았을 것이다. 그 소프트웨어를 유지보수 할때의 그 난감함이란 구지 말로

설명하지 않아도 될만큼 많은 시간과 인력을 필요로 하는 일이다.

이 책의 저자 또한 그러한 예제를 시작으로 해서, 데이터를 중심으로 절차지향적으로

작성된 코드를 점진적으로 객체지향으로 리팩토링 하는 과정을 쉽게 설명하고 있다.

그리고, 중간중간 리팩토링 이전 코드와 이후 코드를 비교하여 그림으로 도식화해서

설명하고 있으며, 리팩토링이 모두 끝난 후에는 바뀐 전체구조를 한 눈에 알아보기 쉽게

UML(Unified Modeling Language)로 보여주고 있는 점 또한 이 책의 장점이라 할 수 있다.

=============================================================

이 책 전반부에서의 또 하나 인상깊은 점은, 객체에 대한 정의를 다음과 같이 명확히

하고 있다는 것이다.

객체의 핵심은 기능을 제공하는 것

이 바로 그것이다.

서론에서도 언급했지만 프로그래밍을 하면서 객체의 정의에 대해 저렇게

간결하고 핵심을 찌르는 정의를 본 적이 없는 것 같다.

그 만큼 이 책의 저자는 객체 지향에 가장 기본인 객체(Object)를 정의하는 것에서

시작해 인터페이스와 클래스, 또 그들이 주고 받는 메세지,  객체의 책임과 크기, 

의존이라는 개념을 파일을 읽고 암호화해서 쓰는 예제의 리팩토링을 통해

잘 설명하고 있다.

또한, 객체 지향의 중요한 원칙인 “캡슐화“를 지키는 두 가지 원칙을 들어

  • Tell, Don’t Ask
  • 데미테르의 법칙(Law of Demeter)

객체 지향적으로 설계할 때의 중요한 과정인 캡슐화에 대한 중요성을 강조하고 있다.

=============================================================

3장에선 객체 지향이 주는 장점인 구현 변경의 유연함을 또 다른 기법으로

다형성“과 “추상화“를 설명하고 있다

다형성과 추상화는 여러 자바 기본서적에서도 다루고 있는 개념이지만 한편으로는

그 개념을 정확히 알고 프로그래밍 하는 개발자가 드문 것이 또한 현실이다.

그러한 점에서 특히 저자의 책에서 주목되는 부분은 “추상화“의 설명 부분이다.

2장에서 다뤘던 파일관련 예제를 들어 추상화를 설명하고 있는데, 그 중에서

중요하다고 생각되는 부분은

변화되는 부분을 추상화하는 것이다. 요구사항이 바뀔 때 변화되는 부분은

  이후에도 변경될 소지가 많다“이다.

그리고, 객체 지향의 유명한 원칙인 “인터페이스에 대고 프로그래밍 하기“를 설명

하면서도, 다시 한번 주위를 환기시키는 부분은 바로

인터페이스를 사용해야 할 때는 변화 가능성이 높은 경우에 한해서 사용해야 한다

라는 점이다.  추상화 과정을 통해 유연성을 얻게 된다고 해서 무턱대고 아무 곳이나

추상화한 인터페이스를 사용한다면 오히려 프로그램의 구조만 복잡해지고 유연함의

효과는 누릴 수 없는 상황이 발생하게 된다고 이 책의 저자는 경고하고 있다.

마지막으로, 지금까지 추상화한 과정을 테스트코드를 만드는 과정을 통해 그 이점을

부각 시키고 있다. 추상화를 하면 실질적인 구현체 없이 진짜처럼 행동하는 객체인

Mock객체를 통해 내가 만든 코드를 더욱 빠르게 테스트 할 수 있다는 것이 바로 그것이다.

=============================================================

끝으로, 전반부의 4장은 객체 지향의 주요 특징으로 “재사용“을 말하면서 그 예로

“상속”과 “조립(composition)”을 예로 들고 있다.

상속을 사용했을시 아래와 같은 3가지 문제점을

  • 불필요한 클래스 증가
  • 상위클래스의 변경이 어렵다는 문제점
  • 상속을 제대로 사용하지 않는 경우

“조립”이라는 방법으로 해결 할 있다는 점을 예제코드를 들어 설명하고 있다.

그리고 “조립”의 또 다른 장점으로 런타임에 조립 대상 객체를 교체할 수 있는 점을

들었다.  물론, 모든 상황에서 객체 조립을 사용해야 한다는 말은 아니며,

상속을 사용하다 보면 변경의 관점에서 유연함이 떨어질 가능성이 높으니

객체 조립을 먼저 고민하라는 것을 강조하고 있다.

이러한 객체 조립과 더불어 “위임(delegation)“에서는 대부분의 경우에 미세한

성능저하보다 위임을 통해서 얻을 수 있는 유연함과 재사용의 장점이 크다는 점을

강조하고 있다.

=============================================================

이 책의 전반부를 읽으면서 장점이라 느낀 점은 객체 지향의 주요 개념을 예제코드와

함께 명확히 설명하면서도, 그에 대한 이해를 그림과 UML로 돕고 있어 초급개발자도

쉽게 다가갈 수 있다는 것이다.

그리고, 중간중간 나오는 객체지향 설계원칙과 디자인패턴 또 부가적으로 공부해야 할

좋은 책들을 소개함으로서 객체 지향에 대해 수박 겉핥기로 공부하는 것이 아닌,

이 책을 통해 처음부터 기본기를 튼튼하게 다질 수 있다는 것도 큰 장점 중 하나일 것이다.

[리팩토링]팩토리 메서드(Factory Method)패턴을 활용하여 동적으로 구현객체 생성하기

기존코드에서 원래 의도하지 않게 프로그래밍하여 나쁜 냄새를 풍기고 있던 코드를

과감하게 리팩토링했다.

우선, 스프링MVC에서 기존 서비스단 코드는 아래와 같다.

@Service
public class TestService {
    @Resource(name = "notLoginedWeekDisplay")
    private WeekDisplay notLoginedWeekDisplay;

    @Resource(name = "loginedWeekDisplay")
    private WeekDisplay loginedWeekDisplay;

    public String display(Member member, int currentWeek) {
        if(member.isGuest()) {
            return notLoginedWeekDisplay.display(currentWeek);
        }

        return loginedWeekDisplay.display(currentWeek);
    }
}

display메서드의 의도는 아래와 같다.

  • 로그인사용자와 비로그인 사용자별로 각기 다른 데이터를 가져와 뷰에 보여줌
  • 데이터는 현재주차와 나머지 다른 주차를 비교하여 만들어짐

로그인/비로그인 사용자별로 뷰를 보여주는 부분은 따로 추상화하여

WeekDisplay 추상클래스를 만들었다.

public abstract class WeekDisplay {
    public abstract String compareSmallerOrEqualWeek(int compareWeek);

    //현재주간과 비교주간을 비교해 결과를 만드는 공통로직
    public String display(int currentWeek) {
       //비교주간(ex: 1~4주)이 현재주간과 같거나 이전이면
       //로그인,비로그인 조건별로 다르게 구현
       if (compareWeek <= currentWeek) {
           return compareSmallerOrEqualWeek(compareWeek);
       }

       //비교주간이 현재주간과 다른경우
       ....
    }
}

그리고, WeekDisplay에서 로그인/비로그인 사용자별로 추상메서드

compareSmallerOrEqualWeek()를 각기 다르게 구현하였다.

즉, “템플릿메서드“패턴을 사용하여 공통로직은 추상클래스인 AbstractWeekDisplay에

위치하고 로그인/비로그인 사용자별로 다르게 동작해야 되는 부분은 따로 추상메서드인

compareSmallerOrEqualWeek()로 빼서 각 구현체별로 다르게 구현하였다.

@Component
public class NotLoginedWeekDisplay extends WeekDisplay {
    @Override
    public String compareSmallerOrEqualWeek(int compareWeek) {
        String week = Integer.toString(compareWeek)+"주차";
        reutrn week +" 비로그인 사용자가 보는 화면입니다.";
    }
}

@Component
public class LoginedWeekDisplay extends WeekDisplay {
    @Override
    public String compareSmallerOrEqualWeek(int compareWeek) {
        String week = Integer.toString(compareWeek)+"주차";
        reutrn week +" 로그인 사용자가 보는 화면입니다.";
    }
}

아래 그림과 같이 추상화를 잘해놓고도, 한가지 간과한 점이 있었다!

siege_class_diagram_1

문제는 바로 로그인/비로그인 조건에 따라 서비스단에서 분기를 태워

WeekDisplay의 구현체의 display()메서드를 바로 호출한다는 점이다.

원래 의도대로라면 아래 코드가 되었을 것이다.

    public String display(Member member) {
        //분기로직 없이 동적으로 AbstractWeekDisplay의 구현체를 가져와
        //데이터를 얻어온다.
        //weekDisplay 구현체는 member여부에 따라 달라진다.
        return weekDisplay.display();
    }

그래서, 해당 분기로직을 삭제하고 로그인/비로그인 조건에 따라 동적으로

WeekDisplay의 구현객체를 가져오도록 “팩토리 메서드“패턴을 사용하였다.

    @Service
    public class WeekDisplayFactory {
        private static WeekDisplay notLoginedWeekDisplay;

        @Resource(name = "loginedWeekDisplay")
	private WeekDisplay loginedWeekDisplay;

	public WeekDisplay create(Member member) {
	    if (member.isGuest()) {
	        if (notLoginedWeekDisplay == null) {
		    return new NotLoginedWeekDisplay();
		}

		return notLoginedWeekDisplay;
	    }

	    return loginedWeekDisplay;
	}
    }

위와 같이 팩토리클래스를 사용함으로서, 서비스단은 아래와 같이 간단해졌다.

public String display(Member member) {
    //weekDisplayFactory는 서비스단에서 DI받음
    WeekDisplay weekDisplay = weekDisplayFactory.create(member);

    return weekDisplay.display();
}

드디어, 원래 의도였던 동적으로 WeekDisplay의 구현객체를 가져와서

화면에 보여주는 것을 실현했다.

그리고 서비스단의 분기로직도 사라졌다.

아래는 이번 리팩토링작업동안 진행했던 테스트케이스 코드이다.

@RunWith(MockitoJUnitRunner.class)
public class ServiceTest {
    @Mock
    private WeekDisplayFactory weekDisplayFactory;

    @Captor
    private ArgumentCaptor<Member> memberArgumentCaptor;

    @InjectMocks
    private TestService service;

    private WeekDisplay notLoginedWeekDisplay;
    private WeekDisplay loginededWeekDisplay;

    @Before
    public void setUp() {
        //각종 초기화 코드들..
    }

    @Test
    public void 비로그인사용자의경우() {
        when(weekDisplayFactory.create(guestMember)).thenReturn(notLoginedWeekDisplay);

	String result = service.display(guestMember);

	verify(weekDisplayFactory).create(memberArgumentCaptor.capture());

	assertThat(guestAccount, sameInstance(memberArgumentCaptor.getValue()));
        assertThat(result, is("비로그인 사용자가 보는 화면입니다."));
    }

    @Test
    public void 로그인사용자의경우() {
        when(weekDisplayFactory.create(loginedMember)).thenReturn(loginedWeekDisplay);

	String result = service.display(loginedMember);

	verify(weekDisplayFactory).create(memberArgumentCaptor.capture());

	assertThat(loginedMember, sameInstance(memberArgumentCaptor.getValue()));
        assertThat(result, is("로그인 사용자가 보는 화면입니다."));
    }
}