객체 지향 5원칙

MINI·2023년 3월 1일
0

java

목록 보기
2/2

1. SRP 단일 책임 원칙

어떤 클래스를 변경해야 하는 이유는 오직 하나 뿐이어야한다. - 로버트 C. 마틴

단일 책임이란

단일 책임 원칙이란 하나의 객체는 하나의 동작만을 책임져야 한다는 원칙

객체가 담당하는 동작이 많아질수록 객체를 변경할때마다 영향을 받는 범위가 매우 커진다.

하나의 객체가 담당하는 책임이 많아질수록 객체간의 관계가 의존적으로 변한다.

즉 객체 지향 프로그래밍(oop)의 정의가 난해 해진다.

코드로 보는 단일 책임

public class Developer {
    private final String type;
    private final String[] role={"frontend","backend","infra"};
    
    public Developer(String type){//역할 주입
        this.type=type;
    }
    
    public void develop(){//역할에 따른 개발 실행
        System.out.println("안녕하세요 저는"+type+"개발자 입니다.");        
        switch (type){
            case "front":
                System.out.println(role[0]+"개발중");
                break;
            case "backend":
                System.out.println(role[1]+"개발중");
                break;
            case "infra":
                System.out.println(role[2]+"개발중");
                break;
        }
    }
}

해당 코드는 주어진 역할에 맞는 개발하는 하는 개발자를 객체로 표현한것이다.

여기서 문제점은 개발자라는 객체가 맡은 역할이 3개나 된다는 것이다.(프론트엔드,백엔드,인프라)

이렇게 하나의 객체에 많은 책임이 몰려있게 될 경우 프로젝트에서 해당 객체의 의존성이 높아지게 된다 이러한 형상은 객체지향의 특징인 캡슐화를 전면 부정하게된다.

단일 책임의 원칙은 1객체=1책임으로 최대한 객체를 간결하고 명확하게 설계해야 한다.

위의 코드를 단일 책임의 원칙으로 다시 설계해 보면

abstract public class Developer {
    protected final String type;

    public Developer(String type){
        this.type=type;
    }

    abstract public void develop();
}

우선 Developer를 상위객체로 선언할수있게 바꿔준다 develop메서드는 type에따라 달라지므로

추상 메서드로 선언했다.

public class FrontEndDeveloper extends Developer{

    public FrontEndDeveloper(String type) {
        super(type);
    }

    @Override
    public void develop() {
        System.out.println("안녕하세요 저는"+type+"개발자 입니다.");
        System.out.println("프론트 앤드 개발중");
    }
}

public class BackEndDeveloper extends Developer{
    public BackEndDeveloper(String type) {
        super(type);
    }

    @Override
    public void develop() {
        System.out.println("안녕하세요 저는"+type+"개발자 입니다.");
        System.out.println("백 앤드 개발중");
    }
}

public class InfraDeveloper extends Developer{
    public InfraDeveloper(String type) {
        super(type);
    }

    @Override
    public void develop() {
        System.out.println("안녕하세요 저는"+type+"개발자 입니다.");
        System.out.println("인프라 개발중");
    }
}

이후 FronEnd,BackEnd,Infra각각 객체로 선언한 후 객체의 책임에 맞게 develop 메서드를 구현한다.

이로써 각각의 객체가 하나의 책임을 갖게 되었다.

2. OCP 개방 폐쇄 원칙

소프트웨어 엔티티(클래스,모듈,함수)는 확장에 대해서는 열려 있어야 하지만 변경에 대해서는 닫혀 있어야 한다. -로버트 C. 마틴

개방 폐쇄 원칙이란

자신의 확장에는 열러있고,주변의 변화에 대해서는 닫혀 있어야 한다. 한마디로 보여줄건 보여주고

,숨길건 숨긴다는 의미

객체 지향 관점으로 예를 들면 객체를 하나 수정한다고 할 때 해당 객체에 의존하고 있는 다른 객체의 코드까지 수정되면 안된다는 의미다.

대표적으로 라이브러리를 생각해보자. 라이브러리를 사용하는 객체가 수정되더라고 라이브러리

코드까지 수정되진 않는다.

코드로 보는 개방 폐쇄

public class DbConnecter {
    public void OracleConnect(){
        System.out.println("Oracle을 연결");
    }
    public void MysqlConnect(){
        System.out.println("Mysql을 연결");
    }
    public void MongoDbConnect(){
        System.out.println("MongoDb을 연결");
    }
}

