Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Sansanアーキテクチャ史 / A Brief History of Sansan's Architecture

Sansan
October 23, 2019

Sansanアーキテクチャ史 / A Brief History of Sansan's Architecture

■イベント
Sansan Builders Box 2019
https://jp.corp-sansan.com/sbb2019/

■登壇概要 

タイトル:Sansanアーキテクチャ史

登壇者: 
プロダクト開発部 荒川聖悟

▼Sansan Builders Box 

https://buildersbox.corp-sansan.com/

Sansan

October 23, 2019
Tweet

More Decks by Sansan

Other Decks in Technology

Transcript

  1. Sansanアーキテクチャ史

    View Slide

  2. 荒川 聖悟(Shogo Arakawa)
    プロダクト開発部 サーバサイドエンジニア
    @adsholoko
    ad-sho-loko
    2018年11月 Sansan株式会社入社
    SansanのAPI開発を主に業務では行ってい
    る。好きな領域は低レイヤ。

    View Slide

  3. Sansan Builders Box
    Agenda
    - Sansanとは?
    - Sansanのアーキテクチャの歴史
    > v1/v2/v3の変遷
    > それぞれの学び

    View Slide

  4. Sansan Builders Box
    Sansanとは?
    AI×人によるデータ入力
    データベース化
    マルチデバイスで活用
    他システムとの連携
    お客様
    AIなどのテクノロジーとオペレーターに
    よる多重入力を活用し、高い入力精度を実現
    名刺をスキャン
    4
    3
    2
    1
    Sansanスキャナ スマートフォンアプリ

    View Slide

  5. Sansan Builders Box
    Sansanとは?
    12年間の歴史のあるアプリケーション

    View Slide

  6. Sansan Builders Box
    Sansanとは?

    View Slide

  7. v1

    View Slide

  8. Sansan Builders Box
    v1 :各レイヤの役割
    BLL
    DAL
    WebForms
    ビジネスロジック担当のレイヤ
    BLLと対応するUIのレイヤ
    SQLなどのデータアクセスを担当するレイヤ

    View Slide

  9. Sansan Builders Box
    v1 : コードの雰囲気( BLL )
    BLL
    DAL
    WebForms // CardInformationBll.cs
    public void DeleteCard(string cardId)
    {
    try
    {
    connLK.OpenConnection();
    connLK.BeginTransaction();
    CardInformationDal dal = new CardInformationDal();
    DataTable dt = dal.GetCard(cardId);
    if (dal.DeleteStack(dt.Rows[0])
    {
    throw new NotExpectedResultException(message);
    }
    connLK.Commit(); // ロールバック処理などは省略
    }
    }

    View Slide

  10. Sansan Builders Box
    v1 : コードの雰囲気( BLL )
    BLL
    DAL
    WebForms // CardInformationBll.cs
    public void DeleteBizCard(string cardId)
    {
    try
    {
    connLK.OpenConnection();
    connLK.BeginTransaction();
    CardInformationDal dal = new CardInformationDal();
    DataTable dt = dal.GetCard(cardId);
    if (dal.DeleteStack(dt.Rows[0])
    {
    throw new NotExpectedResultException(message);
    }
    connLK.Commit(); // ロールバック処理などは省略
    }
    }
    トランザクション管理

    View Slide

  11. Sansan Builders Box
    v1 : コードの雰囲気( BLL )
    BLL
    DAL
    WebForms // CardInformationBll.cs
    public void DeleteBizCard(string cardId)
    {
    try
    {
    connLK.OpenConnection();
    connLK.BeginTransaction();
    CardInformationDal dal = new CardInformationDal();
    DataTable dt = dal.GetCard(cardId);
    if (dal.DeleteStack(dt.Rows[0])
    {
    throw new NotExpectedResultException(message);
    }
    connLK.Commit(); // ロールバック処理などは省略
    }
    }
    DALと1対1で対応

    View Slide

  12. Sansan Builders Box
    v1 : コードの雰囲気( DAL )
    BLL
    DAL
    WebForms // CardInformationDal.cs
    public void DeleteStack(string cardId)
    {
    var sql = new StringBuilder();
    sql.AppendFormat("update {0} c", Const.TableName);
    sql.AppendFormat(" set delete_flag = :{0}", Const.Delete);
    sql.AppendFormat(" where c.{0} = :{1}", Const.CardId, cardId);
    return DBHelper.ExecuteQuery(sql.ToString());
    }
    手動でクエリ結合

    View Slide

  13. Sansan Builders Box
    v1の良い点
    - とにかくシンプルに書き下せる
    - 良くも悪くもほとんど共通化を意識していなかった
    - 画面と業務ロジックの結びつきが明確
    - 1対1で対応するので当たり前

    View Slide

  14. Sansan Builders Box
    v1の悪いところ
    - 画面駆動アーキテクチャになりがち
    - 業務ロジックの使いまわしが効かなくて辛い
    - 各レイヤの責務があいまい
    - 雰囲気でレイヤを使っていて実装に統一性が取れず辛い

    View Slide

  15. v2

    View Slide

  16. Sansan Builders Box
    v2 : 概要
    Domain
    Model
    Repository
    Service ビジネスロジックを担当するレイヤ
    モデルとO/Rマッピングを担当するレイヤ
    データアクセスの抽象化レイヤ

    View Slide

  17. Sansan Builders Box
    v2 : 概要
    Domain
    Model
    Repository
    Service ビジネスロジックを担当するレイヤ
    モデルとO/Rマッピングを担当するレイヤ
    データアクセスの抽象化レイヤ
    用語的には
    いわゆるDDDライクな
    アーキテクチャ

    View Slide

  18. Sansan Builders Box
    v2 : アーキテクチャ図
    ICardDeleteService
    (Interface)
    Service
    Domain
    Model
    Repository
    CardDelete
    Service
    Card
    CardRepository
    ICardRepository
    (Interface)

    View Slide

  19. Sansan Builders Box
    v2 : コードの雰囲気(Service)
    Domain
    Model
    Repository
    Service // CardDeleteServcie.cs
    public CardDeleteResult Delete(string cardId)
    {
    try
    {
    var target = StackRepository
    .Where((stack, card) => card.Id == cardId)
    .Select((stack, card) => new object[] { cardId })
    .SingleOrDefault();
    Transaction.Begin();
    target.Delete();
    CardRepository.Save(new[] { target });
    Transaction.Commit();
    return new CardDeleteResult { Status = CardDeleteResult.DeleteStatus.Suc
    cess, Target = target };
    }
    }

    View Slide

  20. Sansan Builders Box
    v2 : コードの雰囲気(Service)
    Domain
    Model
    Repository
    Service // CardDeleteServcie.cs
    public CardDeleteResult Delete(string cardId)
    {
    try
    {
    var target = CardRepository
    .Where((stack, card) => card.Id == cardId)
    .Select((stack, card) => new object[] { cardId })
    .SingleOrDefault();
    Transaction.Begin();
    target.Delete();
    CardRepository.Save(new[] { target });
    Transaction.Commit();
    return new CardDeleteResult { Status = CardDeleteResult.DeleteStatus.Suc
    cess, Target = target };
    }
    }
    O/R マッピング

    View Slide

  21. Sansan Builders Box
    v2 : コードの雰囲気(Service)
    Domain
    Model
    Repository
    Service // CardDeleteServcie.cs
    public CardDeleteResult Delete(string cardId)
    {
    try
    {
    var target = CardRepository
    .Where((stack, card) => card.Id == cardId)
    .Select((stack, card) => new object[] { cardId })
    .SingleOrDefault();
    Transaction.Begin();
    target.Delete();
    CardRepository.Save(new[] { target });
    Transaction.Commit();
    return new CardDeleteResult { Status = CardDeleteResult.DeleteStatus.Suc
    cess, Target = target };
    }
    }
    Repositoryが抽象化!

    View Slide

  22. Sansan Builders Box
    v2 : コードの雰囲気(Domain Model)
    Domain
    Model
    Repository
    Service // Card.cs
    [TableName("trn_card")]
    public class Card
    {
    [ColumnName("id", ColumnType.VarChar, 10)]
    public string Id { get; set; }
    ...
    }
    テーブル名を指定
    カラムの情報を指定

    View Slide

  23. Sansan Builders Box
    v2 : コードの雰囲気(Repository)
    Domain
    Model
    Repository
    Service foreach (var custom in CustomUpdate)
    {
    if (custom is string)
    {
    var customString = (string)custom;
    switch (customString)
    {
    case "Address.PostalCode":
    addressSetClause.AppendFormat("{0}postal_code = :POSTAL_CODE ", comma);
    var postalCodeParam = DbParameterFactory.Create(card.Address.PostalCode);
    postalCodeParam.ParameterName = ":POSTAL_CODE";
    paramList.Add(postalCodeParam);
    if (string.IsNullOrEmpty(comma)) comma = ",";
    break;
    case "Address.Prefecture":
    addressSetClause.AppendFormat("{0}address_prefecture = :ADDRESS_PREFECTURE ", comma);
    var prefectureParam = DbParameterFactory.Create(card.Address.Prefecture);
    prefectureParam.ParameterName = ":ADDRESS_PREFECTURE";
    paramList.Add(prefectureParam);
    if (string.IsNullOrEmpty(comma)) comma = ",";
    break;
    case "Address.City":
    addressSetClause.AppendFormat("{0}address_city = :ADDRESS_CITY ", comma);
    var cityParam = DbParameterFactory.Create(card.Address.City);
    cityParam.ParameterName = ":ADDRESS_CITY";
    paramList.Add(cityParam);
    if (string.IsNullOrEmpty(comma)) comma = ",";
    break;
    case "Address.Street":
    addressSetClause.AppendFormat("{0}address_street = :ADDRESS_STREET ", comma);
    var streetParam = DbParameterFactory.Create(card.Address.Street);
    streetParam.ParameterName = ":ADDRESS_STREET";
    paramList.Add(streetParam);
    if (string.IsNullOrEmpty(comma)) comma = ",";
    break;
    case "Address.Building":
    addressSetClause.AppendFormat("{0}address_building = :ADDRESS_BUILDING ", comma);
    var buildingParam = DbParameterFactory.Create(card.Address.Building);
    buildingParam.ParameterName = ":ADDRESS_BUILDING";

    View Slide

  24. Sansan Builders Box
    v2 : コードの雰囲気(Repository)
    Domain
    Model
    Repository
    Service foreach (var custom in CustomUpdate)
    {
    if (custom is string)
    {
    var customString = (string)custom;
    switch (customString)
    {
    case "Address.PostalCode":
    addressSetClause.AppendFormat("{0}postal_code = :POSTAL_CODE ", comma);
    var postalCodeParam = DbParameterFactory.Create(card.Address.PostalCode);
    postalCodeParam.ParameterName = ":POSTAL_CODE";
    paramList.Add(postalCodeParam);
    if (string.IsNullOrEmpty(comma)) comma = ",";
    break;
    case "Address.Prefecture":
    addressSetClause.AppendFormat("{0}address_prefecture = :ADDRESS_PREFECTURE ", comma);
    var prefectureParam = DbParameterFactory.Create(card.Address.Prefecture);
    prefectureParam.ParameterName = ":ADDRESS_PREFECTURE";
    paramList.Add(prefectureParam);
    if (string.IsNullOrEmpty(comma)) comma = ",";
    break;
    case "Address.City":
    addressSetClause.AppendFormat("{0}address_city = :ADDRESS_CITY ", comma);
    var cityParam = DbParameterFactory.Create(card.Address.City);
    cityParam.ParameterName = ":ADDRESS_CITY";
    paramList.Add(cityParam);
    if (string.IsNullOrEmpty(comma)) comma = ",";
    break;
    case "Address.Street":
    addressSetClause.AppendFormat("{0}address_street = :ADDRESS_STREET ", comma);
    var streetParam = DbParameterFactory.Create(card.Address.Street);
    streetParam.ParameterName = ":ADDRESS_STREET";
    paramList.Add(streetParam);
    if (string.IsNullOrEmpty(comma)) comma = ",";
    break;
    case "Address.Building":
    addressSetClause.AppendFormat("{0}address_building = :ADDRESS_BUILDING ", comma);
    var buildingParam = DbParameterFactory.Create(card.Address.Building);
    buildingParam.ParameterName = ":ADDRESS_BUILDING";
    リポジトリが”超”肥大化
    ※ 1メソッド内の処理

    View Slide

  25. Sansan Builders Box
    v2 : コードの雰囲気(Repository)
    ファイルの肥大化 モジュールの肥大化
    × = これじゃない感….

    View Slide

  26. その結果…

    View Slide

  27. DDDの導入に失敗した
    By 社内のエンジニア
    ※あくまでも個人の感想です

    View Slide

  28. Sansan Builders Box
    v2の課題と学び
    - Repositoryの肥大化と実装難易度の高さ
    - Domain ModelごとにRepositoryができる
    - 一応レイヤごとに分割したが、業務ごとに分割できずに、肥大化する
    - 「共通化」の認識を共通化させることが出来なかった
    - フロントエンド固有のロジックが共通されたりする(実はAPI専用メソッドなど)
    - エンジニア全員に思想と実装を浸透させることはさらに難しい
    - とにかく遅い!パフォーマンス出ない!
    - 発行されるクエリが多い&謎すぎてチューニング作業が難航

    View Slide

  29. Sansan Builders Box
    v2の良かった点
    - 画面駆動からは脱却できた
    - 手探りながらも業務ロジックを共通化できつつあった
    - まれに効率的(ほとんど聞いたことはない)

    View Slide

  30. v3

    View Slide

  31. Sansan Builders Box
    v3 : アーキテクチャ図
    UI
    Domain
    Controller
    Application
    CardDelete
    Service
    ICardDelete
    DataAccessor
    Card
    QueryService Controller QueryService
    API
    Web
    CardDelete
    DataAccessor
    ICardDelete
    Service
    Infrastructure

    View Slide

  32. Sansan Builders Box
    v3 : アーキテクチャ図
    Domain
    Controller
    Application
    CardDelete
    Service
    ICardDelete
    DataAccessor
    Card
    QueryService Controller QueryService
    API
    Web
    CardDelete
    DataAccessor
    ICardDelete
    Service
    Infrastructure
    フロントエンド固有のクエリを発行
    (基本SELECT文のみ)
    UI

    View Slide

  33. Sansan Builders Box
    v3 : アーキテクチャ図
    Domain
    Controller
    Application
    CardDelete
    Service
    ICardDelete
    DataAccessor
    Card
    QueryService Controller QueryService
    API
    Web
    CardDelete
    DataAccessor
    ICardDelete
    Service
    Infrastructure
    1対1で対応
    UI

    View Slide

  34. Sansan Builders Box
    v3 : アーキテクチャ図
    Domain
    Controller
    Application
    CardDelete
    Service
    ICardDelete
    DataAccessor
    Card
    QueryService Controller QueryService
    API
    Web
    CardDelete
    DataAccessor
    ICardDelete
    Service
    Infrastructure
    ①共通のクエリを発行する
    UI

    View Slide

  35. Sansan Builders Box
    v3 : アーキテクチャ図
    Domain
    Controller
    Application
    CardDelete
    Service
    ICardDelete
    DataAccessor
    Card
    QueryService Controller QueryService
    API
    Web
    CardDelete
    DataAccessor
    ICardDelete
    Service
    Infrastructure
    ②テクノロジ依存を隠蔽化
    UI

    View Slide

  36. Sansan Builders Box
    v3 : アーキテクチャ図(業務レイヤでの分割)
    UI Controller
    Application
    QueryService
    card company admin
    xxxService xxxService xxxService

    View Slide

  37. Sansan Builders Box
    v3 : コードの雰囲気( Application )
    Domain
    Infrastructure
    Application // BizCardService.cs
    public DeleteResult DeleteCard(string cardId)
    {
    var card = CardDataAccessor.GetDeleteTargetCard(cardId);
    if (card == null)
    {
    return DeleteResult.NotFound;
    }
    using (var tx = TransactionScopeFactory.Create())
    {
    CardDataAccessor.DeleteStack(card.Id);
    tx.Complete();
    }
    return DeleteResult.Success;
    }

    View Slide

  38. Sansan Builders Box
    v3 : コードの雰囲気( Application )
    Domain
    Infrastructure
    Application // BizCardService.cs
    public DeleteResult DeleteCard(string cardId)
    {
    var card = CardDataAccessor.GetDeleteTargetCard(cardId);
    if (card == null)
    {
    return DeleteResult.NotFound;
    }
    using (var tx = TransactionScopeFactory.Create())
    {
    CardDataAccessor.DeleteStack(card.Id);
    tx.Complete();
    }
    return DeleteResult.Success;
    }
    トランザクション管理

    View Slide

  39. Sansan Builders Box
    v3 : コードの雰囲気( Domain )
    Domain
    Infrastructure
    Application
    // IBizCardDataAccessor.cs
    public interface IBizCardDataAccessor
    {
    void DeleteCard(string cardId)
    }
    インターフェースを切る

    View Slide

  40. Sansan Builders Box
    v3 : コードの雰囲気( Infrastructure )
    Domain
    Application // BizCardDeleteDataAccessor.cs
    public void DeleteCard(string cardId)
    {
    var sql = @"
    update lkweb.trn_card
    set delete_flag = 1
    where card_id = @cardId";
    using (var conn = DbConnectionUtility.GetDataDbConnection(ucompanyId))
    {
    conn.Execute(sql, new { cardId });
    }
    }
    Infrastructure

    View Slide

  41. Sansan Builders Box
    v3 : コードの雰囲気( Infrastructure )
    Domain
    Application // BizCardDeleteDataAccessor.cs
    public void DeleteStack(string cardId)
    {
    var sql = @"
    update lkweb.trn_card
    set delete_flag = 1
    where card_id = @cardId";
    using (var conn = DbConnectionUtility.GetDataDbConnection(ucompanyId))
    {
    conn.Execute(sql, new { cardId });
    }
    }
    SQLを直で指定する
    Infrastructure

    View Slide

  42. Sansan Builders Box
    v3 : コードの雰囲気( Infrastructure )
    Domain
    Application // BizCardDeleteDataAccessor.cs
    public void DeleteCard(string cardId)
    {
    var sql = @"
    update lkweb.trn_card
    set delete_flag = 1
    where card_id = @cardId";
    using (var conn = DbConnectionUtility.GetDataDbConnection(ucompanyId))
    {
    conn.Execute(sql, new { cardId });
    }
    }
    実装クラス
    Infrastructure

    View Slide

  43. Sansan Builders Box
    v3の良かった点
    - シンプルさと共通化のバランスが取れている
    - 誰が見ても理解しやすい構造になっている
    - さらに部内のエンジニア内でもそれなりに浸透していると感じる
    - 無駄な抽象化は避けている
    - むやみにインターフェース切らない、など

    View Slide

  44. Sansan Builders Box
    v3の課題とこれから
    - モジュールの場所や実装は未だに悩むことがある
    - 悩むべくして悩んでるとも言える
    - v1 / v2共存問題

    View Slide

  45. Sansan Builders Box
    学びのまとめ
    - 時代とともに流行のアーキテクチャは移り変わる
    - いわゆる「レガシー」もこうして生まれる
    - アーキテクチャの浸透は想像以上に難しい
    - 弊部では新入社員向けにアーキテクチャに関する講座を有志で実施している
    - プラットフォームとアプリケーションに合ったアーキテクチャを選ぶ

    View Slide

  46. ご清聴ありがとうございました
    @adsholoko

    View Slide

  47. View Slide