2024.07.01.월.TIL 내일배움캠프 53일차 <객체지향>

김기남·2024년 7월 1일
0
post-thumbnail

안녕하세요, 오늘도 다시금 되새겨보는 시간, 객체지향에 대해 캠프 학습자료를 바탕으로 다시 정리해보았습니다.

객체지향이란?

객체(Object)라는 단어가 사용된 이유를 밝히기 위해 이 단어를 사물, 물건 등의 일상적인 단어로 바꿔보겠습니다. 사물은 일반적으로 속성과 기능을 가지게 되며, 연관성이 있는 사물은 같은 분류로 묶을 수 있습니다. 이 묶는 작업이 굉장히 중요합니다. 공통적으로 가지고 있는 특성을 묶어 그 관계를 구조화 하는 것이 Java 코드에도 적용 되기 때문입니다.

위 설명에 대한 이해를 돕기 위해 우리가 늘상 함께 하는 물건인 스마트폰을 예시로 도식을 준비하였습니다.

대표적인 제조사 두 곳을 기준으로 나누어진 분류는 다시 모델에 따라 새로운 분류로 나뉩니다. 스마트폰의 구성요소를 속성과 기능으로 나타내면 다음과 같습니다.

  • 스마트폰
    • 속성: AP, RAM 용량, 저장 장치, 카메라
    • 기능: 통화 하기, 사진 찍기, 영상 재생하기 등

속성은 대체로 정적인 정보를 다루며, 기능은 작업에 대한 수행을 나타냅니다. 사물이 가진 속성과 기능이 프로그래밍의 세계에서는 각각 멤버변수와 메서드(함수)에 대응되고 우리는 이 방식으로 Java의 생태계에서 사용되는 프로그램을 작성하게 되는 것입니다.

스마트폰이 실제로 만지고 사용할 수 있는 형태로 소비자에게 전달되려면 설계에 맞게 생산하는 과정을 거쳐야 합니다. 이 때 설계도 역할을 하는 것이 클래스(Class), 생산된 스마트폰이 인스턴스(Instance) 입니다. 인스턴스는 번역에 따라 객체, 혹은 개체라고 부르기도 합니다.

클래스와 인스턴스

사물이 가진 속성과 기능을 멤버변수와 메서드로 나타내는 것이 바로 객체지향 구현의 첫 걸음이라고 볼 수 있죠. 은행 계좌는 모두 공통적인 속성과 기능을 가집니다. 이를 뽑아내어 멤버변수와 메서드로 표현하게 되면 개별 계좌를 생성하기 위한 설계도인 클래스가 되는 것입니다.

class BankAccount {

    // 멤버변수
    int bankCode;               // 은행 코드
    int accountNo;              // 계좌 번호
    String owner;               // 예금주
    int balance;                // 잔액
    boolean isDormant;          // 휴면계좌 여부
    int password;               // 비밀번호

		// 메서드(함수)
    void inquiry() { }          // 계좌 조회
    void deposit() { }          // 계좌 입금
    void heldInDormant() { }    // 휴면계좌 전환
}

설계도가 있다고 해서 그 기능을 사용할 수 없듯이 클래스에서 인스턴스를 생성하지 않으면 이를 활용하는 것은 불가능합니다. 클래스로부터 인스턴스를 생성하는 방법과 규칙을 익히기 위해 생성자에 대한 학습을 이어서 진행해봅시다.

생성자(Constructor)

생성자는 클래스로부터 인스턴스를 생성하는 메서드의 한 종류입니다. 클래스 내부에 정의하며, 메서드명이 클래스명과 일치해야 한다는 규칙이 있습니다. 소스를 통해 확인해보시죠.

class BankAccount {

    // 멤버변수
    int bankCode;               // 은행 코드
    int accountNo;              // 계좌 번호
    String owner;               // 예금주
    int balance;                // 잔액
    boolean isDormant;          // 휴면계좌 여부
    int password;               // 비밀번호

		// 메서드(함수)
    void inquiry() { }          // 계좌 조회
    void deposit() { }          // 계좌 입금
    void heldInDormant() { }    // 휴면계좌 전환

