SpringMVC + JSP の環境構築とサンプル実装

前置き

  • この記事は、ローカルPCに開発環境構築をすること、サンプルプログラムを実装することを焦点にあてた記事です。
  • 実際にアプリケーション開発しサーバーで稼働させるには、セキュリティ観点や開発をスケールさせるための設計のために いくつか留意すべきことがあります。そちらについては、別途文献をご参照ください。

構築する開発環境

ゴール:下記構成のSpringBootを用いたWebアプリケーションの開発環境構築.

なお、できるのであれば lombok も導入するとコーディングが捗ります。

実行構成について

  • GradleによりWARファイルをビルドしてTomcat配下に配置。Tomcatが検知してオートデプロイ
  • STSのリモートデバッグ機能を用いて、Tomcatプロセスに繋いでデバッグ

    • SpringBootは単独実行でWebサーバーとして稼働するが、JSPは諸事情あって動かない。かなり面倒くさくビルドも遅くなるが、Tomcatに直接配備する必要がある。
    • JSPは多方面に脆弱であるので、できれば別のテンプレートエンジンを使ったほうが良い。

Oracle DB インスタンスについて

Oracleのローカル開発環境構築は割愛。 なお、この環境構築時にはDockerForWindows、およびOracle公式のDockerImageを用いて、ローカルPC上に構築した。

https://itedge.stars.ne.jp/docker_image_oracle_database_19c/

Oracleの単体稼働に2GBのメモリが必要なため、8GBのPCで開発するには若干パワー不足になる。

必要なソフトウェアのダウンロード

JDK 11

Javaの実行ランタイムと開発用のライブラリ・ツールがまとまったセット。 自分の環境用のインストーラを入手。今回は、Windows向けのものを想定する。

https://www.oracle.com/java/technologies/javase-jdk11-downloads.html

STS

統合開発環境。アプリケーション開発に必要。 予めプラグインを組み込んでセットアップ済みのEclipseが配布されている。 自分の環境用のzipファイルを入手。今回は、Windows向けのものを想定する。

https://spring.io/tools

Tomcat 9

JavaServlet用のWebサーバー。 自分の環境用のzipファイルを入手。今回は、Windows向けのものを想定する。

https://tomcat.apache.org/download-90.cgi

JDKのインストールとセットアップ

  1. 入手したJDKインストーラを実行してJava11をインストールする。(特別設定の変更は不要)

  2. インストール後、コマンドプロンプトかターミナルを起動し、PATHに通っているjavaのバージョンを確認する。

> java -version
java version "11.0.6" 2020-01-14 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.6+8-LTS)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.6+8-LTS, mixed mode)

Tomcatのインストールとセットアップ

  1. 開発環境の環境変数を変更し「JAVA_HOME」にJDKのインストールパスを設定する。 ここでは、 C:\Program Files\Java\jdk-11.0.6 を設定したとする。 設定は、Windowsであれば 設定 > 環境変数を編集 を検索し、ユーザー環境変数に設定する。

  2. 入手したTomcatのバイナリを解凍する。ここでは、解凍し C:\opt\apache-tomcat-9.0.34 に配置したとする。

  3. 解凍したディレクトリのbin配下にコマンドプロンプトやターミナルを起動し、 下記コマンドを実行してTomcatデバッグ用ポートありで起動する。

./catalina jpda start
  1. ブラウザを起動し、http://localhost:8080 にアクセスしてTomcatの管理画面が表示されるか確認する。

STSのインストールとセットアップ

STSの解凍と起動

  1. STSを解凍し、配置しておきたいフォルダに配置する。

  2. SpringToolSuite4.exe を実行する。

Note:メモリ不足の場合、 SpringToolSuite4.ini からJVM引数を編集する。

  1. workspace(ファイル配置場所)を聞かれたら、とりあえず新しいフォルダ先を指定する。 Eclipseのデフォルトに従うなら、 (SpringToolSuite4.exeがあるフォルダ)/workspace

Gradleプラグインのインストール

  1. STS上部メニュー HelpEclipse Marketplace... をクリック
  2. Eclipse MarcketPlaceウィンドウの Search タブで、 Find:Gradle を入力してEnter
  3. Gradle ID Pack x.x.x ~Install
  4. ライセンスの確認ダイアログが表示されたら、「~Agree All~」みたいなラジオボタンを選択して「Finish」
  5. STSを再起動する

文字コードの設定

デフォルトのエンコーディングを、Java標準の UTF-8 に設定する。

  1. STS上部メニュー Window > Preferences をクリック
  2. General > Workspace をクリック
  3. Text file encoding から、 OtherUTF-8 を選択
  4. Apply and Close をクリック

オプション:STSの設定

下記、設定しておくと便利なもの。 もし、開発プロジェクトで標準の環境を作りたい場合、これらを事前設定する手順を追加しておくとよいです。

STS日本語化

保管アクション

  • https://lifeinprogram.com/2018/11/25/post-355/
  • Javaでは、「Organize imports」(自動インポート)や「Format all lines」 + GoogleStyleフォーマッターによる 自動フォーマットがあればだいたい標準化できる。
  • checkStyle は静的解析ツールで、小うるさいことが多い。 新規プロジェクトでレビュー負荷を何がなんでも下げるんだ、という意志を貫きとおす自信がなければ、温かみのある手法に準じるのも一興。

サンプルプロジェクトの作成

