Post

🏰 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

μš”μ²­ Β· 응닡 흐름

  1. ν΄λΌμ΄μ–ΈνŠΈκ°€ μ‚¬μ΄νŠΈμ— μ ‘μ†ν•˜λ©΄ Nginxκ°€ React 정적 λΉŒλ“œ νŒŒμΌμ„ μ‘λ‹΅ν•œλ‹€.
  2. λͺ¨λ“  μš”μ²­μ€ Sentinel-Server API Gatewayλ₯Ό ν†΅ν•œλ‹€.
  3. 인증 Β· 인가 μš”μ²­μ€ Sentinel-Serverμ—μ„œ 직접 μ‘λ‹΅ν•œλ‹€.
  4. 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λΌλŠ” 넀이밍과 κ²Œμ΄λ―Έν”ΌμΌ€μ΄μ…˜ λ””μžμΈμ„ μ μš©ν–ˆμœΌλ‚˜, μ‚¬μ΄νŠΈμ— 처음 λ°©λ¬Έν•˜λŠ” μ‚¬μš©μžκ°€ μ„œλΉ„μŠ€ λͺ©μ μ„ μ§κ΄€μ μœΌλ‘œ μ΄ν•΄ν•˜κΈ° μ–΄λ €μšΈ 것 κ°™λ‹€λŠ” 생각이 λ“€μ—ˆλ‹€.
  • 보닀 전문적이고 λͺ¨λ˜ν•œ λŠλ‚Œμ˜ λ””μžμΈμœΌλ‘œ λ¦¬λ””μžμΈμ„ μ§„ν–‰ν•˜μ—¬ μ„œλΉ„μŠ€μ˜ λͺ©μ κ³Ό κΈ°λŠ₯을 λͺ…ν™•νžˆ μ „λ‹¬ν•˜κ³ μž ν•œλ‹€.
  • https://github.com/brobro332/332-blokey-land-service
This post is licensed under CC BY 4.0 by the author.