티스토리 뷰
728x90
반응형
서론
- 훌륭한 객체지향 코드를 얻기 위해서는 클래스가 아닌 객체에 초점을 맞춰야 합니다. 또한 객체에 초점을 맞추되 협력 안에서 수행하는 객체의 책임에 초점을 맞추어야 합니다.
- 객체지향에서 가장 중요한 것은 객체들 간에 주고받는 메시지입니다.
- 객체들은 메시지 전송자로부터 메시지를 수신하기 위해 퍼블릭 인터페이스를 구성하고 있습니다.
협력과 메시지
💡 클라이언트-서버 모델
- 협력은 어떤 객체가 다른 객체에게 무언가를 요청할 때 시작됩니다. 메시지는 객체 간의 협력을 가능케 하는 매개체입니다.
객체가 다른 객체에게 접근할 수 있는 유일한 방법은 메시지를 통해 접근하는 것입니다.
클라이언트-서버 모델이란?
- 협력 안에서 메시지를 전송하는 객체를 클라이언트, 메시지를 수신하는 객체를 서버라고 부릅니다.
- 협력은 클라이언트가 서버에게 요청하는 단방향 상호작용입니다.
- 협력의 관점에서 객체는 두 가지 종류의 메시지 집합으로 구성됩니다.
- 첫째. 다른 객체의 요청을 받기 위한 퍼블릭 인터페이스의 집합
- 둘째. 다른 객체에게 요청을 하기 위한 메시지의 집합
- 요점은 객체가 독립적으로 수행할 수 있는 것보다 더 큰 책임을 수행하기 위해서는 다른 객체와 협력해야 한다는 것입니다. 그리고 이 협력을 가능케 해주는 것은 바로 메시지입니다.
💡 메시지와 메시지 전송
- 메시지는 객체들이 협력을 위해 사용할 수 있는 의사소통 수단입니다.
- 한 객체가 다른 객체에게 도움을 요청하는 것을 메시지 전송 또는 메시지 패싱이라 합니다.
- 메시지를 전송하는 객체를 메시지 전송자, 메시지를 수신하는 객체를 메시지 수신자라 합니다.
- 클라이언트 - 서버 모델에서는 메시지 전송자를 클라이언트, 메시지 수신자를 서버라고 합니다.
- 메시지는 오퍼레이션명, 인자로 구성되며 메시지 전송은 여기에 메시지 수신자를 추가한 것입니다. 따라서 메시지 전송은 메시지 수신자, 오퍼레이션명, 인자의 조합으로 이루어져 있습니다.
// 수신자 오퍼레이션명 인자
DiscountCondition isSatisfiedBy(Screening screening);
💡 메시지와 메서드
- 메시지를 수신했을 때 실제로 어떤 코드가 실행되는지는 메시지 수신자의 실제 타입에 따라 달려 있습니다.
- 메시지를 수신했을 때 실제로 실행되는 함수 또는 프로시저를 메서드라 부릅니다. 중요한 점은 컴파일 시점과 실행 시점에 따라 실행되는 메서드가 달라질 수 있습니다.(다형성)
- 전통적인 방식의 개발자는 컴파일 시점과 실행 시점에 동일한 코드가 수행됩니다. 반면 객체는 메시지와 메서드라는 두 가지 서로 다른 개념을 실행 시점에 연결해야 하기 때문에 컴파일 시점과 실행 시점의 의미가 달라질 수 있습니다.
- 메시지와 메서드의 구분은 메시지 전송자와 메시지 수신자가 느슨하게 결합될 수 있도록 해줍니다.
💡 퍼블릭 인터페이스와 오퍼레이션
- 객체는 안과 밖을 구분하는 뚜렷한 경계를 가집니다. 외부 객체는 오직 객체가 공개하는 메시지를 통해서만 객체와 상호작용할 수 있습니다. 이처럼 객체가 의사소통을 위해 외부에 공개하는 메시지의 집합을 퍼블릭 인터페이스라 합니다.
- 프로그래밍 언어의 관점에서 퍼블릭 인터페이스에 포함된 메시지를 오퍼레이션이라 부릅니다. 오퍼레이션은 수행 가능한 어떤 행동에 대한 추상화입니다.
- 오퍼레이션이란 실행하기 위해 객체가 호출될 수 있는 변환이나 정의에 관한 명세입니다. 인터페이스의 각 요서는 오퍼레이션입니다. 오퍼레이션은 구현이 아닌 추상화이고 메서드는 이 오퍼레이션을 구현한 것입니다.
- 프로그래밍 언어의 관점에서 객체가 다른 객체에게 메시지를 전송하면 런타임 시스템은 메시지 전송을 오퍼레이션 호출로 해석하고 메시지를 수신한 객체의 실체 타입을 기반으로 적절한 메서드를 찾아 수행합니다.
💡 시그니처
- 오퍼레이션의 이름과 파라미터 목록을 합쳐 시그니처라 부릅니다. 오퍼레이션은 실행 코드 없이 시그니처만을 정의한 것입니다.
- 메서드는 이 시그니처에 구현을 더한 것입니다.
public interface DiscountCondition {
// 오퍼레이션, 시그니처
boolean isSatisfiedBy(Screening screening);
}
public class PeriodCondition implements DiscountCondition {
// 메서드
@Override
public boolean isSatisfiedBy(Screening screening) {
return dayOfWeek.equals(screening.getWhenScreened().getDayOfWeek()) &&
startTime.compareTo(screening.getWhenScreened().toLocalTime()) <= 0 &&
endTime.compareTo(screening.getWhenScreened().toLocalTime()) >= 0;
}
}
public class SequenceCondition implements DiscountCondition {
// 메서드
@Override
public boolean isSatisfiedBy(Screening screening) {
return sequence == screening.getSequence();
}
}
인터페이스와 설계 품질
- 좋은 인터페이스란 최소한의 인터페이스와 추상적인 인터페이스라는 조건을 만족해야 합니다.
- 최소한의 인터페이스는 꼭 필요한 오퍼레이션만을 인터페이스에 포함합니다. 추상적인 인터페이스는 어떻게 수행하는지가 아닌 무엇을 수행하는지를 표현합니다.
- 좋은 인터페이스를 설계하는 가장 좋은 방법은 책임 주도 설계 방법을 따르는 것입니다. 책임 주도 설계 방법은 객체가 메시지를 선택하는 게 아닌 메시지가 객체를 선택하도록 함으로써 클라이언트의 의도를 메시지에 표현할 수 있습니다.
💡 디미터 법칙(Law of Demeter)
- 디미터 법칙이란 협력하는 객체의 내부 구조에 대한 결합으로 발생하는 설계 문제를 해결하기 위해 제안된 원칙입니다.
- 디미터 법칙을 간단히 요약하면 객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하는 것입니다.
- 메시지 전송자가 메시지 수신자의 내부 구조에 대해 물어보고 반환받은 요소에 대해 연쇄적으로 메시지를 전송합니다. 이와 같은 코드를 기차 충돌이라 합니다.
- 기차 충돌은 클래스의 내부 구현이 외부로 노출됐을 때 나타나는 전형적인 형태로 메시지 전송자는 메시지 수신자의 내부 정보를 자세히 알게 됩니다. 그렇기 때문에 캡슐화는 무너지고 내부 구현에 강하게 결합될 수 있습니다.
- 디미터 법칙은 객체는 자기 자신은 스스로 책임지는 존재여야 한다는 사실을 강조합니다.
- 디미터 법칙은 객체 간의 협력을 설계할 때 캡슐화를 위반하는 메시지가 인터페이스에 포함되지 않도록 제한합니다.
- DTO나 컬렉션 객체와 같은 자료 구조의 경우에는 물을 수 밖에 없습니다. 만약 묻는 대상이 객체가 아닌 자료 구조라면 당연히 내부 구조를 노출해야 하므로 디미터 법칙을 적용할 필요가 없습니다.
🧨 디미터 법칙을 준수하지 않은 코드
- 아래 메인 메서드를 보면 학급에서 선생님을 가져온 후 선생님이 가리키는 아이들 중 이름이 홍길동인 아이들만 가져오고 있습니다.
- 필자는 지금까지 이러한 코드를 서슴지 않고 사용했습니다. 하지만 이러한 코드는 디미터 법칙을 위반하고 있습니다.
그 이유는 우리는 퍼블릭 인터페이스를 통해 객체에게 메시지를 전달하는 게 아닌 객체가 가지는 자료를 확인하고 있으며, 다른 객체가 어떤 자료를 가지고 있는지 지나치게 잘 알고 있습니다. 아래 코드는 Getter를 통해 다른 객체가 어떠한 속성을 가지고 있는지 유추할 수 있습니다.
@Getter
@AllArgsConstructor
public class ClassRoom {
private final String name;
private Mentor mentor;
}
@Getter
@AllArgsConstructor
public class Mentor {
private final String name;
private List<Mentee> mentees;
}
@Getter
@AllArgsConstructor
public class Mentee {
private final int sequence;
private final String name;
}
public class Main {
public static void main(String[] args) {
List<Mentee> mentees = List.of(
new Mentee(1, "홍길동"),
new Mentee(2, "이순신"),
new Mentee(3, "주몽"),
new Mentee(4, "유관순"),
new Mentee(5, "광개토대왕"));
Mentor mentor = new Mentor("홍길동", mentees);
ClassRoom classRoom = new ClassRoom("햇님반", mentor);
List<Mentee> findMentees = classRoom.getMentor().getMentees().stream()
.filter(mentee -> mentee.getName().equals("홍길동"))
.collect(Collectors.toList());
}
}
👊 디미터 법칙을 준수해봅시다
- 아래는 수정한 코드입니다. 기존에는 Getter를 사용하여 내부 구조에 접근을 하였지만 하지만 아래는 퍼블릭 인터페이스를 만들고
접근을 하고 있습니다. 또한 디미터 법칙을 준수하기 위해서는 .(도트)를 하나만 사용하도록 강제하는 게 아니라는 점입니다. - 디미터 법칙은 결합도와 관련된 것이며, 객체의 내부 구조가 외부로 노출되는지에 대한 것입니다. 또한 Stream API 같은 경우 동일한 Stream을 반환할 뿐 캡슐화는 그대로 유지되므로 문제가 없습니다. 여러 도트를 사용하더라도 객체의 내부 구현이 노출되지 않는다면 그것은 디미터 법칙을 준수하는 코드로 볼 수 있습니다.
@AllArgsConstructor
public class ClassRoom {
private final String name;
private Mentor mentor;
public Mentor who(Mentor mentor) {
return mentor;
}
}
@AllArgsConstructor
public class Mentor {
private final String name;
private List<Mentee> mentees;
public List<Mentee> isSatisfiedBy(String name) {
return mentees.stream()
.filter(student -> student.contain(name))
.collect(Collectors.toList());
}
}
@AllArgsConstructor
public class Mentee {
private final int sequence;
private final String name;
public boolean contain(String name) {
return this.name.equals(name);
}
}
public class Main {
public static void main(String[] args) {
List<Mentee> mentees = List.of(
new Mentee(1, "홍길동"),
new Mentee(2, "이순신"),
new Mentee(3, "주몽"),
new Mentee(4, "유관순"),
new Mentee(5, "광개토대왕"));
Mentor mentor = new Mentor("홍길동", mentees);
ClassRoom classRoom = new ClassRoom("햇님반", mentor);
List<Mentee> findMentees = classRoom.who(mentor).isSatisfiedBy("홍길동");
}
}
부끄럼 타는 코드(Shy Code)
- 디미터 법칙을 준수하면 부끄럼 타는 코드를 작성할 수 있습니다. 부끄럼 타는 코드란 불필요한 어떤 것도 다른 객체에게 보여주지 않으며, 다른 객체의 구현에 의존하지 않는 코드를 말합니다.
- 디미터 법칙을 따르는 코드는 메시지 수신자의 내부 구조가 전송자에게 노출되지 않으며, 메시지 전송자는 수신자의 내부 구현에 결합되지 않습니다. 따라서 클라이언트와 서버 사이에 낮은 결합도를 유지할 수 있습니다.
💡 묻지 말고 시켜라
- 디미터 법칙은 훌륭한 메시지는 객체의 상태에 관해 묻지 말고 원하는 것을 시켜야 한다는 사실을 강조합니다. 묻지 말고 시켜라는 이런 스타일의 메시지를 작성을 장려하는 원칙을 가리키는 용어입니다.
- 메시지 전송자는 메시지 수신자의 상태를 기반으로 결정을 내린 후 메시지 수신자의 상태를 바꿔서는 안 됩니다. 우리가 구현하고 있는 로직은 메시지 수신자가 담당해야 할 책임입니다. 객체의 외부에서 해당 객체의 상태를 기반으로 결정을 내리는 것은 객체의 캡슐화를 위반합니다.
- 객체지향의 기본은 함께 변경될 확률이 높은 정보와 행동을 하나의 단위로 통합하는 것입니다.
- 묻지 말고 시켜라 원칙에 따르도록 메시지를 결정하다보면 자연스럽게 정보 책임 전문가에게 책임을 할당하게 되고 높은 응집도를 가진 클래스를 얻을 확률이 높아집니다.
💡 의도를 드러내는 인터페이스
- 켄트 백은 메서드를 명명하는 두 가지 방법을 설명했습니다.
- 첫째. 메서드가 작업을 어떻게 수행하는지를 나타내도록 이름 짓는 것, 이 경우 메서드의 이름은 내부의 구현을 드러냅니다.
- 둘째. 어떻게가 아닌 무엇을 하는지 드러내는 것입니다.
- 또한 오퍼레이션의 이름은 협력이라는 문맥을 반영해야 합니다, 오퍼레이션은 클라이언트가 객체에게 무엇을 원하는지를 표현해야 합니다. 다시 말해 객체 자신이 아닌 클라이언트의 의도를 표현하는 이름을 가져야 합니다.
첫번째 방법
- 메서드 이름에서 기간이냐, 순번이냐를 알 수 있습니다. 이는 메서드 수준에서 캡슐화를 위반하고 있으며, 이 메서드들은 클라이언트로 하여금 협력하는 객체의 종류를 알도록 강요합니다.
public class PeriodCondition {
public boolean isSatisfiedByPeriod(Screening screening) {}
}
public class SequenceCondition {
public boolean isSatisfiedBySequence(Screening screening) {}
}
두번째 방법
- 두번째 방법은 메서드 이름이 어떻게가 아닌 무엇을에 집중하고 있습니다.
- 어떻게 수행하는지를 드러내는 이름이란 메서드의 내부 구현을 설명하는 이름입니다. 결과적으로 협력을 설계하기 시작하는 이른 시기부터 클래스의 내부 구현에 관해 고민할 수밖에 없습니다. 반면 무엇을 하는지를 드러내도록 메서드의 이름을 짓기 위해서는 객체가 협력 안에서 수행해야 하는 책임에 관해 고민해야 합니다. 또한 무엇을 하는지 드러내도록 하면 다형성을 제공할 수 있습니다.
public interface DiscountCondition {
boolean isSatisfiedBy(Screening screening);
}
public class SequenceCondition implements DiscountCondition {
@Override
public boolean isSatisfiedBy(Screening screening) {}
}
public class PeriodCondition implements DiscountCondition {
@Override
public boolean isSatisfiedBy(Screening screening) {}
}
원칙의 함정
- 디미터 법칙과 묻지 말고 시켜라 스타일은 객체의 퍼블릭 인터페이스를 깔끔하고 유연하게 만들 수 있는 훌륭한 설계 원칙입니다. 하지만 절대적인 법칙은 아닙니다. 우리가 잊지 말아야 하는것은 결국 설계라는게 트레이드오프의 산물이라는 것입니다. 그렇기 때문에 적용하려는 원칙이 서로 충돌하는 경우 끼워 맞추려고 노력하지 않고 부적합하다고 판단이 들면 과감히 원칙을 무시하는 것입니다.
💡 디미터 법칙은 하나의 .(도트)를 강제하는 규칙이 아닙니다.
- 기차 충돌처럼 보이는 코드라도 객체의 내부 구현에 대한 어떤 정보도 외부로 노출하지 않는다면 그것은 디미터 법칙을 준수한 것입니다.
- 아래 Stream API는 객체의 내부에 대한 어떤 내용도 묻지 않습니다. 그저 객체를 다른 객체로 변환하는 작업을 수행하라고 시킬 뿐입니다. 따라서 디미터 법칙을 위배하지 않습니다.
IntStream.of(1, 15, 20, 3, 9)
.filter(x -> x > 10)
.distinct()
.count();
명령-쿼리 분리 원칙
- 어떤 절차를 묶어 호출 가능하도록 이름을 부여한 기능 모듈을 루틴이라 합니다. 루틴은 다시 프로시저와 함수로 구분할 수 있습니다.
- 명령-쿼리 분리 원칙의 요지는 오페레이션은 부수효과를 발생시키는 명령이거나 부수효과를 발생시키지 않는 쿼리 중 하나여야 합니다. 즉 어떤 오퍼레이션도 명령인 동시에 쿼리여서는 안됩니다.
프로시저
- 프로시저는 정해진 절차에 따라 내부의 상태를 변경하는 루틴의 한 종류입니다.
- 프로시저는 부수효과를 발생시킬 수 있지만 반환 값을 가질 수 없습니다.
- 객체의 상태를 수정하는 오퍼레이션을 명령이라 합니다. 객체의 상태를 변경하는 명령은 반환 값을 가질 수 없습니다.
함수
- 함수는 어떤 절차에 따라 필요한 값을 계산해서 반환하는 루틴의 한 종류입니다.
- 함수는 값을 반환할 수 있지만 부수효과를 가질 수 없습니다.
- 객체와 관련된 정보를 반환하는 오퍼레이션을 쿼리라 합니다. 객체의 정보를 반환하는 쿼리는 객체의 상태를 변경할 수 없습니다.
문제가 발생할 수 있는 상황
- 아래는 함수를 호출하는 동시에 명령을 수행하고 있습니다. 그렇기 때문에 의도치 않은 버그의 발생 확률을 높일 수 있습니다.
// getSomeStatus를 여러번 호출하면 반환값이 달라질 수 있음.
public Status getSomeStatus() {
changeSomeStatus();
}
private void changeSomeStatus() {
// 상태 변경 로직
}
개선 방안
- 기존 private 접근 제한자를 public으로 변경 한 뒤 main 메서드에서 객체의 결과값을 사용하여 클라이언트가 직접 명령을 수행할 수 있도록 변경합니다. 이렇게 한다면 쿼리 메서드는 부수 효과에 대한 부담감이 줄어들고 몇 번을 호출하더라도 다른 부분에 영향을 미치지 않습니다.
- 명령과 쿼리를 분리한다면 예측 가능하고 이해하기 쉬우며 디버깅이 용이한 동시에 유지보수가 수월해집니다.
public Status getSomeStatus() {
// Status 객체에 대한 정보 반환
}
public void changeSomeStatus() {
// 상태 변경 로직
}
public static void main(String[] args) {
Status status = getSomeStatus();
if(status.결과값) {
status.changeSomeStatus();
}
}
💡 명령 - 쿼리 분리와 참조 투명성
- 참조 투명성이란 같은 인수로 함수를 호츨 했을 경우 항상 같은 결과를 반환한다면 참조적으로 투명한 함수라 할 수 있습니다.
- 즉 어떤 입력이 주어졌을 경우 언제, 어디서 호출하든 항상 같은 결과를 반환해야 합니다.
- 모던 자바 인 액션 18 - 함수형 관점으로 생각하기 > 참조 투명성 보기
728x90
반응형
'스터디 > 오브젝트' 카테고리의 다른 글
오브젝트 - 8장 의존성 관리하기 (0) | 2022.10.17 |
---|---|
오브젝트 - 7장 객체 분해 (1) | 2022.10.14 |
오브젝트 - 5장 책임 할당하기 (0) | 2022.10.07 |
오브젝트 - 4장 설계 품질과 트레이드오프 (0) | 2022.10.02 |
오브젝트 - 3장 역할, 책임, 협력 (1) | 2022.09.30 |
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
TAG
- 트랜잭셔널 아웃박스 패턴 스프링부트
- 트랜잭셔널 아웃박스 패턴 스프링 부트 예제
- 람다 표현식
- spring boot 엑셀 다운로드
- pipe and filter architecture
- spring boot redisson sorted set
- 공간 기반 아키텍처
- 레이어드 아키텍처란
- 서비스 기반 아키텍처
- space based architecture
- redis sorted set
- @ControllerAdvice
- spring boot redisson destributed lock
- redis 대기열 구현
- java userThread와 DaemonThread
- spring boot excel download oom
- service based architecture
- transactional outbox pattern
- pipeline architecture
- spring boot redisson 분산락 구현
- spring boot excel download paging
- JDK Dynamic Proxy와 CGLIB의 차이
- microkernel architecture
- polling publisher spring boot
- 자바 백엔드 개발자 추천 도서
- redis sorted set으로 대기열 구현
- java ThreadLocal
- spring boot redis 대기열 구현
- transactional outbox pattern spring boot
- spring boot poi excel download
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
글 보관함