There are a total of 17 posts
2025-09-09
@Aspect
@Component
@Slf4j
public class PerformanceAspect {
@Around("@annotation(Monitored)")
public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
return result;
} finally {
long endTime = System.currentTimeMillis();
long elapsedTime = endTime - startTime;
log.info("Method: {}, Elapsed Time: {}ms",
joinPoint.getSignature().getName(), elapsedTime);
}
}
}
서비스 메서드java@Service
public class UserService {
@Monitored
@Transactional
public void createUser(UserCreateRequest request) {
List<User> users = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
users.add(new User("user" + i, "user" + i + "@example.com"));
}
userRepository.saveAll(users);
}
}
예상 vs 실제 측정 결과@Order 어노테이션이나 명시적인 우선순위 설정이 없다면, 프록시 생성 순서에 따라 실행 순서가 결정된다.@Order 미설정 시 실제 동작:@Transactional의 기본 Order: Ordered.LOWEST_PRECEDENCE (Integer.MAX_VALUE)Ordered.LOWEST_PRECEDENCE (동일한 값)@Transactional의 기본 Order 값은 Ordered.LOWEST_PRECEDENCE(최대값)로 설정되어 있다. 따라서 커스텀 AOP가 트랜잭션보다 먼저 시작하고 나중에 끝나도록 하려면 더 낮은 값을 설정해야 한다.java@Aspect
@Component
@Order(100) // 확실하게 order 를 설정
@Slf4j
public class PerformanceAspect {
@Around("@annotation(Monitored)")
public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
return result;
} finally {
long endTime = System.currentTimeMillis();
long elapsedTime = endTime - startTime;
log.info("Method: {}, Elapsed Time: {}ms",
joinPoint.getSignature().getName(), elapsedTime);
}
}
}
실행 순서 변경 후:@Transactional의 기본값: Ordered.LOWEST_PRECEDENCE (Integer.MAX_VALUE)@Aspect
@Component
@Order(100)
@Slf4j
public class PerformanceAspect {
// ... 성능 측정 로직
}
@Order(100) 설정으로 트랜잭션 커밋 시간까지 포함된 정확한 성능 측정이 가능해졌다.이제 HTTP response 시간과 유사한 측정값을 얻을 수 있어, 대용량 배치 처리 시 실제 소요 시간을 정확히 파악할 수 있게 되었다.결론AOP를 활용한 성능 측정 시 프록시 순서를 반드시 고려해야 한다.특히 대용량 데이터 처리나 배치 작업의 경우, 트랜잭션 커밋 시간이 전체 실행 시간에서 상당한 비중을 차지할 수 있다. 이 시간을 놓치면 실제 성능과 크게 다른 잘못된 측정값을 얻게 된다.html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}2025-08-15
int나 long 타입으로 밀리초나 초 단위로 설정했지만, 이는 가독성과 유지보수성 측면에서 여러 문제점을 가지고 있었다.Spring Boot에서 제공하는 Duration 타입을 활용하면 이러한 문제를 깔끔하게 해결할 수 있다.기존 방식의 문제점단위가 불명확한 설정# 이 값이 초인지 밀리초인지 명확하지 않음
my-app:
request:
timeout: 5000
// 코드에서도 단위를 추측해야 함
@Value("${my-app.request.timeout}")
private long timeout; // 5000이 5초인지 5000초인지?
단위 변환의 번거로움// 설정값이 초 단위일 때 밀리초로 변환
long timeoutInMillis = timeoutInSeconds * 1000;
// 다양한 단위로 변환할 때마다 계산 필요
long timeoutInMinutes = timeoutInSeconds / 60;
Duration을 사용한 해결책명확한 단위 표기my-app:
request:
timeout: 10s # 10초
# timeout: 5000ms # 5000밀리초
# timeout: 2m # 2분
# timeout: 1h # 1시간
Duration은 다음과 같은 단위를 지원한다:ns - 나노초us - 마이크로초ms - 밀리초s - 초m - 분h - 시간d - 일package com.example.demo.config;
import java.time.Duration;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("my-app.request")
public record TimeoutConfiguration(
Duration timeout
) {
public TimeoutConfiguration {
if(timeout == null) {
timeout = Duration.ofSeconds(10);
}
if (timeout.isNegative()) {
throw new IllegalArgumentException("Timeout must be positive");
}
}
}
Service에서 활용하기package com.example.demo.service;
import com.example.demo.config.TimeoutConfiguration;
import java.time.Duration;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class MyService {
private final TimeoutConfiguration timeoutConfiguration;
public void doSomething() {
Duration timeout = timeoutConfiguration.timeout();
log.info("Doing something with timeout. {}ms, {}s, {}h",
timeout.toMillis(),
timeout.getSeconds(),
timeout.toHours());
}
}
다양한 단위로 변환Duration 객체는 편리한 변환 메서드들을 제공한다:Duration timeout = Duration.ofSeconds(30);
long millis = timeout.toMillis(); // 30000
long seconds = timeout.getSeconds(); // 30
long minutes = timeout.toMinutes(); // 0
long hours = timeout.toHours(); // 0
html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}2025-07-28

spring-boot-3

