페이지

2021년 12월 2일 목요일

Swagger 설치 및 설정 기초

Swagger 설치 및 설정 기초

1. 소개

  • OpenAPI Specification - REST API를 정의하는 형식에 대한 규격이며 이전에는 Swagger Specification이라고 하였습니다.
  • Swagger Tool - OpenAPI를 정의할 수 있는 편집기, API를 테스트할 수 있는 UI 등을 포함하고 있습니다.

2. REST API 서버

  1. Swagger Tool로 테스트하고자 하는 API 서버를 실행합니다. API 서버의 URL이 다음과 같다고 가정합니다.

    • URL: http://{server-addr}:8080/api

3. Swagger Editor

3.1. Swagger Editor 다운로드

  1. GitHub에서 다운로드합니다.
  2. 다운로드한 파일의 압축을 풉니다. 이후부터 압축을 푼 폴더를 {editor-dir}로 표기합니다.

3.2. 브라우져에서 직접 열기

  1. 브라우져에서 {editor-dir}/index.html 파일을 엽니다.
  2. 이전에 작성해 두었던 API 정의 파일이 있으면 File -> Import file 메뉴를 통해서 불러 옵니다.

3.3. Node.js를 사용하여 서버로 띄우기

  1. Node.js를 설치합니다.

  2. http-server 모듈을 설치하고 시작합니다.

    npm install -g http-server
    http-server -p 8061 {editor-dir}
    
  3. 브라우져에서 http://localhost:8061 주소를 엽니다.

  4. 이전에 작성해 두었던 API 정의 파일이 있으면 File -> Import file 메뉴를 통해서 불러 옵니다.

3.4. 도커로 띄우기

3.4.1. 설치 및 실행

  1. 도커 이미지를 설치합니다.

    sudo docker pull swaggerapi/swagger-editor
    
  2. 도커 이미지를 실행합니다.

    # 백그라운드 모드로 도커 이미지 실행 
    sudo docker run -d -p 8061:8080 -e BASE_URL=/swagger-editor -e SWAGGER_JSON=/mnt/api.yaml -v /home/test:/mnt swaggerapi/swagger-editor
    
  3. 브라우져에서 http://localhost:8061/swagger-editor 주소를 엽니다.

3.4.2. 도커 이미지 종료

  1. 도커 컨테이너 목록을 표시합니다.

    sudo docker container ls
    CONTAINER ID        IMAGE                   COMMAND                  CREATED             STATUS              PORTS                          NAMES
    a0242be185b4        swaggerapi/swagger-ui   "/docker-entrypoin..."   6 seconds ago       Up 5 seconds        80/tcp, 0.0.0.0:80->8080/tcp   gifted_payne
    
  2. 도커 컨테이너를 종료합니다.

    sudo docker stop gifted_payne
    

3.5. CORS 문제 해결

3.5.1. 문제 증상

Swagger Editor나 Swagger UI에서 API를 테스트했을 때 Swagger 도구에서 아래와 같은 오류 메시지를 표시하는 경우가 있습니다.

Failed to fetch.
Possible Reasons:
* CORS
* Network Failure
* URL scheme must be “http” or “https” for CORS request.

3.5.2. CORS 문제인지 확인하는 방법

크롬 브라우져를 사용하는 경우에 대하여 설명합니다.

  1. 브라우져의 도구 더보기 - 개발자 도구 메뉴를 클릭합니다.
  2. 개발자 도구 창에서 Console 탭을 클릭합니다.
  3. 오류 로그에 blocked by CORS policy 문구가 포함되어 있으면 CORS 문제입니다.

3.5.3. 문제의 원인 및 해결 방법

Swagger 서버 주소와 API 서버 주소간 다음 세 가지 중 하나라도 다르면 CORS 문제가 발생할 수 있습니다.

  • URL 스킴(“http”, “https”)
  • 도메인
  • 포트

브라우져는 JavaScript를 다운받은 서버 주소(여기서는 Swagger 서버)와 JavaScript가 XMLHttpRequest를 사용하여 연결하고자 하는 서버(여기서는 API 서버) 주소가 서로 다를 때 연결하고자 하는 서버가 CORS를 지원하는지 검사합니다. 이 검사를 통과하지 못하면 브라우져는 CORS 오류 메시지를 출력하고 API 호출을 중단합니다.

이 문제를 해결하는 방법으로 여러 가지가 있습니다.

  1. Swagger 서버와 API 서버를 동일한 주소로 서비스
  2. API 서버가 CORS를 지원하도록 구성
  3. Swagger Hub 이용
  4. 브라우져의 CORS 검사 기능을 비활성화 (보안 우려로 비추천)

여기서는 두 번째 방법으로 문제를 해결하고자 하며 다음은 API 서버가 Spring Boot를 사용하는 경우에 대한 해결 방법입니다.

  1. HttpSecuritycors()를 호출합니다.

    @Configuration  
    @EnableWebSecurity  
    @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)  
    public class SecurityConfig extends WebSecurityConfigurerAdapter {  
        @Override  
        public void configure(HttpSecurity http) throws Exception {  
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);  
            http.authorizeRequests().anyRequest().permitAll();  
            http.csrf().disable();  
            http.headers().frameOptions().disable();  
            http.cors();  
        }  
    }
    
  2. CorsRegistryCorsRegistration을 설정합니다.

    @EnableWebMvc  
    @Configuration  
    public class WebMvcConfig implements WebMvcConfigurer {
        @Override  
        public void addCorsMappings(CorsRegistry registry) {  
            CorsRegistration registration = registry.addMapping("/**");  
            registration.allowedMethods("DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT");
        }
    }
    

3.6. API 정의를 파일로 저장하기

  • File -> Save as YAML 메뉴를 클릭하고 api.yaml과 같은 이름을 지정하여 파일로 저장합니다.

4. Swagger UI

4.1. api.yaml 파일 준비

4.1.1. API 서버 주소 지정

servers:
- url: http://{server-addr}:8080/api

4.2. 도커로 띄우기

4.2.1. 설치 및 실행

  1. 도커 이미지를 설치합니다.

    # yum 최신 업데이트
    sudo yum update
    
    # docker 설치 및 시작
    sudo yum install docker
    sudo systemctl start docker
    
    # swagger-ui docker 이미지 pull 
    sudo docker pull swaggerapi/swagger-ui
    
  2. 도커 이미지를 실행합니다.

    # 백그라운드 모드로 도커 이미지 실행
    sudo docker run -d -p 8060:8080 -e BASE_URL=/swagger-ui -e SWAGGER_JSON=/mnt/api.yaml -v /home/test:/mnt swaggerapi/swagger-ui
    
  3. 브라우져에서 http://localhost:8060/swagger-ui 주소를 엽니다.

4.2.2. 도커 이미지 종료

  1. 도커 컨테이너 목록을 표시합니다.

    sudo docker container ls
    CONTAINER ID        IMAGE                   COMMAND                  CREATED             STATUS              PORTS                          NAMES
    a0242be185b4        swaggerapi/swagger-ui   "/docker-entrypoin..."   6 seconds ago       Up 5 seconds        80/tcp, 0.0.0.0:80->8080/tcp   gifted_payne
    
  2. 도커 컨테이너를 종료합니다.

    sudo docker stop gifted_payne
    

참고 자료

OpenAPI

CORS 지원

Written with StackEdit.

2021년 11월 29일 월요일

Tomcat 9 HTTPS 설정 기초

Tomcat 9 HTTPS 설정 기초

1. 개요

1.1. 목적

  • Tomcat 서버에 HTTPS 연결을 추가하는 방법에 대하여 설명합니다.

1.2. 환경

  • 운영체제: Windows 10
  • Java 버전: 8
  • Tomcat 버전: 9

1.3. 표기

다음 세 개의 값은 원하는 다른 값으로 지정할 수 있습니다.

  • your-alias
  • your-file
  • your-password

2. 키 저장소 및 키 생성

  1. 키 저장소와 키를 생성합니다. 키 비밀번호를 입력하는 단계에서 비밀번호를 입력하지 말고 그냥 Enter 키를 누릅니다. Tomcat에서 사용하기 위해서는 키 저장소와 키의 비밀번호가 같아야 합니다.
    >keytool -genkey -alias your-alias -keyalg RSA -keystore your-file.keystore  
    키 저장소 비밀번호 입력:  
    새 비밀번호 다시 입력:  
    이름과 성을 입력하십시오.  
     [Unknown]:  TOMCAT
    조직 단위 이름을 입력하십시오.  
     [Unknown]:  TOMCAT
    조직 이름을 입력하십시오.  
     [Unknown]:  TOMCAT
    구/군/시 이름을 입력하십시오?  
     [Unknown]:  SEOUL  
    시/도 이름을 입력하십시오.  
     [Unknown]:  SEOUL  
    이 조직의 두 자리 국가 코드를 입력하십시오.  
     [Unknown]:  82  
    CN=TOMCAT, OU=TOMCAT, O=TOMCAT, L=SEOUL, ST=SEOUL, C=82이(가) 맞습니까?  
     [아니오]:  예  
    ​  
    <your-alias>에 대한 키 비밀번호를 입력하십시오.  
     (키 저장소 비밀번호와 동일한 경우 Enter 키를 누름):  
    ​  
    Warning:  
    JKS 키 저장소는 고유 형식을 사용합니다. "keytool -importkeystore -srckeystore your-file.keystore -destkeystore your-file.keystore -deststoretype pkcs12"를 사용하는 산업 표준 형식인 PKCS12로 이전하는 것이 좋습니다.
    
  2. JKS 키 저장소를 PKCS12로 이전합니다.
    >keytool -importkeystore -srckeystore your-file.keystore -destkeystore your-file.keystore -deststoretype pkcs12  
    소스 키 저장소 비밀번호 입력:  
    your-alias 별칭에 대한 항목이 성공적으로 임포트되었습니다.  
    임포트 명령 완료: 성공적으로 임포트된 항목은 1개, 실패하거나 취소된 항목은 0개입니다.  
    ​  
    Warning:  
    "your-file.keystore"을(를) Non JKS/JCEKS(으)로 이전했습니다. JKS 키 저장소가 "your-file.keystore.old"(으)로 백업되었습니다.
    
  3. 인증서 목록을 확인합니다.
    >keytool -list -keystore your-file.keystore  
    키 저장소 비밀번호 입력:  
    키 저장소 유형: PKCS12  
    키 저장소 제공자: SUN  
    ​  
    키 저장소에 1개의 항목이 포함되어 있습니다.  
    ​  
    your-alias, 2021. 11. 30, PrivateKeyEntry,  
    인증서 지문(SHA1): 46:0A:69:93:A3:61:8A:F6:00:80:5A:A5:5A:5B:B1:23:9F:6F:FF:A8
    

