[Java] 생성자와 게터세터

자바 객체지향

reference : 뉴렉처 객체지향 프로그래밍 을 정리한 내용입니다.

3. 생성자

객체 초기화

class Program{
    public static void main(){
        ExamList list = new ExamList();
    }
    list.init();
}

class ExamList{
    private Exam[] exams;
    private int current;
    public void init(){
        exams = new Exams[3];
        current = 0;
    }
}

위 코드를 보자. 클래스 Program에서 new ExamList();를 하는 순간 객체의 실체화가 이루어지고 exams, current가 메모리에 잡히게 된다. 이 구조가 실존하게 되는 위치는 메인메모리가 될 거고 메모리 설정을 한 자기공간을 확보하게 되는 게 객체의 실존상태이다.

이렇게 존재하게 된 객체를 식별하기 위해 이름을 부여하게 된다ExamList list. 그리고 이 함수를 초기화하기 위해 init() 함수를 만들었는데, init을 호출하며 참조변수 exams에 배열객체를 넣어주고 current엔 0을 담아 세팅해줬다. 이렇게 초기화 된 상태로 업무적 동작을 하게 된다.

그런데… 그렇게 업무적 동작을 하고 있는데 갑자기 누가 init();을 호출한다면 어떻게 될까. 객체를 만들고 첫세팅하기 위해 만든 초기화 함수였는데 수시로 호출할 수 있으며, 처음에도 호출 가능한데 비즈니스 로직 진행 중에 호출할 수도 있는 순서가 없는 초기화 함수는 이상하다. 이건 초기화라기 보단 객체를 초기화 상태 값으로 돌리는 reset 서비스 함수에 가깝다.

그렇다면 초기화 함수는 어떻게 만들어야 하나?

생성자의 조건

  1. 객체가 생성 되자 마자 무조건 제일 먼저 실행되어야 만 한다.
  2. 생성될 때 단 한번만 실행되어야 만 한다.

초기화는 위와 같은 조건을 만족해야 하며 생성자는 위 조건을 만족한다!
함수는 언제든 호출할 수 있는 이름을 갖고있는 반면 생성자는 이름을 갖고 있지 않다. 명시적으로 호출할 수 없는 특별한 함수다.

그렇다면 생성자는 어떻게 호출하고 어떻게 이용하는가? 지금까지 많~이 써온 new ExamList();는 사실 다음과 같은 구조다.

new ExamList(); = new ExamList + ();
ExamList란 객체를 만들어줘new ExamList + 만들어진 객체를 초기화하는 생성자를 호출해줘()

생성자는 이름이 없기 때문에 무조건 앞에 막 생성된 객체를 앞에 놓아야만 호출할 수 있다. 그래서 new ExamList();는 한 문장이 아니라 new ExamList 문장이 실행된 결과로서 객체가 있어야만 호출할 수 있는 생성자인 것이다.

그렇다면 init()을 생성자로 수정해보자.

public ExamList(){
}

함수는 반환타입을 쓰게 되어있다. 반환타입이 없으면 void를 쓰는데, 생성자는 반환 목적으로 쓰는 게 아니라 객체를 넘겨받기만 하기 때문에 반환타입을 쓰지 않는다.

또한 생성자는 객체를 초기화하는 함수기에 초기화 할 수 있는 객체가 아무거나 오면 안 된다. 반드시 ExamList라는 형식의 객체만이 초기화 할 수 있는 함수로 사용된다는 한정사 같은 것이다. 클래스 안에 객체가 만들어지면 그 객체를 초기화하는 함수다, 라는 걸 나타내기 위한 한정사기 때문에 클래스와 같은 이름을 사용한다. public은 당연히 써야 하고!

  • 생성자는 함수명이 없다.
  • 정의할 때의 함수명은 초기화 할 객체를 한정하기 위한 형식명칭이다.

생성자 오버로드

생성자도 오버로드로 두 개 이상 존재할 수 있다.

