본문 바로가기

항해 99/Java

Java - SOLID

객체 지향의 SOLID 원칙

SOLID는 SRP, OCP, LSP, ISP, DIP의 앞 글자들을 딴 용어

  • SRP (Single Responsibility Principle) - 단일 책임 원칙
  • OCP (Open-Closed Principle) - 개방 폐쇄 원칙
  • LSP (Liscov Substitution Principle) - 리스코프 치환 원칙
  • ISP (Interface Segregation Principle) - 인터페이스 분리 원칙
  • DIP (Dependency Inversion Principle) - 의존 관계 역전 원칙

객체 지향 설계를 잘해서 프로그래밍할 때의 장점

  • 유지보수가 쉬워짐
  • 확장성이 좋아짐
  • 재사용성이 상승
  • 자연적인 모델링이 가능
  • 클래스 단위로 모듈화해서 대형 프로젝트 개발이 용이해짐

 

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

  • 한 클래스는 하나의 책임만 가져야 한다
    • 클래스와 메서드는 하닁 역할만 하도록 하고, 클래스와 메서드는 한 이유로만 변경되어야 한다

단일 책임 원칙 미준수 예제

package solid.dip;

public class Production {

    private String name;
    private int price;

    public Production(String name, int price) {
        this.name = name;
        this.price = price;
    }

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

class ProductionUpdateService {

    public void update(Production production, int price) {
        //validate price
        validatePrice(price);

        //update price
        production.updatePrice(price);
    }

    private void validatePrice(int price) {
        if (price < 1000) {
            throw new IllegalArgumentException("최소가격은 1000원 이상입니다.");
        }
    }
}
  • Production의 책임
    • update() : 상품의 정보를 변경하는 Product의 책임을 호출 한다.
  • ProductionUpdateService 의 책임
    • updatePrice() : 상품의 가격을 변경한다.
    • validatePrice() : 상품의 유효성을 검사한다.

ProductionUpdateService의 역할은 Product의 내용을 변경하는 책임을 호출하는 책임을 가짐, update( )의 책임은 ProductionUpdateService의 책임.

가격을 변경하는 validatePrice( )는 실제 가격 정볼르 바꾸는 Product의 책임으로 보는 게 맞음

 

단일 책임 원칙 준수 예제

package solid.dip;

public class Production {

    private static final int MINIMUM_PRICE = 1000;
    private String name;
    private int price;

    public Production(String name, int price) {
        this.name = name;
        this.price = price;
    }

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

    private void validatePrice(int price) {
        if (price < MINIMUM_PRICE) {
            throw new IllegalArgumentException(String.format("최소가격은 %d원 이상입니다.", MINIMUM_PRICE));
        }
    }
}

class ProductionUpdateService {

    public void update(Production production, int price) {
        //update price
        production.updatePrice(price);
    }
}
  • 유효성 검증의 책임을 Production으로 옮겨 ProductionUpdateService는 상품의 정보를 변경하기 위한 코드만 존재
    • SRP를 지키기 위해서는 각 객체가 할 수 있는 일과 해야 하는 일을 찾아 책임으로 부여하도록 의식

 

 

 

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

  • 확장에는 열려있고 변경에는 닫혀 있어야 한다
    • 소프트웨어의 구성요소(컴포넌트, 클래스, 모듈, 함수)는 확장에는 열려 있어야 하지만 변경에는 폐쇄적이어야 함
    • 기존의 코드를 변경하지 않으면서, 기능을 추가할 수 있도록 설계가 되는 원칙
      • 중요 메커니즘 - 추상화 , 다형성

개방-폐쇄 원칙 미준수 예제

package solid.ocp;

public class Production {
    private String name;
    private int price;
    // N(일반) , E(전자티켓) , L(지역상품)...
    private String option;

    public Production(String name, int price, String option) {
        this.name = name;
        this.price = price;
        this.option = option;
    }
    
    public int getNameLength() {
        return name.length();
    }

