λ³Έλ¬Έ λ°”λ‘œκ°€κΈ°
  • μž₯원읡 κΈ°μˆ λΈ”λ‘œκ·Έ
πŸŽ› Others.../- old dev log

[Best Of the Best ν™œλ™κΈ°] 1μ°¨ νŒ€ ν”„λ‘œμ νŠΈ ν›„κΈ° - λ°±μ—”λ“œλ₯Ό κ°œλ°œν•˜λ©° ν–ˆλ˜ κ³ λ―Όλ“€

by Wonit 2021. 8. 25.

μ˜€λŠ˜μ€ BoB 10κΈ° λ³΄μ•ˆμ œν’ˆκ°œλ°œ νŠΈλž™μ˜ λ³΄μ•ˆ μ†”λ£¨μ…˜ μ œμž‘ μˆ˜μ—…μ—μ„œ ν•œλ‹¬λ™μ•ˆ μ§„ν–‰ν•œ 1μ°¨ νŒ€ ν”„λ‘œμ νŠΈμ— λŒ€ν•΄μ„œ 이야기해보렀 ν•œλ‹€.

ν•΄λ‹Ή 글은 총 2λΆ€μž‘μœΌλ‘œ νŒ€ λΉŒλ”©κ³Ό ν˜‘μ—… κ³Όμ • 그리고 μ„œλΉ„μŠ€ μ„€λͺ… 및 개발 κ³Όμ • 으둜 λ‚˜λ‰˜μ–΄μ Έ μžˆμŠ΅λ‹ˆλ‹€.

 

ν”„λ‘œμ νŠΈ github λ°”λ‘œκ°€κΈ° -> λ³΄μ•ˆ μœ„ν˜‘ νŠΈλž˜ν”½ 뢄석 μ†”λ£¨μ…˜ github

 

GitHub - dhslrl321/L7-monitor: πŸ” L7 μ—μ„œ λ™μž‘ν•˜λŠ” Access Log 기반 νŠΈλž˜ν”½ 뢄석 및 λ³΄μ•ˆ μœ„ν˜‘ 뢄석 μ„œλΉ„μŠ€

πŸ” L7 μ—μ„œ λ™μž‘ν•˜λŠ” Access Log 기반 νŠΈλž˜ν”½ 뢄석 및 λ³΄μ•ˆ μœ„ν˜‘ 뢄석 μ„œλΉ„μŠ€ (BoB 10 κΈ° λ³΄μ•ˆμ œν’ˆκ°œλ°œ 1μ°¨ νŒ€ ν”„λ‘œμ νŠΈ) - GitHub - dhslrl321/L7-monitor: πŸ” L7 μ—μ„œ λ™μž‘ν•˜λŠ” Access Log 기반 νŠΈλž˜ν”½ 뢄석 및 보

github.com


 

μ§€λ‚œ μ‹œκ°„μ€ Front End λ₯Ό λ¦¬μ•‘νŠΈλ‘œ κ΅¬μ„±ν•˜λ©° ν–ˆλ˜ 기술적 κ³ λ―Όκ³Ό λ¬Έμ œλ“€μ— λŒ€ν•΄μ„œ 이야기λ₯Ό ν–ˆμ—ˆλ‹€.

 

μ˜€λŠ˜μ€ λ‚΄κ°€ κ°€μž₯ μ’‹μ•„ν•˜κ³  νŒ€ λ‚΄μ—μ„œ 주둜 λ§‘μ•˜λ˜ Backend 에 λŒ€ν•΄μ„œ 이야기해보렀 ν•œλ‹€.

 

μš°μ„  Backend λŠ” Spring Boot κ°€ Framework 으둜 μ‚¬μš©λ˜μ—ˆλ‹€.

 

μ •ν™•νžˆ Backend Application 이 μ—¬κΈ°μ„œ 무슨 일을 ν•˜λƒλ©΄?

 

μ§€λ‚œ μ‹œκ°„ 이야기 ν–ˆμ—ˆλ˜ 우리 L7 λͺ¨λ‹ˆν„°λ§ μ†”λ£¨μ…˜μ—μ„œλŠ” μ„œλΉ„μŠ€ ν”Œλ‘œμš°κ°€ λ‹€μŒκ³Ό 같이 ν˜λŸ¬κ°€κ²Œ λœλ‹€.

 

