상속

부모 클래스라 불리는 기존의 클래스를 확장하거나 재사용할 수 있는 자식 클래스라 불리는 새로운 클래스를 생성하는 것.

자식 클래스는 부모 클래스의 맴버를 상속받을 수 있으며, 부모 클래스의 기능을 확장하거나 새로운 클래스로 재정의하는 것이 가능하다. 코드를 재사용할 수 있고 계층 구조로 표현할 수 있으며 유지 보수가 증가한다.

  • 단일 상속: 부모 클래스 하나, 자식 클래스 하나
  • 다중 상속: 부모 클래스가 여러 개 (C#에서는 지원X)
  • 인터페이스 상속: 인터페이스를 상속할 때만 다중 상속 가능. 하나의 클래스와 여러 개의 인터페이스를 상속한다.
    // 부모 클래스
    public class Animal
    {
        public string Name { get; set; }
        public int Age { get; set; }

        public void Eat()
        {
            Console.WriteLine("Animal is eating.");
        }

        public void Sleep()
        {
            Console.WriteLine("Animal is sleeping.");
        }
    }

    // 자식 클래스
    public class Dog : Animal
    {
        public void Bark()
        {
            Console.WriteLine("Dog is bark.");
        }
    }

    public class Cat : Animal
    {
        public void Meow()
        {
            Console.WriteLine("Cat is meow.");
        }

        public void Sleep() // 부모 클래스인 Animal의 Sleep을 숨긴다
        {
            Console.WriteLine("Cat Sleep!");
        }
    }

    static void Main(string[] args)
    {
        Dog dog = new Dog();
        dog.Name = "Bobby";
        dog.Age = 3;

        dog.Eat();
        dog.Sleep();
        dog.Bark();

        Cat cat = new Cat();
        cat.Name = "Kkami";
        cat.Age = 10;

        cat.Eat();
        cat.Sleep();
        cat.Meow();
    }
}

 

 

다형성

같은 타입이지만 다양한 동작을 수행할 수 있는 능력

 

가상 메서드

부모 클래스에서 정의되고 자식 클래스에서 재정의된다.

virtual 키워드를 사용하여 선언되며, 자식 클래스에서 필요에 따라 재정의 될 수 있다. 이를 통해 자식 클래스에서 부모 클래스의 메서드를 변경하거나 확장 가능하다.

// 가상 메서드
public class Unit
{
    public virtual void Move() // virtual -> 자신을 상속한 자식들이 재구현 할 수 있음을 선언. 실형태가 다를 수 있음을 확인해라
    {
        Console.WriteLine("두발로 걷기");
    }

    public void Attack()
    {
        Console.WriteLine("Unit 공격");
    }
}

public class Marine : Unit
{

}

public class Zergling : Unit
{
    public override void Move() // override로 재정의
    {
        Console.WriteLine("네발로 걷기");
    }
}

// 가상 메서드 호출
// #1 참조형태와 실형태가 같을때
Marine marine = new Marine();
marine.Move();
marine.Attack();

Zergling zergling = new Zergling();
zergling.Move();
zergling.Attack();

// Unit 클래스로 관리
// #2 참조형태와 실형태가 다를때
List<Unit> list = new List<Unit>();
list.Add(new Marine());
list.Add(new Zergling());

foreach (Unit unit in list)
{
    unit.Move();
}

 

추상 클래스와 메서드

직접적으로 인스턴스를 생성할 수 없다. 주로 이걸 기점으로 만든다는 느낌으로 베이스로 사용된다.

abstract 키워드를 사용하여 선언되며, 추상 메서드는 추상 메서드를 포함할 수 있다. 선언만 하고 구현하지 않기 때문에 자식 클래스에서 반드시 구현되어야 한다.

// 추상클래스
abstract class Shape
{
    public abstract void Draw();
}

class Circle : Shape
{
    public override void Draw() // 추상 메서드를 상속 받았기 때문에 반드시 구현
    {
        Console.WriteLine("Drawing a circle");
    }
}

class Square : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a square");
    }
}

class Triangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a triangle");
    }
}

// 추상 메서드 호출
// Shape shape = new Shape(); // 인스턴스를 만들 수 없음
List<Shape> list = new List<Shape>();
list.Add(new Circle());
list.Add(new Square());
list.Add(new Triangle());

foreach (Shape shape in list)
{
    shape.Draw();
}

 

 

오버라이딩과 오버로딩

 

오버라이딩

