From 4856028b2556d11a99efc926f51ae44bdee65fac Mon Sep 17 00:00:00 2001 From: Sonwonill Date: Wed, 7 Aug 2024 16:00:35 +0900 Subject: [PATCH 1/2] =?UTF-8?q?(add)=20:=20SOLID,=20=EA=B2=8C=EC=9E=84?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=EC=97=90=EC=84=9C=20SOLID=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\354\206\220\354\233\220\354\235\274.md" | 418 ++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 "Computer Science/OOP/SOLID/\354\206\220\354\233\220\354\235\274.md" diff --git "a/Computer Science/OOP/SOLID/\354\206\220\354\233\220\354\235\274.md" "b/Computer Science/OOP/SOLID/\354\206\220\354\233\220\354\235\274.md" new file mode 100644 index 0000000..d5dcb0d --- /dev/null +++ "b/Computer Science/OOP/SOLID/\354\206\220\354\233\220\354\235\274.md" @@ -0,0 +1,418 @@ +# SOLID와 게임 개발에서 SOLID 적용 +일시 : 24.08.08

+## SOLID란? +객체 지향 프로그래밍을 하면서 지켜야하는 5대 원칙입니다. +### S(SRP, Single Responsibility Principle : 단일 책임의 원칙) + 하나의 모듈이 하나의 책임을 가져야 한다는 원칙으로, 해당 클래스를 수정해야하는 이유가 하나여야만 합니다.
+ 단순한 예를 들어 UserService에서 addUser함수를 통해 사용자를 추가하게 될 때 addUser에서 '사용자에게 역할을 부여'와 'email과 password를 암호화'하는 두가지 기능을 수행하게 될 때 사용자와 관련된 서비스에서 사용자 관련 기능 수정 뿐 아니라 암호화와 관련된 수정이 필요할때 UserService를 수정해야해서 이유가 두가지가 될 수 있습니다. +```Java +@Servcie +public class UserService{ + @Autowired + UserRepository repo; + + public void addUser(String email, Strin pwd){ + // email과 pwd를 암호화하는 구문 => 보안 + // 역할을 부여야하는 구문 => 기획 + User user = new User(encrytedEmail, encrytedPwd, role); + + repo.addUser(user); + } + +} +``` + 암호화하는 부분을 컴포넌트로 분리하여서 SRP 지킬 수 있게 하는 것이 타당하다고 생각합니다. +```Java +@Component +public class Encrytor{ + + public String encrytStr(String input){ + // 암호화하는 구문 + return output; + } + +} + +@Service +public class UserService{ + @Autowired + UserRepository repo; + @Autowired + Encrytor encrytor; + + public void addUser(String email, String pwd){ + String encrytedEmail = encrytor.encrytStr(email); + String encrytedPwd = encrytor.encrytStr(pwd); + User user = new User(encrytedEmail, encrytedPwd, role); + + repo.addUser(user); + } + +} +``` +- 장점 : + - 변경이 필요할 때 수정할 대상이 명확해진다. + - 의존성 분리가 되어 변경에 따른 대상만 수정하면 되게 된다.
+ +  단, 요구사항이나, 관련 함수의 사용 용도에 따라 단일 책임이 될수도, 안될 수도 있기 때문에 설계시 잘 고려하여야한다. +### O(OCP, Open-Closed Principle : 개방 폐쇄의 원칙) + 확장에는 열려있고, 변경(수정)에는 닫혀 있어야한다는 원칙으로, 요구사항이 변경될 때 기존의 코드를 변경(수정)하지 않고, 새로운 동작을 추가하여 애플리케이션의 기능, 동작을 확장 할 수 있어야하는 원칙입니다.
+ 기존의 코드를 작성할 때 기능을 사용하는 부분에서 interface나 추상클래스를 통해 메소드를 호출하게 작성하게 되면 추가하고자하는 기능을 해당 interface나 추상 클래스를 구현 혹은 상속 받는 새로운 클래스를 생성하여 기능 확장 후 사용하는 곳에 생성한 클래스를 주입함으로 기능을 확장하는 방식을 사용해야하는 것입니다.
+  기존에는 어떠한 인터페이스나 추상클래스를 구현 혹은 상속하고 있지 않는 Encrytor를 통해 암호화하였는데 새로운 방식의 암호화 방식을 도입하고자 새로운 클래스를 만들고 적용을 하기 위해선 UserService의 Encrytor 부분을 수정을 해야지 적용이됩니다. 이를 방지 하기 위해서 interfac로 Encrytor를 생성하고 Encrytor를 구현하는 암호화 클래스를 생성하고 필요한 구문을 작성하고, UserService 부분에서는 인터페이스를 통해 함수를 호출하게 되면 UserService를 수정하지 않고도 기능을 확장할 수 있게 됩니다. +```Java +//OCP가 지켜지지 않은 코드 +@Component +public class Encrytor{ + + public String encrytStr(String input){ + // 암호화하는 구문 + return output; + } + +} + +@Component +public class NewEncrytor{ + public String encryStr(String input){ + return output; + } +} + +@Service +public class UserService{ + @Autowired + UserRepository repo; + @Autowired + //Encrytor encrytor; + NewEncrytor encrytor; + + public void addUser(String email, String pwd){ + String encrytedEmail = encrytor.encrytStr(email); + String encrytedPwd = encrytor.encrytStr(pwd); + User user = new User(encrytedEmail, encrytedPwd, role); + + repo.addUser(user); + } + +} +//OCP를 지키기 위해 새로 작성한 코드 +public interface Encrytor{ + public String encryStr(String input); +} + +@Component +public class OldEncrytor implements Encrytor{ + + @Override + public String encrytStr(String input){ + // 암호화하는 구문 + return output; + } + +} + +@Component +public class NewEncrytor implements Encrytor{ + + @Override + public String encryStr(String input){ + return output; + } +} + +@Service +public class UserService{ + @Autowired + UserRepository repo; + + private Encrytor encrytor; + + ... + + public void setEncrytor(Encrytor encrytor){ + this.encrytor = encrytor; + } + + public void addUser(String email, String pwd){ + String encrytedEmail = encrytor.encrytStr(email); + String encrytedPwd = encrytor.encrytStr(pwd); + User user = new User(encrytedEmail, encrytedPwd, role); + + repo.addUser(user); + } + +} + +``` +  추후 확장 가능성이 없는 것까지 추상화를 통해 인터페이스나 추상클래스로 작성하는 것은 과도한 설계가 될 수 있기 때문에, 설계 시 어느 정도 예측 가능한 부분을 미리 준비하되 개발에 부하가 되지 않게 하는 것이 좋으며, 필요시 추후 리팩터링을 통해 개선하는 것이 좋습니다. + +### L(LSP, Liskov Substitution Principle : 리스코프 치환 원칙) +  하위 타입은 상위 타입으로 대체될 수 있어야한다라는 원칙으로 상위타입을 통해 하위타입을 사용하게 되었을 때 문제가 발생하여서는 안된다는 것입니다.
+  예를 들어 정사각형은 직사각형이다라는 명제는 참이기 때문에 정사각형을 직사각형의 하위 타입으로 클래스를 설계하였을 경우 +```Java +@Getter @Setter +public class Rectangle{ + private int width, height; +} + +public class Square extends Rectangel{ + + public Square(int length){ + setWidth(length); + setHeight(length); + } + + @Override + public void setWidth(int width){ + this.width = width; + this.height = hegith; + } + + @Override + public void setHeight(int height){ + this.width = height; + this.height = height; + } +} +... + +public void resize(Rectangle rect, int width, int height){ + rect.setWidth(width); + rect.setHeight(height); + if(rect.getWidth() != width || rect.getHeight() != height){ + // 예외 발생 + } +} +``` +  resize가 제대로 동작하지 않는다는 점에서 리스코프치환이 정상적으로 동작하지 않을 뿐더러 피터코드의 상속규칙 중 '자식 클래스는 부모 클래스의 책임을 무시하거나, 재정의하지 않고 확장만을 수행해야한다'라는 규칙도 위반 됩니다. 정사각형이 직사각형을 상속하는 것이 아닌 사각형이라는 추상클래스를 직사각형과 정사각형이 상속받아 크기 변경시 정사각형은 두 개의 길이가 같은지를 비교하여 같을 때 변경하게끔 수정하는 것이 옳다고 생각합니다. 아니면 Resizable이라는 인터페이스를 통해 크기변경을 별도로 사용하는 것도 하나의 방법이라고 생각합니다. +```Java +@Getter @Setter +public class Quadrangle{ + private int width, height; + + public abstract void resize(int width, int height); +} + +public class Rectangle extends Qudrangle{ + @Override + public void resize(int width, int height){ + setWidth(width); + setHeight(height); + } +} + +public class Square extends Qudrangle{ + @Override + public void resize(int width, int height){ + if(width != height) return; + setWidth(width); + setHeight(height); + } +} +-------------------------- +public interface Resizable{ + public void resize(int... length); +} + +public class Rectangle implements Resizable{ + @Override + public void resize(int... length){ + if(length.length != 2) // 직사각형은 2개의 길이가 필요합니다 예외 발생 + setWidth(length[0]); + setHeight(length[1]); + } +} + +public class Square extends Rectangle implements Resizable{ + @Override + public void resize(int... length){ + if(length.length != 1) // 정사각형은 1개의 길이가 필요합니다 예외 발생 + setWidth(length[0]); + setHeight(length[0]); + } +} + +``` +※ 올바른 예외가 맞는지는 확신은 없습니다(스터디하면서 검토 후 맞지 않다면 수정할 계획) ※ +### I(ISP, Interface Segregation principle : 인터페이스 분리 원칙) + 클라이언트의 목적과 용도에 적합한 인터페이스만을 제공한다는 원칙으로, 기능을 인터페이스로 분리하여 필요로 하는 기능외 다른 기능의 간섭을 받게 해서는 안된다는 것입니다. +```Java +public class 복합기{ + // 복사기 기능 + // fax 기능 + // 프린터 기능 + // 스캐너 기능 +} +``` + 클라이언트는 복합기에서 복사기 기능을 사용하려 하지만 복합기를 통해 사용할 경우 그 외의 fax, 프린터, 스캐너의 기능도 있기에 간섭이 발생할 수 있습니다. +``` +public interface 복사기{ + // 복사기 기능 +} +public interface fax{ + // fax 기능 +} +public interface 프린터{ + // 프린터 기능 +} +public interface 스캐너{ + // 스캐너 기능 +} + +public class 복합기 implements 복사기, fax, 프린터, 스캐너{ + // 인터페이스 기능들 재정의 +} +``` + 클라이언트가 복사기 기능을 사용하고자하면 복사기를 통해 복합기에 접근하게 되면 복사기 기능만을 사용하기에 간섭이 발생할 수 없습니다.
+ 인터페이스 분리가 잘 지켜졌다면 변경에 영향을 안받을 수 있다는 장점이 있습니다. + +### D(DIP, Dependency Inversion Principle : 의존 역전 원칙 ) + 구체화에 의존이 아닌 추상화에 의존해야하는 것으로, 고수준 모듈이 저수준 모듈의 구현에 의존하여선 안되고, 고수준 모듈은 저수준 모듈이 구현하고 있는 interface에 의존해야하는 것입니다.
+- 고수준 모듈 : 본질적인 기능을 담당하는 모듈 +- 저수준 모듈 : 고수준 모듈의 기능을 수행하기 위해 도와주는 모듈
+ +```Java +class SnowTire{ + public void run(){ + System.out.println("스노우 타이어로 달리는 중"); + } +} +class RegularTire{ + public void run(){ + System.out.println("일반 타이어로 달리는 중"); + } +} + +class Car{ + private SnowTire t; + // 저수준 모듈의 구현에 의존하고 있기 때문에 + // Car에서 수정을 하여야지 SnowTire를 RegularTire로 변경이 가능해진다. + + public Car(SnowTire t){ + this.t = t; + } + + public void runCar(){ + t.run(); + } + +} +---------------------------- +interface Tire{ + public void run(); +} + +class SnowTire implements Tire{ + @Override + public void run(){ + System.out.println("스노우 타이어로 달리는 중"); + } +} +class RegularTire implements Tire{ + @Override + public void run(){ + System.out.println("일반 타이어로 달리는 중"); + } +} + +class Car{ + private Tire t; + // 저수준 모듈의 interface에 의존하고 있기 때문에 + // Car에서 코드 수정이 아닌 setTire를 호출하여 Tire의 주입되는 + // 의존 객체를 변경함으로 SnowTire, RegularTire로 변경이 가능하다. + + public Car(Tire t){ + this.t = t; + } + + public void runCar(){ + t.run(); + } + + public void setTire(Tire t){ + this.t = t; + } +} +``` + +### 정리 + 어떻게 보면 객체 지향적으로 프로그램을 작성하기 위해서는 당연히 이루어져야하는 부분이라고 생각하지만 막상 코드를 작성하기전에 설계 단계에서 이 모든 원칙을 생각하면서 설계하는 것은 굉장히 어렵다고 생각합니다. 특히 OCP의 경우 예상치 못한 기능을 확장해야 하는 경우도 있기 때문입니다.
+ 설계시 자주 변해야하는 부분은 DIP와 LSP를 잘생각하여 작성해야 OCP를 잘 지킬 수 있을 것이고, SRP와 ISP를 생각하면서 클래스를 설계해야 중복 코드 및 중복된 기능을 줄여 클래스가 방대해지는 것을 막아 가독성을 향상시킬 수 있을 거 같다고 생각하였습니다. + 원칙이 하나씩 적용되는게 아니라 연계적으로 이루어지는 거 같다고 생각합니다. + +### 객체 지향적으로 게임 개발을 하기 위해서는
SOLID를 어떻게 적용할 수 있을까? +※ 개인적인 생각이 많이 들어가는 부분으로 수정이 필요할 수 있습니다. 또한 해당 글은 게임 클라이언트에 집중적으로 작성되었습니다. ※ +FPS게임에서 생각을 해보았을 때 +- SRP의 경우 플레이어가 아이템 혹은 사물에 상호작용을 하게 되었을 때, 상호작용 키를 사용시 플레이어가 지정하는 대상의 interact 함수를 호출하는 방식으로 구현을 하면 될거 같습니다. +```C# +public interface Interactable{ + public int interact(); +} +``` +``` +public class Door : MonoBehavior, Interactable{ + public int interact(){ + //문이 닫혀있다면 열리는 동작, 열려있다면 닫히는 동작 + return itemKey; + // 문과 같은 사물에 대한 상호작용은 플레이어 측에서 별다른 처리를 하지 않음 + } +} +``` +``` +public class Gun : MonoBehavior, Interactable{ + public void interact(){ + // 총이 필드에서 삭제되고 + return itemKey; + // 아이템 키 값에 맞게 총 슬롯에 아이템 키 저장, ui에 표시 + } + + public virtual void shot(); +} +``` +``` +public class Gun5mm : Gun{ + + public override void shot(){ + //연발 + } +} +``` +``` +public class Gun9mm : Gun{ + + public override void shot(){ + //단발 + } +} +``` +``` +public class Player : MonoBehavior{ + + private Gun[] gunSlot = new Gun[2]; + private int currGunIndex = 0; + + public void callInteract(Interactable Object){ + int itemKey = Object.intreact(); + // 아이템 키에 따른 처리 + ... + } + + public void callShot(){ + gunSlot[currGunIndex].shot(); + } + +} + +``` +- 또한, Interactable이라는 인터페이스를 통해 기능을 분리하고 Player는 접근할때 Interface를 통해 접근하기 때문에 ISP와 DIP도 지켜졌다고 생각합니다. +- Player는 Interface로 접근하고 있기 때문에 새로운 아이템을 생성하거나, 구조물이 생겼을 때 Interactable을 구현하는 아이템이난 구조물에 한해서는 Player의 코드 수정 없이 기능이 확장 될 수 있기 때문에 OCP도 지켜졌다고 생각합니다. +-LSP는 상위타입인 Gun을 통해 gunSlot을 구현하고 gunSlot에는 하위 타입인 Gun5mm이나 Gun9mm이 들어갔을 때 shot이라는 함수를 호출 하였을 때 하위타입이 상위타입을 대체할 수 있기 때문에 지켜졌다고 생각합니다. + +  단편적으로 짧게 작성한 코드만으로 SOLID 원칙을 지켰다고 판단하기는 어렵지만 위의 코드를 작성한 방식 처럼 상위타입과 인터페이스를 사용하여 게임로직을 구현한다면 충분히 객체 지향의 장점인 높은 재사용성, 높은 유지보수성, 생산성 향상을 잘 활용하여 게임을 개발 할 수 있을 것 같습니다. + +--- +참고자료
+- **SOLID** + - https://mangkyu.tistory.com/194 + - https://velog.io/@pp8817/SOLID-%EC%A2%8B%EC%9D%80-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%EC%84%A4%EA%B3%84%EC%9D%98-5%EA%B0%80%EC%A7%80-%EC%9B%90%EC%B9%99 + - https://vanslog.io/posts/cs/oop/solid-principle/ \ No newline at end of file From 1f18f0315df7dc1158e9b5a0174b414f7934d58a Mon Sep 17 00:00:00 2001 From: Sonwonill Date: Fri, 9 Aug 2024 00:10:55 +0900 Subject: [PATCH 2/2] =?UTF-8?q?(update)=20:=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EA=B2=80=EC=88=98=20=EB=B0=8F=20=EC=96=91=EC=8B=9D=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OOP/SOLID/\354\206\220\354\233\220\354\235\274.md" | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git "a/Computer Science/OOP/SOLID/\354\206\220\354\233\220\354\235\274.md" "b/Computer Science/OOP/SOLID/\354\206\220\354\233\220\354\235\274.md" index d5dcb0d..7dadaf2 100644 --- "a/Computer Science/OOP/SOLID/\354\206\220\354\233\220\354\235\274.md" +++ "b/Computer Science/OOP/SOLID/\354\206\220\354\233\220\354\235\274.md" @@ -341,8 +341,8 @@ class Car{  원칙이 하나씩 적용되는게 아니라 연계적으로 이루어지는 거 같다고 생각합니다. ### 객체 지향적으로 게임 개발을 하기 위해서는
SOLID를 어떻게 적용할 수 있을까? -※ 개인적인 생각이 많이 들어가는 부분으로 수정이 필요할 수 있습니다. 또한 해당 글은 게임 클라이언트에 집중적으로 작성되었습니다. ※ -FPS게임에서 생각을 해보았을 때 +※ 개인적인 생각이 많이 들어가는 부분으로 수정이 필요할 수 있습니다. 또한 해당 글은 게임 클라이언트에 집중적으로 작성되었습니다. ※
+**FPS게임에서 생각을 해보았을 때** - SRP의 경우 플레이어가 아이템 혹은 사물에 상호작용을 하게 되었을 때, 상호작용 키를 사용시 플레이어가 지정하는 대상의 interact 함수를 호출하는 방식으로 구현을 하면 될거 같습니다. ```C# public interface Interactable{ @@ -360,7 +360,7 @@ public class Door : MonoBehavior, Interactable{ ``` ``` public class Gun : MonoBehavior, Interactable{ - public void interact(){ + public int interact(){ // 총이 필드에서 삭제되고 return itemKey; // 아이템 키 값에 맞게 총 슬롯에 아이템 키 저장, ui에 표시