자바 웹 프로그래밍 Next Step [Chapter 2]

Wonkyun Jung·2023년 4월 24일
0

자바웹프로그래밍

목록 보기
1/2
post-thumbnail

2장 - 문자열 계산기 구현을 통한 테스트와 리팩토링


2.1 main() 메소드를 활용한 테스트의 문제점


public class Calculator {
	
	int add(int i, int j ) {
		return i+j;
	}
	
	int subtract(int i, int j ) {
		return i-j;
	}
	
	int multiply(int i, int j ) {
		return i*j;
	}
	
	int divide(int i, int j ) {
		return i/j;
	}
	
	public static void main() {
		Calculator cal = new Calculator();
		System.out.println(add(3,4));
		System.out.println(subtract(3,4));
		System.out.println(multiply(3,4));
		System.out.println(divide(3,4));
	}
}

기본적으로 테스트할 때 쓰는 코드 실제 서비스를 담당하는 프로덕션 코드와 이 프로덕션 코드가 정상적으로 동작하는지 확인하기 위한 main()으로 나뉜다

문제점: 프로덕션 코드와 테스트 코드(main() 메소드)가 같은 클래스에 위치 : 서비스 시점에 테스트 코드를 같이 배포할 필요가없음

-> 1차적 해결법: 서비스 코드와 테스트 코드를 다른 클래스로 나누자!


class CalculatorTest {
	
	public static void main() {
		Calculator cal = new Calculator();
		System.out.println(add(3,4));
		System.out.println(subtract(3,4));
		System.out.println(multiply(3,4));
		System.out.println(divide(3,4));
	}
}

이 경우에는 서비스 코드와 테스트 코드가 분리는 되어있지만 main() 하나의 메소드에서 프로덕션의 여러 코드의 여러 메소드를 동시에 테스트 하고 있다. 이는 프로덕션 코드의 복잡도가 증가하면 할 수록 main() 메소드의 복잡도도 증가하고, 결과적으로 main() 메소드를 유지하는데 부담이 된다.

-> 2차적 해결법: JUnit을 활용하기



2.2 JUnit을 활용해 main() 메소드 문제점 극복


2.2.1 한 번에 메소드 하나에만 집중

package calcualtor;

import org.junit.Test;

public class CalculatorTest {
	
	@Test
	public void add() {
		Calculator cal = new Calculator();
		System.out.println(cal.add(6,3));
	}
	
	@Test
	public void subtract() {
		Calculator cal = new Calculator();
		System.out.println(cal.subtract(6, 3));
	}
	
}

JUnit Test 구현을 통해서 메소드 각각을 실행해볼 수 있다 (독립적 실행이 가능) 다른 메소드에 영향을 받지 않기 때문에 내가 현재 구현하고 있는 프로덕션 코드에 집중할 수 있다.

2.2.2 결과를 눈이 아닌 프로그램을 통해 자동화


import static org.junit.Assert.assertEquals;
import org.junit.Test;

public class CalculatorTest {
	
	@Test
	public void add() {
		Calculator cal = new Calculator();
		assertEquals(9, cal.add(6, 3));
	}
	
	@Test
	public void subtract() {
		Calculator cal = new Calculator();
		assertEquals(3, cal.subtract(6, 3));
        //assertEquals(2, cal.subtract(6, 3));
	}
}

왼쪽은 subtract()에서 주석 없는 부분 오른쪽은 주석있는 부분 Test 결과


2.2.3 테스트 코드 중복제거


import static org.junit.Assert.assertEquals;
import org.junit.Test;

public class CalculatorTest {
	private Calculator cal; = new Calculator();
	
    @Test
	public void add() {
		assertEquals(9, cal.add(6, 3));
	}
	
	@Test
	public void subtract() {
		assertEquals(3, cal.subtract(6, 3));
	}
}

2.2.4 Annotation 활용


import static org.junit.Assert.assertEquals;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class CalculatorTest {
	
	private Calculator cal;
	
	@Before
	public void setup() {
		cal = new Calculator();
		System.out.println("Before");
	}
	
	@Test
	public void add() {
		assertEquals(9, cal.add(6, 3));
		System.out.println("add");
	}
	
	@Test
	public void subtract() {
		assertEquals(3, cal.subtract(6, 3));
		System.out.println("subtract");
	}
	
	@After
	public void teardown() {
		System.out.println("teardown");
	}
}