    public String getOption() {
        return option;
    }
}
  • 상품 객체의 멤버 변수에 대한 검증 작업을 진행해야 하는 요구 사항을 가정(일반 상품은 이름이 3글자보다 길어야 함)

개방-폐쇄 원칙 미준수 예제 - 검증 클래스

package solid.ocp;

public class ProductionValidator {
    public void validateProduction(Production production) throws IllegalAccessException {
        if (production.getNameLength() < 3) {
            throw new IllegalAccessException("일반 상품의 이름은 3글자보다 길어야 합니다.");
        }
    }
}
  • 위 코드에서 추가적인 요구 사항이 발생 시 코드의 수정이 빈번하게 발생함

개방-폐쇄 원칙 미준수 예제 - 추가사항 발생

package solid.ocp;

public class ProductionValidator {
    public void validateProduction(Production production) throws IllegalAccessException {
        if (production.getOption().equals("N")) {
            if (production.getNameLength() < 3) {
                throw new IllegalAccessException("일반 상품의 이름은 3글자보다 길어야 합니다.");
            }
        } else if (production.getOption().equals("E")) {
            if (production.getNameLength() < 10) {
                throw new IllegalAccessException("전자티켓 상품의 이름은 10글자보다 길어야 합니다.");
            }
        } else if (production.getOption().equals("L")) {
            if (production.getNameLength() < 20) {
                throw new IllegalArgumentException("지역 상품의 이름은 20글자보다 길어야 합니다.");
            }
        }
    }
}
  • 새로운 사항이 추가 될 때마다 코드 수정이 일어나고, 코드는 점점 유지보수가 힘들어지고 코드를 파악하기 힘듬

개방-폐쇄 원칙 준수 예제

interface

package solid.ocp;

public interface Validator {

    boolean support(Production production);

    void validate(Production production) throws IllegalArgumentException;
}

 

구현 클래스

package solid.ocp;

public class DefaultValidator implements Validator{
    @Override
    public boolean support(Production production) {
        return production.getOption().equals("N");
    }

    @Override
    public void validate(Production production) throws IllegalArgumentException {
        if (production.getNameLength() < 3) {
            throw new IllegalArgumentException("일반 상품의 이름은 3글자보다 길어야 합니다.");
        }
    }
}

package solid.ocp;

public class ETicketValidator implements Validator{
    @Override
    public boolean support(Production production) {
        return production.getOption().equals("E");
    }

    @Override
    public void validate(Production production) throws IllegalArgumentException {
        if (production.getNameLength() < 10) {
            throw new IllegalArgumentException("전자티켓 상품의 이름은 10글자보다 길어야 합니다.");
        }
    }
}

package solid.ocp;

public class LocalValidator implements Validator{
    @Override
    public boolean support(Production production) {
        return production.getOption().equals("L");
    }

    @Override
    public void validate(Production production) throws IllegalArgumentException {
        if (production.getNameLength() < 20) {
            throw new IllegalArgumentException("지역 상품의 이름은 20글자보다 길어야 합니다.");
        }
    }
}

 

  • 위 구조에서 새로운 옵션이 생성되어 검증 로직이 추가되어도 ProducValidator 의 validate( )의 수정 없이 해당 검증을 담당할 객체를 추가해 요구사항 충족 가능
  • enum과 stream을 통해 더 간결하게 리팩토링 가능

 

 

 

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

  • 서브 타입은 언제나 자신의 기반 타입으로 변경할 수 있어야 한다
    • 상위 타입은 항상 하위 타입으로 대체될 수 있어야 함을 의미
    • 부모 클래스가 들어갈 자리에 자식 클래스를 넣어도 역할을 하는데 문제가 없어야 함
      • LSP는 객체의 IS-A 관계를 만족하는 것이 아니라 객체 행위의 IS-A를 만족해야 함
      • 리스코프 치환 원칙은 다형성과 확장성을 극대화하며, 개방-폐쇄 원칙을 구성

 

리스코프 치환 원칙 미준수 예제

package solid.lsp;

public class Member {
    private final String name;

    public Member(String name) {
        this.name = name;
    }

