본문 바로가기
SPRING

[파트 1. 스프링 입문] 챕터 1-3. 객체지향 설계 5원칙(SOLID)

by ZZON90 2022. 2. 28.

 객체지향 설계 5원칙. 일명 SOLID에 대해 포스팅합니다.

 

 

결합도와 응집도

 SOLID에 대해 공부하기 전 결합도와 응집도를 먼저 알고 넘어가야 합니다. 좋은 소프트웨어를 설계하기 위해서는 결합도(Coupling)는 낮추고 응집도(Cohesion)은 높여야 합니다.

  • 결합도
    결합도는 클래스(객체) 간 상호 의존도를 나타내는 지표입니다. 결합도가 낮으면 객체 간 상호 의존성이 줄어들어서 객체의 재사용 및 유지보수가 용이합니다.
  • 응집도
    응집도는 하나의 모듈 내부에 존재하는 구성요소들의 기능적 관련성을 의미합니다. 응집도가 높은 모듈은 하나의 책임에 집중하고 독립성이 높아 재사용 및 유지보수가 용이합니다.

 

 

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

 모든 클래스는 하나의 책임만 가지며, 클래스는 그 책임을 완전히 캡슐화해야 함을 일컫습니다. 클래스를 변경해야 하는 이유는 한 가지뿐이어야 합니다.

 

단일 책임 원칙을 위반하면?

 단일 책임 원칙 없이, 하나의 모듈에서 여러 책임을 갖고 있을 경우 어떻게 개발될 수 있는지 보겠습니다.

class Calc{

    private String sign;
    private int num1 = 3
    private int num2 = 5;
    private int result = 0;
    
    public void calculate(){
    	if("+".equals(sign)){
            result = num1+num2;
        }else if("-".equals(sign)){
            result = num1-num2;
        }else if("*".equals(sign)){
            result = num1*num2;
        }else if("/".equals(sign)){
            if(num1==0){
            	result = 0;
                return;
            }
            result = num1/num2;
        }
    }
}

  위 코드는 계산기를 간단하게 구현한 코드입니다. 입력받을 수 있는 변수 2개와 부호가 있을 것이고 결괏값을 가지고 있을 변수가 있을 것입니다. 여기서 숫자와 부호는 입력받았다고 가정하고, calculate 메서드를 실행하면 각 부호에 맞게 결괏값이 출력될 것입니다. 하지만 현재 코드에서 부호가 추가되면 else if 문을 추가해야 합니다. 또 다른 경우로는 기존에 있던 부호의 처리결과를 변경해야 할 때마다 if 구문을 찾아 변경해야 합니다.

 

단일 책임 원칙을 적용하면?

 단일 책임 원칙을 적용하면 다음과 같이 변경할 수 있습니다.

class plusCalc extends Calc{
    public void calculate(){
    	this.result = num1 + num2;
    }
}

class minusCalc extends Calc{
    public void calculate(){
    	this.result = num1 - num2;
    }
}

class multipleCalc extends Calc{
    public void calculate(){
    	this.result = num1 * num2;
    }
}

class divisionCalc extends Calc{
    public void calculate(){
        if(num1==0){
            this.result = 0;
            return;
        }
    	this.result = num1 / num2;
    }
}

  각 부호에 맞게 plusCalc, minusCalc, multipleCalc, divisionCalc 클래스를 만들고, Calc 클래스를 상속받습니다. 다형성을 주기 위해 calculate 메서드를 오버라이딩합니다. 그 후 사용하는 클래스에서 각 객체의 calculate 메서드만 호출하게 되면 각각의 기능에 맞춰 결과가 출력됩니다.

 

 전자의 코드에는 plus, minus 등 여러 가지 책임을 가지고 있지만 후자의 코드에는 책임을 분리를 시킴으로써 해당 부호의 기능만 수행하는 것을 확인할 수 있습니다.

 

 

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

 개방 폐쇄 원칙은 자신의 확장에는 열려있고, 주변의 변화에 대해서는 닫혀 있는 것을 의미합니다. 상위 클래스 또는 인터페이스를 중간에 둠으로써 자신의 변화에는 폐쇄적이지만 인터페이스 또는 외부의 변화에 대해서 확장을 개방해줄 수 있다.

 

 개방 폐쇄 원칙을 공부하기 가장 좋은 예가 있습니다. 바로 JDBC Interface입니다.