spring-boot-4
@Nullable: null이 될 수 있는 값임을 명시@NonNull: null이 될 수 없는 값임을 명시 (보통 기본값)@NullMarked: 클래스나 패키지 단위로 null safety 규칙 적용@NullUnmarked: null safety 검사에서 제외@NullMarked // 이 어노테이션 하나로 클래스 전체에 null safety 적용
public class MemberService {
// 모든 매개변수와 반환값이 기본적으로 @NonNull
public String process(String input) {
return input.toUpperCase(); // input은 null이 될 수 없음이 보장
}
// 명시적으로 @Nullable을 선언해야 null 허용
public String processNullable(@Nullable String input) {
return input != null ? input.toUpperCase() : "";
}
}
✅ Gradle 설정build.gradle.kts 예시kotlinimport net.ltgt.gradle.errorprone.CheckSeverity
import net.ltgt.gradle.errorprone.errorprone
plugins {
java
id("org.springframework.boot") version "4.0.0-M1"
id("io.spring.dependency-management") version "1.1.7"
id("net.ltgt.errorprone") version "4.2.0"
}
group = "org.example"
version = "0.0.1-SNAPSHOT"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
configurations {
compileOnly {
extendsFrom(configurations.annotationProcessor.get())
}
}
repositories {
mavenCentral()
// Spring 마일스톤 버전을 위한 리포지토리
maven { url = uri("https://repo.spring.io/milestone") }
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
errorprone("com.uber.nullaway:nullaway:0.12.7")
errorprone("com.google.errorprone:error_prone_core:2.38.0")
}
tasks.withType<Test> {
useJUnitPlatform()
}
tasks.withType<JavaCompile>().configureEach {
options.errorprone {
disableAllChecks.set(true)
check("NullAway", CheckSeverity.ERROR)
option("NullAway:AnnotatedPackages", "org.example")
}
}
✅ IDE 도움 받기
controller

