본문 바로가기

JAVA

객체지향의 5대 원칙 SOLID

객체지향언어인 JAVA를 공부하면서 한번쯤 들어봤을 법한 SOLID 5대원칙에대해서 포스팅 하고자 한다.

 

SOLID 원칙이란 클린코드의 저자로 알려진 로버트 마틴이 정의한 객체지향 프로그래밍 및 설계의 다섯가지 기본 원칙을 마이클 페더스가 소개한것이다.

 

우리가 개발함에 있어서 자신이 짠 코드 혹은 남이 짠 코드를 유지보수 해야하는 상황에 어떻게 하면 효율적으로 업무를 수행 할 수 있는지에 대한 해결책으로 제시된 원칙이다.

 

SOLID 5대원칙은 다음과 같다.

 

  1. SRP(Single reponsibility principle) 단일 책임 원칙
  2. OCP(Open/Close principle) 개방-폐쇄 원칙
  3. LSP(Liskov subsititution principle) 리스코프 치환 원칙
  4. ISP(Interface segregation principle) 인터페이스 분리 원칙
  5. DIP(Dependency inversion principle) 의존관계 역전 원칙

 

1. SRP (Single reponsibility principle) 단일 책임 원칙


한 클래스는 하나의 책임만 가져야 한다.

 

SRP 원칙은 말그대로 한 클래스는 하나의 책임만 가져야한다는 의미이다.

 

여기서 책임이란 객체가 수행하는 '기능' 정도의 의미라고 생각한다. 하지만 책임이라는 표현이 상황에 따라 문맥에 따라 광범위 하므로 애매함이 느껴지는 표현 같다.

 

다른 표현으로 변화에 유연한 즉, 요구사항에 변화가 있을시 코드에 파급력이 적다면 SRP를 잘 준수했다고 할 수 있다.

 

 

public class Student{
    public void getCource(){...}
    public void addCource(){...}
    public void save(){...}
    public Student load(){...}
    public void printOnReportCard(){...}
    public void printOnAttendanceBook(){...}
}

 

위의 예제에서는 학생이라는 클래스 안에 수강과목을 조회하는 getCource() 메소드, 수강과목을 추가하는 addCource() 메소드, DB에 저장하고, 불러오는 save(), load() 메소드가 있다.

 

이는 하나의 클래스안에 많은 책임을 가지고 있는데, 이경우 변경사항이 있을경우 문제가 발생할 확률이 높다.

 

위 클래스 같은 경우 서로 다른 역할을 수행하는 코드끼리 강하게 결합된다. 예를들어 현재 수강중인 수강과목을 불러오는 메소드와 DB에서 학생정보를 가져오는 메소드는 어딘가에서 연결될 확률이 높다.

 

이러한 상황은 StudentDto - StudentDao - ReportCardDto - AttendanceBookDto 등의 클래스로 쪼개어 관리하는 것이 추후에 발생할 변경사항에 유연하게 대처할 수 있다.

 

 

2. OCP(Open/Close principle) 개방-폐쇄 원칙


소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.

 

OCP 원칙은 기존에 있는 코드를 변경하지 않으면서 기능을 추가할 수 있어야 한다는 원칙이다. 

 

마찬가지로 요구사항이 변경되었을 경우 코드를 변경하지 않으면서 기능을 추가할 수 있어야 한다.

 

public class Sonata{
    public void ride(){
    	System.out.println("ride sonata");
    }
}

public class Avante{
    public void ride(){
    	System.out.println("ride Avante");
    }
}

public class Client{
    Sonata sonata = new Sonata();
    //Avante avante = new Avante();
    public void ride(){
    	sonata.ride();
        //avante.ride();
    }
}

public class Main{
    public static void main(String[] args){
    	Client client = new Client();
        client.ride();
    }
}

 

차를 구매하고 싶은 고객이 있다. 이고객은 소나타를 탈수도 있고, 아반떼를 탈수도 있다. 

 

Client 클래스의 주석을 보면 소나타를 타는 상황에서 아반떼를 타는 상황으로 변경된 경우 ride() 메소드를 수정해야 하는 경우가 발생한다. 이는 OCP 원칙에 위배된다고 할 수 있다.

 

우리는 객체지향의 다형성을 이용하여 이를 해결할 수 있다. 하나의 interface를 추가하자

 

public interface Car{
    public void ride();
}

public class Sonata implements Car{
    public void ride(){
    	System.out.println("ride sonata");
    }
}

public class Avante implements Car{
    public void ride(){
    	System.out.println("ride Avante");
    }
}

public class Client{
    Car car = new Sonata();
    //Car car = new Avante();
    
    public void ride(){
    	car.ride();
    }
}

public class Main{
    public static void main(String[] args){
    	Client client = new Client();
        client.ride();
    }
}

 

Car 인터페이스를 추가하여 Client 클래스에서 어떤 차를 탈지 선택하는 부분이 이전의 코드와 다르게 하나의 인터페이스로 관리가 가능한 것을 확인할 수 있다.

 

하지만 이러한 방법도 사실은 아반떼를 타고싶은 상황에서 주석처리 한것과 같이 코드의 수정이 불가피한 것을 알 수 있다. OCP 원칙을 완전히 지킨것은 아니다.

 

이러한 경우 전략패턴(Strategy Pattern)을 사용해서 보완할 수 있다.

 

public interface Car{
    public void ride();
}

public class Sonata implements Car{
    public void ride(){
    	System.out.println("ride sonata");
    }
}