부모 클래스에 이미 정의되어 있는 메서드를 자식이 재정의 하는 것.(덮어쓰기) 이때 메서드의 이름과 매개변수의 반환 타입이 똑같아야 한다.

부모의 클래스에 정의되어 있는 동작이 아니라 새로운 동작을 재구현 할 수 있다.

 

오버로딩

매개변수의 갯수나 타입, 순서가 다른 동일한 이름의 메서드들이 여러 개 있는 것.

함수를 읽어올 때 골라 읽어올 수 있다.

 

 

클래스(Classes)

클래스의 이름은 함수나 변수의 이름과는 다르게 첫글자를 대문자로 시작한다.

self는 __init__ 생성자 함수를 만들 때 필요한 첫 번째 파라미터이다. self를 넣지 않으면 예기치 못한 오류를 발생시킬 수 있다.

# 클래스 (Classes)

class Student:

    def __init__(self, name, major, is_graduated):
        self.name = name
        self.major = major
        self.is_graduated = is_graduated

    def study(self):
        print(f'{self.name} 학생은 공부 중입니다.')

# 인스턴스

student_1 = Student('김사람', 'ㅇㅇ과', False)

student_1_name = student_1.name
print(student_1_name)

student_1.study()

인스턴스화 된 student_1을 바로 print로 출력하면 지정된 주소값이 나온다. 저장된 값을 불러오기 위해서는 변수를 만들어 넣어주어야한다.study 함수에는 이미 출력 함수가 들어있기 때문에 따로 print에 넣어주지 않아도 출력된다.

 

정보를 수정하는 방법에 대해 알아보자.

class Student:

    def __init__(self, name, major):
        self.name = name
        self.major = major
        self.is_graduated = False

    def study(self):
        print(f'{self.name} 학생은 공부 중입니다.')

# 인스턴스

student_1 = Student('김사람', 'ㅇㅇ과')

student_1.major = 'ㅁㅁ과'
print(student_1.major)

위처럼 직접적으로 정보를 수정할 수 있다. 그러나 이 방법은 보안 부분에서 좋지 않은 방법이다.

 

함수를 구성하여 인스턴스의 속성을 바꾸는 것이 바람직하다.

class Student:

    def __init__(self, name, major):
        self.name = name
        self.major = major
        self.is_graduated = False

    def study(self):
        print(f'{self.name} 학생은 공부 중입니다.')

    def edit_major(self, new_major):
        student_1.major = new_major
        print(f'{student_1.major}로 전공이 변경되었습니다.')

# 인스턴스

student_1 = Student('김사람', 'ㅁㅁ과')

student_1.edit_major('ㅁㅁ과')
print(student_1.major)

 

 

상속(Inheritance)

기존에 사용하던 클래스의 공통적인 속성을 가져와 사용할 수 있다.

class Student:

    def __init__(self, name, major):
        self.name = name
        self.major = major
        self.is_graduated = False

    def study(self):
        print(f'{self.name} 학생은 공부 중입니다.')

# 인스턴스

student_1 = Student('김사람', 'ㅇㅇ과')

student_1.major = 'ㅁㅁ과'
print(student_1.major)

# 상속
class ForeignStudent(Student):

    def __init__(self, name, major, country):
        super().__init__(name, major)
        self.country = country

foreign_stud_1 = ForeignStudent('박사람', '도예학과', '미국')
print(foreign_stud_1.name)
print(foreign_stud_1.major)
print(foreign_stud_1.country)
print(foreign_stud_1.is_graduated)

 

부모 클래스에서 정의된 함수를 자식 클래스에서 덮어 쓰는 것을 오버라이딩이라고 한다.

class Student:

    def __init__(self, name, major):
        self.name = name
        self.major = major
        self.is_graduated = False

    def study(self):
        print(f'{self.name} 학생은 공부 중입니다.')

# 인스턴스

student_1 = Student('김사람', 'ㅇㅇ과')

student_1.major = 'ㅁㅁ과'
print(student_1.major)

# 상속
class ForeignStudent(Student):

    def __init__(self, name, major, country):
        super().__init__(name, major)
        self.country = country
        
    # 오버라이딩
    def study(self):
        print(f'{self.name} is studing now.')

foreign_stud_1 = ForeignStudent('박사람', '도예학과', '미국')

foreign_stud_1.study()

 

만든 클래스를 모듈에서 함수를 불러왔던 것처럼 활용할 수 있다.

