반응형

Dependency Inversion Principle은 의존성 역전의 원칙이라고 한다.

해당 부분은 이해하기 어려울 수 도 있는데,

고수준의 모듈이 저수준의 모듈에 의존하지 않도록 인터페이스나 추상클래스에 의존하도록 하는 것을 말한다.

우선 Dependency Inversion Principle을 준수하지 않은 경우에 대한 예제를 보자

public class Led {
	public void on() {
		System.out.println("LED on");
	}

	public void off() {
		System.out.println("LED off");
	}
}

public class Switch {
	private Led led;
	private boolean isOn;
	
	public Switch(Led led) {
		this.led = led;
		this.isOn = false;
	}

	public void operate() {
		if (isOn) {
			led.off();
		} else {
			led.on();
		}
	}
}

위 코드의 Switch는 Led 타입의 변수를 가지고 있다.

이는 Switch 클래스가 Led에 의존성을 가진다고 볼 수 있다.

이 경우 Switch클래스의 전구는 Led외에는 사용할 수 없게 된다는 문제가 발생 한다.

 

이번에는 Dependency Inversion Principle을 준수하는 예를 보자

interface Light {
	void on();
	void off();
}

public class Led implements Light {
	public void on() {
		System.out.println("LED on");
	}

	public void off() {
		System.out.println("LED off");
	}
}

public class Switch {
	private Light light;
	private boolean isOn;
	
	public Switch(Light light) {
		this.light = light;
		this.isOn = false;
	}

	public void operate() {
		if (isOn) {
			light.off();
		} else {
			light.on();
		}
	}
}

위 경우는 Switch가 하위 클래스에 대한 의존성을 갖는 것이 아닌 인터페이스에 대한 의존성을 갖고 있다.

이 이야기는 인터페이스를 구현하는 하위 클래스를 사용할 수 있다는 이야기가 되며, 이를 통해 하나의 타입에 대한 의존성 문제를 해결할 수 있다.

즉, Dependency Inversion Principle은 하나의 클래스에 대해 의존성이 발생하지 않도록 하는 원칙이다.

반응형
반응형

Interface Segregation Principle은 하나의 거대한 Interface 를 사용하기 보다는 여러개의 작은 Interface를 사용하는 원칙이다.

Interface Segregation Principle을 지키지 않은 코드

public interface Animal {
	void eat();
  void fly();
}

public class Cat implements Animal {
	@Override
	public void eat() {
		System.out.println("Eat Cat food");
	}

	@Override
	public void fly() {
		throw new UnsupportedOperationException("I can not fly!");
	}
}

Animal은 하늘과 육지 동물들을 모두 포함하는 인터페이스이다.

그러다 보니 eat, fly 메소드가 존재한다.

하지만 fly는 육지 동물에게는 필요 없을 수 도 있다.

Animal을 구현하고 있는 Cat은 날 수 없다. 따라서 fly 메소드는 Cat에는 필요가 없다. 하지만 Animal을 구현해야하기 때문에 구현할 필요가 있다.

이는 Interface Segregation Principle이 준수되지 않은 경우 이다.

Interface Segregation Principle을 준수하는 코드

public interface Eater {
	void eat();
}

public interface Flyer {
	void fly();
}

public class Cat implements Eater {
	@Override
	public void eat() {
		System.out.println("Eat Cat food");
	}
}

위 예제 코드를 보면 하나의 Animal 인터페이스에서 Eater, Flyer와 같은 작은 인터페이스로 분리되어 있고, Cat은 필요한 인터페이스만 구현하고 있다.

이렇게 되면 불필요한 메소드를 구현할 필요가 없어지게 된다.

즉, 필요한 의존성만을 가지게 된다.

반응형
반응형

Liskov Substitution Principle은 서브타입이 부모타입으로 대체될 수 있어야 한다는 것이다.

이렇게 보면 이해하기가 조금 난해할 수 있다.

간단히 설명하면 상속 또는 인터페이스를 구현하는 어떤 클래스가 상위 클래스에 대체될 수 있어야하고, 그 때 일관된 동작을 보장해야한다는 것이다.

즉, 일관성과 안정성을 보장해야한다는 원칙이다.

Liskov Substitution Principle을 준수하지 않는 코드

public class Bird {
	
	public void fly() {
		System.out.println("fly high!");
	}
}

public class Penguin extends Bird {
	
	@Override
	public void fly() {
		throw new IllegalStateException("I can not fly!");
	}
}