サンプルプロジェクト作成

  1. STS左側 Package Explorer を左クリック > New > Spring Starter Project をクリック

  2. 下記内容を入力して「Next」

Service URL:任意
Name: プロジェクト名。ここでは「sample」とする

Type:「Gradle(Buildship3.x)」
Packaging:「War」
Java Version:「11」
Language:「Java」

Group: mavenがらみの設定。例えば google なら com.google となっている。「org.pack」とする。
Artifact: mavenがらみの設定。通常プロジェクト名をいれる。ここは「sample」とする
Version: ビルドスクリプトで参照できる。とりあえず0.0.1-SNAPSHOT でよい。
Description: 説明文。
Package: Javaパッケージの接頭辞。ここでは「org.pack.sample」とする。
  1. 定番のフレームワーク依存関係の入力し、Finish

ここでは、下記をチェック。この内容は、 build.gradle に反映される。

  • SQL > MyBatis Framework
  • SQL > Oracle Driver
  • Web > Spring Web

サンプルアプリケーションの実装(共通部分、Mybatis用設定)

下記に従って、ソースコードを変更、または新規作成する。

編集: src/main/java/org/pack/sample/SampleApplication.java

アプリケーションのエントリーポイント(実行起点)となるソースコード

  • @ComponentScan を追加、サブパッケージをスキャンしてDIできるようにする
  • @EnableAutoConfiguration を追加、JavaConfig機能を有効にする。
package org.pack.sample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan
@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class })
@SpringBootApplication
public class SampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(SampleApplication.class, args);
    }

}

新規: src/main/java/org/pack/sample/config/MyBatisConfiguration.java

DI時にマニュアルでインスタンス作成することでいろいろカスタマイズするため設定コード。 このコードはMyBatisに特化。

  • MyBatisの定義クラス Mapper のパッケージを指定
  • ついでに色々要求されたので、いい感じのを記述
package org.pack.sample.config;

import java.sql.Driver;
import java.util.Properties;

import javax.sql.DataSource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;

@Configuration
public class MyBatisConfiguration {
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer() {
        MapperScannerConfigurer configurer = new MapperScannerConfigurer();
        configurer.setBasePackage("org.pack.sample.mapper");
        configurer.setSqlSessionFactoryBeanName("sqlSessionFactory");
        return configurer;
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
          SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
          factoryBean.setDataSource(dataSource);
          return factoryBean.getObject();
    }

    @SuppressWarnings("unchecked")
    @Bean
    public DataSource dataSource(DataSourceProperties properties) throws ClassNotFoundException {
        SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
        dataSource.setDriverClass((Class<? extends Driver>) Class.forName(properties.determineDriverClassName()));
        dataSource.setUrl(properties.determineUrl());
        dataSource.setUsername(properties.determineUsername());
        dataSource.setPassword(properties.determinePassword());
        Properties connectionProperties = new Properties();
        connectionProperties.setProperty("autoCommit", "false");
        dataSource.setConnectionProperties(connectionProperties);
        return dataSource;
    }
}

編集:src/main/resources/application.properties

全体の設定ファイル。

spring.profiles.active=local

spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

mybatis.mapper-locations=classpath*:/com/example/demo/mapper/**/*.xml

編集:src/main/resources/application.properties

ローカル環境用の設定ファイル。Tomcatのcontext.xmlの記述で切り替え可能。

参考: https://exceptionblend.wordpress.com/2013/02/11/springframework-profile/

spring.datasource.url=jdbc:oracle:thin:@localhost:1521:orcl
spring.datasource.username=system
spring.datasource.password=password
spring.datasource.driverClassName=oracle.jdbc.driver.OracleDriver

サンプルアプリケーションの実装(個別実装)

新規:src/main/resources/org/pack/sample/mapper/HealthCheckMapper.xml

MyBatisのMapper向けのSQL定義ファイル。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.pack.sample.mapper.HealthCheckMapper">
    <select id="select" resultType="Long">
        SELECT 1 FROM dual
    </select>
</mapper>

新規:src/main/resources/org/pack/sample/mapper/HealthCheckMapper.xml

MyBatisのMapper向けのJavaファイル。

package org.pack.sample.mapper;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface HealthCheckMapper {
    long select();
}

新規:src/main/java/org/pack/sample/controller/LoginController.java

Webアプリケーションのリクエストを直接処理する。

package org.pack.sample.controller;

import org.jboss.logging.Logger;
import org.pack.sample.mapper.HealthCheckMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("/login")
public class LoginController {
    Logger logger = Logger.getLogger(LoginController.class);
    
    @Autowired
    private HealthCheckMapper healthCheck;

    @RequestMapping(value = "",  method = RequestMethod.GET)
    public String index() {
        long n = healthCheck.select();
        
        logger.info("helthckeck:" + n);
        
        return "login";
    }

    @RequestMapping(value = "",  method = RequestMethod.POST)
    public String login(LoginForm form, Model model) {
        LoginModel loginModel = new LoginModel();
        
        final String loginid = "user";
        final String password = "pass";
        
        if (loginid.equals(form.loginid) && password.equals(form.password)) {
            loginModel.message = "ログインできました.";
        } else {
            loginModel.message = "ユーザーまたはパスワードが違います.";
        }
        
        loginModel.loginid = form.loginid;
        loginModel.password = form.password;
        
        model.addAttribute("loginModel", loginModel);
        
        return "login";
    }

