- 設計
レイヤードアーキテクチャとは:依存関係とディレクトリ構成例・クリーンとの違い
レイヤードアーキテクチャとは、プログラムを役割ごとの層(レイヤー)に分け、層の間の依存を一方向にそろえる構成のことです。 画面や API を受け持つ層、業務ロジックの層、データ取得の層の3つに分ける形がよく知られています。
この記事では、オニオン・ヘキサゴナル・クリーンアーキテクチャとの違いを早見表で示したあと、同じ会員登録機能を「3層レイヤード」と「依存を逆にした構成」の両方で実装し、ディレクトリ構成・import 文・実行結果を並べて比べます。層の向きに反する import を自動で検出する設定例も載せています。用語の背景よりも、構成とコードに絞った内容です。
確認環境(確認日 2026-10-05):Ubuntu 24.04、Python 3.11.15(標準ライブラリの sqlite3、unittest)、import-linter 2.15。図は Mermaid CLI(@mermaid-js/mermaid-cli 12.0.0)で描画できることを確認しています。
この記事の結論
- 違いは層の数ではなく依存の向きです。 データ取得の層に業務ロジックが依存するのが従来のレイヤード、インターフェースで向きを逆にしたものがオニオン・ヘキサゴナル・クリーンです。
- 小規模で CRUD(登録・参照・更新・削除)中心なら、3層レイヤードで足ります。
- 業務ルールが多く、テストでデータベースを切り離したいなら、依存を逆にします。
- どちらを選んでも、層の向きに反する import はツールで検出しておくと、構成が保たれます。
4つの方式の早見表
| 方式 | 層・部品の名前 | 依存の向き | 業務ロジックは DB を知っているか |
|---|---|---|---|
| レイヤード(3層) | プレゼンテーション → ビジネスロジック → データアクセス | 上の層から下の層へ | 知っている(データアクセス層を直接呼ぶ) |
| オニオン | 中心にドメインモデル、その外側にリポジトリのインターフェースを含む層、最も外側に UI・インフラ・テスト | 外側から中心へ | 知らない(インターフェースだけを知る) |
| ヘキサゴナル(ポートとアダプター) | 内側にアプリケーション、境界にポート、外側にアダプター | 外側のアダプターから内側へ | 知らない(ポートだけを知る) |
| クリーン | 内側から Entities、Use Cases、Interface Adapters、Frameworks and Drivers | 外側の円から内側の円へ | 知らない(内側は外側を一切知らない) |
層の名前は、オニオンは Jeffrey Palermo 氏の 2008 年の記事、ヘキサゴナルは Alistair Cockburn 氏の記事、クリーンは Robert C. Martin 氏の 2012 年の記事の記載に基づいています(記事末尾の参考・出典)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「レイヤードアーキテクチャとは:依存関係とディレクトリ構成例・クリーンとの違い」
依存の向きを図で比べる
矢印は「import する側 → される側」を表します。
3層レイヤード
flowchart TD
P[プレゼンテーション層<br>presentation] --> B[ビジネスロジック層<br>business]
B --> D[データアクセス層<br>data]
D --> DB[(データベース)]
業務ロジックがデータアクセス層を import します。データベースの扱いを変えると、その上の層に影響が届く向きです。オニオンアーキテクチャの記事は、UI と業務ロジックがデータアクセスに結び付くことを、従来の層構成でいちばん多く見られる点として挙げています。
依存を逆にした構成
flowchart TD
P[プレゼンテーション層<br>presentation] --> A[アプリケーション層<br>application<br>手順とインターフェース]
I[インフラ層<br>infrastructure<br>SQLite での実装] --> A
A --> DM[ドメイン層<br>domain<br>業務ルール]
I --> DM
I --> DB[(データベース)]
M[main.py<br>組み立て役] -.-> P
M -.-> I
M -.-> A
インフラ層が、内側で宣言されたインターフェースを実装します。矢印はすべて内側(ドメイン層)へ向かい、ドメイン層から出る矢印はありません。 クリーンアーキテクチャの記事はこの決まりを「依存性のルール」と呼び、ソースコードの依存は内側にだけ向かい、内側の円は外側の円について何も知らない、と説明しています。
例1:3層レイヤードで書く会員登録
架空の会員登録機能を題材にします。ルールは3つです。名前は空にできない、メールアドレスには @ が必要、同じメールアドレスは登録できない。
ディレクトリ構成
layered_app/
├── .importlinter
├── main.py
└── membership/
├── __init__.py
├── business/
│ ├── __init__.py
│ └── member_service.py
├── data/
│ ├── __init__.py
│ └── member_dao.py
└── presentation/
├── __init__.py
└── cli.py
コード
membership/data/member_dao.py
"""データアクセス層:SQLite に会員を保存する"""
import sqlite3
class MemberDao:
def __init__(self, db_path):
self.conn = sqlite3.connect(db_path)
self.conn.execute(
"CREATE TABLE IF NOT EXISTS members ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE)"
)
def exists_by_email(self, email):
row = self.conn.execute("SELECT 1 FROM members WHERE email = ?", (email,)).fetchone()
return row is not None
def insert(self, name, email):
cur = self.conn.execute("INSERT INTO members (name, email) VALUES (?, ?)", (name, email))
self.conn.commit()
return cur.lastrowid
membership/business/member_service.py
"""ビジネスロジック層:会員登録のルール。データアクセス層を直接 import する"""
from membership.data.member_dao import MemberDao
class RegistrationError(Exception):
pass
class MemberService:
def __init__(self, db_path):
self.dao = MemberDao(db_path) # 下の層の具体的なクラスを自分で作る
def register(self, name, email):
if not name.strip():
raise RegistrationError("名前を入力してください")
if "@" not in email:
raise RegistrationError("メールアドレスの形式が正しくありません")
if self.dao.exists_by_email(email):
raise RegistrationError("このメールアドレスは登録済みです")
return self.dao.insert(name.strip(), email)
membership/presentation/cli.py
"""プレゼンテーション層:入力を受け取り、結果を表示する"""
from membership.business.member_service import MemberService, RegistrationError
def run(db_path, requests):
service = MemberService(db_path)
for name, email in requests:
try:
member_id = service.register(name, email)
print(f"登録しました: id={member_id} {name} <{email}>")
except RegistrationError as e:
print(f"登録できません: {e}")
main.py
from membership.presentation.cli import run
run(":memory:", [
("山田 太郎", "taro@example.com"),
("山田 次郎", "taro@example.com"),
("鈴木 花子", "hanako.example.com"),
])
実行結果
cd layered_app
python3 main.py
登録しました: id=1 山田 太郎 <taro@example.com>
登録できません: このメールアドレスは登録済みです
登録できません: メールアドレスの形式が正しくありません
ファイルは3つ、import は上から下への2本だけで、流れを追いやすい構成です。一方で、member_service.py の先頭に from membership.data.member_dao import MemberDao があるため、業務ロジックを読み込むと SQLite のモジュールも一緒に読み込まれます。
python3 -c "import sys; import membership.business.member_service; print('sqlite3' in sys.modules)"
True
例2:依存を逆にした構成で書く会員登録
同じ機能を、依存の向きを逆にして書きます。変えるのは2点です。
- 業務ロジックの側に「保存先に求める機能」をインターフェース(Python では
typing.Protocol)として宣言する。 - 具体的なクラス(SQLite での実装)は、業務ロジックが自分で作らず、外から受け取る。
ディレクトリ構成
inverted_app/
├── .importlinter
├── main.py
├── membership/
│ ├── __init__.py
│ ├── application/
│ │ ├── __init__.py
│ │ ├── ports.py
│ │ └── register_member.py
│ ├── domain/
│ │ ├── __init__.py
│ │ └── member.py
│ ├── infrastructure/
│ │ ├── __init__.py
│ │ └── sqlite_member_repository.py
│ └── presentation/
│ ├── __init__.py
│ └── cli.py
└── tests/
├── __init__.py
└── test_register_member.py
コード
membership/domain/member.py
"""ドメイン層:会員と、会員に関するルール。ほかの層を import しない"""
from dataclasses import dataclass
class RegistrationError(Exception):
pass
@dataclass(frozen=True)
class Member:
name: str
email: str
@staticmethod
def create(name, email):
if not name.strip():
raise RegistrationError("名前を入力してください")
if "@" not in email:
raise RegistrationError("メールアドレスの形式が正しくありません")
return Member(name=name.strip(), email=email)
membership/application/ports.py
"""アプリケーション層:外側に求める機能をインターフェースとして宣言する"""
from typing import Protocol
from membership.domain.member import Member
class MemberRepository(Protocol):
def exists_by_email(self, email: str) -> bool: ...
def add(self, member: Member) -> int: ...
membership/application/register_member.py
"""アプリケーション層:会員登録の手順。知っているのはインターフェースだけ"""
from membership.application.ports import MemberRepository
from membership.domain.member import Member, RegistrationError
class RegisterMember:
def __init__(self, repository: MemberRepository):
self.repository = repository # 具体的なクラスは外から受け取る
def execute(self, name, email):
member = Member.create(name, email)
if self.repository.exists_by_email(member.email):
raise RegistrationError("このメールアドレスは登録済みです")
return self.repository.add(member)
membership/infrastructure/sqlite_member_repository.py
"""インフラ層:インターフェースを SQLite で実装する。内側の層を import する"""
import sqlite3
from membership.domain.member import Member
class SqliteMemberRepository:
def __init__(self, db_path):
self.conn = sqlite3.connect(db_path)
self.conn.execute(
"CREATE TABLE IF NOT EXISTS members ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE)"
)
def exists_by_email(self, email: str) -> bool:
row = self.conn.execute("SELECT 1 FROM members WHERE email = ?", (email,)).fetchone()
return row is not None
def add(self, member: Member) -> int:
cur = self.conn.execute(
"INSERT INTO members (name, email) VALUES (?, ?)", (member.name, member.email)
)
self.conn.commit()
return cur.lastrowid
membership/presentation/cli.py
"""プレゼンテーション層:入力を受け取り、結果を表示する"""
from membership.application.register_member import RegisterMember
from membership.domain.member import RegistrationError
def run(usecase: RegisterMember, requests):
for name, email in requests:
try:
member_id = usecase.execute(name, email)
print(f"登録しました: id={member_id} {name} <{email}>")
except RegistrationError as e:
print(f"登録できません: {e}")
main.py
"""組み立て役:具体的なクラスを選んでつなぐのは、ここだけ"""
from membership.application.register_member import RegisterMember
from membership.infrastructure.sqlite_member_repository import SqliteMemberRepository
from membership.presentation.cli import run
usecase = RegisterMember(SqliteMemberRepository(":memory:"))
run(usecase, [
("山田 太郎", "taro@example.com"),
("山田 次郎", "taro@example.com"),
("鈴木 花子", "hanako.example.com"),
])
実行結果
cd inverted_app
python3 main.py
登録しました: id=1 山田 太郎 <taro@example.com>
登録できません: このメールアドレスは登録済みです
登録できません: メールアドレスの形式が正しくありません
表示は例1と同じです。違いは import に表れます。会員登録の手順を読み込んでも、SQLite のモジュールは読み込まれません。
python3 -c "import sys; import membership.application.register_member; print('sqlite3' in sys.modules)"
False
データベースなしでテストする
インターフェースを満たすテスト用のクラスを渡せば、データベースを用意せずに業務ルールをテストできます。
tests/test_register_member.py
"""データベースを使わずに、会員登録の手順をテストする"""
import sys
import unittest
from membership.application.register_member import RegisterMember
from membership.domain.member import RegistrationError
class InMemoryMemberRepository:
"""テスト用の実装。辞書に保存するだけ"""
def __init__(self):
self.members = {}
def exists_by_email(self, email):
return email in self.members
def add(self, member):
self.members[member.email] = member
return len(self.members)
class RegisterMemberTest(unittest.TestCase):
def setUp(self):
self.repository = InMemoryMemberRepository()
self.usecase = RegisterMember(self.repository)
def test_register(self):
self.assertEqual(self.usecase.execute(" 山田 太郎 ", "taro@example.com"), 1)
self.assertEqual(self.repository.members["taro@example.com"].name, "山田 太郎")
def test_duplicate_email(self):
self.usecase.execute("山田 太郎", "taro@example.com")
with self.assertRaises(RegistrationError):
self.usecase.execute("山田 次郎", "taro@example.com")
def test_invalid_email(self):
with self.assertRaises(RegistrationError):
self.usecase.execute("鈴木 花子", "hanako.example.com")
def test_sqlite3_is_not_imported(self):
self.assertNotIn("sqlite3", sys.modules)
if __name__ == "__main__":
unittest.main()
python3 -m unittest -v tests.test_register_member
test_duplicate_email (tests.test_register_member.RegisterMemberTest.test_duplicate_email) ... ok
test_invalid_email (tests.test_register_member.RegisterMemberTest.test_invalid_email) ... ok
test_register (tests.test_register_member.RegisterMemberTest.test_register) ... ok
test_sqlite3_is_not_imported (tests.test_register_member.RegisterMemberTest.test_sqlite3_is_not_imported) ... ok
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OK
2つの構成を import 文で比べる
| ファイルの役割 | 3層レイヤードの import | 依存を逆にした構成の import |
|---|---|---|
| 業務ロジック | from membership.data.member_dao import MemberDao |
from membership.application.ports import MemberRepository(インターフェース) |
| データの保存 | import sqlite3 のみ |
import sqlite3 と from membership.domain.member import Member |
| 具体的なクラスを作る場所 | 業務ロジックの __init__ の中 |
main.py(組み立て役)だけ |
ファイル数(__init__.py を除く) |
4 | 6(テストを含めて 7) |
依存を逆にすると、保存の実装が業務の側の型(Member)に合わせる形になります。そのぶんファイルは増えます。
層の向きに反する import を検出する設定例
構成は、決めただけでは時間とともに崩れやすいものです。Python では import-linter で、層の向きを設定ファイルに書いて検査できます。
pip install import-linter
3層レイヤードの設定
プロジェクトの直下に .importlinter を置きます。layers は上の層から順に書きます。上の層は下の層を import できますが、逆はできません。
[importlinter]
root_package = membership
[importlinter:contract:layers]
name = 3層は上から下へだけ依存する
type = layers
layers =
membership.presentation
membership.business
membership.data
lint-imports
---------
Contracts
---------
Analyzed 7 files, 2 dependencies.
---------------------------------
3層は上から下へだけ依存する KEPT
Contracts: 1 kept, 0 broken.
(冒頭に出るロゴの表示は省いています。以降も同じです。)
ここで、データアクセス層に次のファイルを足してみます。上の層の例外クラスを import しています。
membership/data/member_exporter.py
"""データアクセス層から、上のビジネスロジック層を import している(向きに反する例)"""
from membership.business.member_service import RegistrationError
def export_members(dao):
rows = dao.conn.execute("SELECT id, name, email FROM members").fetchall()
if not rows:
raise RegistrationError("会員が1件もありません")
return rows
---------
Contracts
---------
Analyzed 8 files, 3 dependencies.
---------------------------------
3層は上から下へだけ依存する BROKEN
Contracts: 0 kept, 1 broken.
----------------
Broken contracts
----------------
3層は上から下へだけ依存する
--------------
membership.data is not allowed to import membership.business:
- membership.data.member_exporter -> membership.business.member_service (l.2)
どのファイルの何行目が向きに反しているかが表示され、終了コードは 1 になります。CI(自動チェック)に組み込めば、マージ前に気づけます。
依存を逆にした構成の設定
同じ高さに並ぶ層は | で区切って1行に書きます。この書き方では、並べた層どうしの import も検査の対象になります。さらに、内側の層が sqlite3 を import しないことを forbidden で指定します。外部パッケージを検査に含めるには include_external_packages = True が必要です。
[importlinter]
root_package = membership
include_external_packages = True
[importlinter:contract:layers]
name = 依存は外側から内側へだけ向かう
type = layers
layers =
membership.presentation | membership.infrastructure
membership.application
membership.domain
[importlinter:contract:no-db-inside]
name = 内側の層は sqlite3 を import しない
type = forbidden
source_modules =
membership.domain
membership.application
forbidden_modules =
sqlite3
---------
Contracts
---------
Analyzed 13 files, 9 dependencies.
----------------------------------
依存は外側から内側へだけ向かう KEPT
内側の層は sqlite3 を import しない KEPT
Contracts: 2 kept, 0 broken.
register_member.py に from membership.infrastructure.sqlite_member_repository import SqliteMemberRepository を足すと、2つの決まりの両方で検出されます。
---------
Contracts
---------
Analyzed 13 files, 10 dependencies.
-----------------------------------
依存は外側から内側へだけ向かう BROKEN
内側の層は sqlite3 を import しない BROKEN
Contracts: 0 kept, 2 broken.
----------------
Broken contracts
----------------
依存は外側から内側へだけ向かう
---------------
membership.application is not allowed to import membership.infrastructure:
- membership.application.register_member ->
membership.infrastructure.sqlite_member_repository (l.3)
内側の層は sqlite3 を import しない
--------------------------
membership.application is not allowed to import sqlite3:
- membership.application.register_member ->
membership.infrastructure.sqlite_member_repository (l.3)
membership.infrastructure.sqlite_member_repository -> sqlite3 (l.2)
2つ目の検出では、sqlite3 に届くまでの import の経路が1段ずつ表示されます。直接 import していなくても、間接的にたどり着けば検出されるということです。
pyproject.toml に書く場合は、公式ドキュメントに次の形の例があります(mypackage の部分を自分のパッケージ名に変えます)。
[[tool.importlinter.contracts]]
name = "My layers contract"
type = "layers"
layers = [
"mypackage.high",
"mypackage.medium",
"mypackage.low",
]
どちらを選ぶか:判断表
| 条件 | 選ぶもの |
|---|---|
| 小規模で、画面とテーブルがほぼ1対1の CRUD 中心 | 3層レイヤード |
| 業務ルールが少なく、入力チェックと保存が主な処理 | 3層レイヤード |
| 業務ルールが多く、条件の組み合わせをテストで確かめたい | 依存を逆にする |
| テストでデータベースや外部 API を切り離したい | 依存を逆にする |
| 保存先や外部サービスを入れ替える予定がある | 依存を逆にする |
| 同じ業務ロジックを、画面・バッチ・API の複数の入口から使う | 依存を逆にする(入口ごとにプレゼンテーション層を足す) |
| 初めは小さく、成長に合わせて変えたい | 3層で始め、業務ルールが増えた機能から順にインターフェースを挟む |
3層から依存を逆にした構成へ移す手順
今回の2つの例の差分が、そのまま移行の手順になります。
| 手順 | 行うこと | 例での対応 |
|---|---|---|
| 1 | 業務ロジックが呼んでいる保存用のメソッドを書き出し、インターフェースにする | ports.py の MemberRepository |
| 2 | 業務ロジックの中で具体的なクラスを作るのをやめ、引数で受け取る | RegisterMember.__init__(self, repository) |
| 3 | 入力チェックなど、保存を伴わないルールをドメイン層に移す | Member.create |
| 4 | 具体的なクラスを選んでつなぐ場所を1か所に集める | main.py |
| 5 | 向きの検査を設定に加える | .importlinter |
レイヤード・オニオン・ヘキサゴナル・クリーンの違い
| 観点 | レイヤード(3層) | オニオン | ヘキサゴナル | クリーン |
|---|---|---|---|---|
| 図の形 | 上下に積んだ層 | 同心円 | 六角形と、その外側のアダプター | 同心円 |
| 中心・最下部にあるもの | データアクセス(最下部) | ドメインモデル(中心) | アプリケーション(内側) | Entities(中心) |
| 強調していること | 役割ごとに層を分ける | すべてのコードは中心側の層に依存でき、外側には依存できない | 利用者・プログラム・自動テスト・バッチのどれからでも同じように動かせ、実行時の機器やデータベースから切り離して開発・テストできる | 依存は内側にだけ向かう(依存性のルール) |
| 外部との接点の呼び方 | データアクセス層 | インフラ(外側) | ポートとアダプター(動かす側と動かされる側の2種類) | Interface Adapters と Frameworks and Drivers |
| 今回の例との対応 | 例1 | 例2 | 例2(ports.py がポート、SQLite の実装と CLI がアダプター) |
例2 |
クリーンアーキテクチャの記事は、ヘキサゴナルやオニオンなどの方式は細部に違いがあるものの、目指すところは同じだと述べています。また、円の数は4つに決まっているわけではなく、いくつに分けても依存性のルールは常に当てはまる、とも説明しています。名前の違いよりも、「内側が外側を知らない」という1点を守れているかを確かめるのが近道です。
確かめて分かったこと
- 3層レイヤードでは、業務ロジックを import しただけで sqlite3 が読み込まれました。 依存を逆にした構成では読み込まれず、テストの中でもそれを確認できました。
- 画面の表示は2つの構成でまったく同じでした。 違いが表れるのは import 文と、具体的なクラスを作る場所です。
- ファイル数は 4 から 6 に増えました(
__init__.pyとテストを除く)。小さな機能では、この増加が負担になるかどうかが判断の材料になります。 - import-linter の layers は、層を飛び越す import(プレゼンテーション層からデータアクセス層へ)を検出しませんでした。 上から下への向きだけを見るためです。飛び越しも止めたい場合は、
forbiddenの決まりを別に足します。 - forbidden の検査は間接的な import もたどりました。 application から infrastructure を経由して sqlite3 に届く経路が、2行で表示されました。
- Python の Protocol は、実装クラスが継承を書かなくても使えました。
SqliteMemberRepositoryもテスト用のInMemoryMemberRepositoryもMemberRepositoryを継承していませんが、同じメソッドを持っていればそのまま渡せます。
よくある質問
Q. レイヤードアーキテクチャとは何ですか? A. プログラムを役割ごとの層に分け、層の間の依存を一方向にそろえる構成です。プレゼンテーション・ビジネスロジック・データアクセスの3層がよく使われます。
Q. レイヤードアーキテクチャとクリーンアーキテクチャの違いは? A. 依存の向きです。従来のレイヤードは業務ロジックがデータアクセス層に依存し、クリーンアーキテクチャはインターフェースを挟んで、データアクセスの側が業務ロジックに依存します。
Q. オニオンアーキテクチャとクリーンアーキテクチャの違いは? A. 層の名前と分け方が異なりますが、依存を内側へ向けるという決まりは同じです。コードの構成としては、この記事の例2がどちらにも当てはまります。
Q. 3層アーキテクチャのディレクトリ構成はどう分けますか? A. 層ごとにディレクトリを作り、presentation、business、data のように役割の名前を付けます。層の順序を設定ファイルに書き、検査ツールで向きを確かめると構成が保たれます。
Q. 小さなシステムでもクリーンアーキテクチャにしたほうがよいですか? A. CRUD 中心の小規模なシステムは3層で足ります。業務ルールが増えてきた機能から、インターフェースを挟む形に移していけます。
まとめ
- レイヤードアーキテクチャは、役割ごとに層を分け、依存を一方向にそろえる構成です。
- 4つの方式の違いは層の数ではなく依存の向きにあります。
- 従来の3層は業務ロジックがデータアクセス層に依存し、オニオン・ヘキサゴナル・クリーンはインターフェースで向きを逆にします。
- 依存を逆にすると、データベースなしで業務ルールをテストできます。代わりにファイル数は増えます。
- 小規模な CRUD 中心なら3層、業務ルールが多くテストで DB を切り離したいなら依存を逆にします。
- 層の向きは import-linter などのツールで検査し、自動チェックに組み込みます。
参考・出典
- Robert C. Martin「The Clean Architecture」(2012-08-13): https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
- Jeffrey Palermo「The Onion Architecture : part 1」(2008-07-29): https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/
- Alistair Cockburn「Hexagonal architecture」: https://alistair.cockburn.us/hexagonal-architecture/
- Import Linter Documentation「Layers」: https://import-linter.readthedocs.io/en/stable/contract_types/layers/
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「レイヤードアーキテクチャとは:依存関係とディレクトリ構成例・クリーンとの違い」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。