3. Tomcat 9 설정

  1. %CATALINA_HOME%\conf\server.xml 파일에 아래의 내용을 추가합니다.
    <Connector port="8443" maxThreads="150" scheme="https" secure="true" SSLEnabled="true"   
     keystoreFile="\path\to\your-file.keystore" keystorePass="your-password"   
     clientAuth="false" keyAlias="your-alias" sslProtocol="TLS"/>
    
  2. Tomcat을 재시작합니다.
    >catalina.bat stop
    >catalina.bat start
    

4. 참고 문서

Written with StackEdit.

2021년 11월 8일 월요일

Spring Web + MyBatis + MySQL + Tomcat 사용 기초

Spring Web + MyBatis + MySQL + Tomcat 사용 기초

사용 환경

  • Windows 10
  • Java 1.8
  • Gradle 7.2
  • Spring Boot 2.5.6
  • MyBatis 2.2.0
  • MySQL 5.7
  • Tomcat 9, 10

프로젝트 따라하기

  1. 프로젝트 생성

    브라우져에서 spring initializr를 방문하여 아래와 같이 입력하고 GENERATE 버튼을 클릭합니다.

    • Project: Gradle Project
    • Language: Java
    • Spring Boot: 2.5.6
    • Project Meta:
      • Group: trvoid
      • Artifact: mysql-mybatis
      • Name: mysql-mybatis
      • Package name: trvoid.mybatis
      • Packaging: War
      • Java: 8
    • Dependencies:
      • Spring Web
      • JDBC API
      • MyBatis Framework
      • MySQL Driver

    생성된 프로젝트 파일을 다운로드하여 압축을 풉니다. 프로젝트 폴더 구조는 아래와 같습니다.

    mysql-mybatis
      |-src
        |-main
          |-java
            |-trvoid.mybatis
              |-MysqlMybatisApplication.java
              |-ServletInitializer.java
          |-resources
            |-application.properties
        |-test
          |-java
            |-trvoid.mybatis
              |-MysqlMybatisApplicationTests.java
      |-build.gradle
      |-gradlew.bat
    

    build.gradle

    plugins {
        id 'org.springframework.boot' version '2.5.6'
        id 'io.spring.dependency-management' version '1.0.11.RELEASE'
        id 'java'
        id 'war'
    }
    
    group = 'trvoid'
    version = '0.0.1-SNAPSHOT'
    sourceCompatibility = '1.8'
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-jdbc'
        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
        runtimeOnly 'mysql:mysql-connector-java'
        providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
    }
    
    test {
        useJUnitPlatform()
    }
    

    src/main/java/trvoid/mybatis/MysqlMybatisApplication.java

    package trvoid.mybatis;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class MysqlMybatisApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(MysqlMybatisApplication.class, args);
        }
    
    }
    

    src/main/java/trvoid/mybatis/ServletInitializer.java

    package trvoid.mybatis;
    
    import org.springframework.boot.builder.SpringApplicationBuilder;
    import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
    
    public class ServletInitializer extends SpringBootServletInitializer {
    
        @Override
        protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
            return application.sources(MysqlMybatisApplication.class);
        }
    
    }
    
  2. 데이터 소스 설정

    applicaton.properties 파일을 삭제하고 applicaton.yml 파일을 추가하여 데이터 소스를 설정합니다.

    src/main/resources/application.yml

    spring:
      profiles:
        active: local
    
    ---
    
    spring:
      profiles: local
      datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/my_test_db
        username: u1234
        password: p1234
    
  3. @Mapper 추가

    src/main/java/trvoid/mybatis/model/Car.java

    package trvoid.mybatis.model;
    
    public class Car {
        private int id;
        private String model;
        private String manufacturer;
        
        public Car() {
        
        }
        
        public Car(int id, String model, String manufacturer) {
            this.id = id;
            this.model = model;
            this.manufacturer = manufacturer;
        }
    
        // getters and setters
    
        @Override
        public String toString() {
            return String.format("id:%d, model:%s, manufacturer:%s", id, model, manufacturer);
        }
    }
    

    src/main/java/trvoid/mybatis/mapper/CarMapper.java

    package trvoid.mybatis.mapper;
    
    import org.apache.ibatis.annotations.Insert;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Param;
    import org.apache.ibatis.annotations.Select;
    import trvoid.mybatis.model.Car;
    
    import java.util.List;
    
    @Mapper
    public interface CarMapper {
        String SELECT = " SELECT ID, MODEL, MANUFACTURER FROM CAR ";
    
        @Select(" SELECT COUNT(1) FROM ALL_OBJECTS " +
                " WHERE OBJECT_TYPE = 'TABLE' AND OBJECT_NAME = 'CAR' ")
        int countTable();
    
        @Insert(" CREATE TABLE CAR ( " +
                "    ID NUMBER(10) NOT NULL, " +
                "    MODEL VARCHAR2(100) NOT NULL, " +
                "    MANUFACTURER VARCHAR2(100) NULL, " +
                "    CONSTRAINT CAR_PK PRIMARY KEY (ID) " +
                " ) ")
        void createTable();
    
        @Insert(" INSERT INTO CAR ( " +
                "    ID, MODEL, MANUFACTURER " +
                " ) VALUES ( " +
                "    #{car.id}, #{car.model}, #{car.manufacturer,jdbcType=VARCHAR} " +
                " ) ")
        void insertCar(@Param("car") Car car);
    
        @Select(SELECT + " WHERE ID = #{carId} ")
        Car getCar(@Param("carId") String carId);
    
        @Select(SELECT)
        List<Car> findAll();
    }
    
  4. 컨트롤러 추가

    src/main/java/trvoid/mybatis/controller/MainController.java

    package trvoid.mybatis.controller;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RestController;
    import trvoid.mybatis.mapper.CarMapper;
    import trvoid.mybatis.model.Car;
    
    @RestController
    public class MainController {
        @Autowired
        CarMapper carMapper;
    
        @GetMapping("/")
        public String main() {
            return "hello";
        }
    
        @GetMapping("/car/{id}")
        public Car car(@PathVariable String id) {
            Car car = carMapper.getCar(id);
            return car;
        }
    }
    
  5. WAR 파일 생성

    gradlew build
    

    빌드 결과로 다음 파일이 생성됩니다.

    • build/libs/mysql-mybatis-0.0.1-SNAPSHOT.war
    • build/libs/mysql-mybatis-0.0.1-SNAPSHOT-plain.war
  6. Tomcat 9에 배포

    1. Tomcat을 실행합니다.
    2. WAR 파일의 이름을 mysql-mybatis.war로 변경하여 Tomcat의 webapps 폴더 아래에 복사합니다.
    3. 브라우져에서 http://localhost:8080/mysql-mybatis/car/1 URL에 접속합니다.
    4. 화면에 아래와 같은 내용이 표시되는지 확인합니다.
      {
          "id": 1,
          "model": "J1",
          "manufacturer": "James"
      }
      
    5. Tomcat을 종료합니다.
  7. Tomcat 10에 배포

    1. Tomcat을 실행합니다.
    2. WAR 파일의 이름을 mysql-mybatis.war로 변경하여 Tomcat의 webapps-javaee 폴더 아래에 복사합니다.
    3. 브라우져에서 http://localhost:8080/mysql-mybatis/car/1 URL에 접속합니다.
    4. 화면에 아래와 같은 내용이 표시되는지 확인합니다.
      {
          "id": 1,
          "model": "J1",
          "manufacturer": "James"
      }
      

Written with StackEdit.

Spring Web + Tomcat 사용 기초

Spring Web + Tomcat 사용 기초

사용 환경

  • Windows 10
  • Java 1.8
  • Gradle 7.2
  • Spring Boot 2.5.6
  • Tomcat 9, 10

프로젝트 따라하기

  1. 프로젝트 생성

    브라우져에서 spring initializr를 방문하여 아래와 같이 입력하고 GENERATE 버튼을 클릭합니다.

    • Project: Gradle Project
    • Language: Java
    • Spring Boot: 2.5.6
    • Project Meta:
      • Group: trvoid
      • Artifact: war-api
      • Name: war-api
      • Package name: trvoid.api
      • Packaging: War
      • Java: 8
    • Dependencies:
      • Spring Web

    생성된 프로젝트 파일을 다운로드하여 압축을 풉니다. 프로젝트 폴더 구조는 아래와 같습니다.

    war-api
      |-gradle
      |-src
        |-main
          |-java
            |-trvoid.api
              |-ServletInitializer.java
              |-WarApiApplication.java
          |-resources
            |-application.properties
        |-test
          |-java
            |-trvoid.api
              |-WarApiApplicationTests.java
      |-build.gradle
      |-gradlew.bat
    

    build.gradle

    plugins {
        id 'org.springframework.boot' version '2.5.6'
        id 'io.spring.dependency-management' version '1.0.11.RELEASE'
        id 'java'
        id 'war'
    }
    
    group = 'trvoid'
    version = '0.0.1-SNAPSHOT'
    sourceCompatibility = '1.8'
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-web'
        providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
    }
    
    test {
        useJUnitPlatform()
    }
    
    

    src/main/java/trvoid/api/ServletInitializer.java

    package trvoid.api;
    
    import org.springframework.boot.builder.SpringApplicationBuilder;
    import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
    
    public class ServletInitializer extends SpringBootServletInitializer {
    
        @Override
        protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
            return application.sources(WarApiApplication.class);
        }
    
    }
    

    src/main/java/trvoid/api/WarApiApplication.java

    package trvoid.api;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class WarApiApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(WarApiApplication.class, args);
        }
    
    }
    
  2. 컨트롤러 추가

    src/main/java/trvoid/api/MainController.java

    package trvoid.api;  
    
    import org.springframework.web.bind.annotation.GetMapping;  
    import org.springframework.web.bind.annotation.RestController;  
    
    @RestController  
    public class MainController {  
        @GetMapping("/hello")  
        public String hello() {  
            return "Hello!!!";  
        }  
    }
    
  3. WAR 파일 생성

    gradlew build
    

    빌드 결과로 다음 파일이 생성됩니다.

    • build/libs/war-api-0.0.1-SNAPSHOT.war
    • build/libs/war-api-0.0.1-SNAPSHOT-plain.war
  4. Tomcat 9에 배포

    1. Tomcat을 실행합니다.
    2. WAR 파일의 이름을 war-api.war로 변경하여 Tomcat의 webapps 폴더 아래에 복사합니다.
    3. 브라우져에서 http://localhost:8080/war-api/hello URL에 접속합니다.
    4. 화면에 Hello!!!가 표시되는지 확인합니다.
    5. Tomcat을 종료합니다.
  5. Tomcat 10에 배포

    1. Tomcat을 실행합니다.
    2. WAR 파일의 이름을 war-api.war로 변경하여 Tomcat의 webapps-javaee 폴더 아래에 복사합니다.
    3. 브라우져에서 http://localhost:8080/war-api/hello URL에 접속합니다.
    4. 화면에 Hello!!!가 표시되는지 확인합니다.

