일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 서비스 vs 쓰레드
- 안드로이드 레트로핏 사용법
- 플러터 설치 2022
- Rxjava Observable
- 서비스 쓰레드 차이
- jvm 작동 원리
- 안드로이드 os 구조
- 2022 플러터 설치
- 스택 큐 차이
- 안드로이드 유닛테스트란
- android retrofit login
- 안드로이드 유닛 테스트
- 클래스
- rxjava hot observable
- 큐 자바 코드
- rxjava disposable
- 객체
- ar vr 차이
- 스택 자바 코드
- 안드로이드 라이선스
- 멤버변수
- 안드로이드 유닛 테스트 예시
- 안드로이드 라이선스 종류
- 2022 플러터 안드로이드 스튜디오
- 자바 다형성
- android ar 개발
- 안드로이드 레트로핏 crud
- rxjava cold observable
- jvm이란
- ANR이란
- Today
- Total
나만을 위한 블로그
[JAVA] SOLID 원칙이란? - 2 - 본문
지난 포스팅에선 SOLID 원칙 중 단일 책임 원칙, 개방 폐쇄 원칙에 대해서 정리했었다.
https://onlyfor-me-blog.tistory.com/351
위키백과에 SOLID라는 문서가 있는 걸 확인했는데, 여기에 각 원칙의 핵심이 쓰여 있어서 정리한다.
https://en.wikipedia.org/wiki/SOLID
단일 책임 원칙 : 클래스가 변경되는 데는 1개 이상의 이유가 있어선 안된다. 즉, 모든 클래스는 하나의 책임만 가져야 한다.
개방 폐쇄 원칙 : 소프트웨어 엔티티는...확장을 위해 열려야 하지만 수정을 위해 폐쇄되어야 한다.
리스코프 대체 원칙 : 기본 클래스에 대한 포인터 또는 참조를 사용하는 함수는 모르는 사이에 파생 클래스의 개체를 사용할 수 있어야 한다. 계약에 의한 설계도 참고하라
인터페이스 분리 원칙 : 많은 클라이언트 별 인터페이스가 하나의 범용 인터페이스보다 낫다.
종속성 반전 원칙 : 구체화되지 않은 추상화에 의존하라
이번 포스팅에선 나머지 리스코프 치환 원칙, 인터페이스 분리 원칙, 의존관계 역전 원칙에 대해서 정리하려고 한다.
먼저 리스코프 치환 원칙이다. 리스코프는 사람 이름 같으니 냅두고 치환이 뭔지 사전에 쳐봤다.
치환 : 바꿔 놓음, 어떤 것의 순열을 다른 순열로 바꿔 펼치는 일
순열 : 차례대로 늘어선 줄, 또는 그 차례
치환은 기존에 어떤 순서를 갖고 있던 것을 그대로 바꿔버린다는 뜻같다.
그럼 프로그래밍에서 리스코프 치환 원칙이란 뭘까? 위키백과를 확인해봤다.
치환성은 객체 지향 프로그래밍 원칙이다. 컴퓨터 프로그램에서 자료형 S가 자료형 T의 하위형이라면 필요한 프로그램의 속성(정확성, 수행하는 업무 등)의 변경 없이 자료형 T의 객체를 자료형 S의 객체로 교체(치환)할 수 있어야 한다는 원칙이다. 이 원칙을 엄밀한 용어로 말하자면 (강한) 행동적 하위형화라 부르는 하위형화 관계의 특정한 사례다...(중략)
상속 관계에서 자식 클래스라면 부모 클래스로 교체해도 잘 작동해야 한다는 뜻같다.
이게 맞는지 다른 포스팅을 확인해봤다.
"서브 타입은 언제든 자신의 기반 타입으로 교체할 수 있어야 한다 -로버트 C 마틴"
객체 지향에서의 상속은 조직도나 계층도가 아닌 분류도가 되어야 한다. 객체 지향의 상속은 아래의 조건을 만족해야 한다.
- 하위 클래스 is a kind of 상위 클래스 : 하위 클래스는 상위 클래스의 한 종류다
- 구현 클래스 is able to 인터페이스 : 구현 분류는 인터페이스할 수 있어야 한다
위의 두 문장대로 구현된 프로그램이라면 이미 리스코프 치환 원칙을 잘 지키고 있다고 할 수 있다. 하지만 위 문장대로 구현되지 않은 코드가 있을 수 있는데, 바로 상속이 조직도나 계층도 형태로 구축된 경우다.
아버지를 상위 클래스로 하는 딸이라는 하위 클래스가 있다면 이것이 바로 전형적인 계층도 형태며 상속을 잘못 적용한 예시다. 이를 코드로 한다면 아래처럼 될 것이다.
아버지 춘향이 = new 딸();
이름이 춘향이인 것은 좋지만 아버지의 역할을 맡기고 있다. 춘향이는 아버지형의 객체 참조변수기에 아버지 객체가 가진 행위(메서드)를 할 수 있어야 하는데 춘향이에게 어떤 아버지의 역할을 시킬 수 있을까?
아래는 리스코프 치환 원칙하면 자주 나오는 예제 코드라고 한다.
public class Rectangle
{
public int width;
public int height;
public void setHeight(int height)
{
this.height = height;
}
public int getHeight()
{
return this.height;
}
public void setWidth(int width)
{
this.width = width;
}
public int getWidth()
{
return this.width;
}
public int area()
{
return this.width * this.height;
}
}
public class Square extends Rectangle
{
@Override
public void setHeight(int value)
{
this.width = value;
this.height = value;
}
@Override
public void setWidth(int value)
{
this.width = value;
this.height = value;
}
}
public static void main(String[] args)
{
Rectangle rectangle = new Rectangle();
rectangle.setHeight(5);
rectangle.setWidth(4);
System.out.println("Rectangle의 크기는 " + rectangle.area() + "입니다");
Square square = new Square();
square.setHeight(5);
square.setWidth(4);
System.out.println("Square의 크기는 " + square.area() + "입니다");
/* 상위 클래스(Rectangle)의 객체를 하위 클래스(Square)의 객체로 대체할 수 있어야 한다
* 아래 println()의 결과는 20이 아닌 16을 리턴한다. LSP를 위반하는 것이고 잘못된 다형성을 갖고 있다 */
Rectangle rs = new Square();
rs.setHeight(5);
rs.setWidth(4);
System.out.println("rs의 크기는 : " + rs.area() + "입니다");
}
Rectangle 형태로 객체를 만들었는데 우항에서 실제로 객체화하는건 Square라서 마지막 println()은 16이 나온다.
만약 리스코프 치환 원칙을 잘 지켰다면 Square()로 객체화했더라도 20이 나왔어야 한다.
결과적으로 리스코프 치환 원칙을 잘 지키면 하위 클래스는 상위 클래스의 한 종류가 되야 하고, 상속을 잘 지키면 자연스럽게 리스코프 치환 원칙을 만족하게 된다.
다음은 인터페이스 분리 원칙이다. 위키백과에선 아래와 같이 말하고 있다.
인터페이스 분리 원칙은 클라이언트가 자신이 쓰지 않는 메서드에 의존하지 않아야 한다는 원칙이다. 인터페이스 분리 원칙은 큰 덩어리의 인터페이스들을 구체적이고 작은 단위들로 분리시켜 클라이언트들이 꼭 필요한 메서드만 쓸 수 있게 한다. 이와 같은 작은 단위들을 역할 인터페이스라고도 부른다. 인터페이스 분리 원칙을 통해 시스템의 내부 의존성을 약화시켜 리팩토링, 수정, 재배포를 쉽게 할 수 있다. 인터페이스 분리 원칙은 GRASP의 밀착 원칙과 비슷하다.
쓰이지 않는 메서드에 의존하지 말아야 하고, 큰 인터페이스를 작은 단위로 나눠 필요한 메서드만 쓸 수 있게 하자는 게 인터페이스 분리 원칙의 요지 같다.
영문 위키백과에는 다른 뉘앙스로 쓰여진 설명과 추가적인 설명들이 있어서 추가로 가져왔다.
https://en.wikipedia.org/wiki/Interface_segregation_principle
인터페이스 분리 원칙은 클라이언트가 안 쓰는 메서드에 강제로 의존해선 안 된다고 명시한다. ISP는 매우 큰 인터페이스를 더 작고 보다 구체적인 인터페이스로 분할해 클라이언트가 관심 있는 메서드에 대해서만 알면 된다. 이런 축소된 인터페이스를 역할 인터페이스라고 한다. ISP는 시스템을 분리된 상태로 유지해 리팩토링, 변경, 재배포를 더 쉽게 하기 위한 것이다. GRASP의 High Cohesion Principle과 유사하다.
- 객체 지향 설계의 중요성
객체 지향 설계에서 인터페이스는 코드를 단순화하고 종속성에 대한 결합을 방지하는 장벽을 만드는 추상화 계층을 제공한다. Manifesto for Software Craftmanship에 서명한 많은 소프트웨어 전문가에 따르면, 잘 만들어진 소프트웨어를 만드는 건 작동하는 소프트웨어를 만드는 것만큼 중요하다. 인터페이스를 써서 소프트웨어의 의도를 더 자세히 설명하는 건 종종 좋은 생각이다. 시스템은 여러 수준에서 너무 결합되어 많은 추가 변경없이 한 곳에서 변경하는 게 더 이상 불가능할 수 있다. 인터페이스나 추상 클래스를 쓰면 이 부작용을 방지할 수 있다
이 문서에서 따로 인터페이스 분리 원칙의 기원에 대해 아래와 같이 말하고 있다.
ISP는 로버트 C. 마틴이 제록스(Xerox)를 컨설팅하는 동안 처음 사용하고 공식화했다. 제록스는 스테이플링 및 팩스 같은 다양한 작업을 수행할 수 있는 새로운 프린터 시스템을 만들었다. 이 시스템의 소프트웨어는 처음부터 만들어졌는데, 소프트웨어가 커짐에 따라 수정 작업이 점점 어려워져서 아주 작은 변경에도 1시간의 재배포 주기가 소요되어 개발이 거의 불가능했다. 디자인 문제는 거의 모든 작업에서 단일 Job 클래스를 사용했다는 것이다. 인쇄 작업이나 스테이플링 작업을 해야 할 때마다 Job 클래스가 호출됐다.
그 결과 다양한 클라이언트에 특정한 다수의 메서드가 있는 '뚱뚱한' 클래스가 만들어졌다. 이 디자인 때문에 기본 작업은 아무 소용이 없더라도 인쇄 작업의 모든 방법을 알고 있을 것이다. 마틴이 제안한 솔루션은 오늘날 '인터페이스 분리 원칙'이라고 불리는 걸 확인했다. 제록스 소프트웨어에 적용된 Job 클래스와 해당 클라이언트 사이의 인터페이스 계층은 Dependency Inversion Principle을 써서 추가됐다. 하나의 큰 Job 클래스를 갖는 대신 Staple 또는 Print 클래스에서 각각 쓰이는 Staple Job 인터페이스 또는 Print Job 인터페이스가 생성되어 Job 클래스의 메서드를 호출한다. 따라서 각 작업 유형에 대해 하나의 인터페이스가 만들어졌으며, 모두 Job 클래스에 의해 구현됐다.
다음은 인터넷에서 발견한 인터페이스 분리 원칙의 예제 코드다.
예를 들어 지불하는 기능을 인터페이스로 구현한다고 치면, 대략 이런 느낌의 인터페이스가 나올 것이다.
import java.util.List;
public interface Payment
{
void initiatePayments();
Object status();
List<Object> getPayments();
}
이제 이 기능들을 사용하기 위해 Payment 인터페이스를 구현한 클래스를 작성한다. 예시기 때문에 메서드 안의 비즈니스 로직은 없다.
import java.util.List;
public class BankPayment implements Payment
{
@Override
public void initiatePayments()
{
}
@Override
public Object status()
{
return null;
}
@Override
public List<Object> getPayments()
{
return null;
}
}
이 상태에서 시간이 지나면 더 많은 기능이 추가될 것이다. 그래서 Payment 인터페이스에 기능을 추가한다.
import java.util.List;
public interface Payment
{
void initiatePayments();
Object status();
List<Object> getPayments();
void intiateLoanSettlement();
void initiateRePayment();
}
그리고 이를 구현한 또 다른 클래스를 만든다.
import java.util.List;
public class LoanPayment implements Payment
{
@Override
public void initiatePayments()
{
throw new UnsupportedOperationException("BankPayment가 아닙니다");
}
@Override
public Object status()
{
return null;
}
@Override
public List<Object> getPayments()
{
return null;
}
@Override
public void intiateLoanSettlement()
{
}
@Override
public void initiateRePayment()
{
}
}
메서드가 새로 추가된 인터페이스를 구현한 LoanPayment 클래스를 만들었다. 이제 모든 게 완벽하지 않은가?
아니다. 이전에 구현한 BankPayment 클래스를 다시 확인해보면 붉은 줄이 생기면서 추가로 메서드를 구현하라는 IDE의 경고가 나올 것이다.
하지만 그 메서드는 BankPayment 클래스에 있을 이유가 없는 쓸모없는 메서드다. 그냥 에러만 던질 뿐이다. 이 경우가 인터페이스 분리 원칙이 위반된 경우다.
이걸 인터페이스 분리 원칙을 준수하는 형태로 고치려면 어떻게 해야 할까? 해당 예제에선 위의 코드를 클래스 다이어그램으로 표현하면 아래와 같이 된다고 한다.
이제 이걸 아래와 같이 고친다.
initiatePayments()는 BankPayment 클래스에만 필요하고 initialLoanSettlement()와 initialRePayment()는 LoanPayment 클래스에만 필요한 메서드들이다.
그리고 공통적으로 필요한 건 Payment 인터페이스에 정의된 메서드 2개뿐이다. 이걸 코드로 옮기면 아래와 같다.
import java.util.List;
public interface Payment
{
Object status();
List<Object> getPayments();
}
이제 이 인터페이스를 상속하는 2개의 인터페이스를 만든다.
public interface Bank extends Payment
{
void initiatePayments();
}
public interface Loan extends Payment
{
void intiateLoanSettlement();
void initiateRePayment();
}
그리고 Bank, Loan 인터페이스를 구현한 클래스를 만들고 인터페이스 안의 메서드들을 재정의한다.
마찬가지로 메서드 안의 비즈니스 로직은 생략돼 있다.
import java.util.List;
public class BankPayment implements Bank
{
@Override
public void initiatePayments()
{
}
@Override
public Object status()
{
return null;
}
@Override
public List<Object> getPayments()
{
return null;
}
}
참고로 BankPayment 클래스에서 재정의할 메서드를 선택하는 화면은 인텔리제이 기준으로 아래와 같이 나온다.
그냥 전부 선택하고 OK를 누르면 된다. 이런 식으로 LoanPayment 클래스도 만들어준다.
import java.util.List;
public class LoanPayment implements Loan
{
@Override
public void intiateLoanSettlement()
{
}
@Override
public void initiateRePayment()
{
}
@Override
public Object status()
{
return null;
}
@Override
public List<Object> getPayments()
{
return null;
}
}
이제 인터페이스 분리 원칙을 지키는 클래스 구조가 완성됐다.
즉, 인터페이스 분리 원칙은 맨 위에 요약한 대로 인터페이스가 많은 편이 단일 인터페이스가 있는 것보다 낫다는 것을 말한다.
이 인터페이스 분리 원칙을 준수하면 여러 책임이 있는 부풀려진 인터페이스가 생기지 않게 할 수 있고, 단일 책임 원칙을 따르는 데에도 도움이 될 것이다.
수정할 수 없는 레거시 인터페이스를 처리하는 경우라면 어댑터 패턴이 유용할 수 있다. 어댑터 패턴에 대해선 시간이 되면 나중에 포스팅으로 기록한다.
마지막으로 의존관계 역전(종속성 반전) 원칙이 무엇인지 확인해보자.
의존관계 역전 원칙은 소프트웨어 모듈들을 분리하는 특정 형식을 지칭한다. 이 원칙을 따르면 상위 계층(정책 결정)이 하위 계층(세부 사항)에 의존하는 전통적인 의존관계를 반전(역전)시킴으로써 상위 계층이 하위 계층으로부터 독립되게 할 수 있다. 이 원칙은 다음과 같은 내용을 담고 있다.
1. 상위 모듈은 하위 모듈에 의존해선 안 된다. 상위 모듈, 하위 모듈 모두 모두 추상화에 의존해야 한다.
2. 추상화는 세부 사항에 의존해선 안 된다. 세부 사항이 추상화에 의존해야 한다.
이 원칙은 상위와 하위 객체 모두 동일한 추상화에 의존해야 한다는 객체지향적 설계의 대원칙을 제공한다.
https://en.wikipedia.org/wiki/Dependency_inversion_principle
종속성 반전 원칙은 느슨하게 결합된 소프트웨어 모듈의 특정 형태다. 이 원칙을 따를 때 높은 수준의 정책 설정 모듈에서 낮은 수준의 종속성 모듈로 설정된 기존 종속 관계가 반대로 되어, 높은 수준의 모듈이 낮은 수준의 모듈 구현 세부 정보와 독립적으로 렌더링된다. 원칙은 다음과 같다.
1. 고수준 모듈은 저수준 모듈에 의존해선 안 된다. 둘 다 추상화(인터페이스 등)에 의존해야 한다.
2. 추상화는 세부 사항에 의존해선 안 된다. 세부 사항(구체적 구현)은 추상화에 의존해야 한다.
높은 수준의 개체와 낮은 수준의 개체가 모두 같은 추상화에 의존해야 한다고 지시함으로써 이 디자인 원칙은 일부 사람들이 객체 지향 프로그램이에 대해 생각할 수 있는 방식을 뒤집는다.
이 원칙 뒤에 있는 아이디어는 높은 수준의 모듈과 낮은 수준의 모듈 간의 상호작용을 설계할 때, 상호작용을 그들 사이의 추상적 상호작용으로 생각해야 한다는 것이다. 이는 고수준 모듈 설계 뿐 아니라 저수준 모듈에도 영향을 준다. 저수준 모듈은 상호작용을 염두에 두고 설계해야 하며, 사용 인터페이스를 바꿔야 할 수도 있다. 많은 경우엔 상호작용 자체를 추상적 개념으로 생각하면 추가 코딩 패턴을 도입하지 않고도 구성 요소의 결합을 줄일 수 있으므로 더 가볍고 덜 구현 종속적인 상호작용 스키마만 허용된다. 두 모듈 사이에서 발견된 추상 상호작용 스키마가 일반적이고 일반화가 의미가 있을 때, 이 설계 원칙은 종속성 반전 코딩 패턴으로 이어진다.
역시 위키백과에서 하는 말들은 잘 모르겠다. 수준이 높든 낮든 모두 인터페이스 등이 제공하는 추상화에 의존해야 하고, 구체적 구현은 추상화를 의존해야 한다니?
애초에 고수준 모듈은 뭐고 저수준 모듈은 또 뭔가? 이 단어들부터 짚고 가야 할 필요가 있어 보인다. 두 단어를 구글링한 결과 나오는 글들의 내용 중 핵심만 가져와봤다.
https://javacan.tistory.com/entry/Implement-high-level-policy-without-low-level-detail
고수준 모듈은 의미 있는 기능을 제공하는 모듈이고, 저수준 모듈은 고수준 모듈의 기능을 구현하기 위해 필요한 하위 기능의 실제 구현이다
https://stackoverflow.com/questions/16558256/what-is-high-level-modules-and-low-level-modules
고수준 모듈은 프레젠테이션 계층에서 쓰이는 인터페이스/추상화다. 저수준 모듈은 고수준 모듈이 작업하는 데 도움되는 여러 개의 작은 모듈(하위 시스템)이다.
고수준 모듈은 인터페이스라고 하니 추상 클래스도 포함될 수 있겠다. 그럼 저수준 모듈은 인터페이스나 추상 클래스를 구현한 클래스라고 할 수 있지 않을까?
관련된 예제를 찾다가 이해가 될락말락한 걸 찾았다.
http://wonwoo.ml/index.php/post/1717
public class JSONConverter
{
public String convert(byte[] bytes)
{
// String 형태의 JSON을 반환하는 로직이 있다고 가정
return "json";
}
}
byte[]를 입력받으면 이를 String 형태의 JSON으로 리턴하는 pubilc 메서드를 가진 클래스를 정의한다.
그리고 이를 Response라는 클래스에서 사용한다.
public class Response
{
private JSONConverter jsonConverter = new JSONConverter();
public String response()
{
byte[] bytes = null;
return jsonConverter.convert(bytes);
}
}
이는 종속성 반전 원칙을 위반한 코드다. 고수준 모듈은 저수준 모듈의 구현에 의존해선 안 되고 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다를 위반했다. 즉 고수준 모듈인 Response 클래스는 저수준 모듈인 JSONConverter 구현에 의존하고 있다.
이걸 종속성 반전 원칙을 지키도록 구현한다면 아래와 같이 변한다. 먼저 convert() 추상 메서드를 갖는 인터페이스를 하나 만든다.
public interface Converter
{
String convert(byte[] bytes);
}
그리고 JSONConverter가 이 인터페이스를 구현한다.
public class JSONConverter implements Converter
{
@Override
public String convert(byte[] bytes)
{
return "json";
}
}
이 클래스는 Response에서 사용한다.
public class Response
{
private final Converter converter = new JSONConverter();
public String response()
{
byte[] bytes = null;
return converter.convert(bytes);
}
}
Converter 인터페이스를 만들고 convert()를 이곳에 옮긴 후, JSONConverter에 이 인터페이스를 구현한 다음 Response에서 사용한다.
이렇게 하면 JSONConverter는 Converter 인터페이스를 의존하게 된다.
XML 파싱이 필요해서 JSON에서 XML로 바꿔야 한다면 그 클래스를 만들고 그걸 쓰면 그만이다.
public class XMLConverter implements Converter
{
@Override
public String convert(byte[] bytes)
{
return "xml";
}
}
실제 비즈니스 로직은 없지만 필요한 대로 채워넣으면 XML, JSON 중 원하는 형식대로 데이터를 반환시킬 수 있는 방법을 얻게 됐다.
'JAVA' 카테고리의 다른 글
[JAVA] JVM의 작동원리 (0) | 2021.10.08 |
---|---|
[JAVA] 버퍼란? BufferedReader/Writer란? + 예제 (0) | 2021.09.04 |
[JAVA] Stream API란? (0) | 2021.07.20 |
[JAVA] SOLID 원칙이란? - 1 - (0) | 2021.07.19 |
[JAVA] 문자열의 마지막 문자를 제거하는 방법 (0) | 2021.06.09 |