μ„œλΉ„μŠ€ ν”Œλ‘œμš°

 

  1. κ³ κ°μ‚¬λŠ” 기쑴에 μš΄μ˜μ€‘μ΄λ˜ μ„œλ²„μ™€ ν•΄λ‹Ή μ„œλ²„κ°€ μ €μž₯ν•˜λŠ” access.log κ°€ μ‘΄μž¬ν•œλ‹€.
  2. μš°λ¦¬κ°€ κ°œλ°œν•œ Agent νŒ¨ν‚€μ§€λ₯Ό μ„€μΉ˜ν•œλ‹€.
  3. ν•΄λ‹Ή Agentμ—μ„œ access log 의 μœ„μΉ˜λ₯Ό μ§€μ •ν•˜κ³  뢄석 μ„œλ²„λ‘œ access log λ₯Ό μ „λ‹¬ν•œλ‹€.
  4. 운영 μ„œλ²„μ˜ μ„±λŠ₯을 μœ„ν•΄μ„œ 뢄석은 λ‹€λ₯Έ μ„œλ²„μ—μ„œ μ§„ν–‰ν•œλ‹€.
  5. 뢄석 μ„œλ²„λ‘œ λ“€μ–΄μ˜¨ access log λŠ” 뢄석을 μ§„ν–‰ν•˜μ—¬ 곡격 μœ ν˜•μ— λ”°λΌμ„œ DB에 μ €μž₯ν•œλ‹€.
  6. ν•΄λ‹Ή DB 의 값듀을 ν† λŒ€λ‘œ λ³΄μ•ˆ λ ˆλ²¨μ„ μ‚°μΆœν•˜κ³  각쒅 μ‹œκ°ν™”μ— ν•„μš”ν•œ 데이터λ₯Ό μ •μ œν•˜μ—¬ Front둜 λ°˜ν™˜ν•œλ‹€.
  7. Front ServerλŠ” Backend μ—κ²Œ λ°›μ•„μ˜¨ 데이터λ₯Ό 가지고 λŒ€μ‹œλ³΄λ“œμ—μ„œ μ‹œκ°ν™”λ₯Ό μˆ˜ν–‰ν•œλ‹€.

 

μ—¬κΈ°μ„œ Backendκ°€ κ°œμž…ν•˜λŠ” 6번과 7λ²ˆμ—μ„œ ν•„μš”ν•œ μ›Ή 데이터λ₯Ό DBμ—μ„œ μ‘°νšŒν•˜κ³  μ—¬λŸ¬ 가곡을 ν•œ λ’€ Front μ—κ²Œ μ‘λ‹΅μœΌλ‘œ λ°˜ν™˜ν•΄μ£ΌλŠ” 역할을 μˆ˜ν–‰ν•œλ‹€.

 

λ°±μ—”λ“œμ˜ 기술적 κ³ λ―Ό

 

λ°±μ—”λ“œλŠ” 사싀상 SELECT 쿼리만 날리고 Get Mapping 만 λ˜μ–΄μžˆλŠ” 맀우 μ‹¬ν”Œν•œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μœΌλ‘œ μ„€κ³„λ˜μ—ˆλ‹€.

 

μ›λž˜μ˜ SaaS ν˜•νƒœλΌλ©΄ 인증과 인가에 λŒ€ν•œ 고민을 ν–ˆμ—ˆμ„ν…Œκ³ , 그에 λ”°λΌμ„œ Request Validation μ΄λ‚˜ μ—¬λŸ¬ μ²˜λ¦¬λ“€μ΄ 듀어갔을듯 ν•œλ°, μ›Œλ‚™ κΈ°λŠ₯이 λ‹¨μˆœν•˜κΈ° λ•Œλ¬Έμ— 별닀λ₯Έ 고민을 ν•˜μ§€ μ•Šμ•„λ„ λ˜λŠ” λΆ€λΆ„μ΄μ—ˆλ‹€.

 

ν•˜μ§€λ§Œ 이 κ³Όμ •μ—μ„œλ„ λ‚˜λ¦„ μ—¬λŸ¬ 고민을 ν•˜μ˜€κ³ , λ‚˜λ§Œμ˜ κ·œμΉ™μ„ μ„Έμš°λ €κ³  λ…Έλ ₯ν•˜λ©° λ§Œλ‚¬λ˜ κ²°κ³Όλ¬Όκ³Ό λ¬Έμ œλ“€μ„ κ³΅μœ ν•΄λ³΄λ € ν•œλ‹€.

 

μ£Όμ œλŠ” 크게 2가지 이닀.

 

 

  1. κ΅¬ν˜„ μ•„ν‚€ν…μ²˜
  2. ν…ŒμŠ€νŠΈ μ „λž΅

 

κ΅¬ν˜„ 아킀텍쳐

 

3-Tier κ΅¬μ‘°μ—μ„œ ν”„λ‘œμ νŠΈλ₯Ό ν•  λ•ŒλŠ” 거의 Layered Architecture λ₯Ό Spring Boot μ—μ„œ μ‚¬μš©ν•˜κ³€ ν•œλ‹€.

 

사싀상 Spring Boot μ—μ„œ μ‚¬μš©ν•  μ•„ν‚€ν…μ²˜ νŒ¨ν„΄μ€ λ‚΄κ°€ 많이 λͺ¨λ₯΄λŠ” 뢀뢄도 있고 ν•΄μ„œ 기쑴에 많이 ν•΄μ™”λ˜ λ ˆμ΄μ–΄λ“œ μ•„ν‚€ν…μ²˜λ‘œ API μ„œλ²„λ₯Ό κ΅¬μ„±ν–ˆλ‹€.

 

κ°„λ‹¨ν•˜κ²Œ Layeted Architecture λž€ OSI 7 Layer 와 같이 각각의 λ ˆμ΄μ–΄κ°€ ν•˜λŠ” 일이 λͺ…ν™•ν•˜κ³  ν•˜λ‚˜μ˜ λ ˆμ΄μ–΄μ—μ„œ 끝낸 데이터λ₯Ό λ‹€μŒ λ ˆμ΄μ–΄λ‘œ λ„˜κ²¨μ£ΌλŠ” 방식을 μ·¨ν•œλ‹€

 

 

