[SpringBoot] 전국 병원 정보 API 만들기 1일차

배지원·2022년 11월 1일
0

실습

목록 보기
9/24
post-custom-banner

📄 전국 병의원 정보 검색 API

💡 JAVA → Spring → SpringBoot 순으로 지금까지 배웠던 내용을 복습하면서 API 만들어보기

1. 진행 순서

  1. FileLineParser
  2. DB select, insert를 jdbcTemplate, Create Table
  3. Spring JdbcTemplate
  4. Spring Boot과 JdbcTemplate 연동이 왜 되는 것일까? IoC DI는 어떻게 작동하길래 될까? → 복습겸 응용
  5. 빌드 - Maven
  6. 다양한 정보를 조회할 수 있는 API만들기

DataBase

대용량 데이터(전국 병의원 정보)

fulldata01_01_02_P의원.csv


2. Mysql

(1) Table 설계

  • 어떤 값들을 추출하고 컬럼은 어떤것으로 할 것인지?
no컬럼명Java타입설명
1id(pk)idInt번호
2open_service_nameopenServiceNameVARCHAR(10)개방서비스명
3open_local_government_codeopenLocalGovernmentCodeint개방자치단체코드
4management_number(unique)managementNumbervarchar(40)관리번호
5license_datelicense_datedatetime인허가일자
6business_statusbusinessStatustinyint(2)1: 영업/정상
2: 휴업
3: 폐업
4: 취소/말소영업상태구분
7business_status_codebusinessStatusCodetinyint(2)영업상태코드
2: 휴업
3: 폐업
13: 영업중
8phonephonevarchar(20)소재지전화
9full_addressfullAddressVARCHAR(200)소재지전체주소
10road_name_addressroadNameAddressVARCHAR(200)도로명전체주소
11hospital_namehospitalNameVARCHAR(20)사업장명(병원이름)
12business_type_namebusinessTypeNameVARCHAR(10)업태구분명
13healthcare_provider_counthealthcareProviderCounttinyint(2)의료인수
14patient_room_countpatientRoomCounttinyint(2)입원실수
15total_number_of_bedstotalNumberOfBedstinyint(2)병상수
16total_area_sizetotalAreaSizefloat총면적

(2) Table 생성

  • (1) 직접 설정해서 넣기

  • (2) SQL 구문을 통해 넣기
CREATE TABLE `likelion-db`.`nation_wide_hospitals` (
  `id` INT NOT NULL,
  `open_service_name` VARCHAR(10) NULL COMMENT '개방서비스명',
  `open_local_government_code` INT NOT NULL COMMENT '개방자치단체코드',
  `management_number` VARCHAR(45) NULL COMMENT '관리번호',
  `license_date` DATETIME NULL COMMENT '인허가일자',
  `business_status` TINYINT(2) NULL COMMENT '영업상태구분(boolean)\n1: 영업/정상\n2: 휴업\n3: 폐업\n4: 취소/말소',
  `business_status_code` TINYINT(2) NULL COMMENT '영업상태코드\n2: 휴업\n3: 폐업\n13: 영업중',
  `phone` VARCHAR(20) NULL COMMENT '소재지전화',
  `full_address` VARCHAR(200) NULL COMMENT '소재지전체주소',
  `road_name_address` VARCHAR(200) NULL COMMENT '도로명전체주소',
  `hospital_name` VARCHAR(20) NULL COMMENT '사업자명',
  `business_type_name` VARCHAR(10) NULL COMMENT '업태구분명',
  `healthcare_provider_count` TINYINT(2) NULL COMMENT '의료인수',
  `patient_room_count` INT NULL COMMENT '입원실수',
  `total_number_of_beds` INT NULL COMMENT '병상수',
  `total_area_size` FLOAT NULL COMMENT '총면적',
  PRIMARY KEY (`id`),
  UNIQUE INDEX `management_number_UNIQUE` (`management_number` ASC) VISIBLE);

❓ 컬럼 타입을 세분화 하는 이유 ?