jdbc interface

 application 입장에서는 외부적으로 굉장히 많은 데이터베이스가 존재할 수 있습니다. 그림에 표시된 oracle, mysql, sqlserver 뿐 아니라 다양한 데이터베이스가 존재할 수 있습니다. application에서 데이터베이스를 직접 연결해야 한다고 하면 데이터베이스가 늘어날 때마다 또는 데이터베이스가 변경될 때마다 application을 직접 수정해야 합니다.

 이를 방지하기 위해 jdbc interface를 중간에 두어 application에게는 단 하나의 통로를 두어 폐쇄적이고 외부로는 n개의 데이터베이스를 확장할 수 있도록 개방하고 있습니다.

 

 

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

리스코프 치환 원칙은 서브 타입은 언제나 자신의 상위(기반) 타입으로 교체할 수 있어야 한다는 것입니다. 리스코프 치환 원칙의 중요 포인트는 자식 클래스를 부모 클래스로부터 상속받을 때, 부모 클래스의 코드에서 조건을 수정하거나 사전에 약속한 기획대로 구현하지 않는 것입니다.

 위 단일 책임 원칙에서 설명한 DivisionCalc를 예로 들겠습니다.

class DivisionCalc extends Calc{
    public void calculate(){
        if(num1==0){
            this.result = 0;
            return;
        }
    	this.result = num1 / num2;
    }
}

class DivisionCalc2 extends DivisionCalc{
    public void calculate(){
    	this.result = num1/num2;
    }
}

 DivisionCalc의 calculate 메서드에 대한 변경사항이 발생해 메서드의 내부 로직을 변경해야 하는 상황이 왔다고 가정합니다. 이때 DivisionCalc를 직접 수정하지 않고 DivisionCalc를 상속받아 calculate를 오버라이드 했고 내부의 num1이 0을 체크하는 조건을 제거한다고 하면, 나중에 DivisionCalc를 DivisionCalc2로 치환했을 때는 ArithmeticException 예외가 발생합니다.

 DivisionCalc는 나눗셈에 대한 클래스로 calculate 메서드 수행 시 분자가 0이면 나눗셈을 하지 않고 결괏값을 0으로 세팅합니다. DivisionCalc2는 어떤 이유로 인해, DivsionCalc에서 제공하던 조건을 제거했습니다. 이는 상위 타입의 명세를 하위 타입에서 지킬 수 없는 결과를 만듭니다.

 때문에 상속을 하기 위해서는 상위 타입에서 정한 기능을 하위 타입에서 지킬 수 있을 때 상속해야 합니다. 또는 해당 기능에 대해 오버라이딩 하지 않아야 합니다.

 

 

4. ISP(Interface Segrepation Principle) 인터페이스 분리 원칙

 인터페이스 분리 원칙은 클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안 된다는 것입니다. 프로젝트 요구사항과 설계에 따라 SRP(단일 책임 원칙) 또는 ISP(인터페이스 분리 원칙)을 선택합니다.

 

인터페이스 분리 원칙을 지키지 않는다면?

인터페이스 분리 원칙을 지키지 않을 경우 어떻게 동작하는지 알아보자.

abstract class Map{
    public void showMap();
    public void bicycleGuide();
    public void carGuide();
    public void bikeGuide();
    public void walkGuide();
}

class BicycleNavi extends Map{
    public void showMap(){
        // 사용
    }
    public void bicycleGuide(){
        // 사용
    }
    public void carGuide(){
        // 사용안함
    }
    public void bikeGuide(){
        // 사용안함
    }
    public void walkGuide(){
        // 사용안함
    }
}

class WalkNavi extends Map{
    public void showMap(){
        // 사용
    }
    public void bicycleGuide(){
        // 사용안함
    }
    public void carGuide(){
        // 사용안함
    }
    public void bikeGuide(){
        // 사용안함
    }
    public void walkGuide(){
        // 사용
    }
}

  BicycleNavi는 자전거길을 안내하는 클래스다. BicycleNavi는 맵을 보여주는 showMap 메서드와 자전거길을 안내해주는 bicycleGuide 메서드만 필요하다. 나머지 메서드는 사용하지 않지만 구현해주거나 별도 처리를 해주어야 한다. WalkNavi 또한 도보길을 안내하는 클래스다. WalkNavi도 BicycleNavi와 동일한 문제를 겪는다.

 

인터페이스 분리 원칙을 적용하면?

abstract class Map{
    public void showMap();
}

interface BicycleNavi(){
    public void bicycleGuide();
}

interface WalkNavi(){
    public void walkGuide();
}

class BicycleNaviImpl extends Map, implements BicycleNavi{
    public void showMap(){
        // 사용
    }
    public void bicycleGuide(){
        // 사용
    }
}

