객체 지향은 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용이 된다.
객체 지향의 특징으로는 추상화, 캡슐화, 상속, 다형성 이 있는데 이 중에서 가장 중요한 것이 다형성이다.
다형성을 좀더 쉽게 알기 위해서 실세계와 비유해보겠다.
실세계와 객체 지향을 1:1로 매칭 하면 안 되고 역할과 구현으로 세상을 구분해보겠다.
Ex1. 운전자와 자동차
운전자는 자동차를 운전할 때 면허증만 있으면 운전이 가능하다. 이때 자동차의 기종이 무엇이든 상관없이 운전이 가능하다. 아직 감이 안 오면 다른 예시를 들어보자
Ex2. 공연
남자 주인공과 여자 주인공을 정해야 한다. 이때 주인공들을 역할이고 그 배우들은 구현이라고 해보자. 남자 배우가 누가 오든 여자 배우가 누가 오든 공연을 진행할 수 있으면 상관이 없다. 각자의 역할만 충분하게 해 주면 된다.
Ex3. 요리
요리를 할 때 재료와 사람이 필요하다. 그럼 누가 역할이고 구현일까? 재료가 구현이고 역할은 사람이다. 레시피와 재료가 있으면 어떤 사람이 오든 요리를 완성시킬 수가 있다.
그래서 다형성이 무엇인가요?
"하나의 객체가 여러 가지 타입을 가질 수 있는 것을 의미합니다"
다형성의 본질
- 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경 수 있다.
- 다형성의 본질을 이해하려면 협력이라는 객체 사이의 관계에서 시작해야 한다.
- 클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있다.
여기까지가 Java에 다형성을 이야기한 것이다. 그렇다면 스프링에서의 다형성은 무엇을 의미할까?
제어의 역전(IoC), 의존관계 주입(DI)은 다형성을 활용해서 역할과 구현을 편리하게 다룰 수 있도록 지원한다.
일단 객체 지향 설계의 5가지 원칙에 대해서 알아보고 다형성에 대해 알아보겠다.
객체 지향 설계의 5가지 원칙(SOLID 원칙)
SRP : 단일 책임 원칙 (single responsibility principle)
OCP : 개방 폐쇄 원칙 (Open/closed principle)
LSP : 리스 코프 치환 원칙 (Liskov substitution principle)
ISP : 인터페이스 분리 원칙 (Interface segregation principle)
DIP : 의존관계 역전 원칙 (Dependency inversion principle)
이렇게 객체 지향 설계의 5가지 원칙이 있는데 앞에 한 글자씩을 가져와 SOLID 원칙으로 불리고 있는 것이다.
SRP : 단일 책임 원칙 (single responsibility principle)
한 클래스는 하나의 책임만 가져야 한다.
소스 파일에 다양하고 많은 메서드를 포함하면 병합이 자주 발생한다. 특히 메서드가 서로 다른 액터를 책임진다면 병합이 발생할 가능성이 더 높아진다. 이를 해결하기 위해서는 서로 다른 액터를 뒷받침하는 코드를 분리하는 것이다.
// User 클래스는 SRP를 따르지 않는다.
class User {
private db: Database;
private name: string;
private birth: Date;
constructor(name: string, birth: Date) {
this.db = Database.connect();
}
getUser() {
return this.name + "(" + this.birth + ")";
}
save() {
this.db.users.save({ name: this.name, birth: this.birth });
}
}
User 클래스는 User에 관한 책임만 가져야 한다. 즉, User 클래스는 사용자 모델과 관련된 속성을 정의해야 한다. 하지만 데이터 접근 기능과 저장 기능까지 정의하고 있다. 다시 말해 단일 책임 원칙을 따르지 않고 있다고 할 수 있다.
// SRP를 만족한다.
class User {
constructor(private name: string, private birth: Date) {}
getUser() {
return this.name + "(" + this.birth + ")";
}
}
class UserRepository {
private db: Database;
constructor() {
this.db = Database.connect();
}
save(user: User) {
this.db.users.save(JSON.stringify(user));
}
}
코드를 분리하여 수정해보았다. 이제 User클래스는 데이터 모델과 관련된 속성을 정의하는 책 만만 존재하게 되므로 SRP를 만족하게 된다.
단일 책임 원칙을 제대로 지키면 변경이 필요할 때 수정할 대상이 명확해진다. 그리고 이러한 단일 책임 원칙의 장점은 시스템이 커질수록 서로 많은 의존성을 갖게 되는 상황에서 변경 요청이 오면 딱 1가지만 수정하면 되기 때문이다.
단일 책임 원칙을 적용하여 적절하게 책임과 관심이 다른 코드르 분리하고, 서로 영향을 주지 않도록 추상화함으로써 애플리케이션의 변화에 손쉽게 대응할 수 있다.
OCP : 개방 폐쇄 원칙 (Open/closed principle)
소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
확장에 열려 있다 : 요구사항이 변경될 때 새로운 동작을 추가하여 애플리케이션의 기능을 확장할 수 있다.
수정에 대해 닫혀 있다: 기존의 코드를 수정하지 않고 애플리케이션의 동작을 추가하거나 변경할 수 있다.
// OCP를 만족한다.
class Card {
private code: String;
private expiration: Date;
protected monthlyCost: number;
constructor(code: String, expiration: Date, monthlyCost: number) {
this.code = code;
this.expiration = expiration;
this.monthlyCost = monthlyCost;
}
getCode(): String {
return this.code;
}
getExpiration(): Date {
return this.expiration;
}
monthlyDiscount(): number {
return this.monthlyCost * 0.02;
}
}
/** * 골드 카드 확장 */
class GoldCard extends Card {
monthlyDiscount(): number {
return this.monthlyCost * 0.05;
}
}
/** * 실버 카드 확장 */
class SilverCard extends Card {
monthlyDiscount(): number {
return this.monthlyCost * 0.03;
}
}
월 할인은 카드 유형에 따라 달라지는데 계산을 변경하려면 새로운 카드 유형의 클래스를 만들고 Card 클래스를 상속받아 재정의 하는 것이다. 여기서 객체지향의 특징인 다형성까지 볼 수가 있다.
LSP : 리스 코프 치환 원칙 (Liskov substitution principle)
상호 대해 가능한 객체가 있을 때 서로 치환하더라도 프로그램의 행위가 변하지 않아야 한다
- 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
- 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다는 것, 다형성을 지원하기 위한 원칙, 인터페이스를 구현한 구현체는 믿고 사용하려면, 이 원칙이 필요하다.
// LCP을 만족한다.
abstract class Address {
addressee: string;
country: string;
postalCode: string;
city: string;
street: string;
house: number;
abstract writeAddress(): string;
}
class KoreaAddress extends Address {
writeAddress(): string {
return "Formatted Address Korea" + this.city;
}
}
class UKAddress extends Address {
writeAddress(): string {
return "Formatted Address UK" + this.city;
}
}
class USAAddress extends Address {
writeAddress(): string {
return "Formatted Address USA" + this.city;
}
}
// PrintAddress 메서드에서 받을 파라미터는 치환이 가능하다.
class AddressWriter {
PrintAddress(writer: Address): string {
return writer.writeAddress();
}
}
나라별 주소를 표기하는 법이 모두 다르기 때문에 Address 추상 클래스를 상속받아 재정의하여 구현을 강제하고 있다.
여기서 리스 코프 치환 법칙을 만족하는 코드를 볼 수 있다. 즉, 다양한 나라의 클래스가 와도 프로그램의 행위가 변하지 않게 된다. 여기서도 다형성을 확인할 수가 있다.
ISP : 인터페이스 분리 원칙 (Interface segregation principle)
특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다
객체가 충분히 높은 응집도의 작은 단위로 설계됐더라도, 목적과 관심이 각기 다른 클라이언트가 있다면 인터페이스를 통해 적절하게 분리해줄 필요가 있다. 인터페이스 분리 원칙을 준수함으로써 모든 클라이언트가 자신의 관심에 맞는 퍼블릭 인터페이스(외부에서 접근 가능한 메시지)만을 접근하여 불필요한 간섭을 최소화하며, 기존 클라이언트에 영향을 주지 않은 채로 유연하게 객체의 기능을 확장하거나 수정할 수 있다.
// ISP를 만족하지 않는다.
interface Printer {
copyDocument();
printDocument(document: Document);
stapleDocument(document: Document, tray: Number);
}
// 사용되지 않는 메서드를 구현하고 있다.
class SimplePrinter implements Printer {
public copyDocument() {} // 사용 하지 않음
public printDocument(document: Document) {
console.log("simple copy", document);
}
public stapleDocument(document: Document, tray: Number) {} // 사용 하지 않음
}
// ISP를 만족한다.
interface Printer {
printDocument(document: Document);
}
interface Stapler {
stapleDocument(document: Document, tray: number);
}
interface Copier {
copyDocument();
}
class SimplePrinter implements Printer {
public printDocument(document: Document) {}
}
class SuperPrinter implements Printer, Stapler, Copier {
public copyDocument() {}
public printDocument(document: Document) {}
public stapleDocument(document: Document, tray: number) {}
}
불필요한 기능에 의존하지 않게 되어 훨씬 깔끔한 코드가 되었다. 불필요한 기능 때문에 그 기능이 변경되면 의존하는 또 다른 코드는 재컴파일과 재배포를 할 수 있기 때문이다.
DIP : 의존관계 역전 원칙 (Dependency inversion principle)
추상화에 의존해야지, 구체화에 의존하면 안 된다.
쉽게 이야기해서 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻이다. DIP를 만족시키기 위해 고수준에서 저수준을 의존하는 것을 인터페이스를 통해 역전시킨다.
// DIP를 만족한다.
// 인터페이스를 활용하여 고수준에서 저수준을 의존하는 것을 역전시킨다.
interface IWindow {
open();
close();
}
class CarWindow implements IWindow {
open() {
//...
}
close() {
//...
}
}
// 고수준인 WindowSwitch는 저수준인 CarWindow를 더 이상 의존하지 않는다.
class WindowSwitch {
private isOn = false;
constructor(private window: IWindow) {}
onPress() {
if (this.isOn) {
this.window.close();
this.isOn = false;
} else {
this.window.open();
this.isOn = true;
}
}
}
참고 자료
https://bbaktaeho-95.tistory.com/98
https://mangkyu.tistory.com/194
'Spring' 카테고리의 다른 글
싱글톤 컨테이너 (0) | 2022.07.10 |
---|---|
스프링 컨테이너와 스프링 빈 (0) | 2022.07.10 |
Spring 과 Spring boot (0) | 2022.06.01 |
JpaReporitory vs EntityManager (0) | 2021.11.23 |
엔티티 설계 주의점 (0) | 2021.11.21 |