클래스란

클래스란 특정한 타입을 지정하는것을 의미합니다.

우리는 이미 자바에서 제공하는 Primitive Type을 사용하고있습니다. (byte,short,int,long,folat,double,boolean,char)

 

이 Type은 각각 특정한 목적을 가집니다.

  1. 정수를 표현하기 위한 byte,short,int ,long (1,2,4,8 byte로 표현가능한 크기가 다름)
  2. 실수를 표현하기 위한 float , double (4,8 byte로 표현가능한 크기가 다름)
  3. 참,거짓을 표현하기 위한 boolean
  4. 문자를 표현하기 위한 char

이렇게 타입은 특정한 목적을 가지고 설계됩니다.

 

우리가 만드는 클래스또한 타입을 정의하는 것으로 , 특정한 목적을 가지고 연산을 수행하게 됩니다.

따라서 "클래스를 작성한다"는 것은 '특정한 목적을 갖는 타입을 정의한다" 는 의미입니다.

클래스 설계

클래스를 만들떄 이 클래스를 정의하는 , 즉 근간이되는 것을 Implementation이라 합니다.

 

또한 Implementation은  Abstraction Barrier로 보호되어야 합니다. 유리수를 예시로 들겠습니다.

public class Main {

    public static void main(String[] args) {

        RationalNumber rationalNumber = new RationalNumber(1, 2);

        System.out.println(rationalNumber.numerator + "/" + rationalNumber.denominator);

    }
}

class RationalNumber {

    int numerator; // 분자
    int denominator; // 분모

    public RationalNumber(int numerator, int denominator) {
        this.numerator = numerator;
        this.denominator = denominator;
    }

}

이 클래스는 유리수라는 타입을 지정한 것입니다.

 

유리수의 정의는 정수들의 비로 나타낼수 있는 수를 의미합니다.

 

따라서 2개의 정수를 받아야 하며 이 정수가 유리수를 정의하기 떄문에 Implementation에 속합니다.

하지만 이 클래스는 치명적인 문제점을 갖습니다.

rationalNumber.denominator=5;
System.out.println(rationalNumber.numerator + "/" + rationalNumber.denominator);

이런식으로 언제든지 rationalNumber의 필드변수에 접근하여 값을 바꿀수 있습니다.

 

이렇게 된다면 기존에 정의했던 유리수에서 다른 유리수로 변경되게 됩니다.

 

이렇게 Implementation에 속하는 것들은 의도치 않게 변경된다면 기존의 정의와는 아예 달라지기 떄문에

Abstraction Barrier로 보호할 필요가 있습니다.

public class Main {

    public static void main(String[] args) {

        RationalNumber rationalNumber = new RationalNumber(1, 2);

        System.out.println(rationalNumber.getNumerator() + "/" + rationalNumber.getDenominator());

        //rationalNumber.denominator = 5; 오류발생 !

    }
}

class RationalNumber {

    private int numerator; // 분자
    private int denominator; // 분모

    public RationalNumber(int numerator, int denominator) {
        this.numerator = numerator;
        this.denominator = denominator;
    }

    public int getNumerator() {
        return numerator;
    }

    public int getDenominator() {
        return denominator;
    }

}

이 코드는 유리수를 정의하는 정수를 Abstraction Barrier로 감싼것으로
클래스의 필드변수에 바로 접근하는 것이 아니라 getter를 사용해 우회적으로 접근하게 합니다.

(getter또한 Implementation 에 속합니다.)

 

 

하지만 여전히 치명적인 문제점을 갖습니다. 유리수에 정의에 대해서 누락된 것들이 있기 떄문입니다.

  1. 유리수는 분모에 0이 들어올수 없다.
  2. 분모 분자의 비가 같다면 동일한 유리수다.
  3. 정수가 음수로 들어왔을때의 처리

이 조건들 또한 충족시킬수 있도록 보완하겠습니다.

 

1. 유리수는 분모에 0이 들어올수 없다.

RationalNumber rationalNumber = new RationalNumber(1, 0);
System.out.println(rationalNumber.getNumerator() + "/" + rationalNumber.getDenominator());

유리수는 분모에 0이 들어올수 없음에도 다음과 같이 동작하는 것을 볼 수 있습니다.

