객체 지향의 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 |