개요
자바 프로그래밍을 시작하기에 앞서 자바는 다양한 특징 및 장점을 가지고 있는데
다양한 특징 중 가장 기본적이며, 앞으로 코딩을 하기위해서 어떻게 프로그래밍 해야하는지에 대한
기본을 알려줄 객체 지향 프로그래밍에 알아보겠습니다.
OOP의 정의
객체 지향 프로그래밍(Object Oriented Programming)의 줄임말입니다.
컴퓨터 프로그래밍 기법의 한가지 기법으로, 모든 데이터를 객체(Object)로 취급하며
객체를 위주로 프로그래밍 하는 방법입니다.
※객체(Object)
객체란 객체 지향 프로그래밍의 가장 기본적인 단위이며, 각각 사물의 상태와 속성을 가르킵니다.
Ex) 자바의 신 책 1권은 종이재질의 525페이지 책이며(상태), 자바 프로그래밍에 관련된 내용을 담고 있습니다. (속성)
OOP의 4가지 특징 & 객체 지향 설계 5원칙 SOLID
1. 캡슐화 (Encapsulation)
캡슐화는 객체의 상태와 동작을 하나로 묶는것을 의미합니다.
- 캡슐화된 객체의 세부내용이 외부에 드러나지 않게하여 의도치않는 동작을 방지하는 안전성
- 캡슐화된 객체들은 재사용이 용이하다.
- 접근제어자를 통하여 클래스나 멤버들을 외부에서 접근하지 못하도록 접근을 제어하는 정보은닉성
2. 상속화 (Inheritance / Extends)
상속은 객체들간의 계층적인 관계를 구축하는 방법을 의미합니다. 추상화를 통해 상위 클래스로부터 확장된 여러개의 하위 클래스들이 모두 상위 클래스의 속성과 기능들을 간편하게 사용 할 수 있습니다.
- 상속화를 사용하여 상위 클래스에 있는 특성과 기능을 사용함으로써 재사용성이 높다.
- 속성 또는 기능의 일부분을 자식클래스에서 수정하여 사용할 수 있다.
3. 추상화 (Abstraction)
객체에서 공통적인 속성과 동작을 추출하여 모델링하는 방법을 의미합니다.
- 복잡한 시스템을 단순화하여 이해하기 쉽고, 유지보수하기 편리한 코드를 작성하도록 도와준다.
- 인터페이스와 구현을 분류하여 객체가 가진 특성 중 필수 속성만으로 객체를 묘사하고 유사성만 표현하여 세부적인 상세사항은 각 객체에 따라 다르게 구현되도록 할 수 있다.
4. 다형성 (Polymorphism)
객체가 여러가지 형태를 가질수 있는 능력으로, 사용자의 편의성을 위해 만들어진 특징입니다.
- 오버라이딩(Overriding) : 부모 클래스의 함수를 자식 클래스의 용도에 맞게 재정의 하여 사용 하는 것.
- 오버로딩(Overloading) : 같은 함수 이름이지만, 다른 매개변수를 받는 것.
객체 지향 설계 5원칙의 장단점
객체 지향의 5원칙을 준수하며 설계를 할시, 높은 응집도와 낮은 결합성을 갖추게 됩니다.
응집도가 높은 모듈은 하나의 책임에 집중하고, 독립성이 높아져 재사용성이나, 기능의 수정, 유지보수 등이 용이해집니다.
결합성이 낮은 모듈은 의존성이 줄어들고, 이와같이 재사용성, 수정, 유지보수가 용이해집니다.
객체 지향의 단점을 표현하면, 그만큼 초기에 어떻게 설계하냐에 따라 유지보수, 결합성과 응집도가 결정되기때문에
처음에 객체 지향을 준수하며 설계를 할시에 어떻게 코딩을 해야하는지에 대한 어려움이 있습니다.
요악하자면 객체 지향의 4대 특징을 잘 녹여내어 제대로 활용한 결과를 당연히 나타낸 것이라고 할 수 있습니다.
SRP(Single Responsibility Principle) - 단일책임원칙
정의 : 객체는 단 하나의 책임만을 가져야 한다.
- 어떤 변화에 의해 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다.
- 하나의 클래스는 하나의 책임만을 가져야 한다. 즉, 클래스에 여러책임이 있는 경우 이러한 책임은 새 클래스를 통하여 분리시켜야 한다.
단일 책임 원칙은 하나의 클래스는 하나의 기능만을 담당하여 하나의 책임을 수행하는데 집중되어야 한다는 의미를 가지고 있습니다.
단일 책임 원칙을 준수하는 이유는 기능의 수정이 일어났을때의 파흡효과를 결정시키며, 만약 하나의 클래스가 여러개의 책임을 가지고 있다면 수정 시에 많은 연쇄작용이 일어날 작용이 있어 유지보수 및 수정이 힘들어집니다.
이를 방지하기 위해 하나의 클래스에 하나의 책임만 가지게 하여 한 책임으로부터 다른 책임의 변경으로의 연쇄작용에서 자유롭게 할 수 있습니다.
SRP 원칙의 잘못된 예시 및 수정 결과를 코드로 표현해 원리를 알아보겠습니다.
//SRP의 잘못된 예시
class beforeSRP {
String name;
String position;
beforeSRP(String name, String position) {
this.name = name;
this.position = position;
}
void devWorking()
{
//...
System.out.println("개발팀 업무");
//...
}
void hrWorking()
{
//...
System.out.println("인사팀 업무");
//...
}
void database() // 개발팀에서 사용할 메서드
{
//...
}
void reportHours() // 인사팀에서 사용할 메서드
{
//...
}
}
해당 beforeSRP 클래스는 개발팀과 인사팀의 업무 두가지의 책임을 가지고 있습니다.
개발팀 업무 메서드만 호출을 할 경우에도 인사팀 업무 메서드까지 호출이 되며 이것은 데이터를 효율적으로 사용하지 못하는것을 의미하며,
만약 인사팀에서 메서드를 변경 할 시에도 클래스에서는 여러개의 책임을 가지고 있기 때문에 공유하는 메서드를 사용 시에 다른 팀에게도 영향을 미칠 수 있습니다.
해당 예제 코드를 SRP 원칙을 적용하여 다시 만들어 보겠습니다.
//SRP 적용 예시
class afterSRP
{
afterSRP(String name, String position)
{
this.name = name;
this.position = position;
}
String name;
String position;
void teamWorking()
{
//...
System.out.println((String)position + "TeamWorking");
//...
}
}
class afterSRP_Dev extends afterSRP
{
afterSRP_Dev(String name, String position)
{
super(name, position);
}
void database() // 개발팀만 사용하는 메서드
{
//...
}
}
class afterSRP_Hr extends afterSRP
{
afterSRP_Hr(String name, String position)
{
super(name, position);
}
void reportHours() // 인사팀만 사용하는 메서드
{
//...
}
}
먼저 추상화를 사용하여 각 팀의 공통적인 부분을 추출하여 코드의 중복성을 제거하며, 각각의 클래스마다 재사용성을 용이하게 변경하였습니다.
또한 각각의 클래스로 분류시켜 개발 팀에서는 개발자의 역할만, 인사팀에서는인사팀의 역할만 담당하게 수정하였으며,
만일 변경 사항이 생겨도 각각 분리된 클래스에서 수정, 혹은 추가만 하면 되기 때문에 다른 클래스에 영향을 주지 않습니다.
또한 각각의 클래스는 각각의 역할만 담당시켜 다른 업무의 영향을 받지 않게되어 리펙터링 및 유지보수가 더 용이해졌습니다.
해당 예시 코드를 통하여 SRP의 중요성을 알수 있는데, 각 공통적인 메서드는 상속을 통해 코드의 중복성 제거 및 재사용성을 높이며 이것은 곧 높은 응집도, 낮은 결합성을 표현했다 볼 수 있습니다.
또한 사용하는 코드가 길어질수록 수정해야할 가능성이 높아지는데 원칙을 준수하여 프로그래밍시에 수정시의 용이함 등을 가져올 수 있습니다.
OCP(Open Close Principle) - 개방 폐쇄 원칙
정의 : 확장에 대해서는 열려있어야 하지만, 변경에는 닫혀있어야한다.
- 닫힌 부분은 버그를 수정하기 위해서만 코드를 조정해야한다(최소화).
- 새로운 기능을 도입하기 위해 확장을 통해 손쉽게 구현이 가능해야 한다.
- 기존 코드에 대한 변경을 제한하여 새로운 오류가 발생할 위험을 줄여준다.
확장이 열려 있다는건 기존 구성요소에는 수정이 일어나지 않고, 기존 구성 요소를 쉽게 확장하여 재사용이 가능하도록 만들어야 합니다.
변경이 닫혀 있다는건, 객체를 직접적으로 수정하는건 제한을 두어야 하며, 새로운 변경사항이 발생하였을 경우에는 객체를 직접적으로 수정하지 않고 변경사항을 적용할 수 있도록 설계해야합니다.
OCP 원칙은 객체지향 프로그래밍의 특징 중에서도 추상화와 다형성에 많이 관계되어있는데
아래 예시 코드를 사용하여 알아보겠습니다.
// OCP 적용 전
class beforeOCP_Animal
{
String Type;
beforeOCP_Animal(String Type)
{
this.Type = Type;
}
void bowAnimal(String Type)
{
if(Type.equals("Cat"))
{
System.out.println("야옹");
}
else if(Type.equals("Dog"))
{
System.out.println("멍멍");
}
}
void whereAnimal(String Type)
{
if(Type.equals("Cat"))
{
System.out.println("박스 안에 있습니다.");
}
else if(Type.equals("Dog"))
{
System.out.println("개집 안에 있습니다.");
}
}
}
Animal클래스를 통하여 객체를 생성시 동물의 타입을 적용하여 고양이 또는 강아지일시 각각 동물의 행동을 다르게 출력하게 예시를 적어보았습니다.
해당 코드를 실행시에는 문제가 없지만, 타입이 점점 늘어난다면 if와 else문을 타입의 종류만큼 늘어나게 될것이고, 만약 사용하던 타입을 제거시에도 제거시마다 수정이 필요하니 유지보수에 문제가 생깁니다.
해당 예제코드를 OCP원칙에 적용하여 다시 알아보겠습니다.
// OCP 적용 후
abstract class afterOCP_Animal
{
abstract void bowAnimal();
abstract void whereAnimal();
}
class afterOCP_Dog extends afterOCP_Animal
{
void bowAnimal()
{
System.out.println("멍멍");
}
void whereAnimal()
{
System.out.println("개집 안에 있습니다.");
}
}
class afterOCP_Cat extends afterOCP_Animal
{
void bowAnimal()
{
System.out.println("야옹");
}
void whereAnimal()
{
System.out.println("박스 안에 있습니다.");
}
}
먼저 시작하기에 앞서 추상화 클래스를 사용하여 코드의 중복성 제거 및 재사용성을 증가시켰으며,
다형성의 특징인 오버라이딩을 통하여 각 메서드들이 다른 행동을 가질 수 있도록 하였습니다.
또한 하위 클래스들이 추가 및 수정이 되더라도 다른 클래스들에게는 영향을 주지 않는 모습을 보여주고 있는것을 알 수 있는데 이것은 곧 결합도가 낮다는것을 의미 할 수 있고, 나아가 확장에는 열려있으며 수정에는 닫혀있는 모습을 알 수 있습니다.
LSP (Liskov Substitution Principle) - 리스코프 치환 원칙
정의 : 서브타입은 언제나 자신의 기반타입으로 교체할 수 있어야 한다.
- 하위 클래스 is a kind of 상위 클래스 - 하위 분류는 상위 분류의 한 종류이다.
- 구현 클래스 is able to 인터페이스 - 구현 분류는 인터페이스 할 수 있어야 한다.
- 하위형에서 상위형의 불변 조건은 반드시 유지되어야 한다. ex) 조류 클래스를 상속 시 반드시 부리가 있어야 한다.
- 하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는 데 문제가 없어야 한다.
즉, 리스코프 치환 원칙은 상속과 연관이 있으며 상속은 조직도나 계층도가 아닌 분류도가 돼야합니다.
// LSP 적용 예시
interface figureLSP
{
int getArea();
}
class Rect implements figureLSP
{
private int width;
private int height;
public void serWidth(int width)
{
this.width = width;
}
public void setHeight(int height)
{
this.height = height;
}
public int getWidth()
{
return width;
}
public int getHeight()
{
return height;
}
public int getArea()
{
return width * height;
}
}
class Sphere implements figureLSP
{
private float radius;
public void setHeight(float radius)
{
this.radius = radius;
}
public float getHeight()
{
return radius;
}
public int getArea()
{
return (int)(2 * radius * Math.PI);
}
}
먼저 리스코프 치한 원칙은 서브 타입은 기반 타입을 대체 할 수 있어야 하니 상속을 사용하여 구현하였습니다.
예시 코드를 확인 시에도 Rect클래스와 Sphere클래스는 상속받은 메서드 getArea를 사용 시에도 상위 클래스에 역할을 하는데 문제가 없는것을 알 수 있습니다.
이를통해 클래스들 간의 확장성을 얻을 수 있으며, 5원칙의 장점인 유지보수성이 용이한것과 낮은 결합성을 표현 한 것을 알 수 있습니다.
또한 LSP의 대표적인 사용 예시로는 컬렉션 프레임워크(Collect FrameWork)가 있습니다.
※컬렉션 프레임워크 : 데이터를 저장하는 자료 구조와 데이터를 처리하는 알고리즘을 구조화 하여 클래스로 구현한 것.
컬렉션 프레임워크의 기본적인 예시로 List,Set,Queue 자료구조들은 Collection 인터페이스를 상속받지만, 각각의 공통된 부분은 Collection 인터페이스에서 정의되고 있습니다. 이와 같이 Collection에서 분류를 하지만, 상위 인터페이스의 역할은 불변되며, 상위 분류의 한 종류지만 각각의 역할을 다르게 할 수 있음을 알 수 있습니다.
해당 내용에 관한 자료는 다음 글에서 알아보겠습니다.
ISP - 인터페이스 분리 원칙
정의 : 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다.
- 하나의 범용성 있는 인터페이스 보다 여러개의 분리된 인터페이스를 사용해야 한다.
- 인터페이스를 통해 외부에게 제공할 때는 최소한의 메서드만 제공해야 한다.
- 인터페이이스는 ~할 수 있는 (is able to) 이라는 기준으로 만들어야 한다.
인터페이스는 강제성을 가지고 있기 때문에 최소한의 인터페이스를 여러개로 분리하여 사용해야합니다.
이것은 곧 인터페이스의 멤버 수가 최소화가 되어야 한다는것을 의미한데, 이로 인해
클래스는 더 느슨한 결합과 유연성 및 재사용성을 높이기 때문에 최소한으로 분리시키며
이것을 ISP의 원칙이라 볼 수 있습니다.
인터페이스 분리 원칙은 단일 책임 원칙과 같은 문제를 다른 해결책으로 보여주는 방법이라 할 수 있습니다.
일반 사용자 클래스와 관리자 클래스는 서로 다른 책임을 가질 수 있습니다.
관리자와 일반 사용자의 공통적인 행동인 글쓰기 읽기 수정을 상속받으며, 추가로 삭제 기능을 인터페이스 분리 시킨 뒤 다중 상속을 통하여 관리자 클래스에게 부여하는 모습을 보여줄 수 있습니다.
코드를 사용하여 ISP의 적용 예시를 확인해 보겠습니다.
ISP 적용 예시
interface newMobilePhone
{
void FastCharge();
void ShareData();
void ReadQRCode();
}
interface oldMobilePhone
{
void FoldPhone();
void Antenna();
}
class SamePhone
{
String Number;
int Battery;
void Calling()
{
System.out.println("Calling...");
}
void Charge()
{
if(Battery != 100)
System.out.println("Charging Battery...");
}
void Camere()
{
System.out.println("Take a Picture!");
}
}
class OldPhone extends SamePhone implements oldMobilePhone
{
public void FoldPhone()
{
System.out.println("Folding Phone");
}
public void Antenna()
{
System.out.println("Unroll Antenna");
}
}
class NewPhone extends SamePhone implements newMobilePhone
{
public void FastCharge()
{
System.out.println("Fast Charging!!!");
}
public void ShareData()
{
System.out.println("Sharing Another User...");
}
public void ReadQRCode()
{
System.out.println("Scanning QRCode");
}
}
핸드폰을 예시로 들 수 있는데, 구형 핸드폰에만 지원하는 핸드폰이 접힌다는 기능이나 안테나, 신형 핸드폰에는 고속충전,
데이터 쉐어링, QR코드 스캔을 사용한 결제 등이 있습니다.
이것을 인터페이스로 구현시켜 각각의 기능을 분리시킨 뒤 핸드폰의 공통적인 부분을 클래스화 시켜 나타내었습니다.
이를 통해 각각의 공통적인 기능 외 필요한 기능만을 구현 시킬수 있는것을 나타낼 수 있으며 ISP의 원칙을 지켜낸 예시라고 볼 수 있습니다.
DIP - 의존관계 역전 원칙
정의 : 추상화된 것은 구체적인 것에 의존하면 안된다. 구체적인 것이 추상화된것에 의존해야한다.
- 추상화를 매개로 하여 메시지를 주고 받으면서 관계를 느슨하게 해야한다.
- 자신보다 변하기 쉬운것에 의존하지 말고 추상화된 인터페이스에 의존시켜 수정 시 상위 인터페이스 또는 클래스가 변화에 영향을 받아서는 안된다.
DIP는 고수준 모듈은 저수준 모듈의 구현에 의존되어서는 안되며, 대신 저수준 모듈이 고수준 모듈에서 정의한 추상타입에 의존하여야 합니다.
고수준 모듈인 Character가 추상화클래스인 Weapon에 의존하며, 추상화 된 계층인 Weapon은 각각의 Bow,Knife의 저수준 모듈로 나뉘어 고수준 모듈의 기능을 구현하기 위해 하위 기능을 실제 구현 하는것을 담당하게 합니다.
해당 예시를 코드로 적용하여 나타내 보겠습니다.
// DIP 적용 예시
class Character
{
Character(String Name, int Health)
{
this.Name = Name;
this.Health = Health;
}
enum status
{
Idle,
Attack,
Dead
}
String Name;
Weapon cWeapon;
int Health;
status CharacterStatus = status.Idle;
void setWeapon (Weapon cWeapon)
{
this.cWeapon = cWeapon;
}
void setCharacterStatus()
{
if(Health <= 0)
{
CharacterStatus = status.Dead;
}
}
int cAttack()
{
return cWeapon.Attack();
}
}
interface Weapon
{
int Attack();
}
class Bow implements Weapon
{
private int Damage;
void setDamage(int Damage)
{
this.Damage = Damage;
}
public int Attack()
{
return Damage;
}
}
class Knife implements Weapon
{
private int Damage;
void setDamage(int Damage)
{
this.Damage = Damage;
}
public int Attack()
{
return Damage;
}
}
Character 클래스에는 추상화된 클래스인 Weapon클래스를 통하여 각각의 하위모듈인 Bow,Knife를 받아올 수 있으며 cAttack메서드를 실행 시 Weapon의 변수인 cWeapon의 설정된 타입 및 타입의 데미지를 받아오게 하는 형식으로 구현되어 있습니다.
이것은 곧 Character클래스가 Weapon클래스에 대한 의존성을 가지게 되며, 공격 담당을 하는 cAttack메서드 또한 Weapon클래스에 의존을 하게 됩니다. 이것은 곧 구체적인 모듈이 아닌 추상적인 모듈을 의존하게 되는것을 의미합니다.
DIP의 대표적인 사용 예시를 클린 아키텍처로 볼 수 있습니다.
외부로 갈수록 변하기 쉬우며 이것에 의존하던 것을 추상화된 인터페이스나 상위 클래스를 두어 변하기 쉬운 것(외부 인터페이스)의 변화에 영향을 받지않게 하는것이 의존 역전 원칙이라 볼 수 있습니다.
정리
객체 지향 언어(OOP)는 객체를 위주로 프로그래밍 하는 기법이다.
객체 지향 언어의 특징에는 캡슐화, 상속화, 추상화, 다형성과 SOLID라 불리는 5원칙이 있다.
OOP의 특징과 원칙을 준수해야 하는 이유는 코딩을 하면 높은 응집도와 낮은 결합성을 지니며
이것은 곧 코드의 재사용성, 유지보수, 수정 등이 용이해지는 장점을 가지고있다.
OOP를 학습해야 하는 이유는 객체지향언어로 이루어져있는 언어들(Java,C#,C++ 등)을 효율적으로 사용하기 위해서
가장 먼저 학습해야하는 내용이다.
느낀점
포스팅을 하기 전, 저는 OOP를 알아보기위해 책이나 영상, 타 개발블로그를 살펴 보았습니다.
제가 최근에 읽었던 어떤 책의 저자는 객체 지향 언어를 개발한 의도를 이해하는데 있어
어떤 개발자는 5년, 어떤 개발자는 8년이 걸릴 정도로 가장 기본적이면서,
가장 중요한 개념이라고 서술을 했었던게 가장 인상깊게 느껴졌습니다.
이번 포스팅 작성을 끝으로 객체 지향 언어의 특징과 원칙을 잘 알게 되었다고 생각도 하지만,
만약 제가 면접을 보았을 시 객체 지향 언어를 개발한 의도를 정확하게 파악했다고 말할 수 없다고 생각합니다.
제가 다룰 언어인 Java를 능숙하게 다룰 수 있기위해 앞으로도 꾸준히 객체 지향 언어의 특징들을 더 상세하게
탐구 할 것이며, 그것들을 증명하기 위해 객체 지향 관련 자료들 또한 개발 블로그에 계속 추가해 나가겠습니다.
출처 및 내용 참고
완벽하게 이해하는 OOP - inpa.tistory.com
객체지향 프로그래밍이 뭔가요? - youtube.com/watch?v=vrhIxBWSJ04