clas ExamList{
    private Exam[] exams;
    private int current;
    public ExamList(){
        exams = new Exam[3];
        current = 0;
    }
    public ExamList(int size){
        // 생성자도 함수 특징 갖고있어 파라미터 넘겨받을 수 있음
        exams = new Exam[size];
        current = 0;
    }
}

이렇게 작성하면 생성자를 두 가지 방식으로 호출할 수 있다.
기본값으로만 처리하는 생성자 new ExamList();
내가 원하는 개수만큼 미리 배열을 준비할 수 있는 생성자 new ExamList(10);

오버로드시 주의사항이 있다. 기본생성자가 없는 상태에서 오버로드 생성자 만들었다면 기본생성자new ExamList();를 호출할 수 없다. 상속이란 개념으로 넘어갔을 때 의도적으로 오버로딩 함수만 만들었다면 여러 가지 문제를 만나게 되는데 적절한 조치를 취해줘야 한다.

기본생성자와 오버로드 생성자는 같은 기능이기에 코드의 중복이 발생한다. 중복은 반드시 제거할 수 있어야 한다. 오버로드 함수는 하나만 구현하고 나머지는 그걸 재호출 한다고 했는데([구조적 프로그래밍 참고])(/java/java-structure/) ExamList();에서 ExamList(int size)를 호출할 때, 생성자가 생성자를 어떻게 호출할 수 있는가?

생성자는 함수이름을 갖고 있지 않기 때문에 직접 호출은 불가능하다. 대신 생성자의 특징을 이용해야 한다. 생성자는 이름이 없고, 새로 만들어진 객체로 호출하는 특수한 함수이다. 따라서 갓 생성한 객체를 이용해 호출하면 된다.

class ExamList{
    private Exam[] exams;
    private int current;

    public ExamList(){
        this(3);
    }
    public ExamList(int size){
        exams = new Exam[size];
        current = 0;
    }
}

ExamList()를 넘겨받은 this라는 막 생성된 객체를 통해 소괄호를 열고 값을 넣는다. 값을 넣음으로써 인자있는 생성자로 호출이 전이된다. 즉 여기서 this(...)의 의미는 새로 만들어진 this 객체로 인자를 가지는 생성자를 호출하는 것이다.

생성자는 막 만든 객체를 통해서만 호출할 수 있다는 걸 잘 기억하자. 이렇게 ExamList()ExamList(int size)를 호출하여 코드가 두 번 작성되는 문제를 해결했다.

생성자를 하나도 정의하지 않는다면?

지금까지 우리는 생성자를 정의하지도 않았는데도 사용이 가능했다. 매번 쓰던 new ExamList();를 통해 객체를 생성해 왔는데, 생성자가 없으면 사용할 수 없는 구문이다. 그런데도 어떻게 쓸 수 있었는가? 컴파일러가 생성해줬기 때문이다.

컴파일러가 컴파일 할 때 코드에서 기본생성자를 사용하고 있음에도 불구하고, 클래스에 생성자 선언이 없으니 기본 생성자를 만들어주는 것이다.

이렇게 자동으로 기본생성자가 생성될 때, 참조변수 private Exam[] exams가 실체화 될 경우엔 참조변수 exam엔 null이 들어가고 기본 형식인 값변수 current가 실체화 될 땐 0이 세팅된다. 이런 기본 세팅(null, 0)이 싫으면 반드시 생성자를 직접 추가해서 원하는 값으로 세팅해 줘야 한다.

여기서 한 가지 주의점이, 오버로드 생성자만 있을 때는 컴파일러가 자동으로 기본생성자를 생성해주지 않는다!
객체 생성에 있어 생성자는 반드시 필요한데 오버로드 생성자만 있더라도 객체생성 문법구조를 갖출 수 있기 때문에 컴파일러가 나서서 만들어주지 않는다. 생성자가 하~나도 없을 때만 관여해서 만들어 주는 것이니 주의하자.

4. Getter Setter

A(이후 ExamList)에 관련된 함수를 다 모아 캡슐화를 시켰는데, ExamList에 있는 input이라는 함수가 B(이후 Exam)에 있는 데이터를 쓰려고 한다. input의 주데이터는 ExamList라서 Exam으로 옮길 수도 없다. 이렇게 보조데이터가 Exam에 있을 경우 이럴땐 어떻게 할까?