    // 기본 생성자
    BankAccount() {
        // 인스턴시 생성시 수행할 명령
        // 인스턴스의 멤버변수 초깃값 설정
    }
}

생성자 내부에는 일반적으로 인스턴시 생성시 수행할 명령과 멤버변수의 초깃값을 설정하는 코드를 작성합니다. 여기까지 작성이 되면 인스턴스가 자동으로 생성되는 것일까요? 그렇지 않습니다. 이 부분이 생성자를 다루는 데에 있어서 가장 많이 혼동하는 부분입니다.

BankAccount account1 = new BankAccount();

클래스 내부에 정의된 생성자는 반드시 new 연산자와 함께 쓰여야 새로운 인스턴스를 생성하게 됩니다. 이렇게 생성된 인스턴스는 서로 다른 변수에 할당된 후 프로그램 내부에서 활용됩니다.

만약 클래스 내부에 생성자가 하나도 정의되어 있지 않다면 어떻게 될까요? 인스턴스를 생성할 수 없는 쓸모 없는 클래스가 된다고 생각할 수도 있습니다. 하지만 Java 컴파일러는 클래스 내부에 생성자가 정의되어 있지 않으면 public 클래스명() { } 과 같이 파라미터가 없는 생성자를 자동으로 추가하여 컴파일 합니다. 이 때 추가되는 생성자를 기본 생성자라고 합니다. 만약 파라미터가 있는 생성자를 클래스에 정의하게 되면 기본 생성자는 자동으로 생성되지 않기 때문에 직접 작성해주어야 합니다.

public class BankAccount {

    // 멤버변수
    int bankCode;               // 은행 코드
    int accountNo;              // 계좌 번호
    String owner;               // 예금주
    int balance;                // 잔액
    boolean isDormant;          // 휴면계좌 여부
    int password;               // 비밀번호

		// 메서드(함수)
    void inquiry() { }          // 계좌 조회
    void deposit() { }          // 계좌 입금
    void heldInDormant() { }    // 휴면계좌 전환

    // 기본 생성자
    BankAccount() {
        // 인스턴시 생성시 수행할 명령
        // 인스턴스의 멤버변수 초깃값 설정
    }

    // 기본 생성자
    BankAccount(int bankCode, int accountNo, String owner, int balance, int password) {
        this.bankCode = bankCode;
        this.accountNo = accountNo;
        this.owner = owner;
        this.balance = balance;
        this.password = password;
        this.isDormant = false;
    }
}

파라미터를 활용한 새로운 생성자를 보면 처음 접하는 this 라는 키워드가 눈에 띕니다. 이는 인스턴스 자기 자신을 가리키는 특수한 변수이며, this.bankCode = bankCode; 에서 좌측의 this.bankCode 는 생성될 인스턴스의 변수를, 우측의 bankCode 파라미터로 넘겨받아 인스턴스에 할당할 값을 나타냅니다.

상속(Inheritance)

은행에서 만드는 계좌는 그 종류가 수백 가지에 달할 정도로 많습니다. 하지만 계좌라면 공통적으로 가져할 속성들이 있죠. 프로그래밍으로 공통 속성을 일일이 구현하는 것은 생각만 해도 비효율적이라는 생각이 듭니다. 하지만 이보다 더 큰 문제점은 공통 속성에 수정 사항이 생겼을 때 입니다. 계좌라는 객체 전체에 변경사항을 일일이 적용하려면 이는 신속한 대응과 유지보수에도 큰 걸림돌이 됩니다.

class BankAccount {

    // 멤버변수
    int bankCode;               // 은행 코드
    int accountNo;              // 계좌 번호
    String owner;               // 예금주
    int balance;                // 잔액
    boolean isDormant;          // 휴면계좌 여부
    int password;               // 비밀번호

		// 메서드(함수)
    void inquiry() { }          // 계좌 조회
    void deposit() { }          // 계좌 입금
    void heldInDormant() { }    // 휴면계좌 전환