출력 값

Before
subtract
teardown
Before
add
teardown

메소드 하나를 테스트 할 때 마다 Before에 해당하는 메서드를 실행하고 테스트 메서드 실행 후에 After에 해당하는 메서드를 실행한다.



2.3 문자열 계산기 요구사항 및 실습

2.3.1 요구사항

전달하는 문자를 구분자로 분리한 후 숫자의 합을 구해 반환한다

  • 쉼표 (,) 또는 콜론 (:)으로 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환한다
    예) " " => 0, "1,2" => 3, "1,2,3" => 6 "1,2:3" => 6

  • 쉼표, 콜론 외에도 커스텀 구분자를 지정할 수 있음 문자열 앞 부분의 "//" 와 "\n"사이에 위치하는 문자를 커스텀 문자로 사용
    예) "//;n1;1;2;3"과 같이 값을 입력할 경우 커스텀 구분자는 세미콜론(;) 이며 결과값은 6을 반환해야한다.

  • 문자열 계산기에 음수를 전달하는 경우 RuntimeException으로 예외 처리해야한다.

2.4 테스트와 리팩토링을 통한 문자열 계산기 구현

2.4.1 기본 세팅 및 빈 문자열 및 NULL 값 처리


public class StringCalculator {
	public int add(String text) {
		if(text == null || text.isEmpty()) {
			return 0;
		}
		return 0; 
	}
}



import static org.junit.jupiter.api.Assertions.*;

import org.junit.Before;
import org.junit.jupiter.api.Test;

class StringCalculatorTest {

	private StringCalculator cal;
	
	@Before
	public void setup() {
		cal = new StringCalculator();
	}
	
	@Test
	public void add_null() {
		//null값 또는 빈 문자열을 넣었을 때 
		assertEquals(0, cal.add(null));
		assertEquals(0, cal.add(""));
	}
}


2.4.2 숫자 하나를 문자열로 입력할 경우 해당 숫자를 반환한다 ("1" => 1)

public class StringCalculator {
	public int add(String text) {
		if(text == null || text.isEmpty()) {
			return 0;
		}
		return Integer.parseInt(text); 
	}
}

public class StringCalculatorTest {

	[...] 생략
	
	@Test
	public void add_oneNum() {
		//숫자 하나를 문자열로 넣었을 때 
		assertEquals(0, cal.add("1"));
	}
}


2.4.3 숫자 두개를 쉼표(,) 구분자로 입력할 경우 두 숫자의 합을 반환한다 ("1,2" => 3)

public class StringCalculator {
	
	public int add(String text) {
		if(text == null || text.isEmpty()) {
			return 0;
		}
		
		if(text.contains(",")) {
			String[] values = text.split(",");
			int sum = 0; 
			
			for(String value : values) {
				sum += Integer.parseInt(value);
			}
			
			return sum;
		}
		
		return Integer.parseInt(text); 
	}
}

public class StringCalculatorTest {

	[...] 생략
	
	@Test
	public void add_shimpyo() {
		//숫자 두개를 쉽표로 구분된 문자열을 넣었을 때 
		assertEquals(3, cal.add("1,2"));
	}
}


2.4.4 Code Refactoring: 하나의 메소드가 하나의 역할을 가지도록 분리

public class StringCalculator {

	public int add(String text) {
		if (text == null || text.isEmpty()) {
			return 0;
		}

		String[] values = text.split(",");
		return sum(values);
	}
	
	private int sum(String[] values) {
		int sum = 0;
		for(String value: values) {
			sum+=Integer.parseInt(value);
		}
		return sum; 
	}
}


2.4.5 숫자 두개를 쉼표(,) or 콜론이 사용되었을 때 ("1,2:3" => 6)

public class StringCalculator {

	public int add(String text) {
		if (text == null || text.isEmpty()) {
			return 0;
		}

		String[] values = split(text);
		return sum(values);
	}
	
	[...]
    
	private String[] split(String text) {
		return text.split(",|:");
	}
}


public class StringCalculatorTest {

	private StringCalculator cal;
	
	@Before
	public void setup() {
		cal = new StringCalculator();
	}
	
	[...]
	
	@Test
	public void add_shimOrCol() {
		//숫자가 쉼표 또는 콜론으로 구분된 문자열을 넣었을 때 
		assertEquals(6, cal.add("1,2:3"));
	}
	
}