ν”„λ‘œμ νŠΈ κ΅¬μ‘°λŠ” λ‹€μŒκ³Ό κ°™λ‹€

 

 

Persistence Layer : Domain νŒ¨ν‚€μ§€

 

Domain νŒ¨ν‚€μ§€μ—μ„œλŠ” Data 의 μ˜μ†ν™” (Persistence) 에 κ΄€ν•œ 처리λ₯Ό ν•˜λŠ” 뢀뢄이닀.

 

λ‚˜λŠ” Domain μ•ˆμ—μ„œ 크게 3파트둜 λ‚˜λˆ΄λ‹€.

 

  1. Jpa Entity
  2. JpaRepository λ₯Ό μƒμ†λ°›λŠ” μΈν„°νŽ˜μ΄μŠ€
  3. dto

Entity

 

Entity λŠ” Jpa λ₯Ό κ³΅λΆ€ν•΄λ΄€λ˜ μ‚¬λžŒλ“€μ—κ²ŒλŠ” μΉœκ·Όν•œ 객체이닀.

 

Jpa의 핡심인 persistence context 에 λ“€μ–΄κ°ˆ λŒ€μƒ 객체둜 DB ν…Œμ΄λΈ”μ— λŒ€μ‘λ˜λŠ” ν•˜λ‚˜μ˜ 클래슀둜 μƒκ°ν•˜λ©΄ μ‰¬μš΄λ°, DB ν…Œμ΄λΈ”μ„ Object ν™” ν•˜λŠ” ORM의 νŠΉμ„±μ—μ„œ κ°€μž₯ 기본이 λ˜λŠ” μΈμŠ€ν„΄μŠ€λ“€μ„ μ˜λ―Έν•œλ‹€.

 

JpaRepository λ₯Ό μƒμ†λ°›λŠ” μΈν„°νŽ˜μ΄μŠ€

 

Spring Data Jpa μ—μ„œλŠ” κ°„λ‹¨ν•œ CRUD κΈ°λŠ₯을 μ²˜λ¦¬ν•˜κΈ° μœ„ν•΄μ„œ 기본적으둜 JpaRepository λ₯Ό μ œκ³΅ν•œλ‹€.

 

ν•΄λ‹Ή μΈν„°νŽ˜μ΄μŠ€λ₯Ό 상속받고 λ‚˜λŠ” 쿼리 λ©”μ„œλ“œμ˜ λ©”μ„œλ“œ λ„€μž„μ„ ν†΅ν•œ 쿼리 생성을 μ΄μš©ν•˜μ˜€λ‹€.

 

public interface TotalRepository extends JpaRepository<Total, Long> {
  long countByTimestampBetween(LocalDateTime from, LocalDateTime to);

  List<Total> findTop100ByTimestampBetween(LocalDateTime minusDays, LocalDateTime now);
}

 

DTO

 

DTO λŠ” Martin Fowler κ°€ μ •μ˜ν•œ 데이터 μ „μ†‘λ§Œμ„ μœ„ν•œ 객체λ₯Ό μ˜λ―Έν•œλ‹€.

 

이 글을 μ“°λŠ” μ‹œμ μ— μ•Œκ²Œ λ˜μ—ˆμ§€λ§Œ DTO λŠ” ν•¨μˆ˜κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ”λ°, λ‚΄ μ½”λ“œμ—μ„œλŠ” DTO에 둜직이 μ‘΄μž¬ν•œλ‹€ γ…Žγ…Ž..

 

μˆ˜μ •ν•˜λŠ”κ±Έλ‘œ..

 

Business Layer

 

Buisiness Layer μ—μ„œλŠ” Presenter Layer 와 Persistence Layer 사이에 μœ„μΉ˜ν•˜μ—¬ 우리의 Business Logic 을 κ΅¬μ„±ν•˜κ³  μ •μ˜ν•˜λŠ” λ ˆμ΄μ–΄λ‹€.

 

μ‹€μ œ Database와 μƒν˜Έμž‘μš©μ€ Business layer μ—μ„œ 이뀄지기 λ•Œλ¬Έμ— Transaction Boundary λ₯Ό μ„€μ •ν•΄μ£Όμ—ˆλ‹€.

 

μŠ€ν”„λ§ μ½”λ“œμ—μ„œλŠ” Presentation Layer μ—μ„œ 받은 DTOλ₯Ό 가지고 Entity 둜 λ³€ν™˜ν•˜λ©° λΉ„μ¦ˆλ‹ˆμŠ€ 둜직이 호좜된 이후 λ‹€μ‹œ DTO둜 λ³€ν™˜ν•˜λŠ” 과정을 κ±°μΉœλ‹€.

 

λ˜ν•œ 이 λΆ€λΆ„μ—μ„œ jdoc 을 μž‘μ„±ν•΄μ„œ 가독성을 λ†’μ˜€λ‹€.

 

