Skip to content

backtony/hexagonal-multi-project

Repository files navigation

kafka μ‹€ν–‰ ν›„ ν•΄λ‹Ή ν”„λ‘œμ νŠΈ 기동 κ°€λŠ₯

docker run --name kafka -p 9092:9092 apache/kafka:3.7.1

Hexagonal μ•„ν‚€ν…μ²˜

κ·Έλ¦Ό0

ν—₯사고날 μ•„ν‚€ν…μ²˜(Hexagonal Architecture)λŠ” ν¬νŠΈμ™€ μ–΄λŒ‘ν„° μ•„ν‚€ν…μ²˜(Ports and Adapters Architecture)라고도 λΆˆλ¦¬λŠ” μ†Œν”„νŠΈμ›¨μ–΄ μ•„ν‚€ν…μ²˜ 쀑 ν•˜λ‚˜λ‘œ μ£Όμš” λͺ©ν‘œλŠ” μ‘μš© ν”„λ‘œκ·Έλž¨μ˜ λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ μ™ΈλΆ€ μ„Έκ³„λ‘œλΆ€ν„° κ²©λ¦¬μ‹œμΌœ μœ μ—°ν•˜κ³  ν…ŒμŠ€νŠΈν•˜κΈ° μ‰¬μš΄ ꡬ쑰λ₯Ό λ§Œλ“œλŠ” κ²ƒμž…λ‹ˆλ‹€. 이λ₯Ό μœ„ν•΄ 핡심 λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ€ μ€‘μ•™μ˜ 도메인 μ˜μ—­μ— μœ„μΉ˜ν•˜λ©°, μž…λ ₯κ³Ό 좜λ ₯을 μ²˜λ¦¬ν•˜λŠ” ν¬νŠΈμ™€ μ–΄λŒ‘ν„°λ₯Ό 톡해 외뢀와 μ†Œν†΅ν•©λ‹ˆλ‹€.

  • Adapter : λͺ¨λ“  μ™ΈλΆ€ μ‹œμŠ€ν…œκ³Όμ˜ 직접적인 μƒν˜Έμž‘μš©μ„ λ‹΄λ‹Ή
    • ex) Controller, Kafka Listener, DB DAO
  • Inbound & Outbound port : 각 μ„œλΉ„μŠ€ λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ— 맞게 μ •μ˜λœ μΈν„°νŽ˜μ΄μŠ€

μΈλ°”μš΄λ“œ μ–΄λŒ‘μ²˜ -> μΈλ°”μš΄λ“œ 포트 -> λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 -> μ•„μ›ƒλ°”μš΄λ“œ 포트 -> μ•„μ›ƒλ°”μš΄λ“œ μ–΄λŒ‘ν„°

λ‚΄λΆ€μ˜ λ‘œμ§μ€ 였직 μ™ΈλΆ€λ₯Ό ν†΅ν•΄μ„œλ§Œ 접근이 κ°€λŠ₯ν•œ μ»¨μ…‰μœΌλ‘œ μ™ΈλΆ€ μ„œλΉ„μŠ€μ™€μ˜ μƒν˜Έ μž‘μš©μ„ λ‹΄λ‹Ήν•˜λŠ” AdapterλŠ” λΉ„μ¦ˆλ‹ˆμŠ€ 둜직과의 μž‘μ—…μ„ μ •μ˜ν•œ 포트(μΈν„°νŽ˜μ΄μŠ€)λž‘λ§Œ μ„œλ‘œ ν†΅μ‹ ν•©λ‹ˆλ‹€.

Layered μ•„ν‚€ν…μ²˜μ—μ„œ Hexagonal μ•„ν‚€ν…μ²˜λ‘œ

κ·Έλ¦Ό1

λ ˆμ΄μ–΄λ“œ μ•„ν‚€ν…μ²˜λŠ” λΉ„μ¦ˆλ‹ˆμŠ€ λ ˆμ΄μ–΄κ°€ 인프라 λ ˆμ΄μ–΄μ— μ˜μ‘΄ν•˜μ—¬ κ°•κ²°ν•©λ˜λŠ” ꡬ쑰λ₯Ό 가지고 μžˆμŠ΅λ‹ˆλ‹€. JPA둜 예λ₯Ό λ“€λ©΄, PersistenceInterfaceλŠ” JPA interfaceκ°€ 되고, 이에 λŒ€ν•œ Adapterλ‘œλŠ” SimpleJpaRepositoryκ°€ 될 μˆ˜λ„ 있고 QueryDsl을 μ‚¬μš©ν•œλ‹€λ©΄ 좔가적인 customImpl이 Adapterκ°€ 될 수 μžˆμŠ΅λ‹ˆλ‹€.

μ˜μ‘΄μ„± μ—­μ „ λΉ„μ¦ˆλ‹ˆμŠ€ λ ˆμ΄μ–΄κ°€ 인프라 λ ˆμ΄μ–΄μ— μ˜μ‘΄ν•˜κ²Œ λ˜λ©΄μ„œ 인프라 λ ˆμ΄μ–΄μ˜ μΈν„°νŽ˜μ΄μŠ€κ°€ λ³€κ²½λ˜λ©΄ λΉ„μ¦ˆλ‹ˆμŠ€ λ ˆμ΄μ–΄λ„ ν•¨κ»˜ λ³€κ²½λ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€. 즉, 인프라 λ ˆμ΄μ–΄ λ‚΄μ˜ μ½”λ“œ 변경에 μ˜ν•΄ λΉ„μ¦ˆλ‹ˆμŠ€ λ ˆμ΄μ–΄λ„ ν•¨κ»˜ μ˜€μ—Όλ©λ‹ˆλ‹€. ν—₯사고날 μ•„ν‚€ν…μ²˜μ—μ„œλŠ” λ‹€μŒκ³Ό 같이 μ˜μ‘΄μ„±μ„ μ—­μ „ν•©λ‹ˆλ‹€.