    public void joinTournament() {} // 참가
    public void organizeTournament() {} // 주최
}

package solid.lsp;

// Member를 구현하는 등급별 회원 정의
public class PremiumMember extends Member {
    public PremiumMember(String name) {
        super(name);
    }

    @Override
    public void joinTournament() {
        System.out.println("참가 가능");
    }

    @Override
    public void organizeTournament() {
        System.out.println("주최 가능");
    }
}


package solid.lsp;

public class VIPMember extends Member{
    public VIPMember(String name) {
        super(name);
    }

    @Override
    public void joinTournament() {
        System.out.println("참가 가능");
    }

    @Override
    public void organizeTournament() {
        System.out.println("주최 가능");
    }
}


package solid.lsp;

public class FreeMember extends Member{
    public FreeMember(String name) {
        super(name);
    }

    @Override
    public void joinTournament() {
        System.out.println("참가 가능");
    }

    @Override
    public void organizeTournament() {
        System.out.println("주최 불가");
    }
}
  • Member를 상속받은 서브 클래스 중 FreeMember는 해당 권한이 없기 때문에 동일한 동작이 불가능
    • 서브 클래스가 슈퍼 클래스의 역할을 온전히 수행할 수 없다면 리스코프 치환 원칙에 위배

리스코프 치환 원칙 준수 예제

  • Member 추상 클래스를 인터페이스로 분리하여 선언 후 회원의 등급에 따라 해당 권한을 수행할 수 있는 인터페이스를 구현

interface

package solid.lsp;

// Member 추상 클래스 분리
public interface JoinTournament {
    public void join();
}

package solid.lsp;

public interface OrganizeTournament {
    public void organize();
}

 

구현 클래스

package solid.lsp;

public class PremiumMember extends Member implements JoinTournament, OrganizeTournament{

    public PremiumMember(String name) {
        super(name);
    }

    @Override
    public void join() {

    }

    @Override
    public void organize() {

    }
}

package solid.lsp;

public class VIPMember extends Member implements JoinTournament, OrganizeTournament{
    public VIPMember(String name) {
        super(name);
    }

    @Override
    public void join() {
        System.out.println("참가 가능");
    }

    @Override
    public void organize() {
        System.out.println("주최 가능");
    }
}

package solid.lsp;

public class FreeMember extends Member implements JoinTournament, OrganizeTournament{
    public FreeMember(String name) {
        super(name);
    }

    @Override
    public void join() {
        System.out.println("참가 가능");
    }

    @Override
    public void organize() {
        System.out.println("주최 불가");
    }
}

 

main 클래스

package solid.lsp;

import java.util.List;

public class Main {
    public static void main(String[] args){
        List<JoinTournament> members = List.of(
                new PremiumMember("kim"), 
                new VIPMember("lee"), 
                new FreeMember("hwang"));

        for(JoinTournament member : members){
            member.join();
        }
    }
}

 

 

 

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

  • 하나의 일반적인 인터페이스보다 여러 개의 구체적인 인터페이스가 낫다
    • 각 역할에 맞게 인터페이스를 분리
    • 인터페이스 내에 메서드는 최소한 일수록 좋음(최소한의 기능만 제공하면서 하나의 역할에 집중)
    • 단일 책임 원칙(SRP)과 인터페이스 분할 원칙(ISP)는 같은 문제에 대한 두 가지 다른 해결책(가능한 최소한의 인터페이스를 사용하도록 하여 단일 책임을 강조) - 일반적으로 ISP보다 SRP할 것을 강조

인터페이스 분리 원칙 미준수

package solid.isp;

public interface AllInOneDevice {
    void print();

    void copy();

    void fax();
}
  • 출력, 복사, 팩스 3가지 기능을 가진 복합기 인터페이스

인터페이스 구현

package solid.isp;

public class SmartMachine implements AllInOneDevice{
    @Override
    public void print() {
        System.out.println("print");
    }

    @Override
    public void copy() {
        System.out.println("copy");
    }

