객체 지향 프로그래밍을 의미한다.
객체란 동작의 주체가 되는 요소를 의미.
모든 객체는 상태와 동작을 가진다. TV를 객체라고 한다면 특정 TV만이 가지는 색,크기,가격 등은 상태이며, TV채널 이동, 다시보기, 넷플릭스 연결 등은 기능(동작)이라고 볼 수 있다.
JAVA에서는 다음과 같이 볼 수 있다.
캡슐화
코드를 수정없이 재활용하는 것을 목적으로 함.
클래스라는 캡슐에 기능과 특성을 담아 묶는다.
상속
클래스로부터 속성과 메소드를 물려받는 것을 말한다.
다른 클래스를 가져와서 수정할 일이 있다면, 그 클래스를 직접 수정하는 대신 상속을 받아 변경하고자 하는 부분만 변경.
추상화
객체 지향 관점에서 클래스를 정의하는 것.
불필요한 정보 외 중요한 정보만 표현함으로써 공톡의 속성과 기능을을 묶어서 이름을 붙이는 것.
다형성
하나의 변수명이나 함수명이 상황에 따라 다르게 해석될 수 있음.
대표적인 다형성이 오버 라이딩, 오버 로딩이다.
Employee.class에는 4가지 메소드가 존재한다.
calculatePay() : 회계팀에서 급여를 계산하는 메서드
reportHours() : 인사팀에서 근무시간을 계산하는 메서드
saveDababase() : 기술팀에서 변경된 정보를 DB에 저장하는 메서드
calculateExtraHour() : 초과 근무 시간을 계산하는 메서드 (회계팀과 인사팀에서 공유하여 사용)
그런데 회계팀에서 급여를 계산하는 방식을 새로 변경하여 코드에서 초과 근무 시간을 계산하는 메서드 calculateExtraHour()의 알고리즘 업데이트가 필요해졌다.
그런대 calculateExtraHour()매소드를 변경헀는데, 변경에 의한 파급 효과로 인해 수정 내용이 의도치 않게 reportHours()메소드에도 영향을 주게 되어버린다.(공유하기 때문)
그리고 인사팀에서는 이러한 변경사실을 알지 못하고 메소드 반환 결과가 잘못되었다고 개발팀에 요청을 보내게 된다.
이러한 상황이 바로 SRP에 위배되는 상황이다.
Employee클래스에서 회계팀,인사팀,기술팀 이렇게 3개의 엑터에 대한 책임을 한번에 가지고 있기 때문이다.
즉 이런경우 회계팀, 인사팀, 기술팀의 클래스를 별도로 분리하고 각 클래스의 메소드를 사용하는 방식으로 해결할 수 있다.
확장에 열려있다.
- 모듈의 확장성을 보장하는 것
- 새로운 변경 사항 발생시 유연하게 코드를 추가함으로써 애플리케이션의 기능을 큰 힘을 들이지 않고 확장할 수 있다.
변경에 닫혀있다.
- 객체를 직접 수정하는것은 제한해야 한다는 것을 의마한다.
- 객체를 직접 수정하지 않고도 변경사항을 적용할 수 있도록 설계해야 한다. 그래서 변경에 닫혀있다고 표현.
메인 메소드에서 cat과 dog 동물 객체를 만들고 HelloAnimal 클래스의 hello() 메소드를 통해 실행해보면 오류없이 잘 동작됨을 확인 할 수 있다.
class Animal {
String type;
Animal(String type) {
this.type = type;
}
}
// 동물 타입을 받아 각 동물에 맞춰 울음소리를 내게 하는 클래스 모듈
class HelloAnimal {
void hello(Animal animal) {
if(animal.type.equals("Cat")) {
System.out.println("냐옹");
} else if(animal.type.equals("Dog")) {
System.out.println("멍멍");
}
}
}
public class Main {
public static void main(String[] args) {
HelloAnimal hello = new HelloAnimal();
Animal cat = new Animal("Cat");
Animal dog = new Animal("Dog");
hello.hello(cat); // 냐옹
hello.hello(dog); // 멍멍
}
}
문제는 기능을 추가 이다.
만일 고양이와 개 외에 사자와 양을 추가한다면? 당연하게 HelloAnimal 클래스를 수정해 주어야 한다.
class HelloAnimal {
// 기능을 확장하기 위해서는 클래스 내부 구성을 일일히 수정해야 하는 번거로움이 생긴다.
void hello(Animal animal) {
if (animal.type.equals("Cat")) {
System.out.println("냐옹");
} else if (animal.type.equals("Dog")) {
System.out.println("멍멍");
} else if (animal.type.equals("Sheep")) {
System.out.println("메에에");
} else if (animal.type.equals("Lion")) {
System.out.println("어흥");
}
// ...
}
}
이런식으로 동물이 추가될때마다 계속 코드를 변경해줘야하는 번거로움이 생기게 된다.
이는 처음 설계에서부터 잘못되었기 때문에 발상하는 현상이다.
이제 이를 올바른 OCP대로 설계하려면 다음을 고려해야한다.
1. 먼저 변경(확장)될 것과 변하지 않을 것을 엄격히 구분
2. 이 두 모듈이 만나는 지점이 추상화(추상 클래스 or 인터페이스)를 정의
3. 구현체에 의존하기 보다 정의한 추상화에 의존하도록 코드를 작성.
다음과 같이 클래스를 분리하고 추상화후 구체화 한다면 일일이 코드를 변경하지 않아도 된다.
사자나 양클래스를 추가할때도 동일하게 추가(확장)만 해주면된다.
리스코프 원칙을 지키지 않는 대표적인 예가 직사각형 - 정사각형 문제이다.
직사각형은 정사각형이 아니지만 정사각형은 직사각형이라는 사실에 기반한다.
public class Rectangle {
private int width;
private int height;
public void setWidth(final int width) {
this.width = width;
}
public void setHeight(final int height) {
this.height = height;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
}
따라서 위의 직사각형 객체를 상속받아서 아래처럼 정사각형 객체를 정의할 수 있다.
public class Square extends Rectangle {
@Override
public void setWidth(final int width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(final int height) {
super.setWidth(height);
super.setHeight(height);
}
}
다만 정사격형은 가로와 세로의 길이가 같으므로 setWidth()나 setHeight()를 호출하면 가로와 세로값을 모두 바꿔줘야 하므로 메소드를 재정의 했다.
이제, 다른 클래스에서 Retangle클래스를 이용해 보자.
public void increaseHeight(final Rectangle rectangle) {
if(rectangle.getHeight() <= rectangle.getWidth()) {
rectangle.setHeight(rectangle.getWidth()+1);
}
}
해당 메소드는 직사각형의 가로와 세로를 비교한 다음에, 세로가 가로보다 짧거나 같다면 가로의 길이에 1을 더한 만큼의 길이를 갖게 만드는 역할을 한다. 정사각형이 아닌 직사각형에 대해서는 위 메소드가 올바르게 작동한다.
하지만 정사각형의 경우는 다르다. 정사각형은 항상 가로,세로의 길이가 같으므로 위 메소드를 실행하면 가로와 세로의 길이가 모두 1씩 증가한다.
즉 우리가 원하는 메소드 실행 후, 직사각형의 길이는 가로보다 세로가 길어야 한다는 가정이 꺠진다.
따라서 instanceof를 통해 타입 비교를 해야한다.
public void increaseHeight(final Rectangle rectangle) {
if (rectangle instanceof Square) {
throw new IllegalStateException();
}
if (rectangle.getHeight() <= rectangle.getWidth()) {
rectangle.setHeight(rectangle.getWidth() + 1);
}
}
해당 도형이 정사각형일 경우 예외를 발생시키는 방식으로 코드를 변경할 수 있다.
하지만 이것은 OCP원칙에 어긋나는 코드이다. increaseHeight()가 확장에는 열려있지 않기 때문이다.
따라서 Square 클래스는 Rectangle클래스를 상속받으면 안된다.
아무리 우리가 정사각형은 직사각형이다 라고 이야기해도 위처럼 문제가 발생한다.
클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다는 원칙.
ISP를 위반하는 예를 확인하자
Vechicle은 go(), fly() 메서드를 가진 추상 클래스이다.
AirCraft는 Vechicle을 상속받는다.
Car클래스는 Vechicle을 상속받는다 단 Car는 fly할 수 없기 때문에 fly에서 예외를 발생시킨다.
public class Car extends Vehicle {
public void go() {
System.out.println("Go");
}
public void fly() {
throw new IllegalArgumentException("can not fly");
}
}
Car 클래스는 Vehicle 클래스를 상속받기 때문에 반드시 fly를 구현해야 한다.
하지만 Car class는 fly를 사용하지 않습니다. 이것은 ISP를 위반하는 것이다.
ISP를 적용한다면 다음과 같이 만들 수 있다.
Vehicle 인터페이스를 Movable, Flyable 인터페이스로 나누고 Flyable 클래스에 Movable 클래스를 상속받도록 한다.
Aircraft 클래스가 Flyable 클래스를 상속받도록 한다.
Car 클래스는 Movable 클래스를 상속받도록 한다.
이렇게 되면 Car는 go() 메서드만 구현하면 되고 fly메서드는 구현할 필요가 없어 ISP를 만족하게된다.
Calculator 클래스가 Add클래스를 사용하여 덧셈을 하는 예시.
여기서 Calculator 클래스는 상위 모듈이고 Add클래스는 하위 모듈
public class Add {
public int calculate(int num1, int num2) {
int ret = num1 + num2;
System.out.printf("%d + %d = %d%n", num1, num2, ret);
return ret;
}
}
public class Calculator {
public void start(int num1, int num2) {
Add operation = new Add();
operation.calculate(num1, num2);
}
public static void main(String[] args) {
Calculator cal = new Calculator();
cal.start(1, 2);
}
}
여기서 뺄샘을 추가하고 싶다면 Calculator를 수정해야 한다.
DIP를 적용한다면 다음과 같은 구조를 생각할 수 있다.
Calculator는 Add클래스 대신 인터페이스 역할을 하는 CurrentOperation 추상 클래스에 의존하고 있고 구현체인 Add 및 Sub 클래스 또한 CurrentOperation 추상 클래스에 의존하게 되어 DIP를 만족한다.
이렇게 되면 Calculator는 구현체에 의존하지 않기 때문엑 구현 내용이 변경되어도 수정할 필요가 없다.
interface CurrentOperation {
int calculate(int num1, int num2);
}
class Add implements CurrentOperation {
public int calculate(int num1, int num2) {
int ret = num1 + num2;
System.out.printf("%d + %d = %d%n", num1, num2, ret);
return ret;
}
}
class Sub implements CurrentOperation {
public int calculate(int num1, int num2) {
int ret = num1 - num2;
System.out.printf("%d - %d = %d%n", num1, num2, ret);
return ret;
}
}
class Calculator {
private CurrentOperation operation;
public Calculator(CurrentOperation operation) {
this.operation = operation;
}
public void start(int num1, int num2) {
this.operation.calculate(num1, num2);
}
}
public class Main {
public static void main(String[] args) {
CurrentOperation operation = new Add();
Calculator cal = new Calculator(operation);
cal.start(1, 2);
}
}