따라서 타입 정의에 맞게 바꿔주겠습니다.

public RationalNumber(int numerator, int denominator) {

        if (denominator == 0) {
            throw new IllegalArgumentException("올바른 분모 입력이 아닙니다");
        }

        this.numerator = numerator;
        this.denominator = denominator;
    }

생성자를 통해 객체가 만들어질떄 분모에 대한 제한조건을 거는 것 입니다.

이는 PreCondition을 체크하는 것과 동일합니다.

 

2. 분모 분자의 비가 같다면 동일한 유리수다.

RationalNumber rationalNumber = new RationalNumber(1, 2);
RationalNumber rationalNumber2 = new RationalNumber(2, 4);

System.out.println(rationalNumber.equals(rationalNumber2));

유리수에서 1/2와 2/4는 동일한 비를 같기떄문에 같은유리수지만 실행결과는 false인 것을 알 수 있습니다.

따라서 다음과 같이 수정하겠습니다.

 

public RationalNumber(int numerator, int denominator) {

        if (denominator == 0) {
            throw new IllegalArgumentException("올바른 분모 입력이 아닙니다");
        }

        this.numerator = numerator;
        this.denominator = denominator;

        nomarize();
    }


private void nomarize() {
        int number = gcd(this.numerator, this.denominator);

        this.numerator = this.numerator / number;
        this.denominator = this.denominator / number;
    }

    private int gcd(int number1, int number2) {

        if (number2 == 0) {
            return number1;
        }

        return gcd(number2, number1 % number2);
    }

@Override
public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        RationalNumber that = (RationalNumber) o;
        return numerator == that.numerator && denominator == that.denominator;
    }

2번의 조건을 만족하기 위해 유리수를 기약분수의 형태로 만들어줬습니다.

 

기약분수형태로 만들기 위해 최대공약수를 찾는 gcd 알고리즘을 추가한후 nomarize를 통해 기약분수 꼴로 변경합니다.

이제 1/2 와 2/4 를 비교시 true 값이 나옵니다.

 

 

3. 정수가 음수로 들어왔을때의 처리

RationalNumber rationalNumber3 = new RationalNumber(-1, 2);
System.out.println(rationalNumber3.getNumerator() + "/" + rationalNumber3.getDenominator());

음수가 들어왔을때 출력되는 형식이 다른것을 알 수 있습니다.

따라서 이 양식을 맞춰주도록 하겠습니다.

private void nomarize() {

        int number = gcd(this.numerator, this.denominator);

        this.numerator = this.numerator / number;
        this.denominator = this.denominator / number;

        if (this.denominator < 0) {
            this.denominator *= -1;
            this.numerator *= -1;
        }

    }


@Override
    public String toString() {
        return numerator + "/" + denominator;
    }

이제부터는 음수가 들어갔을떄 모두 동일한 형식을 갖게 됩니다.

 

추가적으로 toString 메서드를 Override해 출력에 편의성을 증가시켰습니다.

지금은 getter를 이용한 다른 연산이 없기 떄문에 getter또한 삭제 가능합니다.

  public static void main(String[] args) {

        RationalNumber rationalNumber3 = new RationalNumber(-1, 2);
        RationalNumber rationalNumber4 = new RationalNumber(1, -2);
        RationalNumber rationalNumber5 = new RationalNumber(-16, -32);

        System.out.println(rationalNumber3);
        System.out.println(rationalNumber4);
        System.out.println(rationalNumber5);
        System.out.println(rationalNumber3.equals(rationalNumber4));
    }

이렇게 유리수 클래스를 설계해봤습니다.

 

클래스를 만든다는 것은 특정한 타입을 정의하는 것으로  이 타입을 만든 목적에 맞게 설계해줘야 합니다.

또한 이 타입의 가져서는 안되는 값들에 대해서도 충분한 고민이 필요합니다.

 

이외에도 간략하게 자동차 클래스를 설계한다면

 

자동차를 정의내리는 바퀴,핸들,이름 뿐만 아니라
자동차라는 타입이 갖는 필수적인 연산들 (가속 , 브레이크 등등)을 Implementation으로 두고 

타입을 설계해 나가면 될 것입니다. 

 

도움이 되셨다면 좋겠습니다.

+ Recent posts