DB에 저장공간을 줄이고, 검색 속도를 최적화 하기 위함

Tinyint

  • 작은 int 범위가 작지만 int에 비해 공간을 덜 차지 한다.
  • 크기 : 0 ~ 255
  • 용량 : 1바이트

Smallint

  • 크기 : -32,768 ~ 32,767
  • 용량 : 2바이트

Int

  • 크기 : 2,147,483,648 ~ 2,147,483,647
  • 용량 : 4바이트

Bigint

  • 크기 : 9,223,372,036,854,775,808‬ ~ 9,223,372,036,854,775,8087
  • 용량 : 8바이트
tinyintsmallintintbigint
크기0 ~ 2^8-2^15 ~ (2^15 - 1)-2^31 ~ (2^31 - 1)-2^63 ~ (2^63 - 1)
8 = 2^315 = 2^4 -131 = 2^5 - 163 = 2^6 - 1
용량1 byte2 byte4 byte8 byte
1 bytetinyint * 2 bytesmallint * 2 byteint * 2 byte

DateTime

  • 날짜와 시간을 모두 포함할때 사용하는 타입이다.
  • YYYY-MM-DD HH:MM:SS 형식 입력으로 1000-01-01 00:00:00 부터 9999-12-31 23:59:59까지 입력이 가능하다.
  • 게시판을 만들때는 댓글기능과 최신순 정렬등 날짜와 시간에 따라 정렬을 해줘야 하는 기능들이 많기 때문에 작성한 날짜와 시간을 저장하기 위해 DateTime을 많이 사용한다.

Float

  • 부동 소수점 타입으로 실수의 값을 대략적으로 표현하기 위해 사용함
  • 용량 : 4바이트

3. Hospital Domain 생성

Lombok이란?


Java 라이브러리로 반복되는getter,setter,toString 등의 메서드 작성 코드를 줄여주는 코드 다이어트 라이브러리입니다.


즉, Lombok을 사용하면 생성자, getter/setter/toString 등의 메서드를 작성하지 않아도 실제로 컴파일된 결과물에서는 자동으로 생성이 되어 있다.


@NoArgsConstructor

  • 비어있는 생성자를 자동으로 만들어줌


    @AllArgsConstructor
  • 모든 필드값을 파라미터로 받는 생성자를 자동으로 만들어줌


    @Getter/Setter
  • Getter/Setter를 자동으로 만들어줌


    @ToString
  • toString을 자동으로 만들어줌

CODE

@AllArgsConstructor         // lombok이 알아서 생성자를 만들어줌 즉, 내가 만들지 않아도 됨
@Getter                     // Getter 생성
@NoArgsConstructor          // 빈 생성자 생성
@Setter
public class Hospital {
    private int id;
    private String openServiceName;
    private int openLocalGovernmentCode;
    private String managementNumber;
    private LocalDateTime licenseDate;
    private int businessStatus;
    private int businessStatusCode;
    private String phone;
    private String fullAddress;
    private String roadNameAddress;
    private String hospitalName;
    private String businessTypeName;
    private int healthcareProviderCount;
    private int patientRoomCount;
    private int totalNumberOfBeds;
    private float totalAreaSize;
}
  • 이전에 Mysql에서 DB를 설계할때 어떤 값들을 추출하고 컬럼과 변수명을 정했었다. 이곳에서도 변수명과 타입을 똑같이 설계해주면 된다. 그래야 DB와 연동할때 편해진다.

4. Parser

(1) 원하는 값 추출

(1) Parser(Interface)

public interface Parser<T> {
    T parse(String str);
}

(2) HospitalParser