# from classes import Student, ForeignStudent
from classes import * # 모든 함수

stud_1 = Student('김사람', 'ㅁㅁ과')
foreign_stud_1 = ForeignStudent('박사람', '도예학과', '미국')

stud_1.study()
foreign_stud_1.study()

 

 

'Back-End > Python' 카테고리의 다른 글

예외 처리와 파일 다루기  (0) 2023.08.10
회원가입 프로그램 실습 - 파이썬(Python) 편  (0) 2023.08.08
함수  (0) 2023.08.07
자료구조 - 리스트 / 튜플 / 딕셔너리  (0) 2023.07.27
조건문과 반복문  (0) 2023.07.20

객체(Object)

일반적으로 사물은 속성과 기능을 갖는다. 속성은 정적인 정보를, 기능은 동적으로 수행할 수 있는 역할, 움직임을 가르킨다.

연관성이 있는 사물은 분류할 수 있다. 공통적으로 가지고 있는 특성을 묶고 그 관계를 코드로 구현하는 것이 자바 객체지향이라고 본다. 사물이 가진 어떤 속성이나 기능이 프로그래밍에서 속성은 맴버 변수, 기능은 메서드(함수)로 대응할 수 있다.

사물을 생산하기 위해 설계하는 것이 클래스, 생산이 된 결과를 인스턴스로 볼 수 있다.(사전이나 서적에 따라 인스턴스 자체를 객체로 보기도 한다.)

 

 

클래스

은행의 계좌에서 일어나는 여러가지 속성과 기능을 나누어 구현해보자.

public class BankAccount {
	//맴버 변수
	int bankCode;
    int accountNo;
    String owner;
    int balance;
    boolean isDormant;
    int passward;
    
    //메서드
    void inquiry() {}
    void deposit() {}
    void withdraw() {}
    void heldInDormant() {}
}

맴버 변수와 메서드를 작성한다.

클래스를 작성할 때, 현실세계의 사물을 어떤 식으로 일반화 시킬지, 공통적인 속성이 무엇인지 파악하는 것이 중요하다.

실제 프로그램을 구현할 때에도 전체적인 업무를 하나하나 개별적으로 구현하는 것이 아니라 공통적인 속성과 기능을 분리한 후, 클래스로 나타내는 것이 좋다.

 

 

생성자

생성자는 클래스 내부에 정의되어야 하며, 생성자 메서드명은 클래스명과 일치해야한다.

BankAccount(
		int bankCode,
        int accountNo,
        String owner,
        int balance,
        int passward,
        boolean isDormant
) {
	this.bankCode = bankCode;
    this.accountNo = accountNo;
    this.owner = owner;
    this.balance = balance;
    this.passward = passward;
    this.isDormant = isDormant;
}

전체적인 코드를 구성하는 클래스명이 BankAccount이기 때문에 생성자 또한 BankAccount로 한다. 생성자는 new 연산자와 함께 사용해야한다. 클래스 내부에 생성자가 없는 경우, 자바에서 생성자 내부에 값이 없는 빈생성자를 자동으로 추가한다. 사용자가 어떤 파라미터를 받는 생성자 함수를 만들어주면 더이상 생성자를 자동으로 만들어주지 않는다.

생성에 필요한 정보들은 생성자 함수 옆 괄호 안에 넣어주어야한다.

중괄호는 데이터를 할당해서 실제적으로 의미있는 값을 가지는 인스턴스를 만드는 과정이다. this는 인스턴스 자기자신을 가르키는 특수한 변수이다. this 뒤에는 클래스에 정의된 맴버 변수가 온다. 맴버 변수를 통해 인스턴스 자기자신에 입력받은 값, 인자로써 입력받은 값을 전달받게 된다. 즉, 왼쪽의 bankCode는 클래스에 정의된 맴버 변수의 의미를 가지며, 오른쪽의 bankCode는 사용자로부터 혹은 프로그램으로부터 입력받은 값을 의미한다.

 

생성자를 사용해서 만드는 것은 다른 곳에서 진행하는 것이 일반적이다. 지금까지 만든 것이 설계도라면 실제로 사용하는 것들은 로직이 구현된 파일에서 진행된다.

public static void main(String[] args) {

    BankAccount bankAccount = new BankAccount();
    System.out.println(account);
    System.out.println(account.bankCode); // 0
    System.out.println(account.isDormant);  // false
}