κ·Έλ¦Ό2

μ˜μ‘΄μ„± μ—­μ „ 원칙을 μ•„μ›ƒκ³ μž‰ μ–΄λŒ‘ν„°μ— μ μš©ν•˜μ—¬ 핡심 도메인 뢀뢄은 λ‹€λ₯Έ λ ˆμ΄μ–΄μ— μ˜μ‘΄ν•˜μ§€ μ•Šκ²Œ λ˜μ–΄ λ…μžμ μœΌλ‘œ 개발 및 배포가 κ°€λŠ₯ν•΄μ‘ŒμŠ΅λ‹ˆλ‹€. 덕뢄에 ν…ŒμŠ€νŠΈ μš©μ΄μ„± λ˜ν•œ ν™•λ³΄λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

μΈν„°νŽ˜μ΄μŠ€ 뢄리 보톡 λΉ„μ¦ˆλ‹ˆμŠ€ λ ˆμ΄μ–΄μ˜ μ„œλΉ„μŠ€λŠ” ν•˜λ‚˜ μ΄μƒμ˜ μœ μ¦ˆμΌ€μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. 즉, UI λ ˆμ΄μ–΄μ—μ„œ λΉ„μ¦ˆλ‹ˆμŠ€ λ ˆμ΄μ–΄μ˜ μ„œλΉ„μŠ€λ₯Ό ν˜ΈμΆœν•  λ•Œ, μ–΄λ–€ μœ μ¦ˆμΌ€μ΄μŠ€λ₯Ό μ‚¬μš©ν•΄μ•Όν• μ§€ λͺ…ν™•ν•˜μ§€ μ•Šμ€ κ²½μš°κ°€ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€. λ˜ν•œ, μœ„ 그림의 appServiceλ₯Ό κ·ΈλŒ€λ‘œ μ‚¬μš©ν•˜κ²Œ 되면 μ—¬λŸ¬ μœ μ¦ˆμΌ€μ΄μŠ€λ“€μ˜ λΆˆν•„μš”ν•œ 뢀뢄듀을 λͺ¨λ‘ μ˜μ‘΄ν•˜λŠ” 상황이 λ°œμƒν•©λ‹ˆλ‹€. ν—₯사고날 μ•„ν‚€ν…μ²˜μ—μ„œλŠ” λ‹€μŒκ³Ό 같이 μΈν„°νŽ˜μ΄μŠ€λ₯Ό λΆ„λ¦¬ν•©λ‹ˆλ‹€.

κ·Έλ¦Ό3

μΈν„°νŽ˜μ΄μŠ€λ₯Ό λΆ„λ¦¬ν•˜κ³  이λ₯Ό appServiceκ°€ κ΅¬ν˜„ν•˜κ²Œ ν•˜κ³  UI λ ˆμ΄μ–΄μ—μ„œλŠ” μ μ ˆν•œ 인컀밍 포트λ₯Ό μ‚¬μš©ν•˜κ²Œ λ©λ‹ˆλ‹€. 이λ₯Ό 톡해 ν•΄λ‹Ή κΈ°λŠ₯κ³Ό κ΄€λ ¨ μ—†λŠ” λ‹€λ₯Έ 뢀뢄에 μ˜μ‘΄ν•˜μ§€ μ•Šκ²Œ λ©λ‹ˆλ‹€.

λ ˆμ΄μ–΄λ“œ μ•„ν‚€ν…μ²˜λ₯Ό ν—₯사고날 μ•„ν‚€ν…μ²˜λ‘œ λ³€ν™˜ν•˜λŠ” 과정을 보면 λ‹€μŒκ³Ό 같은 μž₯점을 λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€.

  • μœ μ§€λ³΄μˆ˜μ„± : μ±…μž„μ΄ λΆ„λ¦¬λ˜μ–΄ μžˆμ–΄ μ½”λ“œ 이해와 μˆ˜μ •μ΄ μš©μ΄ν•©λ‹ˆλ‹€.
  • μœ μ—°μ„± : ν¬νŠΈμ™€ μ–΄λŒ‘ν„°λ₯Ό μ‚¬μš©ν•¨μœΌλ‘œμ¨, λ‹€μ–‘ν•œ 변화에 λŒ€ν•΄ μœ μ—°ν•˜κ²Œ λŒ€μ²˜ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • ν…ŒμŠ€νŠΈ μš©μ΄μ„± : 각 μ»΄ν¬λ„ŒνŠΈλ₯Ό λ…λ¦½μ μœΌλ‘œ μ™ΈλΆ€ μ˜μ‘΄μ„± 없이 ν…ŒμŠ€νŠΈν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • ꡬ쑰적으둜 SOLID 원칙을 λ”μš± μ‰½κ²Œ 적용 κ°€λŠ₯ν•©λ‹ˆλ‹€.

ν—₯사고날 μ•„ν‚€ν…μ²˜κ°€ λͺ¨λ“  상황에 μ ν•©ν•œ 것은 μ•„λ‹™λ‹ˆλ‹€. ν—₯사고날 μ•„ν‚€ν…μ²˜μ˜ 경우 κΈ°μ‘΄ λ ˆμ΄μ–΄λ“œ μ•„ν‚€ν…μ²˜μ— λΉ„ν•΄ μ½”λ“œλŸ‰μ΄ μƒλ‹Ήνžˆ μ¦κ°€ν•˜λ©° 처음 개발 이후 큰 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직의 λ³€ν™”κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν”„λ‘œμ νŠΈμ˜ 경우 였히렀 λ ˆμ΄μ–΄λ“œ μ•„ν‚€ν…μ²˜κ°€ λ”μš± μ•ˆμ •μ μΌ 수 μžˆμŠ΅λ‹ˆλ‹€. ν—₯사고날 μ•„ν‚€ν…μ²˜λŠ” 보톡 λΉ λ₯Έ ν™•μž₯μ„±κ³Ό μœ μ—°μ„±μ΄ ν•„μ—°μ μœΌλ‘œ ν•„μš”ν•œ MSA ν™˜κ²½μ—μ„œ μ μ ˆν•œ μ•„ν‚€ν…μ²˜λΌκ³  ν‘œν˜„ν•˜λŠ” κ²½μš°λ„ μžˆμœΌλ‹ˆ μ μ ˆν•œ 상황에 맞게 μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€.