    /**
    * ログインフォームのリクエストパラメータ。本当は別ファイルに書くべき
    */
    public class LoginForm {
        private String loginid;
        private String password;
        
        public String getLoginid() {
            return loginid;
        }
        public void setLoginid(String loginid) {
            this.loginid = loginid;
        }
        public String getPassword() {
            return password;
        }
        public void setPassword(String password) {
            this.password = password;
        }
    }
    /**
    * ログインフォームのレンダリングに利用するパラメータ。本当は別ファイルに書くべき
    */
    public class LoginModel {
        private String message;
        private String loginid;
        private String password;
        
        public String getMessage() {
            return message;
        }
        public void setMessage(String message) {
            this.message = message;
        }
        public String getLoginid() {
            return loginid;
        }
        public void setLoginid(String loginid) {
            this.loginid = loginid;
        }
        public String getPassword() {
            return password;
        }
        public void setPassword(String password) {
            this.password = password;
        }
    }
}

新規:src/main/webapp/WEB-INF/views/login.jsp

HTMLテンプレート。

webappディレクトリが、WARファイルを配置した時のルートになる。この下に、jsやcssなどを配置する。

また、WEB-INF配下はTomcatの仕様でブラウザから直接アクセスできなくなる。

<%@ page contentType="text/html; charset=UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
</head>
<body>

<form action="login" method="POST">
    <div>${loginModel.message}</div>
    <table>
        <tr>
            <td>ユーザ</td>
            <td><input type="text" name="loginid" value="${loginModel.loginid}"></td>
        </tr>
        <tr>
            <td>パスワード</td>
            <td><input type="password" name="password" value=""></td>
        </tr>
    </table>
    <button type="submit">ログイン</button>
</form>

</body>
</html>

Gradleの依存関係更新

build.gradle または プロジェクト自体 を右クリック > Gradle > Reflesh Gradle Project で build.gradle の解析と Maven central リポジトリからの依存ライブラリの取得を実施する。

ビルドスクリプトの修正とTomcatへの配備

  1. build.gradleを編集し、下記とおりにする
plugins {
    id 'org.springframework.boot' version '2.2.6.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id 'java'
    id 'war'
}

group = 'org.pack'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
// ▽追加
def warName = 'sample.war'
def localTomcatWebapps = 'C:\\opt\\apache-tomcat-9.0.34\\webapps'
// △追加

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.2'
    runtimeOnly 'com.oracle.ojdbc:ojdbc8'
    providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}

// ▽追加
war {
    enabled = true
    archiveName "${warName}"
}

task deployLocal(type: Copy) {
    from "build/libs/${warName}"
    into "${localTomcatWebapps}"
}
// △追加

test {
    useJUnitPlatform()
}
  1. コマンドプロンプトまたはターミナルを起動してサンプルプロジェクト直下に移動、下記コマンドを実行する
gradlew war deployLocal

動作確認

  1. STSでLoginController.java を開き、止めたい場所の行番号をダブルクリックしてブレイクポイントを設定する。
  2. STSでプロジェクトを右クリック>Debug>Debug Configuration.. を選択。
  3. Remote Java Application の新しい構成を追加し、 Project sampleHost localhostPort 8000 で起動。
  4. http://localhost:8080/sample/login にアクセスし動作確認する。

参考委文献

JMeterの各値を外部ファイル参照にするTips

直近JMeterを利用して性能測定したが、外部設定を参照する機能が乏しく、結局ほとんど手書きになってしまった。 外部パラメータファイル化に関して知見ができたので、Tipsで残す。

外部ファイルのデータを読み出す

CSV Data Set Config を使う

  • 外部CSVを読み、ヘッダ行と同じ名前の変数に各行の値を入れて後続処理を続けるconfig
  • スレッドが1つ回るごとに1行進む。スレッドごとの行の勧めかたや共有は設定可能。複数スレッドが同時に次ループに突入しえる場合、採番のしかたはランダムになる
  • 一番楽な外部読み出し

FileToString 関数

下記でテキストデータを丸々取得できる

${__FileToString(path/to/textfile)}

BeanShellSamplerやListener、Preprocessorで頑張る

下記は、JSONファイルを読み込んで 1 階層目を逐次変数に設定する例。

import net.minidev.json.*;
import java.util.Map.Entry;
import java.nio.file.*;

String jsonFile = "./config.json";

Path file = Paths.get(jsonFile);
String text = Files.readAllLines(file).join("\r\n");

JSONObject jsonObj = (JSONObject) JSONValue.parse(text);

for (Entry<String, Object> entry : jsonObj.entrySet()){
    vars.put(entry.getKey(), entry.getValue());
}

変数・Function

スレッド番号の取得

// ctx.getThreadNum() によりスレッド番号取得可能。番号は0始まり、1刻み増加。
String varName = "id-thread-" + ctx.getThreadNum();
vars.put("id", vars.get(varName));

値の二重評価

vars.put("id", "123");
vars.put("sql1", "UPDATE sometable SET status=10 WHERE id=${id};");
${__V(${sql1})}

上記は、sql1 が一度_V()により展開され、一番外の ${}によりもう1度評価され ${id} が展開される。

Yamlでレスポンスを定義できるHTTPモックツール、yamoc

HTTPサーバーを模擬的に起動し、HTTP通信するソフトウェアのテストを補助するスタンドアロンのツールを作成した。

(動作イメージ)

f:id:packpak:20200222151117p:plain