    // 기본 생성자
    BankAccount() {
        // 인스턴시 생성시 수행할 명령
        // 인스턴스의 멤버변수 초깃값 설정
    }

    // 기본 생성자
    BankAccount(int bankCode, int accountNo, String owner, int balance, int password) {
        this.bankCode = bankCode;
        this.accountNo = accountNo;
        this.owner = owner;
        this.balance = balance;
        this.password = password;
        this.isDormant = false;
    }
}

class savingsAccount extends BankAccount {      // 자유입출금통장

    boolean isOverdraft;        // 마이너스 통장 여부
    void withdraw() { }         // 예금 인출
    void transfer() { }         // 계좌 송금
}

class subscriptionAccount extends BankAccount { // 청약통장

    int numOfSubscription;      // 납입횟수
}

class dollarAccount extends BankAccount {       // 달러입출금통장

    void withdraw() { }         // 예금 인출
    void transfer() { }         // 계좌 송금
}

객체 간의 상속 관계에서 상속을 해주는 클래스는 부모 클래스, 상속을 받는 클래스는 자식 클래스 라고 합니다. class 부모클래스 extends 자식클래스 와 같이 extends 키워드로 연결해주면 클래스 간 상속 관계가 형성됩니다. 자식 클래스 내부에 별도로 정의해주지 않아도 부모 클래스에 정의된 멤버변수와 메서드를 그대로 이어 받아 사용할 수 있게 되는 것입니다.

상속을 사용할 때에 주의할 점은 Java 언어가 다중 상속을 지원하지 않는 단일 상속 언어라는 것입니다. class 부모클래스 extends 자식클래스1, 자식클래스2 와 같이 여러 클래스를 상속받지 못하게 함으로써 멤버변수나 메서드에 대한 모호성을 제거하는 효과를 얻게 되었습니다.

오버로딩, 오버라이딩

Java 학습시 많은 분들께서 헷갈리는 두 가지 용어가 있습니다. 바로 오버로딩과 오버라이딩입니다. 부모-자식 관계의 클래스에서 메서드를 효과적으로 정의하고 사용하기 위한 이 두 가지 개념 역시 객체지향을 효과적으로 구현하기 위해 등장했습니다. 코드를 한 번 볼까요? 🧐

class BankAccount {

    // 멤버변수
    int bankCode;               // 은행 코드
    int accountNo;              // 계좌 번호
    String owner;               // 예금주
    int balance;                // 잔액
    boolean isDormant;          // 휴면계좌 여부
    int password;               // 비밀번호

		// 메서드(함수)
    void inquiry() { }          // 계좌 조회
    void deposit() { }          // 계좌 입금
    void heldInDormant() { }    // 휴면계좌 전환

    // 기본 생성자
    BankAccount() {
        // 인스턴시 생성시 수행할 명령
        // 인스턴스의 멤버변수 초깃값 설정
    }

    // 기본 생성자
    BankAccount(int bankCode, int accountNo, String owner, int balance, int password) {
        this.bankCode = bankCode;
        this.accountNo = accountNo;
        this.owner = owner;
        this.balance = balance;
        this.password = password;
        this.isDormant = false;
    }
}

class savingsAccount extends BankAccount {      // 자유입출금통장

    boolean isOverdraft;        // 마이너스 통장 여부
    void withdraw() { }         // 예금 인출
    void transfer() { }         // 계좌 송금
}

class subscriptionAccount extends BankAccount { // 청약통장

    int numOfSubscription;      // 납입횟수
}

class dollarAccount extends BankAccount {       // 달러입출금통장