참고 문서

Written with StackEdit.

2021년 10월 26일 화요일

Gradle 사용 기초

Gradle 사용 기초

Gradle을 자주 사용하지 않는 상황에서 필요할 때 참조하기 위하여 이 문서를 작성합니다.

Gradle 설치

프로젝트 생성 및 빌드

  1. 프로젝트 폴더 생성

    >mkdir demo
    >cd demo
    
  2. 프로젝트 생성

    >gradle init
    Starting a Gradle Daemon (subsequent builds will be faster)
    
    Select type of project to generate:
      1: basic
      2: application
      3: library
      4: Gradle plugin
    Enter selection (default: basic) [1..4] 2
    
    Select implementation language:
      1: C++
      2: Groovy
      3: Java
      4: Kotlin
      5: Scala
      6: Swift
    Enter selection (default: Java) [1..6] 3
    
    Split functionality across multiple subprojects?:
      1: no - only one application project
      2: yes - application and library projects
    Enter selection (default: no - only one application project) [1..2] 1
    
    Select build script DSL:
      1: Groovy
      2: Kotlin
    Enter selection (default: Groovy) [1..2] 1
    
    Select test framework:
      1: JUnit 4
      2: TestNG
      3: Spock
      4: JUnit Jupiter
    Enter selection (default: JUnit Jupiter) [1..4] 1
    
    Project name (default: demo):
    
    Source package (default: demo):
    
    
    > Task :init
    Get more help with your project: https://docs.gradle.org/7.2/samples/sample_building_java_applications.html
    
    BUILD SUCCESSFUL in 1m 26s
    2 actionable tasks: 2 executed
    
  3. 응용프로그램 실행

    >gradlew run
    

    위 명령을 수행하면 소스 파일을 컴파일하고 메인 함수를 실행합니다.

  4. 프로젝트 빌드

    >gradlew build
    

    JAR 파일을 만들고 실행 스크립트와 함께 묶어서 TAR/ZIP 파일을 생성합니다.

자주 사용하는 명령들

  • 사용할 수 있는 작업 목록 보기
    >gradlew tasks
    
  • 빌드 결과 지우기
    >gradlew clean
    

유용한 작업들

특정 클래스의 메인 메쏘드 실행하기

  1. build.gradle 파일에서 application 플러그인을 추가하고 mainClass를 지정합니다.

    plugins {
        id 'application'
    }
    
    application {
        mainClassName = 'my.example.MainClass'
    }
    
  2. application 플러그인의 run 태스크를 사용하여 메인 메쏘드를 실행합니다.

    >gradlew run
    
  3. 실행시에 --args 옵션을 사용하여 인자를 전달할 수 있습니다.

    >gradlew run --args="arg1 arg2"
    

원격 저장소에 올리기

  1. 빌드 스크립트에 아래와 같은 내용을 추가합니다.

    apply plugin: 'maven'
    
    uploadArchives {
        repositories {
            mavenDeployer {
                repository(url: 'http://your.maven.repository.com/nexus/content/repositories/releases/') {
                    authentication(userName: '계정명', password: '비밀번호')
                }
                snapshotRepository(url: 'http://your.maven.repository.com/nexus/content/repositories/snapshot/') {
                    authentication(userName: '계정명', password: '비밀번호')
                }
            }
        }
    }
    

    버전명에 -SNAPSHOT이 붙어 있으면 snapshotRepository로 올라갑니다.

  2. 원격 저장소로 올립니다.

    >gradlew uploadArchives
    

참고 문서

Written with StackEdit.

2021년 10월 13일 수요일

Guava BloomFilter 사용 기초

Guava BloomFilter 사용 기초

사용 환경

  • Windows 10
  • Java 1.8
  • Gradle 7.2
  • Spring Boot 2.5.4
  • Oracle DB 19
  • MyBatis 2.2.0

프로젝트 생성하기

  1. 프로젝트 생성

    브라우져에서 spring initializr를 방문하여 아래와 같이 입력하고 GENERATE 버튼을 클릭합니다.

    • Project: Gradle Project
    • Language: Java
    • Spring Boot: 2.5.5
    • Project Meta:
      • Group: trvoid.bloomfilter
      • Artifact: bloom-filter
      • Name: bloom-filter
      • Package name: trvoid.bloomfilter
      • Packaging: Jar
      • Java: 8
    • Dependencies:
      • JDBC API
      • MyBatis Framework
      • Oracle Driver

    생성된 프로젝트 파일을 다운로드하여 압축을 풉니다. 프로젝트 폴더 구조는 아래와 같습니다.

    bloom-filter
      |-src
        |-main
          |-java
            |-trvoid.bloomfilter
              |-BloomFilterApplication.java
          |-resources
            |-application.properties
        |-test
          |-java
            |-trvoid.bloomfilter
              |-BloomFilterApplicationTests.java
      |-build.gradle
    

    build.gradle 파일에서 의존성 항목을 확인할 수 있습니다.

    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    	implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
        runtimeOnly 'com.oracle.database.jdbc:ojdbc8'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
    }
    

    BloomFilterApplication.java 파일의 내용은 아래와 같습니다.

    package trvoid.bloomfilter;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class BloomFilterApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(BloomFilterApplication.class, args);
        }
    
    }
    
  2. @Mapper 추가

    src/main/java/trvoid/bloomfilter/Reject.java

    package trvoid.bloomfilter;
    
    public class Reject {
        private String fromPhoneNo;
        private String toPhoneNo;
    
        public Reject(String fromPhoneNo, String toPhoneNo) {
            this.fromPhoneNo = fromPhoneNo;
            this.toPhoneNo = toPhoneNo;
        }
    
        public String getFromPhoneNo() {
            return fromPhoneNo;
        }
    
        public void setFromPhoneNo(String fromPhoneNo) {
            this.fromPhoneNo = fromPhoneNo;
        }
    
        public String getToPhoneNo() {
            return toPhoneNo;
        }
    
        public void setToPhoneNo(String toPhoneNo) {
            this.toPhoneNo = toPhoneNo;
        }
    
        @Override
        public String toString() {
            return String.format("Reject[fromPhoneNo:%s, toPhoneNo:%s]", fromPhoneNo, toPhoneNo);
        }
    }
    

    src/main/java/trvoid/bloomfilter/RejectMapper.java

    package trvoid.bloomfilter;
    
    import org.apache.ibatis.annotations.Insert;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Param;
    import org.apache.ibatis.annotations.Select;
    
    import java.util.List;
    
    @Mapper
    public interface RejectMapper {
        String SELECT = " SELECT FROM_PHONE_NO, TO_PHONE_NO FROM REJECT ";
    
        @Select(" SELECT COUNT(1) FROM ALL_OBJECTS " +
                " WHERE OBJECT_TYPE = 'TABLE' AND OBJECT_NAME = 'REJECT' ")
        int countTable();
    
        @Insert(" CREATE TABLE REJECT ( " +
                "    FROM_PHONE_NO VARCHAR2(20) NOT NULL, " +
                "    TO_PHONE_NO VARCHAR2(20) NOT NULL, " +
                "    CONSTRAINT REJECT_PK PRIMARY KEY (FROM_PHONE_NO, TO_PHONE_NO) " +
                " ) ")
        void createTable();
    
        @Insert(" INSERT INTO REJECT ( " +
                "    FROM_PHONE_NO, TO_PHONE_NO " +
                " ) VALUES ( " +
                "    #{reject.fromPhoneNo}, #{reject.toPhoneNo} " +
                " ) ")
        void insertReject(@Param("reject") Reject reject);
    
        @Select("SELECT COUNT(*) FROM REJECT WHERE FROM_PHONE_NO = #{fromPhoneNo} AND TO_PHONE_NO = #{toPhoneNo} ")
        int countCar(@Param("fromPhoneNo") String fromPhoneNo, @Param("toPhoneNo") String toPhoneNo);
    
        @Select(SELECT)
        List<Reject> findAll();
    }
    
  3. 데이터베이스 질의 수행을 위해 CommandLineRunner 추가

    REJECT 테이블이 없을 경우 생성합니다.

    src/main/java/trvoid/bloomfilter/BloomFilterApplication.java

    package trvoid.bloomfilter;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.CommandLineRunner;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    import javax.sql.DataSource;
    
    @SpringBootApplication
    public class BloomFilterApplication implements CommandLineRunner {
        @Autowired
        DataSource dataSource;
    
        @Autowired
        RejectMapper rejectMapper;
    
        public static void main(String[] args) {
            SpringApplication.run(BloomFilterApplication.class, args);
        }
        
        @Override
        public void run(String... args) throws Exception {
            System.out.println("DataSource = " + dataSource);
    
            if (rejectMapper.countTable() == 0) {
                rejectMapper.createTable();
                System.out.println("** Created a table: REJECT");
            }
            
            System.exit(0);
        }
    }
    
  4. 데이터 소스 설정

    src/main/resources/application.properties

    spring.datasource.url=jdbc:oracle:thin:@localhost:1521:orcl
    spring.datasource.username=your_db_username
    spring.datasource.password=your_db_password
    spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
    
  5. 실행

    >gradlew bootRun
    

    출력 결과

    ...
    Create a Bloom Filter instance.
    Add all the REJECT data.
    Test the Bloom Filter.
    0800000000101000000001 -> true
    0800000000102000000001 -> false
    ...
    