main 함수에서 실행할 수 있는 상태로 만들어준 뒤, BankAccount라고 입력하면 우리가 기존에 만들었던 클래스를 불러오게 된다.

위에서 만들었던 생성자 중, 기본 빈생성자를 불러오면 인스턴스의 맴버 변수를 출력했을 때 데이터가 아무것도 할당되어 있지 않기 때문에 객체는 존재하지만 기본값이 출력된다.

 

 

상속

객체지향에서는 상속이라는 개념이 존재한다.

상속을 받는 클래스를 자식 클래스라고 하고 상속의 대상이 되는 클래스를 부모 클래스라고 한다.

객체지향은 재사용과 범주화가 핵심이다. 공통 속성에 수정사항이 생기면 코드 하나하나를 수정하는 것은 비효율적이다.

public class SavingsAccount extends BankAccount {
	boolean isOverdraft;
    void transfer() {};
}

부모 클래스 BankAccount를 재활용하기 위해 자식 클래스인 extends 키워드를 사용한다. 이렇게하면 SavingsAccount 클래스는 부모 클래스인 BankAccount가 가진 모든 것들을 가지고 시작한다. 그리고 맴버 변수를 정의하여 추가하면 된다.

public class DollarAccount extends BankAccount {

    void transfer() {}
}
public class SubscriptionAccount extends BankAccount {

    int numOfSubscription;
}

위처럼 부모 클래스를 상속받아 재활용할 수 있다.

상속을 할 때에는 하나의 클래스만 상속받을 수 있다. 이것을 단일상속이라고 한다. 자바 언어는 단일상속 언어이며, 다른 언어에서는 다중상속을 하는 경우도 있다. 다중상속에는 장단점이 있는데 어떤 부모클래스에서 온 속성인지 파악하기 어렵기 때문에 모호성을 제거하기 위해 자바에서는 단일상속을 지원한다.

 

오버로딩과 오버라이딩

오버로딩과 오버라이딩은 부모 자식관계의 클래스들을 효과적으로 정의하고 활용하기 위해 나온 개념이다.

오버로딩은 부모 클래스에서 상속받은 메서드에서 파라미터를 변경하는 것이다. 이것을 통해 새로운 메서드를 정의한다.

inquiry는 부모 클래스에서는 계좌를 조회하는 메서드이다. 다른 파라미터를 넘겨받고 싶거나 다른 형태의 인자를 넘겨받고 싶다면 인자를 추가할 수 있다.

오버라이딩은 덮어쓰기라는 뜻이다. 부모 클래스에서 상속받은 메서드의 내용을 자식 클래스의 상황에 맞게 변경한다.

오버로딩은 파라미터가 바뀌는 것이고 오버라이딩은 중괄호, 메서드의 코드블락이 변경되는 것이다.

public class DollarAccount extends BankAccount {

    void inquiry(double currencyRate) {}

    void deposit() {

    }
}

위 코드는 double로 달러 환율을 추가해서 함수를 재정의 했다. 잔액을 조회했을 때, 환율을 계산해서 미화로 결과를 확인할 수 있는 오버로딩된 메서드이다.

deposit의 경우, 원화와 달러 입금방식이 다르기 때문에 부모 클래스 BankAccount에 정의된 내용을 쓰지 않고 DollarAccount만의 고유한 입금방식을 취하기 위해 재정의 한다. 오버라이딩이 된 메서드는 부모 클래스의 파라미터 설정을 그대로 따른다.

즉, 파라미터 설정은 오버로딩을 통해 만들고 오버라이딩 된 것은 파라미터는 그대로고 중괄호 안의 내용을 바꿔주는 것이다.

 

 

접근제어자

클래스, 멤버 변수, 메서드, 생성자 등의 접근을 제어한다. 그 중 가장 많이 쓰이는 멤버 변수의 접근제어자에 대해 알아보자.

접근제어자는 일반적으로 타입 앞에 나타내게 되는데 타입 앞에 접근제어자가 없이 선언이 된 경우, 아무나 이 클래스에서 생성된 인스턴스의 코드를 임의로 변경할 수 있다. 즉, 객체 멤버 변수를 무분별하게 접근하고 수정할 수 있다. 보안과 안정성에 큰 문제를 야기할 수 있다.

public static void main(String[] args) {

    BankAccount bankAccount = new BankAccount();
    bankAccount.password = 123456;
    System.out.println(bankAccount.password());

}

