자바로 배우는 리팩토링 입문

들어가며

이 책은 막연하게 리팩토링을 어떻게 시작해야할지, 현재 재직중인 회사의 코드가 우아한지를 1차원적으로 이해하는 데 도움 주는 입문적인 내용이다. 만약 마틴 파울러의 GOF의 디자인 패턴 책이나 조슈아 블로크의 이팩티브 자바를 읽어본 사람이라면 굳이 읽지 않아도 된다. 책의 챕터 별 각 주제 자체는 나쁘진 않지만 자주 현업에서 사용 되는 실제 대표적인 사례 위주로 접근하다보니 서술한 고전 서적들의 내용 중 일부분만 다루기 때문이다.

그래도 만약 회사에서 공짜로 책을 사주거나, 책꽂이 꽃혀있다면 짬을 내서 읽어봐도 무관하다. 책을 읽으면서 에이 이게뭐야, 이건 자주쓰는 거지, 아 그래 저자가 뭘 좀 아는군 이라며 끄덕이는 자신을 본다면, 당신은 월급만 축내며 살아온 프로그래머는 아님을 느끼며 자아도취 하길 바란다. 고백하자면 나는 자아도취 하고 싶어서 책을 봤다. (기분이 매우 좋았다.)

이 책은

GOF 책을 언급했지만, 이 책은 사례 중심이기에 실무에서 실수하는 굉장히 기본적인 내용에 대해서도 나온다. 그래서 아는 내용이 있다면 휙휙 넘기고 이건 처음 보는 데 싶으면 자세히 살펴보고, 추가적으로 구글링 하는 것이 좋다.

개인적으로 책에서 나오는 대부분의 패턴이나 (팩토리 메서드 패턴과 같은), 메소드의 커뮤니케이션을 에러 Exception 으로 소통하는 것에 대해서는 기본 자바 서적(예를 들면 이펙티브 자바라던지)에도 자주 중복되는 기본적인 내용이어서 쉽게 넘겼다. 어셔선 도입이나 널 객체 도입에 대해서는 크게 공감이 되질 않았는 데, 자바8의 Optional 이 생겼다 보니 크게 와닿지 않았다.

인상깊었던 부분은 관측 데이터 복제(옵저버 패턴) 부분이 기억에 남는다. 옵저버 패턴은 개인적으로 자바 서버 사이드 보다는 javascript 클라이언트 사이드에서 많이 접했었는 데, 책에서는 자바 SWING 을 예시로 들다 보니 여러가지 생각이 많이 들었다.

그나마 실무에서 공감이 될만한 것으로는 분류 코드를 하위 클래스로 치환이 많은 도움이 될 거라 본다. 우리 회사도 마찬가지만 대부분 기술 부채가 있는 회사의 실무 코드를 보면 단일 책임 원칙을 위반한 경우가 많다.

예를 들어 특정 스키마에 N개 이상의 레코드가 들어간다던지, 특정 필드의 TYPE 으로 레코드를 분류하거나, 자바 웹 어플리케이션에서도 특정 FORM 의 입력 파라미터가 데이터의 타입을 결정하는 경우를 대표적으로 들 수 있다.

이 경우 여러 컴포넌트의 레이어에서 계속해서 다중 if 나 switch 문을 통해 데이터의 타입에 따른 로직을 결정하는 코드를 자주 볼 것이다. 이에 대해 저자는 책의 절반 이상을 이 주제로 리팩토링을 다룬다.

이 챕터에서 가장 흥미로운 것은 sub type 자신이 상태코드를 갖는 식으로 처리하는 관점도 재밌었는 데, 이는 아래에서 실제 다루었던 실무 코드 일부를 설명하면서 이야기 하겠다.

현재 재직 중인 곳은 게임 서비스 회사인데, 운영자가 특정 유저에게 아이템이나 재화를 선물할 수 있는 기능이 있다. 문제는 선물하는 스키마는 단 하나이고, 이 스키마에 아이템, 재화, 심지어 최근에 생긴 인벤토리용 아이템 모든 것이 다 선물이 된다. SOLID 창시자인 로버트 마틴이 보면 뒷목을 잡을 듯하다.

Sub Type 자신이 어떠한 타입인지 어떻게 행동해야할지를 결정짓도록 해야 한다고 서술한다. 데이터 타입이 무엇인지 자기가 어떠한 성격인지를 sub type 스스로가 설명하게 하게 하기 위해서 팩토리 메서드 패턴으로 처리했다. 그리고 이를 DB에 저장할 때의 행동을 결정 짓기 위한 전략을 정의해줄 팩토리 메서드 패턴를 만들었고, GiftService 의 행동을 결정지어 줄 GiftJob 이라는 전략에 의해서 어느 Repository 를 통해 영속화해야할 지 결정지어진다. 이 다이어그램에서 아쉬운 점이 있다면 Amount 와 Inventory Item 들을 브릿지 패턴을 통해 GiftBox 와 GiftForm 을 합성시키는 법도 생각해볼 수 있다. (브릿지 패턴이 필요한 이유는 Amount 라는 개념이 Amount1 Amount2 Amount3 처럼 다양하게 사용되기 때문이다, 실제 실무 코드에는 반영되어있다.)