블룸필터 적용하기

  1. Guava 라이브러리에 대한 의존성 추가

    build.gradle

    implementation 'com.google.guava:guava:31.0.1-jre'
    
  2. 블룸필터 생성 및 테스트 메쏘드 추가

    src/main/java/trvoid/bloomfilter/BloomFilterApplication.java

    private BloomFilter<String> initBloomFilter() {
    	System.out.println("Create a Bloom Filter instance.");
    	BloomFilter<String> rejectFilter = BloomFilter.create(Funnels.stringFunnel(Charset.forName("UTF-8")),100000);
    
    	System.out.println("Add all the REJECT data.");
    	List<Reject> list = rejectMapper.findAll();
    	list.forEach(x -> rejectFilter.put(x.getFromPhoneNo() + x.getToPhoneNo()));
    
    	return rejectFilter;
    }
    
    private void testBloomFilter(BloomFilter<String> rejectFilter) {
    	System.out.println("Test the Bloom Filter.");
    	String[] values = new String[] {
    		"0800000000101000000001",
    		"0800000000102000000001"
    	};
    
    	for (String s : values) {
    		System.out.println(String.format("%s -> %s", s, rejectFilter.mightContain(s)));
    	}
    }
    
  3. 블룸필터 생성 및 테스트 메쏘드 호출

    src/main/java/trvoid/bloomfilter/BloomFilterApplication.java

    @Override
    public void run(String... args) throws Exception {
    	System.out.println("DataSource = " + dataSource);
    
    	if (rejectMapper.countTable() == 0) {
    		rejectMapper.createTable();
    		System.out.println("** Created a table: REJECT");
    	}
    
    	//insertTestData();
    
    	BloomFilter<String> rejectFilter = initBloomFilter();
    	testBloomFilter(rejectFilter);
    
    	System.exit(0);
    }
    

Written with StackEdit.

2021년 9월 16일 목요일

Spring + MyBatis + Oracle 사용 기초

Spring + MyBatis + Oracle 사용 기초

사용 환경

  • Windows 10
  • Java 1.8
  • Maven 3.8.2
  • Spring Boot 2.5.4
  • Oracle DB 19
  • MyBatis 2.2.0

Hello 프로젝트 따라하기

  1. 프로젝트 생성

    브라우져에서 spring initializr를 방문하여 아래와 같이 입력하고 GENERATE 버튼을 클릭합니다.

    • Project: Maven Project
    • Language: Java
    • Spring Boot: 2.5.4
    • Project Meta:
      • Group: trvoid.mybatis
      • Artifact: basic-mybatis-example
      • Name: basic-mybatis-example
      • Package name: trvoid.mybatis
      • Packaging: Jar
      • Java: 8
    • Dependencies:
      • JDBC API
      • MyBatis Framework
      • Oracle Driver

    생성된 프로젝트 파일을 다운로드하여 압축을 풉니다. 프로젝트 폴더 구조는 아래와 같습니다.

    hello
      |-src
        |-main
          |-java
            |-trvoid.mybatis
              |-BasicMybatisExampleApplication.java
          |-resources
            |-application.properties
        |-test
          |-java
            |-trvoid.mybatis
              |-BasicMybatisExampleApplicationTests.java
      |-pom.xml
    

    pom.xml 파일에서 의존성 항목과 빌드 플러그인을 확인할 수 있습니다.

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>  
           <groupId>org.mybatis.spring.boot</groupId>  
           <artifactId>mybatis-spring-boot-starter</artifactId>  
           <version>2.2.0</version>  
        </dependency>
        <dependency>
            <groupId>com.oracle.database.jdbc</groupId>
            <artifactId>ojdbc8</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>  
                <groupId>org.springframework.boot</groupId>  
                <artifactId>spring-boot-maven-plugin</artifactId>  
            </plugin>
        </plugins>
    </build>
    

    BasicMybtisExampleApplication.java 파일의 내용은 아래와 같습니다.

    package trvoid.mybatis;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class BasicMybatisExampleApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(BasicMybatisExampleApplication.class, args);
        }
    
    }
    
  2. @Mapper 추가

    src/main/java/trvoid/mybatis/Car.java

    package trvoid.mybatis;
    
    public class Car {
        private int id;
        private String model;
        private String manufacturer;
    
        public Car(int id, String model, String manufacturer) {
            this.id = id;
            this.model = model;
            this.manufacturer = manufacturer;
        }
    
        // getters and setters
    
        @Override
        public String toString() {
            return String.format("id:%d, model:%s, manufacturer:%s", id, model, manufacturer);
        }
    }
    

    src/main/java/trvoid/mybatis/CarMapper.java

    package trvoid.mybatis;
    
    import org.apache.ibatis.annotations.Insert;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Param;
    import org.apache.ibatis.annotations.Select;
    
    import java.util.List;
    
    @Mapper
    public interface CarMapper {
        String SELECT = " SELECT ID, MODEL, MANUFACTURER FROM CAR ";
    
        @Select(" SELECT COUNT(1) FROM ALL_OBJECTS " +
                " WHERE OBJECT_TYPE = 'TABLE' AND OBJECT_NAME = 'CAR' ")
        int countTable();
    
        @Insert(" CREATE TABLE CAR ( " +
                "    ID NUMBER(10) NOT NULL, " +
                "    MODEL VARCHAR2(100) NOT NULL, " +
                "    MANUFACTURER VARCHAR2(100) NULL, " +
                "    CONSTRAINT CAR_PK PRIMARY KEY (ID) " +
                " ) ")
        void createTable();
    
        @Insert(" INSERT INTO CAR ( " +
                "    ID, MODEL, MANUFACTURER " +
                " ) VALUES ( " +
                "    #{car.id}, #{car.model}, #{car.manufacturer,jdbcType=VARCHAR} " +
                " ) ")
        void insertCar(@Param("car") Car car);
    
        @Select(SELECT + " WHERE ID = #{carId} ")
        Car getCar(@Param("carId") String carId);
    
        @Select(SELECT)
        List<Car> findAll();
    }
    
  3. 데이터베이스 질의 수행을 위해 CommandLineRunner 추가

    CAR 테이블이 없을 경우 생성하고 세 개의 행을 추가합니다.

    src/main/java/trvoid/mybatis/BasicMybatisExampleApplication.java

    package trvoid.mybatis;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.CommandLineRunner;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    import javax.sql.DataSource;
    import java.util.List;
    
    @SpringBootApplication
    public class BasicMybatisExampleApplication implements CommandLineRunner {
        @Autowired
        DataSource dataSource;
    
        @Autowired
        CarMapper carMapper;
    
        public static void main(String[] args) {
            SpringApplication.run(BasicMybatisExampleApplication.class, args);
        }
    
        @Override
        public void run(String... args) throws Exception {
            System.out.println("DataSource = " + dataSource);
    
            if (carMapper.countTable() == 0) {
                carMapper.createTable();
                System.out.println("** Created a table: CAR");
    
                int carId = ((int)Math.random()) % 100;
    
                Car car1 = new Car(carId + 0, "J1", "Jelly");
                carMapper.insertCar((car1));
                System.out.println(String.format("** Inserted a car: %s", car1.toString()));
    
                Car car2 = new Car(carId + 1, "T1", "Teal");
                carMapper.insertCar((car2));
                System.out.println(String.format("** Inserted a car: %s", car2.toString()));
    
                Car car3 = new Car(carId + 2, "W1", "Wisdom");
                carMapper.insertCar((car3));
                System.out.println(String.format("** Inserted a car: %s", car3.toString()));
            }
    
            System.out.println("== CARS ==");
            List<Car> list = carMapper.findAll();
            list.forEach(x -> System.out.println(x));
            System.out.println("-- CARS --");
    
            System.exit(0);
        }
    }
    
  4. 데이터 소스 설정

    src/main/resources/application.properties

    spring.datasource.url=jdbc:oracle:thin:@localhost:1521:orcl
    spring.datasource.username=your_db_username
    spring.datasource.password=your_db_password
    spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
    
  5. 컴파일

    >mvn compile
    
  6. 실행

    >mvn spring-boot:run
    

    출력 결과

    ...
    DataSource = HikariDataSource (HikariPool-1)
    == CARS ==
    id:1, model:J1, manufacturer:JELLY
    id:2, model:T1, manufacturer:TEAL
    id:3, model:W1, manufacturer:WISDOM
    -- CARS --
    ...
    

Written with StackEdit.

2021년 9월 10일 금요일

Spring + JdbcTemplate + Oracle 사용 기초

Spring + JdbcTemplate + Oracle 사용 기초

사용 환경

  • Windows 10
  • Java 1.8
  • Maven 3.8.2
  • Spring Boot 2.5.4
  • Oracle DB 19

