[Java] 객체지향 5대 원칙 SOLID

2020. 11. 2. 15:34Language/Java

1. 객체지향 5대 원칙

객체지향에는 5가지 원칙이 존재한다. 이 원칙은 프로그래머가 시간이 지나도 유지 보수와 확장이 쉬운 소프트웨어를 만드는데 이 원칙들을 적용할 수 있다.

  • SRP (단일 책임 원칙)
  • OCP (개방-폐쇄 원칙)
  • LSP (리스코프 치환 원칙)
  • DIP (의존 역전 원칙)
  • ISP (인터페이스 분리 원칙)

 

 

2. Single Responsibility Principle (단일 책임 원칙)

"소프트웨어의 설계 부품(클래스, 함수 등)은 단 하나의 책임만을 가져야 한다."

1) 단일 책임 원칙

여기서 말하는 책임의 깁곤 단위는 객체를 의미하며 하나의 객체가 하나의 책임을 가져야 한다. 책임이란 객체가 할 수 있는 것과 해야하는 것으로 나뉜다. 즉, 하나의 객체는 자신이 할 수 있는 것과 해야하는 것만 수행 할 수 있도록 설계되어야 한다.

 

SRP를 지켜야하는 이유는 응집도와 결합도 관점에서 접근하면 된다. 응집도란 한 프로그램 요소가 얼마나 뭉쳐있는가를 나타내는 척도이며 결합도는 프로그램 구성 요소들 사이가 얼마나 의존적인지를 나타내는 척도이다.

2) 예제

예제에서는 학생이라는 클래스는 수강과목을 조회하고 추가하고 데이터베이스에 저장하고 저장된 학생을 불러오고 기록을 출력하는 책임을 담당하고 있다. 이 예제에는 하나의 클래스가 다양한 책임을 갖기 때문에 변경이라는 관점에서 문제가 발생한다.

 

잘 설계된 프로그램은 새로운 요구사항이나 변경이 있을 대 가능한 영향 받는 부분을 최소화해야한다. 하지만 학생 클래스에서는 데이터베이스와 과련된 활동 출력과 관련된 활동이 일어났을 때, 강좌를 조회하고 추가할 때 모두 변경의 대상이 된다. 즉, 변화에 민감한 클래스가 되어버린다.

 

또 다른 문제점으로는, 현재 수강과목을 조회하는 부분과 데이터베이스에서 학생 정보를 가져오는 코드는 어딘가에서 연결될 확률이 높다. 이러한 코드끼리의 결합은 하나의 변화에 대해 많은 변경사항을 발생시키고 관련된 모든 기능을 테스트해봐야하는 단점이 있다. 즉, 유지 보수 하기 어려운 대상이 된다. 따라서 각 객체는 하나의 책임만을 수행할 수 있도록 변경해야 한다.

 

이 클래스의 경우에는 학생 - 학생 DAO - 성적표 - 출석부 등의 클래스를 통해 쪼개어 관리하는 것이 변경에 유연하게 대처할 수 있다.

public class Student {
    public void getCourse(){	}
    public void addCourse() {	}
    public void save(){	}
    public Student load() {	}
    public void printOnReportCard() {	}
    public void printOnAttendanceBook() {	}
}

 

 

 

3. Open-Closed Principle (개방-폐쇄 원칙)

"기존의 코드를 변경하지 않고(Closed) 기능을 수정하거나 추가할 수 있도록(Open) 설계해야 한다."

1) 개방-폐쇄 원칙

이 원칙은 간단히 기존의 코드를 변경하지 않고 기능을 추가할 수 있도록 설계해야한다. OCP에서 중요한 것은 요구사항이 변경되었을 때 코드에서 변경되어야 하는 부분과 변경되지 않아야하는 부분을 명확하게 구분하여, 변경되어야 하는 부분을 유연하게 작성하는 것을 의미한다. 이를 위해 자주 사용되는 문법은 인터페이스이다.

2) 예제

SoundPlayer는 음악을 재생해주는 클래스이고 기본적으로 wav 파일을 재생할 수 있다. 여기서 만약 다른 파일 형식 mp3 파일을 재생하도록 요구사항이 변경 되었을 경우, SoundPlayer의 play 메서드를 수정해야한다.

class SoundPlayer {
	
    void play() { System.out.println("play wav"); }
}

public class Client {
	
    public static void main(String[] args) {
    	
        SoundPlayer sp = new SoundPlayer();
        sp.play();
    }
}

이 예제에서 OCP를 만족시키기 위해서는 play 메서드를 인터페이스로 분리하면 된다. SoundPlayer 클래스에서는 playAlgorithm 인터페이스를 멤버 변수로 만들고 play() 함수에는 인터페이스를 상속받아 구현된 클래스의 play() 함수를 실행하도록 한다.

interface playAlgorithm {
	
    public void play();
}

class Wav implements playAlgorithm {
    @Override
    void play() { System.out.println("play wav"); }
}

class Mp3 implements playAlgorithm {
    @Override
    void play() { System.out.println("play Mp3"); }
}

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();
    }
}

※ 이와 같은 설계를 Strategy Pattern(전략 패턴)이라고 한다.

 

 

 

4. Liskov Substitution Principle (리스코프 치환 원칙)