public class Zoo {
	public static void main(String[] args) {
		
		System.out.println("새들이 나는 공연");
		
		List<Bird> birds = new ArrayList<>();
		birds.add(new Bird());
		birds.add(new Penguin());
		
		birds.forEach(Bird::fly);
	}
}

Liskov Substitution 원칙을 준수 하지 않은 코드를 보면 Bird를 보면 해당 클래스는 fly라는 메소드가 있고 하늘을 나는 기능을 수행하는 메소드임을 알 수 있다.

그러나 Bird를 상속하는 자식 클래스인 Penguin은 Bird를 상속하고 있어 fly 메소드가 있어 하늘을 나는 기능을 수행할 것이라고 예상되나 Penguin은 하늘을 날 수 없다.

이러한 경우 Penguin은 부모 클래스인 Bird를 상속 하였지만 Bird클래스를 대체하여 일관적인 기능(fly)를 제공할 수 없다.

Bird를 사용하는 부분이 Penguin으로 대체 되어도 정상 동작 되어야하나 Penguin은 날지 못하기 때문에 이러한 기능을 제공할 수 없다

Liskov Substitution Principle을 준수하는 코드

public interface Flyable {
	void fly();
}

public class Bird {
	public void sing() {
		System.out.println("Singing");
	}
}

public class Magpie extends Bird implements Flyable {
	@Override
	public void fly() {
		System.out.println("I can fly!");
	}
}

public class Penguin extends Bird {
	
}

public class Zoo {
	public static void main(String[] args) {
		System.out.println("새들이 나는 공연");
		
		List<Bird> allBirds = List.of(new Magpie(), new Penguin());

    System.out.println("새들 노래");
    allBirds.forEach(Bird::sing);

    System.out.println("날 수 있는 새들 공연");
    allBirds.stream()
        .filter(b -> b instanceof Flyable)
        .map(b -> (Flyable) b)
        .forEach(Flyable::fly);
  }
}

Flyable 인터페이스로 fly기능을 분리하고 fly가 필요한 경우 해당 기능을 구현하도록 함으로서 Liskov Substitution Principle을 준수하였다고 볼 수 있다.

 

리스코프 치환 원칙에서 중요한 부분은 일관된 동작을 보장해야 한다는 부분이라고 생각한다.

반응형
반응형

개방 폐쇄 원칙은 소프트웨어의 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다는 원칙이다.

정리하면 기존 코드에 대한 변경 없이 새로운 기능을 추가할 수 있어야 한다.

개방 폐쇄 원칙을 지키게 되면 아래와 같은 장점이 있다.

  1. 유지보수 용이
  2. 재사용성
  3. 확장성

OCP를 위반한 케이스

class DiscountService {
    public int calculateDiscount(int price, String type) {
        switch (type) {
            case "FIXED":
                return price - 1000;
            case "RATE":
                return (int)(price * 0.9);
            default:
                return price;
        }
    }

    public static void main(String[] args) {
        DiscountService discountService = new DiscountService();
        System.out.println(discountService.calculateDiscount(10000, "FIXED")); // 9000
        System.out.println(discountService.calculateDiscount(10000, "RATE"));  // 9000
    }
}

새로운 정책을 추가할 때 마다 calculateDiscount 메소드를 수정해야 한다

해당 메소드는 여러 할인 정책이 있으므로 변경에 닫혀있어야하는 정책을 위반하게 된다

OCP 준수한 케이스

public interface DiscountPolicy {
    boolean supports(String type);
    int discount(int price);
}
public class FixedDiscountPolicy implements DiscountPolicy {
    @Override
    public boolean supports(String type) {
        return "FIXED".equalsIgnoreCase(type);
    }

    @Override
    public int discount(int price) {
        return price - 1000;
    }
}
import java.util.List;

public class DiscountService {
    private final List<DiscountPolicy> policies;

    public DiscountService(List<DiscountPolicy> policies) {
        this.policies = policies;
    }

    public int calculateDiscount(int price, String type) {
        for (DiscountPolicy policy : policies) {
            if (policy.supports(type)) {
                return policy.discount(price);
            }
        }
        return price;
    }
}

----

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        DiscountService discountService = new DiscountService(Arrays.asList(
            new FixedDiscountPolicy(),
            new RateDiscountPolicy()
        ));

        System.out.println(discountService.calculateDiscount(10000, "FIXED")); // 9000
        System.out.println(discountService.calculateDiscount(10000, "RATE"));  // 9000
        System.out.println(discountService.calculateDiscount(10000, "NONE"));  // 10000
    }
}

각 할인 정책은 DiscountPolicy 인터페이스를 구현하게 되기 때문에 DiscountService 는 구현체에 의존하는 것이 아닌 인터페이스에 의존하고 동적 바인딩을 통한 코드 실행 가능해진다

