[우아한테크코스 7기 프리코스] 첫째주 2일차 회고록

1일차 다짐처럼 우선 기능 요구사항 및 프로그래밍 요구사항을 만족하는 0.0 버전을 구현했다. 1주차 미션의 핵심 기능은 '계산기'다. 아래는 구현 과정에서 했던 고민이다.


1. Console 라이브러리 분석

아래는 우아한테크코스에서 제공한 Console 라이브러리이다.

readLine()

  • 콘솔에서 한 줄의 입력을 받아 문자열로 반환하는 메서드

close()

  • scanner가 생성되어 있는 경우, 이를 닫고 다시 null로 설정하는 메서드

getInstance() 메서드

  • scanner가 아직 생성되지 않았을 경우 Scanner(System.in)으로 초기화하고, 생성된 객체를 반환하는 메서드
import java.util.Scanner;

public class Console {
    private static Scanner scanner;

    private Console() {
    }

    public static String readLine() {
        return getInstance().nextLine();
    }

    public static void close() {
        if (scanner != null) {
            scanner.close();
            scanner = null;
        }
    }

    private static Scanner getInstance() {
        if (scanner == null) {
            scanner = new Scanner(System.in);
        }
        return scanner;
    }
}

 


 

2. 문자열에서 구분자와 숫자를 추출하는 과정에서

(1) String vs Matcher/Pattern

 String을 이용하게 되면 indexOf()로 특정 위치를 찾고, substring()으로 구분자와 숫자 문자열을 추출할 수 있다. 반면 Matcher와 Pattern을 이용하면 정규식을 사용해 해당하는 패턴을 찾아준다. 이 때 사용한 정규식 "//(.)\n(.*)"은 "//"로 시작하고, "\n"이 포함된 부분을 찾아준다.

 

 

 '좋은 코드, 나쁜 코드'에 따르면 코드는 읽기 쉽고, 예측 가능해야 한다. 위 과정을 코드로 비교하니 Matcher와 Pattern을 사용한 코드가 효율성 측면에서는 느리지만, 가독성 측면에서 훨씬 뛰어나 Matcher와 Pattern을 사용했다.

 

코드는 읽기 쉽고 예측 가능해야 한다.
- 좋은 코드, 나쁜 코드 -

 

  • String을 사용한 경우
public static int getSum(String input) {
	String delimiter = ",|:";
	String numbers = input;

	if (input.startsWith("//")) {
    int delimiterStartIndex = input.indexOf("//") + 2;
    int delimiterEndIndex = input.indexOf("\n");

    if (delimiterEndIndex > delimiterStartIndex) {
        delimiter = input.substring(delimiterStartIndex, delimiterEndIndex);
        numbers = input.substring(delimiterEndIndex + (input.contains("\n") ? 2 : 1));
    }
	return sumNumbers(numbers, delimiter);
}

 

  • Matcher, Pattern을 사용한 경우
public static int getSum(String input) {
	String delimiter = ",|:";
	String numbers = input;

	Matcher matcher = Pattern.compile("//(.)\n(.*)").matcher(input);
	if (matcher.matches()) {
		delimiter = Pattern.quote(matcher.group(1));
		numbers = matcher.group(2);
	}
	return sumNumbers(numbers, delimiter);
}

 

 또한, 정규식으로 인한 NumberFormatException이 지속적으로 발생했다. 해당 문제는 자바에서의 이스케이프 처리가 제대로 되지 않았기 때문이다. "//(.)\n(.)" 정규식에서 사용되는 "\n"은 줄바꿈을 뜻하는 문자다. 따라서 해당 정규식을 ‘"//(.)\\\\n(.)”’로 바꾸니 문제가 해결되었다.

 

 

  • 바꾼 코드
public static int getSum(String input) {
	String delimiter = ",|:";
	String numbers = input;

	Matcher matcher = Pattern.compile("//(.)\\\\n(.*)").matcher(input);
	if (matcher.matches()) {
		delimiter = Pattern.quote(matcher.group(1));
		numbers = matcher.group(2);
	}
	return sumNumbers(numbers, delimiter);
}

 

(2) String vs StringBuilder

 String은 불변 객체다. 또한 내부적으로 문자 배열을 사용하여 문자열을 저장한다. 반면에 StringBuilder는 가변 객체다. 또한 해당 객체 역시 내부적으로 문자 배열을 사용하지만, 크기를 유동적으로 조절할 수 있다.

 

 

 '이펙티브 자바'에 따르면 StringBuilder sb = new StringBuilder()와 같은 불필요한 객체 생성을 피하라고 한다. 반면에 String과 같은 불변 객체는 언제든지 재사용할 수 있다. StringBuilder를 사용하려 했던 이유는 유동적이고 더 빠르다(근거는 없는,,)는 인식이 있었다. 하지만 두 코드 모두 실행 시간이 약 7sec로 실행 시간 면에서는 큰 차이가 없었기에 String을 사용했다.

 

아이템 6 : 불필요한 객체 생성을 피하라
아이템 17 : 변경 가능성을 최소화하라
아이템 1 : 생성자 대신 정적 팩터리 메서드를 고려하라
- 이펙티브 자바 -

 

  • StringBuilder를 사용한 경우
public static int getSum(String input) {
	String delimiter = ",|:";
	StringBuilder numbers = new StringBuilder(input);

	Matcher matcher = Pattern.compile("//(.)\\\\n(.*)").matcher(input);
	if (matcher.matches()) {
		delimiter = Pattern.quote(matcher.group(1));
		numbers = new StringBuilder(matcher.group(2));
	}
	return sumNumbers(numbers, delimiter);
}

 

  • String을 사용한 경우
public static int getSum(String input) {
	String delimiter = ",|:";
	String numbers = input;

	Matcher matcher = Pattern.compile("//(.)\\\\n(.*)").matcher(input);
	if (matcher.matches()) {
		delimiter = Pattern.quote(matcher.group(1));
		numbers = matcher.group(2);
	}
	return sumNumbers(numbers, delimiter);
}