Hello 프로젝트 따라하기

  1. 프로젝트 생성

    브라우져에서 spring initializr를 방문하여 아래와 같이 입력하고 GENERATE 버튼을 클릭합니다.

    • Project: Maven Project
    • Language: Java
    • Spring Boot: 2.5.4
    • Project Meta:
      • Group: trvoid.jdbc
      • Artifact: basic-jdbc-example
      • Name: basic-jdbc-example
      • Package name: trvoid.jdbc
      • Packaging: Jar
      • Java: 8
    • Dependencies: JDBC API, Oracle Driver

    생성된 프로젝트 파일을 다운로드하여 압축을 풉니다. 프로젝트 폴더 구조는 아래와 같습니다.

    hello
      |-src
        |-main
          |-java
            |-trvoid.jdbc
              |-BasicJdbcExampleApplication.java
          |-resources
            |-application.properties
        |-test
          |-java
            |-trvoid.jdbc
              |-BasicJdcExampleApplicationTests.java
      |-pom.xml
    

    pom.xml 파일에서 의존성 항목과 빌드 플러그인을 확인할 수 있습니다.

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
    
        <dependency>
            <groupId>com.oracle.database.jdbc</groupId>
            <artifactId>ojdbc8</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>  
                <groupId>org.springframework.boot</groupId>  
                <artifactId>spring-boot-maven-plugin</artifactId>  
            </plugin>
        </plugins>
    </build>
    

    BasicJdbcExampleApplication.java 파일의 내용은 아래와 같습니다.

    package trvoid.jdbc;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class BasicJdbcExampleApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(BasicJdbcExampleApplication.class, args);
        }
    
    }
    
  2. JdbcTemplate을 사용하는 @Repository 추가

    src/main/java/trvoid/jdbc/Car.java

    package trvoid.jdbc;
    
    public class Car {
        private int id;
        private String model;
        private String manufacturer;
    
        public Car(int id, String model, String manufacturer) {
            this.id = id;
            this.model = model;
            this.manufacturer = manufacturer;
        }
    
        // getters and setters
    
        @Override
        public String toString() {
            return String.format("id:%d, model:%s, manufacturer:%s", id, model, manufacturer);
        }
    }
    

    src/main/java/trvoid/jdbc/CarRepository.java

    package trvoid.jdbc;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Repository;
    
    import java.util.List;
    
    @Repository
    public class CarRepository {
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        public List<Car> findAll() {
            List<Car> result = jdbcTemplate.query(
                    "SELECT id, model, manufacturer FROM car",
                    (rs, rowNum) -> new Car(
                            rs.getInt("id"),
                            rs.getString("model"),
                            rs.getString("manufacturer")
                    )
            );
    
            return result;
        }
    }
    
  3. 데이터베이스 질의 수행을 위해 CommandLineRunner 추가

    src/main/java/trvoid/jdbc/BasicJdbcExampleApplication.java

    package trvoid.jdbc;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.CommandLineRunner;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    import javax.sql.DataSource;
    import java.util.List;
    
    @SpringBootApplication
    public class BasicJdbcExampleApplication implements CommandLineRunner {
        @Autowired
        DataSource dataSource;
    
        @Autowired
        CarRepository carRepository;
        public static void main(String[] args) {
            SpringApplication.run(BasicJdbcExampleApplication.class, args);
        }
    
        @Override
        public void run(String... args) throws Exception {
            System.out.println("DataSource = " + dataSource);
    
            System.out.println("== CARS ==");
            List<Car> list = carRepository.findAll();
            list.forEach(x -> System.out.println(x));
            System.out.println("-- CARS --");
    
            System.exit(0);
        }
    }
    
  4. 데이터 소스 설정

    src/main/resources/application.properties

    spring.datasource.url=jdbc:oracle:thin:@localhost:1521:orcl
    spring.datasource.username=your_db_username
    spring.datasource.password=your_db_password
    spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
    
  5. 테이블 생성 및 데이터 입력 SQL 작성

    src/main/resources/schema.sql

    CREATE TABLE CAR (
        ID NUMBER(10) NOT NULL,
        MODEL VARCHAR2(100) NOT NULL,
        MANUFACTURER VARCHAR2(100) NOT NULL,
        CONSTRAINT CAR_PK PRIMARY KEY (ID)
    );
    

    src/main/resources/data.sql

    INSERT INTO "CAR" (ID, MODEL, MANUFACTURER) VALUES(1, 'J5','JELLY');  
    INSERT INTO "CAR" (ID, MODEL, MANUFACTURER) VALUES(2, 'W7','WISDOM');  
    INSERT INTO "CAR" (ID, MODEL, MANUFACTURER) VALUES(3, 'T9','TEAL');
    
  6. Oracle 데이터베이스에서 테이블 생성 및 데이터 입력

    >sqlplus
    SQL> start src\main\resources\schema.sql
    SQL> start src\main\resources\data.sql
    SQL> commit;
    
  7. 컴파일

    >mvn compile
    
  8. 실행

    >mvn spring-boot:run
    

    출력 결과

    ...
    DataSource = HikariDataSource (HikariPool-1)
    == CARS ==
    id:1, model:J5, manufacturer:JELLY
    id:2, model:W7, manufacturer:WISDOM
    id:3, model:T9, manufacturer:TEAL
    -- CARS --
    ...
    

Written with StackEdit.

2021년 9월 7일 화요일

Maven + Spring 파일 업로드 기초

Maven + Spring 파일 업로드 기초

사용 환경

  • Windows 10
  • Java 1.8
  • Maven 3.8.2
  • Spring 5.0.6

Hello 프로젝트 따라하기

  1. 프로젝트 생성

    브라우져에서 spring initializr를 방문하여 아래와 같이 입력하고 GENERATE 버튼을 클릭합니다.

    • Project: Maven Project
    • Language: Java
    • Spring Boot: 2.5.4
    • Project Meta:
      • Group: trvoid
      • Artifact: file-upload
      • Name: File Upload
      • Package name: trvoid.fileupload
      • Packaging: Jar
      • Java: 8
    • Dependencies: Spring Web

    생성된 프로젝트 파일을 다운로드하여 압축을 풉니다. 프로젝트 폴더 구조는 아래와 같습니다.

    hello
      |-src
        |-main
          |-java
            |-trvoid.fileupload
              |-FileUploadApplication.java
        |-test
          |-java
            |-trvoid.fileupload
              |-FileUploadApplicationTests.java
      |-pom.xml
    

    pom.xml 파일에서 의존성 항목과 빌드 플러그인을 확인할 수 있습니다.

    <dependencies>
        <dependency>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>  
                <groupId>org.springframework.boot</groupId>  
                <artifactId>spring-boot-maven-plugin</artifactId>  
            </plugin>
        </plugins>
    </build>
    

    FileUploadApplication.java 파일의 내용은 아래와 같습니다.

    package trvoid.fileupload;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class FileUploadApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(FileUploadApplication.class, args);
        }
    
    }
    
  2. 파일 저장소 서비스 추가

    src/trvoid/fileupload/storage/StorageService.java

    package trvoid.fileupload.storage;
    
    import org.springframework.web.multipart.MultipartFile;
    
    public interface StorageService {
        void init();
        public void deleteAll()
        void store(MultipartFile file);
    }
    

    src/trvoid/fileupload/storage/FileSystemStorageService.java

    package trvoid.fileupload.storage;
    
    import org.springframework.stereotype.Service;
    import org.springframework.web.multipart.MultipartFile;
    
    import java.io.IOException;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.nio.file.Paths;
    
    @Service
    public class FileSystemStorageService implements StorageService {
        private final Path rootLocation;
    
        public FileSystemStorageService() {
            this.rootLocation = Paths.get("upload-dir");
        }
    
        @Override
        public void init() {
            try {
                Files.createDirectory(rootLocation);
            } catch (IOException e) {
                throw new RuntimeException("Could not initialize storage", e);
            }
        }
    	
    	@Override  
        public void deleteAll() {  
            FileSystemUtils.deleteRecursively(rootLocation.toFile());  
        }
    
        @Override
        public void store(MultipartFile file) {
            try {
                if (file.isEmpty()) {
                    throw new RuntimeException("Failed to store empty file " + file.getOriginalFilename());
                }
                Files.copy(file.getInputStream(), this.rootLocation.resolve(file.getOriginalFilename()));
            } catch (IOException e) {
                throw new RuntimeException("Failed to store file " + file.getOriginalFilename(), e);
            }
        }
    }
    
  3. 컨트롤러 추가

    src/trvoid/fileupload/FileUploadController.java

    package trvoid.fileupload;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.multipart.MultipartFile;
    import trvoid.fileupload.storage.StorageService;
    
    @RestController
    public class FileUploadController {
        private final StorageService storageService;
    
        @Autowired
        public FileUploadController(StorageService storageService) {
            this.storageService = storageService;
        }
    
        @PostMapping("/file/upload")
        public String handleFileUpload(@RequestParam("file") MultipartFile file,
                                       @RequestParam("fileUsage") String fileUsage) {
            storageService.store(file);
            
            System.out.println(String.format("File Usage: %s", fileUsage));
    
            return "success";
        }
    }
    
  4. 업로드 파일 크기와 요청 크기 제한

    src/main/resources/application.properties

    spring.servlet.multipart.max-file-size=128KB  
    spring.servlet.multipart.max-request-size=128KB
    
  5. 파일 저장소 초기화를 위해 CommandLineRunner 추가

    src/trvoid/fileupload/FileUploadApplication.java

    package trvoid.fileupload;
    
    import org.springframework.boot.CommandLineRunner;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.annotation.Bean;
    import trvoid.fileupload.storage.StorageService;
    
    @SpringBootApplication
    public class FileUploadApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(FileUploadApplication.class, args);
        }
    
        @Bean
        CommandLineRunner init(StorageService storageService) {
            return (args) -> {
                storageService.deleteAll();
                storageService.init();
            };
        }
    }
    
  6. 컴파일

    >mvn compile
    
  7. 실행

    >mvn spring-boot:run
    
  8. 파일 업로드 요청

    HTTP 클라이언트 프로그램(Insomnia, Postman, cURL 등)에서 아래와 같이 지정하여 파일 업로드 요청

    요청이 정상적으로 처리되었다면 아래와 같은 응답이 표시될 것입니다.

    success
    

    현재 폴더 아래에 upload-dir이 생성되었고 그 아래에서 업로드 파일이 존재하는지 확인합니다. 그리고 서버 실행 터미널에 File Usage: xxx와 같은 문자열이 표시되는지 확인합니다.

Written with StackEdit.

Maven + Spring @RestController 사용 기초

Maven + Spring @RestController 사용 기초

사용 환경

  • Windows 10
  • Java 1.8
  • Maven 3.8.2
  • Spring 5.0.6

