///////
Search

TDD,공공데이터 활용_김지수

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.
설계 단계에서 다이어그램 표현 방식 공부하기