public class HospitalParser implements Parser<Hospital> {
    @Override
    public Hospital parse(String str) {
        String[] row = str.split("\",\"");      // \,\ 기준으로 나눔

        Hospital hospital = new Hospital();
        hospital.setId(Integer.parseInt(row[0].replace("\"","")));  // 첫번째는 \으로 시작하기 때문에 replace해주고 시작
        hospital.setOpenServiceName(row[1]);
        hospital.setOpenLocalGovernmentCode(Integer.parseInt(row[3]));
        hospital.setManagementNumber(row[4]);

        int year = Integer.parseInt(row[5].substring(0, 4));
        int month = Integer.parseInt(row[5].substring(4, 6));
        int day = Integer.parseInt(row[5].substring(6, 8));
        hospital.setLicenseDate(LocalDateTime.of(year,month,day,0,0,0));

        hospital.setBusinessStatus(Integer.parseInt(row[7]));
        hospital.setBusinessStatusCode(Integer.parseInt(row[9]));
        hospital.setPhone(row[15]);
        hospital.setFullAddress(row[18]);
        hospital.setRoadNameAddress(row[19]);
        hospital.setHospitalName(row[21]);
        hospital.setBusinessTypeName(row[25]);
        hospital.setHealthcareProviderCount(Integer.parseInt(row[29]));
        hospital.setPatientRoomCount(Integer.parseInt(row[30]));
        hospital.setTotalNumberOfBeds(Integer.parseInt(row[31]));
        hospital.setTotalAreaSize(Float.parseFloat(row[32].replace("\"","")));  // 마지막은 \로 끝나기 때문에 replace로 지워준다
        
        return hospital;
    }
}
  • 이전에 데이터베이스를 설계하면서 어떤 값들을 추출할지 정했었는데 그 값을만을 추출하기 위해 set을 통해 그 값들만을 저장해 준다.

💡 LocalDateTime은 초기화 하는 방식이 다르다


각 년,월,일을 각자 받아서 나중에 한번에 초기화 해줘야한다.


@substring( ) 를 통해 원하는 구역별로 나눌 수가 있다





위의 표처럼 0~3까지는 year, 4~5까지는 month, 6~7까지는 day의 값을 입력받아 @LocalDateTime.of(year,month,day,hour,minute,second) 를 통해 값을 넣어 초기화 할 수 있다.

(3) HospitalParserTest

String line1 = "\"1\",\"의원\",\"01_01_02_P\",\"3620000\",\"PHMA119993620020041100004\",\"19990612\",\"\",\"01\",\"영업/정상\",\"13\",\"영업중\",\"\",\"\",\"\",\"\",\"062-515-2875\",\"\",\"500881\",\"광주광역시 북구 풍향동 565번지 4호 3층\",\"광주광역시 북구 동문대로 24, 3층 (풍향동)\",\"61205\",\"효치과의원\",\"20211115113642\",\"U\",\"2021-11-17 02:40:00.0\",\"치과의원\",\"192630.735112\",\"185314.617632\",\"치과의원\",\"1\",\"0\",\"0\",\"52.29\",\"401\",\"치과\",\"\",\"\",\"\",\"0\",\"0\",\"\",\"\",\"0\",\"\",";
    
    @Test
    @DisplayName("csv 1줄을 Hospital로 잘 만드는지 Test")
    void converToHospital(){
        HospitalParser hp = new HospitalParser();
        Hospital hospital = hp.parse(line1);

        assertEquals(1, hospital.getId()); // col:0
        assertEquals("의원", hospital.getOpenServiceName());//col:1
        assertEquals(3620000,hospital.getOpenLocalGovernmentCode()); // col: 3
        assertEquals("PHMA119993620020041100004",hospital.getManagementNumber()); // col:4
        assertEquals(LocalDateTime.of(1999, 6, 12, 0, 0, 0), hospital.getLicenseDate()); //19990612 //col:5
        assertEquals(1, hospital.getBusinessStatus()); //col:7
        assertEquals(13, hospital.getBusinessStatusCode());//col:9
        assertEquals("062-515-2875", hospital.getPhone());//col:15
        assertEquals("광주광역시 북구 풍향동 565번지 4호 3층", hospital.getFullAddress()); //col:18
        assertEquals("광주광역시 북구 동문대로 24, 3층 (풍향동)", hospital.getRoadNameAddress());//col:19
        assertEquals("효치과의원", hospital.getHospitalName());//col:21
        assertEquals("치과의원", hospital.getBusinessTypeName());//col:25
        assertEquals(1, hospital.getHealthcareProviderCount()); //col:29
        assertEquals(0, hospital.getPatientRoomCount()); //col:30
        assertEquals(0, hospital.getTotalNumberOfBeds()); //col:31
        assertEquals(52.29f, hospital.getTotalAreaSize()); //col:32
    }
  • 1번째 줄에 있는 데이터를 문자열로 저장하고 assertEquals를 통해 각 분야별로 비교해본다.
  • 데이터를 문자열로 저장하기 위해서는 먼저 String line1 = ""; 틀을 만들고 난 후 String line1 = "Data"; 데이터를 넣게되면 intellij에서 자동으로 "\" 백슬러쉬를 넣어 구분해준다.

