객체지향 SOLID 의 원칙


객체지향 SOLID 다섯가지 원칙을 나름대로 정리해보려고 한다.
공부하면서 느낀건데 단순 설명보다는 역시 예제코드가 있어야 이해가 빠르게 되더라.
이에 각 원칙 별 설명과 예제코드를 첨부한다.

SRP(Single Responsibility Principle) - 단일 책임의 원칙

하나의 객체는 하나의 책임만을 가져야함.

public class Student{
    public void getCourses(){..}
    public void addCourse(Course c){..}
    public void save(){...}
    public Student load() {...}
    public void printOnReportCard(){..}
    public void printOnAttendanceBook(){..}
}

위와같이 Student Class가 너무나 많은 책임을 수해앻야하는것은 SRP에 위배된다.
Student Class에 할당된 책임 중에 가장 잘할 수 있는 것은 수강과목을 추가하고 조회하는일이다. 데이터베이스에 학생정보를 저장하거나 불러오는 일이나 성적표, 출석부에 출력하는 일은 다른 Class가 더 잘할 여지가 많다.

public class Student{
    public void getCourses(){..}
    public void addCourse(Course c){..}
}

이와같이 Student Class는 수강과목 추가, 조회 책임만 수행하게하고 다른 일들은 StudentDAO class, 성적표 class, 출석부class 등을 만들어 수행하게 하는것이 객체간의 결합을 느슨하게하고 기능의 수정 및 추가를 용이하게 만든다.

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

기존의 코드를 변경하지 않으면서 기능을 추가할 수 있도록 설계가 되어야함.
클래스를 변경하지 않고도(Closed) 대상 클래스의 환경을 변경할 수 있는(Open) 설계가 되어야함.

public class TimeReminder{
    private MP3 m;
    
    public void reminder(){
        Calendar cal = Calendar.getInstance();
        m = new MP3();
        int hour = cal.get(Calendar.HOUR_OF_DAY);
        
        if (hour >= 22){
            m.playSong()
        }
    }

}

이 코드를 제대로 동작하는지 테스트하려면 22시까지 기다려야한다. OCP를 적용하여 아래와같이 설계한다면 TimeReminder 클래스를 전혀 수정하지않고 시간을 설정해 테스트할 수 있다.


public interface TimeProvider { // interface 도입
    public void setHours(int hours);
    public int getTime();
}

public class FakeTimeProvider implements Timeprovider{  // Timeprovider test stub
    private Calendar cal;

    public FakeTimeProvider(){
        cal = Calendar.getInstance();
    }

    public FakeTimeProvider(int hours){
        cal = Calendar.getInstance();
        setHours(hours)
    }

    public void setHours(int hours){
        cal.set(Calendar.HOUR_OF_DAY, hours); // 주어진시간으로 설정
    }

    public int getTime(){
        return cal.get(Calendar.HOUR_OF_DAY); // 현재 시간 반환
    }
}

public class TimeReminder{
    TimeProvider tProv;
    private MP3 m;

    public void setTimeProvider(TimeProvider tProv){
        this.tProv = tProv; // test stub 혹은 실제 시간 제공 instance 주입
    }
    
    public void reminder(){
        int hours = tProv.getTime();
        if (hours >= 22){
            m.playSong();
        }
    }
}


public static void main(String[] args){
    sut = new TimeReminder();
    tProvStub = new FakeTimeProvider();
    tProvStub.setHours(18);
    sut.setTimeProvider(tProvStub);
}

LSP(Liskov Substitution Principle) - 리스코프 치환 원칙

자식클래스는 최소한 자신의 부모 클래스에서 가능한 행위는 수행할 수 있어야 함.
부모클래스와 자식클래스 사이의 행위가 일관성이 있어야 한다는 의미.
슈퍼클래스의 메서드를 새로운기능으로 override하면 이 원칙에 위배됨.

public class Bag{
    private int price;

    public void setPrice(int price){
        this.price = price;
    }

    public int getPrice(){
        return price;
    }
}
public class DiscountedBag extends Bags{
    private double discountedRate = 0;

    public void setDiscounted(double discountedRate){
        this.discountedRate = discountedRate;
    }

    public void applyDiscount(int price){
        super.setPrice(price - (int)(discountedRate * price));
    }
}

DiscountedBag 클래스는 할인율을 설정해 할인된 가격을 계산하는 기능이 추가되었다.
기존의 Bag class에 있던 가격설정/조회기능은 변경없이 그대로 상속받았다.
Bag class의 메서드들이 DiscountedBag class에서 재정의되지않았으므로 가격설정/조회 기능을 그대로 이용할 수 있다.
따라서 두 클래스의 상속관계는 LSP를 위반하지 않는다.

DIP(Dependency Inversion Principle) - 의존 역전의 원칙

객체간 의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것보다는 변화하기 어려운것, 거의 변화가 없는 것에 의존하라는 원칙이다.

public class Kid{
    private Toy toy;

    public void setToy(Toy toy){
        this.toy = toy;
    }

    public void play(){
        System.out.println(toy.toString());
    }
}

이때 실제 가지고 노는 구체적인 장난감은 변하기 쉬운것이고,
아이가 장난감을 가지고 노는 사실은 변하기 어려운 것이다. 따라서 Toy를 Kid의 객체로 선언해주고, (Kid는 Toy에 의존)
외부에서 setToy로 객체를 주입해 장난감의 종류를 바꿀 수 있다.

public class Main{
    public static void main(String[] args){
        Toy t = new Robot();
        Kid k = new Kid();
        k.setToy(t);
        k.play();
    }
}

이와같이 DIP를 만족하면 의존성주입(DI:Dependency Injection) 기술로 변화를 쉽게 수용할 수 있는 코드를 작성할 수 있다.

ISP(Interface Segregation Principle) - 인터페이스 분리 원칙

클라이언트 자신이 이용하지 않는 기능에 영향을 받지 않도록 인터페이스를 분리시키는 설계 원칙이다.

30_1 이와같이 프린터, 팩스, 복사기 기능이 모두 포함된 복합기 클래스는 매우 비대하지만
이 비대한 클래스의 모든 기능을 client가 동시에 사용하지는 않는다.
이를테면 프린터 클라이언트는 print 기능만 사용한다.
따라서 아래와 같이 인터페이스를 client에 특화되도록 분리시켜야한다.

30_2 이제 clinet들은 자신이 사용하지 않는 메서드들에게 영향을 받지 않아도 된다.




© 2020. by berrrrr

Powered by berrrrr