현재 Main 클래스 내에서 의존성을 직접 DiscountService에 주입하고 있지만 이 역할을 Spring IoC 컨테이너가 해주게 되어 기존 코드를 수정하지 않고 정책을 추가할 수 있게 된다

이를 통해 OCP를 준수하게 된다

반응형
반응형

단일 책임 원칙은 클래스, 객체 혹은 모듈이 하나의 책임을 가져야 한다는 원칙이다.

그렇다면 책임은 무엇일까?

책임이란 클래스 또는 객체가 하나의 작업이나 기능을 수행해야한다는 원칙이다.

그런데 책임이란 이런 작업이나 기능이보다는 역할이라는 말이 더 적합하다.

비디오 가게 코드를 예시로 보면

import java.util.ArrayList;
import java.util.List;

public class VideoStore {
    private List<Video> videos;

    public VideoStore() {
        videos = new ArrayList<>();
    }

    public void addVideo(Video video) {
        videos.add(video);
    }

    public void rentVideo(String title) {
        for (Video video : videos) {
            if (video.getTitle().equalsIgnoreCase(title)) {
                video.rent();
                return;
            }
        }
    }

    public void returnVideo(String title) {
        for (Video video : videos) {
            if (video.getTitle().equalsIgnoreCase(title)) {
                video.returnVideo();
                return;
            }
        }
    }
}

VideoStore 클래스는 비디오 가게에 비디오 리스트를 가지고 있고 추가, 대여, 반납 하는 기능이 있다.

VideoStore 클래스는 단일 책임 원칙이 잘 지켜지고 있다. 비디오 가게라는 역할에 맞는 기능들만을 가지고 있기 때문이다.

단일 책임 원칙을 지키게되면 좀 더 객체지향적으로 프로그래밍 할 수 있고, 다음과 같은 장점을 가질 수 있다.

  1. 높은 응집도
  2. 유지보수성
  3. 변경 용이성
import java.util.ArrayList;
import java.util.List;

public class VideoStore {
    private List<Video> videos;
    private List<User> users;

    public VideoStore() {
        videos = new ArrayList<>();
    }

    public void addVideo(Video video) {
        videos.add(video);
    }

    public void rentVideo(String title) {
        for (Video video : videos) {
            if (video.getTitle().equalsIgnoreCase(title)) {
                video.rent();
                return;
            }
        }
    }

    public void returnVideo(String title) {
        for (Video video : videos) {
            if (video.getTitle().equalsIgnoreCase(title)) {
                video.returnVideo();
                return;
            }
        }
    }
    
    public void addUser(User user) {
        users.add(user);
    }
}

위의 코드는 단일 책임 원칙이 깨진 코드이다.

VideoStore가 비디오만 관리하는 것이 아니라 사용자 정보도 관리하기 때문에 VideoStore는 두 가지 역할을 가지고 있다.

물론 이런 경우에도 기능을 정상적으로 작동할 것이고 문제가 없을 가능성이 크다.

다만 이렇게 단일 책임 원칙이 깨지게 되면, 클래스의 역할 즉 클래스가 해야할 일은 모호해지고, 변경에 대한 복잡도가 증가하게 되어 유지보수성이 떨어진다.

 

 

SRP를 지킨 코드

VideoManager

public class VideoManager {
    private List<Video> videos = new ArrayList<>();

    public void addVideo(Video video) {
        videos.add(video);
    }

    public void rentVideo(String title) {
        for (Video video : videos) {
            if (video.getTitle().equalsIgnoreCase(title)) {
                video.rent();
                return;
            }
        }
    }

    public void returnVideo(String title) {
        for (Video video : videos) {
            if (video.getTitle().equalsIgnoreCase(title)) {
                video.returnVideo();
                return;
            }
        }
    }
}

UserManager

public class UserManager {
    private List<User> users = new ArrayList<>();

    public void addUser(User user) {
        users.add(user);
    }
}

VideoStore

public class VideoStore {
    private VideoManager videoManager = new VideoManager();
    private UserManager userManager = new UserManager();

    public void registerVideo(Video video) {
        videoManager.addVideo(video);
    }

    public void registerUser(User user) {
        userManager.addUser(user);
    }
    
    // business
}

비디오를 관리하고 사용자를 관리하는 것은 각각 VideoManager와 UserManager가 책임을 가지고 있다

그리고 이 Manager들을 활용하여 VideoStore Business 로직을 구현 한다

반응형

+ Recent posts