/**
* ν˜„μž¬λ₯Ό κΈ°μ€€μœΌλ‘œ 24μ‹œκ°„ μ΄λ‚΄μ˜ λͺ¨λ“  비정상 μš”μ²­μ˜ 개수λ₯Ό 좜λ ₯ν•œλ‹€.
*
* @param trafficType νŠΈλž˜ν”½ μœ ν˜•
* @return ν˜„μž¬ μ‹œκ°„μœΌλ‘œλΆ€ν„° -24 μ‹œκ°„μ˜ λͺ¨λ“  비정상 둜그의 개수λ₯Ό λ°˜ν™˜ν•œλ‹€.
*/
public TotalTrafficResponse getTodayTrafficSummaries(TrafficType trafficType) {
  LocalDateTime from = LocalDateTime.now().minusDays(1L);

  long count = 0;

  if(trafficType.equals(TrafficType.ALL)) {
      count = totalRepository.countByTimestampBetween(from, LocalDateTime.now());
  } else if (trafficType.equals(TrafficType.THREAT)) {
      count = abnormalRepository.countByTimestampBetween(from, LocalDateTime.now());
  }

  return TotalTrafficResponse.builder()
          .id(1L)
          .count(count)
          .timestamp(from)
          .build();
}

 

μœ„μ˜ μ½”λ“œλŠ” 였늘 λ°œμƒν•œ 총 νŠΈλž˜ν”½μ„ λ°˜ν™˜ν•œλ‹€.

 

Presentation Layer

 

Presentataion Layer λŠ” 2-tier μ•„ν‚€ν…μ³μ—μ„œ HTML νŽ˜μ΄μ§€λ₯Ό λ°˜ν™˜ν•˜κΈ°λ„ ν•œλ‹€.

 

μš°λ¦¬λŠ” 3-tier 의 μ•„ν‚€ν…μ²˜λ‘œ JSON 을 μ΄μš©ν•œ API Call 을 λ°©ν–₯으둜 μž‘μ•˜κΈ° λ•Œλ¬Έμ— JSON 을 λ°˜ν™˜ν•˜λ„λ‘ ν•œλ‹€.

 

@RestController
@RequestMapping(value = "/api/traffics", produces = "application/json")
public class TrafficController {

    private final TrafficService trafficService;

    public TrafficController(TrafficService trafficService) {
        this.trafficService = trafficService;
    }

    @GetMapping("/summaries")
    public ResponseEntity<TotalSummariesResponse> getTodayTrafficsSummaries() {
        return ResponseEntity.ok().body(trafficService.getTodaySummaries());
    }
}

 

μ‹€μ œ μ½”λ“œμƒμ—μ„œλŠ” μœ„μ™€ 같이 Business Layer 에 μ‘΄μž¬ν•˜λŠ” Service μ½”λ“œλ₯Ό μƒμ„±μž μ£Όμž…μ„ ν†΅ν•΄μ„œ λΉˆμ„ μƒμ„±ν•˜κ³ , API Endpoint μ—μ„œ λ°”λ‘œ ν˜ΈμΆœν•˜μ—¬ μ—°κ²°ν•΄μ£Όμ—ˆλ‹€.

 

이 κ³Όμ •μ—μ„œ μ•Œ 수 μžˆλŠ” 사싀은 Service.java μ—μ„œλŠ” DTOλ₯Ό λ§€κ°œλ³€μˆ˜λ‘œ λ°›κ³  μ μ ˆν•œ λΉ„μ¦ˆλ‹ˆμŠ€λ₯Ό μ²˜λ¦¬ν•œ λ’€, DTO둜 λ°˜ν™˜ν•œλ‹€λŠ” 것이닀

 

이 과정을 ν•˜λ‚˜μ˜ 그림으둜 ν‘œν˜„ν•˜μžλ©΄ λ‹€μŒκ³Ό 같이 λ‚˜μ˜¬ 것이닀

 

 

이런 κ³Όμ •μœΌλ‘œ λ‚˜λŠ” 주둜 κ°œλ°œμ„ ν•΄ μ™”μ—ˆκ³ , ν•΄λ‹Ή ν”„λ‘œμ νŠΈμ—μ„œλ„ μ μš©μ„ ν–ˆμ—ˆλ‹€.

 

이 κ³Όμ •μ—μ„œ μ›λž˜λΌλ©΄ Service μ½”λ“œκ°€ λ“€μ–΄κ°€λŠ” Business Layer μ—μ„œ Validation 이 ν•„μˆ˜μ μœΌλ‘œ 이루어져야 ν•œλ‹€.

 

예λ₯Ό λ“€μ–΄ μž…λ ₯ κ°’, Requets Body 의 Data κ°€ μ μ ˆν•œ 데이터인지λ₯Ό ν™•μΈν•˜λŠ” 과정이 ν•„μš”ν•˜μ§€λ§Œ λͺ¨λ‘ GET μš”μ²­μœΌλ‘œ 처리λ₯Ό ν•˜λŠ” API μ„œλ²„μ—μ„œ μ΄λŠ” λΆˆν•„μš”ν•œ 과정이라 μƒκ°ν•΄μ„œ μƒλž΅μ„ ν•˜μ˜€λ‹€.

 

ν…ŒμŠ€νŠΈ μ „λž΅

 