		void inquiry(double currencyRate) { }       // 오버로딩
    void withdraw() { }         // 예금 인출
    void deposit() { }          // 계좌 입금 - 오버라이딩
    void transfer() { }         // 계좌 송금
}
  • 오버로딩(Overloading - 과적)
    • 조상 클래스에서 상속받은 메서드에서 파라미터를 변경하여 새로운 메서드를 정의하는 것
    • dollarAccount 클래스에 void inquiry(double currencyRate) 가 새롭게 정의되었습니다. 달러 전용 계좌이기 때문에 원화로 환산하기 위한 환율을 추가해준 것이며, 부모 클래스의 메서드명은 그대로 사용합니다.
  • 오버라이딩(Overriding - 덮어쓰기)
    • 조상 클래스에서 상속받은 메서드의 내용을 자식 클래스의 상황에 맞게 변경해서 정의하는 것
    • 같은 입금이라고 하더라도 원화와 달러의 입금 방식이 다르기 때문에 메서드의 내용도 달라야 합니다. void deposit() { } 의 기존 내용을 수정하는 개념으로 보시면 됩니다. 주의할 점은 오버로딩된 메서드와 다르게 부모 클래스의 파라미터 설정을 그대로 따릅니다.

접근 제어자(Access Modifier)와 Getter/Setter

접근 제어자는 용어 그대로 클래스, 멤버 변수, 메서드, 생성자 등에 대한 접근을 제한하는 역할을 수행하는 키워드 입니다. 그 중 접근 제어자가 중요한 역할을 하는 곳이 바로 멤버 변수이죠.

class BankAccount {

    // 멤버변수 - priavte 제어자 사용
    private int bankCode;               // 은행 코드
    private int accountNo;              // 계좌 번호
    private String owner;               // 예금주
    private int balance;                // 잔액
    private boolean isDormant;          // 휴면계좌 여부
    private int password;               // 비밀번호

		// 메서드(함수)
    void inquiry() { }          // 계좌 조회
    void deposit() { }          // 계좌 입금
    void heldInDormant() { }    // 휴면계좌 전환

    // 기본 생성자
    BankAccount() {
        // 인스턴시 생성시 수행할 명령
        // 인스턴스의 멤버변수 초깃값 설정
    }

    // 기본 생성자
    BankAccount(int bankCode, int accountNo, String owner, int balance, int password) {
        this.bankCode = bankCode;
        this.accountNo = accountNo;
        this.owner = owner;
        this.balance = balance;
        this.password = password;
        this.isDormant = false;
    }
}

멤버 변수에 private 을 설정하지 않으면 객체의 멤버 변수에 대해 무분별한 접근 및 수정을 막을 수 없어 보안과 안정성에서 큰 문제를 야기할 수 있습니다. 가령 4자리만 설정될 수 있는 int 형의 password 라는 멤버 변수에 this.password = 123456; 과 같이 의도치 않게 6자리의 값이 할당되어도 제어할 수 있는 방법이 없습니다. 즉, 값을 조회하고 할당하는 것은 this.멤버변수 와 같이 직접 호출을 하는 것이 아니라 전용 메서드를 생성하여 처리해야 합니다.

또한, 굳이 공개될 필요가 없는 시스템 내부의 값들을 가리는 데에도 private 제어자가 사용됩니다. 시스템 내부에서만 사용되는 변수까지 외부의 프로그램에 노출된다면 프로그램이 굉장히 복잡해지겠죠. 이렇게 정보 일부를 가리는 것을 정보 은닉, 혹은 캡슐화 라고 합니다. 엑셀에서 불필요한 컬럼을 숨기기 처리 하는 것과 비슷한 과정입니다.

멤버변수를 직접 호출하지 않고 값을 조회/할당하기 위해서 사용되는 함수를 각각 Getter/Setter 라고 합니다. 다음 예시를 통해 그 형태를 확인해봅시다.

class BankAccount {

    // 멤버변수 - priavte 제어자 사용
    private int bankCode;               // 은행 코드
    private int accountNo;              // 계좌 번호
    private String owner;               // 예금주
    private int balance;                // 잔액
    private boolean isDormant;          // 휴면계좌 여부
    private int password;               // 비밀번호

		// 메서드(함수)
    void inquiry() { }          // 계좌 조회
    void deposit() { }          // 계좌 입금
    void heldInDormant() { }    // 휴면계좌 전환

    // 기본 생성자
    BankAccount() {
        // 인스턴시 생성시 수행할 명령
        // 인스턴스의 멤버변수 초깃값 설정
    }