ファイルパスとレスポンス定義が書かれたyamlを読み込み、一致した条件に対して固定のレスポンスを返す。

port: 8888

paths:
  # matching GET /hello
  - path: /hello
    methods: get
    response:
      status: 200
      bodytext: hello
      headers:
        Content-Type: text/plain

コンセプト

  • ハリボテ、軽量、シンプル
  • yamlファイル1つで状況が完全再現される。
  • レスポンスは単純に固定値を返す。レスポンスを変えたければ、yamlファイルを変更してプロセスを起動しなおす。
  • 手元で動作確認する程度の用途を想定。NginxやらIISが並ぶ環境に代理として立てるときの挙動は保証しない。特にメモリ消費やバッファリングなどを考慮していない。

使い方・入手方法

[1] ツールをビルドするため、.NET Core SDKをインストールする。また、動作する環境には少なくとも .NET Core Runtimeが必要。

https://dotnet.microsoft.com/download

(※Windows限定だが、Visual Studioに.csコードすべて入れてNugetにパッケージaddすれば、.NET Frameworkをランタイムとするexeもビルドできる。ただし自己責任)

[2] Githubに公開しているソースコードを入手する

Github https://github.com/pakuyuya/yamock-cs

[3] Readme.mdに従いビルド・yamlファイルを作成して実行する

わかっているバグ、問題と今後のfeture

  • バグ

    • 同時アクセスすると、ログがかぶってめちゃくちゃ見にくくなる。ロギングを別スレッドでキューイング/ディスパッチする実装に変えたい
  • feture

    • POST時などに設定されるリクエストボディ中のFormパラメータに対してのフィルタ条件の追加
    • フィルタ条件への not equal 演算子追加。less than, grater than は実装が面倒なのでやらない。
    • Proxyの追加。実装がめちゃくちゃ面倒だが、個人的に勉強したい
  • 実装上の問題

    • 自動テストがほぼ未実装
      • 実装が全く整理されていない。現状、2,3のサブパッケージに分かれているが密結合で単独でテストできない。
      • Httpコンポーネントに直接依存したせいで、自動テストが非常に面倒になってしまった。テストコードをまともに書いていないので、いつか改善したい。

ソフトウェア開発において高品質評価をうけるには

例え話

あるシステム刷新のためのプロジェクトで、新しいプログラミング言語フレームワークを採用するにあたり、開発ガイドラインを作ろうという話になった。 日ごろからアーキテクチャに関心を持っていた担当者は意気込み、膨大なプラクティスを実際の実装・設計作業を想定の上で丁寧に体系化し、技術の時流に合ったドキュメントが完成した。

しかし、それを受け取った従来からのエンジニア達は、まったく新しい技術を手に、前システムのプラクティスを混ぜ込みつつシステム開発をし、 開発ガイドラインが意図したものとはずれた方向へ実装してしまった。現状と乖離し、しかし独自の自律性を持ったドキュメントは、「参考にするが、鵜呑みにすると痛い目を見るドキュメント」として見られることとなった。

果たして、このドキュメントは、高品質といえるだろうか?

品質と主観

ひねくれた回答かもしれないが、それは見方によって違う、というのが一番的を得てると思っている。

何かの基準を達成すれば品質がいい、そうでなければ悪いといえる。 「何か」とは、たいてい実際にモノを使ってみたとき、思い通りに使えたかどうかだ。

だから何の基準も提示せず「高品質なもの」を要求するのは、「俺たちに都合のいいもの」を要求しているに過ぎない。

IPAが公開している、「ソフトウェア品質説明の考え方」という資料でも、下記の記述がある。

https://www.ipa.go.jp/files/000040880.pdf

「品質の良し悪しは顧客の評価で決まる」

顧客から低品質評価を受けるパターン

システム開発に従事しておりある程度の規模のIT部門・責任者とのやりとりを経験されているなら、そのうち品質に関するクレームを頂くことになる。

「バグが出るのは作業品質が悪いのではないか」「設計書と実装が乖離しているのは品質の面からどうなのか」「受託している以上品質担保してくれないと困る」

だいたいは『品質』という内容のない言葉を主張することが多いが、請負側は大抵ぐうの音も出ない。なぜなら、色々な経緯の上、それなりのものを納品したはずだらからだ。 それには、いくつかパターンがある

想定しない要求が増え、現実解が低品質なものになった

当初の設計を実装していったら、想定外のことが発覚し折れに折れ、誰から見てもダメなモノに成り下がる。 PoCやフィジビリティを見切れず、ウォーターフォールなら後工程で品質担保・検証するから何かしら出来上がるだろう、という、楽観的な見積もりだが方向転換を許さない環境で生まれる。

もしこの常習犯となりそうだったら、仮設検証とソフトウェア工学において”ここまで確認・検証すればうまくいく”ラインと"変えると過去確認・検証した内容が台無しになる前提"の見切りができていないことを肝に銘じるべきで、もしそんなことを知らない権力に晒された場合は説明責任があなたにあるはずだ。

顧客が求める品質を理解していなかった

例えるのも嫌な例だが、非常に政治的なIT部門の担当者から品質のクレームがあがったとき、彼が本当に求めるものは何かわかるだろうか。

経験則上、下記のような背景がある。

  • システムから直接利害をうけるユーザーからIT部門へクレームや要望があがり、その代理として要望している。
  • IT部門の担当者はコミュニケーションによる費用対効果の最大化を図る志向であることが多く、品質向上をコストを引き合いに求めている。
  • 社内で管理する資産や別ベンダー・部署への提示資料として再利用可能な資料を求めている。

