객체지향을 검색하면 항상 SOLID라는 원칙이 따라다닌다.
이제는 객체지향이라는 단어만큼 친숙해졌지만, 이해는 하지 못한 원칙이다.
솔직히 SOLID 원칙을 모두 나만의 언어로 정리하여 작성하고 명확한 기준이 생긴다면
그때는 객체지향을 이해했다고 말할 수 있는 순간이 아닐까 싶다.
이 글을 시작으로 내가 생각하는 SOLID 원칙을 정리하고 앞으로 계속 수정하려고 한다.
(자신과의 약속…)
Single Responsibility Principle
단일 책임 원칙은 “객체는 단 하나의 책임만 가져야 한다.” 는 원칙이다.
이 책임이라는 말이 너무 모호하다.
책임의 기준을 어떻게 세워야 하고 세웠다면 어디서부터 어디까지가 책임인가? 모르겠다.
하지만 단일 책임을 “모듈(클래스)이 변경되는 이유는 한가지 여야 한다.” 로 이해한다면 아주 아주 조금은
더 이해하는 데 도움이 된다.
SRP를 이해하기 위해 많은 포스팅과 많은 예시 코드를 보면 액터(Actor) 개념이 중요하다는 것을 느꼈다.
나는 액터를 다음과 같이 정리하려고 한다.
어떤 형태로는 모듈(클래스, 메소드 …)를 사용,이용,호출하는 존재이다.
사용, 이용, 호출이요? 어떻게든 사용한다는 의미를 모두 표현하고 싶어서
내가 아는 단어를 다 적었다. 또한, 그게 모듈이든 클래스든, 메소드든 뭐든!!
어떠한 형태든 사용된다면 그건 액터가 사용하는 것이다.
아래 예제 코드는 아래 블로그를 참고해서 변수만을 바꾼 코드이다.
https://inpa.tistory.com/entry/OOP-💠-아주-쉽게-이해하는-SRP-단일-책임-원칙
class Student {
String name;
String position;
Student(String name, String position) {
this.name = name;
this.position = position;
}
// * 성적을 계산하는 메서드 (두 부서에서 공유하여 사용)
void calculateGrade() {
// ...
}
// * 장학금을 계산하는 메서드 (장학금 부서에서 사용)
void calculateScholarship() {
// ...
this.calculateGrade();
// ...
}
// * 동아리 활동 시간을 계산하는 메서드 (학생처에서 사용)
void reportClubActivity() {
// ...
this.calculateGrade();
// ...
}
// * 변경된 정보를 DB에 저장하는 메서드 (IT 부서에서 사용)
void saveDatabase() {
// ...
}
}
많은 사람들이 코드를 보고 당연히 이렇게는 작성 안 하지라고 생각할 수 있다.
하지만 코드를 재사용하고 싶고 학생은 모두 같은 클래스로 묶어서 사용해야겠다. 라고
자칫 잘 못 생각한다면 쉽게 원칙을 따르지 못하는 경우가 발생한다.
(사실 나는 의식해도 잘 따르지 못한다..)
calculateGrade() 메서드의 수정 이유가 두 가지가 될 수 있다.
수정이 가능한 이유로 접근을 한다면 calculateScholarship()
, reportClubActivity()
의 로직을 수정하고자 하는 경우로 두 가지의 경우가 발생한다.
calculateGrade()
를 소유하도록 분리한다.Student는 학생의 기본 정보만을 관리해야한다.
class Student {
private String name;
private String studentId;
private int grade;
private String major;
public void saveToDatabase() {
// 학생 정보를 데이터베이스에 저장하는 로직
}
}
class GradeService {
public void calculateGrade(Student student) {
// 학생의 성적을 계산하는 로직
}
}
class ScholarshipService {
public void calculateScholarship(Student student) {
// 학생의 장학금을 계산하는 로직
}
}
}
학생의 성적을 계산하거나 , 장학금을 계산하는 로직은 별도의 클래스나 서비스 객체에서 처리하는 것이 단일 책임 원칙을 지킬 수 있는 좋은 방법이다.
Open Close Principle
OCP란?
기존 코드를 변경하지 않고, 새로운 기능을 추가할 수 있도록 설계하는 것
확장에 대해서는 개방적이고 수정에 대해서는 폐쇄적이라는 의미이다.
처음 이 말을 들었을 때? 이게 무슨 말이지 했다.
지금 생각해보면 기능을 추가할 때마다 기존 코드를 수정해야지만 기능을 추가할 수 있다면
매우 번거롭고 새로운 기능을 추가하기 싫을 것이다.
OCP가 잘 지켜졌다면 새로운 기능을 추가한 후, 기존 기능의 테스트 로직을 돌릴 때 기도하는 일은
없을거다.
객체를 직접 수정하는 건 제한해야 한다.
새로운 변경 상황이 발생했을 때 객체를 직접적으로 수정한다면
새로운 변경사항에 대해 유연하게 대응할 수 없다.
객체를 직접 수정하지 않고도 변경사항을 적용할 수 있도록 설계하자!!
나는 추상화하면 다형성이 바로 생각난다.
왜냐구요? 솔직히 그냥 외운 거 같다.
추상화가 뭐야? 다형성
다형성이 뭐야? ….
크흠…
나는 다형성을 다양한 형태로 존재할 수 있는 것이라고 말한다.
쉽게 “객체의 주인에 따라 메소드가 바뀌는 것” 그게 다형성이다.
추상화된 클래스, 인터페이스를 구현하는 실 구현체에 따라
같은 이름으로 다른 기능을 제공할 수 있다.
// 추상 클래스
abstract class Bird {
abstract public void fly();
}
// 구현 클래스
class TrueBird extends Bird{
public void fly() {
System.out.print("짹짹 날아오르기");
}
class B2gi extends Bird{
public void fly() {
System.out.print("비둘 비둘 날아오르기");
}
}
psvm() {
Bird 참새 = new TrueBird();
Bird 비둘기 = new B2gi();
참새.fly(); // 짹짹 날아오르기
비둘기.fly(); // 빋둘 비둘 날아오르기
}
}
서로 같은 Bird라는 클래스를 상속받았지만 fly 메서드를 실행하였을 때는
다른 출력 결과를 확인할 수 있다.
요것이 추상화 → 다형성을 보여주는 예제이다.
이와 같은 방법을 활용해서 코드를 작성한다면
확장에는 열려있고 변경에는 닫혀있는 OCP 원칙을 따를 수 있다.
Liskov Substitution Principle
리스코프 치환 원칙이란 올바른 상속 관계의 특징을 정의하기 위해 발표한 것이라고 한다.
서브타입(하위 타입)은 언제나 기반(상위 타입으로 교체)할 수 있어야 한다는 것을 뜻한다.
여기서 교체할 수 있다는 말은 자식 클래스는 최소한 부모 클래스에서 가능한 행위는
수행이 보장되어여 한다는 의미이다.
이말은 부모 클래스가 사용되던 위치에 자식 클래스로 대체 하였을 때 원래도 동일한 의도로 작동
해야 한다는 의미이다.
어찌보면 당연하지만 무분별한 재활용, 비슷한 로직 메소드 추출을 하다 보면 위반할 수 있을 것 같다.
LSP가 무엇인지 정의를 검색한 경우는 여러 번 있지만 이렇게 정리를 직접 해 본 적은 처음이다.
항상 정리할 때 마다 다른 느낌으로 이해했기 때문에 LSP를 바라보는 나의 관점이 바뀐다면
추가적으로 정리하려고 한다.
이전에 봤던 인터넷 강의에서 자동차 클래스에 전진 메소드는 속도가 느리더라도
전진해야 한다. 뒤로 가면 안된다.
뒤로 가면 안 된다고? 정해진 의도로만 동작해야 한다고? 라는
의문이 들면서 이해할 수 없었는데 지금 생각해보면 그때는 상속과 추상화의 목적까지는
생각하지 못했던 거 같다.
첫 인터페이스, 추상 클래스의 기능들은 설계자의 의도가 있을 것이다.
그리고 메소드의 이름과 기능이 다른 것은 클린코드적으로 봤을 때도 틀렸다고 할 수 있다.
예제 코드를 작성하는 것도 좋지만 나는 LSP를 서브,하위 클래스가 구체화하는 메소드는
설계자, 상위타입과 동일한 의도를 가진 기능으로 구체화 되어야 한다.
그렇지 않다면 LSP에 위반한다고 생각한다.
Interface Segregation Principle
하나의 범용적인 통합 인터페이스 보다, 여러 개로 분리된 다수의 개별 인터페이스가 좋다.
쉽게 말해서 최대한 인터페이스를 많이 분리하라는 의미이다.
ISP 원칙을 정리하기 위해서 많은 글을 참고하면서 자주 보게된 내용인데
처음에는 엥!? 무슨 말이야…
인터페이스는 재활용될 수 있는 기능을 묶어서 추상화 해두는 건데
왜 번거롭게 범용적인거 하나를 두지 왜 구지구지 나눠 라는 생각을 했다.
하지만 나눠야 하는 이유에 설득 당하는데 그리 오리걸리지 않았다.
갑자기 커스텀 Yes Custom이라고 하니 당황스러울 수 있다.
하지만 나는 ISP는 아래와 같이 생각한다.
클래스에게 커스터마이징 서비스를 제공하는 것
Interface에 너무 많은 기능을 집약해둔다면 필요 없는 기능까지 구현해야하는
단점이 있다. 하지만 더 세부적인 단위, 좀 더 엄격한 집합단위로 나눈다면 정말 클래스가
원하는 기능만으로 구성하여 더 가볍고 효율적인 클래스를 만들 수 있을 것이다.
삐삐부터 스마트폰까지 개발과정을 ISP를 지키는 개발 과정과
원칙을 지키지 않는 개발과정을 본다면 ISP의 이해를 도울 수 있을 것이다.
// 삐삐 시대의 인터페이스
interface Pager {
void sendMessage(String message);
void receiveMessage();
void showTime();
}
// 삐삐 구현체
class OldPager implements Pager {
public void sendMessage(String message) {
System.out.println("Sending message: " + message);
}
public void receiveMessage() {
System.out.println("Receiving message");
}
public void showTime() {
System.out.println("Showing time");
}
}
// 핸드폰 시대의 인터페이스 (삐삐 인터페이스를 그대로 사용)
interface MobilePhone extends Pager {
void makeCall(String number);
void receiveCall();
}
// 핸드폰 구현체
class OldMobilePhone implements MobilePhone {
public void sendMessage(String message) {
System.out.println("Sending message: " + message);
}
public void receiveMessage() {
System.out.println("Receiving message");
}
public void showTime() {
System.out.println("Showing time");
}
public void makeCall(String number) {
System.out.println("Making call to: " + number);
}
public void receiveCall() {
System.out.println("Receiving call");
}
}
// 스마트폰 시대의 인터페이스 (여전히 삐삐 인터페이스를 사용)
interface Smartphone extends MobilePhone {
void browseInternet();
void takePhoto();
}
// 스마트폰 구현체
class OldSmartphone implements Smartphone {
public void sendMessage(String message) {
System.out.println("Sending message: " + message);
}
public void receiveMessage() {
System.out.println("Receiving message");
}
public void showTime() {
System.out.println("Showing time");
}
public void makeCall(String number) {
System.out.println("Making call to: " + number);
}
public void receiveCall() {
System.out.println("Receiving call");
}
public void browseInternet() {
System.out.println("Browsing the internet");
}
public void takePhoto() {
System.out.println("Taking a photo");
}
}
위 코드는 각 시대의 기기는 이전 기기의 인터페이스를 그대로 사용하고 있다.
기기가 발전하면서 필요없는 기능은 빼고 발전이 이뤄져야 하는데, 필요 없는 기능을 포함해서 기존
레거시를 가져가 성능 좋은 최적화된 스마트폰을 출시해야 하는데 효율이 떨어지는 최신 스마트폰이 개발된다는 단점이 존재한다.
// 삐삐 시대의 인터페이스
interface Pager {
void sendMessage(String message);
void receiveMessage();
}
interface Clock {
void showTime();
}
// 삐삐 구현체
class OldPager implements Pager, Clock {
public void sendMessage(String message) {
System.out.println("Sending message: " + message);
}
public void receiveMessage() {
System.out.println("Receiving message");
}
public void showTime() {
System.out.println("Showing time");
}
}
// 핸드폰 시대의 인터페이스
interface MobilePhone {
void makeCall(String number);
void receiveCall();
}
interface MobileMessageSender extends Pager {
}
// 핸드폰 구현체
class OldMobilePhone implements MobilePhone, MobileMessageSender {
public void sendMessage(String message) {
System.out.println("Sending message: " + message);
}
public void receiveMessage() {
System.out.println("Receiving message");
}
public void makeCall(String number) {
System.out.println("Making call to: " + number);
}
public void receiveCall() {
System.out.println("Receiving call");
}
}
// 스마트폰 시대의 인터페이스
interface Smartphone extends MobilePhone {
void browseInternet();
void takePhoto();
}
// 스마트폰 구현체
class NewSmartphone implements Smartphone, MobileMessageSender {
public void sendMessage(String message) {
System.out.println("Sending message: " + message);
}
public void receiveMessage() {
System.out.println("Receiving message");
}
public void makeCall(String number) {
System.out.println("Making call to: " + number);
}
public void receiveCall() {
System.out.println("Receiving call");
}
public void browseInternet() {
System.out.println("Browsing the internet");
}
public void takePhoto() {
System.out.println("Taking a photo");
}
}
삐삐의 기능부터 Clock이라는 인터페이스를 만들어 분리하여 원하는 기능만 선택하여 구현하게 되는데
나는 이러한 점이 클래스의 커스터마이징과 같다고 생각이 들었다.
인터페이스 단위로 나눈다면 추가적인 확장,수정에서 더 쉬운 환경을 제공할 수 있을 것 같다.
이 예제 코드가 왜 ISP를 지켜야 하고 필요한지 이해하는 데 도움이 많이 되었고
다른 분들께도 도움이 되기를 바란다.