input함수가 Exam 데이터 구조를 직접 사용할 순 없으니 간접적으로 사용할 수 있게 Exam에게 함수를 제공해달라고 해야 한다. 그래야 캡슐이 깨지지 않는다. 다시 말해 ExamList가 Exam에게 함수를 만들어서 데이터 좀 이용하게 해줘, 라고 부탁하는 거다.

//ExamList
int kor = exam.getKor(); //exam.kor;

//Exam
private int kor;
public int getKor(){
    return kor;
}

ExamList가 Exam 데이터 구조의 kor값을 달라는 함수를 만들어달라고 부탁해 만들어진 게 getKor이다. 그러면 getKor함수가 Exam에서 정의되고 ExamList에선 국어성적을 함수를 통해 간접적으로 가져올 수 있게 된다. Exam에서 변수명을 바꿔도 오류가 ExamList가 아닌 Exam에서만 발생해 간단하게 오류해결도 가능하게 되었다.

값을 가져올 때와 비슷하게, ExamList에서 Exam데이터의 값을 설정해야 할 때도 값 설정이 가능한 함수를 Exam에서 만들어 ExamList에게 제공하면 된다setKor. 캡슐도 깨지지 않으면서 간접적으로 정상적으로 쓸 수 있게 하는 방법이다.

//ExamList
exam.setKor(kor); //exam.kor=kor;

//Exam
private int kor;
public void setKor(int kor){
    this kor = kor;
}

굳이 변수 하날 위해 긴 함수를 써야 하나? 이런 의문이 들 수도 있다. 아래 설명을 보자.

//캡슐 안
public class Exam{
    private int kor;
    private int eng;
    private int math;
}
//캡슐 밖
public class Program{
    public ststic void main(String[] args){
        Exam exam = new Exam();
        //exam.kor = 30; 사용안됨
        exam.setKor(30);
    }
}

현재 이 코드부터 설명하면, Exam exam = new Exam()으로 heap공간에 공간kor,eng,math이 생성되었다. 그 뒤 exam.kor=30;Exam구조를 Program 메인 함수에서 직접 사용하려고 하는데 kor은 private로 정의되어 있어 직접 사용이 불가능하다.
값 설정을 위해 Exam에서 kor에 대한 세터를 만들고 Program에선 exam.setKor(30);을 통해 사용 가능하게 된다.

이 과정이 너무 비효율적이고 복잡하게 느껴지는데 그냥 public으로 쓰면 안될까? 그런 의문이 들 수 있다.

사실 속성명이 변경되는 일(exam이 exam1이 된다거나)은 드물다. 하지만 이 일련의 과정의 진짜 목적은 단순 속성명 변경때문이 아니라 데이터구조 변경을 위해 있다. 캡슐화는 데이터구조를 사용하는 걸 모아놓은 것이고, 데이터구조가 변경됨에 따라 종속되는 걸 해결하려는 것이다. 그렇다면 데이터구조가 변경된다는 의미는 무엇인가?

구조화는 계층을 갖는다. 우리가 방청소를 하며 물건들을 작은박스에 담고 큰박스에 옮겨 담는 것처럼 계층을 가질 수 있다.

public class Exam{
    public int kor;
    public int eng;
    public int math;

    private int seq; //1
    private String title; //기말고사
    private Date regDate; //2016-12-13
}

Exam 클래스에 시험에 대한 속성을 추가했다. 변수가 많아져 정리가 필요해 보여 시험과목에 대한걸 따로 선언해 담아 아래처럼 바꾼다.

public class Exam{
    public Subject subject;

    private int seq;
    private String title;
    private Date regDate;
}

이렇게 구조화된 데이터가 중첩이 되고 중첩된 구조화가 가능해졌다. 속성명은 달라지지 않았지만 구조가 깊어졌다. 이럴땐 구조가 한 단계 더 깊어졌기 때문에 직접 변수를 사용한다 하면 방식이 다음과 같이 바뀐다.