システム開発が責務、と思っているエンジニアも少なくないと思われるが、それは、都合のいい顧客像を抱いているだけかもしれない。 実際、これらに配慮すれば、開発したアプリケーションは適切に使われるだろうし、逆に顧客のいる土俵にあがりこんで逆にハックするのも、それはそれで楽しい。

現実解を提示できなかった

下請けや外部から参入したパートナーに作業を依頼した結果、低品質なものが出来上がり品質向上しきれず破綻することも、よく見る光景だ。

言い換えれば、自分が逆に顧客の立場であり、彼らは顧客に対して高品質なものを納品する術を持っていなかった。そして、自らも彼らが低品質を招くパターンに至ることを阻止できなかった。

人月商売では「いい感じでお願いします」が禁句と思っている。人類への感性極まったアーティストか本当に価値観や考え方を見知った同僚にしか通用しない。 事細かに書かれたチェックリストや隙のないルールを提示しても、数割はまともに読まず、他割もないか目の前の課題や問題に熱中したとき、おもしろくないものなど忘れてしまう。

人の心理を理解し、デザインして、この領域における現実解を提示できない限り、ずっとこの問題に悩まされることになる。

最後に

契約を単に徹頭徹尾履行するだけでは、瑕疵とはならないが高品質にはならない。 高品質になるようにデザインされた契約や計画を履行することが価値につながる。

結局、高品質評価を受けるということは主観と満足度を満たすことに落ち着く。 世にいう品質特性とは、よくある品質評価を単に分類しただけであり、顧客が気づいていない品質を予測し満たすための指標であり、”これを満たすことで品質保証したとみなす”のは正直、ありきのビジネスに他ならないと内心思っている。

高品質なものづくりは、その領域においてそれなりの技術と知識を要するが、実践の敷居は低い。間違った道が分かりやすく、定性的に検証可能で、何より実践・関与した人間の中に積み上げが効く。

世の中に、良いものが作られて行ってほしい。

【Docker】PHP5.1.6 の環境を Xampp 1.5.4a で再現する

雑記

2020年現在、10年以上前から続いているレンタルサーバーは未だにPHP5.1.6だったりするし、CMSが依存していることもあり案件になりえることに驚いている。 (ITやWebをただの告知ツールと認識しているのであれば、10年などまったく代謝の動機に至らないかもしれないが。そもそも予算もないだろう。)

レガシーと呼ばれるPHPフレームワークですら動かない環境は、再現するのも一苦労。 当時のオープンソースファミリーだったApachePHPはEOLとなり一部リポジトリからダウンロードできなくなり、i386アーキテクチャは消え失せ、3年前ではビルドできた環境も今や公式リポジトリの消滅によりもはや闇の中を手探りするしかない。

ならば、当時の英知にあやかるしかない、ということで、「XAMPP for Linux」というあらかじめApacheMySQLなどがビルドされたパッケージを用いたDockerファイルを作成。

自動テストができないならせめてE2Eを・・・と悩める同志(?)の一助になれば。

DockerFile

# 32bit アーキテクチャじゃないと古いバージョンのxamppが動かない
FROM i386/centos:6

WORKDIR /usr/local/src

# TimeZoneの設定
RUN ln -sf /usr/share/zoneinfo/Japan /etc/localtime

# Xampp for Linux 1.5.4aのintall
RUN curl -L https://sourceforge.net/projects/xampp/files/XAMPP%20Linux/1.5.4a/xampp-linux-1.5.4a.tar.gz/download>xampp-linux-1.5.4a.tar.gz \
    && tar xvfz xampp-linux-1.5.4a.tar.gz && mv lampp /opt/lampp

EXPOSE 80

ADD entrypoint.sh /app/entrypoint.sh

CMD [ "/app/entrypoint.sh" ]

entrypoint.sh

#!/bin/sh

# デーモン起動
/opt/lampp/lampp start

# 無限ループ
while true; do sleep 1000; done

その実装は新しいルールを持ち込むかもしれない

最近の仕事で、色々もやもやしてきたので書き留める。

誰も何も考えていないようなルール

パートナー稼業でのSEという仕事柄、旧来からのシステムに触れる案件がいくつもあり、 たいがいの保守的な現場でいつ立てたかわからないお触書のようなルールをたくさん見てきた。

  • Gitを採用し変更履歴がすぐに見れるのに、修正の際に // add や // mod コメントを付与し旧コードはコメントアウトし記述は残す。
  • JavaのWebアプリの案件で、COBOLのクライアントアプリのときに作られた汎用的なコーディングルールを順守するようにしている。
  • 先進的なフレームワークを取り入れるのに、社内プロキシによりブラウザからGithubへのアクセスが禁じられている。

明らかに開発ワークフローを阻害するルールはだいたいわかりやすく一覧化されている。

きっと、作った当初はそれなりに考えていたし、今まで変えられずに引き継がれているのはそれなりの理由があると考えるのが妥当だ。

つまり、過去プロジェクトにより開発された資産/手法/知識が、来るべき時にメンテナンスコスト(負債)としてこの手に渡ってきただけのことになる。

コードにちりばめられた暗黙のルール

昨今の技術系記事で「〇〇パターンに従って鵜呑みに実装するのは全然理解できてないよね」と言及しているのをよく見る。