public class Avante implements Car{
    public void ride(){
    	System.out.println("ride Avante");
    }
}

public class Client{
    private Car car;
    
    public void setCar(Car car){
    	this.car = car;
    }
    
    public void ride(){
    	car.ride();
    }
}

public class Main{
    public static void main(String[] args){
    	Client client = new Client();
        client.setCar(new Sonata());
        //client.setCar(new Avante());
        client.play();
    }
}

 

Client 클래스에서 Car 인터페이스를 구현하는 클래스가 추가되어도 Client 클래스의 코드는 수정하지 않아도 사용할수 있다는걸 알 수 있다.

 

하지만 Main 클래스의 주석을 살펴보면 결국 우리가 사용하는 기능을 사용하려면 코드의 수정이 불가피 하다는 것을 알 수 있다.

 

이는 Spring 프레임워크를 사용해 보면서 다시 다뤄보도록 하겠다.

 

3. LSP(Liskov subsititution principle) 리스코프 치환 원칙


프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.

 

LSP 원칙은 리스코프 교수가 제안한 원칙으로 부모와 자식클래스 사이의 행위에는 일관성이 있어야 한다는 의미이다.

 

도형 클래스와 도형 클래스를 상속받는 사각형 클래스가 있다고 가정해보자.

 

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

도형 클래스를 상속받는 사각형 클래스로 주어를 교체해보면 LSP를 확인할 수 있다.

 

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

3가지의 상황 모두 어색하지 않다. 이번엔 주어를 원으로 바꾸어서 생각해보면

 

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

3번째 상황의 경우 뭔가 어색하다는 것을 느낄 수 있다. 이는 LSP 원칙을 위배한 상황으로 설계를 고려해봐야 하는 것이다. 

 

사실 고객의 요구사항의 경우 상황과 문맥에 따라 많은 경우의 수가 생기기 때문에 LSP 원칙에서 말하는 일반화 관계(IS-A)의 상황이 쉽게 정의되지 않는다고 생각한다. 따라서 LSP 원칙에 대해서는 따로 코드를 작성하지 않으려고 한다.

 

4. ISP(Interface segregation principle) 인터페이스 분리 원칙


특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스 하나보다 낫다.

ISP 원칙이란 클라이언트에서 자신이 사용하지 않는 기능에대해서 영향을 받지 않아야 한다는 말이다.

 

즉 SRP원칙과 ISP원칙은 같은 문제점에 대해서 다른 해결책을 제시하는 것이다.

 

SRP는 책임과 역할로 나누어 클래스를 정의하고 ISP는 인터페이스를 쪼개어 다양한 기능을 인터페이스화 함으로써 클라이언트에서 인터페이스를 사용할 때 사용하지 않는 기능에 대해서는 영향을 받지 않아야 하는것이다.

 

예를들어 이벤트 게시판의 CRUD 메소드를 제공하는 클래스가 있다고 가정하자. 

그러나 클라이언트에 따라서 게시판의 기능 중 일부분만 사용할 수 있고, 관리자는 모든 기능이 사용 가능하다고 하자.

이러한 경우 관련된 책임을 수행하므로 SRP 원칙에는 위배되지 않지만 만약 클라이언트와 관리자가 이 클래스가 구현하고있는 인터페이스를 동일하게 사용한다면 ISP 원칙에는 위배되는것이다.

사용자 인터페이스와 관리자 인터페이스를 나누어 ISP 원칙을 지킬 수 있다.

 

5. DIP(Dependency inversion principle) 의존관계 역전 원칙


프로그래머는 '추상화에 의존해야지, 구체화에 의존하면 안된다.'
의존성 주입은 이 원칙을 따르는 방법중 하나이다.

추상화란 변화하기 어렵고 추상적인것 즉, 추상 클래스나 인터페이스를 말한다.

여기서 말하는 구체화란 변화하기 쉬운것 즉, 추상 클래스나 인터페이스를 구현하는 클래스를 말한다.

 

따라서 DIP원칙을 만족한다는 것은 의존관계를 맺을 때, 구현체인 클래스보다 인터페이스나 추상 클래스와 관계를 맺는다는 것을 의미한다.

 

OCP 원칙에서 다루었던 코드를 다시 살펴보면

 

public interface Car{
    public void ride();
}

public class Sonata implements Car{
    public void ride(){
    	System.out.println("ride sonata");
    }
}

public class Avante implements Car{
    public void ride(){
    	System.out.println("ride Avante");
    }
}

public class Client{
    private Car car;
    
    public void setCar(Car car){
    	this.car = car;
    }
    
    public void ride(){
    	car.ride();
    }
}

public class Main{
    public static void main(String[] args){
    	Client client = new Client();
        client.setCar(new Sonata());
        //client.setCar(new Avante());
        client.play();
    }
}

 

Client 클래스를 살펴보면 Car 인터페이스를 '의존성 주입' 이라는 기술로 변화에 유연한 설계를 할 수 있다. 

또한 setCar() 메소드로 Car 인터페이스를 구현하고 있는 클래스들을 쉽게 교체하여 사용이 가능하다.

 

즉 setCar() 메소드를 통해서 Car 인터페이스의 구현체를 car 멤버변수에 주입시키는 것이다. 

 

이처럼 OCP, DIP 원칙을 만족하기위한 설계를 위하여 위와 같은 예시를 들었지만, 사실 Main 클래스에서 사용할때 코드의 변경이 불가피한 것을 알 수 있다. 

 

Spring 프레임워크를 사용하면서 다음과 같은 문제점을 해결해 보고자 한다.