π° 332-blokey-land-service
π° 332-blokey-land-service
κ°μ
- νλ‘μ νΈ, νμ€ν¬, λ§μΌμ€ν€μ ν΅ν©μ μΌλ‘ κ΄λ¦¬ν μ μλ μλΉμ€
- λ°μ΄ν°λ₯Ό μκ°ννμ¬ νλ‘μ νΈ νν©μ μ½κ² νμ ν μ μλ€.
Β κ°λ° κΈ°κ°
v1.0.0Β (2025-06-06 ~ 2025-06-15): λ°±μλλ μ€κ³ λ° μ΄κΈ° κΈ°λ₯ ꡬνv1.0.1Β (2025-06-24 ~ 2025-07-07): νλ‘ νΈμλ κ°λ° λ° λλ©μΈ λ‘μ§ μ λΉv1.0.2Β (2025-07-08): κ°λ° νκ²½μμ μΉ μ μ μ 무ν μλ‘κ³ μΉ¨ λ²κ·Έ ν«ν½μ€v1.1.0Β (2025-07-09 ~ 2025-07-15): ν μ€νΈ νκ²½ κ΅¬μΆ λ° κΈ°μ‘΄ κΈ°λ₯ κ°μ μμ
νλ‘μ νΈ νκ²½
Language:ΒJava 21,ΒTypescriptFramework:ΒSpring boot 3.5.0Database:ΒPostgreSQLIDE:ΒIntelliJ IDEACSR:ΒReactBuild-Tool:ΒGradleORM:ΒJPAQuery Library:ΒQueryDSLDevOps:ΒDocker,ΒDocker-composeTest:ΒJUnit,ΒJaCoCo,ΒSonarQube,ΒCypressCI:ΒGitHub Actions
μμ² Β· μλ΅ νλ¦
- ν΄λΌμ΄μΈνΈκ° μ¬μ΄νΈμ μ μνλ©΄
Nginxκ°Reactμ μ λΉλ νμΌμ μλ΅νλ€. - λͺ¨λ μμ²μ
Sentinel-ServerAPI Gatewayλ₯Ό ν΅νλ€. - μΈμ¦ Β· μΈκ° μμ²μ
Sentinel-Serverμμ μ§μ μλ΅νλ€. Blokey-Land-Serviceμ λνAPIμμ²μReverse Proxyλ₯Ό ν΅ν΄Blokey-Land-Serviceκ° μλ΅νλ€.
μΈνλΌ κ΅¬μ‘° (μμ )
- μμ§ λ°°ν¬λ μ§ννμ§ μμμΌλ©°, μΆν μΈμ€ν΄μ€λ₯Ό λλκΈ° μν΄
Docker-composeλ¨μλ‘ λΆλ¦¬νμλ€. Blokey-Landμλ²μλ μμ§ElasticSearchλ₯Ό λμ νμ§ μμλ€.- μΆν κ΄λ ¨ κΈ°λ₯κ³Ό ν¨κ» μΆκ°λ μμ μ΄λ€.
νμ§ κ΄λ¦¬ νλ‘μΈμ€
β λ¨μ ν μ€νΈ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@DisplayName("μ‘΄μ¬νλ IDλ‘ νλ‘μ νΈλ₯Ό μ‘°ννλ©΄ ν΄λΉ κ°μ²΄λ₯Ό λ°νν΄μΌ νλ€.")
@Test
void givenValidId_whenReadProjectByProjectId_thenReturnProject() {
// given
Long id = 1L;
Project project = Project.builder()
.id(id)
.title("μ λͺ©")
.description("μ€λͺ
")
.status(ProjectStatusType.ACTIVE)
.isPrivate(true)
.estimatedStartDate(LocalDate.now())
.estimatedEndDate(LocalDate.now())
.actualStartDate(LocalDate.now())
.actualEndDate(LocalDate.now())
.build();
when(repository.findById(id)).thenReturn(Optional.of(project));
// when
Project found = service.findProjectByProjectId(id);
// then
assertEquals(project.getId(), found.getId());
assertEquals(project.getTitle(), found.getTitle());
assertEquals(project.getDescription(), found.getDescription());
assertEquals(project.getImageUrl(), found.getImageUrl());
assertEquals(project.getStatus(), found.getStatus());
assertEquals(project.isPrivate(), found.isPrivate());
assertEquals(project.getEstimatedStartDate(), found.getEstimatedStartDate());
assertEquals(project.getEstimatedEndDate(), found.getEstimatedEndDate());
assertEquals(project.getActualStartDate(), found.getActualStartDate());
assertEquals(project.getActualEndDate(), found.getActualEndDate());
verify(repository).findById(id);
}
JUnitμ ν΅ν΄TDDλ¨μ ν μ€νΈλ₯Ό μννλ€.Mockitoνλ μμν¬λ₯Ό μ¬μ©νμ¬ λ ν¬μ§ν 리μ λ©μλ νΈμΆ μ νΉμ κ°μ λ°ννλλ‘ νμ¬DBμ°κ²° μμ΄ ν μ€νΈλ₯Ό μ§νν μ μλ€.- ν΅ν© ν μ€νΈλ₯Ό μννλ©΄ μ’μ§λ§, κ°λ° λ° λΉλ κ³Όμ μμ μκ° λΉμ©μ΄ ν¬λ―λ‘ μλΉμ€ κ³μΈ΅μ λ¨μ ν μ€νΈλ‘ μΆ©μ‘±νμλ€.
β ν΅ν© ν μ€νΈ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Testcontainers
public abstract class ContainerBaseTest {
private static final PostgreSQLContainer<?> POSTGRES_CONTAINER;
static {
Dotenv dotenv = Dotenv.configure()
.ignoreIfMissing()
.load();
dotenv.entries().forEach(entry ->
System.setProperty(entry.getKey(), entry.getValue())
);
POSTGRES_CONTAINER = new PostgreSQLContainer<>("postgres:15-alpine");
POSTGRES_CONTAINER.start();
}
@DynamicPropertySource
static void overrideProps(DynamicPropertyRegistry registry) {
String originalJdbcUrl = POSTGRES_CONTAINER.getJdbcUrl();
String p6spyJdbcUrl = originalJdbcUrl.replace("jdbc:postgresql:", "jdbc:p6spy:postgresql:");
registry.add("spring.datasource.url", () -> p6spyJdbcUrl);
registry.add("spring.datasource.username", POSTGRES_CONTAINER::getUsername);
registry.add("spring.datasource.password", POSTGRES_CONTAINER::getPassword);
registry.add("spring.datasource.driver-class-name", () -> "com.p6spy.engine.spy.P6SpyDriver");
registry.add("file.upload-dir", () -> "DEFAULT");
registry.add("server.port", () -> "8081");
registry.add("server.address", () -> "0.0.0.0");
}
}
- ν΅ν© ν
μ€νΈλ
Testcontainersλ₯Ό μ¬μ©νμ¬ κ°μμDBλ₯Ό μ¬μ©ν μ μλλ‘ μΆμ ν΄λμ€λ₯Ό μμ±νλ€.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Test
@DisplayName("μ¬μ©μ IDλ‘ νλ‘μ νΈ λ° νμ€ν¬ λͺ©λ‘ μ‘°ν μ νλ‘μ νΈ λͺ©λ‘κ³Ό ν΄λΉ νλ‘μ νΈμ νμ€ν¬ λͺ©λ‘μ΄ ν¨κ» λ°νλλ€.")
void givenBlokeyId_whenFindProjectsWithTasksByBlokeyId_thenReturnsProjectsWithTasks() {
// given
taskRepository.save(Task.builder()
.title("μ λͺ© 1")
.description("μ€λͺ
1")
.project(project1)
.milestone(null)
.assignee(blokeyId)
.estimatedStartDate(LocalDate.now())
.estimatedEndDate(LocalDate.now())
.actualStartDate(LocalDate.now())
.actualEndDate(LocalDate.now())
.build()
);
// when
List<Project> result = repository.findProjectsWithTasksByBlokeyId(blokeyId);
// then
assertThat(result).hasSize(2);
assertThat(result.getFirst().getTitle()).isEqualTo("μ λͺ© 1");
assertThat(result.getFirst().getTasks()).isNotNull();
}
- λ ν¬μ§ν 리 λ©μλλ‘ λ§€νλλ 쿼리λ₯Ό ν μ€νΈνκΈ° μν΄ ν΅ν© ν μ€νΈλ₯Ό μ§ννλ€.
β ν μ€νΈ 컀λ²λ¦¬μ§ λͺ¨λν°λ§
JaCoCoλ₯Ό μ¬μ©νμ¬ ν μ€νΈ 컀λ²λ¦¬μ§λ₯Ό κ°λ° νκ²½μμ λͺ¨λν°λ§ νλ€.- ν
μ€νΈ λΆνμ μ½λκΉμ§ 컀λ²λ¦¬μ§ λ²μμ ν¬ν¨λλ©΄ μ€νλ € κ°λ° μ§νμ λ°©ν΄κ° λκΈ° λλ¬Έμ, λμ μ½λλ
Service,Repositoryν΄λμ€λ‘ νμ νμλ€.
β
E2E ν
μ€νΈ
- νλ‘ νΈμλμ ν΅μ¬ νλ‘μΈμ€λ₯Ό
tsνμΌλ‘ μμ±νμ¬ ν μ€νΈλ₯Ό μ§ννλ€. - κ°λ Ή
Blokey-Landμ ν΅μ¬μ κ°νΈμ°¨νΈ νμΈμ΄λ―λ‘ μ΄κΈ°μ μ£Όμ λ°μ΄ν°λ₯Ό μ μ₯νλ νλ‘μΈμ€λ₯Ό μμ±ν΄λ³΄μλ€.
β
CI ν΅ν©μ ν΅ν νμ§ κ΄λ¦¬
1
2
3
4
5
- name: Build and analyze with SonarCloud
working-directory: spring-boot-app
env:
SONAR_TOKEN: $
run: ./gradlew clean build jacocoTestReport sonarqube --info
GitHub Actionsμν¬νλ‘μ°μ μμ κ°μ΄SonarQubeλΆμStepμ μΆκ°νμλ€.
- νμ§ κ²μ΄νΈλ₯Ό μ€μ νμ¬
CIκ° μ±κ³΅μ μΌλ‘ μ§νλλ©΄,SonarQubeν΄λΌμ°λ μλΉμ€λPRμ μ½λλ₯ΌPushν κ²½μ° μλ‘μ΄ μ½λμ λν μ΄μ, μ½λ 컀λ²λ¦¬μ§λ₯Ό λΆμνμ¬ μ½λ©νΈλ₯Ό λ¨κΈ΄λ€.
μ΄λ €μ λ μ
β 벑μλ ν μ΄λΈ μ€κ³ λ° κ΅¬ν
- ν
μ΄λΈμ μ€κ³ν λ
N+1λ± μμμΉ λͺ»ν λ¬Έμ λ₯Ό μλ°©νκΈ° μν΄ κ°κΈμ μ°κ΄ κ΄κ³λ₯Ό κ°μ§ μλλ‘ νμλ€. - κ·Έλ¬λ κ°νΈμ°¨νΈλ₯Ό λ λλ§ ν λλ νλ‘ νΈμλμμ νλ‘μ νΈλ₯Ό μ‘°ννκ³ , ν΄λΉ λͺ©λ‘μ μννλ©° νμ€ν¬λ₯Ό μ‘°ννλ€λ³΄λ λ€νΈμν¬ μμ²μ
N+1λ²μ΄λ νκ² λμλ€. - ν΄λΉ λ¬Έμ λ₯Ό ν΄κ²°νκΈ° μν΄ νλ‘μ νΈμ νμ€ν¬λ μλ°©ν₯ μ°κ΄κ΄κ³λ₯Ό κ°λλ‘ μμ νμλ€.
- μλ²λ¨μμ
DBμN+1λ² μμ²μ νμ§ μλλ‘ ν¨μΉ μ‘°μΈμ μ¬μ©νμ¬ μ‘°ννμλ€.
β νλ‘ νΈμλ μν κ΄λ¦¬
- κ°λ Ή
A β B β Cλ°©ν₯μΌλ‘ μ»΄ν¬λνΈλ₯Ό νΈμΆνκ³Aμ μνλ₯ΌCμμ μ‘°μν΄μΌ ν κ²½μ°Bμμ κ·Έ νλ¦μ΄ λκΈ°λ©΄ μ»΄νμΌμ ν΅ν΄ λ¬Έμ λ₯Ό λλ²κΉ ν μ μμ΄ λ¬Έμ λ₯Ό ν΄κ²°νλλ° μ€λ μκ°μ΄ κ±Έλ Έλ€. - μνλ₯Ό
propsλ‘λ§ μ λ¬νλ ꡬ쑰λ κΉμ΄μ§λ©΄ κ΄λ¦¬κ° μ΄λ €μ μ§λ€λ μ μ μκ² λμλ€. - μΆν
Context,Reduxκ°μ ν¨ν΄μ μ κ·Ήμ μΌλ‘ λμ ν μμ μ΄λ€.
β
Testcontainers 컨ν
μ΄λλ₯Ό 곡μ νμ§ λͺ»νκ³ μ¬μμ±νλ λ¬Έμ
1
2
3
4
5
6
7
8
9
10
11
12
13
@Testcontainers
public abstract class ContainerBaseTest {
@Container
static PostgreSQLContainer<?> postgresContainer = new PostgreSQLContainer<>("postgres:15-alpine");
@DynamicPropertySource
static void overrideProps(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgresContainer::getJdbcUrl);
registry.add("spring.datasource.username", postgresContainer::getUsername);
registry.add("spring.datasource.password", postgresContainer::getPassword);
registry.add("file.upload-dir", () -> "/test-uploads");
}
}
- μ²μμλ
@ContainerAnnotationμ μ¬μ©νμ¬ μμ κ°μ ν΄λμ€λ₯Ό μμλ°μ ν΄λΉ 컨ν μ΄λμ λΌμ΄ν μ¬μ΄ν΄μ κ΄λ¦¬νλ€. - κ·Έλ°λ°
@Testcontainers컨ν μ΄λλ₯Ό 곡μ νμ§ λͺ»νκ³ ν΄λΉ ν΄λμ€λ₯Ό μμ λ°λ μμ ν΄λμ€ κ°μ λ§νΌ 컨ν μ΄λ μ¬μμ±μ μλνλ λ¬Έμ κ° λ°μνλ€. - 컨ν
μ΄λλ₯Ό μμ ν΄λμ€ κ°μ λ§νΌ μ¬μμ±νλ©΄, 리μμ€λ 컀μ§κ³ μ€μ λ‘ μ»¨ν
μ΄λ μμ± μκ° λλ¬Έμ
DBμ°κ²° λ¬Έμ λ‘ ν μ€νΈλ μ€ν¨νλ€. @ContainerAnnotationμ μμ κ³ , μ±κΈν€ λ°©μμΌλ‘ 컨ν μ΄λλ₯Ό κ΄λ¦¬νλ©° μλμΌλ‘ 컨ν μ΄λλ₯Ό μμ±νμ¬ λ¬Έμ λ₯Ό ν΄κ²°νμλ€.
ν₯ν κ³ν
β νλ‘μ νΈμ λ©€λ² λ§€μΉ κΈ°λ₯ μΆκ°
- νλ‘μ νΈμ λ©€λ² λ§€μΉ κΈ°λ₯μ
Blokey-LandμλΉμ€μ ν΅μ¬ κ°μΉ μ€ νλλ‘, μ¬μ©μ μΉνμ μΈ κ²½νμ μ 곡νκΈ° μν΄ κΈ°ν λ¨κ³λΆν° μ€μν μμλ‘ κ³ λ €λμλ€. - μ΄λ₯Ό μν΄ νλ‘μ νΈ λ° μ¬μ©μ λλ©μΈμ μ€ν¬(
skill), ν¬μ§μ (discipline) νλλ₯Ό μΆκ°νκ³ ,ElasticSearchλ₯Ό λμ νμ¬ νλ‘μ νΈμ λ©€λ² κ° μΆμ² λ° λ§€μΉ κΈ°λ₯μ κ°λ°νκ³ μ νλ€.
β κΈ°μ‘΄ κΈ°λ₯ μ΅μ ν
- μμ§ μλ°©ν₯μΌλ‘ μ°κ΄ κ΄κ³λ₯Ό μ νν΄μΌ ν λλ©μΈ κ΄κ³κ° λ¨μμλ€.
- μμΈλ¬ λλ©μΈ μ°κ΄ κ΄κ³ μ νμ λ°λ₯Έ νλ‘ νΈμλμ νλ©΄ ꡬμ±κ³Ό 벑μλμ 쿼리λ₯Ό μ΅μ νν μμ μ΄λ€.
β λμμΈ
- μ΄κΈ°μλ μ¬μ©μμκ² μΉκ·Όν μΈμμ μ£ΌκΈ° μν΄
Blokey-LandλΌλ λ€μ΄λ°κ³Ό κ²μ΄λ―ΈνΌμΌμ΄μ λμμΈμ μ μ©νμΌλ, μ¬μ΄νΈμ μ²μ λ°©λ¬Ένλ μ¬μ©μκ° μλΉμ€ λͺ©μ μ μ§κ΄μ μΌλ‘ μ΄ν΄νκΈ° μ΄λ €μΈ κ² κ°λ€λ μκ°μ΄ λ€μλ€. - λ³΄λ€ μ λ¬Έμ μ΄κ³ λͺ¨λν λλμ λμμΈμΌλ‘ 리λμμΈμ μ§ννμ¬ μλΉμ€μ λͺ©μ κ³Ό κΈ°λ₯μ λͺ νν μ λ¬νκ³ μ νλ€.
GitHub Link
- https://github.com/brobro332/332-blokey-land-service
This post is licensed under CC BY 4.0 by the author.