λ‚΄κ°€ κ°€μž₯ μ’‹μ•„ν•˜λŠ” 뢀뢄이 λ°”λ‘œ Test Code 이닀.

 

ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό μž‘μ„±ν•˜κ³  μ‹€νŒ¨ν•˜λŠ” ν…ŒμŠ€νŠΈλ₯Ό λ¨Όμ € κ΅¬μ„±ν•œ λ’€, μ„±κ³΅ν•˜κΈ° κΉŒμ§€ μ—¬λŸ¬ μž‘μ—…μ„ ν•΄μ£Όμ–΄ ν…ŒμŠ€νŠΈκ°€ ν†΅κ³Όν•˜λ©΄ κ·Έ λ§Œν•œ μ§œλ¦Ών•¨μ„ μ–΄λ””μ„œλ„ λŠλ‚„ 수 μ—†λ‹€

 

ν”νžˆ 말 ν•˜λŠ” TDD λ₯Ό λ‚˜λŠ” μ’‹μ•„ν•˜λŠ”λ°, λ‚΄ λ‚˜λ¦„ κ·œμΉ™μ΄ μ‘΄μž¬ν•œλ‹€.

 

λͺ¨λ“  μ½”λ“œλ₯Ό TDD둜 κ°œλ°œμ„ ν•œλ‹€λ©΄ μ’‹κ² μ§€λ§Œ ν˜„μ‹€μ μœΌλ‘œ 그게 μ‰½μ§€λ§Œμ€ μ•Šλ‹€.

 

정말 λͺ…ν™•ν•˜κ³  κ°„λ‹¨ν•œ μ½”λ“œ, 예λ₯Ό λ“€μ–΄ Entity 의 CRUD ν…ŒμŠ€νŠΈλΌλ˜μ§€? 이런 μ½”λ“œλŠ” 과감히 TDDλ₯Ό ν•˜μ§€ μ•ŠλŠ”λ‹€.

 

κ·Έλƒ₯ 정상 λ‘œμ§μ„ λ¨Όμ € μ§œμ§€λ§Œ, μ–΄λ €μš΄ κ°œλ…μ„ κ΅¬ν˜„ν•΄μ•Ό ν•˜λŠ” κ²½μš°λŠ” 무쑰건 TDDλ₯Ό μ΄μš©ν•œλ‹€.

 

λ‚˜λŠ” 이 κ³Όμ •μ—μ„œ λ§Œλ‚¬λ˜ λ¬Έμ œλ“€κ³Ό 고민듀을 κ³΅μœ ν•΄λ³΄λ € ν•œλ‹€.

 

Behavior Driven Development

 

BDD라고 λΆˆλ¦¬λŠ” 이것은 TDDλ₯Ό κ·Όκ°„μœΌλ‘œ ν•˜λŠ” TDD의 λ˜λ‹€λ₯Έ κ΅¬ν˜„λ²•?이닀

 

BDD에 λŒ€ν•œ μžμ„Έν•œ 사항은 ν•΄λ‹Ή λΈ”λ‘œκ·Έ JUnit5 BDDMockito둜 μ•Œμ•„λ³΄λŠ” TDD와 BDD 차이점 및 μ‹€μŠ΅ μ—μ„œ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€!

 

BDDλŠ” 크게 TDD와 λ‹€λ₯΄μ§€ μ•Šλ‹€.

 

BDDλŠ” Red -> Green -> Refactor 사이클을 λ„λŠ” 것은 λ™μΌν•˜κ³  ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό μ§œλŠ” 방법이 μ’€ 더 체계화 λ˜μ–΄μžˆλ‹€κ³  μƒκ°ν•˜λ©΄ λœλ‹€.

 

μš°λ¦¬κ°€ 일반적으둜 TDDλ₯Ό 배울 λ•Œ, λ‹€μŒκ³Ό 같이 λ°°μš΄λ‹€.

 

given-when-then

 

이 것이 λ°”λ‘œ BDD의 핡심이닀.

 

인간이 읽기 μ‰¬μš΄ λ°©λ²•μœΌλ‘œ ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό λ‚˜λˆ λ†“λŠ” 것을 μ˜λ―Έν•˜λŠ”λ°, λ‚˜λŠ” 이λ₯Ό μœ„ν•΄ JUnit5 의 BDDMockito 클래슀λ₯Ό μ΄μš©ν–ˆλ‹€.

 

class TrafficServiceTest {

    private TrafficService trafficService;

    private final TotalRepository totalRepository = mock(TotalRepository.class);
    private final AbnormalRepository abnormalRepository = mock(AbnormalRepository.class);

    @BeforeEach
    void init() {
        trafficService = new TrafficService(totalRepository, abnormalRepository);

        given(totalRepository.countByTimestampBetween(any(LocalDateTime.class), any(LocalDateTime.class)))
                .willReturn(12327L);
    }

    @ParameterizedTest
    @DisplayName("μ‹œκ°„ νƒ€μž…(5λΆ„) κ°€ 주어지면 8 개의 dto λ°˜ν™˜ - 성곡")
    @MethodSource("paramsForGetTrafficCountByPeriodWithValid")
    void getTrafficBetween_valid(PeriodType periodType) {
        List<TotalTrafficResponse> response = trafficService
                .getTrafficCountByPeriod(periodType);

        TotalTrafficResponse firstTotalResponseData = response.get(0);

        assertAll(
                () -> assertEquals(8, response.size()),
                () -> assertEquals(1, firstTotalResponseData.getId()),
                () -> assertEquals(12327L, firstTotalResponseData.getCount()),
                () -> assertNotNull(firstTotalResponseData.getTimestamp())
        );
    }
}

 