service
sample/src/main/java/org/example/nullablesample/member/MemberController.java:22: error: [NullAway] passing @Nullable parameter 'null' where @NonNull is required
memberService.createMember(null);
^
(see http://t.uber.com/nullaway )
Objects.requireNonNull() 등을 사용한 명시적인 런타임 Null 체크를 통해 방어적으로 코드를 작성해야 한다.2025-06-29
net.devh:grpc-spring-boot-starter 기반으로 구성하는 샘플이 많지만, IntelliJ에서 Spring Boot 프로젝트를 새로 생성해보니 Spring에서 공식으로 제공하는 spring-grpc starter가 있어 이를 이용해보았다. 아직 버전은 1.0.0 미만이지만, 사용해보기에 충분했고 설정도 비교적 단순했다.설정 관련 내용은 GitHub 샘플 코드에 정리해두었다.
이 글에서는 proto 작성부터 서비스 구현까지의 과정을 간단히 정리한다.
syntax = "proto3";
package hello;
option java_multiple_files = true;
option java_package = "org.example.springgrpc.helloservice.proto";
service HelloService {
rpc SayHello(HelloRequest) returns (HelloReply);
rpc StreamHello(HelloRequest) returns (stream StreamHelloReply);
rpc SayBye(ByeRequest) returns (ByeReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
repeated string message = 1;
string byeMessage = 2;
}
message ByeRequest {
string name = 1;
}
message ByeReply {
string message = 1;
}
message StreamHelloReply {
string message = 1;
}
QueryDSL과 마찬가지로 .proto 파일을 기반으로 Java 코드가 자동 생성된다.package는 네임스페이스처럼 동작하며, 실제 호출 시 hello.HelloService.SayHello처럼 전체 경로가 붙는다.service는 gRPC에서 제공하는 함수 목록이고, 각 함수의 입력과 반환 타입을 지정한다.stream 키워드는 스트리밍 응답을 의미한다. 연결이 유지된 채로 여러 데이터를 연속으로 받을 수 있다.message는 요청/응답 데이터 구조이며, repeated는 리스트 형태를 의미한다..proto 파일을 작성한 후 generateProto task를 실행하면 Java 코드가 자동으로 생성된다.java_package에 지정한 패키지 기준이다.@Configuration
public class GrpcClientsConfig {
@Bean
HelloServiceGrpc.HelloServiceBlockingStub stub(GrpcChannelFactory channels) {
return HelloServiceGrpc.newBlockingStub(channels.createChannel("local"));
}
@Bean
HelloServiceGrpc.HelloServiceFutureStub asyncStub(GrpcChannelFactory channels) {
return HelloServiceGrpc.newFutureStub(channels.createChannel("local"));
}
}
GrpcChannelFactory를 통해 설정한 채널 이름(local)을 기준으로 채널을 생성한다. (application.yml 참고)BlockingStub, FutureStub 중 하나를 선택해 Bean으로 등록할 수 있다.@Slf4j
@GrpcService
@RequiredArgsConstructor
public class HelloService extends HelloServiceGrpc.HelloServiceImplBase {
private final HelloServiceBlockingStub stub;
private final HelloServiceFutureStub asyncStub;
@Override
public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
ListenableFuture<ByeReply> byeFuture = asyncStub.sayBye(
ByeRequest.newBuilder()
.setName(request.getName())
.build());
log.info("stream request");
Iterator<StreamHelloReply> streamHello = stub.streamHello(request);
log.info("stream response");
try {
ByeReply byeReply = byeFuture.get();
Builder builder = HelloReply.newBuilder()
.setByeMessage(byeReply.getMessage());
// streamHello Iterator의 각 StreamHelloReply에서 message를 추출하여 추가
while (streamHello.hasNext()) {
log.info("streamHello.hasNext()");
StreamHelloReply reply = streamHello.next();
builder.addMessage(reply.getMessage());
}
log.info("test");
HelloReply helloReply = builder.build();
log.info("HelloReply: {}", helloReply);
responseObserver.onNext(helloReply);
responseObserver.onCompleted();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
@Override
public void streamHello(HelloRequest request,
StreamObserver<StreamHelloReply> responseObserver) {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
responseObserver.onNext(StreamHelloReply.newBuilder()
.setMessage("Hello " + request.getName() + " " + i)
.build());
}
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
responseObserver.onCompleted();
}
@Override
public void sayBye(ByeRequest request, StreamObserver<ByeReply> responseObserver) {
responseObserver.onNext(ByeReply.newBuilder()
.setMessage("Bye " + request.getName())
.build());
responseObserver.onCompleted();
log.info("bye");
}
}
@GrpcService는 gRPC 서버로 등록되기 위한 어노테이션이다.
(@Service만 써도 되지만, 명시적으로 @GrpcService를 쓰면 가독성 측면에서 더 낫다.)sayHello 메서드에서는 FutureStub으로 sayBye를 비동기 호출하고, BlockingStub으로 streamHello를 호출하여 응답을 조합한다.stub.streamHello(request)에서 blocking이 걸릴 줄 알았는데, 실제로는 채널만 열리고, .next()로 값을 꺼낼 때 blocking이 발생한다. (다시 생각해보면 당연한 흐름이다.streamHello는 2초 간격으로 메시지를 보내는 서버 스트리밍 처리 예제다.sayBye는 단순한 unary 응답을 처리하며, 비동기로 구현했다.spring-grpc 덕분에 설정도 복잡하지 않았고, proto 정의 → 코드 생성 → 서버/클라이언트 구성 → 실행까지 한 번에 정리할 수 있었다.2025-06-01
implementation("org.springframework.kafka:spring-kafka")
implementation("org.springframework.boot:spring-boot-starter-actuator")
runtimeOnly("io.micrometer:micrometer-registry-prometheus")
implementation("io.micrometer:micrometer-tracing-bridge-otel")
implementation("io.opentelemetry:opentelemetry-exporter-otlp")
implementation("com.github.loki4j:loki-logback-appender:1.6.0")
implementation("net.ttddyy.observation:datasource-micrometer-spring-boot:1.1.1")
actuator: 메트릭 및 tracing endpoint 노출micrometer-registry-prometheus: Prometheus 메트릭 export 지원micrometer-tracing-bridge-otel: Micrometer 트레이싱 API와 OpenTelemetry SDK 연결opentelemetry-exporter-otlp: 수집된 trace 데이터를 OTLP 포맷으로 Collector/Tempo로 전송loki-logback-appender: 로그를 Loki로 전송할 수 있는 Logback용 appenderdatasource-micrometer-spring-boot:1.1.1: database 까지 trace된다면 좋을 것 같아서 찾아보니, 이런 라이브러리가 있어서 추가해 보았다. (ttddyy 이분 어디서 봤나 했더니 datasource-proxy 라이브러리 만드신분이다. )spring:
application:
name: order-service
kafka:
bootstrap-servers: localhost:9092
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
consumer:
group-id: order-consumer
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
template:
observation-enabled: true
listener:
observation-enabled: true
management:
tracing:
sampling:
probability: 1.0
enabled: true
otlp:
tracing:
endpoint: http://localhost:4317
transport: grpc
observation-enabled: true 설정만으로 메시지 송신/수신 시 자동 적용.<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<springProperty scope="context" name="application" source="spring.application.name"/>
<appender name="LOKI" class="com.github.loki4j.logback.Loki4jAppender">
<batchTimeoutMs>1000</batchTimeoutMs>
<http>
<url>http://${LOKI_HOST:-localhost}:${LOKI_PORT:-3100}/loki/api/v1/push</url>
</http>
<format>
<label>
<pattern>service_name=${application},host=${HOSTNAME},level=%level</pattern>
<structuredMetadataPattern>
level = %level,
thread = %thread,
class = %logger,
traceId = %mdc{traceId:-none},
spanId = %mdc{spanId:-none}
</structuredMetadataPattern>
</label>
<message>
<pattern>${FILE_LOG_PATTERN}</pattern>
</message>
<sortByTime>true</sortByTime>
</format>
</appender>
<root level="INFO">
<appender-ref ref="LOKI" />
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
2.4 기타 인프라 구성요소@RestController
@RequiredArgsConstructor
@Slf4j
public class TestController {
private final KafkaTemplate<String, String> kafkaTemplate;
private final TeamRepository teamRepository;
@GetMapping("/test")
public void sendTestMessage() {
kafkaTemplate.send("my-topic", "Hello World!")
.thenAccept(o -> log.info("Sent message:{}", o.toString()));
}
}
@Component
@Slf4j
@RequiredArgsConstructor
public class MyConsumer {
private final TeamService teamService;
@KafkaListener(topics = "my-topic")
public void listen(String message, Acknowledgment acknowledgment) {
log.info("Received Message: {}", message);
teamService.createTeam(new Team(message));
acknowledgment.acknowledge();
}
}

traceid-table

trace-span

trace-log
ea2cab37... 를 기준으로 데이터가 어떻게 흘러가는지 파악 할 수 있다.2025-05-17
"level": "error" 같은 값을 기준으로 로그를 검색하려면, 단순 텍스트 로그보다 구조화된 JSON 로그가 훨씬 유리하다.
보다 안정적이고 일관된 로그 구조를 만들기 위해 JSON 로그 포맷을 본격적으로 검토하였고, 그 과정에서 Spring Boot 3.4에서 새롭게 추가된 structured logging 기능을 알게 되었다.📦 사전 준비3.4.0 이상spring-boot-starter-log4j2org.springframework.boot.logging.structured.StructuredLogFormatter<T>JsonWriter<T> 유틸리티도 함께 제공한다.🛠 Log4j2 기반 커스텀 포맷터 구현다음은 LogEvent를 기반으로 JSON 로그를 생성하는 커스텀 포맷터 클래스 구현 예시다:package org.example.jsonloggingsmple.config.log;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import org.apache.logging.log4j.core.LogEvent;
import org.springframework.boot.json.JsonWriter;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
public class MyStructuredLoggingFormatter implements StructuredLogFormatter<LogEvent> {
private final JsonWriter<LogEvent> writer = JsonWriter.<LogEvent>of((members) -> {
members.add("time", event -> {
Instant javaInstant = Instant.ofEpochMilli(event.getInstant().getEpochMillisecond());
return LocalDateTime.ofInstant(javaInstant, ZoneId.systemDefault());
});
members.add("level", LogEvent::getLevel);
members.add("marker", LogEvent::getMarker).whenNotNull();
members.add("thread", LogEvent::getThreadName);
members.add("message", (event) -> event.getMessage().getFormattedMessage());
}).withNewLineAtEnd();
@Override
public String format(LogEvent event) {
return this.writer.writeToString(event);
}
}
logging:
structured:
format:
console: org.example.jsonloggingsmple.config.log.MyStructuredLoggingFormatter
config: classpath:log4j2-spring.xml
📄 로그 출력 예시{"time":"2025-05-21T23:54:07.368","level":"WARN","thread":"http-nio-8080-exec-1","message": "hi."}
📚 참고 자료2025-04-20
val queryDslVersion = "6.11"
implementation("io.github.openfeign.querydsl:querydsl-core:${queryDslVersion}")
implementation("io.github.openfeign.querydsl:querydsl-jpa:$queryDslVersion")
annotationProcessor("io.github.openfeign.querydsl:querydsl-apt:$queryDslVersion:jpa")
3. maven 설정 방법<dependency>
<groupId>io.github.openfeign.querydsl</groupId>
<artifactId>querydsl-core</artifactId>
<version>6.11</version>
</dependency>
<dependency>
<groupId>io.github.openfeign.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>${openfeign.querydsl.version}</version>
</dependency>
<dependency>
<groupId>io.github.openfeign.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${openfeign.querydsl.version}</version>
<classifier>jpa</classifier>
<scope>compile</scope>
</dependency>
2025-04-10
/ (슬래시). (점)+ (플러스)* (애스터리스크)# (해시)# (해시)version: '3.8'
services:
rabbitmq:
image: rabbitmq:3.12-management
container_name: rabbitmq
ports:
- "5672:5672" # AMQP
- "15672:15672" # 관리 콘솔
- "1883:1883" # MQTT
environment:
- RABBITMQ_DEFAULT_USER=guest
- RABBITMQ_DEFAULT_PASS=guest
volumes:
- ./rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf
command: >
sh -c "
rabbitmq-plugins enable --offline rabbitmq_mqtt &&
rabbitmq-server
"
rabbitmq_mqtt 플러그인을 활성화해야 한다.1883번이며, 이를 통해 메시지를 송수신한다.test/topic/A 형태의 토픽으로 발행되며,test.topic.A 라우팅 키로 변환하여 처리한다.amq.topic, Queue는 test.queue로 설정한다.@Configuration
class RabbitMqConfig {
companion object {
const val QUEUE_NAME = "test.queue"
const val EXCHANGE_NAME = "amq.topic"
const val ROUTING_KEY = "test.topic.A"
}
@Bean
fun testQueue(): Queue {
return Queue(QUEUE_NAME, true)
}
@Bean
fun topicExchange(): TopicExchange {
return TopicExchange(EXCHANGE_NAME)
}
@Bean
fun binding(queue: Queue, exchange: TopicExchange): Binding {
return BindingBuilder
.bind(queue)
.to(exchange)
.with(ROUTING_KEY)
.noargs()
}
}
@Component
class MqttAmqpConsumer {
@RabbitListener(queues = ["test.queue"])
fun receive(message: String) {
println("메시지 수신: $message")
}
}
topic 타입으로 설정하고 test.topic.A와 test.queue를 바인딩한다.AMQP를 사용하는 Consumer에서 test.queue를 바라보고 있으면 MQTT -> AMQP 메시지 전송이 가능하다.amq.topic exchange에 발행하고,.(dot) 형식으로 작성해야 한다.🧑💻샘플코드https://github.com/deuk9/mqtt-amqp-integration.html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}2025-04-02
spring:
jpa:
properties:
hibernate:
session:
events:
log:
LOG_QUERIES_SLOWER_THAN_MS: 1
hibernate:
ddl-auto: update
logging:
level:
org:
hibernate:
SQL: debug
SQL_SLOW: info
🖌️ 로그 결과일부러 1ms 를 설정하여 로그를 남겼다.org.hibernate.SQL_SLOW : Slow query took 3 millisecondsselect m1_0.id,m1_0.name from member m1_0 where m1_0.id=?html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}2025-02-15
@WebMvcTest를 활용하여 Spring MVC 동작을 검증한다.📌 테스트 코드 작성@WebMvcTest(controllers = {CoffeeController.class})
public abstract class ControllerTestSupport {
@Autowired
protected MockMvc mockMvc;
protected ObjectMapper objectMapper = new ObjectMapper();
@MockitoBean // MockBean으로 Service 레이어를 Mocking
protected CoffeeService coffeeService;
}
class CoffeeControllerTest extends ControllerTestSupport {
@Test
@DisplayName("유효한 요청으로 커피를 생성한다")
void create_createsCoffee_whenValidRequest() throws Exception {
//given
CoffeeCreateRequest request = new CoffeeCreateRequest("Latte", 4000);
//then
mockMvc.perform(post("/coffees")
.contentType("application/json")
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andDo(document("coffees/create",
requestFields(
fieldWithPath("name")
.type(STRING)
.description("The name of the coffee"),
fieldWithPath("price")
.type(NUMBER)
.description("The price of the coffee")
)));
}
}
✅ Controller 테스트 시 고려할 사항@WebMvcTest는 Controller 계층만 로드하기 때문에, Service 레이어는 @MockitoBean을 이용하여 Mocking해야 한다. (@MockBean 은 deprecated 됨)@WebMvcTest를 붙이면 Spring Context가 매번 재생성되므로, Support 클래스를 상속하여 성능을 최적화한다.@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfiguration {
@Bean
@ServiceConnection
PostgreSQLContainer<?> postgresContainer() {
System.out.println("Creating container");
return new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest"))
.withReuse(true);
}
}
@Import(TestcontainersConfiguration.class)
@SpringBootTest
@ActiveProfiles("test")
public abstract class IntegrationTestSupport {
}
📌 Service 레이어 테스트 코드@Transactional
class CoffeeServiceTest extends IntegrationTestSupport {
@Autowired
CoffeeService coffeeService;
@Test
@DisplayName("유효한 요청일 때 커피를 저장한다")
void create_savesCoffee_whenValidRequest() {
//given
CoffeeCreateRequest request = new CoffeeCreateRequest("Latte", 4000);
//when
coffeeService.create(request);
assertThat(coffeeService.getByName("Latte"))
.isNotNull()
.extracting("name", "price")
.containsExactly("Latte", 4000);
}
}
✅ Service 테스트 시 고려할 사항@SpringBootTest를 명시하고, 해당 클래스를 상속받아 테스트를 작성한다.@Transactional
class CoffeeRepositoryTest extends IntegrationTestSupport {
@Autowired
CoffeeRepository coffeeRepository;
@Test
@DisplayName("이름으로 커피를 조회할 때 커피가 존재하면 반환한다")
void findByName_returnsCoffee_whenCoffeeExists() {
//given
Coffee coffee = Coffee.create(new CoffeeCreateRequest("Latte", 4000));
coffeeRepository.save(coffee);
//when
Optional<Coffee> foundCoffee = coffeeRepository.findByName("Latte");
//then
assertThat(foundCoffee)
.isPresent()
.get()
.extracting("name", "price")
.containsExactly("Latte", 4000);
}
}
✅ Repository 테스트 시 고려할 사항JpaRepository에서 직접 추가한 메서드나 QueryDSL을 검증하는 용도로 활용한다.@Transactional을 사용하여 각 테스트가 끝난 후 자동으로 데이터가 롤백되도록 한다.2025-02-14
3.1 이후 버전부터는 더욱 간편하게 사용할 수 있게 변경되었기에, 이에 대한 내용을 정리하고자 한다1. Testcontainers란?Testcontainers는 통합 테스트를 위해 Docker 컨테이너를 사용하여 데이터베이스, 메시지 브로커, 외부 API 등을 손쉽게 실행하고 관리할 수 있는 Java 라이브러리다. 실제와 같은 인프라 서비스를 컨테이너로 띄워 테스트 환경을 구축할 수 있다.2. 의존성 추가Gradle을 사용하는 경우, build.gradle.kts에 다음과 같이 의존성을 추가한다.dependencies {
testImplementation("org.springframework.boot:spring-boot-testcontainers")
testImplementation("org.testcontainers:junit-jupiter")
testImplementation("org.testcontainers:postgresql")
}
PostgreSQL + JPA 테스트를 위해 postgresql 기반 의존성을 추가했다.3. 설정하기Testcontainers 설정 클래스 작성@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfiguration {
@Bean
@ServiceConnection
PostgreSQLContainer<?> postgresContainer() {
System.out.println("Creating container");
return new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest"))
.withReuse(true);
}
}
테스트 지원 클래스 작성@Import(TestcontainersConfiguration.class)
@SpringBootTest
@ActiveProfiles("test")
public abstract class IntegrationTestSupport {
}
주요 설정 설명PostgreSQLContainer를 Spring Bean으로 등록한다.@ServiceConnection을 사용하면 자동으로 JDBC URL, 데이터베이스 정보, 사용자 정보를 Spring DataSource에 매핑해준다.@ServiceConnection을 사용하면 이를 자동화할 수 있다.withReuse(true)를 설정하면 컨테이너를 재사용하여 테스트 속도를 향상시킬 수 있다. 이를 설정하지 않으면 매 테스트마다 컨테이너가 생성되고 삭제되므로 속도가 느려진다.@Transactional
class CoffeeServiceTest extends IntegrationTestSupport {
@Autowired
CoffeeService coffeeService;
@Test
@DisplayName("유효한 요청일 때 커피를 저장한다")
void create_savesCoffee_whenValidRequest() {
// given
CoffeeCreateRequest request = new CoffeeCreateRequest("Latte", 4000);
// when
coffeeService.create(request);
// then
assertThat(coffeeService.getByName("Latte"))
.isNotNull()
.extracting("name", "price")
.containsExactly("Latte", 4000);
}
}
주요 테스트 전략CoffeeService의 비즈니스 로직을 검증할 때, 실제 DB에 연결하여 테스트한다.@Transactional을 추가하여 테스트가 끝나면 자동으로 rollback되도록 한다.
2025-01-10
dockerswarm_sd_configs를 사용하면 Swarm 내부의 변화에 따라 자동으로 타겟을 갱신할 수 있습니다.Prometheus 설정 (prometheus.yml)아래 설정을 적용하면 Docker Swarm의 노드, 태스크, 서비스를 자동으로 검색하고 메트릭을 수집할 수 있습니다.scrape_configs:
- job_name: "swarm-nodes"
dockerswarm_sd_configs:
- host: tcp://localhost:2375
role: nodes
port: 9100
- job_name: "swarm-tasks"
dockerswarm_sd_configs:
- host: tcp://localhost:2375
role: tasks
metrics_path: "/actuator/prometheus"
- job_name: "swarm-services"
dockerswarm_sd_configs:
- host: tcp://localhost:2375
role: services
dockerswarm_sd_configs에서 role 설명nodes: Swarm 클러스터의 각 노드에서 실행되는 Exporter (예: Node Exporter)를 대상으로 메트릭 수집services: Swarm 서비스 단위로 메트릭을 수집하지만, 로드 밸런싱 문제로 인해 특정 컨테이너의 데이터를 지속적으로 수집하는 것이 어렵습니다.tasks: 개별 컨테이너 단위로 모니터링 (예: Spring Actuator 활용)nodesNode Exporter와 같은 모니터링 컨테이너를 감지하여 메트릭을 수집합니다.servicestasks를 활용하는 것이 더 적합합니다.taskstasks는 개별 컨테이너의 메트릭을 수집하는 방식으로, Spring Boot Actuator 등과 연계하여 효과적으로 활용할 수 있습니다.tasks의 포트 설정을 별도로 지정할 수 없습니다.10.x.x.x)와publish port로 접근하지만, 내부 네트워크에서는 target port로 접근해야 정상적으로 데이터를 수집할 수 있습니다.docker run -p 8080:8080처럼publish port와 inner port를 동일하게 설정해야 정상적인 모니터링이 가능합니다.2025-01-03
- job_name: 'http-sd'
scheme: http
http_sd_configs:
- url: http://localhost:8080/api/targets
refresh_interval: "15s"
위 설정에서 scheme을 http로 설정하고, url을 지정하면 Prometheus가 주기적으로 해당 URL을 호출하여 모니터링 대상을 갱신한다. refresh_interval을 통해 갱신 주기를 설정할 수 있다. 위 설정에서는 15초마다 http://localhost:8080/api/targets를 호출하여 최신 모니터링 타겟을 가져오도록 구성했다.HTTP Service Discovery 응답 구조HTTP SD는 아래와 같은 JSON 응답을 통해 동적으로 모니터링 대상을 설정할 수 있다.[
{
"targets": [
"localhost:9100"
],
"labels": {
"job": "node-exporter",
"ip": "localhost",
"application": "my-application"
}
}
]
targets: 모니터링할 대상의 목록을 정의한다.labels: 각 타겟에 대한 추가 정보를 제공하며, Prometheus의 검색 조건이나 대상 식별값으로 활용할 수 있다.2024-01-29
if, when, 반복문(for), 예외 처리, 함수의 정의와 특징을 알아본다.1. if문을 Expression으로 활용하기코틀린에서는 if가 표현식(Expression)으로 사용될 수 있다. Java의 삼항 연산자(?:)는 존재하지 않는다.fun validateScore(score: Int): String = if (score >= 90) "a" else "b"
2. 범위 연산자(in, !in)범위를 명확하게 나타낼 때 사용한다.fun validateScore(score: Int) {
if (score in 0..100) {
println("0과 100 사이입니다.")
}
if (score !in 0..100) {
println("0과 100 사이가 아닙니다.")
}
}
3. when 표현식 활용코틀린의 when 표현식은 Java의 switch-case를 더 확장한 형태이다.타입 체크(is)fun whenSample(obj: Any): Boolean = when (obj) {
is String -> obj.isEmpty()
else -> false
}
or 조건fun judgeNumber(number: Int): Boolean = when(number) {
0, 1, 2 -> true
else -> false
}
조건식 사용fun judgeNumber(number: Int): Boolean = when {
number < 0 -> false
number == 0 -> true
else -> false
}
4. 반복문(for)fun forTest() {
for (i in 1..10) println(i)
for (i in 10 downTo 1) println(i)
for (i in 1..5 step 2) println(i)
}
5. 예외 처리코틀린에서는 모든 예외가 unchecked 예외로 처리된다.// 일반적인 try-catch
fun parseIntOrThrow(str: String): Int = try {
str.toInt()
} catch (e: NumberFormatException) {
throw IllegalArgumentException("숫자가 아닙니다.")
}
// null 반환 형태로 처리
fun parseIntOrNull(str: String): Int? = try {
str.toInt()
} catch (e: NumberFormatException) {
null
}
try-with-resources(use)fun readFile(path: String) {
BufferedReader(FileReader(path)).use { reader ->
println(reader.readLine())
}
}
6. 다양한 함수 표현 방식함수를 간단히 표현할 수 있다.// 기본형
fun max(a: Int, b: Int): Int {
return if (a > b) a else b
}
// 표현식 스타일
fun max2(a: Int, b: Int) = if (a > b) a else b
기본값(Default Parameters)과 명명된 인자(Named Arguments)fun namedParameter(str: String, num: Int, flag: Boolean = false) {
println("$str + $num")
}
fun main() {
namedParameter(str = "test", num = 1)
}
참고자료2024-01-28
var vs val)코틀린에서는 변수를 선언할 때 var과 val 두 가지 키워드를 사용한다.var (mutable): 변경 가능한 변수val (immutable): 변경 불가능한 변수 (Java의 final과 유사)var name = "Kotlin" // 변경 가능
name = "Java" // 가능
val version = 1.8 // 변경 불가능
// version = 2.0 // 오류 발생
가능하면 val을 사용하여 **불변성(immutability)**을 유지하는 것이 좋은 코드 스타일이다.2. 원시 타입과 래퍼 타입Java에서는 int와 Integer가 다르지만, Kotlin에서는 구분하지 않는다.Int, Long, Double 등의 기본 타입은 자동으로 Primitive Type으로 변환되어 연산된다.val a: Int = 10
val result = a.plus(10)
println("result = ${result}") // 20
Kotlin에서는 Boxing, Unboxing을 고려할 필요 없이 사용 가능하다.3. 타입 명시 (Type Inference)Kotlin에서는 변수 타입을 변수명: 타입의 형태로 명시할 수 있다.val a: Int = 3
val b: Int
val c = 3 // Int로 타입 추론
val d = 3L // Long으로 타입 추론
주의: 타입을 명시하지 않으면 반드시 초기화해야 한다.val e // 에러 발생
val f: Int // 에러 발생 (초기화 필요)
4. 널 안전성 (Null Safety)Kotlin은 기본적으로 null 값을 허용하지 않는다. ?를 붙이면 Nullable 타입이 된다.val a: Int? = null
println(a) // null
4.1. 안전한 호출 연산자 (?.)fun getLength(text: String?): Int? {
return text?.length
}
4.2. 엘비스 연산자 (?:)fun getLength(text: String?): Int {
return text?.length ?: 0
}
4.3. 널 아님 단언 (!!)fun getLength(text: String?): Int {
return text!!.length // Null이면 NPE 발생
}
5. new 키워드Java에서는 new 키워드로 객체를 생성하지만, Kotlin에서는 필요 없다.val member = Member("Alice", 20)
6. 플랫폼 타입 (Java-Kotlin 호환성)Java에서 넘어온 코드의 null 가능성을 알 수 없다public class Member {
private final String name;
public User(final String name) { this.name = name; }
public String getName() { return name; }
}
val user = Member(null)
val uppercaseName = user.name.uppercase()
java code에서 @Nullable, @NotNull 으로 명시하면 kotlin과의 호환성을 유지 가능하다.
아니면 kotlin 코드로 한번 랩핑하여 사용하는 방법도 있다.7. 타입 캐스팅 (is, as, as?)fun checkType(obj: Any) {
if (obj is Member) {
println(obj.age) // 스마트 캐스트 (Smart Cast)
}
}
val member = obj as? Member // 안전한 캐스팅
println(member?.age ?: "Not a member")
is 는 java 에서 instanceof 키워드와 동일하다.as 형변환에 사용되는 키워드.operator)class Money(val value: Int) {
operator fun plus(other: Money): Money {
return Money(this.value + other.value)
}
}
val money1 = Money(4000)
val money2 = Money(5000)
val newMoney = money1 + money2 // Money(9000)
9. 객체 비교 (==, ===, compareTo)== → equals() 호출=== → 동일한 객체 참조 여부 비교compareTo() → >, < 연산자 지원data class Member(
val name: String,
val age: Int
) : Comparable<Member> {
override fun compareTo(other: Member): Int {
return compareValuesBy(this, other, Member::age)
}
}
val member1= Member("name1", 1)
val member2 = Member("name2", 2)
println(member1 < member2) // true
fun main() {
val name = "Alice"
val age = 25
println("이름: $name, 나이: $age")
println("내년 나이는 ${age + 1} 입니다.") // 표현식 사용 가능
}
11. 범위 연산 (in, !in)fun checkRange(x: Int) {
if (x in 1..10) {
println("$x는 1~10 범위 안에 있습니다.")
} else {
println("$x는 범위를 벗어났습니다.")
}
}
12. Any, Unit, NothingKotlin에서는 Any, Unit, Nothing 등의 특수 타입도 존재한다.Any : Java의 Object와 유사한 최상위 타입Unit : Java의 void와 유사하지만 실제 타입을 가짐Nothing : 코드가 정상적으로 종료되지 않음을 의미 (무한루프)2023-12-03
build.gradle.kts또는build.gradle 파일에 다음과 같이 의존성을 추가한다.testImplementation 'com.tngtech.archunit:archunit:1.1.0'
2. 코드 검증 테스트 작성2.1 코드 컨벤션 체크아래 테스트는 controller 패키지의 클래스들이 반드시 Controller로 끝나는 이름을 가져야 하며, @RestController 어노테이션이 포함되어 있어야 한다는 규칙을 검증한다.public class ArchitectureTest {
JavaClasses javaClasses;
@BeforeEach
void setup() {
javaClasses = new ClassFileImporter()
.withImportOption(new ImportOption.DoNotIncludeTests())
.importPackages("com.example.deuktest");
}
@Test
@DisplayName("Controller 패키지의 클래스는 Controller로 끝나야 한다.")
void controllerTest() {
ArchRule rule = classes()
.that().resideInAnyPackage("..controller")
.should().haveSimpleNameEndingWith("Controller");
ArchRule annotationRule = classes()
.that().resideInAnyPackage("..controller")
.should().beAnnotatedWith(RestController.class);
rule.check(javaClasses);
annotationRule.check(javaClasses);
}
}
이 테스트가 통과하려면 controller 패키지 내의 모든 클래스가 Controller로 끝나야 하며, @RestController어노테이션이 있어야 한다. 이를 통해 일관된 네이밍 규칙을 유지할 수 있다.2.2 의존성 체크2.2.1 Controller는 Service, Request, Response 패키지의 클래스만 의존할 수 있다.일반적으로 Controller는 비즈니스 로직을 처리하는 Service 계층과 사용자 요청을 받는 Request, 응답을 반환하는 Response 계층과만 의존하는 것이 좋다. 이를 ArchUnit으로 검증하는 코드를 작성하면 다음과 같다.@Test
@DisplayName("Controller는 Service와 Request/Response를 사용할 수 있다.")
void controllerDependencyTest() {
ArchRule rule = classes()
.that().resideInAnyPackage("..controller")
.should().dependOnClassesThat()
.resideInAnyPackage("..service..", "..request..", "..response..");
rule.check(javaClasses);
}
이 검증을 통해 controller 패키지의 클래스가 불필요한 의존성을 가지지 않도록 제한할 수 있다.2.2.2 Controller는 Model을 직접 의존할 수 없다.일반적으로 Model 객체(Entity)는 Service 계층에서 관리되어야 하며, Controller가 직접 Model을 참조하는 것은 바람직하지 않다. 이를 검증하는 테스트는 다음과 같다.@Test
@DisplayName("Controller는 Model을 사용할 수 없다.")
void controllerCantHaveModelTest() {
ArchRule rule = noClasses()
.that().resideInAnyPackage("..controller")
.should().dependOnClassesThat().resideInAnyPackage("..model..");
rule.check(javaClasses);
}
이 검증을 통해 Controller가 직접 Model을 참조하는 실수를 방지할 수 있다.3. 마무리이처럼 ArchUnit을 활용하면 코드의 동작을 테스트하는 것이 아니라 코드 자체의 구조와 규칙을 검증할 수 있다.✅ 주요 활용 사례Controller -> Service -> Repository 등의 의존성 흐름 유지)Repository 패키지는 Controller에서 직접 호출 금지)2023-12-03
plugins {
id 'jacoco'
}
jacoco {
toolVersion ="0.8.8"
}
tasks.named('test') {
useJUnitPlatform()
jacoco{}
finalizedBy(tasks.jacocoTestReport)
}
tasks.jacocoTestReport{
reports {
xml.required = true
html.required = true
csv.required = false
xml.destination(file("build/jacoco/jacoco.xml"))
html.destination(file("build/jacoco/jacoco.html"))
}
finalizedBy(tasks.jacocoTestCoverageVerification)
}
tasks.jacocoTestCoverageVerification {
violationRules {
rule {
enabled = true
element = "CLASS"
limit {
counter = "LINE"
value = "COVEREDRATIO"
minimum = BigDecimal.valueOf(0.5)
}
limit {
counter = "LINT"
value = "TOTALCOUNT"
maximum = BigDecimal.valueOf(100)
}
excludes = List.of(
"*.controller.*",
"com.example.deuktest"
)
}
}
}
tasks.jacocoTestReport 는 테스트 후 결과값을 파일로 저장한다. 위의 샘플에선 xml , html 파일을 생성함.

jacoco 테스트 결과
tasks.jacocoTestCoverageVerification 는 검증에 대한 설정을 할 수 있다.