TDD 방법론
1. TDD의 개념
TDD란?
•
Test Driven Development의 약자로 테스트 주도 개발이라고 한다.
•
작은 단위의 테스트 케이스를 작성하고 이를 통과하는 코드를 실제 프로그램에 사용하는 단계를 반복한다.
•
과정
◦
RED : 실패하는 테스트 코드를 먼저 작성
◦
GREEN : 테스트 코드를 성공시키기 위한 실제 코드를 작성
◦
YELLOW : 중복 코드 제거, 일반화 등의 리팩토링을 수행
TDD를 사용해야하는 이유
•
일반 개발 방식은 요구사항 분석→설계→개발→테스트→배포 형태의 과정을 거친다.
•
이러한 과정은 잠재적인 위험도를 가진다
1.
모든 개발이 이루어지고 테스트를 한다면 시간이 오래 걸린다.
2.
완벽한 설계는 어렵다.
3.
리팩토링을 하기가 힘들어진다.
•
TDD는 테스트 코드를 통과한 코드를 실제 프로그램에 작성하기 때문에 효율적이다.
TDD의 장단점
•
장점
◦
재설계 사간의 단축
◦
디버깅 시간 단축
◦
테스트 문서의 대체 가능
◦
추가 구현의 용이함
•
단점
◦
생산성 저하
TDD를 적용한 공공데이터 읽어오기
1. 목표
TDD를 적용하여 서울시 병의원 위치 정보 데이터를 읽어온다.
Java
복사
2. 설계 (클래스 다이어그램)
•
클래스
◦
main : LineReader, LineWriter, Hospital를 사용하여 파일을 파싱하는 클래스
◦
LIneReader : 파일을 읽어와서 파싱하는 클래스
▪
Paser 인터페이스 변수를 선언하여 Parser의 구현 클래스의 오버라이딩 메소드를 호출
◦
LineWriter : 파일에 sql문을 작성하는 클래스
▪
Writer 인터페이스 변수를 선언하여 Writer의 구현 클래스의 오버라이딩 메소드를 호출
◦
Hospital : 병원 정보를 담는 클래스
◦
SqlWriter : LineWriter를 통해서 파일에 입력할 때 sql문 string을 반환해주는 클래스
▪
Writer 인터페이스를 상속받아 writeStr 메소드를 오버라이딩 한다.
▪
매개변수는 Hospital 타입
◦
HospitalParser : LineReader를 통해서 파일을 파싱할 때 Hospital클래스의 객체에 정보를 담아 객체를 반환해주는 클래스
▪
Parser를 상속받아 parser 메소드를 오버라이딩한다.
▪
리턴타입은 Hospital 타입
•
인터페이스
◦
Parser : LineReader 클래스를 사용할 때 Parser 인터페이스를 통해서 다형성을 실현하고, 다양한 형태의 List들을 반환한다.
◦
Writer : LineWrite 클래스를 사용할 때 Writer 인터페이스를 통해서 다형성을 실현하고, 다양한 형태의 string을 반환한다. (ex. sql문 또는 병원의 정보를 담은 toString문)
•
인터페이스를 Writer에 적용한 이유
◦
미래에 LineReader, LineWriter를 다시 사용할 때 이것을 구현하지 않고 인터페이스를 상속받아 구현클래스를 생성하여 재사용하기 위해서이다.
3. 테스트 케이스 작성
테스트가 필요한 부분
1.
id(병원의 id)가 잘 파싱되었는지
2.
address(병원의 주소)가 잘 파싱되었는지
3.
district(시와 구)가 잘 파싱되었는지
4.
category(의원, 병원 구분)가 잘 파싱되었는지
5.
emergency_room(응급실 존재 여부)이 잘 파싱되었는지
6.
name(병원이름)이 잘 파싱되었는지
7.
subdivision(세부분과)이 잘 파싱되었는지
테스트 케이스 작성
•
gradle 빌드하는 과정
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import public_data.domain.Hospital;
import public_data.parser.HospitalParser;
import static org.junit.jupiter.api.Assertions.*;
class HospitalParserTest {
String line1 = "\"A1120837\",\"서울특별시 금천구 벚꽃로 286 삼성리더스타워 111~114호 (가산동)\",\"C\",\"의원\",\"G099\",\"응급의료기관 이외\",\"2\",\"외과: 상시진료 내과는 당분간 휴진\",\"서울시 송파구 문정동 장지동 법조단지 위례 가락동 가락시장역 위치 삼성서울병원 외래교수 출신 구강외과 전문의 진료 진료과목 - 임플란트 치조골 뼈이식 수술 매복 사랑니 발치 턱관절 악관절 질환의 치료 교정 치료 및 기타 보존 보철(크라운 브릿지 인레이) 신경치료\",\"방이역 1번출구 바로옆 굿모닝 신한증권 뒷건물\",\"가산기대찬치과의원\",\"02-6267-2580\",\"02-920-5374\",\"1930\",\"1930\",\"1930\",\"1930\",\"1930\",\"1500\",\"1500\",\"1500\",\"0900\",\"0900\",\"0900\",\"0900\",\"0900\",\"0900\",\"1000\",\"1000\",\"085\",\"11\",\"126.88412249700781\",\"37.4803938036867\",\"2022-04-07 14:55:00.0\"";
@Test
@DisplayName("test case")
void Parsing(){
HospitalParser hospitalParser = new HospitalParser();
Hospital hospital = hospitalParser.parse(line1);
Assertions.assertEquals("A1120837", hospital.getId());
Assertions.assertEquals("서울특별시 금천구 벚꽃로 286 삼성리더스타워 111~114호 (가산동)", hospital.getAddress());
Assertions.assertEquals("서울특별시 금천구", hospital.getDistrict());
Assertions.assertEquals("C",hospital.getCategory());
Assertions.assertEquals(2,hospital.getEmergency_room());
Assertions.assertEquals("가산기대찬치과의원",hospital.getName());
Assertions.assertEquals("치과",hospital.getSubdivision());
Assertions.assertEquals("INSERT INTO `hospital_data`.`seoul_hospital`\n" +
"('id`,\n" +
"`address`,\n" +
"`district`,\n" +
"`category`,\n" +
"`emergency_room`,\n" +
"`name`,\n" +
"`subdivision`)\n" +
"VALUES\n" +
"('A1120837',\n" +
"'서울특별시 금천구 벚꽃로 286 삼성리더스타워 111~114호 (가산동)',\n" +
"'서울특별시 금천구',\n" +
"'C',\n" +
"2,\n" +
"'가산기대찬치과의원',\n" +
"'치과');",hospital.getSqlInsert());
}
}
Java
복사
4. 개발 후 사용된 코드
Hospital
package public_data.domain;
public class Hospital {
private String id;
private String address;
private String district;
private String category;
private Integer emergency_room;
private String name;
private String subdivision;
public Hospital(String id, String address,String district, String category, Integer emergency_room, String name, String subdivision) {
this.id = id;
this.address =address;
this.district=district;
this.category=category;
this.emergency_room=emergency_room;
this.name=name;
this.subdivision=subdivision;
}
public String getId() {
return id;
}
public String getAddress() {
return address;
}
public String getDistrict() {
return district;
}
public String getCategory() {
return category;
}
public Integer getEmergency_room() {
return emergency_room;
}
public String getName() {
return name;
}
public String getSubdivision() {
return subdivision;
}
@Override
public String toString() {
return "Hospital{" +
"id='" + id + '\'' +
", address='" + address + '\'' +
", district='" + district + '\'' +
", category='" + category + '\'' +
", emergency_room=" + emergency_room +
", name='" + name + '\'' +
", subdivision='" + subdivision + '\'' +
'}';
}
}
Java
복사
Parser<interface>
package public_data.parser;
public interface Parser<T> {
// 어떤 파일을 파싱하느냐에 따라 리턴하는 클래스가 달라진다.
T parse(String str);
}
Java
복사
HospitalParser
package public_data.parser;
import public_data.domain.Hospital;
public class HospitalParser implements Parser<Hospital> {
@Override
public Hospital parse(String str) {//String -> Hospital
str=str.replaceAll("\"","");
String[] splitted = str.split(",");
return new Hospital(splitted[0],splitted[1],this.parsing_district(splitted[1]),splitted[2],Integer.parseInt(splitted[6]),splitted[10],this.parsing_subdivision(splitted[10]));
}
public String parsing_district(String str){
String[] arr =str.split(" ");
return String.format("%s %s",arr[0],arr[1]);
}
public String parsing_subdivision(String name){
String[] subdivisions = {"내과", "외과", "소아과", "피부과", "성형외과", "정형외과", "산부인과", "관절", "안과", "가정의학과", "비뇨기과", "치과"};
for (String str: subdivisions) {
if(name.contains(str)) {
return str;
}
}
return "";
}
}
Java
복사
Writer<interface>
package public_data.writer;
public interface Writer<T> {//다양한 형태의 매개변수를 string을 변환하는 함수
public String writeStr(T t);
}
Java
복사
SqlWriter
package public_data.writer;
import public_data.domain.Hospital;
public class SqlWriter implements Writer<Hospital> {
@Override
public String writeStr(Hospital hospital) {//Hospital의 객체를 받고 sql문을 반환
String sql = String.format("INSERT INTO `hospital_data`.`seoul_hospital`" +
"(`id`," + "`address`," + "`district`," + "`category`," + "`emergency_room`," + "`name`," + "`subdivision`)" + "VALUES" +
"('%s'," +
"'%s'," +
"'%s'," +
"'%s'," +
"%d," +
"'%s'," +
"'%s');\n",hospital.getId(),hospital.getAddress(),hospital.getDistrict(),hospital.getCategory(),hospital.getEmergency_room(),hospital.getName(),hospital.getSubdivision());
return sql;
}
}
Java
복사
LineReader
package public_data;
import public_data.parser.Parser;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class LineReader<T> {
Parser<T> parser;
boolean isRemoveColumnName = true;
public LineReader(Parser parser){
this.parser=parser;
// 다형성 실현
// 이자리에 어떤 구현 클래스가 대입되냐에 따라 파일 입력이 다양해진다.
}
public List<T> readLines(String filename)throws IOException {
BufferedReader br = new BufferedReader(new FileReader(filename));
List<T>result = new ArrayList<>();
String str;
if(isRemoveColumnName)
br.readLine();
while((str=br.readLine())!=null){
result.add(parser.parse(str));
}
return result;
}
}
Java
복사
LineWriter
package public_data;
import public_data.writer.Writer;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.List;
public class LineWriter<T> {
Writer<T> writer;
public LineWriter(Writer<T> write) {
this.writer = write;
}
public void writeLines(List<T> list, String filename) {
File file = new File(filename);
for (T t : list) {
try {
BufferedWriter br = new BufferedWriter(new FileWriter(file, true));
br.write(writer.writeStr(t));
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Java
복사
Main
package public_data;
import public_data.domain.Hospital;
import public_data.parser.HospitalParser;
import public_data.writer.SqlWriter;
import public_data.writer.Writer;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.List;
public class Main {
public static void write(String strs, String filename){
File file = new File(filename);
try{
BufferedWriter writer = new BufferedWriter(new FileWriter(file,true));
writer.write(String.format("%s\n",strs));
writer.close();
}catch (IOException e){
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
LineReader<Hospital> hospitalLineReader = new LineReader<>(new HospitalParser());
SqlWriter sql = new SqlWriter();
LineWriter<Hospital> hospitalLineWriter = new LineWriter(sql);
String filename = "./seoul_hospital.csv";
String sqlFile = "./seoul_hospital_sql.txt";
List<Hospital> hospitalList =hospitalLineReader.readLines(filename);
hospitalLineWriter.writeLines(hospitalList,sqlFile);
}
}
Java
복사
Writer 인터페이스를 굳이 사용한 이유?
•
다양한 객체가 매개변수로 전달 받아서 String으로 파일에 입력될 수 있는데 그때마다 다양한 매개변수 타입 String 반환 형태를 지정하여 메소드를 만들기는 비효율적
•
인터페이스를 제네릭으로 선언하여 다양한 형태의 list를 받을 수 있도록 작성했다.
◦
앞으로 파일에 객체를 string으로 변환하여 입력할 일이 있으면 interface만 상속받아 오버라이딩 해주면 된다.
•
솔직히 굳이 인터페이스를 사용하지 않고 String으로 미리 변환하여 넘겨줄 수 있긴 하다.
•
oop를 적용해보고 싶어서 사용했다.
공부해야 하는 것
1.
gradle이 무엇인지 정확하게 알기
2.
설계 단계에서 다이어그램 표현 방식 공부하기