파싱 로직은 어디에 위치하는게 좋을까?

Jaden Kim·2021년 9월 24일
0

예를 들어 다음과 같은 형식으로 입력이 주어진다고 가정해보자

(a,b)-(c,d)

우리는 이러한 사용자의 입력을 받아서, 형식에 따라 도메인 객체를 저장해야 한다.

public class Coordinate {
    private int x;
    private int y;
    
    public Coordinate(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

이 경우, 문자열을 파싱하는 로직은 어디에 위치하는게 좋을까?

  1. 입력을 받는 UI 클래스에 추가
    이 경우, 입력을 받는 메서드에서 바로 파싱 후 반환하면 된다.
    -> 하지만 UI 클래스가 입력을 받는 역할과 파싱하는 역할을 동시에 맡게 되므로, 단일 책임 원칙에 어긋나게 된다.

  2. 도메인 클래스에 생성자로 추가
    생성자로 입력 문자열을 받고, 이를 파싱해서 필드값으로 저장하는 방식이다.
    -> 이 때에는 도메인 클래스가 입력 형식에 종속적이게 된다는 문제가 발생한다.
    뷰와 컨트롤러 같은 웹단은 도메인 영역에 종속적이어도 괜찮지만, 도메인 영역은 웹단과 독립적이어야 한다.
    입력형식과 같은 UI 영역은 언제든지 바뀔 수 있기 때문에, 도메인이 특정 입력 형식에 종속적인 것은 적절하지 않다.

  3. 파싱을 담당하는 별개의 클래스 작성
    static 메서드를 통해 파싱을 담당하는 클래스를 작성하는 방식이다
    -> 이 경우에는 기능 하나 때문에 추가적인 클래스를 작성하게 되어, 불필요하게 클래스 구조가 복잡해진다는 느낌을 받았다.
    또한 이런 식으로 작성한 클래스는 결국 유틸리티 클래스인 것인데, 객체지향 관점에서 적절하지 않을 수 있다.

객체지향 프로그래밍으로 유틸리티 클래스를 대체하자

  • 결론
    고민 끝에, 나는 따로 Parser 클래스를 작성하는 3번 방식을 선택하게 되었다.
    단점이 존재하긴 하지만, 단일 책임원칙이나 웹단-도메인의 독립을 위배하는 것에 비해서는 감수할 수 있다고 생각했다.
    (하지만 아직 이게 최선인지는 잘 모르겠다ㅠㅠ)
// 인터페이스 클래스
public interface CoordinatesParser {
    String DELIMITER = "-";
    String COMMA = ",";

    boolean isValidPattern(String input);
    Coordinates parseCoordinates(String input);

    default void validateInput(String input) {
        if (!isValidPattern(input)) {
            throw new IllegalArgumentException("올바르지 않은 입력 형식입니다.");
        }
    }

    default List<Coordinate> getCoordinateListFromInput(String input) {
        return getCoordinateListFromValueList(getValueListFromInput(input));
    }

    private List<Integer> getValueListFromInput(String input) {
        List<Integer> valueList = Arrays.stream(input.split(DELIMITER))
                .map(substring -> substring.split(COMMA))
                .flatMap(Arrays::stream)
                .map(string -> string.replace("(", ""))
                .map(string -> string.replace(")", ""))
                .map(Integer::parseInt).collect(Collectors.toList());
        return valueList;
    }

    private List<Coordinate> getCoordinateListFromValueList(List<Integer> valueList) {
        List<Coordinate> coordinateList = new ArrayList<>();
        for (int i = 0; i < valueList.size()/2; i++) {
            coordinateList.add(
                    Coordinate.of(valueList.get(2*i), valueList.get(2*i + 1))
            );
        }
        return coordinateList;
    }
}
// 구현 클래스
public class LineParser implements CoordinatesParser {

    private static final String PATTERN = CoordinatesParserType.LINE.getPattern();

    private LineParser() {
    }

    public static LineParser newInstance() {
        return new LineParser();
    }

    @Override
    public boolean isValidPattern(String input) {
        return input.matches(PATTERN);
    }

    @Override
    public Line parseCoordinates(String input) {
        validateInput(input);

        return Line.of(getCoordinateListFromInput(input));
    }
}

0개의 댓글