본문 바로가기

Java

Java (클래스)

클래스의 설명에 앞서 객체지향 프로그래밍의 개념을 알아보는 것이 클래스를 더불어 자바를 이해하는 데 도움이 될 것이기에 객체지향 프로그래밍을 설명하겠습니다.

 

이 이미지는 객체 지향 프로그래밍(OOP)의 개념을 설명하는 것으로  객체 지향 프로그래밍은 컴퓨터 프로그래밍의 패러다임 중 하나로, 프로그램을 객체라는 기본 단위로 구성하고, 이 객체들이 서로 상호작용하는 방식으로 프로그램의 동작을 설계하는 방법입니다.

 '자동차' 객체는 '운전자'가 사용할 수 있는 여러 자동차 타입(예: K3, 아반떼, 테슬라 모델3)으로 이루어져 있으며, 각 자동차 타입은 '자동차'라는 일반적인 개념을 상속받아 특정 속성과 기능을 가지고 있는 것으로 표현되어 있습니다.

객체 지향 프로그래밍에서는 이러한 객체들이 각각의 속성(데이터 필드)과 행동(메서드)을 가집니다. 객체들은 '클래스'라는 설계도를 바탕으로 생성되며, 이 클래스는 객체의 공통적인 속성과 메서드를 정의합니다. 예를 들어, '자동차' 클래스는 모든 자동차가 가지고 있는 공통적인 속성(예: 바퀴, 엔진)과 행동(예: 가속, 정지)을 정의할 것입니다.

OOP의 핵심 개념에는 다음과 같은 것들이 포함됩니다:

속성과 메서드:  "모델명," "해상도," "가격," "색상" 등등은 클래스의 속성(또는 멤버 변수)을 나타냅니다. 객체는 이러한 속성을 가질 수 있으며, 이러한 속성은 객체의 상태를 나타냅니다. 또한, 객체는 특정 동작을 수행하는 메서드를 가질 수 있습니다. 메서드는 객체의 행위를 정의하고, 객체 간의 상호 작용을 표현합니다.

캡슐화: 객체 지향 프로그래밍은 데이터와 데이터를 다루는 메서드를 하나로 묶는 캡슐화를 강조합니다. 클래스 내부의 상세 구현은 감추고, 외부에서는 클래스의 공개 인터페이스만 접근할 수 있도록 설계합니다. 이렇게 함으로써 코드의 응집성을 높이고 데이터를 보호할 수 있습니다.

상속: 객체 지향 프로그래밍에서는 상속을 통해 기존 클래스의 특성을 재사용하고 확장할 수 있습니다. 새로운 클래스는 기존 클래스의 속성과 메서드를 상속받아 사용할 수 있으며, 이를 통해 코드의 재사용성과 확장성을 높일 수 있습니다.

다형성: 다형성은 객체 지향 프로그래밍의 중요한 특징 중 하나로, 서로 다른 클래스의 객체가 동일한 인터페이스를 공유하고 있을 때 다양한 방식으로 동작할 수 있는 능력을 나타냅니다. 이로써 객체 간의 유연한 상호 작용이 가능해집니다.

 

클래스 

public class _01_Class {
    public static void main(String[] args) {

        // 첫 번째 제품
        String modelName = "까망이";
        String resolution = "FHD";
        int price = 200000;
        String color = "블랙";

        // 새로운 제품을 개발
        String modelName2 = "하양이";
        String resolution2 = "UHD";
        int price2 = 300000;
        String color2 = "화이트";


        // 또 다른 제품을 개발?? -> 코드가 길어짐


        BlackBox bbox = new BlackBox();
        // BlackBox 클래스로부터 bbox라는 객체 생성
        // bbox 객체는 BlackBox 클래스의 인스턴스
        // 클래스는 = 설계도, 라고 생각하고 그 설계도로 만들어진 것이 객체 다른 말로 인스턴스이다

        BlackBox bbox2 = new BlackBox();
    }
}

 

제품이 생산 될 때마다 모델이름, 가격, 색깔, 렌즈 등등을 적으면 상당히 비효율적인 것 뿐만이 아니라 코드가 길어지고 이에 따라 유지 보수도 어려워집니다.

 

그래서 클래스(설계도) 를 이용하여서 객체를 손쉽게 만든 뒤, 그 클래스가 가지는 인스턴스 변수 - 메서드 등을 활용하여서 만들 수 있습니다. 클래스로 만들어진 것을 객체라고 하며 다른말로 인스턴스라고 불린다.

BlackBox b1 = new BlackBox();

 

객체를 만들 때 new 연산자를 사용하여서 만드는 코드이다. 각각의 문장의 뜻하는 바를 정리하면 다음과 같다.

 

BlackBox: 클래스 이름으로 참조형 타입을 나타낸다. 

b1: 변수 이름으로 BlackBox 클래스의 객체를 참조하기 위한 변수입니다. 

new BlackBox(); : BlackBox 클래스의 새로운 객체를 생성하기 위한 표현입니다. new 연산자를 사용하여 객체를 생성하고, 생성된 객체에 대한 참조를 b1 변수에 할당합니다.

 

이제부터 클래스를 살펴보면서 클래스안에  [클래스 변수, 인스턴스 변수, 메서드, 메서드오버라이딩] 등등 알고있던 개념은 복습하고 모르던 개념들을 알아가면서 클래스에 대해서 알아보겠다. 

public class _02_InstanceVeriable {
    public static void main(String[] args) {
        // 클래스 객체 2개로 코딩

        // 처음 만든 블랙박스
        BlackBox b1 = new BlackBox();
        b1.modelName = "까망이";
        b1.resolution = "FHD";
        b1.price = 200000;
        b1.color = "블랙";

        System.out.println(b1.modelName);
        System.out.println(b1.resolution);
        System.out.println(b1.price);
        System.out.println(b1.color);

        System.out.println("----------------------");

        // 새로운 블랙박스 제품
        BlackBox b2 = new BlackBox();
        b2.modelName = "하양이";
        b2.resolution = "UHD";
        b2.price = 300000;
        b2.color = "화이트";

        System.out.println(b2.modelName);
        System.out.println(b2.resolution);
        System.out.println(b2.price);
        System.out.println(b2.color);
    }
}
public class BlackBox {
    // 인스턴스 변수
    String modelName; // 모델명
    String resolution; // 해상도
    int price; // 가격
    String color; // 색상
}

 

