코드의 품질을 해치지 않으면서도 객체지향 언어의 특성을 잘 활용해서 프로그래밍을 연습하고 싶다는 생각에 이 책을 구입했습니다.
첫 장 부터 팩토리 패턴(Factory Pattern)에 대한 설명과 함께 우리가 사용중인 대부분의 객체지향 언어(Java, C#, Ruby ..)의 원리를 자세히 설명하면서 내용을 전개합니다. 제가 공부하면서 생각했던 객체지향 프로그래밍의 가장 큰 틀은 객체의 생명 주기(LifeCycle), 그리고 가시성(Scope)안에서 객체가 가진 속성이 유효하고, 제 기능을 수행한다는 점 이었습니다.

팩토리 패턴(Factory Pattern)

우리가 인터페이스에 맞춰서 코딩을 해야하는 이유

제목은 거창하게 적었지만, 팩토리 패턴이 가진 목적의 핵심입니다.
자바에서 객체를 생성할 때 우리는 new 연산자를 사용해서 다음과 같이 객체를 쉽게 만들었습니다.

class Youtube {
	String name;
}
Youtube youtube = new Youtube();

new 연산자는 클래스의 객체를 생성하는 단일 기능만 수행합니다.
하지만, new연산자가 실행되어 객체를 생성하기 전에 다른 로직이 추가 되어서 상황에 따라 각기 다른 클래스를 만들어야 한다면 어떻게 코드를 수정할까요? 예를 들면, 각각 다른 유투브 코딩 채널의 객체를 생성하는 알고리즘을 설계한다고 가정합시다.
번거로움을 피하기 위해 아래와 같이 팩토리 패턴으로 객체를 정의합니다.

// 추상 클래스 선언 및 test 코드 작성
public abstract class Youtube {
    public abstract String getName();

    public static void main(String[] args) {
        TypeYFactory typeYFactory = new TypeYFactory();

        Youtube y1 = typeYFactory.createYoutube("카일스쿨");
        Youtube y2 = typeYFactory.createYoutube("노마드코");
        Youtube y3 = typeYFactory.createYoutube("동빈나");

        System.out.println(y1.getName());
        System.out.println(y2.getName());
        System.out.println(y3.getName());
    }
}

// 이름 값 받은 경우 반환할 코드 작성
public class DongbinNa extends Youtube{
    @Override
    public String getName() {
        return "DongbinNa";
    }
}

// 추상 클래스화 하는 코드
public abstract class YFactory {
    public abstract Youtube createYoutube(String type);
}

// 이름 값과 매칭되는 객체 생성하는 코드
public class TypeYFactory extends YFactory {

    @Override
    public Youtube createYoutube(String type) {
        Youtube youtube = null;

        switch (type){
            case("노마드코더"):
                youtube = new NomadCoder();
                break;

            case("동빈나"):
                youtube = new DongbinNa();
                break;

            case("조코딩"):
                youtube = new JoCoding();
                break;

                default:
                    youtube = new KyleSchool();
        }

        return youtube;
    }
} 

이렇게, 모듈간의 응집도는 높이고 클래스간 결합도를 낮추는 형태로 설계를 해야합니다. 위의 그림에서와 같이 각각의 자식 클래스가 하나의 부모 클래스와 종속된 형태로 설계가 되었습니다. 추후에 코드 리팩토링을 하게 된다면 우리는 그 부분만 수정하면 됩니다.

올바른 클래스 이름 작성법

  • 클래스의 이름은 기능 중심(What it does)이 아닌, 무엇인지(What it is)에 기반하여 작성하는 것을 권합니다.
class CashFormatter {
	private int dollars;
    CashFormatter(int dlr) {
    	this.dollars = dlr;
    }
    public String format() {
    	return String.format("$ %d", this.dollars);
   }
 }

위 코드의 경우 우리는 CashFormatter라는 클래스가 어떤 일을 하게 될 것인지 쉽게 알 수 있습니다. 클래스의 이름은 고유 명사로서 짓는 것이 더 좋습니다. 이유는 cashformatter 클래스의 경우, 클래스의 이름 특성에서 포매팅 역할에만 한정이 되어있는 것이 문제이기 때문입니다. 이런 경우, 클래스를 재사용하기 어렵습니다. 반면에, Cash라고 명명할 경우, 추후에 작성하게 될 클래스와 함께 코드 설계면에서 용이한 부분이 있습니다.

  • -er로 끝나는 이름을 삼가한다.

코드 안에서 생성자를 효율적으로 사용하기 위한 규칙

  • 주 생성자와 부 생성자를 나누어서 설계하세요.
  • 응집도가 높은 코드일수록 생성자는 많고, 메서드의 수는 적다.
  • 생성자에 코드를 넣지 마세요.
class Cash {
	private Number dollars;
    Cash(String dlr) {
    	this(new StringAsInteger(dlr));
    }
    Cash(Number dlr){
    	this.dollars = dlr;
   }
}

위의 코드의 경우, 객체를 초기화 하는 시점에서 곧장 텍스트를 숫자로 바꾸지 않고 실제로 사용하는 시점까지 객체의 변환 작업을 미룹니다. 이 상태에서 다음과 같이 객체를 선언하면, 어떤 결과가 나올까요?

Cash value = new Cash("10");

이 코드에서 객체 value는 StringAsInteger 인스턴스를 캡슐화 합니다.

위의 코드에서, 데이터 파싱이 여러번 수행되지 않고 on Demand 형태로 파싱을 하도록 설계하고 싶다면 우리가 잘 알고있는 Decorator를 추가해서 파싱 결과를 캐싱 하는 형태로 구현하면 됩니다.

import java.util.ArrayList;
import java.util.Collection;

public class CacheNumber implements Number {
    private Number origin;
    private Collection<Integer> cached = new ArrayList<>(1);
    public CacheNumber(Number number) {
        this.origin = number;
    }

    @Override
    public int intValue() {
        if (this.cached.isEmpty()) {
            this.cached.add(this.origin.intValue());
        }
        return this.cached.get(0);
    }
} 

코드를 설계할 때, 다음 리팩토링 시점을 염두에 두고 코드를 설계를 해야하는 이유를 구체적으로 알아보았습니다. 객체는 리소스의 요청을 받을 때만 기능을 수행하고, 그 전에는 어떠한 일도 하지 않는 형태로 존재하는 것이 우리가 코드 리팩토링을 좀 더 수월하게 하고, 메모리를 낭비하지 않는 첫 번째 방법임을 깨닫게 해주는 파트였습니다.

profile
Software Developer

0개의 댓글