bankAccount의 password를 임의로 지정한 뒤, 출력하면 그대로 덮어씌워져서 출력된다. 비밀번호를 임의로 바꾸는 것도 문제지만 비밀번호를 바꾸는 일정한 규칙이 있다면 그 규칙에 맞지 않는 데이터가 들어갈 수 있다. 그렇기 때문에 하나의 클래스에 정의된 멤버 변수, 그리고 인스턴스에 저장된 속성을 각각 역할에 맞는 함수를 만들어서 변경시켜야한다. 이렇게 마음대로 활용할 수 없도록 하는 것이 접근제어자이다.

 

public class BankAccount {

    // 멤버변수
    private int bankCode;
    private int accountNo;
    private String owner;
    private int balance;
    private boolean isDormant;
    private int password;
    
    // 메서드
    public void inquiry() {}
    public void deposit() {}
    public void withdraw() {}
    public void heldInDormant() {}
    public void changePassword(int password) {
        this.password = password;
    }
 }

 

접근제어자 private은 동일한 클래스에서만 수정과 참조가 가능하다. this를 통해 비밀번호를 대입해준다. 메서드는 접근제어자 public을 통해 외부에서 활용 가능한 형태로 만들어준다.

private은 값을 수정하는 것 뿐만 아니라 조회도 불가능하게 만든다. 그렇기 때문에 멤버 변수의 값을 조회하고 수정하기 위해 GetterSetter를 활용한다.

우클릭을 하여 Generate를 누르면 Getter와 Setter를 지정할 수 있다.

이렇게 하면 기존의 함수에 get과 set이 붙은 형태로 만들어진다.

public int getBankCode() {
        return bankCode;
}

public void setBankCode(int bankCode) {
    this.bankCode = bankCode;
}

public int getAccountNo() {
    return accountNo;
}

public void setAccountNo(int accountNo) {
    this.accountNo = accountNo;
}

public String getOwner() {
    return owner;
}

public void setOwner(String owner) {
    this.owner = owner;
}

public int getBalance() {
    return balance;
}

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

public boolean isDormant() {
    return isDormant;
}

public void setDormant(boolean dormant) {
    isDormant = dormant;
}

public int getPassword() {
    return password;
}

public void setPassword(int password) {
	this.password = password;
}

getter와 setter가 만들어졌기 때문에 passward를 .으로 직접 조회하는 것이 아니라

public static void main(String[] args) {

    BankAccount bankAccount = new BankAccount();
    bankAccount.changePassword(123456);
    System.out.println(bankAccount.getPassword());

}

getPassward를 통해 바뀐 비밀번호를 조회한다.

 

 

인터페이스

클래스와 인터페이스는 다르다. 클래스가 상세한 설계도라면 인터페이스는 스케치 수준의 설계도이다. 파라미터, 반환, 타입만 가질 수 있으며 실제 코드를 구현할 수 있는 중괄호로 어떤 작업이 실행되는지는 정의할 수 없다.

인터페이스는 기능의 표준화를 달성하기 위한 도구이다. 공통적인 기능을 일정한 단위와 범주로 묶어 처리하고 그것을 구현할 클래스에서 각 업무 로직에 맞게 구현할 수 있도록 한 것이다. 그렇기 때문에 클래스에 신규 기능이 생기거나 삭제되는 기능이 생긴다면 인터페이스를 통해 효율적인 작업을 할 수 있다.

우클릭을 하면 New - Java Class에서 인터페이스를 생성할 수 있다.

public interface Withdrawable {
    void withdraw();
}

 

public class SavingsAccount extends BankAccount implements Withdrawable {

    boolean isOverdraft;

    void transfer() {};
    public void withdraw() {
    	System.out.println("Withdraw");
    };
}

추가한 인터페이스를 가져다 활용하려면 클래스 옆에 implements라는 키워드와 인터페이스명을 입력한다. 인터페이스를 명시하면 구현한 인터페이스의 함수를 반드시 클래스에서 구현해야한다. 인터페이스에서 함수를 가져와 재정의 할 때에는 접근제어자 public을 붙여주어야한다.

 

 

'Back-End > Java' 카테고리의 다른 글

Web - 클라이언트와 서버  (0) 2023.05.22
예외 - 예외 처리  (0) 2023.05.19
회원가입 프로그램 실습 - 자바(Java) 편  (1) 2023.05.16
함수  (0) 2023.05.15
자료구조 - 배열, 리스트, 맵  (0) 2023.05.12

+ Recent posts