@SpringBootTest를 사용한 테스트의 실행 성능과 관련한 고민을 많이 듣는다. 이와 관련된 인프런 Spring Boot TDD 강의의 질문이 올라와서 응용프로그램 컨텍스트(Spring application context)를 사용하는 테스트의 실행 성능에 대한 경험으로 답변했고 이 글에서 한 번 더 정리한다.
우선 인프런 Spring Boot TDD 강의 실습은 120개 이상의 테스트가 모두 @SpringBootTest를 사용해 작성되었다. 그렇게 작성한 이유는 강의의 목적이 Spring Boot 앱을 TDD로 개발하는 기본 역량을 단단히 다지는 것이기 때문이다. 실습 마지막에 테스트 실행 성능을 개선하는데, 거의 모든 테스트가 HTTP 요청으로 동작하고 일부 테스트는 복잡하고 긴 시나리오에 기반함에도 테스트를 모두 실행하는데 걸리는 시간은 대략 6초 정도로 감소된다. 이 정도면 작업 흐름이 끊기는 일은 거의 없을 것이다.
여기서 @SpringBootTest를 사용하지만 테스트 실행 시간에 많은 시간이 소비되지 않는 가장 큰 이유는 테스트 수가 100개 이상이지만 만들어지는 응용프로그램 컨텍스트가 2개 밖에 되지 않기 때문이다. 강의 실습의 경우 응용프로그램 컨텍스트 하나가 준비되는 데 400~700 밀리초 정도 소요된다. 그런데 응용프로그램 컨텍스트 수가 테스트 수가 늘어나도 유지되고 재사용되기 때문에 테스트가 많아질수록 상대적으로 응용프로그램 컨텍스트 준비 시간이 차지하는 비중이 줄어드는 것이다.
반면에 실무에서 테스트 클래스마다 필요한 테스트 대역을 준비하기 위해서 응용프로그램 컨텍스트 구성이 계속 달라지는 경우는 새로운 응용프로그램 컨텍스트를 계속 준비하기 때문에 실행 시간이 길어질 수 있다. 과도한 테스트 대역 사용이 일으킬 수 있는 문제 중 하나다. 인프런 강의에서는 H2 인메모리 데이터베이스 외에는 테스트 대역을 사용하지 않는다.
참고로 나는 그동안 실무에서 Spring Boot 응용프로그램을 개발할 때 @SpringBootTest를 사용한 테스트는 10~20% 정도만 작성하고, 나머지는 Spring 독립적인 순수한 모델 영역의 범위를 크게 만들어서 이 부분을 대상으로 80~90% 정도 테스트를 작성했다. 이렇게 하면 응용프로그램 컨텍스트 준비 시간이 방해가 되는 일이 많지는 않다. 또, 여기서 자세히 설명하지는 않지만 응용프로그램 컨텍스트를 재사용하고 캐싱을 유지하면서 테스트 대역을 사용하는 방법도 있다.
테스트 실행 시간이 짧으면 분명히 장점이 있지만, 제품 요구사항과 밀접한 테스트를 자동화하는 가치가 크기 때문에 각 환경에 따라 응용프로그램 컨텍스를 사용한 테스트에 무조건 거부감을 갖기보다는 실용적인 접근이 필요하다.
]]>set 메서드가 새롭게 추가됐다. 이 글은 기존 방식과 새롭게 추가된 set DSL의 차이점과 사용법을 설명한다.
AutoParams로 테스트 데이터를 생성할 때, 특정 매개변수나 속성에 원하는 값을 지정하는 방법은 여러 가지가 있다. 가장 기반이 되는 방법은 autoparams.customization.Customizer 인터페이스를 구현하는 것이고, autoparams.customization.dsl.ArgumentCustomizationDsl 클래스의 freezeArgument DSL 메서드를 사용할 수도 있다.
예를 들어, 아래와 같이 Product 레코드 클래스와 Review 레코드 클래스가 있다고 하자.
public record Product(
UUID id,
String name,
String imageUri,
String description,
BigDecimal priceAmount,
int stockQuantity
) {
}
public record Review(
UUID id,
UUID reviewerId,
Product product,
int rating,
String comment
) {
}
AutoParams를 사용해서 Review 레코드 인스턴스를 생성할 때 product와 rating 값을 고정하려면 다음과 같이 매개변수 이름을 문자열로 지정할 수 있다.
import static autoparams.customization.dsl.ArgumentCustomizationDsl.freezeArgument;
public class TestClass {
@Test
@AutoParams
void testMethod(Product product, @Max(5) int rating, ResolutionContext context) {
context.customize(
freezeArgument("product").to(product),
freezeArgument("rating").to(rating)
);
Review review = context.resolve();
assertSame(product, review.product());
assertEquals(rating, review.rating());
}
}
freezeArgument 메서드는 여러 오버로드를 갖고 다양한 조건을 사용해 매개변수를 지정할 수 있도록 설계되었다. 예를 들어 형식이 서로 달라도 이름이 같은 여러 매개변수에 null을 지정할 수 있다.
다만 이 방식은 유연하지만, 매개변수 이름이 바뀌면 문자열도 함께 수정해야 하므로 리팩터링에 취약하다. 또한, 오타가 발생해도 컴파일 타임에 잡아낼 수 없다는 단점이 있다.
이런 불편함을 해소하기 위해 ArgumentCustomizationDsl 클래스에 set 메서드가 도입됐다. set 메서드는 문자열 대신 속성 접근자(getter 메서드나 레코드 클래스의 접근자)를 사용해 매개변수를 지정한다. 덕분에 리팩터링에 안전하고, 코드 편집기의 자동완성 기능도 활용할 수 있다.
아래 예시에서 set(Review::product)는 Review 레코드의 product 속성에 접근하는 메서드 참조를 사용한다.
import static autoparams.customization.dsl.ArgumentCustomizationDsl.set;
public class TestClass {
@Test
@AutoParams
void testMethod(Product product, @Max(5) int rating, ResolutionContext context) {
context.customize(
set(Review::product).to(product),
set(Review::rating).to(rating)
);
Review review = context.resolve();
assertSame(product, review.product());
assertEquals(rating, review.rating());
}
}
이 방식의 장점은 다음과 같다:
매개변수 이름을 문자열로 지정하는 대신, 타입 안전한 메서드 참조를 사용한다.
매개변수 이름이 바뀌더라도 리팩터링 기능으로 안전하게 코드를 변경할 수 있다.
오타나 잘못된 지정은 컴파일 타임에 바로 확인할 수 있다.
비슷하게 set DSL은 ResolutionContext뿐 아니라 Factory<T>와도 함께 사용할 수 있다.
import static autoparams.customization.dsl.ArgumentCustomizationDsl.set;
public class TestClass {
@Test
@AutoParams
void testMethod(Product product, @Max(5) int rating, Factory<Review> factory) {
Review review = factory.get(
set(Review::product).to(product),
set(Review::rating).to(rating)
);
assertSame(product, review.product());
assertEquals(rating, review.rating());
}
}
이렇게 set DSL을 사용하면 문자열로 매개변수 이름을 직접 지정하지 않고, 타입 안전한 메서드 참조로 매개변수를 지정할 수 있다. 이로써 테스트 데이터 생성 코드를 더 명확하고 직관적으로 작성할 수 있으며, 리팩터링이나 유지보수도 한층 수월해진다. 상황에 따라 freezeArgument와 set 메서드를 적절히 선택해 사용할 수 있다.
]]>
set메서드는 매개변수 이름에 의존합니다. 매개변수 이름에 대해서는 이 글을 참고하세요.
Factory<T> 클래스나 @Customization 애노테이션을 사용해서 컬렉션 크기를 조절할 수 있지만, 10.1.1 버전 릴리스에 @Size 애노테이션을 사용해 컬렉션을 생성할 때 크기를 지정할 수 있는 기능이 추가되었다.
@Size 애노테이션을 매개변수에 적용하고 min 요소에 크기를 지정하면, AutoParams는 지정된 크기의 컬렉션을 생성한다.
@Test
@AutoParams
void testMethod(@Size(min = 5) List<String> list) {
assertThat(list).hasSize(5);
}
@Size 애노테이션과 @FreezeBy 애노테이션을 조합하면 컬렉션 형식 속성의 크기도 지정할 수 있다. Book 클래스가 다음과 같을 때,
@AllArgsConstructor
@Getter
public class Book {
private final String title;
private final List<String> authors;
private final List<String> quotes;
}
AutoParams가 생성한 다음 테스트 메서드의 book 인자의 authors 속성과 quotes 속성은 각각 5개의 요소와 7개의 요소를 갖는다.
@Test
@AutoParams
void testMethod(
@Size(min = 5) @FreezeBy(PARAMETER_NAME) List<String> authors,
@Size(min = 7) @FreezeBy(PARAMETER_NAME) List<String> quotes,
Book book
) {
assertThat(book.getAuthors()).hasSize(5);
assertThat(book.getQuotes()).hasSize(7);
}
위 예제는 매개변수 이름에 의존합니다. 매개변수 이름에 대해서는 이 글을 참고하세요.
10.1.2 버전 릴리스 이후로는 Stream<T> 형식 매개변수에도 @Size 애노테이션을 사용해서 스트림의 요소 수를 지정할 수 있다.
Record 클래스를 사용한다. Record는 설계상 매개변수 이름이 보존된다.
javac를 사용하는 경우 -parameters 옵션을, kotlinc를 사용하는 경우 -java-parameters 옵션을 사용해 컴파일한다. Spring Boot를 사용하는 경우, Spring Boot Gradle 플러그인이나 Spring Boot Maven 플러그인을 사용하면 이 옵션이 자동으로 활성화된다.
@ConstructorProperties 애노테이션을 명시적으로 선언해 매개변수 이름을 지정한다. 이 애노테이션은 일반 메서드에는 적용되지 않으며, 생성자에만 사용할 수 있다. 만약 생성자가 @AllArgsConstructor와 같은 Lombok 애노테이션으로 생성되는 경우, lombok.anyConstructor.addConstructorProperties = true 옵션을 설정하면 이 애노테이션이 자동으로 추가된다. 자세한 내용은 공식 문서(https://projectlombok.org/features/constructor)를 참고하자.
@Test 메서드는 매개변수를 가질 수 없고 매개변수가 있는 테스트는 반드시 @ParameterizedTest를 사용해야 한다고 오해했다. 이런 인식 때문에 @ParameterizedTest와 함께 사용할 수 있는 @AutoSource 애노테이션을 만들었고, 이후 AutoParams의 기능은 이 @AutoSource 애노테이션을 중심으로 확장되었다.
하지만 시간이 지나고 JUnit 5 API에 대한 이해가 깊어지면서, @Test 메서드도 매개변수를 가질 수 있고, 이를 지원하는 확장 기능을 구현할 수 있다는 점을 알게 되었다.
이런 배경에서 10.0.0 버전 릴리스에서는 @Test 메서드에 사용할 수 있는 @AutoParams 애노테이션이 새로 추가되었다.
@AutoParams와 @AutoSource의 사용법을 비교해보면, 간단한 테스트를 기존 방식대로 작성할 경우 다음과 같다.
@ParameterizedTest
@AutoSource
void testMethod(int a, int b) {
Calculator sut = new Calculator();
int actual = sut.add(a, b);
assertEquals(a + b, actual);
}
같은 테스트를 @AutoParams 애노테이션을 사용해서 작성하면 이렇게 표현할 수 있다.
@Test
@AutoParams
void testMethod(int a, int b) {
Calculator sut = new Calculator();
int actual = sut.add(a, b);
assertEquals(a + b, actual);
}
@AutoParams 애노테이션이 더 간단하지만, 하위 호환을 위해 @AutoSource 애노테이션은 여전히 기존 기능을 그대로 제공한다. 또한, 특별한 소스를 통한 데이터와 AutoParams가 생성한 데이터를 조합해서 테스트 메서드에 전달해야 하는 경우를 위해, @ParameterizedTest 메서드에 사용할 수 있는 @ValueAutoSource, @CsvAutoSource와 같은 애노테이션도 변경 없이 그대로 제공된다.
예를 들어, 테스트 메서드에서 AutoParams가 자동으로 생성해주는 테스트 데이터와 함께 Spring 빈을 매개변수로 받고 싶을 때가 있다. 하지만 별도의 조치를 하지 않으면 AutoParams는 Spring 빈을 직접 생성하려고 시도하고, 이 과정에서 오류가 발생하게 된다.
이 글은 이 문제를 해결하는 기존의 방식과 10.0.0 버전 릴리스에 추가된 @UseBeans 애노테이션을 사용하는 방식을 비교 설명한다.
먼저 아래와 같은 인터페이스와 이를 구현한 Spring 빈이 있다고 가정해보자.
public interface MessageSupplier {
String getMessage(String name);
}
@Component
public class HelloSupplier implements MessageSupplier {
@Override
public String getMessage(String name) {
return "Hello, " + name + "!";
}
}
이 Spring 빈을 AutoParams를 사용한 테스트에서 사용하기 위해 다음 예시처럼 @BrakeBeforeAnnotation 애노테이션을 사용할 수 있다.
@SpringBootTest
public class TestClass {
@ParameterizedTest
@AutoSource
@BrakeBeforeAnnotation(Autowired.class)
void testMethod(String name, @Autowired MessageSupplier service) {
String message = service.getMessage(name);
assertTrue(message.startsWith("Hello"));
assertTrue(message.contains(name));
}
}
이 테스트는 다음과 같은 조건을 만족해야만 제대로 동작한다:
@ParameterizedTest 메서드에 @AutoSource를 함께 사용해서 AutoParams가 테스트 매개변수 인자를 제공하게 한다.
Spring 빈 매개변수에는 반드시 @Autowired 애노테이션을 붙여서 Spring 테스트에 주입을 요청한다.
Spring 빈 매개변수는 AutoParams가 자동으로 생성하는 다른 테스트 데이터 매개변수보다 뒤에 위치해야 한다.
@BrakeBeforeAnnotation(Autowired.class)를 통해 AutoParams가 순차적으로 매개변수 인자를 생성하다가 @Autowired 애노테이션을 만나면 인자 생성을 멈추도록 알려야 한다.
이런 설정은 그리 복잡하지는 않지만 그중에 일부는 테스트의 본질과는 거리가 있고 AutoParams와 Spring 테스트가 충돌하지 않도록 하기 위한 방어 코드에 가깝다.
이런 점을 개선하기 위해 10.0.0 버전에서는 autoparams-spring 확장이 새롭게 추가되었다. AutoParams의 핵심 모듈은 여전히 Spring에 의존하지 않지만, 이 확장은 AutoParams를 Spring 테스트와 더 편리하게 사용할 수 있도록 지원한다.
autoparams-spring 확장이 제공하는 @UseBeans 애노테이션을 사용하면 앞서 살펴봤던 테스트를 이렇게 변경할 수 있다.
@SpringBootTest
public class TestClass {
@Test
@AutoParams
@UseBeans
void testMethod(MessageSupplier service, String name) {
String message = service.getMessage(name);
assertTrue(message.startsWith("Hello"));
assertTrue(message.contains(name));
}
}
이 코드에는 다음과 같은 변화가 있다.
@ParameterizedTest와 @AutoSource 조합을 유지할 수도 있지만 @Test와 @AutoParams 조합을 사용할 수도 있다.
@UseBeans 애노테이션을 테스트 메서드에 붙이면, 매개변수 중에서 Spring 빈은 ApplicationContext에서 가져온 개체가 주입되고 나머지는 AutoParams가 생성해준다.
매개변수의 순서와 관계없이 동작한다. Spring 빈이 먼저 오든, 다른 테스트 데이터가 먼저 오든 상관없다.
Spring 빈 매개변수에 @Autowired를 적용할 필요가 없고, 오히려 적용하면 오류가 발생한다.
10.1.2 버전 이후부터는 Spring 빈 매개변수에
@Autowired를 적용해도 오류가 발생하지 않는다.
이렇게 @UseBeans 애노테이션을 사용하는 방식은 기존보다 간단하고, 테스트의 의도를 조금 더 명확하게 표현할 수 있다.
예를 들어 Product 클래스와 Review 클래스가 있다고 해보자. Product는 상품 정보를 나타내고, Review는 특정 상품에 대한 사용자의 평가를 나타낸다. Review 클래스는 내부에 Product를 포함한다.
@AllArgsConstructor
@Getter
public class Product {
private final UUID id;
private final String name;
private final BigDecimal priceAmount;
}
@AllArgsConstructor
@Getter
public class Review {
private final UUID id;
private final UUID reviewerId;
private final Product product;
private final int rating;
private final String comment;
}
테스트 상황에 따라 product 필드와 rating 필드의 값을 고정해서 Review 개체를 생성하고 싶은 경우가 있다. 지금까지는 이런 요구를 코드로 표현하기 위해서 다음과 같은 준비가 필요했다.
먼저 product 필드 값을 고정하려면 생성자 매개변수 인자 값을 제공하는 ObjectGenerator 인터페이스 구현체를 만든다.
public record ProductArgumentFreezer(Product product) implements ObjectGenerator {
@Override
public ObjectContainer generate(ObjectQuery query, ResolutionContext context) {
if (query instanceof ParameterQuery parameterQuery) {
Parameter parameter = parameterQuery.getParameter();
if (parameter.isNamePresent() && parameter.getName().equals("product")) {
return new ObjectContainer(product);
}
}
return ObjectContainer.EMPTY;
}
}
generate 메서드는 주어진 ObjectQuery가 이름이 "product"인 매개변수를 가리키는지를 판단하고, 조건에 따라 고정된 Product 개체를 반환한다. 처리 과정은 다음과 같다.
입력으로 전달된 query가 ParameterQuery의 인스턴스인지 확인한다.
getParameter 메서드를 호출해서 매개변수 정보를 가져온다.
isNamePresent 메서드를 통해 매개변수 이름이 런타임에 노출되는지를 확인한다.
매개변수 이름이 "product"와 일치하는 경우, ProductArgumentFreezer의 product 필드 값을 ObjectContainer에 담아서 반환한다.
위 조건 중 하나라도 만족하지 않으면, 다른 코드에 생성 작업을 위임하도록 빈 컨테이너인 ObjectContainer.EMPTY를 반환한다.
rating 값도 마찬가지로 비슷한 ObjectGenerator 구현체가 필요하다.
public record RatingArgumentFreezer(int rating) implements ObjectGenerator {
@Override
public ObjectContainer generate(ObjectQuery query, ResolutionContext context) {
if (query instanceof ParameterQuery parameterQuery) {
Parameter parameter = parameterQuery.getParameter();
if (parameter.isNamePresent() && parameter.getName().equals("rating")) {
return new ObjectContainer(rating);
}
}
return ObjectContainer.EMPTY;
}
}
테스트 메서드에서 이 구현체들을 적용하면 다음과 같다.
@Test
@AutoParams
void testMethod(Product product, @Max(5) int rating, ResolutionContext context) {
context.applyCustomizer(new ProductArgumentFreezer(product));
context.applyCustomizer(new RatingArgumentFreezer(rating));
Review review = context.resolve();
assertSame(product, review.getProduct());
assertEquals(rating, review.getRating());
}
이렇게 하면 원하는 동작을 수행하지만, 매개변수 하나를 고정하기 위해 ObjectGenerator 구현체를 매번 만들고 조건을 검사하는 코드를 반복해야 한다. 테스트에서 “이 값을 고정하고 싶다”는 단순한 의도를 표현하기에 다소 과한 구조일 수 있다.
하지만 이번에 추가된 DSL을 사용하면 같은 목적을 간단하게 달성할 수 있다.
import static autoparams.customization.dsl.ArgumentCustomizationDsl.freezeArgument;
public class TestClass {
@Test
@AutoParams
void testMethod(Product product, @Max(5) int rating, ResolutionContext context) {
context.customize(
freezeArgument("product").to(product),
freezeArgument("rating").to(rating)
);
Review review = context.resolve();
assertSame(product, review.getProduct());
assertEquals(rating, review.getRating());
}
}
ArgumentCustomizationDsl 클래스의 freezeArgument 정적 메서드로 시작하는 관용구 코드는 매개변수 이름을 기준으로 인자를 고정한다. ObjectGenerator를 직접 구현할 필요 없이, 단순하고 선언적인 코드만으로 같은 효과를 낼 수 있다. 코드가 간결해질 뿐 아니라, 테스트의 의도가 더 명확하게 드러난다.
테스트에서 매개변수를 고정하는 이유는 분명하다. 특정 값을 사용한 상태에서 시스템의 동작을 확인하고 싶은 것이다. 10.0.0 버전 이전에는 AutoParams를 사용해서 이 요구를 구현하는 데 적지 않은 준비가 필요했다. autoparams.customization.dsl 패키지는 그런 부담을 없애준다. 매개변수 하나를 고정하기 위해서 ObjectGenerator 구현체를 새로 만들 필요가 없다. 테스트는 더 간결하고, 더 설명적이며, 더 읽기 쉬워진다.
DSL 추가가 단순한 문법 개선처럼 보일 수도 있지만 실용적인 테스트 작성을 고민해 본 사람이라면 이 변화가 반가울 것이다. AutoParams의 DSL은 테스트의 의도를 효과적으로 드러내는 목적에 한 발짝 더 가까워진 기능이다.
]]>참고
freezeArgument(String parameterName)메서드는 런타임에 매개변수 이름을 확인할 수 있어야 동작한다. 하지만 Java는 기본적으로 바이트코드에 매개변수 이름을 포함하지 않는다. 이 기능이 제대로 작동하려면 다음 방법 중 하나를 사용해야 한다:
Record 클래스를 사용한다. Record는 설계상 매개변수 이름이 보존된다.
javac를 사용하는 경우-parameters옵션을,kotlinc를 사용하는 경우-java-parameters옵션을 사용해 컴파일한다. Spring Boot를 사용하는 경우, Spring Boot Gradle 플러그인이나 Spring Boot Maven 플러그인을 사용하면 이 옵션이 자동으로 활성화된다.
@ConstructorProperties애노테이션을 명시적으로 선언해 매개변수 이름을 지정한다. 이 애노테이션은 일반 메서드에는 적용되지 않으며, 생성자에만 사용할 수 있다. 만약 생성자가@AllArgsConstructor와 같은 Lombok 애노테이션으로 생성되는 경우,lombok.anyConstructor.addConstructorProperties = true옵션을 설정하면 이 애노테이션이 자동으로 추가된다. 자세한 내용은 공식 문서(https://projectlombok.org/features/constructor)를 참고하자.
이벤트는 보통 이런 특징들을 갖는다.
시스템 상에서 발생한 사실을 묘사한다.
이미 과거에 벌어진 일이기 때문에 검증 대상이 아니다.
생산자가 정의한다. 따라서 소비자가 생산자에 물리적으로 또는 논리적으로 의존한다.
생산자는 소비자를 특정하지 않는다. 그래서 이벤트 전송 행위를 발행한다고 표현한다.
생산자는 이벤트 소비 효과를 염두하지 않는다.
반면 명령은 대개 이런 특징들을 갖는다.
생산자가 소비자에게 바라는 바를 기술한다.
소비자가 생산자의 요청을 수용할 수 있는지 검증한다.
소비자가 정의한다. 따라서 생산자가 소비자에게 물리적으로 또는 논리적으로 의존한다.
생산자는 소비자를 특정한다. 그래서 명령 전송 행위를 보낸다고 표현한다.
생산자는 소비자에게 기대하는 명령 소비 효과가 있다.
사례를 들어보자. 마케팅 시스템이 혜택 시스템에게 쿠폰 발급을 요청할 수 있다. 요청은 혜택 시스템에 의해 IssueCoupon 명령으로 정의되고 쿠폰 발급에 필요한 정보를 담는다. 혜택 시스템은 명령에 담긴 대상 사용자 식별자, 할인 금액, 유효기간 등을 검증하고 조건을 만족하지 않으면 쿠폰 발급을 실패시킬 수 있다.
쿠폰 발급이 성공하면 혜택 시스템은 CouponIssued 이벤트를 발행한다. CouponIssued 이벤트는 정산 시스템이 수신해 내용을 기록하고 이후 월 마감 작업에 이용한다. 또한 혜택 시스템은 발행된 쿠폰에 대한 정보를 담아 알림 시스템에 SendNotification 명령을 보낸다. 알림 시스템은 사용자에게 쿠폰 발행을 알리는 푸시 알림을 보낸다.
이들 시스템의 의존관계는 이렇다. 마케팅 시스템과 정산 시스템은 혜택 시스템에 의존한다. 혜택 시스템은 알림 시스템에 의존한다. 아키텍처 복잡도가 커질 수록 구성요소들의 의존관계 관리는 설계 품질에 큰 영향을 미친다. 메시지 성격에 따라서 의존관계가 달라질 수 있으니 이벤트와 명령을 배치할 때 관련 범위의 아키텍처를 충분히 고려하는 것이 좋다.
]]>Companies use their reversed Internet domain name to begin their package names—for example, com.example.mypackage for a package named mypackage created by a programmer at example.com.
관습 제안의 이유는 이렇다.
This works well unless two independent programmers use the same name for their packages. What prevents this problem? Convention.
이건 전통인가 악습인가?
두 내용을 정리하면, 프로그래머들이 패키지 이름을 겹치지 않게 지정할 수 있도록 회사의 인터넷 도메인 이름을 뒤집어 패키지 이름 앞에 배치하자는 것이다.
하지만 난 지금까지 이런 이름 공간 명명 관습이 쓰이지 않는 생태계에서 이름 공간 이름이 com., org. 등으로 시작하지 않기 때문에 코드 이름이 충돌해 문제가 발생했다는 얘기를 들은 적이 없다.
만약 이름이 ‘Cool Company’이고 cool-company.com 도메인을 사용하는 회사에 소속되어 Java로 새로운 클래스를 만드는 중이라면 패키지 이름은 com.coolcompany. 대신 그저 coolcompany.으로 시작하자. coolcompany.coredomain.AwesomeClass! 간결하고 설명력 충분하고 멋지기까지 한 이름이다. 심지어 소스코드 디렉터리 깊이도 줄여줄 것이다.
만약 내가 잘 해낼 수 있을 거란 자신감이 충분할 때엔 백로그나 태스크 수준에서 코딩을 한 뒤 변경된 코드를 어떻게 나눠 커밋할 지 고민하는 반면, 내가 조금 더 집중해야 한다는 판단이 들면 코드 베이스에 어떤 변화를 줘야할 지 먼저 글로 작성해 본다. 그리고 이 범위를 넘는 코드 추가나 변경은 커밋하기 전에 만들지 않는다. 커밋 메시지 범위를 벗어나지만 꼭 필요한 일이 발견되면 일단 할 일 목록에 추가한다. 그리고는 다음 번 커밋의 메시지를 쓸 때 할 일 목록에서 가장 높은 우선순위를 갖는 하나를 고른다.
커밋 메시지 주도 개발을 사용하면 불필요한 코딩과 생각의 비용을 줄일 수 있고 작은 보폭의 전진이 유지되어 문제가 발생했을 때 값 싸게 고칠 수 있다. 지출 계획을 세운 소비와 그렇지 않은 경우를 떠올리면 어떤 장점을 가지는 지 쉽게 이해할 수 있을 것이다. 이런 방법은 테스트 주도 개발이 습관이 되며 자연스럽게 터득한 방법인데 재밌게도 주위에서 이런 방법을 쓴다고 하는 사람을 한 명 본 적이 있다.
난 이 방식을 쓰는 여부와 무관하게 커밋 메시지는 50/72 규칙을 따른다. Windows 용 Git Bash의 Vim에는 기본으로 50/72 규칙이 적용되어 있기 때문에 코딩을 먼저 할 때엔 그것을 사용하고, 커밋 메시지 주도 개발을 할 때엔 Visual Studio Code에서 커밋 메시지를 먼저 작성한다.
나는 주로 Consolas와 맑은 고딕 글꼴을 사용하는데 안타깝지만 Visual Studio Code에서 맑은 고딕 글꼴의 한글 문자는 정확한 전폭(full width)을 사용하지 않는다. 고맙게도 네이버가 배포하는 ‘네이버 나눔고딕 코딩글꼴’의 한글 문자는 전폭을 정확히 사용한다. 그래서 이 글꼴을 설치하고 ‘Git Commit Message’ 언어에 대해 이 글꼴과 50, 72 눈금자를 설정하면 문제가 해결된다.
{
"[git-commit]": {
"editor.rulers": [50, 72,],
"editor.fontFamily": "NanumGothicCoding"
}
}
팀에 좋은 코드 리뷰 문화를 정착시키기 위해 작은 커밋 작성은 꼭 필요하다. 작은 커밋을 만드는 건 처음 해보면 쉽지 않을 수 있는데 연습하는 과정에서 커밋 메시지 주도 개발이 도움이 될 수 있을 것이다.
]]>