안동민 개발노트

본문 시작

System·Math·Random 기본 도구

프로세스 환경, 수학 계산, 재현 가능한 의사 난수를 각각 작은 예제로 구분합니다.

표준 라이브러리는 자주 필요한 기능을 이미 구현해 제공합니다.

System은 현재 Java 프로세스의 입출력과 환경에, Math는 상태 없는 수치 계산에, Random은 의사 난수 순서에 접근합니다.

세 도구는 편리하지만 역할이 다르므로 한 객체처럼 묶어 생각하지 않습니다.

이전 문서에서 기본형과 래퍼를 배웠고, 이 문서가 끝나면 실행 환경의 값·결정적 계산·외부에서 주어지는 난수를 구분할 수 있습니다.

타입 자체를 조사하는 Class와 리플렉션은 ch28에서 처음부터 배웁니다.


System의 입출력과 속성

System.out은 일반 출력, System.err는 오류 출력, System.in은 표준 입력입니다.

System.getProperty는 현재 JVM의 시스템 속성을 읽습니다. 일부 속성은 실행 중에도 변경될 수 있으므로 모두 시작 시점에 고정된 값으로 보지는 않습니다.

src/SystemBasics.java — 정상 최소 예제
public class SystemBasics {
    public static void main(String[] args) {
        System.out.println("java=" + System.getProperty("java.version"));
        System.out.println("os=" + System.getProperty("os.name"));
        System.out.println("separator=" + System.lineSeparator().length());
        System.err.println("diagnostic=sample");
    }
}

Java 버전과 운영체제 이름은 컴퓨터마다 다릅니다.

프로그램의 정답으로 문자열 전체를 고정하지 말고 실제로 필요한 조건만 검사합니다.

업무 메서드 곳곳에서 전역 환경을 직접 읽으면 입력이 숨으므로, 시작점에서 읽어 필요한 객체에 값으로 전달하는 편이 좋습니다.


경과 시간 측정

System.nanoTime()은 두 시점 사이의 경과 시간을 재는 기준입니다.

달력 시각이나 파일에 저장할 현재 날짜로 해석하지 않습니다.

src/ElapsedTime.java
public class ElapsedTime {
    public static void main(String[] args) {
        long start = System.nanoTime();

        long total = 0;
        for (int number = 1; number <= 100_000; number++) {
            total += number;
        }

        long elapsed = System.nanoTime() - start;
        System.out.println("total=" + total);
        System.out.println("elapsed-nonnegative=" + (elapsed >= 0));
    }
}

경과 나노초의 실제 숫자는 실행마다 달라집니다.

짧은 코드 한 번의 시간으로 성능 결론을 내리면 JIT 준비, 운영체제 스케줄링, 다른 프로세스의 영향을 분리하지 못합니다.

정확한 벤치마크는 ch38에서 다룹니다.


Math의 결정적 계산

여기서 사용하는 Math.ceil, min, max, abs는 같은 입력에 같은 계산 결과를 냅니다. Math.random()처럼 난수 생성기를 사용하는 메서드까지 이 설명을 일반화하지는 않습니다.

src/MathBasics.java
public class MathBasics {
    public static void main(String[] args) {
        int postCount = 47;
        int pageSize = 25;

        int pages = (int) Math.ceil((double) postCount / pageSize);
        int bounded = Math.min(Math.max(postCount, 1), 600);
        int distance = Math.abs(postCount - 50);

        System.out.println("pages=" + pages);
        System.out.println("bounded=" + bounded);
        System.out.println("distance=" + distance);
    }
}
pages=2
bounded=47
distance=3

ceil은 올림, min·max는 작은 값과 큰 값을 선택하고 abs는 절댓값을 구합니다.

금액처럼 정확한 십진 계산이 필요한 값에는 부동소수점 double보다 BigDecimal 같은 별도 표현을 검토합니다.


Random과 시드

의사 난수 생성기는 내부 상태에서 다음 값을 계산합니다.

생성할 때 같은 시드를 주면 같은 구현에서 같은 순서를 다시 얻을 수 있어 예제를 재현하기 쉽습니다.

src/SeededPicker.java
import java.util.Random;

public class SeededPicker {
    public static void main(String[] args) {
        String[] categories = {"notice", "question", "free"};
        Random random = new Random(42);

        int first = random.nextInt(categories.length);
        int second = random.nextInt(categories.length);

        System.out.println("first=" + categories[first]);
        System.out.println("second=" + categories[second]);
    }
}

nextInt(bound)는 0 이상 bound 미만을 반환하므로 배열 인덱스로 사용할 수 있습니다.

배열 길이가 0이면 유효한 범위가 없으므로 호출 전에 입력을 확인해야 합니다.

고정 시드는 디버깅과 예제 재현에 적합하지만 비밀번호 재설정 토큰이나 인증 코드를 만들기에는 예측 가능합니다.

보안 목적에는 SecureRandom을 사용하고 운영 정책에 맞는 엔트로피와 저장 방식을 정합니다.