//구조화 전
System.out.print("국어:");
exam.kor = scan.nextInt();
//구조화 한 후
System.out.print("국어:");
exam.subject.kor = scan.nextInt();
//구조화 후 Setter를 썼다면
System.out.print("국어:");
record.setKor(scan.nextInt());

Exam이 갖고있는 속성은 똑같이 6개며 속성명도 그대로다. 그런데도 코드를 변경해야 하는 상황이 된다. 이럴때 Getters/Setters를 쓰면 Exam 안에서만 달라지고 외부에선 변화가 감지 되지 않고 오류가 발생하지 않는다.

이렇게 게터세터는 내부적으로 바꿔도 외부에선 문제가 없도록, 구조변경 했을 때 문제가 발생하지 않도록 사용하는 것이다. 구조변경은 계속 있을 수밖에 없다. 게터세터 없이 직접 변수를 사용하면 수정이 어마어마하게 일어날 수밖에 없다😰

일반적으로 속성 값 설정하면 가져가는 일이 많으니 무조건 묵시적으로 게터세터를 필수로 추가하게 된다. 이클립스에선 오른쪽 클릭 -> Source -> Generate Getters and Setters를 통해 간편하게 게터세터 추가도 가능하니 많이 많이 활용하자~!

참고로 int total = kor+eng+math;같은 것도 순수하게 Exam 데이터 구조만을 이용하고 있으니 Exam이 하도록 주는게 바람직하다. int total = exam.total(); 처럼 변경해주자.

UI 코드는 분리하는 것이 기본

하나의 캡슐이라 해도 분리해야 할 게 있는데, 캡슐 안에서 메소드 안에 변화가 일어날 것들은 분리하는게 좋다. UI가 대표적이다.

현재 ExamList엔 input()print()가 있고 이들의 역할은 다음과 같다.

input() : 사용자로부터 성적을 **입력** 받음, 성적을 목록에 추가
print() : 목록에서 성적을 꺼냄, 꺼낸 성적을 **출력**함

List 본연의 임무는 추가하거나 꺼내는 것인데, 입력과 출력이 같이 들어가 있다. 이걸 다음과 같이 역할을 나눌 수 있다.

  • 입출력(사용자와 상호작용) : 사용자로부터 입력 받기 / 사용자에게 출력하기
  • 입출력과 상관 X : 목록에서 성적을 꺼내기 / 성적을 목록에 추가하기

입출력은 콘솔, 윈도우, 웹, 모바일 플랫폼 등을 사용한다. 입출력은 어떤 UI의 플랫폼을 쓰느냐에 따라 달라지는데… 입출력을 제외한 것들, 예를 들어 목록관리나 데이터 입력받기 위한 그릇으로서의 총점과 평균 같은 알고리즘 같은 것들은 UI가 달라져도 그대로 쓸 수 있다.

콘솔과 일치화된 데이터관리 클래스는 콘솔 외 다른 곳에선 재사용이 어렵다. 데이터관리를 위한 것들을 그대로 쓰고 싶다면 콘솔로부터 자유롭게 해줘야 한다. 콘솔을 분리해서 재사용하는게 좋겠는가, 플랫폼 바꿀때마다 그것들을 다~ 다시 만드는게 낫나? 답은 생각할 것도 없이 재사용이 최고다!!! 그래서 분리해 줘야한다.

현재 input()은 사용자로부터 입력input()과 성적을 목록에 추가add()가 같이 있다.
print()는 목록에서 성적 꺼내기get()과 사용자에게 출력print()가 같이 있다.
이걸 get과 add라는 메소드를 따로 만들어 ExamConsole에 추가해줘 ExamList에서 분리할 거다. 완성된 구조는 다음과 같다.

클래스명 변수 함수
ExamConsole - list:ExamList + input()
+ print()
ExamList - list:Exam[]
- current:int
+ add(Exam)
+ get():Exam

태그:

카테고리:

업데이트:

댓글남기기