※ 본문은 코드스테이츠에서 제공해준 학습자료를 공부하고 정리한 내용임을 알립니다.
객체지향 프로그래밍 OOP(Object Oriented Programming)
객체지향 프로그래밍 방식은 Class를 정의하여 객체를 생성하여 사용하는 방식이라고 정리할 수도 있다. Class를 정의하는 방식을 통해 우리는 중복된 코드를 줄이고 코드의 재사용성을 높이면서 유지보수가 편리해지는 이점을 얻을 수 있다.
이 같은 객체지향의 효과를 더 높이기 위해 객체지향 3요소를 인지해야 한다.
객체지향(OOP) 3요소
- 캡슐화 : 값의 보호를 위해 객체의 필드, 메소드를 필요에 의해 감추거나 드러내는 것
- 다형성 : 동일한 타입을 가진 여러 객체가 같은 속성을 가지는 성질
- 상속성 : 특정 클래스를 부모 클래스로 지정하여 내용을 물려받아 기능을 확장하는 것
클래스, 객체, 인스턴스
이 세가지는 자바 프로그래밍을 진행하면서 가장 많이 듣게 될 것이다.
- 클래스 : 객체를 만들기 위한 설계도 혹은 툴
- 객체 : 구현할 대상, 클래스에 선언된 모양 그대로 생성된 실제
- 인스턴스 : 클래스를 바탕으로 실제로 만들어진 실체
public class SampleData{
//클래스 상태
}
SampleData data; //객체
data = new SampleData(); //인스턴스화
//data는 SampleData 클래스의 인스턴스 (객체를 변수에 할당)
Class
클래스 구조
package Member;
public class MemberData {
}
접근제한자 | class | 이름 {
//클래스 내용
}
package는 클래스의 파일 경로이다. 해당 클래스가 어떤 파일 구조 아래 있는지 명시적으로 작성해주는 것을 권고한다. 클래스를 만들 때는 class 키워드를 사용하여 위의 예시코드와 같이 작성한다. 클래스를 만들 때 대부분 새로운 .java 파일을 만들지만 한 파일 안에 여러 개의 클래스를 작성할 수도 있다.
클래스의 구성 멤버
클래스에는 객체가 가져야 하는 구성 멤버가 선언된다. 구성 멤버에는 필드, 생성자, 메소드가 있다. 이 구성 멤버들은 생략되기도 하고, 여러 개가 작성될 수도 있다.
public class SampleClass {
//필드
private String name;
//생성자
public SampleClass() {...}
//메소드
public void simpleMethod() {...}
}
필드
필드는 객체의 고유 데이터, 부품 객체, 상태 정보를 저장하는 곳이다. 선언 형태는 변수를 선언하는 것과 비슷하다. 그러나 이를 변수라고 부르지 않는다. 변수는 생성자와 메소드 내에서만 사용되고 생성자와 메소드가 실행 종료되면 자동으로 소멸된다. 하지만 필드는 생성자와 메소드 전체에서 사용되며 객체가 소멸되지 않는 한 객체와 함께 존재한다.
필드 선언
필드 선언은 중괄호 블록 어디서든 존재할 수 있다. 생성자 선언과 메소드 선언의 앞과 뒤, 어느 곳에서나 필드 선언이 가능하다.
필드 사용
필드를 사용한다는 것은 필드 값을 읽고, 변경하는 작업이다. 클래스 내부의 생성자나 메소드를 사용할 경우 단순히 필드 이름으로 읽고 변경하면 되지만, 클래스 외부에서 사용할 경우 우선적으로 클래스로부터 객체를 생성한 뒤 필드를 사용해야 한다.
생성자
생성자는 new 키워드로 호출되는 특별한 중괄호 {} 블록이다. 생성자는 객체 생성 시 초기화를 담당한다. 필드를 초기화하거나 메소드를 호출해서 객체를 사용할 준비를 한다. 생성자를 실행시키지 않고는 클래스로부터 인스턴스를 만들 수 없다.
생성자 선언
모든 클래스는 생성자가 반드시 존재하며, 하나 이상을 가질 수 있다.
접근제한자 | class() {}
중괄호 {} 블록 내용이 비어있는 생성자를 기본 생성자라고 한다. 만약 클래스 내부에 생성자 선언을 생략했다면 컴파일러는 기본 생성자를 자동으로 추가한다. 외부에서 받아올 변수가 없다면 기본 생성자만으로 충분하다. 하지만 받아올 파라미터가 있다면 그에 맞는 형식을 만들어줘야 한다. 파라미터를 입력할 시, 파라미터는 외부의 값을 생성자 블록 내부로 전달하는 역할을 한다. 그리고 매개 값을 생성자가 받기 위해서는 올바른 파라미터 타입과 함께 생성자를 선언해야 한다.
public class Car {
Car(String model, String color, int price) {
this.model = model;
this.color = color;
this.price = price;
}
}
public class ListOfCar {
Car myDreamCar ("BMW", "black", 200000000);
}
메소드
메소드는 객체의 동작에 해당하는 중괄호 {} 블록을 의미한다. 중괄호 블록은 이름을 갖는데, 이것이 메소드 명이다. 메소드를 호출하게 되면 중괄호 블록에 있는 모든 코드들이 일괄적으로 실행된다. 메소드는 필드를 읽고 수정하는 역할도 하지만 다른 객체를 생성해서 다양한 기능을 수행하기도 하고, 객체 간 데이터 전달의 수단이 되기도 한다. 따라서 외부로부터 파라미터를 받을 수도 있고 실행 후 어떤 값을 리턴할 수도 있다.
메소드 선언
접근제한자 | 리턴 타입 | 메소드 명(매개 변수) {
//실행 블록
}
리턴 타입
리턴 타입은 메소드 실행 후 리턴하는 값의 타입이다. 메소드는 리턴 값이 있을 수도 있고 없을 수도 있다. 메소드가 실행된 후 결과를 호출한 곳에 값을 넘겨주는 경우에는 리턴 값이 있어야 한다. 리턴 값이 없는 경우에는 void라는 리턴 타입을 쓴다. 그러나 void의 경우 리턴이라는 단어를 못 쓰는 것이 아니고 return; 과 같은 형태로 작성하는 것이 기본이니 생략하는 것이다.
메소드 호출
메소드는 클래스 내/외부의 호출에 의해서 실행된다. 클래스 내부의 다른 메소드에 의해 호출될 경우에는 단순히 메소드 이름으로 호출하면 되지만, 클래스 외부에서 호출할 경우에는 우선 클래스로부터 인스턴스를 생성한 뒤, 참조 변수를 이용해서 메소드를 호출해야 한다. 그 이유는 객체가 존재해야 메소드도 존재하기 때문이다.
Getter, Setter
일반적으로 객체지향 프로그래밍에서의 객체의 데이터는 객체 외부에서 직접적으로 접근하는 것을 막는다. 객체의 데이터를 외부에서 마음대로 읽고 변경할 경우 객체의 무결성이 깨어질 수 있기 때문이다. 따라서 데이터를 변경할 때는 메소드를 통해서 변경하는 방법을 선호한다. 이러한 역할을 하는 메소드가 Setter이다. 또한 외부에서 객체의 데이터를 읽을 때도 메소드를 사용한다. 이 역할은 Getter 메소드가 한다.
Getter와 Setter는 생성자 블록에서 필드 값을 다룰 때 사용하기도 한다. 생성자 파라미터에 직접적으로 매개 값을 넣지 않고 기본 생성자로 우선 객체를 만든 다음 Setter 메소드로 매개 값을 전달하여 데이터를 변경하는 방법을 사용하기도 한다.
instance
public class AClass {
public String subject() {
return "Computer Science";
}
}
public static void main() {
AClass a = new AClass(); //인스턴스화
}
자바에서 모든 클래스는 객체이다. 자바에서 객체는 자료형으로 취급된다. 그렇기에 클래스를 참조 자료형으로써 인스턴스화할 수 있다.
this
인스턴스 멤버란 객체를 생성한 후 사용할 수 있는 필드와 메소드를 말하는데, 이들을 각각 인스턴스 필드, 인스턴스 메소드라고 부른다. 인스턴스 필드와 메소드는 객체에 소속된 멤버이기 때문에 객체 없이는 사용이 불가능하다.
자바에서 this의 용도는 객체 외부에서 인스턴스 멤버에 접근하기 위해 참조 변수를 사용하는 것과 마찬가지로, 객체 내부에서도 인스턴스 멤버에 접근하기 위해 사용하는 것이다.
this는 또한 생성자와 메소드의 파라미터 이름이 필드와 같을 경우, 인스턴스 멤버의 필드임을 명시하기 위해 사용한다.
클래스 상속
클래스 상속의 개념
자식은 상속을 통해서 부모가 물려준 것을 자연스럽게 이용할 수 있다. 객체지향 프로그래밍에서도 부모 클래스의 멤버를 자식 클래스에게 물려줄 수 있다. 부모 클래스를 상위 클래스라고 부르기도 하고, 자식 클래스를 하위 클래스, 파생 클래스라고 부르기도 한다.
상속은 이미 잘 개발된 클래스를 재사용하여 새로운 클래스를 만들기 때문에 코드의 중복을 줄여주는 장점이 있다. 더불어서 클래스의 수정을 최소화할 수 있다. 부모 클래스의 수정은 모든 자식 클래스의 수정과 같기에 유지 보수 시간을 최소화시켜준다.
그러나 상속을 해도 부모 클래스의 모든 필드와 메소드를 물려받는 것은 아니다. 부모 클래스에서 private 접근 제한을 갖는 필드와 메소드는 상속 대상에서 제외된다. 부모 클래스와 자식 클래스가 다른 패키지에 존재한다면 당연히 default 접근 제한을 갖는 필드와 메소드도 상속 대상에서 제외된다.
클래스 상속을 하기 위해서는 extends 키워드를 사용한다.
부모 생성자 호출
자식 객체를 생성하면, 부모 객체가 먼저 생성되고 자식 객체가 그 다음에 생성된다.
public class SportsCar extends Car {
SprotsCar myDreamCar = new SprotsCar();
}
위 코드는 SportsCar 객체만 생성하는 것처럼 보이지만 사실 내부적으로 부모인 Car 객체가 먼저 생성되고 SportsCar 객체가 생성된다.
모든 클래스는 클래스의 생성자를 호출해야 생성된다. 이는 부모 객체도 마찬가지이다. 부모 생성자는 자식 생성자의 맨 첫 줄에서 호출된다. 자식 생성자가 명시적으로 선언되지 않았다면 컴파일러는 super 키워드를 이용하여 기본 생성자를 생성한다.
Override
부모 클래스에 작성된 모든 메소드가 자식 클래스에서도 그대로 사용할 수 있도록 설계되어 있다면 이상적이지만, 어떤 메소드는 자식 메소드에서 사용하기에 적합하지 않을 수 있다. 이 경우 상속된 일부 메소드를 수정해서 사용해야 한다. 이런 경우 메소드를 재정의해서 사용할 수 있도록 Override 기능을 제공한다.
다시 정리하면, 상속된 메소드의 내용이 자식 클래스에 맞지 않을 경우, 동일한 메소드를 재정의하는 것을 오버라이딩한다고 한다. 메소드가 오버라이딩되었다면 자식 객체에서 메소드 호출 시, 오버라이딩된 자식 메소드가 호출된다.
메소드를 오버라이딩할 때는 아래 규칙에 주의해서 작성해야 한다.
- 부모의 메소드와 동일한 리턴 타입, 메소드 이름, 매개변수 리스트를 가져야 한다.
- 접근 제한을 더 강하게 오버라이딩할 수 없다. (접근 제한자를 수정할 수 없다.)
- 새로운 Exception을 throws할 수 없다.
//Calculator.java
public class Calculator {
public double areaCircle(double r){
System.out.println("Calculator 객체의 원주율 계산하기");
return 3.14 * r * r;
}
}
//Computing.java
public class Computing extends Calculator{
@Override
public double areaCircle(double r){
System.out.println("Computing 객체의 원의 넓이 구하기");
return 3.141592 * r * r;
}
}
//Test.java
public class Test {
public static void main(String[] args) throws Exception {
int r = 10;
Calculator cal = new Calculator();
System.out.println("원의 넓이 " + cal.areaCircle(r));
Computing comp = new Computing();
System.out.println("원의 넓이 " + comp.areaCircle(r));
}
}
/*
Calculator 객체의 원주율 계산하기
원의 넓이 314.0
Computing 객체의 원의 넓이 구하기
원의 넓이 314.1592
*/
위 예제에서는 areaCircle이라고 하는 원의 넓이를 구하는 메소드를 더 정확한 원의 넓이를 구하는 메소드로 수정하기 위해 오버라이딩을 했다. 이러한 경우, 부모 클래스의 메소드를 오버라이딩하는 것은 내용만 새로 정의하는 것이므로 선언부는 부모의 것과 완벽히 동일해야 한다.
@Override 어노테이션은 생략 가능하지만 작성해주면 정확히 오버라이딩된 것인지 컴파일러가 체크하기 때문에 실수를 줄일 수 있다.
그러나 오버라이딩이 불가능한 메소드도 있다. 메소드를 선언할 때 final 키워드를 붙이게 되면 이 메소드는 최종적인 메소드이므로 오버라이딩할 수 없는 메소드가 된다. 즉, 부모 클래스를 상속해서 자식 클래스를 선언할 때 부모 클래스에 선언된 final 메소드는 자식 클래스에서 재정의할 수 없다.
Overloading
오버라이딩은 부모 클래스를 상속할 때, 부모 클래스의 메소드를 수정해서 사용하는 것이라면, 오버로딩은 같은 클래스 내에서 같은 이름을 갖는 메소드라 할지라도 매개변수의 개수 혹은 타입이 다르면 같은 이름의 메소드를 사용할 수 있다는 것이 특징이다. 단, 리턴값만 다르다면 오버로딩을 할 수 없다.
보통 함수 1개는 한 가지의 기능을 한다. 그러나 자바에서는 한 개의 함수에 매개변수의 타입, 개수를 다르게 지정하면 여러 가지의 기능을 구현할 수 있기 때문에 '과적하다'라는 단어인 오버로딩이라는 이름이 붙었다.
/OverloadingMethods.java
public void print(){
System.out.println("오버로딩1");
}
public String print(Integer a){ **//매개변수 존재**
System.out.print("오버로딩2");
return a.toString();
}
public void print(String a){ //**오버로딩2와 매개변수 타입이 다름**
System.out.println("오버로딩3");
System.out.println(a);
}
public String print(Integer a, Integer b){
//오버로딩2, 오버로딩3과 매개변수 개수가 다름
System.out.println("오버로딩4");
return a.toString() + b.toString();
}
}
//OverloadingTest.java
public class OverloadingTest {
public static void main(String[] args) throws Exception {
OverloadingMethods testOverloading = new OverloadingMethods();
testOverloading.print();
System.out.println(testOverloading.print(123));
testOverloading.print("Hello World");
System.out.println(testOverloading.print(7,13));
}
}
/*
오버로딩1
오버로딩2
123
오버로딩3
Hello World
오버로딩4
713
*/
항목 | Override | Overloading |
---|---|---|
접근제어자 | 자식 클래스에서 더 넓은 범위의 접근 제어자 가능 | 모든 접근 제한자 사용 가능 |
리턴 타입 | 동일해야 함 | 달라도 가능 |
메소드 명 | 동일해야 함 | 동일해야 함 |
매개변수 | 동일해야 함 | 서로 달라야 함 |
적용 범위 | 상속관계에서 적용됨 | 같은 클래스 내에서 사용 |
추상 클래스
사전적 의미로 추상(abstract)은 실체 간에 공통되는 특성을 추출한 것을 의미한다. 클래스에서도 추상 클래스가 존재한다. 객체를 직접 생성할 수 있는 클래스를 실체 클래스라고 한다. 그러나 이 클래스들의 공통적인 특성을 추출해서 선언한 클래스는 추상 클래스라고 한다.
추상 클래스가 부모이고 실체 클래스가 자식으로 구현되어 실체 클래스는 추상 클래스의 모든 특성(필드와 메소드)을 물려받고, 추가적은 특성을 가질 수 있다.
추상 클래스를 사용하는 이유가 무엇인가?
첫번째로는 실체 클래스들의 공통된 필드와 메소드 이름을 통일하려는 목적으로 사용한다. 실체 클래스를 설계하는 사람이 여러 사람일 경우 실체 클래스마다 필드와 메소드가 제각기 다른 이름을 가질 수 있다. 이 때 추상 클래스를 상속하면 필드와 메소드 이름을 통일하여 조금 더 효율적으로 사용할 수 있다.
두번째로는 실체 클래스를 작성할 때 시간을 절약할 수 있게 된다. 공통적인 필드와 메소드를 추상 클래스에 모두 선언해두고, 실체 클래스마다 다른 점만 실체 클래스에 선언하게 되면 실체 클래스를 작성하는 시간을 절약할 수 있다.
추상 클래스의 선언
추상 클래스를 선언할 때에는 클래스 선언에 abstract 키워드를 붙여야 한다. abstract를 붙이면 new 키워드를 통한 객체 생성은 할 수 없고 상속을 통해 자식 클래스만 만들 수 있다. 그리고 추상 클래스도 생성자가 반드시 있어야 한다.
추상 메소드와 Override
추상 클래스는 실체 클래스가 공통적으로 가져야 할 필드와 메소드들을 정의해 놓은 추상적인 클래스이므로 실체 클래스의 멤버(필드, 메소드)를 통일화하는 목적을 가지고 있다. 모든 실체들이 가지고 있는 메소드 실행 내용이 같다면, 추상 클래스에 메소드를 작성하는 것이 좋다. 하지만 메소드 선언만 일원화하고 실행 내용은 클래스마다 달라야 하는 경우가 있다.
예를 들어 모든 동물은 소리를 내기 때문에 Animal 추상 클래스에서 sound()라는 메소드를 정의했다고 한다. 그러면 어떤 소리를 내도록 해야 하는데, 이것은 실체에서 직접 작성해야 될 부분임을 알게 된다. 이런 경우를 위해서 추상 클래스는 추상 메소드를 선언할 수 있다. 추상 메소드는 추상 클래스에서만 선언할 수 있는데, 메소드의 선언부만 있고 실행 블록이 없는 메소드를 의미하게 됩니다.
추상 클래스를 설계할 때, 하위 클래스가 반드시 실행 내용을 채우도록 강요하고 싶은 메소드가 있을 경우, 해당 메소드를 추상 메소드로 선언하면 된다. 그리고 자식 클래스는 반드시 추상 메소드를 오버라이딩해서 실행 내용을 작성해야 한다. 그렇지 않으면 컴파일 에러가 발생하고, 이것이 추상 메소드의 위력이라고 할 수 있다.
public abstract class Animal {
public String kind;
public abstract void sound();
}
//Dog.java
public class Dog extends Animal {
public Dog() {
this.kind = "포유류";
}
@Override
public void sound() {
System.out.println("멍멍");
}
}
//DogExample.java
public class DogExample {
public static void main(String[] args) throws Exception {
Animal dog = new Dog();
dog.sound();
}
}
//멍멍
Interface
자바에서 인터페이스는 객체의 사용 방법을 정의한 타입이다. 따라서 코드를 작성하기 전에 전체 코드를 설계하는 부분에서 어떤 메소드와 필드를 가진 객체를 사용할 것인가를 정하는 기본 설계도라고 생각하면 된다.
인터페이스 선언은 class 키워드 대신에 interface 키워드를 사용한다. 그리고 클래스가 필드, 생성자, 메소드를 구성멤버로 가지는 데 비해 인터페이스는 객체로 생성될 수 없기 때문에 생성자를 가질 수 없고, 상수와 메소드만을 구성 멤버로 가지게 됩니다. 인터페이스는 구현 클래스를 통해 사용된다.
Interface에서 사용할 수 있는 메소드들
인터페이스에서 사용할 수 있는 메소드들은 abstract 메소드, default 메소드, static 메소드 총 3가지가 있다.
abstract 메소드
객체가 가지고 있는 메소드를 설명한 것으로, 호출할 때 필요한 매개 값과 리턴 타입만 알려주고, 실행 블록이 없는 메소드로 작성된다. 왜냐하면 인터페이스를 통해 호출된 메소드는 최종적으로 객체에서 실행되기 때문이다. 따라서 실제 실행 블록은 구현 클래스에서 재정의(Override)하여 작성된다. 추상메소드를 작성할 때에는 접근제한자 뒤에 abstract라는 키워드를 작성해야 하지만, 이를 생략하더라도 컴파일 과정에서 자동으로 붙게 된다.
default 메소드
인터페이스에서 선언되지만 구현 객체가 가지고 있는 인스턴스 메소드라고 생각해야 한다. 작성 형태는 클래스의 인스턴스 메소드와 동일하지만, 리턴 타입 앞에 default 키워드를 작성해야 한다. 뿐만 아니라 구현 클래스의 메소드와 같이 실행블록을 작성해야 한다.
static 메소드
default 메소드와는 달리 객체가 없어도 인터페이스만으로 호출이 가능하다. 형태는 클래스의 정적 메소드와 동일하다. 정적 메소드는 public 특성을 갖기 때문에 접근제한자인 public을 생략하더라도 컴파일 과정에서 자동으로 붙게 된다.
public interface Customer {
//abstract 메소드
public String getOrder();
//default 메소드
default void requestBill() {
System.out.println("영수증 주세요");
}
//static 메소드
static void sayHello() {
System.out.println("안녕하세요")
}
}
인터페이스는 서로 다른 두 가지를 연결해주는 것이라고 생각하면 좀 더 이해하기 쉽다. 이러한 역할을 하는 현실 세계의 물건은 대표적으로 USB가 있다. USB 포트를 통해 컴퓨터와 디지털 카메라, 핸드폰 등을 연결하게 된다.
추상 클래스와 Interface의 차이점
추상 클래스는 상속받아서 자식 클래스를 만들기 때문에 내부의 추상 메소드를 꼭 상속받지 않아도 된다. 그러나 인터페이스는 개발 시 꼭 필요한 메소드들을 추상 메소드로 작성한 것들이기 때문에 실제로 컴파일 에러가 없다고 하더라도 개발 설계의 구멍이 될 수 있으므로 무조건 인터페이스의 추상 메소드는 implements 받아서 구현 객체에 메소드를 Override해서 작성해야 한다. 따라서 추상 클래스와 Interface는 처음 사용 목적부터 다르다고 볼 수 있다.
'JVM > Java' 카테고리의 다른 글
Generics (0) | 2021.06.26 |
---|---|
예외처리 (0) | 2021.06.26 |
제어문 (0) | 2021.06.26 |
자료형 (0) | 2021.06.26 |
Java 소개 (0) | 2021.06.26 |