むやみやたらに「操作を抽象化したクラスから実態を生成し動作を動的に切り替えているから〇〇Strategy」とか命名規則をつけてコメントに「〇〇のStrategyクラスです」とだけつける、など。

この実装は、開発ワークフローにおいて「Strategyというパターン名を事前に理解しておくこと」という暗黙のルールを組み込む。 Strategyパターンというのが、実装上非常に理にかなっていて理解必須なのであれば有用だが、それ以上のメリットは見込めないだろう。(いわゆるDIもこのパターンを継承しているが、恐らくStrategyの名前を出すだけで終わるのは非常に厳しい。)

また、システムを覗くと、文面化・一覧化されてすらいないルールで埋め尽くされていることが多い。

仮にセミナー管理システムがあっとしたら、きっと次のようなルールがあるだろう。

  • あるセミナーを設定するには、講師を選択する
  • 講師は講師グループに属しており、大阪、名古屋などの地理的な分類、セミナーの専門性による分類を反映したものであるセミナーの種別によって選択できる・できないがある
  • ある講師グループは、特定のセミナーしか受けられないような特殊な条件が含まれる
  • 講師は資格や業務上のランク、および稼働時間上限ブッキングや休暇を考慮する必要がある

これだけなら小中学校の面倒くさい文章題を解くようなものかもしれないが、ここから画面やバッチに落とし込んだとき、もはや別制約が生まれてくる。

  • 講師は事前に審査などのワークフローを追加したシステムを経由して登録されている必要があり、適切なデータを用意して担当部署やバッチの処理を待たないと登録できない
  • 講師グループ入力は間違えることができず変更が多々あるので、担当部署で一元的にマスター管理している。また、現場目線での管理項目を持っている。
  • セミナー実施または中止後の実績入力は、各部署が必要なすべての管理項目を入力する必要がある。

もはや誰も理解したくないような現状が突貫で出来上がり、だいたい初版システムは「各位の利害のすり合わせが十分にできなかった」と評価が下される。

そしてだいたい、改修案件がちょろっとした工数で「うちもAI取り入れる」とか「マーケティングツールと連携する」と立ち上がる。

業務ルールが増えていく中、「三方良し」よろしく各位の効率の良いワークフローを維持し続けるのは至難だ。

ゴシップ的に「古いルールが悪」というのは危険思想

システムのルールに対して、最初参画したときは文句を言う人はいれど、長らく保守で馴染んだり開発したりした後はごく少ないはずだ。

ごく個人の分析では、それを踏襲しないとワークフローが成立しないことを知っているか否か、とみている。

たとえば、冒頭で出した3例からとりあげよう。

  • Gitを採用し変更履歴がすぐに見れるのに、修正の際に // add や // mod コメントを付与し旧コードはコメントアウトし記述は残す。

これも、並行開発・納品されたコードをマージするときは、何も考えずにコメントの範囲を切り貼りすればよく、加えて必要範囲外の修正を抑制する効果もある。 コミット作業する人物がGitに不慣れで、色々修正した後に一括コミットしてしまい、どこが誰の修正によって入ったかがGitログで判別できなくなった、というの避けられる。

これらを低減するには、「他社や他プロジェクトからのマージは、必ずGitのコミットログごと納品させる」「マージした時点、手直しした時点で2段階にコミットを分ける」などの新たなルールで置き換えて、わざわざ各開発者必読の読み物を1つ用意する必要がある。

  • JavaのWebアプリの案件で、COBOLのクライアントアプリのときに作られた汎用的なコーディングルールを順守するようにしている。

こんなとんでもルールだって、実はISO9001を取得した際の重要な証跡であり、変更するには全社的に見直しをする必要があるかもしれない。

管理視点、コストゲーム観点からすれば、そのままにするのも理解できないことではない。(現状をそのままにしておくのは、感性が鈍いとしか言いようがないが)

もしほんとうに根絶しようとするなら同じ土俵に上がりこんで、理解して否定すべきではないか。

蚊帳の外から好き放題に言うのは、ゴシップ誌が揶揄されている点にひどく似ている。

ワークフローとルールをデザインするために、恐れずルールを持ち込む

ワークフローを規定する上で、ルールは非常に強力な道具だ。 ワークフローを破壊する規定外の行為を制限する一方、プラス方面への想定外の作用も制限する。

1つのコードを持ち込むことで、ルールが生まれる。 つまりはそのコードをメンテナンスしたり、別ベンダーに渡したり、読んだり、あるいはシステムを使ったりするうえで影響する。

つまりは、どうあがこうと書いたコードは引き継ぐ誰かに影響するし、ならばこそいいものを作ればよい。

ちなみに、私は、ルールを必要最低限に最小化しようとする。

一番の理由は至極個人的で、あまりあれこれと試行錯誤しようとしない、色んな手段があると管理できなくなる、一つのよくできたツールを深く使いこなすのが好きだからだ。

だが、最近Golangに触れ評価されているところを見ると、割と普遍的かもしれない。 ミニマリズムは、コードを引き継ぐものが抱えるルールを減らしてくれる。開発ワークフローについてまわるすべてを効率化してくれる。

書いているものは例え小さいコードであっても、デザインの余地はある。

【Amazon Redshift】分散スタイルの効果実証ログ

Amazon Redshift の分散スタイルのチューニング結果を手元で試したログ。