계산 입력과 난수의 호출 상태를 구별한다

MathBasics의 명시적 수치 입력은 pages2 bounded47 distance3을 결정한다. SeededPicker의 Random42는 같은 객체에 nextInt3를 두 번 호출하여 free 다음 notice를 고른다.

MathBasics와 SeededPicker는 별도 프로그램입니다. 같은 숫자 인수를 반복하는 계산과, 호출할 때 내부 상태가 진행하는 난수를 구별합니다.

명시적 계산 입력과 난수의 시드 및 호출 순서
실행 지점결과를 정하는 입력·상태출력
MathBasics게시글 47개, 페이지 크기 25. ceil, min/max, abs에 값을 전달합니다.
pages=2
bounded=47
distance=3
난수 첫 호출new Random(42)의 첫 nextInt(3)은 인덱스 2입니다.first=free
난수 둘째 호출같은 객체의 다음 nextInt(3)은 인덱스 0입니다. 시드를 다시 설정하지 않습니다.second=notice
MathBasics
결과를 정하는 입력·상태: 게시글 47개, 페이지 크기 25. ceil, min/max, abs에 값을 전달합니다.
출력:
pages=2
bounded=47
distance=3
난수 첫 호출
결과를 정하는 입력·상태: new Random(42)의 첫 nextInt(3)은 인덱스 2입니다.
출력: first=free
난수 둘째 호출
결과를 정하는 입력·상태: 같은 객체의 다음 nextInt(3)은 인덱스 0입니다. 시드를 다시 설정하지 않습니다.
출력: second=notice

새 Random(42)에 같은 메서드 호출 순서를 적용하면 같은 난수 순서를 재현합니다. 같은 객체를 계속 호출하는 것은 처음부터 재생하는 것과 다릅니다.

계산 비교는 여기서 사용한 Math 메서드에 한정합니다. Math.random()은 난수 생성기를 사용합니다. 일반 Random은 보안 토큰 용도가 아닙니다.


배열 복사

System.arraycopy는 한 배열의 연속 구간을 다른 배열로 복사합니다.

시작 위치와 길이의 단위는 원소 개수입니다.

src/ArrayCopyBasics.java
import java.util.Arrays;

public class ArrayCopyBasics {
    public static void main(String[] args) {
        int[] source = {20, 30, 40, 50};
        int[] target = new int[2];

        System.arraycopy(source, 1, target, 0, 2);
        System.out.println(Arrays.toString(target));
    }
}
[30, 40]

잘못된 시작 위치나 너무 긴 길이는 배열 범위 예외를 만듭니다.

단순 반복문이 더 읽기 쉬운 작은 변환이라면 반복문을 사용하고, 연속 구간 복사라는 의도가 분명하면 arraycopy 또는 Arrays.copyOfRange를 선택합니다.

arraycopy는 원본의 두 값을 대상 슬롯에 복사한다

System.arraycopy(source,1,target,0,2)는 source1의30을 target0에, source2의40을 target1에 복사한다. 원본20,30,40,50은 유지되고 대상만0,0에서30,40이 된다.

System.arraycopy(source, 1, target, 0, 2)에서 시작 인덱스는 원본 1, 대상 0, 길이는 2개입니다.

원본과 대상의 두 인덱스 대응 원본 source의 1번30과 2번40이 대상 target의 0번과 1번으로 각각 복사된다. 원본의 네 값20,30,40,50은 남고 대상은30,40이 된다. 연결선은 값 복사이며 객체 이동이나 참조 공유가 아니다. 값 복사 값 복사 source · 복사 뒤에도 원본 유지 [0] 20 [1] 30 [2] 40 [3] 50 target [0] 30 [1] 40
원본 배열 source
[20, 30, 40, 50]. 복사 후에도 이 네 값이 유지됩니다.
첫 번째 대응
source[1]의 30을 target[0]에 복사합니다.
두 번째 대응
source[2]의 40을 target[1]에 복사합니다.
대상 배열 target
new int[2]의 [0, 0]이 [30, 40]으로 바뀝니다.

원문의 출력은 [30, 40]입니다. 값은 복사되며 원본에서 지워지지 않습니다. 대상 배열의 길이 2도 바뀌지 않습니다.


도구 선택 기준

질문도구
프로세스의 출력·속성이 필요한가System
같은 입력의 수치 결과가 필요한가Math
난수 순서가 필요한가Random
예측하기 어려운 보안 난수가 필요한가SecureRandom
타입의 필드·메서드를 실행 중 조사하는가ch28의 Class·리플렉션

연습 문제

1부터 100 사이의 게시글 수를 무작위로 하나 고르고, 페이지당 25개씩 보여 줄 때 몇 페이지가 필요한지 올림해 출력하세요.

Random(7)을 사용해 반복 실행 결과를 재현하고, 결과가 항상 1~100 범위인지 Math.min·Math.max 없이 조건문으로 확인하세요.