Conversation
| plugins { | ||
| kotlin("jvm") version "1.9.23" apply false | ||
| id("org.springframework.boot") version "3.4.7" apply false | ||
| id("io.spring.dependency-management") version "1.1.7" apply false |
There was a problem hiding this comment.
settings.gradle.kts 에 plugin 을 정의하는것이 다소 생소합니다.
이렇게도 많이 사용하나요?
There was a problem hiding this comment.
말씀 주신 대로 일반적으로 plugins 블록은 build.gradle.kts에서 사용하는 것이 표준이며, settings.gradle.kts에서는 흔하지 않습니다.
현재 구조는 멀티모듈 구성을 고려해 플러그인 버전 정의를 중앙 집중화하려다 보니 적용하게 되었는데, 더 명확하게 구분해 build.gradle.kts로 옮기는 것이 좋을 것 같습니다.
대신에 version catalog라는 기능을 활용하면 모듈간의 통일된 버전 관리를 할 수 있다고 해서 학습한 뒤 적용해보겠습니다.
| import java.util.UUID; | ||
|
|
||
| @Component | ||
| public class TraceIdFilter extends OncePerRequestFilter { |
There was a problem hiding this comment.
둘 다 공통 기능을 분리하기 위한 도구지만, 적용 대상과 동작 시점이 다릅니다.
- 필터는 웹 요청이 컨트롤러에 도달하기 전에 서블릿 컨테이너 레벨에서 동작합니다.
- AOP는 스프링 빈의 메서드 실행 전후에 동작하는 로직 수준의 기능입니다.
| import java.io.IOException; | ||
| import java.util.UUID; | ||
|
|
||
| @Component |
There was a problem hiding this comment.
스프링 프레임워크에서 @Component는 스프링 빈(Bean)으로 등록하기 위해 사용합니다. 클래스 레벨의 어노테이션이며, 붙은 클래스는 자동으로 스프링의 ApplicationContext에 등록되어 관리됩니다.
| @NotNull HttpServletResponse response, | ||
| FilterChain filterChain) throws ServletException, IOException { | ||
| String traceId = UUID.randomUUID().toString(); | ||
| MDC.put("traceId", traceId); |
There was a problem hiding this comment.
사용자의 요청과 처리하는 스레드를 연결하여 볼 수 있다는 점에서 유의미합니다. traceId를 기준으로 해당 요청의 로그를 명확히 식별 및 추적할 수 있습니다.
MDC는 스레드 로컬 기반이므로 해당 스레드에서만 접근 가능합니다. 즉, traceId를 MDC에 저장하면 이 요청을 처리하는 모든 로직에서 찍히는 로그가 자동으로 같은 tracdId를 포함할 수 있습니다.
.../shared-config/src/main/java/com/polynomeer/shared/config/global/GlobalExceptionHandler.java
Show resolved
Hide resolved
| import java.net.URI; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| @RestControllerAdvice |
There was a problem hiding this comment.
@RestController는 두 가지 어노테이션을 합쳐 놓은 것입니다. 이 어노테이션이 붙은 컨트롤러 클래스는 다음 특징을 갖습니다. 뷰(View) 렌더링이 없고, REST API 개발에 최적화 되어있으며, 데이터 변환을 자동처리 해준다는 특징이 있습니다.
@RestController = @Controller + @ResponseBody@Controller
스프링 MVC에서 컨트롤러 클래스임을 표시하는 어노테이션입니다.
주로 HTTP 요청을 처리하고, 뷰(JSP, Thymeleaf 등)를 반환할 때 사용됩니다.@ResponseBody
메서드의 반환값을 HTTP 응답 본문(body)에 직접 담아 클라이언트에 전송합니다.
즉, 반환값이 JSON, XML, 문자열 등으로 변환되어 그대로 응답됩니다.
There was a problem hiding this comment.
@RestController 의 설명은 매우 흡족합니다만, @RestControllerAdvice 입니다.
.../shared-config/src/main/java/com/polynomeer/shared/config/global/GlobalExceptionHandler.java
Show resolved
Hide resolved
|
|
||
| @Override | ||
| public void addCorsMappings(CorsRegistry registry) { | ||
| registry.addMapping("/**") |
There was a problem hiding this comment.
모든 URL 경로에 CORS 설정을 적용하겠다는 의미입니다. 추후에 클라이언트용 API만 별도로 설정하는것을 고려해보겠습니다. registry.addMapping("/api/**")
There was a problem hiding this comment.
필요하다면 phase를 나누어 loca, default는 /** 으로 선언하고
dev, stage, production 과 같은 phase는 디테일한 설정을 해두는 것이 좋겠습니다.
만약, 사내 인프라에서 gate keeper가 있고, 스캐닝을 포함한 공격까지 방어를 보장하면 /**도 괜찮겠습니다.
|
|
||
| dependencies { | ||
| implementation("org.springframework.boot:spring-boot-starter") | ||
| implementation("org.springframework.boot:spring-boot-starter-web") |
There was a problem hiding this comment.
spring-boot-starter-web 가 무엇을 하는건가요?
| @Getter | ||
| public abstract class BaseException extends RuntimeException { | ||
| private final ErrorCode errorCode; | ||
| private final Map<String, Object> metadata; |
There was a problem hiding this comment.
왜 Map 으로 에러 메시지를 저장하시나요??
그렇다면 어떠한 key 를 갖는지 너무 불분명 해지는것 같은데요
다른 객체를 선언하지 않고 map 으로 정의하신 이유가 궁금합니다.
shared/shared-common/src/main/java/com/polynomeer/shared/common/error/TickerErrorCode.java
Show resolved
Hide resolved
shared/shared-common/src/main/java/com/polynomeer/shared/common/error/TickerErrorCode.java
Show resolved
Hide resolved
| import org.springframework.http.HttpStatus; | ||
|
|
||
| @AllArgsConstructor | ||
| public enum TickerErrorCode implements ErrorCode { |
There was a problem hiding this comment.
enum이 interface를 갖는다는 것은 여러 enum을 앞으로 만들 것으로 예상되는데 하나의 enum에서 모두 처리할 순 없나요?
| private final T data; | ||
| private final String message; | ||
|
|
||
| public CommonResponse(String code, T data, String message) { |
There was a problem hiding this comment.
생성자보단 함수를 만들어서 success(data), error(code, message) 같이 만들면 어떨까요?
There was a problem hiding this comment.
error를 ProblemDetail로 직접 응답하는 것으로 설계했기때문에, error 함수를 고려하지는 않았는데, 좀 더 고민해보겠습니다. 감사합니다.
|
어떤 이슈와 related 되었나요? |
app/app-api-ticker/build.gradle.kts
Outdated
| mavenCentral() | ||
| } | ||
|
|
||
| dependencies { |
There was a problem hiding this comment.
여기엔 shared-config가 왜 포함되지 않나요? 따로 config를 잡을 예정인가요?
There was a problem hiding this comment.
app-api-price만 우선적으로 실행해보아서 누락되었습니다. 현재 app-api-ticker 모듈 자체를 제외했으므로, 해당 사항에 대한 변경은 없습니다.
app/app-api-ticker/build.gradle.kts
Outdated
| dependencies { | ||
| implementation("org.springframework.boot:spring-boot-starter-web") | ||
| compileOnly("org.projectlombok:lombok") | ||
| developmentOnly("org.springframework.boot:spring-boot-devtools") |
There was a problem hiding this comment.
devtools 를 사용해보시면 알겠지만 Thymeleaf, FreeMarker 를 사용하지 않는다면 클래스로딩이 오히려 집중력을 떨어트릴 수 있는데요 어떠한 기능때문에 devtools를 사용하셨나요?
There was a problem hiding this comment.
멀티모듈로 구성한 프로젝트라서 부팅 속도가 쉽게 느려지곤 합니다. 따라서 devtools의 자동 재시작(Automatic Restart)기능을 활용하여 개발 생산성을 높이고자 했습니다. 그런데 devtools를 학습해보니 '클래스로더의 분리'라는 방식이 클래스패스의 충돌을 유발할 수 있다는 것을 알게 되었습니다. 따로 글로 정리하다보니 실효성이 많이 떨어지는 것 같습니다. 하지만 개인 프로젝트 특성상 devtools의 잠재적 문제를 인식하고 사용하는 것이 충분히 가능할 것이라고 판단했습니다. 우선, 사용해보면서 불편한 점과 그 원인을 학습해보겠습니다.
추가로, devtools는 핫 리로드 방식이 아니며, 이를 지원하는 도구가 몇 가지 있습니다. 먼저, JRebel은 핫 리로드의 표준이라 할 수 있으며, 상용에서도 사용가능할 정도로 정교한 핫 리로드를 지원합니다. 다만, 가격이 비싸서 개인이 쓰기에는 부담스럽다는 단점이 있습니다. 두번째로는 DCEVM (Dynamic Code Evolution VM) + HotswapAgent를 조합해서 사용하는 방법입니다. 사실상 JRebel의 무료 대안인데, JVM 패치가 필요하므로 설치가 조금 번거롭고, 일부 프레임워크와의 버전 호환성 문제가 존재합니다.
| implementation("org.springframework.boot:spring-boot-starter-data-jpa") | ||
| implementation("org.springframework.boot:spring-boot-starter-web") | ||
| compileOnly("org.projectlombok:lombok") | ||
| developmentOnly("org.springframework.boot:spring-boot-devtools") |
There was a problem hiding this comment.
Spring Batch 기반 배치 서버는 spring-boot-devtools의 주요 타겟이 아니라서 성능적 이득이 거의 없습니다. devtools가 제공하는 자동 재시작, Live Reload, 캐시 무효화 등의 기능이 발동할 상황 자체가 없습니다.
사용할 경우, 생산성 향상은 거의 없고, 오히려 안정성을 해칠 가능성이 있습니다. 예를 들면, JPA를 사용할 경우 Hibernate 캐시 문제가 발생할 수 있습니다. static이나 ThreadLocal로 JobContext를 쓰는 경우에도 충돌 가능성이 있습니다. 또한 Batch Listener, ItenProcessor, ItemWriter 등에서 클래스 로더가 다르면 ClassCastException이 발생합니다.
따라서 배치서버에서는 특히나 devtools를 사용하지 않는 것이 안전합니다.
|
|
||
| dependencies { | ||
| implementation("org.springframework.boot:spring-boot-starter-batch") | ||
| implementation("org.springframework.boot:spring-boot-starter-data-jpa") |
There was a problem hiding this comment.
timeseries 도 jpa로 이득을 볼 수 있나요??
batch에선 어떠한 DB에 대해 작업을 계획중이신가요?
There was a problem hiding this comment.
collector 배치에서 TimescaleDB로 직접 저장하는 구조입니다. 그리고 이 규모와 실시간성에 대한 요구가 커질수록 성능이 중요시됩니다. 이때 JPA를 사용하는 것은 무조건 성능상 불리합니다. 따라서 해당 의존성을 제거하고, JDBC나 JOOQ + Native를 사용하려고 합니다.
| dependencies { | ||
| implementation("org.springframework.boot:spring-boot-starter-batch") | ||
| implementation("org.springframework.boot:spring-boot-starter-data-jpa") | ||
| implementation("org.springframework.boot:spring-boot-starter-web") |
There was a problem hiding this comment.
Yahoo Finance API를 호출하기 위해서 필요하다고 생각했었는데, 필요없는 것으로 확인했습니다. 해당 의존성은 제거하겠습니다. MVC 관련 기능을 사용하는 것이 아니고, API를 클라이언트를 통해서 호출만 하면 됩니다.
따라서 WebClient, RestClient, RestTemplate 정도의 선택사항이 있습니다. WebClient는 WebFlux의 의존성에 포함되는데, WebFlux를 사용할 필요는 없다고 판단했습니다. collector에서는 RestClient를 사용하여 일괄 호출하는 구조로 작성하려고 합니다.
|
until this friday |
… subpackages - Removed separate shared-common and shared-config modules - Moved their contents into shared module under common/ and config/ packages - Simplified project structure for better maintainability
…dules into a single infra module
|
불필요한 부분을 제거했으므로 merge 하셔도 좋겠습니다. |
관련 이슈
Close #8
작업 내용 요약
공통 기능과 설정을 shared 모듈에 구성하고, common, config로 나누어서 작성합니다.