    // 기본 생성자
    BankAccount(int bankCode, int accountNo, String owner, int balance, int password) {
        this.bankCode = bankCode;
        this.accountNo = accountNo;
        this.owner = owner;
        this.balance = balance;
        this.password = password;
        this.isDormant = false;
    }

		public int getBalance() {               // Getter 
        return balance;
    }

    public void setBalance(int balance) {   // Setter
        this.balance = balance;
    }

}

1) Getter

  • 멤버 변수의 값을 조회하기 위한 메서드
  • get멤버변수명() 의 형식으로 명명
  • 객체변수.get멤버변수명() 의 형식으로 사용

2) Setter

  • 멤버 변수에 값을 할당하기 위한 메서드
  • set멤버변수명(파라미터) 의 형식으로 명명
  • 객체변수.set멤버변수명(파라미터) 의 형식으로 사용

인터페이스(Interface)

인터페이스는 설계 이전 스케치 수준의 클래스입니다. 메서드의 이름과 파라미터, 반환 형식만 가질 뿐 실제 구현부는 가질 수 없습니다. 클래스처럼 인스턴스를 생성하는 것도 불가능하죠. 그렇다면 왜 인터페이스라는 개념이 등장하게 된 것일까요? 인터페이스는 기능의 표준화를 달성하도록 하는 도구입니다. 공통적인 기능을 일정한 단위로 인터페이스로 묶어 처리한 다음 이를 구현할 클래스에서 각 업무 로직에 맞게 구현할 수 있습니다. 만약 여러 클래스에 걸쳐 신규 기능이 생기거나 삭제 기능이 있다면 인터페이스라는 표준화 도구를 통해 효율적인 코드 작성이 가능해집니다.

메서드 구현은 implements 키워드를 사용하여 별도의 클래스 파일에서 해주어야 하며 예시를 통해 확인해보겠습니다.

class BankAccount {

    // 멤버변수
    int bankCode;               // 은행 코드
    int accountNo;              // 계좌 번호
    String owner;               // 예금주
    int balance;                // 잔액
    boolean isDormant;          // 휴면계좌 여부
    int password;               // 비밀번호

		// 메서드(함수)
    void inquiry() { }          // 계좌 조회
    void deposit() { }          // 계좌 입금
    void heldInDormant() { }    // 휴면계좌 전환

    // 기본 생성자
    BankAccount() {
        // 인스턴시 생성시 수행할 명령
        // 인스턴스의 멤버변수 초깃값 설정
    }

    // 기본 생성자
    BankAccount(int bankCode, int accountNo, String owner, int balance, int password) {
        this.bankCode = bankCode;
        this.accountNo = accountNo;
        this.owner = owner;
        this.balance = balance;
        this.password = password;
        this.isDormant = false;
    }
}

class savingsAccount extends BankAccount implements Withdrawable {      // 자유입출금통장

    boolean isOverdraft;        // 마이너스 통장 여부
    public void withdraw() { }  // 예금 인출 - 인터페이스 구현부
    void transfer() { }         // 계좌 송금
}

class subscriptionAccount extends BankAccount { // 청약통장

    int numOfSubscription;      // 납입횟수
}

class dollarAccount extends BankAccount implements Withdrawable {       // 달러입출금통장

    void inquiry(double currencyRate) { }       // 오버로딩
    public void withdraw() { }                  // 예금 인출 - 인터페이스 구현부
    void deposit() { }                          // 계좌 입금 - 오버라이딩
    void transfer() { }                         // 계좌 송금
}
public interface Withdrawable {

    public void withdraw();
}

예시를 보면 인출기능이 불필요한 subscriptionAccount 클래스를 제외하고 나머지 savingsAccount , dollarAccount 는 withdraw()(인출기능)에 대해 인터페이스를 구현하도록 설정되었습니다. 인터페이스를 통해 기능단위를 붙였다, 떼었다 하는 것이 손쉽게 가능합니다.

profile
새로운 시작~!

0개의 댓글