BlackBox라는 클래스의 인스턴스 변수들은 선언하여서 객체를 만든 뒤 b1.modelName 과 같이 객체를 이용하여 인스턴스변수에 접근하여 객체의 변수에 값을 넣어줄 수 있다. 여기서 인스턴스 변수는 클래스의 객체(인스턴스)가 생성될 때마다 독립적인 메모리 공간을 갖고 생성이 되는 것으로 이는 클래스 변수에서 다시 짚고 넘어가겠다.

 

또한 BlackBox 클래스는 설계도 역할로 객체를 생성하기 위해 만들어졌으니, 메인 메소드는 필요하지 않다. 다음으로 클래스 내의 메서드는 객체에서 어떻게 이용되는지를 살펴보겠다.

public class BlackBox {
    // 인스턴스 변수
    String modelName; // 모델명

    // 클래스 변수
    static boolean canAutoReport = false; // 자동 신고 기능

    }
public class _03_ClassVariables {
    public static void main(String[] args) {
        BlackBox b1 = new BlackBox();
        b1.modelName = "까망이";
        System.out.println(b1.modelName);

        BlackBox b2 = new BlackBox();
        b2.modelName = "하양이";
        System.out.println(b2.modelName);

        // 특정 범위를 초과하는 충돌 감지 시 자동 신고 기능 개발 여부 -> 클래스 변수 사용
        System.out.println(" -- 개발 전 -- ");
        System.out.println(b1.modelName + " 자동 신고 기능 : " + b1.canAutoReport);
        System.out.println(b2.modelName + " 자동 신고 기능 : " + b1.canAutoReport);
        System.out.println("모든 블랙박스 제품 자동 신고 기능 : " + BlackBox.canAutoReport); 
        // 권장 되는 방식

        // 기능 개발
        BlackBox.canAutoReport = true;

        System.out.println(" -- 개발 후 -- ");
        System.out.println(b1.modelName + " 자동 신고 기능 : " + b1.canAutoReport);
        System.out.println(b2.modelName + " 자동 신고 기능 : " + b1.canAutoReport);
        System.out.println("모든 블랙박스 제품 자동 신고 기능 : " + BlackBox.canAutoReport); 
        // 권장 되는 방식
    }
}

 

먼저, 인스턴스 변수는 객체가 생성될 때마다 독립적으로 생성되는 변수로서, 객체마다 다른 값을 가질 수 있고 객체별로 값이 달라질 수 있다. 하지만 클래스 변수는 클래스 자체에 속하며, 모든 객체가 공유하는 변수로 클래스 변수는 객체 간에 데이터를 공유하고 유지하고자 할 때 사용된다.

 

static 키워드를 사용하여 클래스 변수를 선언하며, 이를 통해 모든 객체에서 클래스 변수의 값을 공유할 수 있다. 클래스 변수는 객체가 아닌 클래스 수준에서 데이터를 관리하고, 객체 간에 공통적인 값을 저장하고 사용하는데 유용하기에 클래스 변수를 사용하여 여러 객체 간에 데이터를 공유하고 일관된 동작을 유지할 수 있습니다.

 

이에 따라서 b1 객체와 b2 객체 즉 객체가 달라도 같은 값을 출력해내며 여기서 중요한 점은 b1.canAutoReport와 같이 클래스 내의 메서드를 호출 할 때는 클래스 자체를 이용하여 메서드를 호출하는 것이 권장되는 방식이다.

 

b1.canAutoReport = x

BlackBox.canAutoReport = o

 

 

클래스의 메서드는 다양한 방법으로 활용이 된다. 아래의 코드는 그 예시이다.

public class BlackBox {
    // 인스턴스 변수
    String modelName; // 모델명

    // 클래스 변수
    static boolean canAutoReport = false; // 자동 신고 기능

    // 메소드
    void autoReport() {
        if (canAutoReport) {
            System.out.println("충돌이 감지되어 자동으로 신고합니다.");
        } else {
            System.out.println("자동 신고 기능이 지원되지 않습니다.");
        }
    }

    void insertMemoryCard(int capacity) {
        System.out.println("메모리 카드 " + capacity + "GB가 삽입되었습니다.");
    }

    int getVideoFileCount(int type) {
        if (type == 1) {
            return 9; // 일반 영상 파일 수
        } else if (type == 2) {
            return 1; // 이벤트 영상 파일 수
        }
        return 0; // 기타 경우
    }
}
public class _04_Method {
    public static void main(String[] args) {
        BlackBox b1 = new BlackBox();
        b1.modelName = "까망이";

        b1.autoReport(); // 지원 안 됨
        BlackBox.canAutoReport = true;
        b1.autoReport(); // 지원 됨

        b1.insertMemoryCard(256);

        // 일반 영상 : 1
        // 이벤트 영상 (충돌 감지) : 2
        int fileCount = b1.getVideoFileCount(1); // 일반 영상
        System.out.println("일반 영상 파일 수 : " + fileCount + "개");

        fileCount = b1.getVideoFileCount(2); // 이벤트 영상
        System.out.println("이벤트 영상 파일 수 : " + fileCount + "개");
    }
}

 

메서드에 파라미터 값을 요구하게 만들어 객체가 값을 전달해야 할 때도 있고 클래스 변수의 값을 바꾸어서 if문을 활용한 메서드도 보인다. 이전에 학습했던 메서드 오버로드도 설계도 역할을 하고 있는 클래스에게 대입해보겠다.

public class BlackBox {
    // 인스턴스 변수
    String modelName; // 모델명