이러한 리팩토링을 통해 기존 다중 if 나 switch 문이 각 컨트롤러, 서비스, 영속성 계층에서 여러번 호출 되던 것이, 단 하나의 if 문으로 바뀌었다. if 문은 컨트롤러에서 GiftJobFactory 를 호출하는 순간 팩토리 클래스 내부에서만 이루어진다. 팩토리 클래스는 Form 의 type 을 구별해서 적절한 entity 와 그 entity 와 연관 된 repository 를 엮어준다.

사실 여기서 GiftEntity 스스로가 Repository 를 집약(aggretation) 시켜서 직관적으로 되지 않느냐고 생각해볼 수 있다. 뜬금 없이 GiftJob 왜 나오냐면 runtime 시점에 Repository 는 상황에 따라서 바뀔 수 있기 때문이다. 우리는 다중 클러스터링 DB를 사용하는 데, 특정 상황에 따라서 repository 가 바뀌어야할 수 있다. 클래스 다이어그램은 안나오지만, repository 를 주입하는 것도 어떠한 전략에 의해서 주입시키게끔 되어 있다. 팩토리 패턴을 사용하지 않고 엔티티 자체가 repository 를 이용할 수 있게 한다면 특정 repository 와의 커플링이 심해지고 느슨하게 연결하기 어려우며, DB 와 연관이 없는 데, 엔티티 클래스가 인스턴스화 하면서 Repository 를 의존한다면 뭔가 모양새가 이상하다. 또한 벌크삽입과 같은 특정 기능을 사용하기 어려운 데, 자기 스스로 자신을 영속화한다고 접근하면 무조건 처리가 단일 저장만 되기 때문이다. GiftJob 은 이를 고려해서 한 개의 엔티티이든 여러 개의 엔티티이든 구분안하고 벌크삽입을 하게끔 설계되어 있는 것을 볼 수 있다.

마무리

책 자체는 리팩토링에 흥미를 위한 실무 사례 위주의 입문 서적이다. 이 책을 통해 무엇을 깨우친다기 보다는, 얼마나 내가 우아한 코드를 작성하고 있었는 지 또는 이 코드가 상상을 초월하는 냄새가 나는 코드인지를 1차원적으로 접근해볼 수 있다.

기타

위 이미지는 PlantUML 로 작성되었다. 이 아티클만을 위한 리소스이기에 따로 repository에는 올리지 않는다. 소스는 아래를 참조.

@startuml
abstract class GiftBox{
    -userId;
    -period;
    -message;
    -regDate;

    +getRegDate()
    +setRegDate(regDate)
    +getUserId()
    +setUserId(userId)
    +getMessage()
    +setMessage(message)
    {abstract} +getRewardType()
    {abstract} +getRewardValue()
}

abstract class GiftBoxEntity extends GiftBox{
    -endDate;
    -userName;

    +getUserName()
    +setUserName()
    +setCalcEndDate()
    {abstract} +getGiftUniqueCode()
   +getEndDate()
   +setEndDate(endDate)
}

class GiftBoxForm extends GiftBox{

    {abstract} +getFormType()

}

note bottom of AmountGiftBoxForm : getFormType() 은 getRewardType()과 같다.
class AmountGiftBoxForm extends GiftBoxForm{
    -rewardValue
    +getRewardValue()
    +getRewardType()
    +getFormType()
    +setRewardValue(rewardValue)
    +getChargeAmountType()
}

class ItemGiftBoxForm extends GiftBoxForm{
}

class InventoryGiftBoxForm extends GiftBoxForm{
}

note bottom of AmountGiftBox : getRewardValue 는 rewardValue 를 반환한다. \n rewardType 은 getAmountType() 에서 얻는다. \n rewardValue 는 rewardValue 에서 얻는다.
class AmountGiftBox extends GiftBoxEntity{
    -amountType;
    -rewardValue;
    +setRewardType(amountType)
    +getAmountType()
    +getRewardType()
    +getRewardValue()
    +getGiftUniqueCode()
}

note bottom of InventoryGiftBox : getRewardValue 는 inventory 를 반환한다. \n rewardType 은 inventory 에서 얻는다. \n getRewardValue()의 반환값은 inventory 에서 얻는다.
class InventoryGiftBox extends GiftBoxEntity{
    -inventory;
    +setInventory(inventory)
    +getInventory()
    +getRewardType()
    +getRewardValue()
    +getGiftUniqueCode()
}

note bottom of ItemGiftBox : getRewardValue 는 items 를 반환한다. \n rewardType 은 4이다. \n getRewardValue()의 반환값은 items 이다.
class ItemGiftBox extends GiftBoxEntity{
    -items;
    +getRewardType()
    +getRewardValue()
    +getGiftUniqueCode()

}

interface GiftService <T extends GiftBoxEntity> {
    {abstract} +gift(giftJob);
}

interface Repository <T extends GiftBoxEntity, ?> {
    saveAll(List<T>)
}

class GiftJobFactory{
    {static}+buildGiftJob(GiftBoxForm) : GiftJob
}

class GiftJob <T extends GiftBoxEntity>{
    -giftEntityRepository : Repository<T,?>
    -gift : List<T>

    +getGift()
    +getGiftRepository()
}

GiftJob o- Repository
GiftJob o-- GiftBoxEntity
GiftJobFactory -.> GiftBoxForm
GiftService o-- GiftJob

GiftJobFactory -.> GiftJob

@enduml