"자식 클래스는 부모클래스에게 가능한 행위를 수행할 수 있어야 한다."

1) 리스코프 치환 원칙

이 원칙은 리스코프 교수가 제안한 설계 원칙으로 부모 클래스와 자식 클래스 사이의 행위에는 일관성이 있어야 한다는 원칙이다. 객체 지향 프로그래밍에서 부모 클래스의 인스턴스 대신 자식 클래스의 인스턴스를 사용해도 문제가 없어야 한다는 의미이다.

 

상속 관계에서는 일반화 관계(IS-A)가 성립해야한다. 일반화 관계에 있다는 것은 일관성이 있다는 뜻이다.

2) 예제 (1)

도형 클래스를 정의했을 때의 조건을 생각해보면 쉽다.

  • 도형은 둘레를 가지고 있다.
  • 도형은 넓이를 가지고 있다.
  • 도형은 각을 가지고 있다.

도형 클래스를 상속받는 사각형 클래스의 일반화 관계는 단어를 교체해보면 확인할 수 있다.

  • 사각형은 둘레를 가지고 있다.
  • 사각형은 넓이를 가지고 있다.
  • 사각형은 각을 가지고 있다.

반대로 원 클래스의 경우, 원이 각을 가지고 있지 않기 때문에 LSP를 만족하지 않은 설계가 된다.

3) 예제 (2)

다른 예제에서 가방과 할인된 가방의 관계를 생각했을 때, Bag를 사용하고 있는 부분을 DiscountedBag로 대체해도 위반이 되지 않는다. 부모의 기능을 오버라이딩하지 않고 그대로 사용하고 있기 때문에 일반화 관계가 성립된다. 하지만 DiscountedBag에는 applyDiscount 기능이 있기 때문에 부모와 자식은 대체관계가 되지 않는다. 즉, 자식 클래스가 부모 클래스를 오버라이딩하거나 추가적인 기능을 통해 부모의 상태를 변경시키는 것은 LSP를 위반하는 것이다.

public class Bag {
	
    private double price;
    
    public double getPrice() {
        return price;
    }
    
    public void setPrice(double price) {
        this.price = price;
    }
}
public class DiscountedBag extends Bag{
	
    private double discountRate;
    
    public void setDiscountRate(double discountRate) {
        this.discountRate = discountRate;
    }
    
    public void applyDiscount(int price) {
        super.setPrice(price- (int)(discountRate * price));
    }
}

 

 

 

5. Dependency Inversion Principle (의존 역전 원칙)

"의존 관계를 맺을 때, 변화하기 쉬운 것 보단 변화하기 어려운 것에 의존해야 한다는 원칙이다."

1) 의존 역전 원칙

변화하기 쉬운 것이란 구체적인 것을 뜻하고, 변화하기 어려운 것이란 추상적인 것을 의미한다. 객체 지향적인 관점에서 볼때, 변화하기 쉬운 것은 구체화 된 클래스를 의미하고, 변화하기 어려운 것은 추상 클래스나 인터페이스를 의미한다. 즉, DIP를 만족한다는 것은 의존관계를 맺을 때, 구체적인 클래스보다 인터페이스나 추상 클래스와 관계를 맺는다는 것을 의미한다.

2) 예제

setFile 함수를 통해 실행하고자 하는 파일을 쉽게 바꿀 수 있듯, 새로운 오디오 파일 포맷을 실행시키고자 한다면 새로운 클래스를 만들고 play 인터페이스를 상속받아 구현한 뒤 setFile을 이용하여 file 멤버 변수에 주입시키면된다. (의존성 주입)

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();
    }
}

 

 

 

6. Interface Segregation Principle (인터페이스 분리 원칙)

" 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다. 하나의 일반적인 인터페이스보다는, 여러 개의 구체적인 인터페이스가 낫다."

1) 인터페이스 분리 원칙

이 원칙은 결국 자신이 사용하지 않는 기능에는 영향을 받지 말아야 한다는 의미이다. 이 원칙은 인터페이스를 클라이언트에 특화되도록 분리시키라는 설계 원칙이라고 할 수 있다.

2) 예제

복합기를 이용하는 다양한 사람을 생각해보면 쉽다. 복사를 하고 싶은 사람과 프린트를 하고 싶은 사람, 팩스를 보내고 싶은 사람은 복합기가 다양한 기능을 제공하지만 본인이 원하는 기능만이 작동하면 되며 자신이 이용하지 않는 기능에 대해서는 영향을 받지 않는다. 이러한 기능을 제공하고 싶을 때 사용되는 것이 ISP이며 사용 방법은 범용의 인터페이스를 만드는 것이 아닌 클라이언트에 특화된 인터페이스를 사용해야한다.


[참고] dev-momo.tistory.com/entry/SOLID-%EC%9B%90%EC%B9%99

[참고] velog.io/@kyle/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-SOLID-%EC%9B%90%EC%B9%99-%EC%9D%B4%EB%9E%80

728x90

'Language > Java' 카테고리의 다른 글

[Java] JVM, JDK (JRE), JAR vs WAR  (0) 2021.04.22
[Java] LocalDate, LocalTime, LocalDateTime  (0) 2021.04.03
[Java] 명명 규칙  (0) 2020.07.21
[Java] Process, Thread  (0) 2020.07.13