μœ„μ˜ μ½”λ“œλ₯Ό ν™•μΈν•΄λ³΄μž.

 

Unit ν…ŒμŠ€νŠΈλ₯Ό μœ„ν•΄μ„œ ν…ŒμŠ€νŠΈ λŒ€μƒ 클래슀λ₯Ό κ³ λ¦½ν•˜μ˜€λ‹€.

 

μ™„λ²½ν•œ 고립을 μœ„ν•΄μ„œ λŒ€μƒ 클래슀λ₯Ό μ œμ™Έν•œ λͺ¨λ“  클래슀λ₯Ό κ°€μ§œλ‘œ λ§Œλ“€μ–΄μ£ΌλŠ” 과정이 ν•„μš”ν•˜λ‹€.

 

λ‚˜λŠ” mock() 을 μ΄μš©ν•΄μ„œ νŒŒλΌλ―Έν„°λ‘œ λ“€μ–΄κ°€λŠ” ν΄λž˜μŠ€κ°€ κ°€μ§œλ‘œ μ–΄λ–€ 일을 μˆ˜ν–‰ν• μ§€ κ²°μ •ν•΄μ£Όμ—ˆλ‹€.

 

init() λ©”μ„œλ“œμ—μ„œλŠ” ν…ŒμŠ€νŠΈ λŒ€μƒ ν΄λž˜μŠ€κ°€ ν•˜λ‚˜μ˜ 일을 μˆ˜ν–‰ν•  λ•Œ, ν•„μš”ν•œ 의쑴 관계에 λŒ€ν•΄μ„œ μ£Όμž…μ‹œμΌœμ€€λ‹€.

 

이 κ³Όμ •μ—μ„œ given() μ΄λΌλŠ” BDDMockito 의 λ©”μ„œλ“œλ₯Ό μ‚¬μš©ν–ˆλ‹€.

 

그리고 ν…ŒμŠ€νŠΈ λŒ€μƒ 클래슀의 λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•˜μ—¬ κ²°κ³Όλ₯Ό μ˜ˆμΈ‘ν•  수 μžˆλ„λ‘ ν•˜μ—¬ then 을 μž‘μ„±ν–ˆλ‹€.

 

ν…ŒμŠ€νŠΈ μ½”λ“œμ— Parameter μ „λ‹¬ν•˜κΈ°

 

ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό μž‘μ„±ν•˜λ‹€λ³΄λ©΄ μ€‘λ³΅λœ ν…ŒμŠ€νŠΈ μ½”λ“œκ°€ 많이 λ°œμƒν•˜κ²Œ λœλ‹€.

 

이 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄μ„œ λ‚˜λŠ” JUnit5 에 μ‘΄μž¬ν•˜λŠ” ParameterizedTest λ₯Ό μ΄μš©ν•˜μ˜€λ‹€.

 

ν…ŒμŠ€νŠΈ μ½”λ“œμ— νŒŒλΌλ―Έν„°λ₯Ό λ„£λŠ” 방법에 λŒ€ν•΄μ„œλŠ” ν•΄λ‹Ή λΈ”λ‘œκ·Έμ˜ @ParameterizeTest 둜 ν•˜λ‚˜μ˜ ν…ŒμŠ€νŠΈ μ½”λ“œμ—μ„œ μ—¬λŸ¬ 개의 νŒŒλΌλ―Έν„° ν…ŒμŠ€νŠΈν•˜κΈ° μ—μ„œ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€!

 

λ‚˜λŠ” 이λ₯Ό Enum νƒ€μž…μ„ λ§€κ°œλ³€μˆ˜λ‘œ λ°›λŠ” μ„œλΉ„μŠ€ λ‘œμ§μ— λŒ€ν•œ ν…ŒμŠ€νŠΈλ₯Ό ν•  λ•Œ μ‚¬μš©ν–ˆλ‹€.

 

ν”„λ‘œμ νŠΈμ—μ„œ μ‹€μ‹œκ°„ νŠΈλž˜ν”½μ„ νŠΉμ • 주기에 맞게 λ°˜ν™˜ν•΄μ•Ό ν•˜λŠ” μ½”λ“œκ°€ μžˆλŠ”λ°, 이 μƒν™©μ—μ„œ Test Codeλ₯Ό μ‹€ν–‰ν•˜λŠ” Method의 λ§€κ°œλ³€μˆ˜λ‘œ λ°›μ•„μ„œ μ²˜λ¦¬ν–ˆλ‹€.

 

class TrafficServiceTest {

    private TrafficService trafficService;

    private final TotalRepository totalRepository = mock(TotalRepository.class);
    private final AbnormalRepository abnormalRepository = mock(AbnormalRepository.class);

    @BeforeEach
    void init() {

        trafficService = new TrafficService(totalRepository, abnormalRepository);

        given(totalRepository.countByTimestampBetween(any(LocalDateTime.class), any(LocalDateTime.class)))
                .willReturn(12327L);

        given(abnormalRepository.countByTimestampBetween(any(LocalDateTime.class), any(LocalDateTime.class)))
                .willReturn(417L);
    }

