Spring Boot 3.4 + Log4j2로 Structured Logging 적용하기

2025-05-17

운영 환경에서 로그를 수집하고 분석하는 과정에서, 특정 키워드나 필드 값을 기준으로 로그를 필터링하거나 분류하고 싶을 때가 자주 발생한다. 예를 들어 "level": "error" 같은 값을 기준으로 로그를 검색하려면, 단순 텍스트 로그보다 구조화된 JSON 로그가 훨씬 유리하다. 보다 안정적이고 일관된 로그 구조를 만들기 위해 JSON 로그 포맷을 본격적으로 검토하였고, 그 과정에서 Spring Boot 3.4에서 새롭게 추가된 structured logging 기능을 알게 되었다.📦 사전 준비
  • Spring Boot 3.4.0 이상
  • 로깅 구현체: spring-boot-starter-log4j2
  • Java 17 이상
  • 🧱 StructuredLogFormatter란?Spring Boot 3.4부터 도입된 org.springframework.boot.logging.structured.StructuredLogFormatter<T>
    인터페이스는 구조화된 로그 출력을 위한 포맷터 역할을 한다.Spring Boot는 사용하는 로깅 구현체에 맞춰 해당 포맷터를 자동 연동하며,
    JSON 형태의 로그 생성을 쉽게 도와주는 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); } }
  • logback 으로 적용은 spring-boot-logback-샘플 을 참고한다. (제너릭 인터페이스가 다름)
  • ⚙️ 설정 방법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."} 📚 참고 자료
  • Spring Boot Structured Logging 공식 문서
  • html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 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 .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}

    Openfeign QueryDsl JPA 설정하기

    2025-04-20

    기존에 Querydsl 프로젝트가 버전 업그레이드가 잘 되지 않고 있다는 점은 알고 있었지만, 그동안은 큰 문제가 없어 그냥 사용해왔다.그러던 중, 새로 진행하는 토이 프로젝트에서 한번 설정해 보고싶어 테스트 해봤다. 적용 과정은 의외로 간단했고, 특히 Spring Boot 3.x + Jakarta EE + Hibernate 6 환경에서의 호환성 문제도 자연스럽게 해결할 수 있어 만족스러웠다.게다가 Spring Data 공식 문서에서도 해당 포크에 대해 언급하고 있다는 점에서, 앞으로의 표준처럼 느껴진다.1. 환경 구성은 아래와 같다.
  • java 21
  • gradle(kotlin)
  • spring boot 3.4
  • 2. Gradle(kotlin) 설정하는 방법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>
  • 기존에 비해 정말 간단하게 설정이 가능함(플러그인 설치할 필요 없음.)
  • 참고 자료
  • OpenFeign
  • Spring Data Extensions
  • 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 .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 .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}

    RabbitMQ 에서 MQTT와 AMQP 혼용 통신

    2025-04-10

    🧐 조사하게 된 계기기존에는 RabbitMQ를 AMQP 기반으로만 사용하고 있었으나, MQTT 지원 요구가 생기며 조사하게 되었다. RabbitMQ가 MQTT 플러그인을 통해 AMQP와 MQTT를 혼용할 수 있다는 사실을 확인하였다. 이에 따라 Docker 환경에서 MQTT → AMQP 메시지 흐름까지 테스트하였다.✅ MQTT와 AMQP 0.9.1 비교항목MQTTAMQP 0.9.1설명목적IoT, 저전력 장치 통신엔터프라이즈 메시징, 시스템 간 통합경량 통신 vs 신뢰성 위주의 통신구조Topic 기반 Pub/SubExchange → Queue 기반라우팅 방식 차이메시지 흐름Publisher → Broker → SubscriberPublisher → Exchange → Queue → ConsumerAMQP는 중간에 Exchange가 있음토픽 구분자/ (슬래시). (점)계층 구분 방식 차이싱글 레벨 와일드카드+ (플러스)* (애스터리스크)하나의 레벨만 매칭멀티 레벨 와일드카드# (해시)# (해시)0개 이상의 모든 레벨 매칭대표 사용처IoT 센서, 게이트웨이, 모바일금융, 백엔드 마이크로서비스적용 영역 차이브로커 예시Mosquitto, EMQX, RabbitMQ (MQTT 플러그인)RabbitMQ, ActiveMQ공통 브로커도 존재함 (예: RabbitMQ)✅ RabbitMQ 실행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 "
  • MQTT 프로토콜을 사용하기 위해서는 rabbitmq_mqtt 플러그인을 활성화해야 한다.
  • MQTT 기본 포트는 1883번이며, 이를 통해 메시지를 송수신한다.
  • 해당 플러그인을 통해 MQTT <-> AMQP 간 통신이 가능하다.
  • 각 프로토콜은 각자의 topic 규칙으로 송수신하면 RabbitMQ가 이를 변환하여 처리한다.
  • ✅ RabbitMQ 도식화
    RabbitMQ 도식화

    RabbitMQ 도식화

  • MQTT 메시지는 test/topic/A 형태의 토픽으로 발행되며,
    RabbitMQ에서는 이를 test.topic.A 라우팅 키로 변환하여 처리한다.
  • Exchange는 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") } }
  • Queue와 Exchange 를 묶는 설정이다.
  • Exchange 는 topic 타입으로 설정하고 test.topic.Atest.queue를 바인딩한다.
  • AMQP를 사용하는 Consumer에서 test.queue를 바라보고 있으면 MQTT -> AMQP 메시지 전송이 가능하다.
  • ✅ AMQP → MQTT 메시지 흐름이번에는 반대 방향, 즉 AMQP에서 발행된 메시지를 MQTT 클라이언트가 수신하는 경우를 살펴보겠다. 이 경우에는 AMQP Publisher가 메시지를 amq.topic exchange에 발행하고,
    MQTT 클라이언트가 구독하고 있는 토픽과 일치하는 라우팅 키로 메시지를 전달해야 한다.라우팅 키는 MQTT 토픽과의 호환을 위해 .(dot) 형식으로 작성해야 한다.🧑‍💻샘플코드https://github.com/deuk9/mqtt-amqp-integrationhtml 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}

    Hibernate에서 Slow Query 로그 남기기

    2025-04-02

    🐌 Hibernate에서 느린 쿼리(Slow Query) 로그 남기기운영 중인 시스템에서 쿼리 하나하나의 성능을 일일이 확인하기는 어렵다. 하지만 일정 시간 이상 소요되는 **느린 쿼리(Slow Query)**를 자동으로 로그에 기록하면, 성능 병목 지점을 빠르게 파악할 수 있다. Spring Boot + Hibernate 환경이라면 Hibernate 자체 기능을 활용해 쉽게 설정 가능하다.⚙️ 설정 방법 (Spring Boot3.x + Hibernate 6.x)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);}

    Spring boot 에서 레이어별 테스트 적용하기

    2025-02-15

    Spring Boot 애플리케이션을 개발할 때, 각 레이어(controller, service, repository)에 대한 단위 테스트와 통합 테스트를 작성하는 것은 매우 중요하다. 테스트 작성에 대한 정답은 없지만 앞으로 이런식으로 작성해보겠다는 취지로 고민하여 작성하였다.1. Controller 레이어 테스트Controller 레이어의 테스트에서는 @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 됨)
  • 각각의 Controller마다 @WebMvcTest를 붙이면 Spring Context가 매번 재생성되므로, Support 클래스를 상속하여 성능을 최적화한다.
  • 2. Service 레이어 테스트Service 레이어는 실제 데이터베이스와 상호작용하므로 Mocking 없이 테스트를 진행한다. 이를 위해 Testcontainers를 활용하여 PostgreSQL 환경을 직접 실행한다.📌 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 { } 📌 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 테스트 시 고려할 사항
  • Mocking 없이 실제 DB와 연결된 환경에서 테스트를 수행한다.
  • Testcontainers를 사용하여 실제 PostgreSQL이 테스트 시 실행된다.
  • Spring context 재생성을 방지하기 위해 부모 클래스에 @SpringBootTest를 명시하고, 해당 클래스를 상속받아 테스트를 작성한다.
  • 3. Repository 레이어 테스트Repository 테스트는 Spring Data JPA에서 직접 작성한 메서드나 QueryDSL을 검증하는 용도로 작성한다.📌 Repository 테스트 코드@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을 사용하여 각 테스트가 끝난 후 자동으로 데이터가 롤백되도록 한다.
  • 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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}

    Testcontainers 쉽게 적용하기

    2025-02-14

    최근 사내에서 테스트 문화를 정착시키기 위해 Testcontainers 사용 방법에 대해 다시 조사하게 되었다. 이전에 사용할 때에는 Spring의 도움 없이 수동으로 설정했던 기억이 있다. 그러나 Spring Boot 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 { } 주요 설정 설명
  • PostgreSQLContainerSpring Bean으로 등록한다.
  • @ServiceConnection을 사용하면 자동으로 JDBC URL, 데이터베이스 정보, 사용자 정보를 Spring DataSource에 매핑해준다.
  • 기존에는 컨테이너의 정보를 직접 가져와서 테스트 환경에 주입해야 했지만, @ServiceConnection을 사용하면 이를 자동화할 수 있다.
  • withReuse(true)를 설정하면 컨테이너를 재사용하여 테스트 속도를 향상시킬 수 있다. 이를 설정하지 않으면 매 테스트마다 컨테이너가 생성되고 삭제되므로 속도가 느려진다.
  • 4. Testcontainers를 활용한 테스트 작성@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되도록 한다.
    • Testcontainers는 컨테이너를 재사용하기 때문에, rollback을 하지 않으면 데이터가 남아 다른 테스트에 영향을 줄 수 있다.
  • Mocking 없이 실제 데이터베이스를 사용하여 실제 운영 환경과 유사한 테스트를 수행할 수 있다.
  • 참고자료
  • Spring Boot Testcontainers 공식 문서
  • Testcontainers
  • 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 .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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}

    Prometheus에서 Docker Swarm Service Discovery 활용하기

    2025-01-10

    Docker Swarm 환경에서 Prometheus를 활용해 노드, 태스크, 서비스의 메트릭을 동적으로 수집하는 방법을 소개합니다. 기존의 정적 설정 방식과 달리, 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 활용)
  • 역할별 활용 방안1.nodes
  • 각 Swarm 노드에서 실행 중인 Node Exporter와 같은 모니터링 컨테이너를 감지하여 메트릭을 수집합니다.
  • 노드 레벨의 시스템 리소스 사용량을 확인할 때 유용합니다.
  • 2.services
  • 특정 서비스에 대한 메트릭을 수집할 수 있지만, Swarm의 로드 밸런싱으로 인해 특정 컨테이너의 데이터를 지속적으로 수집하는 것이 어렵습니다.
  • 따라서, 개별 컨테이너 단위의 모니터링이 필요할 경우 tasks를 활용하는 것이 더 적합합니다.
  • 3.tasks
  • tasks는 개별 컨테이너의 메트릭을 수집하는 방식으로, Spring Boot Actuator 등과 연계하여 효과적으로 활용할 수 있습니다.
  • 제가 설정방법을 찾지 못했을 수도 있으나 아래와 같은 문제가 있었습니다.
  • 주의 사항:
    • tasks의 포트 설정을 별도로 지정할 수 없습니다.
    • 테스트 결과 내부 네트워크 IP + publish port 로 접근하려고 합니다.
    • 내부 네트워크 IP (10.x.x.x)와publish port로 접근하지만, 내부 네트워크에서는 target port로 접근해야 정상적으로 데이터를 수집할 수 있습니다.
    • docker run -p 8080:8080처럼publish portinner port를 동일하게 설정해야 정상적인 모니터링이 가능합니다.
  • 참고자료
  • Prometheus 공식 문서
  • 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 .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);}

    HTTP Service Discovery를 활용한 Prometheus 동적 타겟 관리

    2025-01-03

    모니터링해야 할 대상이 많지만, Kubernetes나 Eureka와 같은 서비스 디스커버리(Service Discovery)를 사용할 수 없는 환경도 존재할 수 있다. 이런 경우, 정적인 설정(static config)으로 모든 타겟을 등록해야 하는 불편함이 있다. 그러나 HTTP 기반 서비스 디스커버리(HTTP Service Discovery, HTTP SD)를 활용하면 이 문제를 효과적으로 해결할 수 있다.Prometheus 설정- job_name: 'http-sd' scheme: http http_sd_configs: - url: http://localhost:8080/api/targets refresh_interval: "15s" 위 설정에서 schemehttp로 설정하고, 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의 검색 조건이나 대상 식별값으로 활용할 수 있다.
  • 마무리HTTP Service Discovery는 Kubernetes와 같은 별도 서비스 디스커버리 없이도 Prometheus에서 동적인 모니터링 구성을 가능하게 해준다. 특히, 정적인 설정으로 인해 발생하는 유지보수 부담을 줄이고, 모니터링 대상을 실시간으로 유연하게 관리할 수 있도록 돕는다. 단점으로는 이를 위한 서버 구현과 유지보수가 필요하다.참고자료
  • 프로메테우스 홈페이지
  • html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}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);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}

    코틀린 정리 (2) - if, when, for, method

    2024-01-29

    이번 포스팅에서는 Kotlin의 핵심 문법 중에서도 자주 사용하는 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) } 참고자료
  • 자바 개발자를 위한 코틀린 입문
  • ChatGPT
  • 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 .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);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}

    코틀린 정리(1)

    2024-01-28

    코틀린(Kotlin)은 간결한 문법과 강력한 타입 시스템을 제공하여 개발 생산성을 높이는 데 초점을 맞춘 언어다. 특히, 변수 선언, 널 안전성(Null Safety), 타입 시스템, 연산자 등에서 기존 Java보다 더 편리한 기능을 제공한다.1. 변수 선언 (var vs val)코틀린에서는 변수를 선언할 때 varval 두 가지 키워드를 사용한다.
  • var (mutable): 변경 가능한 변수
  • val (immutable): 변경 불가능한 변수 (Java의 final과 유사)
  • var name = "Kotlin" // 변경 가능 name = "Java" // 가능 val version = 1.8 // 변경 불가능 // version = 2.0 // 오류 발생 가능하면 val을 사용하여 **불변성(immutability)**을 유지하는 것이 좋은 코드 스타일이다.2. 원시 타입과 래퍼 타입Java에서는 intInteger가 다르지만, Kotlin에서는 구분하지 않는다.
    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 형변환에 사용되는 키워드.
  • 8. 연산자 오버로딩 (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
  • java 에서 compare 때문에 애를 많이 먹었었는데 명시적으로 <> 연산자를 통하여 비교 가능
  • 10. 문자열 템플릿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 : 코드가 정상적으로 종료되지 않음을 의미 (무한루프)
  • 참고자료
  • 자바 개발자를 위한 코틀린 입문
  • ChatGPT
  • html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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 .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 .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}

    archunit 을 활용한 코드 컨벤션 검증

    2023-12-03

    코드를 개발할 때, 로직의 정확성을 검증하는 단위 테스트(Unit Test)나 통합 테스트(Integration Test) 외에도 코드 자체에 대한 검증(Code Verification) 이 필요할 때가 있다. 이는 코드 컨벤션, 안티 패턴, 클래스 간의 의존성 등을 사전에 점검하여 유지보수성을 높이고, 프로젝트의 아키텍처를 일관되게 유지하는 데 유용하다.이러한 코드 검증을 위해 ArchUnit 라이브러리를 활용할 수 있다. ArchUnit은 런타임 전에 코드를 분석하여 패키지 구조, 클래스 네이밍 규칙, 의존성 등을 체크할 수 있도록 도와주는 강력한 도구이다. 이 글에서는 ArchUnit을 이용해 코드 검증 테스트를 작성하는 방법을 살펴본다.1. ArchUnit 의존성 추가Gradle 프로젝트에서 ArchUnit을 사용하려면,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는 ServiceRequestResponse 패키지의 클래스만 의존할 수 있다.일반적으로 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에서 직접 호출 금지)
  • 안티 패턴 감지 및 코드 품질 향상
  • 참고 자료
  • 쥬쥬와 하루만에 끝내는 스프링 테스트
  • 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 .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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}

    Jacoco 를 이용한 코드 커버리지 테스트

    2023-12-03

    코드 커버리지는 테스트가 얼마나 많은 코드 라인을 실행하는지를 나타내는 중요한 지표이다. Jacoco는 코드 커버리지를 측정하고 일정 수준에 도달하는지 검증하는 도구이다.1. 설정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 테스트 결과

    jacoco 테스트 결과

  • tasks.jacocoTestCoverageVerification 는 검증에 대한 설정을 할 수 있다.
    1. Line 의 50% 를 커버하지 못하면 검증 실패
    2. 각 클래스의 라인수가 100줄이 넘으면 실패
  • 참고 자료
  • 쥬쥬와 하루만에 끝내는 스프링 테스트
  • 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);}