객체지향이란?
객체지향이란 프로그램 설계 방법론의 일종이다. 프로그램에 필요한 데이터를 추상화 시키고 객체(Object)라는 기본 단위를 만든다. 이렇게 만들어진 객체끼리 상호작용을 하는 것이 객체지향 프로그램이다. 객체 지향 프로그램은 유연한 유지 보수, 손쉬운 확장이 쉬워진다. 즉, 개발의 생산성이 높아진다.
모든 일에는 메뉴얼이라는 것이 존재하고 이는 길잡이 역할을 한다. SOLID 원칙이란 객체 지향 프로그램의 메뉴얼이다.
객체 지향 프로그래밍을 사용하는 여러 디자인 패턴은 모두 SOLID 원칙에 입각하여 만들어진 것이기 때문에, SOLID 원칙에 대한 개념을 제대로 잡고 가는 것이 중요하다.
1. SRP(Sigle Responsibility Principle : 단일 책임의 원칙)
클래스(객체)는 단 하나의 책임만 가져야 한다.클래스(객체)가 변경되는 이유가 한가지여야 한다.
클래스나 객체는 하나의 기능만 담당하도록 구성해야 한다는 것이다.
해당 클래스나 객체가 여러 대상, 액터에 대해 책임을 가져서는 안되고, 오직 하나의 액터에 대해서만 책임을 져야 한다는 것이는 표현이 더 정확하다고 한다. 즉, 클래스가 변경되는 이유가 하나이어야 한다라는 것.
예를 들어, 웹 페이지에서 User 클래스가 회원가입 기능을 가지고 있음과 동시에 로그인을 처리하는 기능 또한 가진다고 가정해보자. 이러한 경우, 회원가입 방식이 변경되면 User 클래스의 코드를 수정해야 한다.
이 수정은 로그인 기능과는 전혀 관련이 없기 때문에 SRP에 위배된다고 볼 수 있다.
SRP 위배
public class UserManager {
private Database db;
public UserManager() {
this.db = new Database();
}
public void saveUser(User user) {
// 회원가입 로직
db.save(user);
}
public boolean authenticateUser(String username, String password) {
// 로그인 로직
User user = db.getUser(username);
return user != null && user.getPassword().equals(password);
}
}
UserManager 클래스 안에 회원가입과 로그인 로직이 모두 존재한다.
따라서 SRP에 위배된다.
SRP 준수
public class UserManager {
private Database db;
public UserManager() {
this.db = new Database();
}
public void saveUser(User user) {
// 회원가입 로직
db.save(user);
}
}
public class Authenticator {
private Database db;
public Authenticator() {
this.db = new Database();
}
public boolean authenticateUser(String username, String password) {
// 로그인 로직
User user = db.getUser(username);
return user != null && user.getPassword().equals(password);
}
}
회원가입은 UserManager 클래스에 로그인은 Authenticator 클래스에 분리해서 구현했다.
따라서 SRP 준수한다.
2. OCP(Open-Closed Principle : 개방-폐쇄 원칙)
클래스(객체)는 확장에 대해 열려있고 수정에 대해 닫혀있어야 한다.
확장에 대해 열려있다 : 요구 사항이 변경될 때 새로운 로직을 추가하여 기능을 확장할 수 있어야 한다.
수정에 대해 닫혀있다 : 기존의 코드를 수정하지 않아도 기능을 추가하거나 변경할 수 있어야 한다.
즉, 프로그램의 기능을 변경할 때 기존의 코드를 수정하지 않고 확장을 통해 변경할 수 있어야 한다.
아래는 OCP 설명하기 위한 코드이다. 도형을 그리는 기능을 구현하고 있다.
OCP 위배
public class Shape {
private String type;
public Shape(String type) {
this.type = type;
}
public void draw() {
if (type.equals("circle")) {
System.out.println("Draw Circle");
} else if (type.equals("square")) {
System.out.println("Draw Square");
}
// 새로운 도형 종류가 추가될 때마다 draw 메서드를 수정해야 함
}
}
위 코드는 새로운 도형이 추가될 때마다 darw 메서드를 계속해서 수정해야 한다.
따라서 OCP에 위배된다.
OCP 준수
public interface Shape {
void draw();
}
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Draw Circle");
}
}
public class Square implements Shape {
@Override
public void draw() {
System.out.println("Draw Square");
}
}
OCP가 본질적으로 의미하는 것은 추상화이다.
위 코드에서 가장 상위 클래스는 Shape을 Interface로 만들어 추상화하고 새로운 도형이 만들어질 때마다 새로운 Class가 만들어지고 Shape Interface를 상속받는다.
기존의 코드를 수정하지 않기 때문에 OCP를 준수한다.
3. ISP(Interface Segregation Principle : 인터페이스 분리 원칙)
클래스(객체)는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.
클라이언트의 목적과 용도에 적합한 인터페이스만을 제공해주어야 한다.
구현할 객체의 무의미한 메서드 구현을 방지하기 위해 반드시 필요한 메서드만을 상속/구현하게 한다.
스마트폰과 2G 핸드폰을 만들기 위해 하나의 추상 클래스를 만들었다고 가정해보자. 이 추상 클래스에는 전화, 메세지, LTE의 기능이 구현되어 있다.
스마트폰은 이 추상 클래스를 그대로 상속 받아서 구현하면 될 것이다.
그러나 2G 핸드폰은 이 클래스를 그대로 상속 받으면 LTE이라는 사용하지도 않는 메서드를 구현하게 되는 상황이 발생한다.
각 객체가 필요한 메서드만 상속 받아서 사용할 수 있도록 전화/메세지/LTE를 각각 따로따로 인터페이스로 구현하여 분리해줘야 한다는 것이 ISP이다.
ISP 위배
// 기본적인 핸드폰 인터페이스
public interface Phone {
void call(String number);
void sendMessage(String message, String number);
void internet();
}
// 스마트폰
public class Smartphone implements Phone {
@Override
public void call(String number) {
System.out.println("Making a call to: " + number);
}
@Override
public void sendMessage(String message, String number) {
System.out.println("Sending message '" + message + "' to: " + number);
}
@Override
public void internet() {
System.out.println("인터넷에 연결 되었습니다.");
}
}
// 2G 핸드폰
public class BasicPhone implements Phone {
@Override
public void call(String number) {
System.out.println("Making a call to: " + number);
}
@Override
public void sendMessage(String message, String number) {
System.out.println("Sending message '" + message + "' to: " + number);
}
@Override
public void internet() {
System.out.println("지원하지 않는 기능입니다.");
}
}
2G 핸드폰에서 쓸모 없는 인터넷 기능이 추가되어 있다.
ISP 준수
// 기본적인 핸드폰 인터페이스
public interface call {
void call(String number);
}
public interface sendMessage {
void sendMessage(String message, String number);
}
public interface internet {
void internet();
}
// 스마트폰
public class Smartphone implements call, sendMessage, internet {
@Override
public void call(String number) {
System.out.println("Making a call to: " + number);
}
@Override
public void sendMessage(String message, String number) {
System.out.println("Sending message '" + message + "' to: " + number);
}
@Override
public void internet() {
System.out.println("인터넷에 연결 되었습니다.");
}
}
// 2G 핸드폰
public class BasicPhone implements call, sendMessage {
@Override
public void call(String number) {
System.out.println("Making a call to: " + number);
}
@Override
public void sendMessage(String message, String number) {
System.out.println("Sending message '" + message + "' to: " + number);
}
}
각각의 기능을 인터페이스로 분리해주면서 2G 핸드폰에는 인터넷 기능을 상속 받지 않도록 했다.
따라서 ISP를 준수한다.
4. LSP(Liskov Substitution Principle : 리스코프 치환 원칙)
부모 클래스(객체)와 이를 상속한 자식 클래스(객체)가 있을 때 자식 객체가 부모 객체를 완전히 대체할 수 있어야 한다.
결국 객체지향 프로그래밍의 특징인 다형성을 이야기하는 것이다.
객체지향 프로그래밍에서는 상속이라는 개념이 존재한다. 부모의 특성을 자식이 똑같이 가지는 것이다. 이를 활용하여 트리 모양으로 프로그램을 확장해 나갈 수 있다.
하지만 이 과정에서 어긋난 방향으로 상속을 받아 확장되는 경우가 생긴다. 리스코프 치환 원칙은 자식 객체가 확장하는 방향이 온전히 부모 객체를 따르도록 하는 원칙이다.
그 중 가장 흔하게 예를 드는 것이 직사각형과 정사각형이다.
정사각형도 네 개의 변을 가지고 있는 직사각형의 한 종류라고 볼 수 있다. 다음 코드에서는 정사각형 객체가 직사각형 객체를 상속 받았을 때 발생하는 문제이다.
LSP 위배
class Rectangle {
public int width;
public int height;
// 너비 반환, Width Getter
public int getWidth() {
return width;
}
// 너비 할당, Width Setter
public void setWidth(int width) {
this.width = width;
}
// 높이 반환, Height Getter
public int getHeight() {
return height;
}
// 높이 할당, Height Setter
public void setHeight(int height) {
this.height = height;
}
//직사각형 넓이 반환 함수
public int getArea() {
return width * height;
}
}
public class Main {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.setHeight(5);
rectangle.setWidth(10);
System.out.println(rectangle.getArea()); // 50 출력
}
}
직사각형은 정상적으로 넓이 50이 출력된다.
그렇다면 정사각형은 어떨까?
앞서 말한 LSP에 따르면 부모 객체를 완전히 대체할 수 있어야 한다고 했다. 그렇다면 같은 값으로 세팅을 하면 같은 출력이 나와야 하는 것이 당연하다.
class Square extends Rectangle{
@Override
public void setHeight(int height) {
super.setHeight(height);
super.setWidth(getHeight());
}
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(getWidth());
}
}
public class Main {
public static void main(String[] args) {
Square square = new Square();
square.setHeight(10);
square.setHeight(5);
System.out.println(square.getArea()); // 25 출력
}
}
25가 출력됐다. 코드 상으로는 마지막에 5로 set을 해줬기 때문에 당연한 결과다.
결과적으로 보면 제대로 동작하는 코드이지만, 직사각형 클래스의 동작과 그를 상속 받은 자식 클래스인 정사각형 클래스의 동작이 완전히 다르다는 것을 알 수 있다.
즉, LSP를 위반하는 코드이다.
LSP 준수
도형으로 정의하는 것이 아닌 Shape이라는 더 상위 개념으로 정의한다.
public class Shape {
public int width;
public int height;
// 너비 반환, Width Getter
public int getWidth() {
return width;
}
// 너비 할당, Width Setter
public void setWidth(int width) {
this.width = width;
}
// 높이 반환, Height Getter
public int getHeight() {
return height;
}
// 높이 할당, Height Setter
public void setHeight(int height) {
this.height = height;
}
// 사각형 넓이 반환
public int getArea() {
return width * height;
}
}
//직사각형 클래스
public class Rectangle extends Shape {
public Rectangle(int width, int height) {
setWidth(width);
setHeight(height);
}
}
//정사각형 클래스
public class Square extends Shape{
public Square(int length) {
setWidth(length);
setHeight(length);
}
}
public class Main {
public static void main(String[] args) {
Shape rectangle = new Rectangle(10, 5);
Shape square = new Square(5);
System.out.println(rectangle.getArea()); // 50
System.out.println(square.getArea()); // 25
}
}
더 이상 Square Class와 Rectangle Class는 더 이상 상속 관계가 아니므로, LSP를 준수한다.
5. DIP(Dependency Inversion Principle : 의존 역전 원칙)
고수준 모듈은 저수준 모듈에 의존해서는 안된다.
구체화에 의존하면 안되고 추상화에 의존해야 한다.
즉, 변경할 일이 거의 없는 고수준 모듈이, 변경이 잦은 저수준 모듈에 의존하면 안된다는 것이다.
대신 저수준 모듈이 고수준 모듈에 정의한 추상화 타입에 의존해야 한다.
고수준 모듈 : 어떤 의미 있는 단일 기능을 제공하는 모듈 (interface, 추상 클래스)
저수준 모듈 : 고수준 모듈의 기능을 구현하기 위해 필요한 하위 기능의 실제 구현 (메인클래스, 객체)
요리로 비유를 하자면 고수준 모듈은 요리사이고 저수준 모듈은 식재료이다.
요리사는 바뀔 일이 거의 없지만 식재료는 수시로 바뀌기 때문이다.
DIP 위배
public class Chef {
private Pizza recipe;
public void setRecipe(Pizza recipe) {
this.recipe = recipe;
}
public void cook() {
System.out.println(recipe.toString());
}
}
public class Main{
public static void main(String[] args) {
Pizza pizza = new Pizza();
Chef c = new Chef();
c.setRecipe(pizza);
c.cook();
}
}
위 코드에서 Chef는 Pizza라는 객체에 의존하고 있다. 이러한 코드의 경우 요리가 변경될 경우 다음과 같이 바뀌게 된다.
public class Chef {
private Pizza recipe;
private Chicken recipe; // 새로운 메뉴 추가
// 요리사가 가지고 있는 레시피 만큼 Chef 클래스 내에 메서드가 존재해야함.
public void setRecipe(Pizza recipe) {
this.recipe = recipe;
}
// Overloading
public void setRecipe(Chicken recipe) {
this.recipe = recipe;
}
public void cook() {
System.out.println(recipe.toString());
}
}
위와 같이 요리사라는 고수준 모듈이 변하기 쉬운 재료라는 저수준 모듈에게 영향을 받는 일이 발생한다.
재료를 하나 더 추가하기 위해 Chef라는 클래스에서 수정이 일어나기 때문이다.
DIP 준수
public class Chef {
private Recipe recipe;
public void setRecipe(Recipe recipe) {
this.recipe = recipe;
}
public void cook() {
System.out.println(recipe.toString());
}
Chef 클래스 안에서 더 이상 구체적인 메뉴를 생성하지 않는다. Recipe라는 더 큰 개념의 클래스를 의존하고 있다.
setRecipe를 통해 재료를 변경할 수 있다.
만약 피자를 만들 계획이라면 Recipe라는 추상 클래스를 상속받아 다음과 같이 Pizza 객체를 구한다.
public class Pizza extends Recipe {
public String toString() {
return "Pizza";
}
}
public class Main{
public static void main(String[] args) {
Recipe pizza = new Pizza();
Chef c = new Chef();
c.setRecipe(pizza);
c.cook();
}
}
만약 계획이 바뀌어 치킨을 만들어야 한다면 똑같이 Recipe를 상속받는 Chicken 객체를 만들면 된다.
public class Chicken extends Recipe {
public String toString() {
return "Chicken";
}
}
public class Main{
public static void main(String[] args) {
Recipe chicken = new Chicken();
Chef c = new Chef();
c.setRecipe(chicken);
c.cook();
}
}
새로운 음식을 요리했지만, Chef 클래스의 코드는 건드리지 않는다.
'Computer Science' 카테고리의 다른 글
간단 정리 - is-a관계와 has-a관계 (0) | 2024.04.08 |
---|---|
Inheritance(상속), Association(연관), Aggregation(집합), Composition(구성) in JAVA (0) | 2024.04.08 |
객체지향이란?
객체지향이란 프로그램 설계 방법론의 일종이다. 프로그램에 필요한 데이터를 추상화 시키고 객체(Object)라는 기본 단위를 만든다. 이렇게 만들어진 객체끼리 상호작용을 하는 것이 객체지향 프로그램이다. 객체 지향 프로그램은 유연한 유지 보수, 손쉬운 확장이 쉬워진다. 즉, 개발의 생산성이 높아진다.
모든 일에는 메뉴얼이라는 것이 존재하고 이는 길잡이 역할을 한다. SOLID 원칙이란 객체 지향 프로그램의 메뉴얼이다.
객체 지향 프로그래밍을 사용하는 여러 디자인 패턴은 모두 SOLID 원칙에 입각하여 만들어진 것이기 때문에, SOLID 원칙에 대한 개념을 제대로 잡고 가는 것이 중요하다.
1. SRP(Sigle Responsibility Principle : 단일 책임의 원칙)
클래스(객체)는 단 하나의 책임만 가져야 한다.클래스(객체)가 변경되는 이유가 한가지여야 한다.
클래스나 객체는 하나의 기능만 담당하도록 구성해야 한다는 것이다.
해당 클래스나 객체가 여러 대상, 액터에 대해 책임을 가져서는 안되고, 오직 하나의 액터에 대해서만 책임을 져야 한다는 것이는 표현이 더 정확하다고 한다. 즉, 클래스가 변경되는 이유가 하나이어야 한다라는 것.
예를 들어, 웹 페이지에서 User 클래스가 회원가입 기능을 가지고 있음과 동시에 로그인을 처리하는 기능 또한 가진다고 가정해보자. 이러한 경우, 회원가입 방식이 변경되면 User 클래스의 코드를 수정해야 한다.
이 수정은 로그인 기능과는 전혀 관련이 없기 때문에 SRP에 위배된다고 볼 수 있다.
SRP 위배
public class UserManager {
private Database db;
public UserManager() {
this.db = new Database();
}
public void saveUser(User user) {
// 회원가입 로직
db.save(user);
}
public boolean authenticateUser(String username, String password) {
// 로그인 로직
User user = db.getUser(username);
return user != null && user.getPassword().equals(password);
}
}
UserManager 클래스 안에 회원가입과 로그인 로직이 모두 존재한다.
따라서 SRP에 위배된다.
SRP 준수
public class UserManager {
private Database db;
public UserManager() {
this.db = new Database();
}
public void saveUser(User user) {
// 회원가입 로직
db.save(user);
}
}
public class Authenticator {
private Database db;
public Authenticator() {
this.db = new Database();
}
public boolean authenticateUser(String username, String password) {
// 로그인 로직
User user = db.getUser(username);
return user != null && user.getPassword().equals(password);
}
}
회원가입은 UserManager 클래스에 로그인은 Authenticator 클래스에 분리해서 구현했다.
따라서 SRP 준수한다.
2. OCP(Open-Closed Principle : 개방-폐쇄 원칙)
클래스(객체)는 확장에 대해 열려있고 수정에 대해 닫혀있어야 한다.
확장에 대해 열려있다 : 요구 사항이 변경될 때 새로운 로직을 추가하여 기능을 확장할 수 있어야 한다.
수정에 대해 닫혀있다 : 기존의 코드를 수정하지 않아도 기능을 추가하거나 변경할 수 있어야 한다.
즉, 프로그램의 기능을 변경할 때 기존의 코드를 수정하지 않고 확장을 통해 변경할 수 있어야 한다.
아래는 OCP 설명하기 위한 코드이다. 도형을 그리는 기능을 구현하고 있다.
OCP 위배
public class Shape {
private String type;
public Shape(String type) {
this.type = type;
}
public void draw() {
if (type.equals("circle")) {
System.out.println("Draw Circle");
} else if (type.equals("square")) {
System.out.println("Draw Square");
}
// 새로운 도형 종류가 추가될 때마다 draw 메서드를 수정해야 함
}
}
위 코드는 새로운 도형이 추가될 때마다 darw 메서드를 계속해서 수정해야 한다.
따라서 OCP에 위배된다.
OCP 준수
public interface Shape {
void draw();
}
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Draw Circle");
}
}
public class Square implements Shape {
@Override
public void draw() {
System.out.println("Draw Square");
}
}
OCP가 본질적으로 의미하는 것은 추상화이다.
위 코드에서 가장 상위 클래스는 Shape을 Interface로 만들어 추상화하고 새로운 도형이 만들어질 때마다 새로운 Class가 만들어지고 Shape Interface를 상속받는다.
기존의 코드를 수정하지 않기 때문에 OCP를 준수한다.
3. ISP(Interface Segregation Principle : 인터페이스 분리 원칙)
클래스(객체)는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.
클라이언트의 목적과 용도에 적합한 인터페이스만을 제공해주어야 한다.
구현할 객체의 무의미한 메서드 구현을 방지하기 위해 반드시 필요한 메서드만을 상속/구현하게 한다.
스마트폰과 2G 핸드폰을 만들기 위해 하나의 추상 클래스를 만들었다고 가정해보자. 이 추상 클래스에는 전화, 메세지, LTE의 기능이 구현되어 있다.
스마트폰은 이 추상 클래스를 그대로 상속 받아서 구현하면 될 것이다.
그러나 2G 핸드폰은 이 클래스를 그대로 상속 받으면 LTE이라는 사용하지도 않는 메서드를 구현하게 되는 상황이 발생한다.
각 객체가 필요한 메서드만 상속 받아서 사용할 수 있도록 전화/메세지/LTE를 각각 따로따로 인터페이스로 구현하여 분리해줘야 한다는 것이 ISP이다.
ISP 위배
// 기본적인 핸드폰 인터페이스
public interface Phone {
void call(String number);
void sendMessage(String message, String number);
void internet();
}
// 스마트폰
public class Smartphone implements Phone {
@Override
public void call(String number) {
System.out.println("Making a call to: " + number);
}
@Override
public void sendMessage(String message, String number) {
System.out.println("Sending message '" + message + "' to: " + number);
}
@Override
public void internet() {
System.out.println("인터넷에 연결 되었습니다.");
}
}
// 2G 핸드폰
public class BasicPhone implements Phone {
@Override
public void call(String number) {
System.out.println("Making a call to: " + number);
}
@Override
public void sendMessage(String message, String number) {
System.out.println("Sending message '" + message + "' to: " + number);
}
@Override
public void internet() {
System.out.println("지원하지 않는 기능입니다.");
}
}
2G 핸드폰에서 쓸모 없는 인터넷 기능이 추가되어 있다.
ISP 준수
// 기본적인 핸드폰 인터페이스
public interface call {
void call(String number);
}
public interface sendMessage {
void sendMessage(String message, String number);
}
public interface internet {
void internet();
}
// 스마트폰
public class Smartphone implements call, sendMessage, internet {
@Override
public void call(String number) {
System.out.println("Making a call to: " + number);
}
@Override
public void sendMessage(String message, String number) {
System.out.println("Sending message '" + message + "' to: " + number);
}
@Override
public void internet() {
System.out.println("인터넷에 연결 되었습니다.");
}
}
// 2G 핸드폰
public class BasicPhone implements call, sendMessage {
@Override
public void call(String number) {
System.out.println("Making a call to: " + number);
}
@Override
public void sendMessage(String message, String number) {
System.out.println("Sending message '" + message + "' to: " + number);
}
}
각각의 기능을 인터페이스로 분리해주면서 2G 핸드폰에는 인터넷 기능을 상속 받지 않도록 했다.
따라서 ISP를 준수한다.
4. LSP(Liskov Substitution Principle : 리스코프 치환 원칙)
부모 클래스(객체)와 이를 상속한 자식 클래스(객체)가 있을 때 자식 객체가 부모 객체를 완전히 대체할 수 있어야 한다.
결국 객체지향 프로그래밍의 특징인 다형성을 이야기하는 것이다.
객체지향 프로그래밍에서는 상속이라는 개념이 존재한다. 부모의 특성을 자식이 똑같이 가지는 것이다. 이를 활용하여 트리 모양으로 프로그램을 확장해 나갈 수 있다.
하지만 이 과정에서 어긋난 방향으로 상속을 받아 확장되는 경우가 생긴다. 리스코프 치환 원칙은 자식 객체가 확장하는 방향이 온전히 부모 객체를 따르도록 하는 원칙이다.
그 중 가장 흔하게 예를 드는 것이 직사각형과 정사각형이다.
정사각형도 네 개의 변을 가지고 있는 직사각형의 한 종류라고 볼 수 있다. 다음 코드에서는 정사각형 객체가 직사각형 객체를 상속 받았을 때 발생하는 문제이다.
LSP 위배
class Rectangle {
public int width;
public int height;
// 너비 반환, Width Getter
public int getWidth() {
return width;
}
// 너비 할당, Width Setter
public void setWidth(int width) {
this.width = width;
}
// 높이 반환, Height Getter
public int getHeight() {
return height;
}
// 높이 할당, Height Setter
public void setHeight(int height) {
this.height = height;
}
//직사각형 넓이 반환 함수
public int getArea() {
return width * height;
}
}
public class Main {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.setHeight(5);
rectangle.setWidth(10);
System.out.println(rectangle.getArea()); // 50 출력
}
}
직사각형은 정상적으로 넓이 50이 출력된다.
그렇다면 정사각형은 어떨까?
앞서 말한 LSP에 따르면 부모 객체를 완전히 대체할 수 있어야 한다고 했다. 그렇다면 같은 값으로 세팅을 하면 같은 출력이 나와야 하는 것이 당연하다.
class Square extends Rectangle{
@Override
public void setHeight(int height) {
super.setHeight(height);
super.setWidth(getHeight());
}
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(getWidth());
}
}
public class Main {
public static void main(String[] args) {
Square square = new Square();
square.setHeight(10);
square.setHeight(5);
System.out.println(square.getArea()); // 25 출력
}
}
25가 출력됐다. 코드 상으로는 마지막에 5로 set을 해줬기 때문에 당연한 결과다.
결과적으로 보면 제대로 동작하는 코드이지만, 직사각형 클래스의 동작과 그를 상속 받은 자식 클래스인 정사각형 클래스의 동작이 완전히 다르다는 것을 알 수 있다.
즉, LSP를 위반하는 코드이다.
LSP 준수
도형으로 정의하는 것이 아닌 Shape이라는 더 상위 개념으로 정의한다.
public class Shape {
public int width;
public int height;
// 너비 반환, Width Getter
public int getWidth() {
return width;
}
// 너비 할당, Width Setter
public void setWidth(int width) {
this.width = width;
}
// 높이 반환, Height Getter
public int getHeight() {
return height;
}
// 높이 할당, Height Setter
public void setHeight(int height) {
this.height = height;
}
// 사각형 넓이 반환
public int getArea() {
return width * height;
}
}
//직사각형 클래스
public class Rectangle extends Shape {
public Rectangle(int width, int height) {
setWidth(width);
setHeight(height);
}
}
//정사각형 클래스
public class Square extends Shape{
public Square(int length) {
setWidth(length);
setHeight(length);
}
}
public class Main {
public static void main(String[] args) {
Shape rectangle = new Rectangle(10, 5);
Shape square = new Square(5);
System.out.println(rectangle.getArea()); // 50
System.out.println(square.getArea()); // 25
}
}
더 이상 Square Class와 Rectangle Class는 더 이상 상속 관계가 아니므로, LSP를 준수한다.
5. DIP(Dependency Inversion Principle : 의존 역전 원칙)
고수준 모듈은 저수준 모듈에 의존해서는 안된다.
구체화에 의존하면 안되고 추상화에 의존해야 한다.
즉, 변경할 일이 거의 없는 고수준 모듈이, 변경이 잦은 저수준 모듈에 의존하면 안된다는 것이다.
대신 저수준 모듈이 고수준 모듈에 정의한 추상화 타입에 의존해야 한다.
고수준 모듈 : 어떤 의미 있는 단일 기능을 제공하는 모듈 (interface, 추상 클래스)
저수준 모듈 : 고수준 모듈의 기능을 구현하기 위해 필요한 하위 기능의 실제 구현 (메인클래스, 객체)
요리로 비유를 하자면 고수준 모듈은 요리사이고 저수준 모듈은 식재료이다.
요리사는 바뀔 일이 거의 없지만 식재료는 수시로 바뀌기 때문이다.
DIP 위배
public class Chef {
private Pizza recipe;
public void setRecipe(Pizza recipe) {
this.recipe = recipe;
}
public void cook() {
System.out.println(recipe.toString());
}
}
public class Main{
public static void main(String[] args) {
Pizza pizza = new Pizza();
Chef c = new Chef();
c.setRecipe(pizza);
c.cook();
}
}
위 코드에서 Chef는 Pizza라는 객체에 의존하고 있다. 이러한 코드의 경우 요리가 변경될 경우 다음과 같이 바뀌게 된다.
public class Chef {
private Pizza recipe;
private Chicken recipe; // 새로운 메뉴 추가
// 요리사가 가지고 있는 레시피 만큼 Chef 클래스 내에 메서드가 존재해야함.
public void setRecipe(Pizza recipe) {
this.recipe = recipe;
}
// Overloading
public void setRecipe(Chicken recipe) {
this.recipe = recipe;
}
public void cook() {
System.out.println(recipe.toString());
}
}
위와 같이 요리사라는 고수준 모듈이 변하기 쉬운 재료라는 저수준 모듈에게 영향을 받는 일이 발생한다.
재료를 하나 더 추가하기 위해 Chef라는 클래스에서 수정이 일어나기 때문이다.
DIP 준수
public class Chef {
private Recipe recipe;
public void setRecipe(Recipe recipe) {
this.recipe = recipe;
}
public void cook() {
System.out.println(recipe.toString());
}
Chef 클래스 안에서 더 이상 구체적인 메뉴를 생성하지 않는다. Recipe라는 더 큰 개념의 클래스를 의존하고 있다.
setRecipe를 통해 재료를 변경할 수 있다.
만약 피자를 만들 계획이라면 Recipe라는 추상 클래스를 상속받아 다음과 같이 Pizza 객체를 구한다.
public class Pizza extends Recipe {
public String toString() {
return "Pizza";
}
}
public class Main{
public static void main(String[] args) {
Recipe pizza = new Pizza();
Chef c = new Chef();
c.setRecipe(pizza);
c.cook();
}
}
만약 계획이 바뀌어 치킨을 만들어야 한다면 똑같이 Recipe를 상속받는 Chicken 객체를 만들면 된다.
public class Chicken extends Recipe {
public String toString() {
return "Chicken";
}
}
public class Main{
public static void main(String[] args) {
Recipe chicken = new Chicken();
Chef c = new Chef();
c.setRecipe(chicken);
c.cook();
}
}
새로운 음식을 요리했지만, Chef 클래스의 코드는 건드리지 않는다.
'Computer Science' 카테고리의 다른 글
간단 정리 - is-a관계와 has-a관계 (0) | 2024.04.08 |
---|---|
Inheritance(상속), Association(연관), Aggregation(집합), Composition(구성) in JAVA (0) | 2024.04.08 |