(2) 파일 읽고 데이터 추출

(1) ReadLineContext

public class ReadLineContext<T>{
    private Parser<T> parser;

    public ReadLineContext(Parser<T> parser){
        this.parser = parser;
    }

    public List<T> readByLine(String filename) throws IOException {

        List<T> result = new ArrayList<>();
        BufferedReader reader = new BufferedReader(
                new FileReader(filename)
        );
        String str;
        while ((str = reader.readLine()) != null) {
            try {
                result.add(parser.parse(str));
            }catch(Exception e){
                System.out.printf("파싱중 문제가 생겨 이 라인은 넘어갑니다. 파일내용:%s",str.substring(0,20));
            }
        }
        reader.close();
        return result;
    }

}
  • File을 한줄씩 읽어 원하는 데이터를 파싱하여 저장하는 곳

(2) ParserFactory(DI/IOC)

@Configuration
public class ParserFactory {
    
    @Bean
    public ReadLineContext<Hospital> hospitalReadLineContext(){
        return new ReadLineContext<Hospital>(new HospitalParser());
    }
}
  • ReadLineContext와 HospitalParser간의 의존 작업을 수행한다.
    • ReadLineContext 클래스의 생성자로 HospitalParser의 구현체를 보내어 ReadLineContext의 Parser 구현체에 저장한다.
  • Bean파일로 설정하기 위해서는 메서드 위에@Bean 을 작성하여 Bean으로 설정할 수 있다.
  • 이때, @Bean을 사용하기 위해서는 클래스명 위에 @Configuration을 사용해야 @Bean을 사용할 수 있으며 Spring의 설정 파일이라는 것을 의미한다.

💡 Bean ?

  • Spring Ioc 컨테이너가 관리하는 자바 객체를 빈이라고 부른다.
  • Spring은 싱글톤 객체만 사용하기 때문에 Bean을 통해 사용
  • 의존성 주입

(3) ParserFactoryTest

@SpringBootTest(classes = ParserFactory.class)
class HospitalParserTest {
    
    @Autowired      // new를 통한 객체 생성 대신 spring에서 자동으로 autowired를 통해 DI를 해줌
    ReadLineContext<Hospital> hospitalReadLineContext;

    @Test
    void name() throws IOException {
        // 서버환경에서 build할 때 문제가 생길 수 있다.
        // 어디에서든 실행할 수 있게 자는 것이 목표
        String filename = "C:\\SpringBoot_DBFile\\fulldata_01_01_02_P_의원2.csv";
        List<Hospital> hospitalList = hospitalReadLineContext.readByLine(filename);
        assertTrue(hospitalList.size() > 1000);
        assertTrue(hospitalList.size() > 10000);
    }
  • Spring의 싱글톤을 위해 Bean파일을 호출하기 위해서는 @SpringBootTest(classes = ParserFactory.class)를 통해 Test클래스에서 Bean을 사용할 수 있도록 설정해주며 어떤 Bean 파일을 사용할 것인지도 선언해 준다.
  • @Autowired 를 통해 해당 변수 및 메서드에 스프링이 관리하는 Bean을 자동으로 매핑해주는 개념이다. 즉, Bean에 기능을 부여한다고 봐도 될것 같다
profile
Web Developer
post-custom-banner

0개의 댓글