[C++ to Java] 2. Object-Oriented Programming
Java에서의 OOP에 대해 알아보자. 2021-12-23

OOP in Java

안녕하세요! JustKode 입니다. 사실 제목까지 이렇게 분리 하고, 이렇게 이야기 하기도 뭐한게, JavaObject-Oriented Programming을 위해 태어난 언어이고, 안에서 OOP를 하는 것은 당연한 이야기 이기 때문입니다. 우리는 이번 시간에 Java의 OOP에 대해, **C++**과 비교하여 제대로 짚고 넘어 가고자 합니다.

Pass by Value, Pass by Reference

일단, **C++**에서 사용 했던, Pass by ValuePass by Reference에 대해서 Java를 기준으로 간단하게 복습 하고 넘어 가겠습니다.

  • Pass by Value: 함수의 파라미터로 변수의 을 넘겨 주는 것입니다. 간단한 함수를 작성 하는 데에 유용합니다. Java에서 기본적으로 제공하는 변수(int, float, long, char 등)가 배열 형태가 아닌 형식으로 제공 되었고, 이를 Pass by Value로 넘겼다면 문제는 없지만, 배열, 객체 등을 Pass by Value로 넘겼다면, 많은 데이터를 넘기게 되어, 속도상 문제가 발생 합니다.
  • Pass by Reference: 함수의 파라미터로 변수의 참조값을 넘겨 주는 것입니다. Java에서는 객체와, 배열의 참조값이 해당합니다. 빠르게 변수를 넘겨 줄 수 있다는 장점이 있지만, 참조값을 넘겨 받다 보니, 객체의 어트리뷰트를 잘못 건드려, 의도치 않은 Side-Effect를 발생 시킬 수 있습니다.

이제 Class, Object를 다루다 보니, Pass by Reference를 신경 써야할 것 같아, 다음과 같이 이야기 하였습니다.

Make Class

한 번 Class를 만들어 볼까요? Class를 만드는 형식은 다음과 같습니다.

Player.java

public class Player {
    private String userName;
    private int healthPoint;
    private int attackPoint;

    public String getUserName() {return this.userName;};
    public int getHealthPoint() {return this.healthPoint;};
    public int getAttackPoint() {return this.attackPoint;};

    public void setAttackPoint(int attackPoint) {
        if (attackPoint < 0)
            return;
        this.attackPoint = attackPoint;
    }

    public void setHealthPoint(int healthPoint) {
        if (healthPoint < 0)
            return;
        this.healthPoint = healthPoint;
    }

    public void setUserName(String userName) {
        if (userName.length() < 2)
            return;
        this.userName = userName;
    }

    public void printUserInfo() {
        System.out.println("========================");
        System.out.println("플레이어 이름: " + this.userName);
        System.out.println("체력: " + this.healthPoint);
        System.out.println("공격력: " + this.attackPoint);
        System.out.println("========================");
    }
}

private:, public: 을 이용하여, 제한자를 적용 하던 **C++**와 다르게, Java는 멤버 함수, 멤버 변수 하나하나에 private, public을 적용 하는 모습입니다. 생성자를 작성하지 않으면, **C++**처럼 아무런 기능을 하지 않는 기본 생성자가 만들어 집니다.

또한, **C++**에서와 같이, this 를 사용 하여, 객체 자기자신의 멤버변수에 접근 가능합니다.

같은 Package내에 존재 한다면, Main.java 에서 굳이 import 하지 않아도 됩니다.

Main.java

public class Main {
    static public void main(String[] args) {
        Player player = new Player();
        player.setUserName("JustKode");
        player.setHealthPoint(100);
        player.setAttackPoint(10);
        player.printUserInfo();
    }
}
========================
플레이어 이름: JustKode
체력: 100
공격력: 10
========================

Constructor

생성자는 클래스 이름의 함수를 작성 하여 만들 수 있습니다. 다음과 같이 말이죠.

Player(String userName, int healthPoint, int attackPoint) {
    setUserName(userName);
    setHealthPoint(healthPoint);
    setAttackPoint(attackPoint);
    printUserInfo();
}