    @Override
    public void fax() {
        System.out.println("fax");
    }
}

 

출력 기능만 필요한 인쇄기 구현

package solid.isp;

public class PrinterMachine implements AllInOneDevice{
    @Override
    public void print() {
        System.out.println("print");
    }

    @Override
    public void copy() {
        throw new UnsupportedOperationException();
    }

    @Override
    public void fax() {
        throw new UnsupportedOperationException();
    }
}
  • 위 코드의 경우 인터페이스만 알고 있는 클라이언트는 printer에서 copy 기능이 구현되어 있는지 안 되어 있는지 알 수 없어 예상치 못한 오류를 만날 수 있음
    • 하나의 인터페이스를 분리하여 여러 개의 인터페이스로 나누어서 위 문제를 해결

인터페이스 분리 원칙 준수

package solid.isp;

public interface PrinterDevice {
    void print();
}

package solid.isp;

public interface CopyDevice {
    void copy();
}

package solid.isp;

public interface FaxDevice {
    void fax();
}
  • 인터페이스를 기능 별로 분리

구현 클래스

public class SmartMachine implements PrinterDevice, CopyDevice, FaxDevice{
    @Override
    public void copy() {
        System.out.println("print");
    }

    @Override
    public void fax() {
        System.out.println("copy");
    }

    @Override
    public void print() {
        System.out.println("fax");
    }
}

public class PrinterMachine implements PrinterDevice{
    @Override
    public void print() {
        System.out.println("print");
    }
}

 

 

 

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

  • 구체적인 것이 추상화된 것에 의존해야 한다. 자주 변경되는 구체 클래스에 의존하지 마라
    • 의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것보다 변화하기 어려운 것, 거의 변화가 없는 것에 의존
    • 구체적인 클래스보다 상위 클래스, 인터페이스, 추상 클래스와 같이 변하지 않을 가능성이 높은 클래스와 관계를 맺을 것
      • DIP원칙을 따르는 가장 인기 있는 방법 : 의존성 주입(DI : Dependency Injection) 활용

 

의존관계 원칙 미준수

package solid.dip;

// oracle Url
public class OracleJdbcUrl {
    private final String dbName;

    public OracleJdbcUrl(String dbName){
        this.dbName = dbName;
    }

    public String get(){
        return "jdbc:oracle...." + dbName;
    }
}

package solid.dip;

//ConnectToDatabase 선언
public class ConnectToDataBase {
    public void connect(OracleJdbcUrl oracleJdbc){
        System.out.println("Connecting to " + oracleJdbc.get());
    }
}
  • 인수로 OracleJdbcUrl을 구체적으로 넘겨준다면 MySQL 등 다른 형태의 Url을 받으려면 메서드 오버로딩을 하거나 클래스 코드 수정이 필요함

의존관계 원칙 준수

//인터페이스 선언
package solid.dip;

public interface JdbcUrl {
    public String get();
}

//oracle Url
public class OracleJdbcUrl implements JdbcUrl{
    private final String dbName;

    public OracleJdbcUrl(String dbName) {
        this.dbName = dbName;
    }
    @Override
    public String get() {
        return "jdbc:oracle...." + dbName;
    }
}

//ConnectToDatabase 인수 수정
public class ConnectToDataBase {
    public void connect(JdbcUrl jdbc) {
        System.out.println("Connecting to " + jdbc.get());
    }
}
  • JdbcUrl을 구현하는 모든 클래스를 인수로 받아 동일하게 적용시킬 수 있다.
    • 상위 모듈이 하위 모듈에 대한 구체적인 정보를 가지는 것보다 추상화된 정보를 가짐으로써 코드의 유연성 확보와 확장의 편의성이 증가하는 것

 

'항해 99 > Java' 카테고리의 다른 글

Java - Garbage Collector, Java Map  (1) 2024.04.08
Java - 컴파일, JVM 스택 / 힙 메모리, 클래스 / 인스턴스  (0) 2024.04.05
WIL-4  (0) 2024.03.03
객체지향 생활체조 9가지 원칙  (1) 2024.03.01
빌터 패턴  (1) 2024.02.26