λ©€ν‹° ν”„λ‘œμ νŠΈλ‘œ Hexagonal μ•„ν‚€ν…μ²˜ κ΅¬μΆ•ν•˜κΈ°

ν•˜λ‚˜μ˜ λ ˆν¬μ§€ν† λ¦¬λ‘œ μ•ˆμ— μ—¬λŸ¬ 개의 ν”„λ‘œμ νŠΈλ₯Ό λ§Œλ“€κ³  각각의 ν”„λ‘œμ νŠΈλŠ” λ©€ν‹° λͺ¨λ“ˆλ‘œ κ΅¬μ„±ν•˜λŠ” 데λͺ¨λ₯Ό λ§Œλ“€μ–΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

사전 지식

λ©€ν‹° ν”„λ‘œμ νŠΈ λ©€ν‹° λͺ¨λ“ˆμ„ κ΅¬μΆ•ν•˜κΈ° μœ„ν•΄μ„œλŠ” 사전 지식이 ν•„μš”ν•©λ‹ˆλ‹€.

implementationκ³Ό api

  • implementation : implementationλ₯Ό μ‚¬μš©ν•˜λ©΄ ν•΄λ‹Ή 쒅속성은 ν˜„μž¬ λͺ¨λ“ˆ λ‚΄λΆ€μ—μ„œλ§Œ μ ‘κ·Όν•  수 μžˆμŠ΅λ‹ˆλ‹€. 즉, 이 λͺ¨λ“ˆμ„ μ˜μ‘΄ν•˜λŠ” λ‹€λ₯Έ λͺ¨λ“ˆμ—μ„œλŠ” implementation으둜 μΆ”κ°€λœ 쒅속성에 직접 μ ‘κ·Όν•  수 μ—†μŠ΅λ‹ˆλ‹€.
  • api : ν˜„μž¬ λͺ¨λ“ˆκ³Ό 이 λͺ¨λ“ˆμ„ μ˜μ‘΄ν•˜λŠ” λ‹€λ₯Έ λͺ¨λ“ˆμ—μ„œλ„ μ ‘κ·Όν•  수 μžˆλ„λ‘ ν•©λ‹ˆλ‹€. 즉, ν˜„μž¬ λͺ¨λ“ˆμ΄ api둜 쒅속성을 μΆ”κ°€ν•˜λ©΄, 이 λͺ¨λ“ˆμ„ μ‚¬μš©ν•˜λŠ” λͺ¨λ“  λ‹€λ₯Έ λͺ¨λ“ˆλ„ ν•΄λ‹Ή 쒅속성을 직접 μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
// A Module
public class A

// B Module
implementation project(':A')

// C Module
implementation project(':B')

public class C {
  public void act() {
    new A() // compile error
  }
}

A λͺ¨λ“ˆμ—μ„œ AλΌλŠ” 클래슀λ₯Ό μ œκ³΅ν•œλ‹€κ³  ν–ˆμ„λ•Œ, B λͺ¨λ“ˆμ—μ„œ Aλͺ¨λ“ˆμ„ implementation으둜 μ˜μ‘΄μ„±μ„ κ°€μ Έμ˜¨λ‹€λ©΄ Bλͺ¨λ“ˆμ—μ„œλŠ” A 클래슀λ₯Ό μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 이 μƒνƒœμ—μ„œ C λͺ¨λ“ˆμ—μ„œ B λͺ¨λ“ˆμ„ implementation으둜 μ˜μ‘΄μ„±μœΌλ‘œ κ°€μ Έμ˜¨λ‹€λ©΄ Cλͺ¨λ“ˆμ—μ„œλŠ” A클래슀λ₯Ό μ‚¬μš©ν•  수 μ—†μŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ Bλͺ¨λ“ˆμ—μ„œ implementation이 μ•„λ‹Œ apiλ₯Ό μ‚¬μš©ν•΄μ„œ Aλͺ¨λ“ˆμ˜ μ˜μ‘΄μ„±μ„ κ°€μ Έμ™”λ‹€λ©΄ Cλͺ¨λ“ˆμ—μ„œλ„ Aλͺ¨λ“ˆμ—μ„œ μ œκ³΅ν•˜λŠ” κΈ°λŠ₯을 μ‚¬μš©ν•  수 μžˆμœΌλ―€λ‘œ A 클래슀λ₯Ό μ‚¬μš©ν•  수 있게 λ©λ‹ˆλ‹€.

include와 includeBuild

https://docs.gradle.org/current/userguide/composite_builds.html

  • include : 일반적으둜 ν•˜λ‚˜μ˜ 루트 ν”„λ‘œμ νŠΈμ™€ μ—¬λŸ¬ μ„œλΈŒ ν”„λ‘œμ νŠΈλ‘œ κ΅¬μ„±λœ κ΅¬μ‘°μ—μ„œ μ‚¬μš©λ©λ‹ˆλ‹€. includeλ₯Ό μ‚¬μš©ν•˜λ©΄ μ„œλΈŒ ν”„λ‘œμ νŠΈλ“€μ„ ν•˜λ‚˜μ˜ μ„€μ •μœΌλ‘œ κ²°ν•©ν•˜κ³ , 이듀 사이에 μ˜μ‘΄μ„±μ„ 관리할 수 μžˆμŠ΅λ‹ˆλ‹€.
  • includeBuild : λ‹€λ₯Έλ₯Έ 독립적인 λΉŒλ“œλ₯Ό ν¬ν•¨ν•˜λŠ” 데 μ‚¬μš©λ©λ‹ˆλ‹€. 보톡 μ—¬λŸ¬ 개의 λ…λ¦½μ μœΌλ‘œ λΉŒλ“œ κ°€λŠ₯ν•œ ν”„λ‘œμ νŠΈλ₯Ό ν•˜λ‚˜μ˜ λΉŒλ“œμ— ν¬ν•¨ν•˜κ³ μž ν•  λ•Œ μ‚¬μš©ν•©λ‹ˆλ‹€.

