객체지향 프로그래밍
  • 캡슐화 - 관련된 데이터와 기능을 하나의 단위로 묶는 것. 정보를 은닉하고 외부에서 직접적인 접근을 제한하여 안정성과 유지보수성을 높힌다.
  • 상속 - 부모 클래스의 특성과 동작을 자식 클래스가 상속받아 재사용한다. 코드의 중복을 줄이고 코드의 구조화와 유지보수를 용이하게 한다.
  • 다형성 - 오버로딩, 오버라이딩을 통해 하나의 메서드 이름으로 다양한 동작을 할 수 있다. 코드의 가독성과 재사용성을 높힌다.
  • 추상화 - 복잡한 기능을 단순화하여 필요한 기능에 집중한다.(세부 구현을 감추고 개념을 구현)
  • 객체 - 데이터와 메서드를 가지고 있으면서 실제로 상호작용하며 프로그램이 동작하는 것. 클래스로 구현되어 모듈화나 재사용성이 높다.

 

 

클래스

객체를 생성하기 위한 템플릿 또는 설계도. 데이터와 메서드를 하나로 묶은 사용자 정의 타입. 속성이나 동작에 대한 정의가 되어 있다. 클래스를 통해 인스턴스(객체)를 만들 수 있다.

- 구성요소: 필드(맴버변수), 메서드(맴버함수), 생성자(객체가 형성될 때 자동으로 호출되는 메서드), 소멸자(메모리에서 소멸될 때 자동으로 생성되는 메서드)

 

객체

클래스로 생성된 인스턴스. 클래스의 실체화된 형태. 독립적인 상태로 고유의 데이터를 가지고 있다.

 

// 클래스 선언
class Person
{
    public string Name;
    public int Age;

    public void PrintInfo()
    {
        Console.WriteLine("Name: " + Name);
        Console.WriteLine("Age: " + Age);
    }
}

Person p = new Person(); // 레퍼런스 타입
p.Name = "John";
p.Age = 30;
p.PrintInfo(); // 출력: Name: John, Age: 30

 

구조체와 클래스

  • 구조체와 클래스 모두 사용자 정의 형식. 우리가 원하는 자료형과 기능들을 뭉쳐 놓은 상태이다.
  • 구조체는 값 형식이며, 스택이라는 메모리에 각자 자동으로 할당된다.
  • 클래스는 참조 형식이며, 힙 영역에 동적 할당된다.
  • 구조체는 상속을 받을 수 없지만 클래스는 단일 상속과 다중상속이 가능하다.
  • 구조체는 작은 크기의 데이터나 단순한 구조를 작성할 때 적합하고 복잡한 경우에 클래스가 훨씬 더 사용하기 편하다.

 

접근 제한자

  • public: 외부에서 자유롭게 접근 가능
  • private: 같은 클래스 내부의 메서드나 필드에서 접근 가능
  • protected: 같은 클래스 내부와 상속 받은 자식까지만 접근 가능

 

필드

클래스 내부에 선언된 변수(맴버 변수). 데이터를 저장한다.

객체의 특징이나 속성들을 표현하기 위해 사용하며, 일반적으로 외부에서 접근하지 못하도록  private로 접근 제한자를 사용한다.

class Player
{
    // 필드 선언
    private string name;
    private int level;
}

 

메서드

클래스 내부에 선언된 함수(맴버 함수). 클래스의 동작을 정의한다.

클래스나 구조체에 대한 기능들을 정리하며, 일반적으로 외부에서 호출할 수 있도록 public으로 만들어준다.

class Player
{
    // 필드
    private string name;
    private int level;

    // 메서드
    public void Attack()
    {
        // 공격 동작 구현
    }
}

Player player = new Player();  // Player 클래스의 인스턴스 생성
player.Attack();  // Attack 메서드 호출

 

생성자

객체를 생성할 때 호출되는 메서드. 객체를 초기화하고 필요한 초기값을 설정하는 역할을 한다.

  • 클래스와 동일한 이름을 가지고 있으며, 반환 타입이 없다.
  • 여러 개를 정의 할 수 있고 매개변수의 개수와 타입에 따라 다른 생성자를 호출 할 수 있다.(오버로딩)
  • 기본적으로 매개변수가 없는 디폴트 생성자가 자동으로 생성되지만, 최소 하나라도 생성자를 만들었다면 디폴트 생성자는 더 이상 만들어지지 않는다.
class Person
{
    private string name;
    private int age;

    public void PrintInfo()
    {
        Console.WriteLine($"Name: {name}, Age: {age}");
    }
}



Person person1 = new Person();

 

소멸자

객체가 소멸이 될 때 호출되는 메서드.

  • 클래스와 동일한 이름을 가지고 있으며, 이름 앞에 ~ 기호를 붙인다.
  • 가비지 컬렉터(Garbage Collector)가 클래스가 아무데도 사용되지 않아서 메모리를 해제할 때 소멸자가 자동으로 호출된다.
  • 파일 핸들, 네트워트 연결, 데이터베이스 연결 등 외부적인 파일들에 대한 자원을 해제할 수 있다.
  • 배열이나 다른 클래스들의 선언을 인위적으로 해제할 수 있다.
  • 소멸이 언제 되는지에 대한 로깅과 디버깅을 할 수 있다.
  • 오버로딩 할 수 없다.
class Person
{
    private string name;

    public Person(string newName)
    {
        name = newName;
        Console.WriteLine("Person 객체 생성");
    }

    ~Person()
    {
        Console.WriteLine("Person 객체 소멸");
    }
}

 

프로퍼티(Property)

은닉화한 필드값(private 등)을 외부에서도 접근할 수 있는 중간 매개 역할을 만들어주는 것.

프로퍼티를 사용하여 데이터 유효성이나 제한을 거는 등 필드 접근을 제한하면 코드의 안정성과 가독성을 높힐 수 있다.

class Person
{
    private string name;
    private int age;

    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    public int Age
    {
        get { return age; }
        set { age = value; }
    }
}

Person person = new Person();
person.Name = "John";   // Name 프로퍼티에 값 설정
person.Age = 25;        // Age 프로퍼티에 값 설정

Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");  // Name과 Age 프로

보통 변수와 동일한 이름을 사용한다.(완전히 동일하면 안되기 때문에 대문자로 변환)

 

프로퍼티 접근 제한자 적용과 유효성 검사 예제

class Person
{
    private string name;
    private int age;

    public string Name
    {
        get { return name; }
        private set { name = value; } // 프로퍼티 자체는 외부에서 접근 가능하지만 세팅은 클래스 내부에서 하겠다는 제한
    }

    public int Age
    {
        get { return age; }
        set
        {
            if (value >= 0) // 대입하려는 값이 유효한가(나이에 음수가 들어오지 못하도록 제한)
                age = value;
        }
    }
}

Person person = new Person();
person.Name = "John";     // 컴파일 오류: Name 프로퍼티의 set 접근자는 private입니다.
person.Age = -10;         // 유효성 검사에 의해 나이 값이 설정되지 않습니다.

Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");  // Name과 Age 프로퍼티에 접근하여 값을 출력합니다.

 

자동 프로퍼티

프로퍼티를 간단히 정의하고 사용할 수 있도록 한다. 필드의 선언과 접근자 메서드의 구현을 컴파일러가 자동으로 처리한다.

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Person person = new Person();
person.Name = "John";     // 값을 설정
person.Age = 25;          // 값을 설정

Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");  // 값을 읽어 출력

 

 

객체(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