2025-05-17
"level": "error"
같은 값을 기준으로 로그를 검색하려면, 단순 텍스트 로그보다 구조화된 JSON 로그가 훨씬 유리하다.
보다 안정적이고 일관된 로그 구조를 만들기 위해 JSON 로그 포맷을 본격적으로 검토하였고, 그 과정에서 Spring Boot 3.4에서 새롭게 추가된 structured logging
기능을 알게 되었다.📦 사전 준비3.4.0
이상spring-boot-starter-log4j2
org.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
번이며, 이를 통해 메시지를 송수신한다.RabbitMQ 도식화
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-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}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 milliseconds
select 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 활용)nodes
Node Exporter
와 같은 모니터링 컨테이너를 감지하여 메트릭을 수집합니다.services
tasks
를 활용하는 것이 더 적합합니다.tasks
tasks
는 개별 컨테이너의 메트릭을 수집하는 방식으로, 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
는 검증에 대한 설정을 할 수 있다.