자바는 객체지향 언어이다.
객체지향이 무엇이고, 왜 객체지향을 사용할까.
객체지향과 절차지향은 서로 반대되는 개념이 아니다.
절차지향은 어떠한 목적을 달성하기 위해 순서대로 코드를 작성하는 것에 집중한다.
객체지향도 마찬가지로 어떤 목적을 달성하기 위해 코드를 작성한다. 하지만 객체라는 개념을 이용해서 코드의 가독성, 유지보수 및 확장 가능성을 염두한다는 차이가 있다.
객체지향은 객체들간의 관계를 통해 프로그램을 구성하는 것이다.
여기서 객체는 멤버 변수와 메서드를 가지는 하나의 독립된 개체이다.
public class Object {
private int intValue;
private String stringValue;
...
public void method() {}
...
}
객체라는 개념을 도입함으로써 가질 수 있는 이점이 있다.
-> 코드 복잡성 저하, 가독성 상승, 유지 보수성 상승
객체지향의 대표적인 특징으로 캡슐화, 상속, 다형성이 있다.
캡슐화는 우리가 흔히 사용하는 기계장치들을 생각해보면 된다.
기계들은 대부분 복잡한 구성요소와 작동 원리는 숨기고 사용자가 필요한 기능을 사용할 수 있도록 보다 단순한 인터페이스를 제공한다. 자바의 객체도 이러한 방식으로 작동한다.
객체를 사용하기 위해서 객체를 생성할 필요가 있다. 또 생성 후 사용할 수 있도록 객체의 데이터를 설정하는 과정이 필요하다.
객체를 생성하기 위해서는 객체의 생성자를 이용한다.
Java는 기본 생성자를 자동으로 추가한다.
public Object() {
}
생성자를 가지는 이유는 객체의 존재 이유와 일맥상통한다.
객체는 변수를 그룹화 하고, 그 변수를 이용하는 메서드를 함께 저장함으로써 외부로의 불필요한 노출을 최소화 하기 위함이다.
생성자가 없다면, 객체를 생성한 후 초기화가 필요한 변수들을 직접 외부에서 메서드를 호출해 설정하거나, 변수에 직접 접근해서 초기화를 진행한다. 이 경우 반 강제로 초기화를 위한 메서드 및 변수가 외부에 노출된다.
따라서 생성자를 통해 변수를 초기화하고, 접근제어자로 변수를 숨긴다.
public class Object {
int value;
public Object(int value) {
this.value = value;
}
}
this는 자기 자신의 참조값을 가리킨다.this키워드로 변수명이 매개변수와 겹치는 경우 멤버변수를 가리킬 수 있다.this()로 자기 자신의 생성자를 호출할 수 있다.특정 메서드나 필드 변수에 무분별하게 접근하는 것을 막을 필요가 있다.
public class Character {
int level;
}
위와 같은 클래스에서 level 변수의 값이 100을 넘을 수 없다고 할 때, 외부에서 직접 접근하여 값을 바꾸는 것을 막을 방법이 없다.
만약 외부에서 Character.level 로 접근하여 값을 100보다 큰 값으로 바꾼다면 문제가 발생한다.
이를 방지하고자 접근제어자를 이용한다.
public class Character {
private int level
public void levelUp() {
if(level>100) {
System.out.println("레벨 허용 범위 초과);
}
level++;
}
}
private접근제어자로 level변수를 숨기고 접근할 수 있는 전용 메서드를 구현한다.
위와 같은 형식으로 변수를 의도한 대로만 이용할 수 있게 제한할 수 있다.
접근제어자는 클래스나 메서드 레벨에서도 사용할 수 있다.
내부에서만 사용하는 클래스, 메서드가 존재하는 경우에도 접근제어자를 활용하여 외부로부터 감출 수 있다.
객체를 도입하고 접근 제어자를 함께 활용함으로써 객체지향의 중요한 개념 중 하나인 캡슐화를 적용할 수 있다.
static은 객체의 인스턴스 간 변수를 공유하도록 하는 키워드이다.

static 키워드를 사용한 변수는 메서드 영역에 생성된다. 이를 클래스 변수 라고 부른다.
객체는 하나의 개념이고, 객체가 실체를 가지게 되면 인스턴스라고 부른다.
하나의 객체로부터 생겨난 인스턴스들이 객체에서 공유해야하는 값, 새로 생성할 필요가 없는(공유해도 상관없는) 값은 static키워드를 붙여 선언한다.
그렇게 되면 힙 영역에 존재하는 인스턴스에서 메서드 영역에 존재하는 변수를 공유할 수 있다.
정적 변수나 메서드는 인스턴스의 생성 없이 사용할 수 있다.
이는 JVM에서 클래스를 로딩 하는 시점에 생성되기 때문이다.
인스턴스가 생성되지 않은 상태에서 사용이 가능하기 때문에, 인스턴스 변수나 인스턴스 메서드는 이용할 수 없다.
public class Object {
private int fieldValue;
private static int staticValue;
public void method() {
fieldValue = 1;
staticValue = 1;
staticMethod();
}
public static void staticMethod() {
//fieldValue = 1; -> 컴파일 오류(인스턴스 변수 접근)
//method(); -> 컴파일 오류(인스턴스 메서드 접근)
staticValue = 1;
}
}
비슷한 역할을 하는 객체들은 공통된 기능을 가질 수 있다.
이때 그 역할을 각 객체마다 정의하고 구현하는 것은 비효율적이다.
따라서 부모 클래스와 자식 클래스를 정의하고 상속을 통해 관계를 맺음으로써,
자식 클래스가 부모 클래스를 이용할 수 있도록 한다.
상속은 다음과 같이 extends 키워드를 사용하여 구현한다.
public class ElectricCar extends Car {
...
}
만약 ElectricCar 객체의 인스턴스를 생성하면, Java는 Heap영역에 Car 객체의 인스턴스도 함께 생성한다. 즉 자식 객체의 인스턴스 생성 시 부모 객체의 정보도 포함하여 생성된다.
때문에 자식 객체 인스턴스는 부모 객체 인스턴스에 접근하고 기능을 활용할 수 있게 된다.

자식 클래스는 부모 클래스의 기능을 재정의 할 수 있다. 이것을 Override라고 한다.
public class Car {
public void defaultMethod() {
System.out.println("helloWorld");
}
public void move() {
System.out.println("Car.move");
}
}
public class ElectricCar extends Car {
@Override
public void move() {
System.out.println("ElectricCar.move");
}
}
위와 같이 클래스가 존재하고 ElectriCar가 Car를 상속하는 상태라고 하면
ElectricCar는 defaultMethod()를 따로 정의하지 않았지만 부모 클래스에 해당 메서드가 존재하기 때문에 사용할 수 있다.
move()메서드는 재정의 되었기 때문에 ElectricCar의 move()를 호출하면 ElectricCar.move가 출력된다.
super는 부모의 참조값을 나타낸다.
super()로 부모의 생성자를 호출할 수 있다.
원리에 나와있듯 자식 클래스 인스턴스 생성 시 부모 클래스의 인스턴스도 함께 생성된다. 즉, 부모 클래스의 생성자도 호출된다.

다형성은 객체가 다양한 형태를 지닐 수 있음을 의미한다.
A 와 B가 존재할 때, 만약 A가 할 수 있는 모든 것들을 B도 할 수 있다면?
B도 A로 취급할 수 있다.
앞서 다룬 상속도 위의 상황에 속한다.
부모 객체의 기능을 자식 객체는 모두 사용할 수 있다. 따라서 자식 객체를 부모 객체로 취급할 수 있게 된다.
public static void main() {
Car car = new ElectricCar();
}
이러한 현상을 다형적 참조라고 한다.
abstract class abstractClass {
public void defaultMethod() {
System.out.println("helloWorld");
}
public abstract void abstractMethod();
}
추상클래스는 직접 인스턴스를 생성할 수 없는 클래스이다.
추상클래스는 추상메서드를 가질 수 있다.
추상 메서드는 기존 메서드와 동일하되, 메서드 바디부분이 없다. 따라서 추상 클래스를 상속받는 자식 클래스는 필수적으로 추상 메서드를 오버라이드 해야 한다.
추상적인 개념을 추상클래스로 정의하고 구현체에서 이를 구현함으로써 공통된 기능을 수행할 수 있다.
이런 추상 클래스를 더 간단하게 구현한 것이 인터페이스이다.
순수 추상 클래스
public abstract class AbstractAnimal {
public abstract void sound();
public abstract void move();
}
인터페이스
public interface InterfaceAnimal {
void sound();
void move();
}
public abstract 키워드를 생략할 수 있다.인터페이스가 기존 클래스 간 상속과 달리 다중 상속을 지원할 수 있는 이유는 다음과 같다.

두 인터페이스가 동일한 추상 메서드를 가지는 상황이다. 하지만 인터페이스 자체는 구현을 가지지 않는다.
따라서 Child 객체에서 methodCommon()을 하나 구현하면 어떤 인터페이스를 사용해 호출하든 구현된 메서드를 사용하면 되기 때문에 모호하지 않다.
인터페이스와 상속을 통해 다형성이라는 특징을 만족시킨다. 이렇게 해서 얻을 수 있는 장점은 다음과 같다.
Animal 인터페이스를 구현한 Dog, Cat 클래스가 있다.
public static void main() {
Animal dog = new Dog();
Animal cat = new Cat();
makeSound(dog);
makeSound(cat);
}
private static void makeSound(Animal animal) {
animal.sound();
}
makeSound()메서드와 같이 인터페이스 타입으로 Dog, Cat 클래스를 모두 받을 수 있다.
만약 인터페이스가 존재하지 않았다면, 각각의 클래스마다 메서드를 추가해줘야 한다. 또 같은 종류의 클래스를 새로 만드는 경우에도 똑같이 메서드를 추가해줘야 한다.
코드 중복으로 인해 가독성이 저하되고 확장성이 떨어질 것이다.
Open for extension: 새로운 기능의 추가나 변경 사항이 생겼을 때, 기존 코드는 확장할 수 있어야 한다.
Closed for modification: 기존의 코드는 수정되지 않아야 한다.
좋은 객체 지향 설계 원칙 중 하나이다.
다형성을 활용하여 이를 만족시킬 수 있다.
public static void main() {
Driver driver = new Driver();
Car car = new CarV1();
Car newCar = new CarV2();
driver.driveCar(car);
driver.driveCar(newCar);
}
public class Driver {
...
public void driveCar(Car car) {
car.startEngine();
...
}
}
Driver 클래스는 Car 인터페이스에 의존한다. 즉 Car 인터페이스에 대한 정보를 가지고 Car의 구현체를 사용한다.
덕분에 새로운 자동차를 추가하고 Driver 클래스가 사용하도록 하여도 Driver 클래스 내부는 수정할 필요가 없다.
구체적인 타입이 아니라 추상화된 타입에 의존하기 때문에 가능한 것이다.
훌륭하네... 다형성에 특징을 이제 알았으니 스프링에서는 어떻게 이 다형성을 활용하는지 다시 생각하면 더 좋은 스터디가 될것같음