    // 기본 생성자
    BlackBox() {
        // 기본 생성자 로직은 현재 제외됩니다.
    }

    // 메소드 오버로딩 - 매개변수에 따라 다른 동작을 하는 record 메소드들
    void record(boolean showDateTime, boolean showSpeed, int min) {
        System.out.println("녹화를 시작합니다.");
        if (showDateTime) {
            System.out.println("영상에 날짜와 시간 정보가 표시됩니다.");
        }
        if (showSpeed) {
            System.out.println("영상에 속도 정보가 표시됩니다.");
        }
        System.out.println("영상은 " + min + "분 단위로 기록됩니다.");
    }

    // 매개변수가 없는 record 메소드 - 기본 설정으로 녹화
    void record() {
        record(true, true, 5); // 기본값으로 날짜/시간, 속도 표시 및 5분 단위로 녹화 설정
    }
}

 

클래스를 보게되면 record 메서드는 3개의 매개변수를 요구하며 각 매개변수에 따른 if 문을 돌려 값을 달리 출력하도록 구현되어있다. else if 문을 쓰지 않은 이유는 showDataTime이 참일 경우 아래의 showSpeed가 참이여도 실행이 되지 않기 때문이다.

 

중요한 점은 같은 메서드 명인 record가 아래에도 존재하며 그 메서드는 아무런 매개변수를 받지 않지만 메서드 안에 메서드가 기본값으로 설정되어 그 설정된 값이 출력된다. 

 

    public static void main(String[] args) {
        BlackBox b1 = new BlackBox();
        b1.modelName = "까망이";

        b1.record(false,false,10);
        System.out.println("---------------------------");
        b1.record(true,false,3);
        System.out.println("---------------------------");
        b1.record();
    }
}

출력값

녹화를 시작합니다
영상은10분 단위로 기록됩니다
---------------------------
녹화를 시작합니다
영상에 날씨정보가 표시됩니다
영상은3분 단위로 기록됩니다
---------------------------
녹화를 시작합니다
영상에 날씨정보가 표시됩니다
영상에 속도 정보가 표시됩니다
영상은5분 단위로 기록됩니다

 

// 클래스 메소드 -> 클래스 변수를 바로 가져다 쓸 수 있다
static void callServiceCenter() {
    System.out.println("서비스 센터(1588-0000) 로 연결됩니다");
    //modelName = "test"; -> 클래스 메소드 내에선 인스턴스 변수는 수정 불가
    canAutoReport = false; // -> 클래스 메소드 내에선 클래스 변수 수정 가능
}
public class _06_ClassMethod {
    public static void main(String[] args) {
        BlackBox b1 = new BlackBox();
        b1.callServiceCenter();

        BlackBox b2 = new BlackBox();
        b2.callServiceCenter();

        BlackBox.callServiceCenter();

        String s = String.valueOf(3);
    }
}

 

클래스 메소드는 클래스 변수와 같이 공통적인 값을 모든 객체에게 적용시키고 싶을 때 사용하는 것으로, 클래스 변수와 마찬가지로 static 을 붙인 것을 알 수 있다.  위에서 본 클래스 변수인 canAutoReport는 클래스 메서드에 들어와있는 것을 볼 수 있지만 인스턴스 변수는 오류를 일으켜 주석으로 되었다.

 

여기서 인스턴스 변수는 객체가 생성됨과 동시에 메모리를 할당 받는 것으로 당연히 설계도 역할을 하는 클래스 안에서 단독으로 쓰여질 수 없기에 오류를 일으킨다. 

 

This

내가 많이 헷갈려하고 자주 까먹는 부분이기에 좀 더 자세하고 다양한 경우에서 어떻게 쓰이는지 깊이있게 알아보겠다.

 

public class BlackBox {
    // 인스턴스 변수
    String modelName; // 모델명

    // modelName에 추가 문자열을 붙이는 메서드
    void appendModelName(String modelName) {
        this.modelName += modelName;
    }
}
public class _07_This {
    public static void main(String[] args) {
        BlackBox b1 = new BlackBox();
        b1.modelName = "까망이"; // 까망이 (최신형)으로 업데이트 예정
        b1.appendModelName(" (최신형)");
        System.out.println(b1.modelName);
    }
}

 

우선 위 코드에서는 인스턴스 변수인 modelName을 업데이트 하고 싶어 appendModelName이라는 메서드를 만든 뒤, 매개변수 명을 ModelName으로 한 뒤 값을 주어서 기존 인스턴스 변수를 업데이트 하는 메서드이다.

 

appendModelName의 매개변수 명칭을 modelName 즉 인스턴스 변수명과 같기 때문에 this를 붙이지 않을 경우를 보겠다.

