Service 메서드를 처음 여는 사람은 두 가지를 알고 싶어 한다. 이 메서드가 무엇을 전제하고 동작하는지, 그리고 어디까지 왔을 때 무엇이 보장되는지. 보통은 주석이 그 역할을 한다.
// dto는 컨트롤러에서 이미 검증됨
// 여기서 재고는 확보된 상태
한 줄씩 보면, 첫 줄은 “이 DTO는 컨트롤러가 형식 검증을 이미 끝냈다”는 뜻이고, 둘째 줄은 “재고는 이미 확보해 뒀으니 더 줄어들 걱정은 안 해도 된다”는 뜻이다. 즉 이 메서드를 읽는 다음 사람은 이 두 줄의 말을 그대로 믿고 코드를 짜게 된다.
주석의 문제는 코드가 바뀌어도 그대로 남는다는 것이다. 검증 로직이 사라져도, 재고 확보 순서가 바뀌어도 주석은 여전히 옛말을 하고 있다. 읽는 사람은 그 주석을 믿을지 말지부터 판단해야 한다.
assert는 같은 말을 검증되는 형태로 적는 방법이다. 그리고 여기에는 두 가지 목적이 있다.
첫째, 흐름을 빠르게 파악하게 한다. 메서드 상단의 사전조건은 “이 코드가 무엇을 믿고 동작하는지”를, 중간의 불변조건은 “여기까지 왔다면 무엇이 참인지”를 선언한다. 읽는 사람이 로직을 거슬러 올라가며 전제를 재구성하지 않아도 된다.
둘째, 문제를 숨기지 않는다. 전제가 깨졌는데도 그럴듯하게 넘어가는 코드는 버그를 데이터로 만든다. assert는 “이건 있을 수 없다”를 선언하고, 실제로 일어나면 그 자리에서 멈춘다. 오류 지점과 증상 지점의 거리를 좁히는 것이 디버깅 시간을 줄이는 가장 확실한 방법이다.
이 글은 Assert로 프로그래머의 실수를 잡아내기에서 정리한 “검증 계층 통과 후는 assert 영역”이라는 원칙을, 계층별 DTO 네이밍 표준의 Command → Service 구간에 구체적으로 적용한다.
원칙 1. Service의 계약은 “값 검증은 끝났다”이다
Service 메서드가 CreateOrderCommand를 받았을 때 무엇을 가정할 수 있는가. 이걸 먼저 못 박아야 나머지가 정해진다.
이 표준에서 Service의 계약은 이렇다 — 들어온 값은 이미 형식 검증을 통과했다. Service는 비즈니스 규칙만 판단한다.
그러면 그 계약을 지키는 것은 누구인가. 각 진입점이다.
@Valid"] --> CMD["CreateOrderCommand
순수 데이터 홀더"] C2["Batch Job
자체 값 검증"] --> CMD C3["CLI Adapter
자체 값 검증"] --> CMD CMD --> SVC["OrderService"] SVC -.->|"assert · 개발/테스트에서만"| A["계약 위반 감지
AssertionError"] SVC --> BIZ["비즈니스 규칙 검증
ErrorCodeException"] style CMD fill:#2d3748,stroke:#4299e1,stroke-width:2px,color:#e2e8f0 style SVC fill:#1a202c,stroke:#48bb78,stroke-width:3px,color:#e2e8f0 style A fill:#2d3748,stroke:#ed8936,stroke-width:2px,color:#e2e8f0 style BIZ fill:#2d3748,stroke:#f56565,stroke-width:2px,color:#e2e8f0
그림을 읽는 법은 이렇다. Controller와 배치 Job, CLI Adapter는 들어오는 경로는 다르지만 각자 값 검증을 마친 뒤 똑같이 Command를 만들어 OrderService에 넘긴다. OrderService는 그 Command를 받아 두 가지를 한다 — 개발·테스트에서만 켜지는 assert로 “진입점이 약속을 지켰는지”를 점검하고(점선 화살표), 실제 업무 규칙은 ErrorCodeException을 던지는 비즈니스 규칙 검증으로 처리한다.
HTTP로 들어오면 계층별 검증 표준대로 @Valid가 처리한다. 배치나 CLI처럼 HTTP가 아닌 경로로 들어온다면, 그 진입점이 자기 방식으로 값 검증을 마친 뒤 Command를 만들어야 한다. 어느 경로든 Service에 도달할 때는 값 검증이 끝나 있다는 것이 계약이다.
Command는 순수 데이터 홀더다
그래서 Command는 생성자에서 검증하지 않는다.
// ✅ 검증 없이 값만 옮긴다
@Getter
public class CreateOrderCommand {
private final String productName;
private final Integer quantity;
private final String couponCodeOrNull;
private CreateOrderCommand(String productName, Integer quantity, String couponCodeOrNull) {
this.productName = productName;
this.quantity = quantity;
this.couponCodeOrNull = couponCodeOrNull;
}
public static CreateOrderCommand of(String productName, Integer quantity, String couponCodeOrNull) {
return new CreateOrderCommand(productName, quantity, couponCodeOrNull);
}
}
한 줄씩 따라가기. @Getter는 Lombok이 이 클래스의 모든 필드에 대해 get필드명() 조회 메서드를 자동으로 만들어 주는 표시다 — productName 필드가 있으면 getProductName()이 따로 적지 않아도 생긴다. 생성자는 private이라 밖에서 직접 new로 못 만들고, 정적 메서드 of(...)를 통해서만 객체를 만들 수 있다.
Null-free 객체 설계의 “생성자가 완전성을 보장한다”와 어긋나 보이지만, 이유가 있다. 검증을 여러 곳에 두면 어디가 진짜 방어선인지 흐려진다. Command가 자체 검증을 하면 진입점 검증이 느슨해져도 티가 안 나고, 결국 “어디선가는 막히겠지”라는 상태가 된다.
방어선을 경계 한 곳으로 몰아두면 책임이 분명해진다. 그리고 그 방어선이 뚫렸는지는 — 다음 원칙이 감시한다.
원칙 2. 사전조건 assert로 계약을 명시한다
메서드 첫머리에 “내가 무엇을 가정하고 동작하는지”를 적는다.
@Service
@RequiredArgsConstructor
class OrderService implements OrderCommandUseCase {
private final StockRepository stockRepository;
private final OrderRepository orderRepository;
public ServiceResult<OrderDomain> createOrder(CreateOrderCommand command) {
assert command.getProductName() != null && !command.getProductName().isBlank()
: "productName은 진입점에서 검증되어야 한다";
assert command.getQuantity() != null && command.getQuantity() > 0
: "quantity는 진입점에서 1 이상으로 검증되어야 한다";
StockDomain stock = stockRepository.findByProductName(command.getProductName())
.orElseThrow(() -> ErrorCodeException.of(ErrorCode.PRODUCT_NOT_FOUND));
stock.validateOrderable(command.getQuantity());
// ...
}
}
한 줄씩 보면, @Service는 이 클래스를 스프링이 관리하는 Service 빈으로 등록하는 애너테이션이고, @RequiredArgsConstructor는 final로 선언한 필드만 받는 생성자를 자동으로 만들어 주는 롬복 애너테이션이다. implements OrderCommandUseCase는 이 클래스가 UseCase 인터페이스가 약속한 메서드를 실제로 구현한다는 뜻이다. 반환 타입 ServiceResult<OrderDomain>은 OrderDomain을 그대로 돌려주지 않고 한 겹 감싼 ServiceResult다 — Service의 반환 모양을 통일해 두면 나중에 메타데이터가 필요해져도 이 타입만 넓히면 된다. stockRepository.findByProductName(...)은 Repository를 거쳐 실제 DB에서 재고를 조회하는 호출이고, 결과가 없으면 .orElseThrow(...)가 예외를 던진다.
이 assert들이 하는 일은 사용자 입력 검증이 아니다. 그 값들은 이미 검증됐다. 여기서 잡히는 것은 진입점이 계약을 어겼다는 사실 — 즉 프로그래머의 실수다. 배치를 새로 만들면서 값 검증을 빠뜨렸다면, 운영에 나가기 전 테스트에서 걸린다.
로직이 실제로 의존하는 것만 적는다
전제를 전부 나열하면 노이즈가 된다. 기준은 하나다 — 이 메서드의 로직이 그 전제에 실제로 기대고 있는가.
// ❌ 이 메서드는 couponCode를 쓰지도 않는다
assert command.getCouponCodeOrNull() == null || !command.getCouponCodeOrNull().isBlank();
// ❌ 아무 정보도 주지 않는다
assert command != null;
command != null은 적어봐야 읽는 사람에게 알려주는 게 없다. 바로 다음 줄에서 역참조하니 NPE로 즉시 드러나기 때문이다. assert의 가치는 “이 조건이 깨진 채로도 코드가 한참 굴러가는” 경우에 있다.
원칙 3. 사후조건 assert로 결과를 보장한다
사전조건보다 실질적인 버그를 더 많이 잡는 쪽이다. 계산이나 상태 전이가 끝난 뒤, 결과가 만족해야 할 조건을 적는다.
public ServiceResult<OrderDomain> applyDiscount(ApplyDiscountCommand command) {
BigDecimal originalPrice = order.getTotalPrice();
BigDecimal finalPrice = discountPolicy.apply(originalPrice, command.getCouponCodeOrNull());
assert finalPrice.compareTo(originalPrice) <= 0
: "할인 후 금액이 원래 금액보다 클 수 없다";
assert finalPrice.compareTo(BigDecimal.ZERO) >= 0
: "할인 후 금액은 음수일 수 없다";
return ServiceResult.of(order.changePrice(finalPrice));
}
할인 정책에 새 쿠폰 타입을 추가하다 부호를 잘못 쓰면, 이 assert가 정책 클래스가 아니라 그것을 쓰는 지점에서 즉시 잡아낸다. 이런 종류의 버그는 assert가 없으면 한참 뒤 결제 금액이 이상하다는 제보로 발견된다.
흐름을 나누는 표지가 된다
사전조건과 사후조건을 같이 쓰면 메서드가 자연스럽게 구간으로 나뉜다.
public ServiceResult<OrderDomain> confirmOrder(ConfirmOrderCommand command) {
assert command.getOrderId() != null : "orderId는 진입점에서 검증되어야 한다";
OrderDomain order = orderRepository.findById(command.getOrderId())
.orElseThrow(() -> ErrorCodeException.of(ErrorCode.ORDER_NOT_FOUND));
order.validateConfirmable();
assert order.isPending() : "confirmable 검증을 통과했다면 PENDING 상태여야 한다";
OrderDomain confirmed = order.confirm();
assert confirmed.isConfirmed() : "confirm() 이후에는 CONFIRMED 상태여야 한다";
return ServiceResult.of(orderRepository.save(confirmed));
}
가운데 assert가 “여기까지 왔다면 이건 참이다”를 선언한다. 새로 합류한 사람이 이 메서드를 읽을 때, 각 구간이 무엇을 전제하고 무엇을 만들어내는지 주석 없이 파악할 수 있다. 그리고 이 설명은 테스트가 돌 때마다 사실인지 확인된다.
원칙 4. 잘못된 상태를 조용히 보정하지 않는다
assert의 반대편에는 문제를 발견하고도 그럴듯하게 넘어가는 코드가 있다.
// ❌ 잘못된 값을 조용히 고친다
int quantity = command.getQuantity() <= 0 ? 1 : command.getQuantity();
// ❌ 예외를 삼키고 기본값으로 계속 간다
BigDecimal price;
try {
price = pricingClient.fetch(productId);
} catch (Exception e) {
log.warn("가격 조회 실패", e);
price = BigDecimal.ZERO;
}
두 코드 모두 그 순간에는 아무 일도 일어나지 않는다. 대신 잘못된 값이 정상인 척 DB에 저장되고, 몇 주 뒤 “정산이 안 맞는다”는 제보로 발견된다. 그때는 어느 지점에서 틀어졌는지 추적하기가 훨씬 어렵다. 로그를 남겼더라도 마찬가지다 — 아무도 안 보는 warn 로그는 문제를 숨긴 것과 같다.
quantity <= 0은 진입점에서 이미 걸렀어야 하는 값이다. 여기까지 왔다는 건 계약이 깨졌다는 뜻이므로, 보정할 게 아니라 드러내야 한다.
// ✅ 계약 위반이면 멈춘다
assert command.getQuantity() > 0 : "quantity는 진입점에서 1 이상으로 검증되어야 한다";
int quantity = command.getQuantity();
가격 조회 실패처럼 운영에서도 실제로 일어날 수 있는 일은 assert의 영역이 아니다. 이건 예외로 다룬다. 다만 기준은 똑같다 — 조용히 넘어가지 않는 것.
// ✅ 실패를 드러낸다. 삼키지 않으면 예외가 그대로 올라간다
BigDecimal price = pricingClient.fetch(productId);
기본값으로 대체해야 하는 비즈니스적 이유가 실제로 있다면 그건 보정이 아니라 정책이므로, 그 사실이 코드에 드러나야 한다.
// ✅ 기본값이 정책이라면 이름으로 밝힌다
BigDecimal price = pricingPolicy.fetchOrFallbackToListPrice(productId);
규칙은 하나다. 있을 수 없는 일은 assert로, 일어날 수 있는 실패는 예외로 드러낸다. 어느 쪽이든 숨기지 않는다.
원칙 5. assert를 쓰면 안 되는 곳
반증: JDK 21.0.9 — switch문 assert false -ea:AssertionError·-da:통과(반환 0), switch표현식 default -> { assert false; } 컴파일 실패(에러 1) / 대조군 switch문 throw new AssertionError() -ea:AssertionError·-da:AssertionError, switch표현식 default -> throw new AssertionError() -ea:AssertionError·-da:AssertionError — 증명
여기서 -ea는 assert를 켜는 JVM 옵션이고(enable assertions), -da는 그 반대로 assert를 끄는 옵션이다(disable assertions) — 아무 옵션도 안 주면 꺼진 것과 같다. 위 인용 줄의 “-da:통과”는 assert를 끈 상태에서는 assert false가 아무 일도 없이 그냥 지나갔다는 뜻이고, “-da:AssertionError“는 꺼도 여전히 AssertionError가 났다는 뜻이다.
assert는 운영에서 꺼진다. 이 사실 하나에서 금지 규칙이 전부 파생된다.
외부 입력 검증에 쓰지 않는다
// ❌ 운영에서는 이 줄이 사라진다 — 무방비
assert email.matches(EMAIL_PATTERN);
이건 계층별 검증 표준의 영역이다. “이게 없으면 잘못된 데이터가 통과하는” 검증은 절대 assert로 쓰지 않는다.
부수 효과가 있는 식을 넣지 않는다
// ❌ 개발과 운영의 동작이 달라진다
assert orderRepository.updateStatus(id) > 0;
assert 안의 식은 읽기만 해야 한다. 상태를 바꾸면 -ea 여부에 따라 프로그램이 다르게 동작하고, 이런 버그는 재현조차 어렵다.
도달 불가 분기는 AssertionError를 직접 던진다
// ❌ 운영에서 꺼지면 아무 일 없이 통과한다
default -> { assert false : "처리되지 않은 상태"; }
// ✅ -ea와 무관하게 항상 동작한다
default -> throw new AssertionError("처리되지 않은 주문 상태: " + status);
assert 문과 AssertionError를 던지는 것은 다르다. “여기 도달하면 그 자체로 버그”인 지점은 운영에서도 막아야 하므로 직접 던진다.
자바의 switch는 두 가지 자리에 쓸 수 있다. switch 문은 값을 돌려주지 않아도 되는 자리고, 각 분기가 하던 일을 하고 그냥 끝나도 된다. switch 표현식은 전체가 하나의 값으로 평가돼야 하는 자리다(int v = switch (status) { ... };처럼 변수에 대입하는 경우) — 그래서 모든 분기가 반드시 값을 내놓거나 예외를 던져야 한다. 이 차이는 case 값:을 쓰느냐 case 값 ->를 쓰느냐와는 별개다. 화살표(->) 문법은 switch 문에서도 쓸 수 있고, 위 ❌ 예시의 default -> { assert false; }도 지금은 값을 돌려주지 않는 switch 문 자리에 있어서 컴파일된다.
단 위 ❌ 예시가 성립하는 것은 switch 문일 때다. 실측에서 switch 문의 assert false는 -ea를 켜면 멈추고 끄면 조용히 통과했다(주장대로다). 그런데 switch 표현식에서는 assert false가 컴파일조차 되지 않는다.
error: switch rule completes without providing a value
(switch rules in switch expressions must either provide a value or throw)
컴파일러가 「값을 주거나 던져라」라고 말한다. 표현식 쪽에서는 이 표준이 정한 답을 컴파일러가 이미 강제하고 있어서, 조용히 통과할 자리가 없다.
이전 상태를 참조하는 사후조건은 비용을 확인한다
// 이 줄은 운영에서도 실행된다 (assert 밖이므로)
int before = order.getQuantity();
processOrder(order);
assert order.getQuantity() == before - 1;
Java의 assert는 “이전 값”을 자동으로 잡아주지 않아서 미리 변수에 담아야 하는데, 그 변수를 만드는 코드는 assert가 아니므로 운영에서도 실행된다. 무거운 연산이라면 사후조건을 포기하는 편이 낫다.
원칙 6. -ea는 테스트와 로컬 개발에서만 켠다
assert가 실제로 실행되지 않으면 이 표준 전체가 무의미하다. 다행히 테스트는 기본으로 켜져 있다. 자바 코드를 실제로 컴파일하고 실행해 주는 것은 Gradle이나 Maven 같은 빌드 도구다. 그 안에서 테스트를 돌리는 작업이 Gradle에서는 Test 태스크, Maven에서는 Surefire라는 플러그인이고, 둘 다 “assert를 켤지”를 뜻하는 enableAssertions 기본값이 true다. 그래서 손대지 않아도 테스트에서는 assert가 항상 동작한다.
로컬 실행에는 명시해야 한다.
tasks.named('bootRun') {
jvmArgs('-ea')
}
이 코드는 bootRun이라는 Gradle 작업(스프링 부트 앱을 로컬에서 바로 실행할 때 쓰는 작업)을 찾아, 그 작업으로 앱을 실행할 때 JVM에 -ea 옵션을 추가하라고 설정하는 것이다.
운영에서는 켜지 않는다. 검증이 필요한 것은 이미 예외로 막았고, assert는 개발 단계에서 계약 위반을 잡는 장치이기 때문이다. -ea 옵션의 자세한 동작은 기존 assert 글에 정리해두었다.
단 도달 불가 분기의 throw new AssertionError(...)는 -ea와 무관하게 운영에서도 실행된다(원칙 5). 그게 터졌을 때 어떻게 드러나는지는 예외 처리 표준이 받는다 — 전용 ASSERTION_FAILED 코드로 갈라 로그와 알림에 표시한다. 테스트는 그 코드를 받으면 실패로 다룬다. 다른 500과 같은 코드로 두면 우리 버그가 평범한 서버 오류에 묻히고, 그러면 「개발 단계에서 계약 위반을 잡는다」는 이 표준의 목적이 운영 직전에서 무너진다.
판단 기준 정리
| 상황 | 수단 | 이유 |
|---|---|---|
| 사용자 입력 형식 검증 | Bean Validation(@Valid, 진입점) |
운영에서도 반드시 동작해야 함 |
| 비즈니스 규칙 위반 | ErrorCodeException |
정상적으로 발생 가능한 실패 |
| 진입점이 계약을 지켰는지 | assert (사전조건) |
프로그래머의 실수 감지 |
| 계산·상태 전이 결과 보장 | assert (사후조건) |
프로그래머의 실수 감지 |
| 복잡한 분기 후 상태 선언 | assert (중간 불변조건) |
흐름을 나누는 표지 |
| 도달하면 안 되는 분기 | throw new AssertionError |
운영에서도 막아야 함 |
| 부수 효과가 있는 검사 | assert 금지 | 환경에 따라 동작이 달라짐 |
잘못된 값 보정 (<= 0이면 1로) |
금지 — assert로 드러냄 | 버그가 데이터로 남는다 |
| 예외 삼키고 기본값 사용 | 금지 — 예외를 올림 | 실패가 조용히 묻힌다 |
| 기본값 대체가 정책일 때 | 메서드 이름으로 밝힘 | 보정과 정책은 다르다 |
이 표준을 정하기까지
출발점은 “Command가 들어온 시점에 무엇을 가정할 수 있는가”였다. 기존 assert 글에서 “검증 계층 통과 후는 assert 영역”이라고 정리해뒀으니, Request → Command → Service 구조에서는 Service 진입점이 정확히 그 자리다. 여기서 assert를 쓰면 전제를 코드로 남길 수 있겠다는 생각에서 시작했다.
그런데 assert가 운영에서 꺼진다는 사실이 범위를 강하게 제한했다. “이게 없으면 잘못된 데이터가 통과하는” 검증에는 쓸 수 없다는 뜻이고, 뒤집으면 이미 다른 곳에서 보장된 것을 재확인할 때만 쓸 수 있다. 중복처럼 보이지만 그게 정확히 assert의 용도다 — 사용자 입력이 아니라 파이프라인이 깨졌는지를 잡는 것.
문제는 진입점이 여러 개일 때였다. HTTP는 @Valid가 막아주지만 배치나 CLI는 그 경로를 안 거친다. 그 경로에서 값 검증이 빠지면 Service는 무방비가 되는데, assert는 운영에서 꺼져 있으니 잡아주지도 못한다.
그래서 Service의 계약을 먼저 정의했다. “들어온 값은 이미 형식 검증을 통과했다”를 계약으로 못 박고, 그 계약을 지키는 책임을 각 진입점에 두었다. HTTP면 @Valid, 배치면 배치가, CLI면 CLI가 알아서 값 검증을 마치고 Command를 만든다. 계약이 명확해지니 assert의 역할도 분명해졌다 — 계약이 지켜졌는지를 개발·테스트 단계에서 감시하는 장치.
Command를 순수 데이터 홀더로 둔 것도 같은 이유다. Command가 자체 검증을 하면 방어선이 두 겹이 되는데, 그러면 진입점 검증이 느슨해져도 티가 안 난다. 결국 “어디선가는 막히겠지” 상태가 되고, 진짜 방어선이 어디인지 아무도 모르게 된다. 방어선을 경계 한 곳으로 몰고, 그게 뚫렸는지는 assert가 감시하는 구조가 더 명확했다.
사전조건과 사후조건을 둘 다 쓰기로 한 건 흐름 문서화 때문이다. 실제로 버그를 더 많이 잡는 쪽은 사후조건이지만, 사전조건이 있어야 “이 메서드가 무엇을 전제하는지”가 메서드 상단에서 드러난다. 둘을 같이 쓰면 메서드가 구간으로 나뉘고, 각 구간의 전제와 결과가 눈에 보인다.
정리하다 보니 이 표준의 진짜 목적이 계약 검증이 아니라는 걸 알게 됐다. 계약은 수단이고, 실제로 얻으려는 건 두 가지였다 — 메서드를 처음 보는 사람이 전제와 보장을 빠르게 파악하는 것, 그리고 문제가 생겼을 때 숨기지 않고 즉시 드러내는 것. 그렇게 보니 assert의 반대편에 무엇이 있는지도 분명해졌다. quantity가 0이면 1로 고쳐 넘어가거나, 예외를 삼키고 기본값으로 계속 가는 코드다. 그 코드들은 그 순간엔 조용하지만 잘못된 값을 정상인 척 DB에 남기고, 몇 주 뒤 정산이 안 맞는다는 제보로 돌아온다. 그래서 “조용히 보정하지 않는다”를 별도 원칙으로 세웠다.
마지막으로 남용을 막을 기준이 필요했다. 전제를 전부 적으면 진짜 중요한 것이 묻히기 때문이다. assert command != null 같은 건 바로 다음 줄에서 NPE로 드러나므로 적을 이유가 없다. 정리해보니 기준은 “이 조건이 깨진 채로도 코드가 한참 굴러가는가”였다. 즉시 드러날 것은 적지 않고, 조용히 잘못된 결과를 만들어낼 것만 적는다.
정리
- assert의 목적은 두 가지다. 흐름을 빠르게 파악하게 하는 것, 그리고 문제를 숨기지 않는 것
- Service의 계약은 “값 검증은 끝났다”이다. 그 계약을 지키는 건 각 진입점(Controller, 배치, CLI)의 책임이다
- Command는 순수 데이터 홀더다. 방어선을 경계 한 곳에 두어야 책임이 흐려지지 않는다
- 사전조건 assert는 계약을 명시한다. 잡히는 것은 사용자 입력 오류가 아니라 진입점의 계약 위반이다
- 사후조건 assert는 계산 결과를 보장한다. 실제 버그는 이쪽에서 더 많이 잡힌다
- 적는 기준은 “깨진 채로도 코드가 한참 굴러가는가”이다. 즉시 드러날 것은 적지 않는다
- 잘못된 상태를 조용히 보정하지 않는다. 있을 수 없는 일은 assert로, 일어날 수 있는 실패는 예외로 드러낸다
- 외부 입력 검증과 부수 효과에는 쓰지 않는다. 운영에서 꺼지기 때문이다
- 도달 불가 분기는
AssertionError를 직접 던진다. 운영에서도 막아야 한다 -ea는 테스트와 로컬에서만 켠다. 테스트는 기본값이 이미 켜져 있다
자신만의 철학을 만들어가는 중입니다.
댓글남기기