문제를 여러 개의 객체 단위로 나눠 작업하는 방식으로, 객체들이 서로 유기적으로 상호작용하는 프로그래밍 이론이다.
OOP는 코드 재사용성과 생산성의 향상 효과를 볼 수 있고, 유지보수의 편의성 덕택에 협업이 중요하고 규모가 큰 대형 프로젝트에서 사용된다.
객체 지향 프로그래밍의 가장 큰 특징은 클래스를 이용해 연관 있는 처리 부분(함수)과 데이터 부분(변수)를 하나의 객체(인스턴스)로 묶어 생성해 사용한다.
객체 지향 프로그래밍은 캡슐화, 추상화, 상속성, 다형성 네 가지 특징을 가진다.
데이터와 관련 기능을 묶는 것으로 데이터와 코드의 형태를 외부로부터 알 수 없게 하고, 데이터의 구조와 역할, 기능을 하나의 캡슐 형태로 만드는 방법이다.
캡슐화의 중요한 목적은 변수를 private로 선언하여 데이터를 보호하고,
보호된 변수는 getter나 setter등의 메서드를 통해서만 간접적으로 접근을 허용하는 것이다.
캡슐화를 하면 정보은닉을 할 수 있다는 특징이 있고, 외부의 영향 없이 객체 내부 구현 변경이 가능하다.
추상화는 객체의 공통적인 속성과 기능을 추출하여 정의하는 것이다.
실제로 존재하는 객체들을 프로그램으로 만들기 위해 공통 특성을 파악해 필요없는 특성을 제거하는 과정을 가리킨다.
객체들은 실제 그 모습이지만, 클래스는 객체들이 어떤 특징들이 있어야 한다고 정의하는 추상화된 개념이다.
정리하자면 추상화는 객체들의 공통된 특징을 파악해 정의해 놓은 설계 기법이다.
두 가지 방식의 추상화 방법이 있는데 특정한 성질, 공통 성질(일반화)를 뽑아내는 방식이다.
상속이란 기존 상위 클래스에 기능을 가져와 재사용할 수 있으면서도 동시에 하위클래스에 새로운 기능도 추가할 수 있는 것이다.
OOP에서 부모 클래스, 자식 클래스라고 표현한다.
상속이 필요한 이유는 코드의 중복을 없애기 위해서이다.
(상속 관계를 맺으면 자식 객체를 생성할 때 부모 클래스의 속성들을 자동으로 물려받기 때문에 자식 클래스에서 또 정의할 필요가 없어진다.)
다형성이란 상속과 연관있는 개념으로 한 객체가 상속을 통해 기능을 확장하거나 변경하여 다른 여러형태(객체)로 재구성되는 것을 말한다.
(즉, 한 객체가 여러 타입의 기능을 제공하는 것)
오버로드(Overload), 오버라이드(Override)가 다형성의 대표적인 예이고, 이것을 구현하는 것을 오버로딩(Overloading), 오버라이딩(Overriding)이라고 한다.
오버로딩은 하나의 클래스 안에서 같은 이름의 메서드를 사용하지만, 각 메서드마다 다른 용도로 사용되며 그 결과물도 다르게 구현하는 것을 말한다.
(오버로딩이 가능하려면 메서드끼리 이름은 같지만 매개변수의 개수나 데이터 타입이 달라야 한다)
오버라이딩은 하위 클래스(자식)가 상위 클래스(부모)에서 만들어진 메서드를 자신의 입맛대로 다시 재창조해서 사용하는 것을 말한다.
다향성을 사용하면 같은 이름의 속성을 유지함으로서, 속성을 사용하기 위한 인터페이스를 유지하고, 메서드 이름을 낭비하지 않게 된다.
API가 많아질수록 복잡성은 증가하기 때문에 다형성은 유용하며 코드 재사용성을 늘려주어 유지보수가 용이하도록 도와주는 개념이다.
OOP는 위 네가지 특성들을 통해 어떤 대상을 추상화
하여 공통점을 찾고, 그것을 캡슐화
해서 한 군데에 모아 객체를 만들고, 새로운 객체가 상속
받아 재사용이 가능하게 만들어준다.
상속받은 객체는 다형성
을 통해 기능을 수정 또는 추가하여 재사용할 수 있다.
객체지향 5대 원칙이라고 불리는 SOLID 원칙에 대해 정리해보자.
SRP(단일 책임 원칙), OCP(개방-폐쇄 원칙), LSP(리스코프 치환 원칙), DIP(의존 역전 원칙), ISP(인터페이스 분리 원칙)을 말하며, 프로그래머가 시간이 지나도 유지 보수와 확장이 쉬운 소프트웨어를 만드는데 이 원칙들을 적용할 수 있다.
소프트웨어의 설계 부품(클래스, 함수 등)은 단 하나의 책임만을 가져야 한다
여기서 책임이란 기능
의 의미로 해석하면 된다.
설계를 잘한 프로그램(=응집도는 높고 결합도는 낮은 프로그램)은 기본적으로 새로운 요구사항과 프로그램 변경에 영향을 받는 부분이 적다. 만약 한 클래스가 수행할 수 있는 기능, 즉 책임이 많아진다면 클래스 내부의 함수끼리 강한 결합을 발생할 가능성이 높아진다. 이는 유지보수에 비용이 증가하기 때문에 책임을 분리시킬 필요가 있다.
기존의 코드를 변경하지 않고(Closed) 기능을 수정하거나 추가할 수 있도록(Open) 설계해야 한다.
OCP에 만족하는 설계를 할 때 변경되는 것이 무엇인지에 초점을 맞춘다. 자주 변경되는 내용은 수정하기 쉽게 설계하고, 변경되지 않아야 하는 것은 수정되는 내용에 영향을 받지 않게 하는 것이 포인트다. 이를 위해 자주 사용되는 문법이 인터페이스
이다.
SoundPlayer 클래스는 음악을 재생해주는 클래스로 기본적으로 wav파일을 재생할 수 있다. 그러나 SoundPlayer가 다른 포맷의 파일을 재생할 수 있도록 요구사항이 변경되었다고 가정해보자. 요구사항을 만족시키기 위해서는 play() 메소드를 수정해야하는데 이럴 경우 OCP 원칙에 위배된다.
class SoundPlayer {
void play() {
System.out.println("play wav"); //wav 재생
}
}
public class Client {
public static void main (String[] args) {
SoundPlayer sp = new SoundPlayer();
sp.play();
}
}
그렇다면 어떻게 변경해야 OCP 원칙을 만족시키는가? 인터페이스를 사용해보자.
먼저 변해야하는 것은 무엇인가? 위 클래스에서는 play() 메소드가 변해야 하는 것이므로 play() 메소드를 인터페이스로 분리해보자.
재생하고자 하는 파일 클래스를 만들어 PlayAlgorithm 인터페이스의 play() 메소드를 재정의하도록 설계한다.
interface playAlgorithm {
public void play();
}
class Wave implements playAlgorithm {
@Override
public void play() {
System.out.println("Play wav");
}
}
class Mp3 implements playAlgorithm {
@Override
public void play() {
System.out.println("Play Mp3");
}
}
SoundPlayer 클래스에서는 playAlgorithm 인터페이스를 멤버 변수로 만든다. 그 후 SoundPlayer의 play() 함수는 인터페이스를 상속받아 구현된 클래스의 play()함수를 실행시키게 한다. 마지막으로 메인함수에서 setter를 이용해 우리가 플레이하고자 하는 파일의 객체를 지정해주면 된다.
class SoundPlayer {
private playAlgorithm file;
public void setFile(playAlgorithm file) {
this.file = file;
}
public void play() {
file.play();
}
}
public class Client {
public static void main(String[] args) {
SoundPlayer sp = new SoundPlayer();
sp.setFile(new Wav());
sp.setFile(new Mp3());
sp.play();
}
}
결과적으로 SoundPlayer 클래스 변경 없이 재생되는 파일을 바꿀 수 있으므로 위 코드는 OCP를 만족한다.
자식 클래스는 부모 클래스에서 가능한 행위를 수행할 수 있어야 한다.
상속 관계에서는 일반화 관계(IS-A)가 성립해야한다. 일반화 관계에 있다는 것은 일관성이 있다는 것이다. 따라서 리스코프 치환 원칙은 일반화 관계에 대해 묻는 것이라 할 수 있다.
예를 들어 도형 클래스와 사각형 클래스가 있고, 사각형 클래스는 도형 클래스의 상속을 받는다고 하자.
일반화 관계(일관성인지 확인하기 위해 도형 -> 사각형을 넣어보자)
이상한 부분이 보이지 않기에 일관성이 있다고 할 수 있다.
의존 관계를 맺을 때, 변화하기 쉬운 것보다는 변화하기 어려운 것에 의존해야한다는 원칙이다
변화하기 쉬운 것 = 구체적인 것, 변화하기 어려운 것 = 추상적인 것을 말한다.
객체지향적인 관점에서 보자면 변화하기 쉬운 것이란 구체화된 클래스를 의미하고, 변화하기 어려운 것은 추상클래스나 인터페이스를 의미한다. 따라서 DIP를 만족한다는 것은 의존관계를 맺을 때, 구체적인 클래스보다 인터페이스나 추상 클래스와 관계를 맺는다는 것을 의미한다.
class SoundPlayer {
private playAlgorithm file;
public void setFile(playAlgorithm file) {
this.file = file;
}
public void play() {
file.play();
}
}
public class Client {
public static void main(String[] args) {
SoundPlayer sp = new SoundPlayer();
sp.setFile(new Wav());
sp.setFile(new Mp3());
sp.play();
}
}
setFile을 이용해 실행하고자 하는 파일을 쉽게 바꿀 수 있다. 마찬가지로 새로운 오디오 파일 포맷(FLAC)을 실행시키고자 한다면, 새로운 클래스(FLAC)를 만든 후 playAlgorithm 인터페이스를 상속받아 구현한 후 setFile 메소드를 통해 file 멤버 변수에 주입하면 된다. 이와 같은 기술을 의존성 주입
이라 한다.
한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다. 하나의 일반적인 인터페이스보다는 여러 개의 구체적인 인터페이스가 낫다
다시 말해, 자신이 사용하지 않는 기능(인터페이스)에는 영향을 받지 말하야한다는 의미이다.
예를 들어, 우리는 스마트폰으로 전화, 웹서핑, 사진 촬영 등 다양한 기능을 사용할 수 있다. 그런데 전화를 할 때는 웹서핑, 사진촬영 등의 다른 기능은 사용하지 않는다. 따라서 전화 기능과 웹서핑 기능, 사진 촬영 기능은 각각 독립된 인터페이스로 구현하여 서로에게 영향을 받지 않도록 설계해야 한다.