Hello 프로젝트 따라하기

  1. 프로젝트 생성

    브라우져에서 spring initializr를 방문하여 아래와 같이 입력하고 GENERATE 버튼을 클릭합니다.

    • Project: Maven Project
    • Language: Java
    • Spring Boot: 2.5.4
    • Project Meta:
      • Group: trvoid
      • Artifact: rest
      • Name: rest
      • Package name: trvoid
      • Packaging: Jar
      • Java: 8
    • Dependencies: Spring Web

    생성된 프로젝트 파일을 다운로드하여 압축을 풉니다. 프로젝트 폴더 구조는 아래와 같습니다.

    hello
      |-src
        |-main
          |-java
            |-trvoid
              |-RestApplication.java
        |-test
          |-java
            |-trvoid
              |-RestApplicationTests.java
      |-pom.xml
    

    pom.xml 파일에서 의존성 항목과 빌드 플러그인을 확인할 수 있습니다.

    <dependencies>
        <dependency>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>  
                <groupId>org.springframework.boot</groupId>  
                <artifactId>spring-boot-maven-plugin</artifactId>  
            </plugin>
        </plugins>
    </build>
    

    RestApplication.java 파일의 내용은 아래와 같습니다.

    package trvoid;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class RestApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(RestApplication.class, args);
        }
    
    }
    
  2. 컨트롤러 추가

    src/main/java/trvoid/Greeting.java

    package trvoid;
    
    public class Greeting {
        private String to;
        private String message;
    
        public Greeting(String to, String message) {
            this.to = to;
            this.message = message;
        }
    
        // getters and setters
    }
    

    src/main/java/trvoid/HelloController.java

    package trvoid;  
    
    import org.springframework.web.bind.annotation.GetMapping;  
    import org.springframework.web.bind.annotation.PathVariable;  
    import org.springframework.web.bind.annotation.RestController;  
    
    @RestController  
    public class HelloController {  
        @GetMapping(value="/{name}")  
        public Greeting sayHello(@PathVariable String name) {  
            return new Greeting("Server", name, "Hello");  
        } 
    }
    
  3. 컴파일

    >mvn compile
    
  4. 실행

    >mvn exec:java -Dexec.mainClass=trvoid.RestApplication
    

    실행 결과

    ...
      .   ____          _            __ _ _
     /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
    ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
     \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
      '  |____| .__|_| |_|_| |_\__, | / / / /
     =========|_|==============|___/=/_/_/_/
     :: Spring Boot ::                (v2.5.4)
    
    2021-09-08 10:45:34.564  INFO 39908 --- [lication.main()] trvoid.RestApplication                   : Starting RestApplication using Java 1.8.0_261 on DESKTOP-NBBJSSF with PID 39908 (D:\DevTest\Spring\rest-maven\target\classes started by WISECAN in D:\DevTest\Spring\rest-maven)
    2021-09-08 10:45:34.564  INFO 39908 --- [lication.main()] trvoid.RestApplication                   : No active profile set, falling back to default profiles: default
    2021-09-08 10:45:35.142  INFO 39908 --- [lication.main()] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
    2021-09-08 10:45:35.157  INFO 39908 --- [lication.main()] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
    2021-09-08 10:45:35.157  INFO 39908 --- [lication.main()] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.52]
    2021-09-08 10:45:35.204  INFO 39908 --- [lication.main()] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
    2021-09-08 10:45:35.204  INFO 39908 --- [lication.main()] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 617 ms
    2021-09-08 10:45:35.563  INFO 39908 --- [lication.main()] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
    2021-09-08 10:45:35.614  INFO 39908 --- [lication.main()] trvoid.RestApplication                   : Started RestApplication in 1.292 seconds (JVM running for 16.457)
    
  5. 브라우져로 REST API 호출 결과 확인

    브라우져에서 주소창에 http://localhost:8080/홍길동을 입력하고 엔터를 치면 아래와 같은 결과를 얻습니다.

    {"to":"홍길동","message":"Hello from Server"}
    

Written with StackEdit.

2021년 9월 6일 월요일

Gradle + Spring 사용 기초

Gradle + Spring 사용 기초

사용 환경

  • Windows 10
  • Java 1.8
  • Gradle 7.2
  • Spring 5.0.6

Hello 프로젝트 따라하기

  1. 프로젝트 생성

    >gradle init
    

    선택 항목

    Starting a Gradle Daemon (subsequent builds will be faster)
    
    Select type of project to generate:
      1: basic
      2: application
      3: library
      4: Gradle plugin
    Enter selection (default: basic) [1..4] 2
    
    Select implementation language:
      1: C++
      2: Groovy
      3: Java
      4: Kotlin
      5: Scala
      6: Swift
    Enter selection (default: Java) [1..6] 3
    
    Split functionality across multiple subprojects?:
      1: no - only one application project
      2: yes - application and library projects
    Enter selection (default: no - only one application project) [1..2] 1
    
    Select build script DSL:
      1: Groovy
      2: Kotlin
    Enter selection (default: Groovy) [1..2] 1
    
    Select test framework:
      1: JUnit 4
      2: TestNG
      3: Spock
      4: JUnit Jupiter
    Enter selection (default: JUnit Jupiter) [1..4] 1
    
    Project name (default: hello-gradle): hello
    Source package (default: hello): trvoid
    
    > Task :init
    Get more help with your project: https://docs.gradle.org/7.2/samples/sample_building_java_applications.html
    

    생성된 프로젝트 폴더 구조는 아래와 같습니다.

    hello
      |-app
        |-src
          |-main
            |-java
              |-trvoid
                |-App.java
            |-resources
          |-test
            |-java
              |-trvoid
                |-AppTest.java
            |-resources
        |-build.gradle
      |-gradle
        |-wrapper
          |-gradle-wrapper.jar
          |-gradle-wrapper.properties
      |-gradlew
      |-gradlew.bat
      |-settings.gradle
    
  2. 의존성 추가

    app/build.gradle

    implementation 'org.springframework:spring-core:5.0.6.RELEASE'
    implementation 'org.springframework:spring-context:5.0.6.RELEASE'
    
  3. 서비스 추가

    app/src/main/java/trvoid/HelloManager.java

    package trvoid;
    
    public interface HelloManager {
        public String getServiceName();
    }
    

    app/src/main/java/trvoid/HelloManagerImpl.java

    package trvoid;
    
    public class HelloManagerImpl implements HelloManager {
        @Override
        public String getServiceName() {
            return "My Hello Service";
        }
    }
    
  4. 빈 추가

    app/src/main/java/trvoid/AppConfig.java

    package trvoid;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class AppConfig {
        @Bean(name="helloService")
        public HelloManager getHelloManager() {
            return new HelloManagerImpl();
        }
    }
    
  5. 메인 클래스 수정

    app/src/main/java/trvoid/App.java

    package trvoid;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    
    public class App {
        public static void main(String[] args) {
            ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
            
            HelloManager hello = (HelloManager)context.getBean("helloService");
            
            System.out.println(hello.getServiceName());
        }
    }
    
  6. 빌드

    >gradlew build
    
  7. 실행

    >gradlew run
    

    실행 결과

    > Task :app:run
    9월 07, 2021 11:29:27 오전 org.springframework.context.support.AbstractApplicationContext prepareRefresh
    정보: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@45ff54e6: startup date [Tue Sep 07 11:29:27 KST 2021]; root of context hierarchy
    My Hello Service
    
    BUILD SUCCESSFUL in 6s
    2 actionable tasks: 2 executed
    

Written with StackEdit.

Maven + Spring 사용 기초

Maven + Spring 사용 기초

사용 환경

  • Windows 10
  • Java 1.8
  • Maven 3.8.2
  • Spring 5.0.6

Hello 프로젝트 따라하기

  1. 프로젝트 생성

    >mvn archetype:generate -DgroupId=trvoid -DartifactId=hello -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4 -DinteractiveMode=false
    

    생성된 프로젝트 폴더 구조는 아래와 같습니다.

    hello
      |-src
        |-main
          |-java
            |-trvoid
              |-App.java
        |-test
          |-java
            |-trvoid
              |-AppTest.java
      |-pom.xml
    
  2. 의존성 추가

    pom.xml

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>5.0.6.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.0.6.RELEASE</version>
    </dependency>
    
  3. 서비스 추가

    src/main/java/trvoid/HelloManager.java

    package trvoid;
    
    public interface HelloManager {
        public String getServiceName();
    }
    

    src/main/java/trvoid/HelloManagerImpl.java

    package trvoid;
    
    public class HelloManagerImpl implements HelloManager {
        @Override
        public String getServiceName() {
            return "My Hello Service";
        }
    }
    
  4. 빈 추가

    src/main/java/trvoid/AppConfig.java

    package trvoid;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class AppConfig {
        @Bean(name="helloService")
        public HelloManager getHelloManager() {
            return new HelloManagerImpl();
        }
    }
    
  5. 메인 클래스 수정

    src/main/java/trvoid/App.java

    package trvoid;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    
    public class App {
        public static void main(String[] args) {
            ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
            
            HelloManager hello = (HelloManager)context.getBean("helloService");
            
            System.out.println(hello.getServiceName());
        }
    }
    
  6. 컴파일

    >mvn compile
    
  7. 실행

    >mvn exec:java -Dexec.mainClass=trvoid.App
    

    실행 결과

    [INFO] Scanning for projects...
    [INFO]
    [INFO] ----------------------------< trvoid:hello >----------------------------
    [INFO] Building hello 1.0-SNAPSHOT
    [INFO] --------------------------------[ jar ]---------------------------------
    [INFO]
    [INFO] --- exec-maven-plugin:3.0.0:java (default-cli) @ hello ---
    9월 07, 2021 10:30:09 오전 org.springframework.context.support.AbstractApplicationContext prepareRefresh
    정보: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@2695bde4: startup date [Tue Sep 07 10:30:09 KST 2021]; root of context hierarchy
    My Hello Service
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time:  0.773 s
    [INFO] Finished at: 2021-09-07T10:30:09+09:00
    [INFO] ------------------------------------------------------------------------
    

Written with StackEdit.

2021년 6월 8일 화요일

경사하강법에서 손실 값의 변화

경사하강법에서 손실 값의 변화

1. 데이터 기반 예측 문제의 해결 과정

데이터 기반 예측 문제의 해결 과정을 아래의 세 단계로 나누어 볼 수 있습니다.

  1. 주어진 데이터세트의 특성을 잘 나타내는 모델 함수를 정의합니다. 모델 함수가 가지고 있는 파라미터들은 데이터세트에 맞도록 값이 조정되는 요소들입니다.
  2. 데이터세트의 참값과 모델 함수가 예측하는 값의 차이의 정도를 나타내는 비용 함수를 정의합니다.
  3. 데이터세트에 대하여 비용을 줄여나가는 방향으로 모델 파라미터를 조정합니다. 비용이 최솟값에 가까워질 때까지 파라미터 조정 과정을 반복합니다.