λ©€ν‹° λͺ¨λ“ˆμ˜ 경우 보톡 include만 μ‚¬μš©ν•˜μ§€λ§Œ λ©€ν‹° ν”„λ‘œμ νŠΈλ₯Ό λ§Œλ“œλŠ” 경우, 타 ν”„λ‘œμ νŠΈμ˜ λͺ¨λ“ˆμ„ κ°€μ Έμ˜€κΈ° μœ„ν•΄ includeBuildλ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.

β”œβ”€β”€ common-library
β”‚ β”œβ”€β”€ json
β”‚ β”œβ”€β”€ settings.gradle.kts
β”‚ └── build.gradle.kts
β”œβ”€β”€ sample-service
β”‚ β”œβ”€β”€ application
β”‚ β”œβ”€β”€ build.gradle.kts
β”‚ └── settings.gradle.kts
β”œβ”€β”€ build.gradle.kts
└── settings.gradle.kts

μœ„μ™€ 같은 ꡬ쑰의 sample-service, common-library ν”„λ‘œμ νŠΈκ°€ μžˆμ„ λ•Œ, common-library ν”„λ‘œμ νŠΈμ˜ json λͺ¨λ“ˆμ„ sample-service ν”„λ‘œμ νŠΈμ˜ application λͺ¨λ“ˆμ—μ„œ μ‚¬μš©ν•˜κΈ° μœ„ν•΄μ„œ includeBuildλ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.

// root ν”„λ‘œμ νŠΈμ˜ settings.gradle.kts
includeBuild("common-library")
includeBuild("sample-service")

// sample-service의 settings.gradle.kts
rootProject.name = "sample-service"

includeBuild("../common-library")

// sample-service의 application λͺ¨λ“ˆμ˜ build.gradle.kts
dependencies {
  // νŒ¨ν‚€μ§€λͺ…:λͺ¨λ“ˆλͺ…
  implementation("com.sample.hexagonal.common:json")
}

μœ„μ™€ 같은 ꡬ쑰둜 μ˜μ‘΄μ„±μ„ λ°›μ•„μ˜¬ 수 μžˆμŠ΅λ‹ˆλ‹€.

ν”„λ‘œμ νŠΈ ꡬ쑰

β”œβ”€β”€ build-plugin
β”œβ”€β”€ common-library
β”‚ β”œβ”€β”€ exception
β”‚ β”œβ”€β”€ json
β”‚ └── utils
β”œβ”€β”€ sample-service
β”‚ β”œβ”€β”€ adapter
β”‚ β”‚ β”œβ”€β”€ inbound
β”‚ β”‚ β”‚ β”œβ”€β”€ controller
β”‚ β”‚ β”‚ └── listener
β”‚ β”‚ └── outbound
β”‚ β”‚     β”œβ”€β”€ producer
β”‚ β”‚     └── repository
β”‚ β”œβ”€β”€ application
β”‚ β”œβ”€β”€ domain
β”‚ β”œβ”€β”€ infrastructure
β”‚ β”‚ β”œβ”€β”€ h2
β”‚ β”‚ └── mongo
β”‚ β”œβ”€β”€ server
β”‚ β”‚ β”œβ”€β”€ api
β”‚ β”‚ └── consumer
  • build-plugin : μ—¬λŸ¬ λͺ¨λ“ˆμ—μ„œ μ‚¬μš©ν•  build-plugin을 κ΄€λ¦¬ν•˜λŠ” ν”„λ‘œμ νŠΈ
  • common-library : μ—¬λŸ¬ λͺ¨λ“ˆμ—μ„œ 곡용으둜 μ‚¬μš©ν•  libraryλ₯Ό κ΄€λ¦¬ν•˜λŠ” ν”„λ‘œμ νŠΈ
  • sample-service : μ„œλΉ„μŠ€ ν”„λ‘œμ νŠΈ
    • adapter.inbound : μ™ΈλΆ€ μ‹œμŠ€ν…œκ³Όμ˜ μƒν˜Έμž‘μš©μ„ λ‹΄λ‹Ήν•˜λŠ” Adapter λͺ¨λ“ˆλ‘œ μ™ΈλΆ€μ—μ„œ λ‚΄λΆ€λ₯Ό ν˜ΈμΆœν•˜λŠ” μ—­ν•  (controller, kafka-listener λͺ¨λ“ˆ)
    • adapter.outbound : μ™ΈλΆ€ μ‹œμŠ€ν…œκ³Όμ˜ μƒν˜Έμž‘μš©μ„ λ‹΄λ‹Ήν•˜λŠ” Adapter λͺ¨λ“ˆλ‘œ λ‚΄λΆ€μ—μ„œ μ™ΈλΆ€λ₯Ό ν˜ΈμΆœν•˜λŠ” μ—­ν•  (repository, kafka-producer λͺ¨λ“ˆ)
    • application : λΉ„μ¦ˆλ‹ˆμŠ€ λͺ¨λ“ˆ
    • domain : 도메인 λͺ¨λ“ˆ
    • infrastructure : outbound λͺ¨λ“ˆμ—μ„œ μ‚¬μš©ν•˜λŠ” μ™ΈλΆ€ 인프라 (h2, mongo λͺ¨λ“ˆ)
    • server : μ„œλ²„λ₯Ό λ„μš°κΈ° μœ„ν•œ (api, consumer λͺ¨λ“ˆ)

μ΅œμƒμœ„ settings.gradle.kts

includeBuild("build-plugin")
includeBuild("common-library")
includeBuild("sample-service")