하지만, 이처럼 생성자를 작성하게 하면, 아무런 파라미터를 받지 않는 new Player()에 에러가 발생 합니다. 기본 생성자가 사라졌기 때문이죠. 그러면, 이에 맞게 Main.java에 있는 new Player()를 수정 해 주면 됩니다.

Player.java

public class Player {
    private String userName;
    private int healthPoint;
    private int attackPoint;

    Player(String userName, int healthPoint, int attackPoint) {
        setUserName(userName);
        setHealthPoint(healthPoint);
        setAttackPoint(attackPoint);
        printUserInfo();
    }

    public String getUserName() {return this.userName;};
    public int getHealthPoint() {return this.healthPoint;};
    public int getAttackPoint() {return this.attackPoint;};

    public void setAttackPoint(int attackPoint) {
        if (attackPoint < 0)
            return;
        this.attackPoint = attackPoint;
    }

    public void setHealthPoint(int healthPoint) {
        if (healthPoint < 0)
            return;
        this.healthPoint = healthPoint;
    }

    public void setUserName(String userName) {
        if (userName.length() < 2)
            return;
        this.userName = userName;
    }

    public void printUserInfo() {
        System.out.println("========================");
        System.out.println("플레이어 이름: " + this.userName);
        System.out.println("체력: " + this.healthPoint);
        System.out.println("공격력: " + this.attackPoint);
        System.out.println("========================");
    }
}

Main.java

public class Main {
    static public void main(String[] args) {
        Player player = new Player("JustKode", 100, 10);
    }
}
========================
플레이어 이름: JustKode
체력: 100
공격력: 10
========================

Copy Constructor

JavaClass type의 변수는 사실 **C++**의 포인터와 큰 차이가 없습니다. 그리하여 우리는 C++ 처럼, 대입을 이용한 복사를 수행 할 수는 없지만, new Class(Class c)를 이용 하여, 복사 생성자의 역할을 할 수 있도록 할 수 있습니다. 다음과 같이 말이죠.

Player(Player p) {
    this(p.userName, p.healthPoint, p.attackPoint);
}

우리는 this를 이용하여, 생성자를 재사용 할 수 있습니다. 재사용은 적절한 방향으로 많이 사용해야, 여러분의 손목을 보호할 수 있습니다.

Player.java

public class Player {
    private String userName;
    private int healthPoint;
    private int attackPoint;

    Player(String userName, int healthPoint, int attackPoint) {
        setUserName(userName);
        setHealthPoint(healthPoint);
        setAttackPoint(attackPoint);
        printUserInfo();
    }

    Player(Player p) {
        this(p.userName, p.healthPoint, p.attackPoint);
    }

    public String getUserName() {return this.userName;};
    public int getHealthPoint() {return this.healthPoint;};
    public int getAttackPoint() {return this.attackPoint;};

    public void setAttackPoint(int attackPoint) {
        if (attackPoint < 0)
            return;
        this.attackPoint = attackPoint;
    }

    public void setHealthPoint(int healthPoint) {
        if (healthPoint < 0)
            return;
        this.healthPoint = healthPoint;
    }

    public void setUserName(String userName) {
        if (userName.length() < 2)
            return;
        this.userName = userName;
    }

    public void printUserInfo() {
        System.out.println("========================");
        System.out.println("플레이어 이름: " + this.userName);
        System.out.println("체력: " + this.healthPoint);
        System.out.println("공격력: " + this.attackPoint);
        System.out.println("========================");
    }
}

Main.java

public class Main {
    static public void main(String[] args) {
        Player player1 = new Player("JustKode", 100, 10);
        Player player2 = new Player(player1);
    }
}
========================
플레이어 이름: JustKode
체력: 100
공격력: 10
========================
========================
플레이어 이름: JustKode
체력: 100
공격력: 10
========================

제어자 (modifier)

다음은 **제어자 (modifier)**에 대해서 알아 보는 시간을 가져 보도록 하겠습니다. 제어자는 클래스, 변수 또는 메서드의 선언부에 함께 사용하여, 부가적인 의미를 부여합니다.

Static

