๊ฐ์
ArchUnit์ Java ์ฝ๋์ ์ํคํ ์ฒ๋ฅผ ๊ฒ์ฌํ๊ธฐ ์ํ ๊ฐ๊ฒฐํ๊ณ ํ์ฅ ๊ฐ๋ฅํ ์คํ์์ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค. ArchUnit์ Java์ ๊ธฐ๋ณธ ๋จ์ ํ ์คํธ ํ๋ ์์ํฌ๋ฅผ ํ์ฉํ์ฌ, ์ฃผ์ด์ง Java ๋ฐ์ดํธ ์ฝ๋๋ฅผ ๋ถ์ํ๊ณ ๋ชจ๋ ํด๋์ค์ ๊ตฌ์กฐ๋ฅผ ํด์ํจ์ผ๋ก์จ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ํคํ ์ฒ๋ฅผ ์ฒด๊ณ์ ์ผ๋ก ํ ์คํธํ ์ ์๊ฒ ํด ์ค๋๋ค.
- ํจํค์ง ๋ฐ ํด๋์ค ์์กด์ฑ ๊ฒ์ฌ: ํจํค์ง์ ํด๋์ค ๊ฐ์ ์์กด ๊ด๊ณ๋ฅผ ๋ถ์ํ๊ณ , ๊ฒฉ๋ฆฌ๋ ๊ตฌ์กฐ๋ฅผ ์ ์งํ๋์ง ํ์ธ
- ์์ ๊ด๊ณ ๋ฐ ์ํ ์ฐธ์กฐ ๊ฒ์ฌ: ํด๋์ค ๊ฐ์ ์์ ๊ตฌ์กฐ๋ฅผ ๋ถ์ํ๊ณ , ์ํ ์ฐธ์กฐ๊ฐ ์๋์ง ๊ฒ์ฌ
- ๋ ์ด์ด ์ํคํ ์ฒ ๊ฒ์ฌ: ๋ ์ด์ด ๊ฐ์ ์์กด์ฑ์ ๊ฒ์ฌํ์ฌ, ๋ช ํํ๊ณ ๊ฒฌ๊ณ ํ ๋ ์ด์ด ๊ตฌ์กฐ๋ฅผ ์ ์งํ๋์ง ํ์ธ
- ์ฝ๋ฉ ์ปจ๋ฒค์ ๊ฒ์ฌ: ์ฌ์ฉ์๊ฐ ์ ์ํ ์ฝ๋ฉ ๊ท์น์ ๊ฒ์ฌํ์ฌ, ์ผ๊ด๋ ์ฝ๋ฉ ์คํ์ผ์ ์ ์งํ๋์ง ํ์ธ
์ ์ฒด ์ฝ๋๋ ๊นํ๋ธ์์ ํ์ธ ๊ฐ๋ฅํฉ๋๋ค.
์ฌ์ฉ ๋ฐฉ๋ฒ
ArchUnit์ ์ฌ์ฉํ๊ธฐ ์ํด์ ๋ณต์กํ ์ธํ๋ผ ์ค์ ์ด๋ ์๋ก์ด ์ธ์ด ํ์ต์ด ํ์ํ์ง ์์ต๋๋ค. ๊ธฐ์กด ์คํ๋ง ํ๋ก์ ํธ์ ๊ฐ๋จํ๊ฒ ์๋์ ์์กด์ฑ์ ์ถ๊ฐํจ์ผ๋ก์จ ์ฝ๊ฒ ์ฌ์ฉํ ์ ์์ต๋๋ค.
dependencies {
//archunit
testImplementation 'com.tngtech.archunit:archunit:1.1.0'
}
์ฝ๋
ํ ์คํธ ํด๋์ค์ ์ด๊ธฐ ์ค์
class ArchitectureTest {
JavaClasses javaClasses;
@BeforeEach
public void beforeEach() {
javaClasses = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) // ํ
์คํธ ํจํค์ง๋ ์ ์ธ
.importPackages("com.example.ArchUnit");
}
// ์ดํ ํ
์คํธ ๋ฉ์๋๋ค...
}
ClassFileImporter
๋ ArchUnit์ ์ฌ์ฉํ์ฌ Java ํด๋์ค๋ฅผ ๊ฐ์ ธ์ฌ ๋ ์ฃผ๋ก ์ฌ์ฉ๋๋ ํด๋์ค์
๋๋ค. ClassFileImporter
๋ ๋ค์ํ ์ฒด์ด๋ ๋ฉ์๋๋ฅผ ์ ๊ณตํ๋ฉฐ, ๊ฐ์ ธ์ฌ ํด๋์ค์ ๋ฒ์์ ์กฐ๊ฑด์ ์ธ๋ฐํ๊ฒ ์ง์ ํ ์ ์์ต๋๋ค. ์ฌ๊ธฐ์์ ์ฌ์ฉ๋ withImportOption
๋ฉ์๋๋ ๊ฐ์ ธ์ฌ ํด๋์ค๋ค์ ๋ํ ํํฐ๋ง ์ต์
์ ์ค์ ํ๋ ๋ฐ ์ฌ์ฉ๋ฉ๋๋ค.
ClassFileImporter์ ์ฃผ์ ๋ฉ์๋
- withImportOption(ImportOption option): ๊ฐ์ ธ์ฌ ํด๋์ค์ ๋ํ ์ต์ ์ ์ค์ ํฉ๋๋ค. ์๋ฅผ ๋ค์ด, ํ ์คํธ ํด๋์ค๋ฅผ ์ ์ธํ๊ฑฐ๋ ํน์ ํจํด์ ๋ง๋ ํด๋์ค๋ง ํฌํจ์ํค๋ ๋ฑ์ ์ต์ ์ ์ค์ ํ ์ ์์ต๋๋ค.
- importPackages(String... packages): ์ง์ ๋ ํจํค์ง ๋ด์ ํด๋์ค๋ค์ ๊ฐ์ ธ์ต๋๋ค. ์ฌ๋ฌ ํจํค์ง๋ฅผ ๋์์ ์ง์ ํ ์ ์์ต๋๋ค.
- importClasses(Class... classes): ํน์ ํด๋์ค๋ค์ ์ง์ ์ง์ ํ์ฌ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค.
- importUrl(URL url): URL์ ํตํด ํด๋์ค๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค. ์ด๋ ์น ๊ธฐ๋ฐ ๋ฆฌ์์ค๋ JAR ํ์ผ ๋ฑ์์ ํด๋์ค๋ฅผ ๊ฐ์ ธ์ฌ ๋ ์ ์ฉํฉ๋๋ค.
- importJar(File jarFile): JAR ํ์ผ๋ก๋ถํฐ ํด๋์ค๋ค์ ๊ฐ์ ธ์ต๋๋ค.
ImportOption์ ์ฃผ์ ์ต์ ๋ค
- DO_NOT_INCLUDE_TESTS: ํ ์คํธ ํด๋์ค๋ฅผ ์ ์ธํฉ๋๋ค. ์ผ๋ฐ์ ์ผ๋ก ํ ์คํธ ์ฝ๋์ ์ํคํ ์ฒ๋ฅผ ๊ฒ์ฌํ ํ์๊ฐ ์์ ๋ ์ฌ์ฉ๋ฉ๋๋ค.
- DO_NOT_INCLUDE_ARCHIVES: ์์นด์ด๋ธ(์: JAR ํ์ผ) ๋ด์ ํด๋์ค๋ฅผ ์ ์ธํฉ๋๋ค.
- DO_NOT_INCLUDE_JARS: JAR ํ์ผ ๋ด์ ํด๋์ค๋ฅผ ์ ์ธํฉ๋๋ค.
- Predefined: ์ฌ์ ์ ์ ์๋ ์ฌ๋ฌ ์ต์
๋ค์ ์ ๊ณตํฉ๋๋ค. ์๋ฅผ ๋ค์ด,
DO_NOT_INCLUDE_TESTS
๋Predefined
์ต์ ์ค ํ๋์ ๋๋ค.
์ค์ ๋ก๋ ์ต์ ๋ค์ด ๋ ๋ง์๋ฐ, ์คํ ์์ค๋ฅผ ์ดํด๋ณด๋ฉด ์ข์ ๊ฒ ๊ฐ์ต๋๋ค.
ํด๋์ค ์ด๋ฆ ๊ฒ์ฌ
@Test
@DisplayName("Controller ํจํค์ง ๋ด์ ํด๋์ค์ ์ด๋ฆ์ 'Controller'๋ก ๋๋์ผ ํ๋ค.")
void controllersShouldBeNamedCorrectly() {
ArchRule rule = ArchRuleDefinition.classes()
.that().resideInAPackage("..controller..")
.should().haveSimpleNameEndingWith("Controller")
.allowEmptyShould(true); // ๋น ๊ฒฐ๊ณผ ํ์ฉ
rule.check(javaClasses);
}
@Test
@DisplayName("์๋น์ค ํจํค์ง ๋ด์ ํด๋์ค์ ์ด๋ฆ์ 'Service'๋ก ๋๋์ผ ํ๋ค.")
void servicesShouldBeNamedCorrectly() {
ArchRule rule = ArchRuleDefinition.classes()
.that().resideInAPackage("..service..")
.should().haveSimpleNameEndingWith("Service")
.allowEmptyShould(true); // ๋น ๊ฒฐ๊ณผ ํ์ฉ
rule.check(javaClasses);
}
@Test
@DisplayName("๋ฆฌํฌ์งํ ๋ฆฌ ํจํค์ง ๋ด์ ํด๋์ค์ ์ด๋ฆ์ 'Repository'๋ก ๋๋์ผ ํ๋ค.")
void repositoriesShouldBeNamedCorrectly() {
ArchRule rule = ArchRuleDefinition.classes()
.that().resideInAPackage("..repository..")
.should().haveSimpleNameEndingWith("Repository")
.allowEmptyShould(true); // ๋น ๊ฒฐ๊ณผ ํ์ฉ
rule.check(javaClasses);
}
์ ์ธ ๊ฐ์ ํ ์คํธ ๋ฉ์๋๋ ArchUnit์ ์ฌ์ฉํ์ฌ ํน์ ํด๋์ค ์ ํ์ ์ด๋ฆ์ด ์ผ์ ํ ํจํด์ ๋ฐ๋ฅด๋์ง ๊ฒ์ฌํฉ๋๋ค. ๊ฐ ๋ฉ์๋๋ ๋ค์๊ณผ ๊ฐ์ ๊ณผ์ ์ ๊ฑฐ์นฉ๋๋ค.
- ArchRuleDefinition.classes()๋ฅผ ์ฌ์ฉํ์ฌ ๊ท์น์ ์ ์ํ๋ค. ์ด๋ ํด๋์ค ์ ํ์ ๋ํ ๊ท์น์ ์ ์ํ๊ธฐ ์ํ ์์์ ์ด๋ค.
- .that().resideInAPacakage("..controller..")์ ๊ฐ์ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ํน์ ํจํค์ง ๋ด์ ํด๋์ค๋ฅผ ๋์์ ์ง์ ํ๋ค. ์ฌ๊ธฐ์ "..controller.."์ ๊ฐ์ ๋ฌธ์์ด์ ํจํค์ง ๊ฒฝ๋ก๋ฅผ ๋ํ๋ด๋ฉฐ, ์ด ๊ฒฝ๋ก๋ ํจํค์ง ์ด๋ฆ์ ์ผ๋ถ๋ฅผ ํฌํจํ๊ฑฐ๋ ์ ์ฒด๋ฅผ ํฌํจํ ์ ์๋ค.
- .should().havaSimpleNameEndingWith("Controller")์ ๊ฐ์ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ํด๋์ค ์ด๋ฆ์ด ํน์ ๋ฌธ์์ด๋ก ๋๋์ผ ํ๋ค๋ ๊ท์น์ ์ค์ ํ๋ค. ์ฌ๊ธฐ์๋ ๊ฐ๊ฐ "Controller", "Service", "Repository"๋ก ๋๋๋ ์ด๋ฆ์ ๊ฐ์ง ํด๋์ค๋ง ํต๊ณผ์ํจ๋ค.
- .allowEmptyShould(true)๋ ํด๋น ๊ท์น์ ๋ํด ๊ฒ์ฌํ ๋์์ด ์์ ๊ฒฝ์ฐ(์: ํน์ ํจํด์ ๋ฐ๋ฅด๋ ํด๋์ค๊ฐ ์๊ฑฐ๋, ํจํค์ง๊ฐ ์๋ ๊ฒฝ์ฐ) ํ ์คํธ๊ฐ ์คํจํ์ง ์๋๋ก ํ๋ค.
- rule.check(javaClasses)๋ ์ ์๋ ๊ท์น์ javaClasses์ปฌ๋ ์ ์ ๋ํด ์คํํ๋ค. ์ฌ๊ธฐ์ javaClasses๋ ํ ์คํธ ๋์์ด ๋๋ ํด๋์ค๋ค์ ์งํฉ์ด๋ค.
์ ํ ์คํธ๋ฅผ ํตํด ์ฝ๋๋ฒ ์ด์ค ๋ด์์ ์ผ๊ด๋ ๋ค์ด๋ฐ ์ปจ๋ฒค์ ์ ์ ์งํ๊ณ , ๊ฐ ํจํค์ง(๊ณ์ธต)์ ํด๋์ค๋ค์ด ๊ทธ๋ค์ ์ญํ ์ ๋ง๋ ์ด๋ฆ์ ๊ฐ์ง๊ณ ์๋์ง ํ์ธํ ์ ์์ต๋๋ค.
์ปจํธ๋กค๋ฌ์ ์์กด์ฑ ๊ฒ์ฌ
@Test
@DisplayName("์ปจํธ๋กค๋ฌ๋ ์๋น์ค ํด๋์ค์๋ง ์์กดํด์ผ ํ๋ค.")
void controllersShouldOnlyDependOnServices() {
ArchRule rule = ArchRuleDefinition.classes()
.that().resideInAPackage("..controller..")
.should().onlyDependOnClassesThat()
.resideInAnyPackage("..service..", "java..", "..controller..")
.allowEmptyShould(true); // ๋น ๊ฒฐ๊ณผ ํ์ฉ
rule.check(javaClasses);
}
์ด ํ ์คํธ๋ ์ปจํธ๋กค๋ฌ ํด๋์ค๊ฐ ์๋น์ค ํด๋์ค, ์๋ฐ ๊ธฐ๋ณธ ํด๋์ค, ๋๋ ๋ค๋ฅธ ์ปจํธ๋กค๋ฌ ํด๋์ค์๋ง ์์กดํด์ผ ํ๋ค๋ ๊ท์น์ ๊ฒ์ฌํฉ๋๋ค.
์ด๋ฅผ ํตํด ์ปจํธ๋กค๋ฌ๊ฐ ์๋น์ค ๊ณ์ธต์๋ง ์์กดํ๊ณ , ๋ค๋ฅธ ๊ณ์ธต(์: ๋ฆฌํฌ์งํ ๋ฆฌ, ์ ํธ๋ฆฌํฐ ํด๋์ค ๋ฑ)์ ์ง์ ์ ์ผ๋ก ์์กดํ์ง ์๋๋ก ํจ์ผ๋ก์จ, ์ฝ๋์ ๋ชจ๋์ฑ๊ณผ ์ ์ง๋ณด์์ฑ์ ๋์ผ ์ ์์ต๋๋ค.
ํ๋ ์ธ์ ์ ๊ฒ์ฌ
@Test
@DisplayName("๋ชจ๋ ํด๋์ค๋ ํ๋ ์ธ์ ์
์ ์ฌ์ฉํ์ง ์์์ผ ํ๋ค.")
void allClassesShouldNotUseFieldInjection() {
ArchRule rule = ArchRuleDefinition.fields()
.that().areDeclaredInClassesThat().resideInAPackage("..")
.should().notBeAnnotatedWith(Autowired.class)
.allowEmptyShould(true); // ๋น ๊ฒฐ๊ณผ ํ์ฉ
rule.check(javaClasses);
}
์ ํ
์คํธ๋ ๋ชจ๋ ํ๋ก์ ํธ ๋ด์ ํด๋์ค์ ๋ํด @Autowired
๋ฅผ ํตํ ์์กด์ฑ ์ฃผ์
์ ์ฌ์ฉํ๋์ง ๊ฒ์ฌํฉ๋๋ค.
์ํ ์์กด์ฑ ๊ฒ์ฌ
@Test
@DisplayName("ํด๋์ค๋ค์ ์ํ ์์กด์ฑ์ ๊ฐ์ง๋ฉด ์ ๋๋ค.")
void noCyclicDependencies() {
ArchRule rule = SlicesRuleDefinition.slices()
.matching("com.example.ArchUnit.(*)..")
.should().beFreeOfCycles()
.allowEmptyShould(true); // ๋น ๊ฒฐ๊ณผ ํ์ฉ
rule.check(javaClasses);
}
์ ํ ์คํธ๋ ํ๋ก์ ํธ ๋ด์ ํด๋์ค๋ค ์ฌ์ด์ ์ํ ์์กด์ฑ์ด ์กด์ฌํ์ง ์์์ผ ํ๋ค๋ ๊ท์น์ ์ ์ํฉ๋๋ค.
SlicesRuleDefinition.slices()
๋ ํ๋ก์ ํธ ๋ด์ ํด๋์ค๋ฅผ ์ฌ๋ฌ ์ฌ๋ผ์ด์ค๋ก ๊ทธ๋ฃนํํ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ฉฐ, matching("com.example.ArchUnit.(*)..")
๋ถ๋ถ์ com.example.ArchUnit
ํจํค์ง ์๋์ ๋ชจ๋ ํด๋์ค๋ฅผ ๋์์ผ๋ก ๊ท์น์ ์ ์ฉํ๊ฒ ๋ค๋ ๊ฒ์ ๋ํ๋
๋๋ค. ์ฌ๊ธฐ์ `(*)`๋ ํ์ ํจํค์ง๋ฅผ ํฌํจํ๋ ์์ผ๋์นด๋๋ฅผ ์๋ฏธํฉ๋๋ค.
should().beFreeOfCycles()
๋ถ๋ถ์ ์ด ์ฌ๋ผ์ด์ค๋ค ์ฌ์ด์ ์ํ ์์กด์ฑ์ด ์์ด์ผ ํ๋ค๋ ๊ท์น์ ์ค์ ํฉ๋๋ค. ๋ง์ฝ ์ํ ์์กด์ฑ์ด ๋ฐ๊ฒฌ๋๋ฉด, ํ
์คํธ๋ ์คํจํฉ๋๋ค.
ํด๋์ค ์ ๊ทผ ๊ฒ์ฌ
@Test
@DisplayName("์๋น์ค ํด๋์ค๋ ์ปจํธ๋กค๋ฌ ํด๋์ค์ ์ ๊ทผํ์ง ์์์ผ ํ๋ค.")
void servicesShouldNotAccessControllers() {
ArchRule rule = ArchRuleDefinition.classes()
.that().resideInAPackage("..service..")
.should().onlyAccessClassesThat()
.resideOutsideOfPackage("..controller..")
.allowEmptyShould(true); // ๋น ๊ฒฐ๊ณผ ํ์ฉ
rule.check(javaClasses);
}
์ ํ ์คํธ๋ ์๋น์ค ํด๋์ค๋ค์ด ์ปจํธ๋กค๋ฌ ํด๋์ค์ ์ ๊ทผํ์ง ์์์ผ ํ๋ค๋ ๊ท์น์ ์ ์ํฉ๋๋ค. ์ผ๋ฐ์ ์ผ๋ก ๊ณ์ธตํ๋ ์ํคํ ์ฒ์์ ์๋น์ค ๋ ์ด์ด๊ฐ ์ปจํธ๋กค๋ฌ ๋ ์ด์ด์ ์ข ์๋์ง ์๋ ๊ฒ์ ๋ชฉํ๋ก ํ๋ฉฐ ์ปจํธ๋กค๋ฌ์ ์๋น์ค๋ฟ๋ง ์๋๋ผ ๋ค์ํ๊ฒ ๊ฒ์ฌํ ์ ์์ต๋๋ค.
.should().onlyAccessClassesThat().resideOutsideOfPackage("..controller..")
๋ถ๋ถ์ ์๋น์ค ํด๋์ค๋ค์ด "controller" ํจํค์ง ์ธ๋ถ์ ํด๋์ค๋ค์๋ง ์ ๊ทผํด์ผ ํ๋ค๋ ๊ท์น์ ์ค์ ํฉ๋๋ค. ๋ง์ฝ ์๋น์ค ํด๋์ค๊ฐ ์ปจํธ๋กค๋ฌ ํด๋์ค์ ์ ๊ทผํ๋ ๊ฒฝ์ฐ๊ฐ ๋ฐ๊ฒฌ๋๋ฉด, ํ
์คํธ๋ ์คํจํฉ๋๋ค.
ํ ์คํธ ๊ด๋ จ ๊ฐ์ด ๋ณด๋ฉด ์ข์ ๊ธ
'BackEnd๐ฑ > Spring' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
ShedLock์ผ๋ก ๋ค์ค ์ธ์คํด์ค ํ๊ฒฝ์์ ๋จ์ผ ์ค์ผ์ค๋ฌ ๋์ ๋ณด์ฅํ๊ธฐ (2) | 2023.11.24 |
---|---|
default method๋ก JpaRepository ์ข ๋ ์ฐ์ํ๊ฒ ์จ๋ณด๊ธฐ (0) | 2023.11.16 |
@Scheduled ์ฌ์ฉํ ๋ ์ค๋ ๋ ์ค์ (0) | 2023.11.09 |
LocalStack์ ํ์ฉํ AWS S3 ํ ์คํธ ํ๊ฒฝ ๊ตฌ์ถํ๊ธฐ (2) | 2023.10.22 |
TestContainer๋ก ํตํฉ ํ ์คํธ ํ๊ฒฝ ๊ตฌ์ถํ๊ธฐ (3) | 2023.10.15 |
๋ถ์ฐ๋ฝ์ผ๋ก ์ ์ฐฉ์ ์ด๋ฒคํธ ๊ตฌํํ๊ธฐ (3) | 2023.08.31 |
๋๊ธ