Java의 assert는 개발자들 사이에서 “있지만 거의 쓰지 않는” 기능으로 취급받습니다. 왜일까요?

실제로 assert는 강력한 개발 도구이지만, 기본적으로 비활성화되어 있고, 그 의도가 명확하지 않으면 남용될 수 있기 때문입니다. 이 글에서는 assert를 올바르게 이해하고, 언제 써야 하고 언제 피해야 하는지 명확히 구분하는 방법을 알아봅시다.

Assert의 기본 개념

Assert는 무엇인가?

Assert는 특정 조건이 true인지 확인하는 검증 도구입니다:

// 기본 형태
assert 조건식;

// 메시지와 함께
assert 조건식 : "에러 메시지";

조건이 false일 때 java.lang.AssertionError가 발생합니다.

핵심 특징

graph LR A["Assert 특징"] --> B["기본 비활성화
-ea 옵션 필요"] A --> C["Zero 오버헤드
운영환경"] A --> D["AssertionError 발생
회복 불가능"] A --> E["개발/테스트 용도
검증 도구"] style B fill:#4a5568,stroke:#e2e8f0 style C fill:#4a5568,stroke:#e2e8f0 style D fill:#4a5568,stroke:#e2e8f0 style E fill:#4a5568,stroke:#e2e8f0

“기본적으로 비활성화” 라는 점이 가장 중요합니다.

# Assert가 작동하지 않음 (기본값)
java MyClass

# Assert가 활성화됨
java -ea MyClass

# 특정 패키지만 활성화
java -ea:com.mycompany... MyClass

Assert vs 예외 처리: 명확한 경계 그리기

Assert와 예외 처리의 가장 큰 차이는 “데이터의 출처”입니다.

graph LR A["데이터 검증"] --> B["내부 데이터
내가 생성/통제"] A --> C["외부 데이터
API/File/Network"] B --> D["Assert 사용"] C --> E["예외 처리 사용"] style D fill:#48bb78,stroke:#e2e8f0 style E fill:#f56565,stroke:#e2e8f0

Assert를 사용해야 할 경우

원칙: 내가 완전히 통제할 수 있는 코드 내에서만 사용합니다.

// ✅ 올바른 Assert 사용

// 1. 내가 호출하는 private 메서드
private void processOrder(Order order) {
    assert order != null : "Order는 null이 아니어야 함";
    // ... 처리 로직
}

// 2. 생성자에서의 불변식 검증
private User(String name, int age) {
    assert name != null && !name.isEmpty() : "이름은 비어있으면 안 됨";
    assert 0 <= age && age <= 150 : "나이는 0~150 사이여야 함";
    this.name = name;
    this.age = age;
}

// 3. 알고리즘 후 결과 검증
public void sort(int[] arr) {
    // 정렬 로직...
    
    // 불변식: 배열이 정렬되어 있어야 함
    for (int i = 0; i < arr.length - 1; i++) {
        assert arr[i] <= arr[i + 1] : "배열이 정렬되지 않음";
    }
}

예외 처리를 사용해야 할 경우

원칙: 외부 데이터나 다른 개발자가 호출할 가능성이 있는 경우입니다.

// ✅ 올바른 예외 처리

// 1. Public API 메서드
public User createUser(String name, int age) {
    if (name == null || name.isEmpty()) {
        throw new IllegalArgumentException("이름은 비어있으면 안 됨");
    }
    if (age < 0 || age > 150) {
        throw new IllegalArgumentException("나이는 0~150 사이여야 함");
    }
    return new User(name, age);
}

// 2. API 요청 처리
@PostMapping("/users")
public User createUserFromApi(@RequestBody UserDto dto) {
    // API 요청은 신뢰할 수 없는 외부 데이터
    if (dto == null) {
        throw new BadRequestException("요청 본문이 비어있음");
    }
    // ... 검증 로직
}

// 3. File I/O
public List<User> loadUsersFromFile(String filename) {
    try {
        File file = new File(filename);
        if (!file.exists()) {
            throw new FileNotFoundException(filename);
        }
        // ... 파일 읽기
    } catch (IOException e) {
        throw new RuntimeException("파일 읽기 실패", e);
    }
}

현실의 경계선: 애매한 경우들

실무에서는 명확한 흑과 백만 존재하지 않습니다. 다음과 같은 경계선상의 경우들을 살펴봅시다:

경우 1: 같은 팀 개발자가 호출하는 메서드

public class StringUtils {
    public static String normalize(String input) {
        // 같은 팀 개발자가 호출
        // assert? 예외?
        
        assert input != null && !input.isEmpty() 
            : "정규화할 문자열은 비어있으면 안 됨";
        return input.trim().toLowerCase();
    }
}

결론: Assert 사용 가능합니다. 팀 개발자가 -ea 옵션으로 테스트하면 오류를 빠르게 발견할 수 있습니다.

경우 2: 테스트 코드에서의 호출

@Test
void testUserCreation() {
    // 테스트 코드는 내부 코드
    User user = new User("John", 25);
    
    // 이 생성자는 assert를 가질 수 있음
    // 테스트 프레임워크는 보통 -ea로 실행됨
    assertThat(user.getName()).isEqualTo("John");
}

결론: Assert 사용 가능합니다. 테스트 코드는 같은 팀에서 작성/유지보수하므로 내부로 간주됩니다.

경우 3: 검증 계층을 통과한 후

@Controller
public class UserController {
    @PostMapping("/users")
    public User createUser(@RequestBody @Valid UserDto dto) {
        // @Valid로 이미 검증됨
        // 이 후는 안전한 데이터
        
        // 이 지점에서는 assert 사용 가능
        assert dto.getName() != null;
        assert dto.getAge() >= 0;
        
        return new User(dto.getName(), dto.getAge());
    }
}

결론: 검증 계층 통과 후는 assert 영역입니다. 더 이상의 방어 코드는 필요하지 않습니다.

graph TB A["외부 요청"] --> B["검증 계층
@Valid/if-throw"] B --> |검증 실패| C["BadRequestException"] B --> |검증 성공| D["내부 처리"] D --> |논리 오류| E["AssertionError
assert 체크"] D --> |정상| F["결과 반환"] style C fill:#f56565,stroke:#e2e8f0 style E fill:#ed8936,stroke:#e2e8f0 style F fill:#48bb78,stroke:#e2e8f0

Assert 올바르게 사용하기: 실무 가이드

원칙 1: 반드시 -ea 옵션으로 개발/테스트

Assert를 사용하려면 팀 차원에서 다음을 설정해야 합니다:

IDE 설정 (IntelliJ):

  • Run → Edit Configurations → VM options에 -ea 추가

Maven:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <argLine>-ea</argLine>
    </configuration>
</plugin>

Gradle:

test {
    jvmArgs += '-ea'
}

CI/CD 파이프라인:

# 테스트 실행 시 반드시 -ea 포함
java -ea -jar application.jar

원칙 3: 불변식 검증에 활용

Assert의 가장 강력한 사용법은 알고리즘 불변식 검증입니다:

public int binarySearch(int[] sortedArr, int target) {
    // 전제조건: 배열은 정렬되어 있어야 함
    assert isSorted(sortedArr) : "배열이 정렬되어 있지 않음";
    
    int left = 0, right = sortedArr.length - 1;
    
    while (left <= right) {
        int mid = left + (right - left) / 2;
        
        if (sortedArr[mid] == target) {
            return mid;
        } else if (sortedArr[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    
    // 사후조건: 없으면 -1 반환
    assert left == sortedArr.length || right == -1;
    
    return -1;
}

// 배열이 정렬되어 있는지 확인
private boolean isSorted(int[] arr) {
    for (int i = 0; i < arr.length - 1; i++) {
        if (arr[i] > arr[i + 1]) {
            return false;
        }
    }
    return true;
}

원칙 4: Fail Fast 철학

Assert가 true여야 한다면, false 상황에서는 빠르게 실패해야 합니다:

// ❌ 나쁜 예: 조용히 실패
private void transfer(Account from, Account to, int amount) {
    assert from != null;
    assert to != null;
    
    // null인 경우 여기서 NullPointerException 발생
    int balance = from.getBalance();
    // ... 이후 로직이 실행될 수 없음
}

// ✅ 좋은 예: AssertionError로 명확하게 실패
private void transfer(Account from, Account to, int amount) {
    assert from != null : "발신자 계정이 null";
    assert to != null : "수신자 계정이 null";
    assert amount > 0 : "이체 금액은 양수여야 함";
    
    // 여기 도달 시 모든 전제조건이 만족됨
    from.withdraw(amount);
    to.deposit(amount);
}

Java는 assert가 기본적으로 비활성화된 이유

이것은 Java의 철학과 관련이 있습니다:

1. 성능 철학

Java는 운영 환경 성능을 최우선으로 합니다. Assert 체크로 인한 성능 저하를 완전히 제거하려면 기본 비활성화가 필수입니다.

2. 명시적 의도

Assert를 기본 비활성화함으로써 개발자에게 “나는 이 코드를 검증하겠다”는 의도를 명시하게 합니다. 실수로 사용되는 것을 방지합니다.

3. 설계 철학의 차이

  • Python/C: Assert를 기본 도구로 간주 (항상 활성화)
  • Java: Assert를 선택적 도구로 간주 (활성화 선택)

다른 언어와의 비교:

# Python: 기본 활성화 (python -O로 제거 가능)
assert condition, "메시지"
// C++: 기본 활성화 (#define NDEBUG로 제거)
assert(condition);
// Java: 기본 비활성화 (java -ea로 활성화)
assert condition : "메시지";

결론 및 체크리스트

Assert를 올바르게 사용하기 위한 최종 체크리스트입니다:

Assert 사용 전 확인 사항

✅ Assert를 사용해도 좋은 경우:

  • 내가 완전히 통제하는 메서드인가?
  • 알고리즘의 불변식을 검증하는 것인가?
  • 이미 검증 계층을 통과한 데이터인가?
  • 팀에서 -ea 옵션으로 개발/테스트하기로 약속했는가?

❌ Assert를 피해야 하는 경우:

  • Public API 메서드인가?
  • 외부 API, 파일, 네트워크 데이터인가?
  • 사용자 입력이나 설정값인가?
  • -ea 옵션이 보장되지 않는가?

실무 팁

  1. 팀 규약 수립: “우리 팀은 디버그 빌드에서 -ea를 항상 활성화한다”
  2. 코드 리뷰: PR에서 assert 사용이 적절한지 검토
  3. 문서화: 메서드의 전제조건과 사후조건을 명확히 주석으로

당신의 경험을 공유해보세요

질문: 당신의 프로젝트에서 assert를 활용하고 있나요?

  • 팀 차원에서 -ea를 강제하고 있나요?
  • Assert와 예외 처리를 어떻게 구분하고 있나요?
  • 혹시 assert로 인해 겪은 문제가 있나요?

댓글로 당신의 경험과 의견을 공유해주세요! 다른 개발자들의 실무 경험이 정말 도움이 됩니다.


자신만의 철학을 만들어가는 중입니다.
최상단으로 이동했습니다!
확대 이미지

댓글남기기