이렇게 얻은 모델 함수를 사용하여 새로운 데이터가 주어질 때 예측하고자 하는 값이 얼마일지 추정합니다.

2. 경사하강법(Gradient Descent)

구분 설명
Gradient Descent(GD) 데이터세트 전체를 대상으로 손실을 계산하고 모델 파라미터 조정 (데이터세트의 크기: NN)
Stochastic Gradient Descent(SGD) 데이터세트에서 임의로 선택한 한 개의 데이터를 대상으로 손실을 계산하고 모델 파라미터 조정
Mini-batch Gradient Descent(MGD) 데이터세트에서 임의로 선택한 mm 개의 데이터를 대상으로 손실을 계산하고 모델 파라미터 조정 (1<m<N1 < m < N)

2.1. 모델 파라미터 변경에 따른 손실의 변화율

2.2. 손실을 줄이는 방향으로 모델 파라미터 수정

2.3. 손실 값의 변화

2.3.1. Gradient Descent

진짜 경사도를 계산해서 파라미터를 조정하기 때문에 이 과정을 반복함에 따라 손실 값은 줄어드는 방향으로 이동합니다.

2.3.2. Mini-batch Gradient Descent

진짜 경사도가 아니라 근사 경사도를 계산해서 파라미터를 조정하기 때문에 이 과정을 반복하는 과정에서 손실 값은 증가와 감소를 반복하면서 어떤 값으로 수렴하는 경향을 보입니다.

2.3.3. Stochastic Gradient Descent

MGD에서 부분집합의 크기가 11이면 SGD, 전체집합의 크기 NN과 같으면 GD가 됩니다.

3. 더 읽을 자료

SGD가 왜 통하는지에 대한 이론적 배경이 궁금하다면 아래의 자료가 도움이 될 것입니다.

Written with StackEdit.

2021년 4월 30일 금요일

당뇨병 발병 예측 - Gaussian Naive Bayes 모델

diabetes-onset-by-gaussian-nb

당뇨병 발병 예측 - Gaussian Naive Bayes 모델

이 글에서는 피마 인디언 당뇨병 데이터세트를 사용하여 어떻게 당뇨병 발병을 예측할 수 있는지 베이지안 추론 방식으로 보여줍니다. 이 글의 전개 과정은 아래와 같습니다.

  1. 당뇨병 발병 데이터세트를 준비합니다.
  2. 발병 여부별로 측정값의 히스토그램을 그려서 분포를 파악합니다.
  3. 발병 여부에 따라서 측정값이 어떻게 분포할 수 있는지 설명하는 모델을 정의합니다.
  4. 측정값이 주어질 때 발병 여부를 예측하는 분류기를 구현합니다.
  5. 데이터 세트를 훈련 데이터와 검증 데이터로 나누어 분류기를 훈련시키고 예측 성능을 구합니다.

라이브러리 준비

사용할 파이썬 라이브러리들을 임포트합니다.

In [23]:
from collections import defaultdict
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from scipy.stats import norm
import matplotlib.pyplot as plt

%matplotlib inline

np.set_printoptions(precision=6)
np.random.seed(7)

데이터 준비

UCI Machine Learning에서 제공하는 Pima Indians Diabetes Database를 사용하여 데이터세트를 준비합니다.

  • 9개의 속성을 가지고 있음
    • 측정 항목(measured)
      • Pregnancies
      • Glucose
      • BloodPressure
      • SkinThickness
      • Insulin
      • BMI
      • DiabetesPedigreeFunction
      • Age
    • 예측 항목(target)
      • Outcome

데이터 읽기

In [24]:
ds_diabetes = pd.read_csv('datasets_228_482_diabetes.csv')

ds_diabetes.head()
Out[24]:
Pregnancies Glucose BloodPressure SkinThickness Insulin BMI DiabetesPedigreeFunction Age Outcome
0 6 148 72 35 0 33.6 0.627 50 1
1 1 85 66 29 0 26.6 0.351 31 0
2 8 183 64 0 0 23.3 0.672 32 1
3 1 89 66 23 94 28.1 0.167 21 0
4 0 137 40 35 168 43.1 2.288 33 1

데이터 탐색

데이터 통계

측정 데이터별 통계 자료는 아래와 같습니다.

In [25]:
ds_diabetes.describe()
Out[25]:
Pregnancies Glucose BloodPressure SkinThickness Insulin BMI DiabetesPedigreeFunction Age Outcome
count 768.000000 768.000000 768.000000 768.000000 768.000000 768.000000 768.000000 768.000000 768.000000
mean 3.845052 120.894531 69.105469 20.536458 79.799479 31.992578 0.471876 33.240885 0.348958
std 3.369578 31.972618 19.355807 15.952218 115.244002 7.884160 0.331329 11.760232 0.476951
min 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.078000 21.000000 0.000000
25% 1.000000 99.000000 62.000000 0.000000 0.000000 27.300000 0.243750 24.000000 0.000000
50% 3.000000 117.000000 72.000000 23.000000 30.500000 32.000000 0.372500 29.000000 0.000000
75% 6.000000 140.250000 80.000000 32.000000 127.250000 36.600000 0.626250 41.000000 1.000000
max 17.000000 199.000000 122.000000 99.000000 846.000000 67.100000 2.420000 81.000000 1.000000

발병 여부별 측정값의 히스토그램

발병 여부별로 측정값의 히스토그램을 그립니다. 이것을 참고하여 발병 여부에 따라 측정값이 어떤 분포를 따르는지 추정할 수 있습니다.

In [26]:
def separate_by_targets(X, y):
    separated = defaultdict(lambda: [])
    
    row_count = X.shape[0]
    for row in np.arange(row_count):
        measured = X[row, :]
        target = y[row]
        separated[target].append(measured)
    
    for target in separated.keys():
        separated[target] = np.array(separated[target])
        
    return separated

def plot_feature_histograms_for_a_target(separated, target, feature_names, target_name, bin_count):
    ds_measured = separated[target]
    
    fig = plt.figure(figsize = (16,10))
    fig.suptitle(f'feature histograms for {target_name}')
    for col in np.arange(len(feature_names)):
        plt.subplot(241 + col)
        plt.hist(ds_measured[:, col], bins=bin_count)
        plt.grid(True)
        plt.xlabel('measured')
        plt.ylabel('frequency')
        plt.title(feature_names[col])
    plt.show()
    
separated = separate_by_targets(ds_diabetes.iloc[:,0:8].values, ds_diabetes.iloc[:,8].values)
for target in np.arange(len(separated.keys())):
    feature_names = ds_diabetes.columns[0:8]
    target_name = 'Outcome:' + str(target)
    plot_feature_histograms_for_a_target(separated, target, feature_names, target_name, 24)

위의 히스토그램에서 발병 여부별로 측정값의 분포를 정규분포로 간주하는 것은 일부 속성의 경우에는 무리가 없어 보이지만 그렇지 않은 속성들도 있음을 알 수 있습니다. 그럼에도 불구하고 모든 속성이 정규분포를 따른다고 가정했을 때 어느 정도의 예측 정확도를 보여 줄 것인지 확인해 볼 수 있습니다.

발병 여부별 측정값의 상자그림

발병 여부에 따라 측정값의 통계가 어떻게 달라지는지 더 명확하게 파악하기 위하여 발병 여부별로 측정값의 상자그림을 그려봅니다.

In [27]:
feature_names = ds_diabetes.columns[0:8]
target_names = {0:'Outcome:0', 1:'Outcome:1'}

plt.figure(figsize = (16,10))
plt.suptitle('box plots of features')
for col in np.arange(len(feature_names)):
    plt.subplot(241 + col)
    data_for_boxplot = []
    labels = []
    for target in np.arange(len(separated.keys())):
        data_for_boxplot.append(separated[target][:, col])
        labels.append(target_names[target])
    plt.boxplot(data_for_boxplot, labels = labels)
    plt.grid(True)
    plt.xlabel('target')
    plt.ylabel('measured')
    plt.title(f'{feature_names[col]}')
plt.show()

위의 상자그림들로부터 포도당(glucose) 수치가 당뇨병 발병 여부를 예측하는데 가장 중요하게 쓰일 수 있음을 알 수 있습니다.

모델 정의

발병 여부에 따른 측정값의 분포를 설명하기 위하여 모델을 정의합니다. 여기에서는 발병 여부가 주어질 때 측정을 수행하여 얻는 값들의 발생 가능성이 아래의 분포를 따른다고 가정합니다.

  • 조사대상군으로부터 수집한 데이터에서 구한 발병 여부별 측정값 평균과 표준편차를 사용하는 정규분포

예를 들어 당뇨병이 발병했을 때 포도당 수치에 해당하는 값들의 발생 가능성이 어떻게 분포하는지는 아래의 방법으로 구합니다.

  1. 데이터세트에서 당뇨병 발병에 해당하는 것들만 따로 모읍니다.
  2. 포도당 값들의 평균과 표준편차를 구합니다.
  3. 위에서 구한 평균과 표준편차를 사용하는 정규분포를 그립니다.

이제 발병 여부별 측정값 평균과 표준편차를 구하여 테이블 형태로 저장합니다.

In [28]:
def get_norm_params(separated):
    targets = separated.keys()
    target_count = len(targets)
    feature_count = separated[0].shape[1]

    thetas = np.zeros((target_count, feature_count))
    sigmas = np.zeros((target_count, feature_count))
    
    for target in targets:
        ds_measured = separated[target]
        thetas[target,:] = np.mean(ds_measured, axis=0)
        sigmas[target,:] = np.std(ds_measured, axis=0)
        
    return thetas, sigmas

thetas, sigmas = get_norm_params(separated)

위에서 구한 평균과 표준편차 값들을 사용하여 정규분포 곡선을 그려 봅니다.

In [29]:
stops_per_feature = [20, 300, 150, 100, 800, 80, 3, 100]
def plot_feature_norm_for_a_target(thetas, sigmas, target, feature_names, target_name):
    fig = plt.figure(figsize = (16,10))
    fig.suptitle(f'feature probability distribution for {target_name}')
    for col in np.arange(len(feature_names)):
        x_arr = np.linspace(0, stops_per_feature[col], 100)
        y_arr = norm.pdf(x_arr, thetas[target, col], sigmas[target, col])
        plt.subplot(241 + col)
        plt.plot(x_arr, y_arr)
        plt.grid(True)
        plt.xlabel('measured')
        plt.ylabel('probability')
        plt.title(feature_names[col])
    plt.show()
    