結論

  • 分散スタイル設定の主目的は、ネットワークトラフィック抑制。
  • BIツールから投げられるような、取得する総データ量が軽微なクエリに対して、レスポンスの改善は期待できない。
  • バッチ処理によるGROUP BY や、JOINを多量にして、VARCHAR(50)とかデータ量の多いデータをどんどん取得・集計するクエリにはちょっと効果があるかも。

何故分散スタイルをチューニングするのか

再分散を抑制するため。

  • Redshiftは演算を実施するスライスたちにデータを分散させ、並列で実行させるアーキテクチャ
  • 再分散とは、分散したデータを1か所に集めないとできない演算が発生したときに、ネットワーク経由ですべてのノードにデータが完全に複製している状態を仮想で作り出す実行ステップ。

再分散はどれほど怖いのか?

  • 再分散はテーブル結合やGROUP BY 句を伴うクエリで発生する。

  • 単純なクエリで、最終的に転送するデータ量が少ないテーブルに対しては脅威とならない。

  • 最終的に取得する・転送するデータ量が多いとき、多量のbyteでデータ転送が発生するとき、再分散だけで数分要するなどのケースが発生しえる。

    • GROUP BY 後に流れる総データ量が数百GBに達するINSERT SELECTや、数千万レコードと数千万レコードでのJOINが発生するなど、明確なボトルネックが発生するケースのみチューニングする価値がある。
  • 何より肝心なのが、単体のレスポンスには直接関与することは少ない。たいていのクエリでは、EVENからDISTにして5~10%性能向上する程度。

  • トラフィック抑制には一定有効。逆に言えば、利用者があまりおらずトラフィックやリソースが競合しない環境ではチューニングに対する定常的な効果は薄い。

脅威とならないケースのコスト検証

  • あまり脅威とならないケースから。
  • 下記は、SQLを実行後、SELECT pg_last_query_id(); などでquery idを取得して svl_query_summaryテーブルの明細を出したもの。
-- サンプルテーブルに対して行取得
-- sandbox.tdata_even→ 1000万レコード
-- sandbox.master_a_10000→ 1万レコード
-- 取得結果→ 1万レコード
SELECT  master_a_10000.codea, count(*)
FROM sandbox.tdata_even
  LEFT JOIN sandbox.master_a_10000 ON (tdata_even.codea_loop = master_a_10000.codea)
GROUP BY codea;

SELECT pg_last_query_id();
SELECT query,stm,seg,step,maxtime,rows,bytes,label,workmem FROM svl_query_summary WHERE query = 6023269 ORDER BY query, stm, seg, step;

実績取得結果

query stm seg step maxtime rows bytes label workmem
6023269 0 0 0 64765 10000 130000 scan tbl=5892173 name=master_a_10000 0
6023269 0 0 1 64765 10000 0 project 0
6023269 0 0 2 64765 10000 80000 bcast 0
6023269 0 1 0 65898 10000 80000 scan tbl=852075 name=Internal Worktable 0
6023269 0 1 1 65898 10000 0 project 0
6023269 0 1 2 65898 10000 160000 hash tbl=405 256376832
6023269 1 2 0 213503 10000000 130000000 scan tbl=5891185 name=tdata_even 0
6023269 1 2 1 213503 10000000 0 project 0
6023269 1 2 2 213503 10000000 0 project 0
6023269 1 2 3 213503 10000000 0 hjoin tbl=405 0
6023269 1 2 4 213503 10000000 0 project 0
6023269 1 2 5 213503 10000000 0 project 0
6023269 1 2 6 213503 1000 24000 aggr tbl=414 60555264
6023269 1 2 7 213503 1000 0 dist 0
6023269 1 3 0 215655 1000 16000 scan tbl=852077 name=Internal Worktable 0
6023269 1 3 1 215655 1000 24000 aggr tbl=417 242221056
6023269 1 3 2 215655 1000 0 project 0
6023269 1 3 3 215655 1000 0 project 0
6023269 1 3 4 215655 1000 0 return 0
6023269 1 3 5 215655 0 0 merge 0
6023269 1 3 6 215655 0 0 aggr tbl=425 0
6023269 1 3 7 215655 0 0 project 0
6023269 1 4 0 216345 1000 16000 scan tbl=852078 name=Internal Worktable 0
6023269 1 4 1 216345 1000 17000 return 0
  • 上記のうち、labelがbcast(ブロードキャスト)およびdist(必要分だけ分散)が再分散の実行ステップ明細になる。
  • ご覧の通り、scan(スキャン)やproject(データ選択操作)で処理したbytesの総量は130MB にも上っているが、肝心の再分散の転送バイト数は非常に小さいものとなっている。
    • Redshiftは列指向で処理するため、SQLで指定した列のみを処理している。再分散についても同様で、100列あろうとSQLに書いた列のみが対象となる。
    • GROUP BY 演算(project、aggr)に対しても発生するが、svl_query_summaryを見るに、どうやら1スライス内でできる限り演算量を減らした後、必要分だけ分散している。

改善ケース:表結合による大量レコードの分散

  • 通常、大量レコードのテーブル×数レコードのテーブルの結合では、負荷の少ないほうのテーブルが分散する。(ANALYZEにて統計情報を最新化しておく必要あり。また、BCAST_BOTHの場合はどちらのテーブルも分散される)
  • レコード数が少ないテーブルの分散は数十kbに満たないこと多々あり、これらがボトルネックとなることはまれ。

