지난 시간에는 행위로 코드를 설계하는 것에 대해 알아보았다. (여기 클릭)
이번 시간에는 단순 행위 설계에서 더 나아가 SOILD 에 대해 다시 알아보고,
Interface를 활용해서 어떻게 객체지향스러운 코드를 작성할 수 있는지 알아보고자 한다.
이번에도 최대한 공식 영어 명칭에 대해서 분석하고 보려고 한다. 흐름은 실용주의 프로그래밍 책을 따라갈 것이다.
1. SOLID 분석
아오.... solid 정말 많이 들어봤다. 그런데 사실 직접 뜻에 대해서 분석은 안햇던 것 같다.
단순히 암기를 한거지? 으으음...!!!
그래서 필자는 현재 다시 이를 훑어보려고 한다.
Single Responsibility Principle
The single-responsibility principle (SRP) is a computer programming principle that states that
"A module should be responsible to one, and only one, actor."
The term actor refers to a group (consisting of one or more stakeholders or users) that requires a change in the module.
단일 책임의 원칙은 하나의 모듈이 하나, 그리고 오직 하나의 액터에 책임이 있어야 한다는 컴퓨터 프로그래밍 원칙이다.
용어 액터는 모듈의 변화를 필요로 하는 그룹을 나타낸다.
여기서 해당 글을 이해하기 위해서는 모듈과 액터가 정확히 무엇인지 알아야 한다.
모듈은 프로그램을 구성하는 구성 요소의 일부이며, 액터는 영어로 Actor니까 행위자, 즉 시스템과 상호 작용하는 외부 존재이다.
그러면 다시 글을 보자.
단일 책임의 원칙은 하나의 모듈이 하나, 오직 하나의 액터에 책임이 있어야 한다는 원칙
사실 명확하게 글을 읽지 않으면 아 그냥 클래스가 하나의 책임만 가져야 한다~ 구나~
할 수 있는데... 뭔가 좀 다르다?
모듈, 액터라는 단어 때문이라고 생각한다.
모듈은 소프트웨어나 시스템을 기능별로 묶어 분리한 독립적인 단위이다.
액터는 시스템과 상호작용(interaction)을 하는 시스템 외부의 존재란다.
쉽게 말하면 메시지를 전달하는 주체이다. 이것도 솔직히 잘 모르겟다.
GPT는 이해관계자 라고 설명해주었다.
결국 단일 책임 원칙에서 말하는 책임은 액터에 대한 책임으로, 액터에 먼저 집중을 해야 한다.
하나의 모듈을 변경할 이유가 여러가지가 되는 경우는 여러 사람들이 그 모듈을 사용할 경우이다.
여러사람이 사용을 하는 경우 하나의 액터를 위해서만 수정하면 쉽겟지만, 그렇지 않은경우 유지보수가 힘들어진다.
예를 들어보자.
가정집 내 집안일을 위한 모듈로 interface를 정의하겠다.
public interface HouseWorkService() {
public void doLaundry();
public void makeCookie();
}
가정일에는 여러개가 있지만, 빨래를 하거나 쿠키를 만드는 일이 있을것이다.
그런데 갑자기 자동차를 생산하는 일을 한다면?
public interface HouseWorkService() {
public void doLaundry();
public void makeCookie();
public void makeCar();
}
사실 차를 만드는 일은 집안일을 하는 사람이 담당할 게 아니라 정비공이나 근로자가 할 일이다.
서로 다른 액터가 하나의 인터페이스에 섞였으며 이는 단일책임의 원칙에 위반한다.
하나의 액터를 깨뜨렸기 때문이다.
정 자동차를 생성하는 일을 누가 해야 한다면, 근로자용 service를 만들어야 할 것이다.
Open-Closed Principle
In object-oriented programming, the open–closed principle (OCP) states
"software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification";[1] that is, such an entity can allow its behaviour to be extended without modifying its source code.
OOP 에서, 개방폐쇄의 원칙은 소프트웨어 entity들이 확장에는 열려있으나 수정에는 닫혀있어야 함을 말한다.
그것은, 하나의 엔티티가 그 소스 코드를 수정하는것 없이 확장될 수 있도록 할 수 있음을 말한다.
확장에는 열려 있는데 수정에는 닫힌 코드를 어떻게 작성해야할까?
역할에 의존하면 된다.
우리는 이미 역할을 선언하기 좋은 Java에서 제공하는 툴을 알고 있다.
interface를 쓰면 된다. 추상화된 역할에 의존하게 만들면 이것이 쉬워진다.
예를 들어서 회사가 있는데, 여기서 담당하는 일이 현재 자동차를 만드는 일이다.
처음에는 자동차를 만드는 일을 했는데, 이번에는 빌딩을 짓는 일을 하게 되었다.
그런데 다시 ... worker 가 농사일을 하게 된다면? Farming으로 갈아 끼워야 한다.
Worker는 처음에 자동차를 만들었지만, 점점 기능이 많아질 때 마다 어떤 일을 할지 직접 세팅을 해야 한다.
하지만 행위에 집중을 하게 되면 이것은 쉬워진다.
역할에 집중해 간접적으로 구현체를 사용하도록 했다.
이렇게 추상화된 역할에 의존하게 만들면 OCP의 원칙을 달성할 수 있다!
Liskov substitution principle
It is based on the concept of "substitutability" – a principle in object-oriented programming stating that an object (such as a class) may be replaced by a sub-object (such as a class that extends the first class) without breaking the program.
이것은 "치환"의 컨셉에 기반한다. OOP에서 하나의 객체 (클래스 같은)가 프로그램을 망치는 것 없이 (breaking) 하위 객체로 대체될 수 있다는 것을 말하는 원칙이다.
breaking 을 보니 단순히 부순다는 말 보다는 뭔가 망치다는 표현이 더 맞는거 같았다.
글을 보니 위를 어렵게 표현을 했는데, 다음과 같다.
자료형 S 가 자료형 T 의 하위형이라면, 프로그램에서 자료형 T 의 객체는 프로그램의 속성을 변경하지 않고 자료형 S 의 객체로 교체할 수 있다.
즉, 파생클래스가 완벽하게 기본 클래스를 대체할 수 있어야 한다.
이를 가장 잘 설명하는 고전 예시가 있는데, Rectangle과 Square이다.
Rectangle.java
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
public class Rectangle {
long width;
long height;
public long calculateArea() {
return width * height;
}
}
Square.java
public class Square extends Rectangle {
public Square(long length) {
super(length, length);
}
}
RiskovTest.java
public class RiskovTest {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle(10, 5);
System.out.println("rectangle = " + rectangle.calculateArea());
Rectangle rectangle2 = new Square(10);
rectangle2.setHeight(5);
System.out.println("rectangle2 = " + rectangle2.calculateArea());
}
}
이거만한 예제가 없다 정말.
리스코프 치원 법칙에 따르면 파생클래스가 완벽하게 기본 클래스를 대체할 수 있어야 하는데 현재 틀렸다.
new Square() 로 생성된 rectangle2는 Rectangle의 구현체라서, 높이를 5로 변경하면 너비도 5로 변하는게 아니라
높이 5, 너비 10의 area를 가지게 된다. 정사각형을 선언했는데 너비가 예상햇던것과 전혀 달라지는 버그!
파생클래스는 기본 클래스에서 정의한 의도를 모두 지켜야 한다.
그래서 이 의도를 지키고자 오버라이딩을 하려고 한다.
간단하다. 정사각형의 경우 너비를 세팅하면 이를 동일하게 높이에도 세팅해준다.
마찬가지로 높이를 세팅하면 이를 동일하게 너비에 세팅해준다.
Square.java - 수정
public class Square extends Rectangle {
public Square(long length) {
super(length, length);
}
@Override
public void setWidth(long width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(long height) {
super.setWidth(height);
super.setHeight(height);
}
}
그런데 이것도 이상하다.
만약에 rectangle2를 다른 사용자가 받는다면 Square처럼 작동한다고 전혀 기대를 못할 것이다.
아니 왜 너비를 변경햇는데 높이도 변경되지... 하는 버그가 생기기에 찾기 힘들 것이다.
그러므로 이렇게 하면 안되고 가변 세터를 제거하고 인터페이스로 단순화하는게 좋을 것이다.
Interface segregation principle
In the field of software engineering, the interface segregation principle (ISP) states that no code should be forced to depend on methods it does not use.
소프트웨어 공학에서 인터페이스 분리 원칙(Interface Segregation Principle, ISP)은 클라이언트가 사용하지 않는 메서드에 의존하도록 강요받아서는 안 된다고 말한다.
// 나쁜 예: 비대한 인터페이스
interface MultiFunctionDevice {
void print();
void scan();
void fax();
}
// 단순 프린터만 쓰는 클라이언트도 scan(), fax()에 '의존'하게 됨 → ISP 위반
// 좋은 예: 역할별 분리
interface Printer { void print(); }
interface Scanner { void scan(); }
interface Fax { void fax(); }
// 클라이언트는 자신이 쓰는 인터페이스만 의존
이건 뭐... 별거 없네...
Dependency inversion principle
In object-oriented design, the dependency inversion principle is a specific methodology for loosely coupled software modules.
When following this principle, the conventional dependency relationships established from high-level, policy-setting modules to low-level, dependency modules are reversed,
thus rendering high-level modules independent of the low-level module implementation details. The principle states
객체 지향 설계에서 의존성 역전 원칙(Dependency Inversion Principle, DIP)은 느슨하게 결합된 소프트웨어 모듈을 위한 구체적인 방법론이다.
이 원칙을 따르면, 정책을 결정하는 고수준 모듈에서 세부 구현에 의존하는 저수준 모듈로 향하던 전통적인 의존 관계가
역전되어, 고수준 모듈이 저수준 모듈의 구현 세부사항으로부터 독립적이게 된다.
첫째, 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위모듈과 하위 모듈 모두 추상화에 의존해야 한다.
줄째, 추상화는 세부 사항에 의존해서는 안된다. 세부 사항이 추상화에 의존해야 한다.
이를 잘 이해하기 위해 의존성 주입에 대해 다시 생각해보자.
의존성이란 뭘까? 클래스 A가 클래스 BImpl를 사용한다면? A는 BImpl클래스에 의존한다.
다음과 같은 구조가 되는 것이다.
사용하기만 해도 이렇게 의존성이 추가가 된다고 표현한다.
그런데 의존성을 주입한다는 말은 무엇인가?
의존성을 직접 넣어주는 것이다. 언제? method 호출시에!
많은 사람들이 익히 알고 있는 주입 방법이 3가지가 있다.
세터(수정자) 주입, 생성자 주입, 필드주입이 있는데, 이는 알아서 찾아보길 바란다.
여기서 의존성 역전은 어떻게 일어나냐?
위에서 A가 B를 의존하는 그림을 다음과 같이 만들어 보았다.
B라는 Interface를 만들고 A가 B Interface에 의존하도록 바꿨다.
BImpl 클래스는 B Interface를 구현하므로 B Interface에 의존한다.
상위 인터페이스를 만들고, 기존의 두 클래스 A, BImpl가 interface에 의존하도록 변경했다.
이를 다른 말로 '추상화를 이용한 간접 의존 형태로 바꿨다' 라고 한다.
즉, 의존성을 역전시켰다.
아니 도대체 어딜봐서 역전이 된건가? 화살표를 보면 된다.
BImpl로 들어오던 화살표가 BImpl에서 B interface로 나가는 방향으로 변경되었다.
의존성 추가를 위해 바라보는 클래스의 화살표 방향이 변경된 것이다.
사실 이렇게 하면 자동적으로 리스코프 치환 법칙이 성립한다.
자, 그럼 다시 보자.
첫째, 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위모듈과 하위 모듈 모두 추상화에 의존해야 한다.
둘째, 추상화는 세부 사항에 의존해서는 안된다. 세부 사항이 추상화에 의존해야 한다.
이제 좀 이해가 되나?
2. 디자인 패턴
사실 필자는 이 책의 저자와 마찬가지로 디자인 패턴에 대해 환상을 가지고 있던 것 같다.
그래서 이전에는 책도 사서 외우고 심지어 글도 적었다.
하지만 지금 생각해보면 그냥... 하다보니 찾아서 하면 되는 것 같았다.
결국 의도는 의존성 혹은 연결을 끊고, 기존의 코드가 변경되길 원하지 않으며
추상화를 통해 재사용성이 높은 코드를 만들고 싶으면 사용하는 것이다.
패턴은 결국 귀납적으로 나온 것들인데, 이러한 기초가 있다면 이해가 잘 될 것 같다.
객체지향 하면 책도 많이 읽고 그랬는데,
짧은 나의 직장 경력으로 보면 이를 제대로 사용하고 구현하는 회사는 없던것 같다.
기술은 상황에 따라서 적용을 해야 하는게 맞지만, 그것이 아니라 이렇다 저렇다의 이유로 상급자의 의견에 따라
혹은 이미 고객사와 계약시에 정해진 기술로 개발을 진행하는 경우가 많다.
매번 새로운 기술을 찾아서 최신 기술을 적용하는게 능사는 아니지만,
실제로 fancy하게 요즘 트렌드에 맞게 개발하는 프로젝트는 사실 보지 못한 것 같다.
물론 자고로 코드란 회사에서의 상황에 맞게 작성하는게 맞다보니, 이전에 학습을 했어도 잊어버리기 십상이고
실제로 풍부한 코드를 작성할 기회가 매번 주어지는건 아니다 보니 다시 이렇게 글로 정리하게 되었다.
주저리 주저리 각설하고! 다음에도 이어서 예시를 더 구체적으로 탐구해 보겠다.
지난번에는 하다가... 정말 내가 너무 흥미를 못느껴서 포기했는데 이번에는 다양한 예시를 많이 섞어 섞어서 해볼 생각이다.
GPT에게 뭐가 좋을지 물어보려고!
참고
https://www.geeksforgeeks.org/software-engineering/difference-between-module-and-software-component/
'programming language > Java' 카테고리의 다른 글
[Java] 객체지향 연습 4 - 서비스에 대한 이해와 Layered Architecture 적용(feat. Spring 공식문서) (1) | 2025.10.04 |
---|---|
[Java] 객체지향 연습 2 - 행위로 코드 설계하기 (0) | 2025.09.22 |
[Java] 객체지향 연습 1 - 상황 부여와 객체지향 예시 (0) | 2025.09.21 |
[Java] Stream(스트림) - 2탄 (0) | 2025.05.08 |
[Java] 람다 표현식(Lambda Expreesion) - 2탄 (0) | 2025.05.01 |