2.4.6 "//"와 "\n" 문자 사이에 커스텀 구분자를 지정할 수 있다 ("//;\n1;2;3" => 6)

public class StringCalculator {

	public int add(String text) {
		if (isBlank(text)) {
			return 0;
		}

		String[] values = split(text);
		return sum(values);
	}

	[...]
    
	private String[] split(String text) {
		
		//패턴과 매칭되는 문자열을 넣는다
		Matcher m = Pattern.compile("//(.)\n(.*)").matcher(text);
		
		//매칭되는 구분자가 있다면 
		if(m.find()) {
			String customDelimeter = m.group(1);
			return m.group(2).split(customDelimeter);
		}
		
		return text.split(",|:");
	}
    
    [...]
	
}


public class StringCalculatorTest {

	private StringCalculator cal;
	
	@Before
	public void setup() {
		cal = new StringCalculator();
	}
	
	[...]
	
	@Test
	public void add_custom() throws Exception{
		//커스텀 문자열이 들어오면 
		assertEquals(6, cal.add("//;\n1;2;3"));
	}
	
}


2.4.7 문자열 계산기에 음수를 전달하는 경우 RuntimeException 예외를 throw한다

public class StringCalculator {

	public int add(String text) {
		if (isBlank(text)) {
			return 0;
		}

		String[] values = split(text);
		return sum(values);
	}
	
	private int sum(String[] values) {
		int sum = 0;
		for(String value: values) {
			sum+=toPositive(value);
		}
		return sum; 
	}
    
    [...]
    
    private int toPositive(String value) {
		
		int number = Integer.parseInt(value);
		if(number < 0 ) {
			throw new RuntimeException();
		}
		return number;
	}
	


public class StringCalculatorTest {

	private StringCalculator cal;
	
	@Before
	public void setup() {
		cal = new StringCalculator();
	}
	
	[...]
	
	@Test(expected = RuntimeException.class)
	public void add_negative() throws Exception{
		cal.add("-1,2,3");
	}
	
}


2.5 최종 코드

  • StringCalculator.java
package stringcalculator;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class StringCalculator {

	public int add(String text) {
		if (isBlank(text)) {
			return 0;
		}

		String[] values = split(text);
		return sum(values);
	}
	
	private int sum(String[] values) {
		int sum = 0;
		for(String value: values) {
			sum+=toPositive(value);
		}
		return sum; 
	}
	
	private String[] split(String text) {
		
		//패턴과 매칭되는 문자열을 넣는다
		Matcher m = Pattern.compile("//(.)\n(.*)").matcher(text);
		
		//매칭되는 구분자가 있다면 
		if(m.find()) {
			String customDelimeter = m.group(1);
			return m.group(2).split(customDelimeter);
		}
		
		return text.split(",|:");
	}
	
	private boolean isBlank(String text) {
		return text == null || text.isEmpty();

	}
	
	private int toPositive(String value) {
		
		int number = Integer.parseInt(value);
		if(number < 0 ) {
			throw new RuntimeException();
		}
		return number;
	}
}


  • StringCalculatorTest.java
package stringcalculator;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.Before;
import org.junit.Test;

public class StringCalculatorTest {

	private StringCalculator cal;
	
	@Before
	public void setup() {
		cal = new StringCalculator();
	}
	
	@Test
	public void add_null() {
		//null값 또는 빈 문자열을 넣었을 때 
		assertEquals(0, cal.add(null));
		assertEquals(0, cal.add(""));
	}
	
	@Test
	public void add_oneNum() throws Exception {
		//숫자 하나를 문자열로 넣었을 때 
		assertEquals(1, cal.add("1"));
	}
	
	@Test
	public void add_shimpyo() {
		//숫자 두개를 쉼표로 구분된 문자열을 넣었을 때 
		assertEquals(3, cal.add("1,2"));
	}
	
	@Test
	public void add_shimOrCol() {
		//숫자가 쉼표 또는 콜론으로 구분된 문자열을 넣었을 때 
		assertEquals(6, cal.add("1,2:3"));
	}
	
	@Test
	public void add_custom() throws Exception{
		//커스텀 문자열이 들어오면 
		assertEquals(6, cal.add("//;\n1;2;3"));
	}
	
	@Test(expected = RuntimeException.class)
	public void add_negative() throws Exception{
		cal.add("-1,2,3");
	}
	
}

0개의 댓글