클래스를 쓰는 이유

Hermaeus Mora · · Backend

1. 캡슐화

가장 핵심적인 이유. 변수를 특정 메서드로만 제어하기 위함이다. 예를 들어 아무 함수나 balance 변수에 접근할 수 있다면 잔고가 음수가 되는 경우가 발생할 수 있다. 이때 withdraw 메서드만 balance 변수에 접근하도록 강제하는 것이 캡슐화이다.

public final class Account {
    private long balance; // 외부 접근 불가
 
    public void withdraw(long amount) {
        if (amount <= 0)        throw new IllegalArgumentException("amount must be positive");
        if (amount > balance)   throw new IllegalStateException("insufficient funds");
        balance -= amount;
    }
 
    public long balance() { return balance; }
}

2. 다형성

다형성은 호출자가 구현체를 몰라도 사용할 수 있도록 한다. 예를 들어 Java의 InputStream 추상 클래스의 구현체는 파일, HTTP 응답 바디, gzip 등으로 다양하다.

만약 countLines 함수를 아래와 같이 작성한다면

static int countLines(InputStream in) throws IOException {
    byte[] buf = new byte[4096];
    int lines = 0, k;
    while ((k = in.read(buf)) != -1) {
        for (int i = 0; i < k; i++) if (buf[i] == '\n') lines++;
    }
    return lines;
}

해당 함수는 입력 파라미터의 구현체가 파일인지, HTTP 응답 바디인지, gzip인지 몰라도 사용할 수 있다.

// gzip 파일
...
try (InputStream file = new GZIPInputStream(new FileInputStream("logs.json.gz"))) {
    System.out.println(countLines(file));
}
 
// HTTP 응답 바디
...
HttpResponse<InputStream> resp = HttpClient.newHttpClient().send(
    HttpRequest.newBuilder(URI.create("https://api.github.com")).build(),
    HttpResponse.BodyHandlers.ofInputStream());
try (InputStream body = resp.body()) {
    System.out.println(countLines(body));
}
 
// 소켓
...
try (InputStream sock = socket.getInputStream()) {
    System.out.println(countLines(sock));
}

다른 예로, 두 클래스를 합성하는 경우가 있다.

interface Engine { void idling(); }
interface Transmission { void buildPressure(); }
 
class Car {
    private final Engine engine;
    private final Transmission transmission;
 
    public Car(Engine e, Transmission t) {
        this.engine = e;
        this.transmission = t;
    }
    
    public void start() {
        this.engine.idling();
        this.transmission.buildPressure();
    }
}
 
...
new Car(new GasolineEngine(), new ManualTransmission()).start();
new Car(new ElectricEngine(), new AutoTransmission()).start();

이 경우에도 Car는 Engine과 Transmission이 어떻게 구현되었는지 몰라도 사용할 수 있다.

3. 상속

위의 countLines 메서드는 스트림을 소비하기 때문에 다른 작업을 추가하려면 다시 스트림을 읽어야 한다. 만약 읽는 동시에 카운트를 한다면 더 효율적일 것이다. 이때 상속을 사용하여 InputStream 의 모든 기능을 그대로 쓰면서 기능을 추가할 수 있다.

class LineCountingInputStream extends FilterInputStream {
    private long lines = 0;
 
    public LineCountingInputStream(InputStream in) { super(in); }
 
    @Override
    public int read() throws IOException {
        int b = in.read();
        if (b == '\n') lines++;
        return b;
    }
 
    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        int n = in.read(b, off, len);
        if (n > 0) {
            for (int i = off; i < off + n; i++) {
                if (b[i] == '\n') lines++;
            }
        }
        return n;
    }
 
    public long lines() { return lines; }
}
 
try (LineCountingInputStream in = new LineCountingInputStream(new GZIPInputStream(new FileInputStream("logs.json.gz")))) {
	byte[] data = in.readAllBytes();
    doSomething(data);
    System.out.println(in.lines());
}