[Java 기본 문법] 5. 객체와 클래스
2021.08.14 - [Java] - [자바 기본 문법] 4. 참조타입
[자바 기본 문법] 4. 참조타입
2021.08.13 - [Java] - [Java 기본 문법] 1. 변수와 타입 2021.08.13 - [Java] - [Java 기본문법] 2. 연산자 2021.08.13 - [Java] - [Java 기본 문법] 3. 제어문 1번 포스팅에서 자바의 타입에는 기본타입과 참조..
blog.robinjoon.space
본격적으로 객체지향 프로그래밍에 대해 알아보고, 자바에서 이를 구현한 문법과 구조를 알아볼 것이다.
객체지향 프로그래밍(OOP)이란?
객체지향 프로그램이란, 프로그램을 구성하는 부품인 객체를 먼저 만들고, 이를 조합해 원하는 동작을 하는 프로그램을 작성하는 프로그래밍 기법이다. 여기서 객체란, 자신의 속성을 가지고, 행동을 가지며, 다른것과 식별 가능한 것을 말한다. 예를 들어보자. 하나의 웹사이트를 만드는 것을 상상해보자. 이 때, 우리가 만들고자 하는 웹 사이트는 다양한 내부데이터와, 사용자에게 제공할 기능을 가지고있고, 다른것들과 구별되므로 하나의 객체다. 이 웹사이트에는 회원가입기능과, 게시글 작성기능, 댓글기능등 여러 기능을 제공한다. 각 기능들은 각 기능을 수행하기 위한 좀 더 단순한 동작들과, 속성들을 가지고있을 것이다. 예를들어, 게시글 작성기능에는 로그인된 회원인지 확인하는 기능과, 게시판에 글을 쓸 권한이 있는지 확인하는 기능, 사용자가 작성한 글을 서버의 데이터베이스에 저장하는 기능등의 더 세부적인 기능들로 구성된다.
이처럼, 시스템 전체를 하나의 객체로 보고, 그 객체를 구현하기 위해 필요한 객체를 찾고 구현하는것을 반복해 하나의 시스템을 만드는 것을 객체지향 프로그래밍(OOP)라고 할 수 있다.
객체의 구성요소
객체는 행동과 상태로 구성된다. 자바에선 이를 매서드와 필드라고 부른다. 객체는 다른 객체와 상호작용하기 위해 매서드를 이용한다. 필드는 객체의 속성을 의미한다. 사람의 키와 몸무게같은것들이 객체의 필드이다.
객체지향 프로그래밍의 특징
객체지향 프로그래밍에는 여러 특징들이 있다. 이 특징들은 자바를 공부하다보면 자연히 알게 된다. 여기선 간단하게 알아보자.
캡슐화
캡슐화란, 객체 내부의 상태를 외부로부터 감추는 것을 의미한다.
자동차를 운전하는 상황을 생각해보자. 우리는 자동차 외부에 공개된 핸들과, 패달을 이용해 운전한다.
우리는 자동차 내부에서 무슨일이 일어나는지 알지 못한다. 우리는 자동차를 이용해 원하는 장소로 이동할 수 있으면 그만이다. 객체지향 프로그래밍에서도 마찬가지다. 객체는 다른 객체가 외부에 제공하는 매서드만을 이용해 상호작용한다. 다른 객체의 내부 상태가 어떤지 전혀 알지 못한다. 이는, 객체의 안정성을 보장하는 것이다. 만일, 자동차 엔진이 외부에 노출되어있다고 상상해보자. 누군가 그걸 잘못 건들면 자동차가 망가질 것이다. 소프트웨어의 객체도 마찬가지다. 내부의 상태를 외부에 노출하지 않음으로서 객체를 보호하는 것이다.
다형성
다형성이란 대체 가능성을 의미한다.
자동차를 운전하는 상황을 예시로 들자. 한 모델의 자동차를 운전할 줄 알면 다른 자동차도 운전할 수 있다. 이는 여러 모델의 자동차가 사람의 입장에서 대체 가능함을 의미한다. 구체적으로, 자동차가 사람이 원하는 목적지로 갈 수 있게만 한다면 어떤 자동차인지 상관없다는 것이다. 자바는 하나의 타입에 여러 객체들을 대입함으로서 다형성을 지원한다. 인터페이스 타입에는 그 인터페이스를 구현한 객체들을 모두 대입할 수 있고, 부모타입에는 모든 자식타입의 객체를 대입할 수 있다. 이로인해, 객체는 부품화가 가능해지고, 재사용성이 높아지게 된다.
객체와 클래스
클래스의 구조
하늘에서 자동차가 둑 하고 떨어지지 않는다. 자바에서도 마찬가가지다. 객체가 저절로 생기지 않는다.
객체를 만들기 위해선 그 객체의 설계를 코드로 옮긴 클래스가 필요하다. 클래스에는 그 클래스로 만들어질 객체의 필드와 매서드가 정의되어있다. 클래스의 구조는 다음과 같다.
(접근 제한자) class 클래스이름{
(접근제한자) 타입 필드이름;
...
(접근제한자) 반환타입 매서드이름(파라미터타입 이름){
실행문;
}
}
예시 코드를 보자
public class Car{ // public class 는 반드시 소문자로 작성해야 하고, 중괄호는 생략할 수 없다.
// 클래스 선언시 public class라 했다면 코드파일의 이름은 클래스이름.java 여야한다.
private Tire[] tires; // private, public 등은 접근제한자라는건데, 자바에서 캡슐화를 위해 제공하는 것들이다.
private Engine engine;
String name;
public boolean strtEngine(Key key){
...
}
public boolean stopEngine(){
...
}
public Car(Tire[] tires, Engine engine){
this.tires = tires;
this.engine = engine;
}
...
}
이처럼 클래스에는 그 클래스로 만들어질 객체의 필드와 매서드를 정의한다. 클래스의 이름을 정할 때 지켜야 할 규칙은 이렇다.
- 하나 이상의 문자로 이루어져야 한다.
- 첫 번째 글자는 숫자가 될 수 없다.
- '$', '_' 이외의 특수문자는 사용할 수 없다.
- 자바 키워드는 사용할 수 없다.
그리고, 관습적으로 클래스의 이름은 항상 대문자로 쓴다. 여러 단어를 혼합하여 클래스이름을 정했다면, 각 단어의 첫 글자는 대문자로 쓴다. 자바 명세에는 클래스이름이 유니코드 문자면 모두 가능하지만, 실제 자바 환경을 어떻게 구축하였는지에 따라 문자인코딩에 문제가 있을 수 있으니 클래스 이름은 영어로 쓰자.
클래스에서 필드를 정의하는 방법은 기본적으로 변수를 선언하는 것과 같다. 다만, 타입 앞에 접근제한자를 추가로 쓸 수 있다. 접근제한자의 종류와 특성은 다음 포스팅에서 다루고 여기선, 일반적으로 필드는 private 접근제한자를 사용한다고 알고 넘어가면 된다. 아래는 예시 코드다.
public class Car{
private Tire[] tires; // private, public 등은 접근제한자라는건데, 자바에서 캡슐화를 위해 제공하는 것들이다.
private Engine engine;
String name;
...
}
클래스에서 매서드를 정의하는 방법은 아래와 같다. (접근제한자를 제외하면, C언어의 함수와 형태가 같다)
(접근제한자) 반환타입 매서드이름(파라미터타입 파라미터이름){
실행문
return (반환타입의변수나 리터럴)
}
매서드에 붙일 수 있는 접근제한자는 public, protected, default, private이 있다. 이들에 대한 설명은 나중에 하고, 우선은 public을 붙여서 작성하자.
반환타입은 매서드의 결과를 어떤 타입으로 돌려줄것인지를 의미한다. 만일, 굳이 응답할 결과가 없다면 void라 쓴다.
매서드이름은 변수명 작성규칙대로 작성하면 된다.
파라미터는, 매서드 호출시 매서드 동작에 필요한 데이터를 제공하기 위해 사용한다. 만일, 파라미터가 필요없다면 공백으로 비워두면 된다.
매서드 블록 내부에 return을 만나면 매서드의 실행은 중단되고 return 뒤에있는 것을 매서드를 호출한 객체에 반환해준다. 만일 반환타입이 void인 경우, 그냥 return; 이라 쓰면 매서드가 종료된다.
매서드는 그 파라미터의 타입과 개수가 다르다면 같은 이름의 매서드를 여러개 정의 할 수 있다. 이를 매서드 오버로딩이라 한다.
아래는 예시 코드이다.
public int sum(int[] array){
int sum = 0;
for(int i=0;i<array.length;i++){
sum+=array[i];
}
return sum;
}
public void setName(String name){
this.name = name; // this.name 은 자기자신의 name 필드를 의미한다.
return; // 반환타입이 void인 경우 return;은 생략가능하다.
}
자바에선 객체를 생성하기 위한 특수한 매서드인 생성자라는것을 제공한다. 생성자의 선언방법은 이렇다.
(접근제한자) 클래스이름(파라미터타입 파라미터이름){
실행문 // 주로 다른 생성자를 호출하거나 자신의 필드를 초기화하는 코드다.
}
구체적인 코드는 아래와같다.
public class Car{
private Tire[] tires;
private Engine engine;
String name;
...
public Car(Tire[] tires, Engine engine, String name){
this.tires = tires;
this.engine = engine;
this.name = name;
}
}
생성자는 말 그대로 객체를 생성하는 것이기에, 객체의 필드의 초기화를 진행한다. 여기서 this.필드이름 은, 이 생성자가 생성할 객체의 필드를 지칭한다. (구체적으로, this 는 객체 자기자신을 뜻하는 키워드다.)
만일, 클래스 작성 시 별도로 생성자를 작성하지 않았다면 컴파일러가 기본생성자를 자동으로 추가해준다. 기본생성자는 아래 코드다.
public 클래스이름(){
}
만일, 클래스 작성 시 생성자를 하나라도 추가하였다면, 기본생성자는 추가되지 않는다. 자신이 만든 생성자와 기본 생성자 모두 사용하고 싶다면, 둘다 직접 코드에 작성하면 된다. 즉, 생성자는 여러개 있을 수 있다(생성자 오버로딩).
단, 생성자의 파라미터의 개수나 타입의 순서가 달라야 한다. 아래는 Car 클래스의 여러 생성자 코드다.
public Car(Tire[] tires, Engine engine){
this.tires = tires;
this.engine = engine;
}
public Car(String name){
this.name = name;
}
public Car(Tire[] tires, Engine engine, String name){
this(tires, engine);
// this() 는 클래스에 정의된 다른 생성자를 의미한다. 코드의 중복을 제거하기 위해 사용한다.
// this() 는 생성자 코드의 맨 위에만 사용할 수 있다.
/* 필드 초기화는 하나의 생성자에만 작성하고,
다른 생성자에선 이를 호출하는 방식으로 사용할 수 있다.*/
this.name = name;
}
객체 생성과 객체의 필드와 매서드 사용
자바에서 객체를 생성하는 유일한 방법은 생성자를 사용하는 것이다. 사용방법은 다음과 같다.
클래스이름 변수 = new 생성자;
위의 Car 클래스를 예시로 들면 이렇다.
Car mycar = new Car(tires, engine, name); // 매개변수의 선언코드는 생략했다.
객체의 필드를 사용하기 위해서는 "객체이름.필드이름" 이렇게 하면 된다.
String name = "람보르기니";
Car mycar = new Car(tires, engine, name);
System.out.println(mycar.name); // 람보르기니 출력
mycar.name = "BMW";
System.out.println(mycar.name); // BMW 출력
객체의 매서드호출은 "객체이름.매서드" 이렇게 하면 된다. (객체의 매서드를 사용하는 것을 매서드를 호출한다고 표현한다)
Car mycar = new Car(tires, engine, name);
boolean isStart = mycar.strtEngine(new Key());
boolean isStop = mycar.stopEngine();
객체의 어떤 매서드 내부에서 자신의 필드에 접근하거나 자신의 매서드를 호출할 때는 객체이름을 생략하고 사용할 수 있다.
public class Test{
public void method1(){
...
method2();
}
public void method2(){
}
}
객체는 필드와 매서드를 가진다고 했다. 반대로 말하면 매서드와 필드는 객체의 일부기 때문에 객체가 없으면 사용할 수 없다. 따라서 일반적인 필드와 매서드는 객체가 있어야만 사용할 수 있다.
클래스의 또다른 역할
자바 기본문법 시리즈에서 언급하진 않았지만, 일반적인 Java EE 환경에선 main() 매서드를 작성하고 그 안에 여태 언급한 코드들을 작성해 실행해야 한다.
public class Main{
public static void main(String[] args){
Tire[] tires = new Tire[4];
Engine engine = new Engine();
String name = "BMW";
Key key = new Key();
Car car = new Car(tires, engine, name);
car.startEngine(key);
}
}
이 Main 클래스는 객체의 설계도로 존재하는게 아니라, 프로그램의 실행을 위한 방아쇠의 역할을 한다.
정적 필드와 매서드
지금까지 언급한 필드와 매서드는 모두 인스턴스 필드와 인스턴스 매서드다. 이제 정적필드와 정적 매서드에 대해 알아보자.
정적 필드와 정적 매서드란?
위에서 언급하진 않았지만, 세상에는 수많은 자동차가 있는 것 처럼, 하나의 클래스로 여러개의 객체를 만들 수 있다. 같은 클래스로 만들어진 객체는 구조적으로 가지고있는 필드나 매서드는 같지만, 필드에 적힌 값은 서로 다르다. 그러나 프로그램을 작성하다보면, 한 클래스로 만들어진 객체가 모두 같은 값을 가지는 경우도 있을 수 있다. 예를 들어, 화면에 원을 그리기 위해 Circle 클래스를 작성했다고 하자. 이 클래스로 만들어진 원들은 각자 반지름과 색상등의 필드는 다르지만, 원주율은 모두 π 로 같다. 따라서, 이 원주율이라는 값을 모든 객체가 따로따로 가지고 있는 것은 비효율적이다.
또, 가끔은 굳이 객체로 만들어서 사용해야 할 필요가 있나 싶은 것들도 있다. 매서드의 결과가 오직 파라미터에만 영향을 받는다면 굳이 객체로 만들 필요가 없다. 이런 경우를 위해 자바는 정적필드와 정적 매서드를 제공한다. 이를 위해선 static 키워드를 사용한다. 아래는 정적 필드를 가지는 Circle 클래스이다.
public class Circle{
static float pi = 3.14f;
Color color;
float r;
...
}
아래는 정적 매서드를 가지는 Calculator 클래스이다.
public class Calculator {
static int add(int a, int b){
return a+b;
}
static int minus(int a, int b){
return a-b;
}
}
이런 정적 필드와 매서드를 사용하기 위해서는 클래스.필드, 클래스.매서드 로 사용한다.
System.out.println(Circle.pi);
System.out.println(Calculator.add(1,3));
정적필드와 매서드를 객체를 이용해 참조할 수도 있다. 그러나 이럴 경우 대부분의 IDE에서 경고를 표시한다.
정적 필드와 매서드를 사용할 때 주의해야 할 것이 있다. 정적 필드와 매서드는 객체없이 사용할 수 있다. 따라서, 객체가 있어야 존재하는 인스턴스 필드나 매서드는 정적 필드나 매서드에서 사용할 수 없다. 객체가 없으니 객체 자신을 의미하는 this 키워드도 사용할 수 없다.
정적 필드의 초기화
정적 필드는 객체생성전에 존재하기 때문에 생성자에서 초기화 할수 없다. 따라서 정적 필드는 선언과 동시에 초기화 하는것이 보통이다. 그러나 만일 별도의 계산이 필요한 경우 정적 초기화 블록을 사용하여 초기화를 한다.
static {
정적 필드의 초기화
}
정적 초기화 블록에서도 마찬가지로 인스턴스 필드와 매서드, this 키워드를 사용할 수 없다.
final 필드와 상수
final 필드
객체를 설계하다보면, 어떤 필드는 그 값이 변하면 안되는 경우가 있다. 위에서 예를 든 Circle 클래스의 pi 는 원주율을 의미하고, 원주율은 절대불변의 값 이므로, 이것이 변경이 가능하면 추 후 문제가 생길 수 있다. 이런 상황을 위해 자바에선 final 키워드를 제공한다. final 키워드를 붙인 필드는 선언과 동시에 초기값을 지정해주어야 하며, 절대 변경할 수 없다.
public class Circle{
final float pi = 3.14f;
Color color;
float r;
...
public void metod(){
this.pi = 1.1f; // 컴파일 에러
}
}
상수
상수란 절대불변인 값을 의미한다. 수학의 원주율 같은것이 그것이다. 이런 불변의 값을 저장하는 필드를 자바에선 상수라 한다. 상수를 저장하기 위해서는 final 키워드만으로는 부족하다. 상수란 불변하기에 모든 객체에서 따로 가지고 있을 필요가 없다. 따라서 정적인 동시에 불변이므로 static final 이다. 즉, 위의 Circle 클래스는 이렇게 고처야 한다.
public class Circle{
static final float pi = 3.14f;
Color color;
float r;
...
public void metod(){
this.pi = 1.1f; // 컴파일 에러
}
}
패키지
패키지는 클래스를 체계적으로 관리하기 위한 것이다. 수십 수백개의 클래스가 체계적으로 관리되지 않는다면 결국 프로그램은 더이상 유지보수할 수 없게 된다. 패키지는 다른 패키지를 포함할 수 있다. 패키지와 클래스의 구조를 쉽게 윈도우의 폴더와 파일 관계로 생각하면 편하다. 파일이름이 같아도 폴더가 다르면 다른 파일로 인식하는 것처럼, 클래스이름이 같아도 패키지가 다르면 다른 클래스이다.
패키지를 만드는 법은 간단하다. 코드 제일 위에 package 키워드를 사용하면 된다.
package 상위패키지.하위패키지;
패키지 이름을 정하는것도 규칙이 있다.
- 숫자로 시작하면 안되고, _, $를 제외한 특수문자는 사용할 수 없다.
- java로 시작하는 패키지는 자바 표준 API에서만 사용하므로 사용해서는 안된다.
- 모두 소문자로 작성하는것이 관례다.
같은 패키지에 속하는 클래스는 아무 제약없이 서로에서 서로를 사용할 수 있다. 그러나, 다른 패키지의 클래스를 사용하기 위해서는 import 키워드를 이용해야 한다. 아래는 b 패키지에 속한 Test 클래스를 사용하기 위해 import하는 코드다.
package a;
import b.Test;
public class Main{
public static void main(String[] args){
Test test = new Test();
}
}
만일, 특정 패키지에 포함된 모든 클래스를 사용할 수 있도록 import 하고싶다면 클래스이름대신 *을 사용하면 된다.
package a;
import b.*;
public class Main{
public static void main(String[] args){
Test test = new Test();
Test2 test2 = new Test2();
}
}
다음 글