DbConnector라는 데이터 베이스 연결을 제공하는 클래스라고 가정하자. 해당 객체는 연결할 데이터베이스 타입에 맞는 커넥트 메소드를 호출한다.

public class Main {
    public static void main(String[] args) {
        DbConnecter dbConnecter=new DbConnecter();
        dbConnecter.OracleConnect();
        dbConnecter.MysqlConnect();
        dbConnecter.MongoDbConnect();
    }
}

해당 객체의 문제점은 데이터베이스 타입이 추가될때마다 DbConnecter에 메서드를 추가 해야한다는거다. 이 방법은 매우 비효율적이다. 이 부분을 OCP(개방폐쇄)에 맞게 리펙토링 해보자.

public interface Connecter {
    void connect();
}

DbConnecter에서 공통적으로 사용하는 데이터 베이스 연결 메서드를 인터페이스화 했다.

public class OracleConnecter implements Connecter{
    @Override
    public void connect() {
        System.out.println("Oracle을 연결");
    }
}

public class MysqlConnecter implements Connecter{
    @Override
    public void connect() {
        System.out.println("Mysql을 연결");
    }
}

public class MongoDbConnecter implements Connecter{
    @Override
    public void connect() {
        System.out.println("MongoDb을 연결");
    }
}

각 데이터베이스 타입에 따른 객체를 따로 생성한후 Connecter를 상속받아 타입에 맞는 연결 메서드를 작성한다.

public static void main(String[] args) {
        Connecter oracleConnecter=new OracleConnecter();
        Connecter mysqlConnecter=new MysqlConnecter();
        Connecter mongoDbConnecter=new MongoDbConnecter();
        oracleConnecter.connect();
        mysqlConnecter.connect();
        mongoDbConnecter.connect();
    }

리펙토링 전에는 한 객체에 모든 타입의 데이터 베이스 연결이 종속적이였는데

리펙토링 후에는 데이터 베이스 타입에 맞는 객체를 따로 선언하여 Connecter라는 인터페이스를 상속받아 Connect 메서드를 호출하면 된다. 이로써 다른 데이터 베이스 타입이 추가되어도 Connecter인터페이스만 상속받으면 되기때문에 확장에 자유로운 코드가 됬다.

3. LSP 리스코프 치환 원칙

서브 타입은 언제나 자신의 기반 타입(base type)으로 교체할 수 있어야 한다.-로버트 C. 마틴

리스코프 치환 원칙이란

상위 객체와 이를 상속한 하위 객체가 있을때 상위 객체를 호출하는 동작에서 하위 객체가 상위 객체를 완전히 대체할 수 있다는 원칙이다.

하위 클래스 is a kind of 상위 클래스 - 하위 분류는 상위 분류의 한 종류다.

구현 클래서 is able to 인터페이스 - 구현 분류는 인터페이스할 수있어야 한다.

위 두 문장대로 구현된 프로그램이면 이미 리스코프 치환 원칙을 잘 지키고 있다고 할 수 있다.

4. ISP 인터페이스 분리 원칙

클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맞으면 안 된다.-로버트 C. 마틴

인터페이스 분리 원칙이란

객체는 자신이 호출하지 않는 메서드에 의존하지 않아야한다는 원칙이다.

코드로 보는 인터페이스 분리 원칙

abstract public class SmartPhone
{   
    public void call(String number)
    {
        System.out.println(number + " 통화 연결");
    }   
    
    public void message(String number, String text)
    {
        System.out.println(number + ": " + text);
    }

    public void wirelessCharge()
    {
        System.out.println("무선 충전");
    }
    public void ar()
    {
        System.out.println("AR 기능");
    }
    abstract public void biometrics();
}

스마트폰이라는 추상객체를 선언했다 스마트폰에는 전화,문자,무선충전,AR,생체인식 기능을 추상 메서드로 선언하였다.


public class S20 extends SmartPhone
{
    @Override
    public void biometrics()
    {
        System.out.println("S20 생체인식 기능");
    }
}

public class S2 extends SmartPhone
{   
    @Override
    public void wirelessCharge()
    {
        System.out.println("지원 불가능한 기기");
    }
    @Override
    public void ar()
    {
        System.out.println("지원 불가능한 기기");
    }
    @Override
    public void biometrics()
    {
        System.out.println("지원 불가능한 기기");
    }
}

