OOP는 너무 거대한 개념이라 다 다루지는 못하지만 간단히만 알아보자
간단한 설명
OOP : 중심적 프로그래밍 패러다임. 현실 세계의 사물들을 객체라고 보고, 그 객체로부터 개발하고자 하는 애플리케이션에 필요한 특징들을 뽑아와 프로그래밍하는 것(추상화)
장점
OOP로 작성한 코드는 재사용성이 높다.
자주 사용되는 로직을 라이브러리로 만들어두면 개발로드가 줄어들고, 신뢰성도 확보가 됨
- 라이브러리를 각종 예외상황에 맞게 잘 구현하면, 개발자가 사소한 실수를 해도 에러를 컴파일 단계에서 잡아낼 수 있음
- 내부적으로 어떻게 돌아가는지 몰라도 그냥 사용하면 됨.
객체 단위로 코드가 나뉘어져있어 디버깅과 유지보수가 쉽다.
데이터 모델링 할때, 객체와 매핑하는 것이 훨씬 수월하기에 요구사항을 보다 명확하게 파악하여 프로그래밍 가능
단점
- 객체 간의 정보 교환이 모두 메시지 교환을 통해 일어나므로, overhead가 발생하게 됨.
- 하지만, 하드웨어의 발전으로 많이 보완됨
객체가 상태를 가짐
객체 안에는 변수가 존재하고, 이 변수를 통해 객체가 예측할 수 없는 상태를 갖게 되어 어플리케이션 내부에서 버그를 발생시킴
-> 따라서 함수형 프로그래밍을 발전 시켜 해결하고자 함
객체 지향적 설계원칙(SOLID)
- SBP(Single Responsibility Principle) : 단일 책임 원칙
- 클래스는 단 하나의 책임을 가져야하며 클래스를 변경하는 이유는 단 하나의 이유이어야 함
- OCP(Open-Closed Principle) : 개방-폐쇄 원칙
- 확장에는 열려있어야하고, 변경에는 닫혀있어야한다.
- OCP는 다형성을 통해 지켜질 수 있다.
- 다형성 : 하나의 객체 혹은 메소드가 여러 타입을 참조할 수 있음.
- 객체 다형성 : 자식 객체가 부모 객체의 인스턴스로 할당될 수 있음
- 메소드 다형성 : 오버로딩을 통해 구현 가능
- IoC 컨테이너에서 DI를 통해 다형성을 이용하여 OCP를 지켜준다. -> 유지보수가 매우 쉬워짐
- 다형성 : 하나의 객체 혹은 메소드가 여러 타입을 참조할 수 있음.
- LSP(Liskov Substitution Principle) : 리스코프 치환원칙
- 상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야한다.
- ISP(interface Segregation Principle) : 인터페이스 분리 원칙
- 인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야한다.
- DIP(Dependency Inversion Principle) : 의존 역전 원칙
- 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다.
좀 더 자세히
용어 및 정의
정의
- 객체 지향의 가장 기본은 객체이며, 객체의 핵심은 기능을 제공하는 것이다.
- 실제로 객체를 정의할 때 사용하는 것은 객체가 제공해야할 기능이며, 객체 내부의 데이터로 정의되지 않는다.
- 이러한 기능들을 오퍼레이션(operation)이라고 부른다.
시그니처
- 객체 지향으로 설계하기 위해서는 오퍼레이션의 사용법을 알아야한다.
- 오퍼레이션의 사용법은 다음 세가지로 구성되고, 이 세가지를 시그니처라고 부른다.
- 기능 식별 이름
- 파라미터 및 파라미터 타입
- 기능 실행 결과값 및 타입
인터페이스
- 객체가 제공하는 모든 오퍼레이션의 집합을 객체의 인터페이스라고 부른다.
메시지
- 오퍼레이션의 실행을 요청하는 것을 메시지를 보낸다라고 표현한다.
- 자바에서는 메서드를 호출하는 것이 메시지를 보내는 과정에 해당함
설계 원칙
책임
객체마다 자신만이 제공할 수 있는 기능에 대한 책임이 있다.
객체가 갖는 책임의 크기는 작을 수록 좋다.
- 한 객체가 너무 많은 기능을 포함하면(책임이 크면), 그 기능과 관련된 데이터들도 모두 한 객체에 포함되게 되고, 객체에 정의된 많은 오퍼레이션들이 데이터를 공유하는 방식으로 프로그래밍 되는데, 이는 절차지향과 다를바가 없기 때문이다.
이것을 해결하기 위해,
SBP(Single Responsibility Principle) : 단일 책임 원칙
이 필요하다.
개방-폐쇄 원칙
- “기존 코드를 변경하지 않으면서 코드의 수정을 허용하는 것”
- 즉, 원하는 기능을 구현하였을때, 사용자는 우리가 짠 코드를 변경할 순 없으나, 그 기능은 변경가능하도록 하는 것이다.
- 예시
- Car라는 클래스가 있을때, 사용자는 Car라는 클래스를 확장하여 Taxi 라는 클래스를 만들었다고 해보자.
- 이때 Taxi 에는 Car의 기능을 포함하여 새로운 기능 (요금 받기… 등)이 필요하다. 따라서 이러한 기능들은 Taxi 클래스 안에서 정의하여 구축할 수 있다.
- 또 Car 클래스에 속한 기본적인 기능(accelate(), break() 등)에 변경이 필요하면, 이 기능들을 오버라이드하여 필요한 기능들을 구축할 수 있다.
- 위와 같이, Car라는 클래스를 변경하지 않으면서, Car의 확장은 허용하는 것이
OCP 원칙
이다.
리스코프 치환 원칙
상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야한다.
상위 타입인 fruits에 하위타입인 Apple이 있다면, fruits를 사용하는 곳에서는 Apple을 사용해도 문제 없도록 해야한다.
위배한 예시
- 직사각형은 정사각형이 아니지만, 정사각형은 직사각형이다. 따라서 직사각형을 상속하여 정사각형 객체를 정의했다고 해보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
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; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14
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); } }
- 이때 직사각형을 인자로 받고, 세로가 가로보다 짧다면, 세로를 가로의 길이에 1을 더한 만큼의 길이를 갖게 만들어 세로를 더 길게만드는 메서드가 있다고 해보자.
- 이때 정사각형은 항상 가로와 세로의 길이가 같으므로, 위 메서드를 실행하면, 가로와 세로의 길이가 같게 된다. 위 메서드의 수행결과에 문제가 발생한 것이다.
- 따라서 이는 리스코프 치환원칙에 위배되므로, 정사각형은 직사각형을 상속받으면 안된다.
리스코프 치환원칙은 기능의 명세와 확장에 대한 것이다.
- Retangle 클래스의 setHeight() 메서드는 아래와 같은 기능을 명세한다.
- 높이 값을 파라미터로 전달받은 값으로 변경한다.
- 폭값은 변경되지 않는다.
- 하지만, Square의 setHeight()는 높이와 폭 모두 변경하게 된다. 이렇게 상위 타입에서 정한 명세를 하위 타입에서도 그대로 지킬 수 없다면 상속을 하면 안된다.
- Retangle 클래스의 setHeight() 메서드는 아래와 같은 기능을 명세한다.
의존성
- 한 객체가 다른 객체를 이용한다는 것은, 한 객체의 코드에서 다른 객체를 생성하거나 다른 객체의 메서드를 호출한다는 것을 의미한다.
- 이러한 의존의 영향은 꼬리에 꼬리를 문것처럼 계속 전파되고, 변경한 여파가 다시 자기 자신까지 변화시킬 수 있는데, 이것을 순환의존이라고 한다.
- 이것을 해결하기 위해,
DIP(Dependency Inversion Principle) : 의존 역전 원칙
이 필요하다
인터페이스 분리 원칙
인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야한다.
특정 클라이언트를 위한 인터페이스를 여러개 만드는 것이 범용 인터페이스 하나보다 낫다
예시
ISP를 적용하기 전
1 2 3 4 5 6
// ISP를 적용하지 않은 예제 public interface multifunction { void copy(); void fax(Address from, Address to); void print(); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
public class CopyMachine implements multifunction { @Override public void copy() { System.out.println("### 복사 ###"); } @Override public void fax(Address from, Address to) { // 사용하지 않는 인터페이스가 변경되어도 함께 수정이 일어난다. } @Override public print() { // 사용하지 않는 인터페이스가 변경되어도 함께 수정이 일어난다. } }
- CopyMachine에 필요없는 fax, print 도 모두 구현해줘야한다.
- 만약 multifunction의 fax() print()의 리턴타입이 변경되면 CopyMachine에서도 모두 변경해줘야한다.
ISP를 적용한 이후
1 2 3 4 5 6 7 8 9 10 11 12
// ISP가 적용된 예제 public interface Print{ void print(); } public interface Copy { void copy(); } public interface Fax { void fax(Address from, Address to); }
1 2 3 4 5 6
public class copyMachine implements Copy { @Override void copy() { System.out.println("### 복사 ###"); } }
캡슐화
캡슐화
객체가 내부적으로 기능을 어떻게 구현하는지를 숨기는 것
- 객체지향은 기본적으로 캡슐화를 통해서 한 곳의 변화가 다른 곳에 미치는 영향을 최소화한다.
- 내부 기능 구현이 변경되더라도, 그 기능을 사용하는 코드는 영향을 받지 않도록 해주어서, 내부 구현 변경의 유연함을 주는 기법이 캡슐화이다.
캡슐화를 위한 두 개의 규칙
Tell, Don’t Ask
데이터를 물어보지 않고, 기능을 실행해 달라고 말하라는 규칙이다.
데이터를 읽는 것은 데이터를 중심으로 코드를 작성하게 만드는 원인이 된다.
데이터를 private로 클래스 내부에 숨기고, 메소드를 통해 데이터에 접근해야한다.
1 2 3 4 5 6
public class Customer { private Wallet wallet; public Wallet getWallet() { return wallet; } }
데미테르 법칙 : 한 객체 안에서 다른 객체의 메서드를 부를때는 다음 상황일때만이다.
- 메서드에서 생성한 객체의 메서드만 호출
- 파라미터로 받은 객체의 메서드만 호출
- 필드로 참조되는 객체의 메서드만 호출
객체지향 설계과정
- 제공해야할 기능을 찾고, 또는 세분화하고 그 기능을 알맞는 객체에 할당한다.
- 기능을 구현하는데 필요한 데이터를 객체에 추가한다.
- 객체에 데이터를 먼저 추가하고, 그 데이터를 이용하는 기능을 넣는다.
- 기능은 최대한 캡슐화하여 구현한다.
- 객체간에 어떻게 메시지를 주고 받을 지 결정한다.
상속을 통한 재사용의 단점
- 상위 클래스 변경의 어려움
- 어떤 클래스를 상속 받는 다는 것은 그 클래스에 의존한다는 뜻이고, 따라서 의존하는 클래스의 코드가 변경되면 영향을 받을 수 있다.
- 클래스의 불필요한 증가
- 유사한 기능을 확장하는 과정에서 클래스의 개수가 불필요하게 증가할 수 있다.
- 상속의 오용
- 같은 종류가 아닌 클래스의 구현을 재사용하기 위해 상속을 받게 되면, 문제가 발생한다.
- 상속을 받는 클래스가 상위 클래스와 IS-A 관계가 아닌 경우에 발생함
객체 조립 : 위 단점의 해소법
- 객체지향언어에서 객체 조립은 보통 필드에서 다른 객체를 참조하는 방식으로 구현된다.
- 상속에 비해 조립을 통한 재사용의 단점은 상대적으로 런타임 구조가 복잡해진다는 것이고, 상속보다 구현이 어렵다는 것이다.
- 하지만, 변경의 유연함을 확보하는데서 오는 장점이 크기 때문에, 상속보다 조립하는 방법을 먼저 고려해야한다.
상속은 언제 사용하는가?
- 재사용이 아닌, 기능의 확장이라는 관점에서 상속을 적용해야함
- 명확한 IS-A 관계가 성립 되어야함.
출처
https://asfirstalways.tistory.com/177