void appendModelName(String modelName) {
    modelName += modelName;

 

이럴 경우에는 "(최신형)" 의 값을 지닌 modelName을 받아 "(최신형) += "(최신형) 와 같은 식, 즉 같은 매개변수 값을 한 번더 업데이트 하는 모습이 되어버린다. 

 

내가 원하는 코드는 기존 인스턴스 변수에 매개변수를 업데이트하는 방식이여야 하니, 왼쪽은 인스턴스 변수를 가리켜야 하고 오른쪽은 넘어온 매개변수 값을 의미하여야 한다. 

 

하지만 그저 modelName ++ modelName 으로 할 시 툴 내에서 이를 다르게 구분을 하지 못하기 때문에 왼쪽 값에 인스턴스 변수를 가리키기 위하여 this. 값을 붙인 것이다. 그렇게 되면 우리가 원하는 대로  왼쪽은 인스턴스 변수를 가리켜야 하고 오른쪽은 넘어온 매개변수 값을 의미하게 된다. 

 

물론 메서드 내 매개변수 명칭을 인스턴스 명칭과 똑같이 하지 않는다면 굳이 this를 붙이지 않아도 되지만 코드의 통일성과 올바른 변수 명 등등의 이유로 써야할 때가 많다. 

 

지금까지는 this의 인스턴스 메서드에서 인스턴스 변수 사용을 하는 예시코드였다. 

 

public class MyClass {
    private int value;

    public MyClass() {
        this(0); // 다른 생성자를 호출하여 초기화
    }

    public MyClass(int value) {
        this.value = value;
    }
}

 

this(0)는 다른 생성자를 호출하는 코드이다.  클래스 내의 생성자에서 다른 생성자를 호출할 때 사용되며 구체적으로 이 코드는 파라미터가 없는 MyClass 생성자를 호출하며, 파라미터로 0을 전달하여 객체를 초기화한다.

 

즉, this(0)는 현재 클래스 내의 다른 생성자인 MyClass(int value)를 호출하고, 그 결과로 value 필드가 0으로 초기화한다. 이를 통해 코드의 중복을 피하면서 객체를 초기화할 수 있다.

 

 

다른 주제로 넘어가보자면  지금까지 우리는 객체를 생성한 뒤에 객체로 인스턴스 변수 값 하나하나에 접근을 하며 값을 직접 넣어주고 있었다. 이는 상당히 비효율적이며 클래스를 사용하는 의미를 퇴색시킨다. 그리하여 사용하는 것이 생성자다.

 

생성자

public class _08_Constructor {
    public static void main(String[] args) {
        BlackBox b1 = new BlackBox();
        b1.modelName = "까망이";
        b1.resolution = "FHD";
        b1.price = 200000;
        b1.color = "블랙";

        // 클래스 내 생성자를 활용하여 위와 같이 각 인스턴스 변수마다 값을 매기는 것이 아닌
        // 매개변수 자체로 값을 넣을 수 있다.

        BlackBox b2 = new BlackBox("하양이", "UHD", 300000, "화이트");
        System.out.println(b2.modelName);
        System.out.println(b2.resolution);
        System.out.println(b2.price);
        System.out.println(b2.color);
    }
}
int serialNumber; 
static int counter = 0;

BlackBox() {
    System.out.println("기본 생성자 호출");
    this.serialNumber = ++counter;
    System.out.println("새로운 시리얼 넘버를 발급받았습니다 : " + this.serialNumber);
}

BlackBox(String modelName, String resolution, int price, String color) {
    this(); 
    System.out.println("사용자 정의 생성자 호출");
    this.modelName = modelName;
    this.resolution = resolution;
    this.price = price;
    this.color = color;
}

 

먼저 생성자란 무엇인지 살펴보겠다:

 

BlackBox b2 = new BlackBox("하양이", "UHD", 300000, "화이트") 와 같이 객체를 생성하는 것과 동시에 인스턴스 변수에 값을 넣을 수 있다. 이는 생성자를 이용한 것이다.

 

생성자의 이름은 클래스 이름과 동일해야 하며 생성자는 클래스가 객체로 생성될 때 호출되는 특별한 메서드로, 클래스의 이름을 사용하여 정의하고 오버로딩 기능을 사용하여 파라미터의 갯수나 타입을 달리 정의하여 다양한 생성자를 생성하는 것도 가능하다 .

 

위의 코드를 보게되면 생성자를 중괄호 안에 this를 활용하여 인스턴스 변수안에 매개변수 값으로 채우고 있다. 하지만 여기서 중요한 점은 바로 this()이다. 이 코드에서는 serialNumber가 BlackBox() 생성자 안에서 ++가 되고 있기에 어떤 생성자를 불러오는지에 따라 번호가 달리 나올 수 있다. 

 

그래서 모든 생성자에서 번호를 카운팅 해야하는데 그에 맞는 코드를 전부 치는 것이 아니라,  this()를 활용한다. 이는 기본생성자를 호출하는 것으로 생성자 안에서의 this는 다른 생성자 호출 기능을 한다. BlackBox() 생성자는 매개변수가 없으니 this()로 호출한 것이고 반대로 기본 생성자가 매개변수 4개를 받는 생성자를 호출 할 시에는 this(String modelName, String resolution, int price, String color) 로 호출하였을 것이다.

 

어쨌든 매개변수 4개를 지니는 생성자는 기본 생성자를 호출하여 안전하게 serialNumber가 카운트 되게끔 만들 수 있다.  정리하자면 생성자는 오버라이딩이 가능하며, 생성자안에서 this사용 시 다른 생성자를 호출이 가능하다.

 

추가로 객체를 만듦과 동시에 값을 넣어주고 싶을 땐 다음의 코드를 참조하면 된다:

public BlackBox() {
    this.name = "카메라";
}

 

BlackBox 객체를 생성할 때마다 name 필드는 "카메라"로 초기화됩니다. 이것은 객체가 생성됨과 동시에 name 필드에 초깃값을 주는 것입니다.

 Getter, Setter

// Getter & Setter
    String getModelName() {
        return modelName; // getter 부분
    }
    void setModelName(String modelName) {
        this.modelName = modelName; // setteer 부분
    };

    String getResolution() {
        if (resolution == null || resolution.isEmpty()) {
            return "판매자에게 문의하세요";
        }
        return resolution;
    }

    void setResolution(String resolution) {
        this.resolution = resolution; // 인스턴스 변수인 resolution은 매개변수 resolution 이다
    }

    int getPrice() {
        return price;
    }

    void setPrice(int price) {
        if (price < 100000) {
            this.price = 100000;
        }
        else {
            this.price = price;
        }
    }

    String getColor() {
        return color;
    }

    void setColor(String color) {
        this.color = color;
    }
}
public class _09_GetterSetter {
    public static void main(String[] args) {
        BlackBox b1 = new BlackBox();
        b1.modelName = "까망이";
        //b1.resolution = "FHD";
        b1.price = 200000;
        b1.color = "블랙";

        // 할인 행사
        b1.price = - 5000;
        System.out.println("가격" + b1.price + "원");

        // 고객 문의
        System.out.println("해상도 : " + b1.resolution );

        System.out.println("----------------------");

        BlackBox b2 = new BlackBox();
        b2.setModelName("하양이");
        b2.setPrice(-5000);
        b2.setColor("화이트");

        System.out.println("가격 : " + b2.getPrice() + "원");
        System.out.println("해상도 : " + b2.getResolution());
    }
}

 