s20에는 스마트폰의 모든 기능을 지원하지만 S2라는 예전 모델 기준에서는 무선충전,AR,생체인식

같은 기능은 지원하지 않는다.

하지만 기능을 지원하지 않아도 추상매서드로 선언되어있기때문에 스마트폰 객체를 상속받은 이상

구현해야하는 문제점이 있다. 이름 인터페이스 분리 원칙에 따라 리펙토링 해보자.


public class SmartPhone
{
    
    public void call(String number)
    {
        System.out.println(number + " 통화 연결");
    }
    public void message(String number, String text)
    {
        System.out.println(number + ": " + text);
    }
}

스마트폰 객체에는 전화기의 공통적인 기능 전화,문자 메서드만 구현한다.


public interface WirelessChargable
{    
    void wirelessCharge();
}

public interface ARable
{    
    void ar();
}

public interface Biometricsable
{  
    void biometrics();
}

나머지 무선충전,AR,생체인식기능은 인터페이스로 만든다.

public class S20 extends SmartPhone implements WirelessChargable, ARable, Biometricsable
{
    @Override
    public void wirelessCharge()
    {
        System.out.println("무선충전 기능");
    }
    @Override
    public void ar()
    {
        System.out.println("AR 기능");
    }
    @Override
    public void biometrics()
    {
        System.out.println("생체인식 기능");
    }
}

public class S2 extends SmartPhone
{
    @Override
    public void message(String number, String text)
    {
        System.out.println("In S2");
        
        super.message(number, text);
    }
}

S20,S2둘다 전화,문자는 가능하니 스마트폰 기능을 상속받고 추가로 필요한 기능은 인터페이스로 상속받아 사용한다.

같은 원리로 새로운 기능이 추가되면 스마트폰 객체를 수정하는 것이 아니라 새로운 인터페이스를 만들어 상속받게 만든다.

이로써 불필요한 책임을 제거하고 기능을 인터페이스로 잘게 쪼개서 확장성을 크게 향상시킨다.

5. DIP 의존 역전 원칙

추상화된 것은 구체적인 것에 의존하면 안 된다. 구체적인 것이 추상화된 것에 의존해야 한다.-로버트 C. 마틴

프로그래머는 추상화에 의존해야지 구체적인 것에 의존하면 안된다.

의존 역전 원칙이란

객체는 저수준 모듈보다 고수준 모듈에 의존해야 한다는 원칙이다.

고수준 모듈:인터페이스,추상객체 같은 추상화된 것

저수준 모듈:구현된 객체

즉 객체는 객체보단 인터페이스,추상객체에 의존해야 한다는 것이다.

코드로 보는 의존 역전 원칙

public class Robot {
    public String toString() {
        return "Robot";
    }
}

public class Lego {
    public String toString() {
        return "Robot";
    }
}

public class Kid {

    private Robot toy;

    public Kid(Robot toy) {
        this.toy = toy;
    }

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

아이라는 객체가 play이라는 메소드로 장난감을 가지고 논다고 해보자.

public class Main {
    Robot robot=new Robot();
    Kid kid=new Kid(robot);
		kid.play()
}

이 코드의 문제점은 아이 객체가 생성될 때 Robot객체 밖에 받지 못한다는 것이다.

하지만 아이는 로봇말고도 다른 장난감으로 놀고 싶을때가 있을것이다.

이 문제를 의존 역전의 원칙으로 해결해보자.

public interface Toy {
    String toString();
}

public class Robot implements Toy{
    @Override
    public String toString() {
        return "Robot";
    }
}

public class Lego implements Toy{
    @Override
    public String toString() {
        return "Robot";
    }
}

Toy라는 인터페이스를 만들고 로봇과 레고 객체가 Toy인터페이스를 상속받게한다.

public class Kid {

    private Toy toy;

    public Kid(Toy toy) {
        this.toy = toy;
    }

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

아이는 더이상 로봇객체를 주입받지 않고 Toy 인터페이스를 받기 때문에 더이상 어떤 장남감이 들어올지 고려하지 않아도 된다.

Reference

스프링 입문을 위한 자바 객체 지향의 원리와 이해

profile
느리지만 꾸준히

0개의 댓글