バッドケースのコスト検証

-- 1000万レコードx1000万レコードで、1レコードあたり 150byte程度のデータを1万レコードに集約
SELECT a.codea_loop, count(*)
FROM sandbox.tdata_even a join sandbox.tdata_even b using (sha512)
GROUP BY a.codea_loop;
SELECT pg_last_query_id();
SELECT query,stm,seg,step,maxtime,rows,bytes,label,workmem FROM svl_query_summary WHERE query = 6023418 ORDER BY query, stm, seg, step;
query stm seg step maxtime rows bytes label workmem
6023687 0 0 0 5821035 10000000 1400000000 scan tbl=5891185 name=tdata_even 0
6023687 0 0 1 5821035 10000000 0 project 0
6023687 0 0 2 5821035 10000000 1400000000 bcast 0
6023687 0 1 0 5835378 10000000 1400000000 scan tbl=853799 name=Internal Worktable 0
6023687 0 1 1 5835378 10000000 0 project 0
6023687 0 1 2 5835378 10000000 1520000000 hash tbl=405 256376832
6023687 0 1 2 5822111 0 0 hash tbl=405 0
6023687 1 2 0 15292438 10000000 1450000000 scan tbl=5891185 name=tdata_even 0
6023687 1 2 1 15292438 10000000 0 project 0
6023687 1 2 2 15292438 10000000 0 project 0
6023687 1 2 3 15292438 10000000 0 hjoin tbl=405 0
6023687 1 2 4 15292438 10000000 0 project 0
6023687 1 2 5 15292438 10000000 0 project 0
6023687 1 2 6 15292438 1000 24000 aggr tbl=414 18087936
6023687 1 2 7 15292438 1000 0 dist 0
6023687 1 3 0 15305103 1000 16000 scan tbl=853810 name=Internal Worktable 0
6023687 1 3 1 15305103 1000 24000 aggr tbl=417 72351744
6023687 1 3 2 15305103 1000 0 project 0
6023687 1 3 3 15305103 1000 0 project 0
6023687 1 3 4 15305103 1000 0 return 0
6023687 1 3 5 15305103 0 0 merge 0
6023687 1 3 6 15305103 0 0 aggr tbl=424 0
6023687 1 3 7 15305103 0 0 project 0
6023687 1 4 0 15305790 1000 16000 scan tbl=853811 name=Internal Worktable 0
6023687 1 4 1 15305790 1000 17000 return 0
  • bcast により、1,400,000,000 bytes の転送がある。全ノード含めて1.4GB弱。
    • 再分散のためのネットワーク帯域、メモリスペース、およびCPUパワーすべてを消費していると思われる。
    • このクエリ単体ではあまりボトルネックとならないが、複数並列で稼働した場合に、各リソースを奪い合いボトルネックとなることが起こりえる。
  • また、このクエリは全体で21秒かかったが、結合に使ったのが128バイトのVARCHARであり、HASH演算と結合にかかったコストが非常に高かったことが主要因。

改善する

先ほどEVEN分散だったテーブルを、sha512列を分散キーに選んだテーブルに変更しなおしたテーブルを作成。

SELECT a.codea_loop, count(*)
FROM sandbox.tdata_distsha512 a join sandbox.tdata_distsha512 b using (sha512)
GROUP BY a.codea_loop;
SELECT pg_last_query_id();
query stm seg step maxtime rows bytes label workmem
6023693 0 0 0 4495695 10000000 1400000000 scan tbl=5893210 name=tdata_distsha512 0
6023693 0 0 1 4495695 10000000 0 project 0
6023693 0 0 2 4495695 10000000 0 project 0
6023693 0 0 3 4495695 10000000 1520000000 hash tbl=403 256376832
6023693 1 1 0 13986572 10000000 1450000000 scan tbl=5893210 name=tdata_distsha512 0
6023693 1 1 1 13986572 10000000 0 project 0
6023693 1 1 2 13986572 10000000 0 project 0
6023693 1 1 3 13986572 10000000 0 hjoin tbl=403 0
6023693 1 1 4 13986572 10000000 0 project 0
6023693 1 1 5 13986572 10000000 0 project 0
6023693 1 1 6 13986572 2000 48000 aggr tbl=412 21626880
6023693 1 1 7 13986572 2000 0 dist 0
6023693 1 2 0 13994773 2000 32000 scan tbl=853838 name=Internal Worktable 0
6023693 1 2 1 13994773 1000 24000 aggr tbl=415 86507520
6023693 1 2 2 13994773 1000 0 project 0
6023693 1 2 3 13994773 1000 0 project 0
6023693 1 2 4 13994773 1000 0 return 0
6023693 1 2 5 13994773 0 0 merge 0
6023693 1 2 6 13994773 0 0 aggr tbl=422 0
6023693 1 2 7 13994773 0 0 project 0
6023693 1 3 0 13995445 1000 16000 scan tbl=853839 name=Internal Worktable 0
6023693 1 3 1 13995445 1000 17000 return 0
  • 先ほどと比べると、stm 0 の再のbcastとInternalWorktableに対するscan / projectがなくなり、seg1が消滅した。
  • ただし、改善しても所要秒数は 18.5 秒と、10%強しか改善していない。
  • ネットワークトラフィックは1.4GB弱から数十KBにまで低減している。(詳しくは、STL_DISTやSTL_BCASTテーブルから観測可能。)