클래스의 멤버 변수에 직접 접근하는 것을 피하고, 이를 위해 getter와 setter 메서드를 사용한다. 이러한 메서드를 통해 멤버 변수에 안전하게 접근하고 수정할 수 있으며, 데이터의 유효성 검사 및 가공도 할 수 있다. 이를 통해 객체의 데이터를 캡슐화 할  수 있다.

 

위의 코드에서는 인스턴스 변수에 -5000 값을 주어  출력시 -5000 값 그대로 출력되게 놔두거나 혹은 변수에 값을 주어주지 않아 null 값이 나타나도 따로 로직을 구현할 수 없어 그대로 출력이 되곤한다. 이러한 문제를 위하여 getter, setter를 사용한다 

 

Getter 메서드 

getter 메서드는 멤버 변수의 값을 반환하는 역할을 한다. 일반적으로 인스턴스 변수의 이름 앞에 get을 붙여서 메서드 이름을 작성한다.

public String getModelName() {
    return modelName; // modelName 멤버 변수의 값을 반환
}

 

위의 코드에서 getModelName 메서드는 modelName 인스턴스 변수의 값을 반환합니다. 또한, getter 메서드 내에서 추가적인 로직을 적용할 수도 있습니다. 예를 들어, resolution 변수의 값이 비어있을 때 "판매자에게 문의하세요"라는 기본값을 반환하도록 하는 경우:

public String getResolution() {
    if (resolution == null || resolution.isEmpty()) {
        return "판매자에게 문의하세요";
    }
    return resolution;
}

 

Setter 메서드

setter 메서드는 인스턴스 변수의 값을 설정하는 역할을 한다. 일반적으로 인스턴스 변수의 이름 앞에 set을 붙여서 메서드 이름을 작성하며 이 때, 매개변수를 받아서 인스턴스 변수에 값을 설정한다.

public void setModelName(String modelName) {
    this.modelName = modelName; // modelName 멤버 변수의 값을 설정
}

 

위의 코드에서 setModelName 메서드는 modelName 인스턴스 변수의 값을 설정하는 역할을 한. this 키워드를 사용하여 현재 객체의 modelName 인스턴스 변수에 매개변수로 받은 값을 대입합니다. 또한, setter 메서드 내에서 유효성 검사를 할 수 있다. 예를 들어, price 변수가 특정 범위를 벗어나면 최소값으로 설정하도록 하는 경우:

public void setPrice(int price) {
    if (price < 100000) {
        this.price = 100000;
    } else {
        this.price = price;
    }
}

 

Getter와 Setter 활용

종합적으로 getter와 setter 메서드를 사용하면 인스턴스 변수에 안전하게 접근하고 수정할 수 있으며, 데이터의 유효성을 검사하거나 가공할 수 있다. 이를 통해 객체의 데이터를 캡슐화하고 오류를 방지할 수 있으며, 코드의 가독성과 유지 보수성을 향상시킬 수 있다. 사용 예시로는 아래와 같이 BlackBox 클래스의 인스턴스를 생성하고 getter와 setter를 사용하여 데이터를 설정하고 출력할 수 있다:

BlackBox b2 = new BlackBox();
b2.setModelName("하양이");
b2.setPrice(-5000);
b2.setColor("화이트");

System.out.println("가격 : " + b2.getPrice() + "원");
System.out.println("해상도 : " + b2.getResolution());

 

접근 제어자 

private 해당 클래스 내에서만 접근 가능
public 모든 클래스에서 접근 가능
default  (제어자가 없을  때 기본적용)  같은 패키지 내에서만 접근가능 
protected 같은 패키지 내에서, 다른 패키지인 경우 자식 클래스에서 접근 가능

 

접근 제어자는 객체 지향 프로그래밍 언어에서 클래스, 변수, 메서드 등의 멤버에 대한 접근 권한을 제어하기 위해 사용이 되고 접근 제어자는 해당 멤버에 대한 외부 접근을 제한하거나 허용할 수 있으며, 코드의 안정성과 보안을 강화하고 객체 지향 프로그래밍의 핵심 원칙 중 하나인 정보 은닉을 구현하는 데 사용이 된다.

접근제어자 private 일 때, 접근 방법

private int price;

 

만일 인스턴스 변수인 price에 private를 적용하였다고 가정하였을 땐,  설계도 역할인 클래스 내에서만 접근이 가능하다. 만일 타 클래스에서 접근시 오류가 난다.

 

BlackBoxRefurbish b1 = new BlackBoxRefurbish(); 
b1.modelName = "까망이";
b1.setPrice(200000);
b1.color = "블랙";

 

b1.price = 20000; 과 같이 인스턴스 변수를 설정한 클래스에서 price를 private을 주었으므로 이렇게 다른 클래스에서는 수정이 불가능하다. 그러므로 해당 클래스에서 설정한 public getter&setter로 통하여서만 값을 수정 할 수 있다.

 

 

접근제어자 사용 예시

public String modelName; // 모델명
String resolution; // 해상도 // 아무것도 안 적으면 default 이다.
private int price; // 가격
protected String color; // 색상

// A 패키지 내부 클래스에서 정의된 인스턴스 변수

 

확장성 있게 접근제어자의 기능을 보기위해, 만일 A 패키지 클래스 내부에 이러한 인스턴스 변수들을 설정하였고 B패키지 내부에서 위 인스턴스 변수들을 사용하기 위해 접근을 하였다고 가정을 해보자.

 

public class _00_AcceessModifierTest {
    public static void main(String[] args) {

        BlackBoxRefurbish b1 = new BlackBoxRefurbish();
        b1.modelName = "까망이"; // public -> 다른 패키지에서도 가능
        b1.resolution = "FHD"; // default -> 같은 패키지가 아니므로 불가능
        b1.price = 200000; // private -> 해당 클래스가 아니므로 불가능
        b1.color= "블랙"; // protected -> 같은 패키지도 아니고 자식 클래스도 아니므로 불가능
    }
}