μ΅œμƒμœ„ settings.gradle.ktsμ—λŠ” composite buildλ₯Ό μœ„ν•΄ includeBuildλ₯Ό μ‚¬μš©ν•˜μ—¬ 각각의 ν”„λ‘œμ νŠΈλ₯Ό λ“±λ‘ν•΄μ€λ‹ˆλ‹€.

build-plugin ν”„λ‘œμ νŠΈ

build-plugin ν”„λ‘œμ νŠΈλŠ” 타 ν”„λ‘œμ νŠΈμ˜ λͺ¨λ“ˆμ—μ„œ μ‚¬μš©ν•  곡톡적인 μ˜μ‘΄μ„±μ„ κ΄€λ¦¬ν•˜κΈ° μœ„ν•œ λͺ©μ μ˜ ν”„λ‘œμ νŠΈ μž…λ‹ˆλ‹€. κ³΅μ‹λ¬Έμ„œμ—μ„œ κ°€μ΄λ“œν•˜λŠ” Precompiled script plugin 방식을 μ‚¬μš©ν•˜μ—¬ μ—¬λŸ¬ ν”„λ‘œμ νŠΈμ—μ„œ 곡용으둜 μ‚¬μš©ν•  build-plugin을 λ§Œλ“€κ² μŠ΅λ‹ˆλ‹€.

β”œβ”€β”€ build.gradle.kts
β”œβ”€β”€ settings.gradle.kts
└── src
    └── main
        └── kotlin
            β”œβ”€β”€ sample-kotlin-jvm.gradle.kts
            └── sample-springboot.gradle.kts

build-plugin ν”„λ‘œμ νŠΈμ˜ treeκ΅¬μ‘°λŠ” μœ„μ™€ κ°™μŠ΅λ‹ˆλ‹€.


sample-kotlin-jvm.gradle.kts ν•΄λ‹Ή νŒŒμΌμ€ spring이 μ•„λ‹Œ λ‹¨μˆœ kotlinλ§Œμ„ μ‚¬μš©ν•˜λŠ” λͺ¨λ“ˆμ„ μœ„ν•œ ν”ŒλŸ¬κ·ΈμΈ νŒŒμΌμž…λ‹ˆλ‹€.

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jlleitschuh.gradle.ktlint.reporter.ReporterType

plugins {
    id("org.jlleitschuh.gradle.ktlint")
    id("org.jetbrains.kotlinx.kover")
    id("java-library")

    kotlin("jvm")
    kotlin("kapt")
}

repositories {
    mavenCentral()
}

java {
    sourceCompatibility = JavaVersion.VERSION_17
}