static 제어자를 이용하여, 클래스 내부에 Static 변수, Static 함수를 만들 수 있습니다. Static 변수는, 객체 메모리가 아닌, 클래스 메모리에 있는 변수 라고 생각 하면 됩니다. 다음과 같은 예시에 주로 사용 됩니다.

  • 생성된 인스턴스 갯수 확인
  • Const Variable 설정 (메모리 절약 가능)
  • System Class 처럼, 유틸 함수를 제공하는 Class를 만들고자 할 때.

우리는 이를 이용하여, 현재 생성된 객체가 몇 개 인지 확인하는 로직을 만들 수 있습니다. 일단 클래스 내에 static 이라는 제어자를 이용하여 Static 변수를 초기화 한 후, 이를 static 제어자를 이용하여 만든 Static Function으로 확인 할 수 있습니다.

Player.java

public class Player {
    private String userName;
    private int healthPoint;
    private int attackPoint;
    static int objectNum = 0;

    Player(String userName, int healthPoint, int attackPoint) {
        setUserName(userName);
        setHealthPoint(healthPoint);
        setAttackPoint(attackPoint);
        printUserInfo();
        objectNum++;
    }

    Player(Player p) {
        this(p.userName, p.healthPoint, p.attackPoint);
    }

    public String getUserName() {return this.userName;};
    public int getHealthPoint() {return this.healthPoint;};
    public int getAttackPoint() {return this.attackPoint;};

    public void setAttackPoint(int attackPoint) {
        if (attackPoint < 0)
            return;
        this.attackPoint = attackPoint;
    }

    public void setHealthPoint(int healthPoint) {
        if (healthPoint < 0)
            return;
        this.healthPoint = healthPoint;
    }

    public void setUserName(String userName) {
        if (userName.length() < 2)
            return;
        this.userName = userName;
    }

    public void printUserInfo() {
        System.out.println("========================");
        System.out.println("플레이어 이름: " + this.userName);
        System.out.println("체력: " + this.healthPoint);
        System.out.println("공격력: " + this.attackPoint);
        System.out.println("========================");
    }

    public static void printCurrentObjectNum() {
        System.out.println("현재 생성된 객체의 갯수는 " + objectNum +"개 입니다.");
    }
}

Main.java

public class Main {
    static public void main(String[] args) {
        Player player1 = new Player("JustKode", 100, 10);
        Player player2 = new Player("JustCode", 100, 20);
        Player player3 = new Player("JustKoode", 100, 30);
        Player.printCurrentObjectNum();
    }
}
========================
플레이어 이름: JustKode
체력: 100
공격력: 10
========================
========================
플레이어 이름: JustCode
체력: 100
공격력: 20
========================
========================
플레이어 이름: JustKoode
체력: 100
공격력: 30
========================
현재 생성된 객체의 갯수는 3개 입니다.

final

final 제어자는 변경 될 수 없다는 의미를 가지고 있습니다. 클래스, 메서드, 멤버변수, 지역변수에 적용 할 수 있습니다.

  • 클래스: 변경 될 수 없는, 확장 할 수 없는 클래스가 되며, 조상 클래스가 될 수 없는 상태가 됩니다.
  • 메서드: 변경될 수 없는 메서드가 되며, 이는 파생 클래스에서 오버라이딩 할 수 없는 상태가 됩니다.
  • 멤버변수, 지역변수: 변수 앞에 final이 붙으면, 값을 변경할 수 없는 상수가 됩니다.
final class FinalClass {
    final int FINAL_VARIABLE = 10;

    final int getMaxSize() {
        final int FV = FINAL_VARIABLE;
        return FV;
    }
}

abstract

abstract 제어자는 미완성의 의미를 가지고 있습니다. 메서드의 선언부만 작성하고 실제 수행 내용은 구현 하지 않는 추상 메서드를 선언 하는데 사용 합니다. 클래스에 사용 하는 경우, 해당 클래스가 추상 클래스라는 것을 의미 하게 됩니다. 추상 클래스는 인스턴스를 생성할 수 없으며, 이를 사용 하기 위해선, 추상 클래스파생 클래스에서 추상 메서드를 오버라이딩 해야 합니다.

abstract class AbstractClass {
    abstract void func();
}

접근 제어자 (access modifier)

접근 제어자는 멤버 또는 클래스에 사용되어, 해당하는 멤버 또는 클래스를 외부에서 접근하지 못하도록 제한하는 역할을 합니다.

  • private: 같은 클래스 내에서만 접근이 가능 합니다.
  • protected: 같은 패키지 내에서, 혹은 해당 클래스를 상속 받은 다른 패키지의 자손 클래스에서 접근이 가능 합니다.
  • default (제어자 입력 하지 않은 경우): 같은 패키지 내에서만 접근이 가능 합니다.
  • public: 접근 제한이 없다.

초기화 블럭(Initalization Block)

초기화 블럭은 클래스 변수(ex: static 으로 제어된 변수)를 초기화 하는 클래스 초기화 블럭과 객체의 변수를 초기화 하는 인스턴스 초기화 블럭으로 나누어 집니다. 이는 생성자보다 먼저 호출 됩니다. 클래스 초기화 블럭은 클래스가 메모리에 처음 로딩 될 때, 한 번만 실행 됩니다. 다음과 같이 클래스 내에 작성 하여 사용 합니다.

클래스 초기화 블럭static 제어자와 중괄호를 이용하여 작성하고, 인스턴스 초기화 블럭은 중괄호 만을 이용 하여 작성 합니다.

...
private String userName;
private int healthPoint;
private int attackPoint;
private final int USER_ID;
static int objectNum;

static {
    objectNum = 0;
}

{
    userId = objectNum++;
}
...

이를 이용하여, 아까 만들었던 Player 객체에 userId 로직을 다음과 같이 만들 수 있습니다.

Player.java

public class Player {
    private String userName;
    private int healthPoint;
    private int attackPoint;
    private final int USER_ID;
    static int objectNum;

    static {
        objectNum = 0;
    }

    {
        USER_ID = objectNum++;
    }

    Player(String userName, int healthPoint, int attackPoint) {
        setUserName(userName);
        setHealthPoint(healthPoint);
        setAttackPoint(attackPoint);
        printUserInfo();
    }

    Player(Player p) {
        this(p.userName, p.healthPoint, p.attackPoint);
    }

    public String getUserName() {return this.userName;};
    public int getHealthPoint() {return this.healthPoint;};
    public int getAttackPoint() {return this.attackPoint;};

    public void setAttackPoint(int attackPoint) {
        if (attackPoint < 0)
            return;
        this.attackPoint = attackPoint;
    }

    public void setHealthPoint(int healthPoint) {
        if (healthPoint < 0)
            return;
        this.healthPoint = healthPoint;
    }

    public void setUserName(String userName) {
        if (userName.length() < 2)
            return;
        this.userName = userName;
    }

    public void printUserInfo() {
        System.out.println("========================");
        System.out.println("플레이어 이름: " + this.userName);
        System.out.println("체력: " + this.healthPoint);
        System.out.println("공격력: " + this.attackPoint);
        System.out.println("유저 아이디: " + this.USER_ID);
        System.out.println("========================");
    }

    public static void printCurrentObjectNum() {
        System.out.println("현재 생성된 객체의 갯수는 " + objectNum + "개 입니다.");
    }
}

Main.java

public class Main {
    static public void main(String[] args) {
        Player player1 = new Player("JustKode", 100, 10);
        Player player2 = new Player("JustCode", 100, 20);
        Player player3 = new Player("JustKoode", 100, 30);
        Player.printCurrentObjectNum();
    }
}
========================
플레이어 이름: JustKode
체력: 100
공격력: 10
유저 아이디: 0
========================
========================
플레이어 이름: JustCode
체력: 100
공격력: 20
유저 아이디: 1
========================
========================
플레이어 이름: JustKoode
체력: 100
공격력: 30
유저 아이디: 2
========================
현재 생성된 객체의 갯수는 3개 입니다.

마치며

다음 시간에는 Java 에서의 상속과 다형성 그리고 인터페이스의 개념에 대해서 배워 보겠습니다.