// B 패키지 내부 클래스
  • modelName 변수는 접근제어자 public이기에 모든 클래스에서 제한 없이 접근 가능하기에 다른 패키지에서도 접근을 허용
  • resolution 변수는 접근제어자에 아무것도 기입하지 않았으므로 default를 할당받게 된다. 그리고 default 같은 패키지 내에서만 접근을 허용하므로 다른 패키지에서는 이 변수를 사용하지 못한다.  
  • price 변수는 접근제어자가 private 이기 때문에 구현한 클래스 내부에서만 사용이 가능하고 그 외엔 사용이 불가능하다.
  • color는 접근제어자가 protected 이므로, 같은 패키지도 아니며 자식 클래스도 아니므로 사용이 불가능하다.

 

상속

 

public class Camera { // 부모 클래스
    public String name;

    public Camera() {
        this("카메라");
    }

    protected Camera(String name) { // 자식클래스에서도 접근 가능
        this.name = name;
    }

    public void takePicture() {
        // 사진 촬영
        System.out.println(this.name + " : 사진을 촬영합니다");
    }

    public void recordVideo() {
        // 동영상 녹화
        System.out.println(this.name + " : 동영상을 촬영합니다");
    }

    public  void showMainFeature() {
        System.out.println(this.name + " 의 주요 기능 : 사진 촬영, 동영상 녹화");
    }
}
public class FactoryCam extends  Camera{ // 자식 클래스
public FactoryCam() {
        super("공장카메라");
    }

    public void recordVideo() {
        super.recordVideo(); 
        detectFire();
    }


    public void detectFire() {
        // 화재 감지
        System.out.println("화재를 감지합니다");
    }

    public  void showMainFeature() { // 메서드 오버라이딩
        System.out.println(this.name + " 의 주요 기능 : 화재 감지");
    }
}
public class SpeedCam extends Camera { // 자식 클래스
    public SpeedCam() {
        super("과속단속 카메라");
    }

    public void takePicture() {
        super.takePicture();
        checkSpeed();
        recognizeLicensePlate();
    }

    public void checkSpeed() {
        // 속도 체크
        System.out.println("속도를 측정합니다");
    }

    public void recognizeLicensePlate() {
        // 번호 인식
        System.out.println("차량 번호를 인식합니다");
    }

    @Override // annotation // 메소드 오버리이딩
    public void showMainFeature() {
        System.out.println(this.name + " 의 주요 기능 : 속도 측정, 번호 인식");
    }
}

 

이 코드들은 상속 관계를 가지고 있는 클래스들을 보여준다. 상속은 자바에서 객체 지향 프로그래밍의 중요한 개념 중 하나로, 클래스 간에 부모-자식 관계를 형성하여 코드의 재사용성을 높이는 데 사용된다.

 

먼저 부모클래스에서 공통으로 사용할 메서드를 정한 뒤, 상속받는 클래스가 부모클래스의 메서드를 상속을 받은 뒤, 추가로 자신이 가져야 할 메서드를 추가하거나 혹은 기존의 메서드에 오버라이딩을 하여 자기에게 필요한 메서드로 재생성 하는 방향으로 상속을 받게 된다. 

 

여담으로 IS-A는 부모와 자식 관계를 나타내는 말로 Factory Cam is a Camera 이 문장을 거꾸로 하면은 말이 안 되지만 이 자체로는 말이 되는 문구이다. 이렇듯 IS-A는 부모와 자식관계를 나타내는 말이다.

 

extend

public class SpeedCam extends Camera
public class FactoryCam extends  Camera

 

상속 관계는 한 클래스가 다른 클래스의 특성을 상속받아 재사용하고 확장할 수 있도록 한다. 상속을 통해 코드 중복을 줄이고 유지보수를 용이하게 만들 수 있다. 이러한 상속 관계는 패키지에 관계 없이 클래스 간에 정의될 수 있으며, 클래스 간의 관계를 명시적으로 나타내기 위해 extends 키워드를 사용하며 상속을 1개 초과의 클래스에게는 받을 수 없다.

 

상속의 기능

정리해보자면 Camera 클래스가 부모 클래스이며 FactoryCam과 SpeedCam은 부모 클래스의 상속을 받는 자식 클래스라는 것을 알 수 있다. 상속을 하면 어떠한 점을 재사용하고 확장을 하는지 알아보자  

Camera camera = new Camera();
FactoryCam factoryCam = new FactoryCam();
SpeedCam speedCam = new SpeedCam();

System.out.println(camera.name);
System.out.println(factoryCam.name);
System.out.println(speedCam.name);

camera.takePicture();
factoryCam.recordVideo(); /
speedCam.takePicture();

 

만일 FactoryCam과 SpeedCam에 Camera 부모 클래스가 가지는 공통적인 메서드가(recordVideo(), takePicture()) 등등을 지니고 있지 아니하더라도, 부모 클래스인 Camera가 메서드를 가지고 있으므로 부모 클래스 메서드를 실행한다. 즉 자식 클래스에 정의되어 있지 않은 메서드여도 부모 클래스에 정의되어 있는 메서드라면 부모클래스의 메서드가 실행된다.

 

또한 메서드뿐만이 아니라 부모클래스의 변수도 상속을 받기 때문에 String name을 공유한다. 그렇기에 자식 클래스에서 name 변수를 지워도 부모 클래스에서는 있기 때문에 this.name을 사용하여도 문제 없다.  

 

메서드 오버라이딩

메소드 오버라이딩은 자식 클래스에서 부모 클래스의 메소드를 새롭게 재정의하는 개념을 의미한다. 메소드 오버라이딩을 통해 자식 클래스는 부모 클래스의 메소드를 그대로 사용할 수 있지만 자기의 특성과 상황의  필요에 따라 다르게 구현할 수 있으며, 이를 통해 다형성을 구현할 수 있다. 다형성은 동일한 메소드 호출로 다른 동작을 수행할 수 있는 능력을 나타내며, 객체 지향 프로그래밍에서 중요한 개념 중 하나이다.

 

 

우선 Camera인 부모클래스에서는 recordVideo 메서드와 takePicture 메서드와 showMainFeature 메서드가 있다. 그러한 메서드를 재사용하는 오버라이딩을 거쳤기에 각 클래스를 살펴보자:

 

public  void showMainFeature() {
    System.out.println(this.name + " 의 주요 기능 : 사진 촬영, 동영상 녹화");
}

 

부모클래스의 showMainFeature() 메서드이다. 이를 상속받은 클래스들은 다음과 같이 재생성 할 수 있다.

public  void showMainFeature() { // 메서드 오버라이딩
    System.out.println(this.name + " 의 주요 기능 : 화재 감지");
}
public void showMainFeature() {
    System.out.println(this.name + " 의 주요 기능 : 속도 측정, 번호 인식");
}

 

 

 

public void recordVideo() {
    // 동영상 녹화
    System.out.println(this.name + " : 동영상을 촬영합니다");
}
public void takePicture() {
    // 사진 촬영
    System.out.println(this.name + " : 사진을 촬영합니다");
}

부모클래스의 recordVideo() 메서드와 takePicture 메서드이다. 이를 상속받은 클래스들은 다음과 같이 재생성 할 수 있다.

public void recordVideo() {
    super.recordVideo();
    detectFire();
}
public void takePicture() {
    super.takePicture();
    checkSpeed();
    recognizeLicensePlate();
}

 

super()

super()는 자바에서 부모 클래스의 생성자를 호출하기 위한 특별한 키워드입니다. 부모 클래스의 생성자를 호출하는 데 사용됩니다. 클래스간의 상속 관계에서 자식 클래스는 부모 클래스의 기능을 상속받게 됩니다. 그리고 자식 클래스의 객체가 생성될 때, 부모 클래스의 생성자도 함께 호출되어 초기화 과정이 진행됩니다. 

 

이때 super()를 사용하여 부모 클래스의 생성자를 명시적으로 호출할 수 있습니다. 예를 들어, 위의 코드에서 FactoryCam 클래스의 생성자에서 super("공장카메라")와 같이 super()를 사용하여 Camera 클래스의 생성자를 호출하고 있습니다. 이것은 FactoryCam 클래스가 Camera 클래스를 상속하고 있으며, Camera 클래스의 생성자를 호출하여 FactoryCam 객체를 초기화하겠다는 의미입니다.

 

public FactoryCam() {
        super("공장카메라");
    }

 

FactoryCam 클래스 중 FactoryCam 메서드에서 아래 Camera의 생성자를 호출하여서 this.name을 공장카메라로 설정함

protected Camera(String name) { // 자식클래스에서도 접근 가능
    this.name = name;
}

 

다른 예시도 더 살펴보겠다:

public void recordVideo() {
    // 동영상 녹화
    System.out.println(this.name + " : 동영상을 촬영합니다");
}

 

위에는 부모클래스에서 정의된 생성자로 호출 시 print를 출력한다. 상속받는 클래스는 재정의 할 수도 있지만, 정의된 내용도 필요할 때 각 각 따로 적는 것이 아니라 super 명령어로 호출 시킨 뒤, 재생성 한 것으로 super를 이용하여서 코드의 양을 줄일 수 있다.

public void recordVideo() {
    super.recordVideo();
    detectFire();
}

 

다양성

        Camera camera = new Camera();
        FactoryCam factoryCam = new FactoryCam();
        SpeedCam speedCam = new SpeedCam();

        Camera camera = new Camera();
        Camera factoryCam = new FactoryCam();
        Camera speedCam = new SpeedCam();

 

각 클래스 객체를 생성할 때, 부모 클래스는 상속하는 자식 클래스 객체를 만들 수도 있다 둘 다 기능적으로 같으나 차이점은 있다.

 

부모 클래스에서 확장하여 새롭게 생성된 메소드를 지니는 자식 클래스는 객체 생성 시, 해당 메소드를 사용 가능하지만, 부모 클래스로 부터 만들어진 객체는 부모 클래스에 정의 되지 않은 메서드는 사용 불가능 하다. 

 

사용하려면 다음과 같은 코드를 따라야 한다:

if (camera instanceof Camera) {
    System.out.println("카메라입니다");
}

if (factoryCam instanceof FactoryCam) {
    ((FactoryCam) factoryCam).detectFire();
}

if (speedCam instanceof SpeedCam) {
    ((SpeedCam) speedCam).checkSpeed();
    ((SpeedCam) speedCam).recognizeLicensePlate();
}

 

if 문의 조건문을 통하여서  객체가 해당 클래스의 인스턴스인지 확인하는 것으로 참일 경우, 중괄호 내 코드를 실행한다. 그리고 부모 클래스 타입이 아닌 형 변환을 통하여서  자식 클래스가 지닌 메서드를 사용할 수 있다.

 

 

Camera 클래스: 이 클래스는 모든 카메라의 기본 기능을 정의하는 부모 클래스이다. Camera 클래스에는 takePicture() 및 recordVideo()와 같은 메서드가 있고 모든 카메라에서 공통으로 사용된다. 이 클래스는 다른 클래스들이 상속하는 기반 클래스다.

FactoryCam 클래스: 이 클래스는 Camera 클래스를 상속하여 만들어진 자식 클래스다. FactoryCam은 공장에서 사용되는 카메라로, 부모 클래스인 Camera의 모든 기능을 상속을 받는다. 또한 recordVideo() 메서드와 showMainFeature() 메서드를 오버라이딩하여 화재 감지 기능을 추가하며 이것은 "IS-A" 관계로서 FactoryCam은 Camera의 하위 클래스다.