dependencies {
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")

    implementation("io.github.microutils:kotlin-logging-jvm:3.0.5")

    testImplementation("io.kotest:kotest-runner-junit5-jvm:5.8.0")
    testImplementation("io.kotest:kotest-assertions-core-jvm:5.8.0")
    testImplementation("io.kotest:kotest-framework-datatest:5.8.0")
    testImplementation("io.mockk:mockk:1.13.8")
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "17"
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

sample-springboot.gradle.kts ν•΄λ‹Ή νŒŒμΌμ€ spring을 μ‚¬μš©ν•˜λŠ” λͺ¨λ“ˆμ„ μœ„ν•œ ν”ŒλŸ¬κ·ΈμΈ νŒŒμΌμž…λ‹ˆλ‹€.

plugins {
    id("sample-kotlin-jvm") // μ•žμ„œ μž‘μ„±ν•œ sample-kotlin-jvm.gradle.kts νŒŒμΌμ„ ν”ŒλŸ¬κ·ΈμΈμœΌλ‘œ μ‚¬μš©ν•©λ‹ˆλ‹€.
    id("org.springframework.boot")
    id("io.spring.dependency-management")
    kotlin("plugin.spring")
}

dependencies {
    kapt("org.springframework.boot:spring-boot-configuration-processor")

    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("io.kotest.extensions:kotest-extensions-spring:1.1.3")
    testImplementation("com.ninja-squad:springmockk:4.0.2")
}

μ•žμ„œ λ§Œλ“€μ—ˆλ˜ sample-kotlin-jvm.gradle.kts νŒŒμΌμ„ ν”ŒλŸ¬κ·ΈμΈμœΌλ‘œ μ‚¬μš©ν–ˆμŠ΅λ‹ˆλ‹€. 이와 같은 λ°©μ‹μœΌλ‘œ 이제 μ•žμœΌλ‘œ λ§Œλ“€μ–΄λ‚Ό λͺ¨λ“ˆλ“€μ˜ build.gradle.kts νŒŒμΌμ—λŠ” spring이 ν•„μš”ν•œ 경우 id("sample-springboot") λ₯Ό μ‚¬μš©ν•˜κ³  kotlin에 λŒ€ν•œ μ˜μ‘΄μ„±λ§Œ ν•„μš”ν•  경우 id("sample-kotlin-jvm") 을 μ‚¬μš©ν•΄μ„œ build.gradle.kts νŒŒμΌμ„ λ”μš± κ°„κ²°ν•˜κ²Œ λ§Œλ“€μ–΄λ‚Ό 수 μžˆμŠ΅λ‹ˆλ‹€.

build.gradle.kts build.gradle.kts의 plugins λΈ”λ‘μ—λŠ” μ›λž˜λŠ” ν”ŒλŸ¬κ·ΈμΈμ˜ 버전 정보λ₯Ό ν•¨κ»˜ λͺ…μ‹œν•΄μ•Ό ν•©λ‹ˆλ‹€. ν•˜μ§€λ§Œ convention ν”ŒλŸ¬κ·ΈμΈμ„ λ§Œλ“€ λ•ŒλŠ” λͺ…μ‹œν•œ ν”ŒλŸ¬κ·ΈμΈμ˜ 버전은 build.gradle의 dependency둜 μ§€μ •ν•΄μ€˜μ•Ό ν•©λ‹ˆλ‹€. κ΄€λ ¨ λ‚΄μš©μ€ forumsμ—μ„œ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

plugins {
    `kotlin-dsl`
}

repositories {
    mavenCentral()
    gradlePluginPortal()
}

dependencies {
    // jvm
    implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20")
    implementation("org.jetbrains.kotlin.kapt:org.jetbrains.kotlin.kapt.gradle.plugin:1.9.20")
    implementation("org.jetbrains.kotlinx.kover:org.jetbrains.kotlinx.kover.gradle.plugin:0.7.5")
    implementation("org.jlleitschuh.gradle:ktlint-gradle:11.0.0")

    // spring
    implementation("org.jetbrains.kotlin:kotlin-allopen:1.9.20")
    implementation("org.springframework.boot:spring-boot-gradle-plugin:3.2.0")
    implementation("io.spring.gradle:dependency-management-plugin:1.1.4")
}

common-library ν”„λ‘œμ νŠΈ

common-libraryλŠ” μƒλž΅ν•˜κ² μŠ΅λ‹ˆλ‹€. μžμ„Έν•œ μ½”λ“œλŠ” githubλ₯Ό μ°Έκ³  λ°”λžλ‹ˆλ‹€.

sample-service ν”„λ‘œμ νŠΈ

β”œβ”€β”€ adapter
 β”œβ”€β”€ inbound
  └── controller
 └── outbound
     └── repository
β”œβ”€β”€ application
β”œβ”€β”€ domain
β”œβ”€β”€ infrastructure
 └── h2
β”œβ”€β”€ server
 └── api

데λͺ¨ μ½”λ“œμ—λŠ” μ—¬λŸ¬κ°€μ§€ λͺ¨λ“ˆμ΄ 더 μžˆμ§€λ§Œ λ³Έ ν¬μŠ€νŒ…μ—μ„œλŠ” sample-service의 μœ„ λͺ¨λ“ˆμ— λŒ€ν•΄μ„œλ§Œ μ„€λͺ…ν•˜κ² μŠ΅λ‹ˆλ‹€.

domain λͺ¨λ“ˆ

build.gradle.kts

plugins {
    id("sample-kotlin-jvm")
}

domain λͺ¨λ“ˆμ€ μ•žμ„œ λ§Œλ“€μ–΄λ‘” build-plugin ν”„λ‘œμ νŠΈμ˜ kotlin ν”ŒλŸ¬κ·ΈμΈλ§Œ μ‚¬μš©ν•©λ‹ˆλ‹€.

Sample

class Sample(
    val id: String? = null,
    name: String,
    val createdAt: LocalDateTime = LocalDateTime.now(),
    updatedAt: LocalDateTime = LocalDateTime.now(),
) {
    // μƒλž΅
}

μ•žμœΌλ‘œ λ‚˜μ˜€λŠ” μ½”λ“œλŠ” 이 Sample 클래슀λ₯Ό 기반으둜 μž‘μ„±λ©λ‹ˆλ‹€.

application λͺ¨λ“ˆ

β”œβ”€β”€ build.gradle.kts
└── src
    └── main
        └── kotlin
            └── com
                └── sample
                    └── hexagonal
                        └── sample
                            └── application
                                β”œβ”€β”€ port
                                β”‚ β”œβ”€β”€ inbound
                                β”‚ β”‚ └── sample
                                β”‚ β”‚     β”œβ”€β”€ SampleDeleteInboundPort.kt
                                β”‚ β”‚     β”œβ”€β”€ SampleFindInboundPort.kt
                                β”‚ β”‚     └── SampleSaveInboundPort.kt
                                β”‚ └── outbound
                                β”‚     └── sample
                                β”‚         β”œβ”€β”€ SampleDeleteOutboundPort.kt
                                β”‚         β”œβ”€β”€ SampleFindOutboundPort.kt
                                β”‚         └── SampleSaveOutboundPort.kt
                                β”œβ”€β”€ service
                                β”‚ └── sample
                                β”‚     └── SampleService.kt

application λͺ¨λ“ˆμ˜ κ°„λž΅ν•œ tree κ΅¬μ‘°λŠ” μœ„μ™€ κ°™μŠ΅λ‹ˆλ‹€. inbound와 outboundλŠ” μ•žμ„œ μ–ΈκΈ‰ν–ˆλ“―μ΄ μ™ΈλΆ€μ—μ„œ λ‚΄λΆ€λ‘œ, λ‚΄λΆ€μ—μ„œ μ™ΈλΆ€λ‘œ λ‚˜κ°€λŠ” ν†΅λ‘œ μΈν„°νŽ˜μ΄μŠ€μž…λ‹ˆλ‹€. applicationλͺ¨λ“ˆμ˜ serviceλŠ” InboundPort μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•˜κ²Œ λ©λ‹ˆλ‹€. μ™ΈλΆ€μ—μ„œ λ‚΄λΆ€λ‘œ λ“€μ–΄μ˜€λŠ” μš”μ²­μ€ InboundPortλ₯Ό 톡해 λ‚΄λΆ€λ‘œ λ“€μ–΄μ˜€κ³  λ‚΄λΆ€μ—μ„œ μ™ΈλΆ€λ‘œ λ‚˜κ°€λŠ” μš”μ²­μ€ OutbountPort μΈν„°νŽ˜μ΄μŠ€λ₯Ό 톡해 λ‚˜κ°€κ²Œ λ©λ‹ˆλ‹€.

build.gradle.kts

plugins {
    id("sample-springboot")
}

dependencies {
    api(project(":domain"))
    implementation("com.sample.hexagonal.common:kafka-producer")
    implementation("com.sample.hexagonal.common:exception")
    implementation("com.sample.hexagonal.common:kafka-producer")

    implementation("org.springframework.data:spring-data-commons")
    implementation("org.springframework:spring-context")
    implementation("org.springframework:spring-tx")
}

// μƒλž΅

application λͺ¨λ“ˆμ€ domain λͺ¨λ“ˆκ³Ό common-library ν”„λ‘œμ νŠΈμ—μ„œ ν•„μš”ν•œ λͺ¨λ“ˆμ„ μ˜μ‘΄μ„±μœΌλ‘œ λ°›μ•„μ„œ μ‚¬μš©ν•©λ‹ˆλ‹€.

SampleService

@Service
class SampleService(
    private val sampleDeleteOutboundPort: SampleDeleteOutboundPort,
    private val sampleFindOutboundPort: SampleFindOutboundPort,
    private val sampleSaveOutboundPort: SampleSaveOutboundPort,
) : SampleDeleteInboundPort, SampleSaveInboundPort, SampleFindInboundPort {

    @Transactional
    override fun saveSample(name: String): Sample {
        return sampleSaveOutboundPort.save(
            Sample.create(name),
        )
    }
  // μƒλž΅
}

SampleServiceλŠ” InbountPort μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•˜κ²Œ 되고 μ™ΈλΆ€λ‘œμ˜ μš”μ²­μ€ OutbountPort μΈν„°νŽ˜μ΄μŠ€λ₯Ό μ‚¬μš©ν•˜μ—¬ μ²˜λ¦¬ν•©λ‹ˆλ‹€.

adapter.inbound.controller λͺ¨λ“ˆ

데λͺ¨ μ½”λ“œμ—λŠ” μ—¬λŸ¬ inbound λͺ¨λ“ˆμ΄ μžˆμ§€λ§Œ controller λͺ¨λ“ˆλ§Œ μ‚΄νŽ΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

build.gradle.kts

plugins {
    id("sample-springboot")
}

dependencies {
    implementation(project(":application"))
    implementation("com.sample.hexagonal.common:json")
    implementation("com.sample.hexagonal.common:utils")

    implementation("org.springframework.boot:spring-boot-starter-web")
}

// μƒλž΅

application λͺ¨λ“ˆκ³Ό common-library ν”„λ‘œμ νŠΈμ—μ„œ ν•„μš”ν•œ λͺ¨λ“ˆμ„ μ˜μ‘΄μ„±μœΌλ‘œ λ°›μŠ΅λ‹ˆλ‹€.

SampleController

@RestController
class SampleController(
    private val sampleFindInboundPort: SampleFindInboundPort,
    private val sampleSaveInboundPort: SampleSaveInboundPort,
    private val sampleDeleteInboundPort: SampleDeleteInboundPort,
) {

    @PostMapping("/v1/sample")
    fun saveSample(@RequestBody sampleSaveRequest: SampleSaveRequest): SampleResponse {
        val sample = sampleSaveInboundPort.saveSample(sampleSaveRequest.name)
        return SampleResponse.from(sample)
    }
    // μƒλž΅
}

controllerλŠ” 내뢀와 ν†΅μ‹ ν•˜κΈ° μœ„ν•΄ InbountPort μΈν„°νŽ˜μ΄μŠ€λ₯Ό μ‚¬μš©ν•˜μ—¬ μš”μ²­ν•©λ‹ˆλ‹€.

adapter.outbound.repository λͺ¨λ“ˆ

데λͺ¨ μ½”λ“œμ—λŠ” μ—¬λŸ¬ outbound λͺ¨λ“ˆμ΄ μžˆμ§€λ§Œ repository λͺ¨λ“ˆλ§Œ μ‚΄νŽ΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

build.gradle.kts

plugins {
    id("sample-springboot")
}

dependencies {
  implementation(project(":application"))
  implementation(project(":infrastructure:h2"))
//    implementation(project(":infrastructure:mongo"))
}

// μƒλž΅

Repositoryλͺ¨λ“ˆμ€ applicationκ³Ό infra의 h2λͺ¨λ“ˆμ„ μ˜μ‘΄μ„±μœΌλ‘œ μ‚¬μš©ν•©λ‹ˆλ‹€.

SampleRepository

@Repository
class SampleRepository(
    private val sampleDao: SampleEntityDao,
) : SampleDeleteOutboundPort, SampleFindOutboundPort, SampleSaveOutboundPort {

    override fun save(sample: Sample): Sample {
        return sampleDao.save(SampleMapper.mapDomainToEntity(sample))
            .let { SampleMapper.mapEntityToDomain(it) }
    }
    // μƒλž΅
}

RepositoryλŠ” λ‚΄λΆ€μ—μ„œ μ™ΈλΆ€μ˜ μš”μ²­μ— μ‚¬μš©λ˜λŠ” OutboundPort μΈν„°νŽ˜μ΄μŠ€μ˜ κ΅¬ν˜„μ²΄λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.

infrastructure.h2 λͺ¨λ“ˆ

build.gradle.kts

dependencies {
    implementation("com.sample.hexagonal.common:utils")
    implementation("com.sample.hexagonal.common:json")

    api("org.springframework.boot:spring-boot-starter-data-jpa")
    runtimeOnly("com.h2database:h2")
}

// μƒλž΅

h2 λͺ¨λ“ˆμ—μ„œλŠ” common-libraryμ—μ„œ ν•„μš”ν•œ μ˜μ‘΄μ„±μ„ μΆ”κ°€ν•©λ‹ˆλ‹€.

entity & dao

@Entity
class SampleEntity(

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,

    val name: String,

    @CreatedDate
    val createdAt: LocalDateTime,

    @LastModifiedDate
    val updatedAt: LocalDateTime,
)
@Repository
interface SampleEntityDao : JpaRepository<SampleEntity, Long>

h2 λͺ¨λ“ˆμ—μ„œλŠ” Repository λͺ¨λ“ˆμ—μ„œ μ‚¬μš©ν•  인프라 κΈ°μˆ λ“€μ„ κ΅¬ν˜„ν•©λ‹ˆλ‹€.

server.api λͺ¨λ“ˆ

build.gradle.kts

plugins {
  id("sample-springboot")
}

dependencies {
    implementation(project(":domain"))
    implementation(project(":application"))
    implementation(project(":adapter:inbound:controller"))
    implementation(project(":adapter:outbound:repository"))
    implementation(project(":adapter:outbound:producer"))
    implementation(project(":infrastructure:h2"))

    implementation("com.sample.hexagonal.common:actuator")
    implementation("org.springframework.boot:spring-boot-starter-web")

    // λͺ…μ‹œμ μœΌλ‘œ ν™•μΈν•˜κΈ° μœ„ν•΄μ„œ μΆ”κ°€
    implementation("com.sample.hexagonal.common:utils")
    implementation("com.sample.hexagonal.common:json")
    implementation("com.sample.hexagonal.common:kafka-producer")
    implementation("com.sample.hexagonal.common:exception")
}

// μƒλž΅

server-api λͺ¨λ“ˆμ€ μ„œλ²„λ₯Ό λ„μš°κΈ° μœ„ν•œ 껍데기 λͺ¨λ“ˆμž…λ‹ˆλ‹€. μ»΄ν¬λ„ŒνŠΈ μŠ€μΊ”μ„ μœ„ν•΄ ν•΄λ‹Ή μ„œλ²„λ₯Ό λ„μšΈλ•Œ μ‚¬μš©ν•  μ˜μ‘΄μ„±λ“€μ„ μΆ”κ°€ν•©λ‹ˆλ‹€. μ™ΈλΆ€ ν”„λ‘œμ νŠΈ λͺ¨λ“ˆμ˜ 경우 이미 λ‚΄λΆ€ λͺ¨λ“ˆμ˜ μ˜μ‘΄μ„±μœΌλ‘œ λ“€μ–΄κ°€ 있기 λ•Œλ¬Έμ— μŠ€ν”„λ§ λΆ€νŠΈμ˜ μ»΄ν¬λ„ŒνŠΈ μŠ€μΊ” λ©”μ»€λ‹ˆμ¦˜μƒ ν΄λž˜μŠ€νŒ¨μŠ€μ— μ‘΄μž¬ν•˜λŠ” λͺ¨λ“  νŒ¨ν‚€μ§€λ₯Ό μŠ€μΊ”ν•  수 있기 λ•Œλ¬Έμ— μ™ΈλΆ€ ν”„λ‘œμ νŠΈ λͺ¨λ“ˆμ€ μ˜μ‘΄μ„±μœΌλ‘œ μΆ”κ°€ν•˜μ§€ μ•Šμ•„λ„ λ©λ‹ˆλ‹€. ν•˜μ§€λ§Œ μ™ΈλΆ€ ν”„λ‘œμ νŠΈ λͺ¨λ“ˆμ„ μΆ”κ°€ν•˜μ§€ μ•ŠμœΌλ©΄ μ»΄ν¬λ„ŒνŠΈ μŠ€μΊ”μœΌλ‘œ 지정할 basePackageλ₯Ό λˆ„λ½ν•  κ°€λŠ₯성이 있기 λ•Œλ¬Έμ— λͺ…μ‹œμ μœΌλ‘œ μ˜μ‘΄μ„±μ„ 좔가해두고 basePackageλ₯Ό μ§€μ •ν• λ•Œ μ°Έκ³ ν•  수 μžˆλ„λ‘ ν•΄λ‘μ—ˆμŠ΅λ‹ˆλ‹€.

SampleApplication

@SpringBootApplication(
    scanBasePackages = [
        "com.sample.hexagonal.sample.server.api",
        "com.sample.hexagonal.sample.adapter",
        "com.sample.hexagonal.sample.application",
        "com.sample.hexagonal.sample.infrastructure",
        "com.sample.hexagonal.common"
    ],
)
class SampleApplication

fun main(args: Array<String>) {
    TimeZone.setDefault(TimeZone.getTimeZone(ZoneOffset.UTC))
    runApplication<SampleApplication>(*args)
}

μ™ΈλΆ€ ν”„λ‘œμ νŠΈμ˜ μ˜μ‘΄μ„±μ€ common-library ν”„λ‘œμ νŠΈμ˜ λͺ¨λ“ˆλ§Œ μ‚¬μš©ν•˜λ―€λ‘œ common을 μΆ”κ°€ν–ˆκ³  λ‚˜λ¨Έμ§€λŠ” λ‚΄λΆ€ λͺ¨λ“ˆμ˜ νŒ¨ν‚€μ§€ 경둜λ₯Ό μΆ”κ°€ν–ˆμŠ΅λ‹ˆλ‹€. ν˜„μž¬ 데λͺ¨ μ½”λ“œμƒμ—μ„œλŠ” 사싀 com.sample.hexagonal 만 λͺ…μ‹œν•˜κ±°λ‚˜ com.sample.hexagonal.sample으둜 sample-service νŒ¨ν‚€μ§€ 경둜λ₯Ό μž…λ ₯ν•΄μ„œ μŠ€μΊ” λͺ©λ‘μ„ κ°„μ†Œν™”ν•  수 μžˆμŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ μœ„μ™€ 같이 ν•œ μ΄μœ λŠ” λ§Œμ•½ ν˜„μž¬ api μ„œλ²„μ—μ„œ adapter.controller λͺ¨λ“ˆμ„ μ‚¬μš©ν•˜λ‚˜ adapter.controller.external νŒ¨ν‚€μ§€λ§Œ μ‚¬μš©ν•˜λ―€λ‘œ ν•΄λ‹Ή νŒ¨ν‚€μ§€λ§Œ μΆ”κ°€ν•˜κ³  싢을 수 있고 μ΄λŠ” μŠ€μΊ” λͺ©λ‘μ„ 톡해 μ»¨νŠΈλ‘€ν•  수 μžˆλ‹€λŠ” 것을 λ³΄μ—¬λ“œλ¦¬κΈ° μœ„ν•¨μž…λ‹ˆλ‹€.

μ°Έκ³ 

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages