4 분 소요

Java


개요

이번 포스팅에서는 지난 포스팅에서 배웠던 클래스와 객체에 대해 되짚어보고, 객체 지향에 대해 더 자세히 알아보자.

cf) 이 글은 2023년 4월 11일에 진행됐던 sscc 자바 스터디 4주차 스터디 내용을 정리한 글입니다.


1. 클래스와 객체

클래스 == 설계도
객체 == 설계도로 만들어짐

클래스는 빵 틀, 객체는 빵 틀을 이용해 만들어진 빵과 같은 느낌이다.

객체가 가진 데이터 == 멤버 변수(필드)
객체가 할 수 있는 행동 == 멤버 함수(메소드)

또한 각 객체는 서로에게 영향을 주지 않는다.
같은 클래스의 객체를 여러 개 생성하면 각 객체마다 고유의 필드를 가지고 있기 때문이다.
즉, 각 객체는 독립적이다.

생성자

지난 번 스터디에서 생성자에 대하여 조금 설명을 했으나, 다시 한 번 복습해보자.

필드의 값을 내가 원하는 값으로 설정, 즉 초기화를 하기 위한 가장 효율적인 수단은 생성자를 사용하는 것이다.

기본 생성자

생성자가 없을 땐 Java가 자동으로 default 생성자를 만들어준다.
디폴트 생성자는 매개변수로 아무것도 받지 않고, 메소드 내부에는 아무 문장도 정의되어 있지 않다.

디폴트 생성자가 자동으로 생성되기 때문에 프로그래머가 명시적으로 생성자를 구현하지 않았더라도 우리는 컴파일 오류 없이 new연산자를 통해 객체를 생성할 수 있는 것이다.

여기서 주의해야할 점은 생성자가 하나도 선언되지 않은 경우에만 디폴트 생성자가 자동으로 삽입된다는 것이다.
프로그래머가 클래스에 생성자를 하나라도 작성한 경우 디폴트 생성자는 자동 삽입되지 않는다는 의미이다.

예제를 보며 그 의미를 이해해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Circle {
	int radius;
    
    public Circle(int r) {
    	radius = r;
    }
}

public class Example {
	public static void main(Strring [] args){
    	Circle pizza = new Circle(10);	//1.
        Circle donut = new Circle();	//2. 컴파일 에러
    }
}

위 코드에서 public Circle(int r) {…} 은 생성자이다. 이 생성자가 없었더라면 java에서 자동으로 디폴트 생성자를 삽입해주니 오히려 1번 문장이 컴파일 에러가 나고, 2번 문장은 정상 실행될 것이다.

this 키워드

코딩을 조금 해본 사람이라면 매번 변수 이름을 생각하는 과정은 매우 귀찮고 어렵다.

특히 클래스를 만들 때 생성자는 보통 매개변수를 필드 변수에 대입하여 초기화를 할 때 사용하므로, 필드에 존재하는 변수들의 이름과 생성자 매개변수의 역할이 동일한 경우가 많다.

그렇다고 매개변수명을 필드에 있는 변수명과 동일하게 지어버리면 생성자 내부에서 필드에 있는 변수에 접근할 수가 없게 되어버리므로 이름을 다르게 지어야 하는데, 이 과정이 참 골치아프다.

이럴 때 사용 가능한 것이 바로 this 키워드다.
this는 객체 자기 자신을 가리키는 포인터라고 생각하면 된다.
사용법은 this.변수이름 이런 식으로 사용한다.
this를 사용함으로써 우리는 생성자 내부에서도 변수명이 같은 객체의 멤버 변수에 접근할 수 있게 되는 것이다.


2. 객체지향을 사용하는 이유

우리는 지금까지 객체를 만들고 사용하는 기본적인 방법을 배웠다.
근데 이거 왜 배울까? 왜 객체지향을 사용해야 하는 걸까?

1. 동일 기능을 하는 것 여러개 만들기

객체지향의 개념이 없다면 일일이 변수를 선언하고 이름짓고 직접 관리해야 한다.
하지만 객체로 묶으면 관련이 있는 정보끼리 모아서 체계적으로 관리할 수 있다.

또한 객체들은 같은 멤버 함수를 공유하므로 클래스만 봐도 그 객체가 어떤 역할을 하고, 기능을 가지는지 쉽게 파악이 가능하다.

만약 내가 프로그램을 막 짜다가 값이 이상하다든지 어딘가 오류가 발생해서 디버깅을 하는데, 객체가 오류의 원인인 것 같다고 하면 나는 이 객체의 명세서 역할을 하는 클래스를 찾아가서 클래스를 분석해보면 된다.

이를 통해 얻을 수 있는 장점은 다음과 같다.

  • 단순히 프로그래밍이 쉬워지는 것을 넘어 유지보수에 매우 유리해진다.

  • 또한 현실 세계와 매우 닮아있다. 현실 세계에서 우리들은 인간(클래스)이며, 숨을 쉰다거나 밥을 먹어야 생체활동이 가능하다는 등 동일한 특성(메소드)을 공유하고 있지만, 사람(객체)마다 키, 몸무게, 외모, 지식 등의 고유한 특성을 가지고 있다.
    현실 세계에 존재하는 명사들을 객체로 표현하는 것이 가능해지고, 이는 프로그램을 더욱 유연하고 우리 삶에 가깝게 만들어주는 기반이 된다.

  • 같은 클래스로 만들어진 객체들은 정확히 같은 행동을 보장한다.

2. 다형성 구현 가능

아직 배우진 않았지만 상속, 오버라이딩, 인터페이스 등을 이용해서 1개의 이름의 함수나 클래스가 상황에 따라 다른 일을 하게 만들 수 있다.

ex)
cat.speak() //야옹 출력
dog.speak() //멍멍 출력

3. 캡슐화 (데이터 은닉)

데이터(필드)와 그 데이터를 처리하는 로직(메소드)를 클래스로 묶는다.

설계 방법

    1. 데이터를 클래스 밖에서 바꿀 수 없게 한다.
    1. 데이터를 바꿀 수 있는 로직은 오직 클래스 내에만 존재한다.
      • 클래스 내부의 함수가 아니면 값을 바꿀 수 없다. (데이터 보호)
      • 개발자가 예상하지 못한 곳에서 값이 바뀔 가능성을 최소화한다.

접근 지정자

이 캡슐화를 구현하도록 도와주는 녀석들이 바로 접근 지정자이다.

접근 지정자는 4가지가 있다.

  • private : 해당 객체 내에서만 접근 가능
  • protect : 해당 객체와 상속받은 객체에서 접근 가능
  • (default): 아무것도 안 쓰면 디폴트. 같은 패키지 내에서 접근 가능
  • public : 어디서든 접근 가능 (객체 외부에서도 꺼내쓸 수 있음)

protected와 default는 아직 상속과 패키지를 안 배웠으니 자세히는 몰라도 된다.

private으로 선언된 멤버 변수는 객체 밖에서는 접근 자체가 안 되니 객체 밖에서 바꿀 수 없다.
-> 반드시 내가 정의한 메소드를 이용해야만 값이 바뀐다.
-> 객체의 행동이 제한된다.
-> 유지보수가 용이해진다.

getter, setter

위에서 private 변수는 외부에서 접근이 안 된다고 했다.
그럼 여기서 한 가지 의문점이 생긴다.
나중에 이 변수 값을 이용해야 한다거나 변경이 필요해진다면?

이와 같은 상황을 방지하고자 대부분의 클래스는 프로그래머가 getter와 setter 메소드를 정의해놓는다.

getter와 setter는 프로그래머들의 암묵적인 룰이지만, 너무도 많이 사용해서 intelliJ에서는 아예 getter setter를 정의하려고 하면 추천 코드 항목에 뜰 정도이다.

getter

1
2
3
4
5
6
7
Class Pokemon {
	private String type;
    private int age;
    
    public int getAge() {
    	return age;
    }

위 코드에서 age는 private 변수이므로 외부에서 접근할 수 없다. 하지만 getAge() 메소드는 public으로 선언되어 있으므로 언제든지 접근할 수 있다. 이를 통해 프로그래머는 read only로 age 변수의 값을 얻어 활용할 수 있다.

이 getAge() 메소드를 getter라고 부른다.

setter

1
2
3
4
5
6
7
Class Pokemon {
	private String type;
    private int age;
    
    public void setType(String str) {
    	this.type = str;
    }

위 코드에서 type은 private 변수이므로 외부에서 변경이 불가능하지만, setter를 명시적으로 사용함으로써 값을 프로그래머가 의도한 곳에서 안전하게 변경할 수 있다.

하지만 꼭 필요한 곳이 아니라면 setter 정의는 자제하는 것이 좋다.
결국 setter도 개발자 마음대로 멤버 변수의 값을 바꿀 수 있기 때문에 private으로 선언한 의미가 없어진다.

setter 대신, 프로그램 시나리오에 맞는 함수들을 만들어 값 변경에 사용하는 것이 좋다.

예를 들어, 앞서 살펴본 Pokemon 클래스에 포켓몬이 나이를 먹는 기능이 추가되었다고 생각해보면 setter를 정의하는 것이 아니라 나이가 1살만 늘어날 수 있는 plusAge()를 정의하여 값을 변경하는 것이 좋다.


오늘은 이렇게 객체지향에 대한 조금 더 깊은 내용을 다뤄보았다.

조금은 어색하더라도 객체지향 언어인 java를 배우고 사용하기로 마음 먹었다면 이 객체지향적인 프로그래밍을 하기 위해 고민하고 노력해보자.

익숙하면 이런 방식으로 코드를 짜는 것이 오히려 좋고 편하다.

댓글남기기