    @ParameterizedTest
    @DisplayName("μ‹œκ°„ νƒ€μž…(5λΆ„) κ°€ 주어지면 8 개의 dto λ°˜ν™˜ - 성곡")
    @MethodSource("paramsForGetTrafficCountByPeriodWithValid")
    void getTrafficBetween_valid(PeriodType periodType) {
        List<TotalTrafficResponse> response = trafficService
                .getTrafficCountByPeriod(periodType);

        TotalTrafficResponse firstTotalResponseData = response.get(0);

        assertAll(
                () -> assertEquals(8, response.size()),
                () -> assertEquals(1, firstTotalResponseData.getId()),
                () -> assertEquals(12327L, firstTotalResponseData.getCount()),
                () -> assertNotNull(firstTotalResponseData.getTimestamp())
        );
    }
    private static Stream<Arguments> paramsForGetTrafficCountByPeriodWithValid() {
        return Stream.of(
            Arguments.of(PeriodType.WEEK),
            Arguments.of(PeriodType.DAY),
            Arguments.of(PeriodType.FIVE_MINUTE)
        );
    }
}

paramasForGetTrafficCountByPeriodWithValid λ©”μ„œλ“œμ—μ„œ 각각 주기에 λ§žλŠ” Enum νƒ€μž…μ„ λ°˜ν™˜ν•˜κ³ ,

 

getTrafficBetween_valid μ—μ„œ ν•΄λ‹Ή νŒŒλΌλ―Έν„°λ₯Ό 받도둝 μ½”λ“œλ₯Ό μž‘μ„±ν•΄μ„œ 3개의 λ™μΌν•œ ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό ν•˜λ‚˜λ‘œ 쀄이도둝 ν–ˆλ‹€.

 

이런 λ°©μ‹μœΌλ‘œ 쑰금의 가독성을 μ±™κ²Όκ³ , λ‚΄κ°€ 자주 μ• μš©ν•˜λŠ” 방법이기도 ν•˜λ‹ˆ 이런 고민이 μžˆλŠ” μ‚¬λžŒλ“€μ—κ²ŒλŠ” κΌ­ μΆ”μ²œν•˜λŠ” 방법이닀.

 

Coverage

 

λ‚΄κ°€ κ΅¬ν˜„ν•΄μ•Όν•  API λͺ…μ„ΈλŠ” λ‹€μŒκ³Ό κ°™μ•˜λ‹€.

 

 

API듀을 κ΅¬ν˜„ν•  λ•Œ, λ‚˜λ§Œμ˜ 약속이 μžˆλŠ”λ° 그것은 λ°”λ‘œ 컀버리지λ₯Ό 일정 μˆ˜μ€€ μ΄μƒμœΌλ‘œ κΌ­ λ§Œμ‘±μ‹œν‚€λŠ” 것이닀.

 

λ‚˜λŠ” 기쀀을 νŠΉλ³„ν•œ κ²½μš°κ°€ μ•„λ‹ˆλΌλ©΄ μ½”λ“œ 컀버리지 90% μ΄μƒμœΌλ‘œ μž‘λŠ”λ‹€.

 

100%도 ν•  수 μžˆλ‹€λ©΄ μ’‹κ² μ§€λ§Œ μ•½κ°„μ˜ Tradeoffκ°€ μžˆλŠ”λ“― ν•˜λ‹€.

 

 

예λ₯Ό λ“€λ©΄ 둬볡에 κ΄€ν•œ μ½”λ“œλ‚˜, DTO에 κ΄€ν•œ λͺ¨λ“  ν…ŒμŠ€νŠΈλ₯Ό λ§Œμ‘±μ‹œν‚€λ©΄ μ’‹κ² μ§€λ§Œ, 생산성을 보자면 90%κ°€ λ‚˜μ—κ²ŒλŠ” μ ν•©ν–ˆκ³ , 만쑱슀러운 μˆ˜μ€€μ΄μ—ˆλ‹€.

 

Backend λ₯Ό κ°œλ°œν•˜λ©° λŠλ‚€ 후기와 감정

 

μ΄λ²ˆμ— λ‚΄κ°€ 맑은 APIλŠ” 사싀 크게 μ–΄λ €μš΄ 뢀뢄이 μ‘΄μž¬ν•˜μ§€ μ•Šμ•˜λ‹€.

 

λ¬Έμ œκ°€ μžˆλ‹€λ©΄ λ‚΄κ°€ λ„ˆλ¬΄ λ§Žμ€ 뢀뢄을 λ§‘μ•˜λ˜ 것... 이라고 ν•˜κ³ μ‹Άλ‹€.

 

Front Back, 그리고 인프라와 λ°°ν¬κΉŒμ§€ λ‚΄κ°€ λ§‘μ•„λ²„λ €μ„œ 더 멋지고 μš°μ•„ν•œ μ½”λ“œλ₯Ό μ§œλŠ”λ°μ— 신경을 많이 쓰지 λͺ»ν–ˆλ‹€.

 

κ·Έλž˜λ„ λ‚΄κ°€ 정해놓은 λ‚˜λ¦„μ˜ μˆ˜μ€€μ€ λ§Œμ‘±ν•˜λŠ” ν”„λ‘œμ νŠΈμ§€λ§Œ 이 ν”„λ‘œμ νŠΈμ—μ„œ μ•„μ‰¬μš΄ 뢀뢄을 꼽자면 2개 μ •λ„λ‘œ 말할 수 μžˆλ‹€.

 

  1. active profile λ₯Ό μ‚¬μš©ν•˜μ§€ λͺ»ν•œ 것
  2. Between 연산을 κ°œμ„ ν•˜μ§€ λͺ»ν•œ 것

 

active profile λ₯Ό μ‚¬μš©ν•˜μ§€ λͺ»ν•œ 것

 

ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό μž‘μ„±ν•  λ•Œ, λ‚˜λŠ” H2 DBλ₯Ό μ‚¬μš©ν•΄μ„œ λ©”λͺ¨λ¦¬μ—μ„œ λŒμ•„κ°€κ²Œ ν•΄μ„œ ν…ŒμŠ€νŠΈλ₯Ό ν•˜κ³  μ‹€μ œ Production ν™˜κ²½μ—μ„œλŠ” Mysql 이 κ΅¬μ„±λ˜μ–΄ μžˆμ—ˆλ‹€.

 

μ˜€λŠ˜μ„ μœ„ν•΄μ„œ Spring Boot 의 Active Profiles 에 λŒ€ν•΄μ„œ 곡뢀λ₯Ό ν–ˆμ—ˆλŠ”λ°, μ‹œκ°„μ΄ λ„ˆλ¬΄ μ΄‰λ°•ν•œ λ‚˜λ¨Έμ§€ DB μ„€μ •ν•˜λŠ” μ„€μ • νŒŒμΌμ—μ„œ 단지 μ£Όμ„μœΌλ‘œ Test ν™˜κ²½κ³Ό Production ν™˜κ²½μ„ λ‚˜λˆ΄λ‹€..

 

spring:
  # H2 μ½˜μ†” μ„€μ •
  h2:
    console:
      path: /h2-console
      enabled: true
      settings:
        web-allow-others: true
  # DB μ„€μ •
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:l7-monitor
    username: l7
    password:

  # jpa μ„€μ •
  jpa:
    hibernate:
      ddl-auto: create-drop
#  datasource:
#    driver-class-name: com.mysql.cj.jdbc.Driver
#    url: jdbc:mysql://localhost:3306/bob_l7?characterEncoding=utf8
#    username: root
#    password: 1111

 

κ·ΈλŸ¬λ‹€λ³΄λ‹ˆ λΉŒλ“œν•˜λŠ” κ³Όμ •μ—μ„œ Production ν™˜κ²½μ—μ„œλŠ” ν…ŒμŠ€νŠΈ μ½”λ“œκ°€ κΌ¬μ—¬μ„œ λΉŒλ“œκ°€ λ˜μ§€ μ•Šμ•˜κ³  κ²°κ΅­ gradle bootJar 둜 λΉŒλ“œλ₯Ό 해버렸닀..

 

Spring boot의 profiles 에 λŒ€ν•΄μ„œλŠ” ν•΄λ‹Ή λΈ”λ‘œκ·Έμ˜ Profile.active 을 μ΄μš©ν•˜μ—¬ λ‹€λ₯Έ μ„€μ • 정보(application.properties) 뢈러였기

 

λ§Œμ•½ λ‹€μŒμ— μ—¬μœ κ°€ λœλ‹€λ©΄ κΌ­ Active Profiles λ₯Ό μ‚¬μš©ν•˜λ € ν•œλ‹€.

 

Between 연산을 κ°œμ„ ν•˜μ§€ λͺ»ν•œ 것

 

Spring Data JPA μ—μ„œλŠ” λ‹€μ–‘ν•œ 쿼리 λ©”μ„œλ“œκΈ°λŠ₯을 μ§€μ›ν•˜λŠ”λ°, κ·Έ 쀑 ν•˜λ‚˜κ°€ λ°”λ‘œ λ©”μ„œλ“œ μ΄λ¦„μœΌλ‘œ 쿼리λ₯Ό λ§Œλ“œλŠ” κΈ°λŠ₯이 μžˆλ‹€.

 

λ‚΄ API μ—μ„œ 였늘의 νŠΈλž˜ν”½λŸ‰μ„ μ‹œκ°„λŒ€ λ³„λ‘œ μ•Œλ €μ£ΌλŠ” μ½”λ“œκ°€ μžˆμ—ˆλ‹€.

 

ν•΄λ‹Ή μ½”λ“œμ—μ„œλŠ” long countByTimestampBetween(LocalDateTime from, LocalDateTime to); μ΄λΌλŠ” 쿼리 λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•˜λŠ”λ°, 보닀싢이 ν•˜λ‚˜μ˜ μ‹œκ°„λŒ€λ§Œ 쑰회λ₯Ό ν•  수 있기 λ•Œλ¬Έμ— 8κ°œκ°€ ν•„μš”ν•˜λ‹€λ©΄ 총 8λ²ˆμ„ ν˜ΈμΆœν•˜κ²Œ λœλ‹€.

 

이λ₯Ό NameQuery λ₯Ό μ΄μš©ν•˜κ±°λ‚˜ λ‹€λ₯Έ 방법을 μ΄μš©ν•œλ‹€λ©΄ ν•˜λ‚˜μ˜ μΏΌλ¦¬μ—μ„œλ„ ν•  수 μžˆμ§€λ§Œ ν•˜μ§€ μ•Šμ•„ 아쉬움이 λ‚¨λŠ”λ‹€.

 

λŒ“κΈ€