SpeedCam 클래스: 이 클래스도 Camera 클래스를 상속하여 만들어진 자식 클래스입니다. SpeedCam은 속도 측정 및 차량 번호 인식과 같은 특수 기능을 추가로 정의하며 Camera의 모든 기능을 상속받습니다. 이 클래스 또한 SpeedCam() 메서드와 showMainFeature()를 오버라이딩 하였고, 역시 "IS-A" 관계로서 SpeedCam은 Camera의 하위 클래스다.

 

 

기본 자료형과 참조 자료형의 차이

기본 자료형: int, float, double, boolean 등 기본 자료형 변수에는 직접 값이 저장됩니다. 변수를 다른 변수에 할당하거나 수정해도 서로 영향을 주지 않습니다. 

 

참조 자료형: String, 사용자 정의 클래스 등 참조 자료형 변수에는 객체에 대한 참조(메모리 주소)가 저장됩니다. 변수를 다른 변수에 할당하면 동일한 객체를 참조하게 되므로 하나를 수정하면 다른 변수도 변경될 수 있습니다.

 

int a = 10;
int b = 20;
b = a;

 

위 코드에서 a와 b는 기본 자료형 변수이므로 a 값을 b에 할당해도 서로 영향을 주지 않습니다.

 

Camera c1 = new Camera();
Camera c2 = new Camera();
c2 = c1;

 

여기서 c1과 c2는 Camera 클래스의 객체에 대한 참조를 저장하는 참조 자료형 변수입니다. c1과 c2는 처음에 각각 다른 객체를 참조하고 있습니다. 

 

c2 = c1; 코드를 실행하면 c2는 c1이 참조하는 객체를 참조하게 됩니다. 이제 두 변수는 동일한 객체를 참조하게 됩니다.

c2.name = "고장난 카메라";

 

객체 내의 속성을 변경하는 것은 객체에 대한 참조를 통해 이루어집니다. 따라서 c2를 통해 객체의 name 속성을 변경하면 c1을 통해서도 같은 객체의 name이 변경됩니다.

 

c2 = null;

 

c2에 null을 할당하면 c2는 더 이상 어떤 객체도 참조하지 않게 됩니다. 이제 c1은 그 자체로 유효한 객체를 참조하게 된다.

 

Final

 

final 변수: 변수를 final로 선언하면 해당 변수는 상수(Constant)가 됩니다. 이 변수는 한 번만 값을 할당할 수 있으며, 이후에는 변경할 수 없습니다.

 

final int MAX_VALUE = 100;
MAX_VALUE = 200; // 에러 발생! final 변수는 값을 변경할 수 없음

 

final 변수에 초깃값이 없을 경우 즉 final int MAX_VALUE; 로 끝이나서 따로 값이 정해지지 않은 경우에는, 생성자에서 값을 넣으면 넣어진다.

 

 

final 메서드: 메서드를 final로 선언하면 해당 메서드는 하위 클래스에서 오버라이딩(재정의)할 수 없습니다. 즉, 해당 메서드의 구현 내용은 변경할 수 없습니다.

class Parent {
    final void printMessage() {
        System.out.println("부모 클래스의 메시지");
    }
}

class Child extends Parent {
    void printMessage() { // 에러 발생! final 메서드는 오버라이딩할 수 없음
        System.out.println("자식 클래스의 메시지");
    }
}

 

final 클래스: 클래스를 final로 선언하면 해당 클래스는 상속할 수 없게 됩니다. 다른 클래스가 이 final 클래스를 상속하는 것이 불가능해집니다.

final class FinalClass {
    // ...
}

// 아래의 코드는 에러를 발생시킴
class SubClass extends FinalClass {
    // ...
}

 

Enum

Enum(열거형)은 상수들의 집합을 나타내는 자료형입니다. Java에서는 Enum을 사용하여 서로 관련 있는 상수들을 효과적으로 정의하고 사용할 수 있습니다.

 

예시:

// 달력: JAN, FEB~
// 옷 사이즈, OS 종류, 해상도 등등

 

해상도(Resolution)라는 열거형을 정의해보겠습니다. 각 해상도는 이름과 해당 폭(Width)을 가지고 있습니다.

enum Resolution {
    HD(1280), FHD(1920), UHD(3840);

    private final  int width;

    Resolution(int width) {
        this.width = width;
    }

    public int getWidth() {
        return width;
    }
}

 

위의 코드에서는 Resolution이라는 Enum을 정의하고, 각 해상도 값에 대한 폭(Width)을 설정하였습니다. 이제 이 Enum을 사용하는 예제 코드를 통해 Enum의 사용법을 살펴보겠습니다.

public class ResolutionExample {
    public static void main(String[] args) {
        Resolution resolution = Resolution.HD; // HD 해상도 선택
        System.out.println("선택한 해상도: " + resolution);

        // 선택한 해상도에 따라 동작을 수행
        switch (resolution) {
            case HD:
                System.out.println("일반 화질");
                break;
            case FHD:
                System.out.println("고화질");
                break;
            case UHD:
                System.out.println("초고화질");
                break;
        }

        // 모든 해상도 출력
        for (Resolution res : Resolution.values()) {
            System.out.println(res + " : Width=" + res.getWidth());
        }
    }
}

 

위 코드에서는 Resolution 열거형을 사용하여 해상도를 선택하고, 선택한 해상도에 따라 동작을 수행합니다. 또한, 모든 해상도를 출력하기 위해 Resolution.values()를 사용하여 Enum의 모든 값을 순회하였습니다. Enum을 사용하면 코드가 더 가독성 있고 유지보수가 쉬워집니다. 특히, 연관된 상수를 그룹화하고 관리하는 데 매우 유용한 자료형입니다.

'Java' 카테고리의 다른 글

Java (제네릭스)  (1) 2024.01.04
Java (추상 클래스와 인터페이스, 의존성 주입)  (2) 2024.01.03
Java (메서드)  (2) 2024.01.02
Java (배열)  (1) 2023.12.31
Java (이중 반복문)  (0) 2023.12.31