for target in np.arange(len(separated.keys())):
    plot_feature_norm_for_a_target(thetas, sigmas, target, feature_names, target_names[target])

베이지안 추론

베이즈 정리에 기반하여 다음과 같이 세 단계를 거쳐 추론하는 것을 베이지안 추론이라고 합니다.

  1. 기존의 믿음 (prior belief)
  2. 새로운 증거 (new evidence)
  3. 믿음의 수정 (update belief -> posterior belief)

베이즈 정리

베이즈 정리는 아래의 식으로 표현됩니다.

  • $P(H|E) = \frac{P(E|H)\times P(H)}{P(E)}$

위 식에서 각 항목의 의미는 다음과 같습니다.

  • $E$ : 사건 (event)
  • $H$ : 추론하고자 하는 값 (hypothesis)
  • $P(H)$ : E가 발생하기 전의 H에 대한 확률분포 (prior probability distribution)
  • $P(E|H)$ : H를 알고 있을 때 E의 발생 가능도 (likelihood)
  • $P(E)$ : H에 관계없이 E의 발생 가능도 (marginal likelihood)
  • $P(H|E)$ : E가 발생한 후의 H에 대한 확률분포 (posterior probability distribution)

위의 식을 분류 문제에 적용하기 위하여 E와 H를 다음과 같이 정의합니다.

  • E: 측정값 ($v_{measured}$)
  • H: 측정값으로부터 추정하는 실제값 ($v_{actual}$)

측정값이 $v_{measured}$일때 추정하는 실제값 $v_{actual}$의 확률분포를 아래와 같이 조건부확률로 표현할 수 있습니다.

  • $P(v_{actual}|v_{measured})$

이를 베이즈 정리에 따라 표현하면 아래와 같습니다.

  • $P(v_{actual}|v_{measured})=\frac { P(v_{measured}|v_{actual})\times P(v_{actual}) }{ P(v_{measured}) }$

위 식의 각 항목에 대한 의미는 다음과 같습니다.

  • $P(v_{actual})$ : 측정값을 알기 전의 실제값 $v_{actual}$에 대한 확률분포
  • $P(v_{measured}|v_{actual})$ : 실제값이 $v_{actual}$일때 측정값 $v_{measured}$을 얻을 가능도
  • $P(v_{measured})$ : 실제값이 무엇이냐에 관계없이 측정값 $v_{measured}$을 얻을 가능도
  • $P(v_{actual}|v_{measured})$ : 측정값이 $v_{measured}$일때 추정하는 실제값 $v_{actual}$에 대한 확률분포

기존의 믿음

당뇨병 데이터세트로부터 얻은 당뇨병 발병 여부의 분포를 기존의 믿음으로 간주합니다. 즉 당뇨병 발병 여부가 알려지지 않은 새로운 환자가 있을 때 여덟 가지 속성의 값을 측정하기 전에는 그 환자의 당뇨병 발병 여부는 데이터 세트로부터 얻은 분포를 따른다고 믿는 것입니다. 이를 사전확률(prior probability)이라고 합니다.

In [30]:
def get_priors(separated):
    targets = separated.keys()

    priors = np.zeros(len(targets))
    
    total_count = 0
    for target in targets:
        count = separated[target].shape[0]
        total_count += count
        priors[target] = count
    
    priors /= total_count
    
    return priors

priors = get_priors(separated)
print(priors)
[0.651042 0.348958]

위의 결과는 새로운 환자에 대하여 측정을 하기 전까지는 아래의 확률로 당뇨병 발병 여부를 추정할 수 있음을 의미합니다.

  • $P(target=0) = 0.651042$
  • $P(target=1) = 0.348958$

새로운 증거

새로운 환자가 있고 여덟 가지 속성을 측정하여 얻은 값은 다음과 같다고 가정합니다.

Pregnancies Glucose BloodPressure SkinThickness Insulin BMI DiabetesPedigreeFunction Age
6 148 72 35 100 50 1 40
In [31]:
measured = np.array([[6, 148, 72, 35, 100, 50, 1, 40]])

실제값이 $target = t$일 때 속성별로 측정값 $m_1=v_1;m_2=v_2;m_3=v_3;m_4=v_4;m_5=v_5;m_6=v_6;m_7=v_7;m_8=v_8$을 얻을 가능성을 나타내는 가능도(likelihood)를 구합니다. 이때 각각의 특성은 서로 독립적이라고 가정하고 속성별로 확률을 계산합니다. 이러한 가정을 하기 때문에 Bayes 방식이라는 말 앞에 Naive를 덧붙여서 부릅니다.

  • $P(m_1=v_1;m_2=v_2;m_3=v_3;m_4=v_4;m_5=v_5;m_6=v_6;m_7=v_7;m_8=v_8|target=t) \\ \qquad = P(m_1=v_1|target=t) \\ \qquad \times P(m_2=v_2|target=t) \\ \qquad \times P(m_3=v_3|target=t) \\ \qquad \times P(m_4=v_4|target=t) \\ \qquad \times P(m_5=v_5|target=t) \\ \qquad \times P(m_6=v_6|target=t) \\ \qquad \times P(m_7=v_7|target=t) \\ \qquad \times P(m_8=v_8|target=t)$
In [32]:
def get_likelihoods(thetas, sigmas, measured):
    target_count = thetas.shape[0]
    instance_count = measured.shape[0]

    likelihoods = np.zeros((instance_count, target_count))
    
    for target in np.arange(target_count):
        l = norm.pdf(measured, thetas[target, :], sigmas[target, :])
        likelihoods[:, target] = np.prod(l, axis=1)
        
    return likelihoods

likelihoods = get_likelihoods(thetas, sigmas, measured)
print(likelihoods)
[[6.622313e-15 1.418506e-13]]

실제값이 무엇인지에 관계없이 측정값으로 $m_1=v_1;m_2=v_2;m_3=v_3;m_4=v_4;m_5=v_5;m_6=v_6;m_7=v_7;m_8=v_8$을 얻을 가능성을 나타내는 주변가능도(marginal likelihood)는 아래와 같이 구합니다.

  • $P(m_1=v_1;m_2=v_2;m_3=v_3;m_4=v_4;m_5=v_5;m_6=v_6;m_7=v_7;m_8=v_8) \\ \qquad = P(m_1=v_1;m_2=v_2;m_3=v_3;m_4=v_4;m_5=v_5;m_6=v_6;m_7=v_7;m_8=v_8|target=0) \times P(target=0) \\ \qquad + P(m_1=v_1;m_2=v_2;m_3=v_3;m_4=v_4;m_5=v_5;m_6=v_6;m_7=v_7;m_8=v_8|target=1) \times P(target=1)$
In [33]:
marginal_likelihoods = np.sum(likelihoods * priors, axis=1)
print(marginal_likelihoods)
[5.381133e-14]

믿음의 수정

새로운 증거(new evidence)를 활용하여 기존의 믿음(prior)을 수정합니다. 이렇게 얻은 확률을 사후확률 (posterior)이라고 부릅니다. 측정값이 주어진 상태에서 발병 여부를 바꾸어 가면서 사후확률을 구합니다.

In [34]:
def get_posteriors(priors, thetas, sigmas, X):
    likelihoods = get_likelihoods(thetas, sigmas, X)
    marginal_likelihoods = np.sum(likelihoods * priors, axis=1)
    likelihood_ratios = likelihoods / marginal_likelihoods.reshape(len(marginal_likelihoods), -1)
    posteriors = likelihood_ratios * priors
    return posteriors

posteriors = get_posteriors(priors, thetas, sigmas, measured)
print(posteriors)
[[0.080121 0.919879]]
In [35]:
target_names_arr = ['Outcome:0', 'Outcome:1']
plt.figure(figsize = (6, 4))
plt.bar(target_names_arr, posteriors[0,:], width=0.4)
plt.grid(True)
plt.title('posterior distribution')
plt.show()

발병 여부별 사후확률 중에서 최대값에 해당하는 결과를 실제값으로 간주합니다. 이 과정을 Maximum A Posteriori(MAP) 추정이라고 부릅니다.

In [36]:
predicted = np.argmax(posteriors, axis=1)

for i in np.arange(measured.shape[0]):
    print(f'{measured[i,:]} => {target_names[predicted[i]]}')
[  6 148  72  35 100  50   1  40] => Outcome:1

분류기 구현

위에서 정의한 함수들을 사용하여 분류기 클래스를 아래와 같이 구현할 수 있습니다.

In [37]:
class GaussianNB:
    def fit(self, X, y):
        # separate by targets
        separated = separate_by_targets(X, y)
        
        # get priors
        self.priors = get_priors(separated)
        
        # get normal distribution parameters
        self.thetas, self.sigmas = get_norm_params(separated)
        
    def predict(self, X):
        posteriors = get_posteriors(self.priors, self.thetas, self.sigmas, X)
        predicted = np.argmax(posteriors, axis=1)
        return predicted
        
    def score(self, X, y):
        return sum(self.predict(X) == y) / len(y)

예측 성능

In [38]:
X, y = ds_diabetes.iloc[:,0:8].values, ds_diabetes.iloc[:,8].values
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=7)

nb = GaussianNB()
nb.fit(X_train, y_train)

score = nb.score(X_test, y_test)
print(f'score = {score:.4f}')
score = 0.7532

마무리

  • 이 글에서 구현한 분류기는 측정값이 정규분포를 따르는 다른 종류의 데이터세트에 대해서도 동작합니다.
In [ ]:
 

국어 맞춤법 참고 자료

  제목 설명(인용) 출처 IT 글쓰기와 번역 노트 IT 기술 문서 및 서적을 집필/번역/교정하면서 얻은 경험/정보/지식을 공유합니다. 전뇌해커 [우리말 바루기] ‘대로’의 띄어쓰기 명사 뒤에서는 붙여 쓰고, 그 외에는 띄어 쓴다고 생각하면 쉽다. 다...