class WalkNaviImpl extends Map, implements WalkNavi{
    public void showMap(){
        // 사용
    }
    public void walkGuide(){
        // 사용
    }
}

 기존 모든 기능을 가지고 있던 Map 클래스는 필수 동작인 showMap을 제외한 *Guide 메서드는 각 기능에 맞춰 interface로 분리합니다. 자전거길은 안내하는 BicycleNavi interface, 도보길을 안내하는 WalkNavi interface를 각각 만들어 줍니다.

 실제 구현할 BicycleNaviImpl과 WalkNaviImpl 클래스에서는 Map 클래스를 상속받고 각각 BicycleNavi와 WalkNavi interface를 구현합니다. 이렇게 함으로써 사용하지 않는 메서드와 의존관계를 맺지 않게 구현할 있습니다.

 

 

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

 객체는 저수준 모듈보다 고수준 모듈에 의존해야 한다는 원칙입니다. 객체지향에서 고수준, 저수준 모듈의 정의는 다음과 같습니다.

  • 고수준 모듈 : 인터페이스 또는 추상 클래스 같은 객체의 형태나 개념
  • 저수준 모듈 : 구현된 객체

즉, 쉽게 말하면 자신보다 변하기 쉬운 것에 의존하지 않아야 한다는 의미입니다.

 

의존 역전 원칙을 지키지 않는다면?

public class Person {
	private SpringClothes clothes;
	
	public Person(SpringClothes clothes) {
		this.clothes = clothes;
	}
	
	public String getColor() {
		return clothes.getColor();
	}
	
	public String getType() {
		return clothes.getType();
	}
	
	public void getInfo() {
		System.out.println("종류 : " + getType());
		System.out.println("색 : " + getColor());
	}
}

public class SpringClothes {
	private String color;
	private String type;
	
	public SpringClothes(String type, String color) {
		this.type = type;
		this.color = color;
	}
	
	public String getType() {
		return this.type;
	}
	
	public String getColor() {
		return this.color;
	}
}

 사계절 옷 중 하나인 봄 옷을 구현한 SpringClothes 객체가 있습니다. 어떤 한 사람은 SpringClothes에 주입한 옷을 입을 수 있습니다. 어떤 한 사람이 옷을 입고 해당 옷의 종류와 색을 알려준다고 가정합니다.

 하지만, 옷에는 봄옷만 있는 것이 아닙니다. 여름옷, 겨울옷 다양한 옷이 존재할 수 있습니다. 그러나 이 Person 객체는 애초에 봄옷을 제외한 다른 옷을 입을 수 있는 구조가 아닙니다. Person 인스턴스 생성 시 SpringClothes에 의존성을 가지기 때문입니다. 다른 계절의 옷을 입고 싶다면 Person의 코드를 변경해야 합니다. 이는 개방 폐쇄 원칙을 위반합니다.

 

의존 역전 원칙을 지킨다면?

 의존 역전 원칙을 지키기 위해서는 의존 대상을 저수준 모듈에서 고수준 모듈로 변경해야 합니다.

public class Person {
	private Clothes clothes;
	
	public Person(Clothes clothes) {
		this.clothes = clothes;
	}
	
	public String getColor() {
		return clothes.getColor();
	}
	
	public String getType() {
		return clothes.getType();
	}
	
	public void getInfo() {
		System.out.println("종류 : " + getType());
		System.out.println("색 : " + getColor());
	}
}

public interface Clothes {
	public String getType();
	public String getColor();
}

public class SpringClothes implements Clothes {
	private String color;
	private String type;
	
	public SpringClothes(String type, String color) {
		this.type = type;
		this.color = color;
	}
	
	public String getType() {
		return this.type;
	}
	
	public String getColor() {
		return this.color;
	}
}

 SpringClothes보다 고수준 모듈인 Clothes interface를 생성합니다. Clothes interface는 종류와 색을 알려주는 메서드만 선언합니다. 그 후 SpringClothes에서 Clothes를 구현합니다. 만약 SummerClothes를 만들고 싶을 경우 동일하게 Clothes를 implements 하면 됩니다.

 Person에서는 SpringClothes에서 Clothes로 변경합니다. Person은 앞으로 SpringClothes에 의존하지 않고 Clothes에 의존하게 됩니다. 즉, 어떤 옷을 입던 Person은 수정이 발생할 일이 없습니다. 이는 개방 폐쇄 원칙 또한 준수할 있습니다.

 

 

정리

 글을 정리하다 보니 실제 프로그래밍할 때 실제 사용하고 있는 원칙도 있고 아닌 원칙도 있었습니다. 개념과 이론을 실제 프로그래밍과 비교하니 조금 더 공부하는 데 도움이 된 것 같습니다. 객체지향 5원칙인 SOLID를 잘 이해하고 넘어가야 간단하고 더 나은 프로그램을 개발할 수 있는 것 같습니다.

댓글