๐ฐ 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
,ยTypescript
Framework
:ยSpring boot 3.5.0
Database
:ยPostgreSQL
IDE
:ยIntelliJ IDEA
CSR
:ยReact
Build-Tool
:ยGradle
ORM
:ยJPA
Query Library
:ยQueryDSL
DevOps
:ยDocker
,ยDocker-compose
Test
:ยJUnit
,ยJaCoCo
,ยSonarQube
,ยCypress
CI
:ยGitHub Actions
์์ฒญ ยท ์๋ต ํ๋ฆ
- ํด๋ผ์ด์ธํธ๊ฐ ์ฌ์ดํธ์ ์ ์ํ๋ฉด
Nginx
๊ฐReact
์ ์ ๋น๋ ํ์ผ์ ์๋ตํ๋ค. - ๋ชจ๋ ์์ฒญ์
Sentinel-Server
API 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");
}
}
- ์ฒ์์๋
@Container
Annotation
์ ์ฌ์ฉํ์ฌ ์์ ๊ฐ์ ํด๋์ค๋ฅผ ์์๋ฐ์ ํด๋น ์ปจํ ์ด๋์ ๋ผ์ดํ ์ฌ์ดํด์ ๊ด๋ฆฌํ๋ค. - ๊ทธ๋ฐ๋ฐ
@Testcontainers
์ปจํ ์ด๋๋ฅผ ๊ณต์ ํ์ง ๋ชปํ๊ณ ํด๋น ํด๋์ค๋ฅผ ์์ ๋ฐ๋ ์์ ํด๋์ค ๊ฐ์ ๋งํผ ์ปจํ ์ด๋ ์ฌ์์ฑ์ ์๋ํ๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค. - ์ปจํ
์ด๋๋ฅผ ์์ ํด๋์ค ๊ฐ์ ๋งํผ ์ฌ์์ฑํ๋ฉด, ๋ฆฌ์์ค๋ ์ปค์ง๊ณ ์ค์ ๋ก ์ปจํ
์ด๋ ์์ฑ ์๊ฐ ๋๋ฌธ์
DB
์ฐ๊ฒฐ ๋ฌธ์ ๋ก ํ ์คํธ๋ ์คํจํ๋ค. @Container
Annotation
์ ์์ ๊ณ , ์ฑ๊ธํค ๋ฐฉ์์ผ๋ก ์ปจํ ์ด๋๋ฅผ ๊ด๋ฆฌํ๋ฉฐ ์๋์ผ๋ก ์ปจํ ์ด๋๋ฅผ ์์ฑํ์ฌ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ์๋ค.
ํฅํ ๊ณํ
โ ํ๋ก์ ํธ์ ๋ฉค๋ฒ ๋งค์นญ ๊ธฐ๋ฅ ์ถ๊ฐ
- ํ๋ก์ ํธ์ ๋ฉค๋ฒ ๋งค์นญ ๊ธฐ๋ฅ์
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.