티스토리 뷰
728x90
반응형
서론
- 사람들은 한 번에 해결하기 어려운 커다란 문제를 맞닥뜨릴 경우 이 문제를 해결 가능한 작은 문제로 나누는 경향이 있다고 합니다. 이렇게 나눠진 문제들 역시 한 번에 해결하기 어렵다면 또다시 더 작은 문제로 나눌 수 있습니다. 이처럼 큰 문제를 해결 가능한 작은 문제로 나누는 작업을 분해라고 합니다.
프로시저 추상화와 데이터 추상화
- 프로그래밍 언어를 통해 표현되는 추상화의 발전은 다양한 프로그래밍 패러다임의 탄생으로 발전했는데 프로그래밍 패러다임은
프로그래밍을 구성하기 위해 사용하는 추상화의 종류와 이 추상화를 이용해 소프트웨어를 분해하는 방법의 두 가지 요소로 결정됩니다. - 프로그래밍 패러다임이란 적절한 추상화의 윤곽을 따라 시스템을 어떤 식으로 나눌것인지를 결정하는 원칙과 방법의 집합입니다. 여기에는 프로시저 추상화와 데이터 추상화가 있습니다.
💡 프로시저 추상화
- 프로시저 추상화는 소프트웨어가 무엇을 해야하는지 추상화합니다.
- 프로시저 추상화는 기능 분해의 길로 들어서는 것입니다. 기능 분해는 알고리즘 분해라고도 부릅니다.
💡 데이터 추상화
- 데이터 추상화는 두 가지로 구분할 수 있습니다.
- 첫째. 데이터를 중심으로 타입을 추상화하는 것(추상 데이터 타입)
- 둘째. 데이터를 중심으로 프로시저를 추상화하는 것(객체지향)
프로시저 추상화와 기능 분해
💡 메인 함수로서의 시스템
- 기능 분해의 관점에서 추상화의 단위는 프로시저이며 시스템은 프로시저를 단위로 분해합니다.
- 프로시저는 반복적으로 실행되거나 거의 유사하게 실행되는 작업들을 하나의 장소에 모아놓음으로써 로직을 재사용하고 중복을 방지할 수 있습니다.
- 프로시저를 추상화라고 부르는 이유는 내부의 자세한 구현은 모르더라도 퍼블릭 인터페이스만 알면 프로시저를 사용할 수 있기 때문입니다.
- 전통적인 기능 분해 방법은 하향식 접근법을 따릅니다. 하향식 접근법이란 시스템을 구성하는 가장 최상위를 정의하고, 이 최상위 기능을 좀 더 작은 기능으로 분해해 나가는 방법입니다. 각 세분화 단계는 위 단계보다 더 구체적이어야 합니다.
- 기능 분해의 결과는 최상위 기능을 수행하는데 필요한 절차들을 실행되는 시간의 순서에 따라 나열한 것입니다.
- 기능 분해 방법은 기능을 중심으로 필요한 데이터를 결정합니다. 기능 분해라는 관점에서는 주인공은 기능이며 데이터는 기능을 보조할 뿐입니다. 또한 하향식 접근법은 먼저 필요한 기능을 생각하고 나누며, 이 과정에서 필요한 데이터의 종류와 저장 방식을 식별합니다.
예제
- 상황 예시: 북 스터디에 참여한다.
- 전통적인 기능 분해 방법을 통해 최상위의 표현을 여러 개의 더 구체적인 표현으로 나눕니다.
표현 방법
- 북 스터디에 참여한다.
- 책을 읽는다.
- 읽을 책을 정한다.
- 반복해서 읽는다.
- 정리한다.
- 특정 블로그에 정리한다.
- 특정일에 참여 장소로 이동한다.
- 오늘이 특정일인지 확인한다.
- 교통수단을 이용하여 이동한다.
- 공부한 내용을 공유한다.
- 책을 읽는다.
구현 예제
- 아래 예제에서 볼 수 있듯이 각 세부 사항은 위 단계를 구체적으로 표현하고 있습니다. 하향식 기능 분해 방식으로 설계한 시스템은 메인 함수를 루트로 하는 트리로 표현할 수 있습니다.
public class Main {
public static List<String> sites = List.of("티스토리", "노션", "벨로그", "깃헙");
public static String bookName = "오브젝트";
public static String site = "티스토리";
public static String vehicle = "지하철";
public static void main(String[] args) {
String contents = readingBook(bookName);
String link = organizedContents(contents, site);
move(vehicle, link);
}
private static String readingBook(String bookName) {
for (int i = 0; i < 3; i++) {
// 반복해서 책 읽기
}
return bookName;
}
private static String organizedContents(String contents, String site) {
return sites.stream()
.filter(s -> s.equals(site))
.map(s -> s + "에 " + contents + "를 정리합니다.")
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("해당 사이트는 존재하지 않습니다."));
}
private static void move(String vehicle, String link) {
LocalDate toDay = LocalDate.now();
LocalDate dueDay = LocalDate.of(2022, 10, 15);
if (toDay.compareTo(dueDay) == 0) {
System.out.println(link);
System.out.println(vehicle + "을 타고 이동하여 도착합니다.");
System.out.println("정리한 내용을 토대로 스터디를 진행합니다.");
} else {
throw new IllegalStateException("오늘은 스터디날이 아니네");
}
}
}
💡 하향식 기능 분해의 문제점
- 사실상 시스템은 하나의 메인 함수로 구현되어 있지 않습니다.
- 기능 추가 및 요구사항 변경으로 인해 메인 함수를 빈번히 수정해야 합니다. 예를들어 불참 기능이 추가되었을 경우 수정 발생
- 비지니스 로직이 사용자 인터페이스와 강하게 결합됩니다.
- 하향식 분해는 너무 이른시기에 순서를 고정시키므로 유연성과 재사용성이 떨어집니다.
- 데이터 형식이 변경될 경우 부작용을 예측할 수 없습니다.
- 프로시저 추상화(하향식 기능 분해)는 변경에 취약합니다. 설계가 필요한 이유는 오늘의 기능은 정상적으로 수행하되 내일의 변경을 받아들이기 위해서이지만 프로시저 추상화는 이를 어렵게 만듭니다.
🧨 하나의 함수라는 비현실적인 아이디어
- 어떠한 시스템도 최초에 릴리즈됐던 모습을 영원히 그대로 유지하지 않습니다. 시간이 지나 새로운 요구 사항이 발생하고 그에 맞게 새로운 기능을 추가하게 됩니다. 그렇기 때문에 시스템이 오직 하나의 메인 함수만으로 구현된다는 개념과는 모순이 발생하게 됩니다.
🧨 메인 함수의 빈번한 재설계
- 하향식 기능 분해의 관점으로 바라볼 때 메인 함수를 유일한 정상으로 간주함으로 기존 로직과 상관없는 새로운 함수를 추가했을 때 적절한 위치를 찾기 힘들며, 메인 함수의 구조를 변경하게 됩니다.
- 아래 예제는 스터디 진행을 위해 참여자 불참여자를 확인하는 메서드가 새로 생겼을 경우 어떻게 해결해야 하는가에 대한 논의입니다.
public class Main {
public static List<String> personList = List.of("홍길동", "이순신", "주몽", "유관순", "광개토대왕");
public static List<Boolean> isAttend = List.of(true, true, false, true, false);
public static String bookName = "오브젝트";
public static String site = "티스토리";
public static String vehicle = "지하철";
// 메인 함수에서 적절한 위치는?
public static void main(String[] args) {
String contents = readingBook(bookName);
String link = organizedContents(contents, site);
move(vehicle, link);
}
// 참여 및 불참여를 확인하는 메서드
private static void checkedNotAttend() {
for (int i = 0; i < personList.size(); i++) {
System.out.println(personList.get(i) + message(isAttend.get(i)));
}
}
private static String message(boolean isAttend) {
if (isAttend) return "는 참여 가능합니다.";
return "참여가 불가능 합니다.";
}
}
// 기존 함수를 수정
public static void main(String[] args) {
study(); // <- 기존 로직을 해당 함수로 이동
checkedNotAttend();
}
🧨 비지니스 로지과 사용자 인터페이스의 결합
- 하향식 기능 분해는 비지니스 로직을 설계하는 초기 단계부터 입력 방법과 출력 양식을 함께 고민하도록 강요합니다.
🧨 성급하게 결정된 실행순서
- 하향식 기능 분해는 각 과정을 더 작은 함수로 분해하고, 분해된 함수들의 실행 순서를 결정하는 작업을 요약할 수 있습니다.
이것은 설계 시작점부터 시스템이 무엇이 아닌 어떻게 동작해야하는지에 초점을 맞추게 됩니다. - 어떻게 동작하는지에 초점을 맞추게 된다면 기능이 추가되거나 변경될 때마다 기존 제어구조의 변경을 발생시킵니다. 이를 해결하기 위해서는 시간적인 제약이 아닌 논리적인 제약으로 설계 기준을 잡아야합니다.
객체지향은 함수간의 호출 순서가 아닌 객체간의 논리적인 관계를 중심으로 설계를 이끌어 나갑니다. - 또한 하향식 기능 분해의 모든 문제는 결합도 입니다. 하위 함수는 상위 함수의 문맥에 강하게 결합됩니다.
🧨 데이터 변경으로 인한 부작용
- 하향식 기능 분해의 가장 큰 문제는 어떤 데이터를 어떤 함수가 사용하고 있는지 추적하기 힘듭니다. 따라서 어떠한 데이터 변경으로 인해 어떤 함수에 영향을 미치게 될지 예상하기 힘듭니다.
💡 하향식 분해가 유용한 경우는?
- 프로그래밍 과정에서 이미 해결된 알고리즘을 문서화하고 서술하는데 훌륭한 기법입니다. 하지만 새로운 것을 개발하고, 설계하고, 발견하는데는 적합한 방법이 아닙니다.
모듈
💡 정보 은닉과 모듈
- 시스템의 변경을 관리하는 기본적인 전략은 함께 변경되는 부분을 하나의 구현 단위로 묶고 퍼블릭 인터페이스를 통해서만 접근 가능하도록 만드는 것입니다.
- 정보 은닉은 시스템을 모듈 단위로 분해하기 위한 기본 원리로 시스템에서 자주 변경되는 부분을 안정적인 인터페이스 뒤로 감춰야 한다는것이 핵심입니다.
- 모듈은 변경될 가능성이 있는 비밀을 내부로 감추고, 퍼블릭 인터페이스를 외부에 제공함으로써 내부의 비밀에 접근하지 못하게 합니다.
모듈 <> 기능 분해
- 모듈과 기능 분해는 상호 배타적인 관계가 아닙니다. 시스템을 모듈 단위로 분해한 후 각 모듈 내부를 구현하기 위해 기능 분해 방법을 적용할 수 있습니다. 기능 분해가 하나의 어플리케이션을 만들기 위해 필요한 기능들을 순차적으로 찾아가는 탐색의 과정이라면 모듈 분해는 숨겨야 하는 비밀을 선택하고 비밀을 감싸는 보존의 과정입니다. 모듈을 분해한 후 기능 분해를 이용해 각 모듈에 필요한 퍼블릭 인터페이스를 구현할 수 있습니다.
모듈이 감춰야하는 두 가지
- 복잡성
- 모듈이 너무 복잡한 경우 이해하고 사용하기 어렵습니다. 그렇기 때무에 모듈을 추상화할 수 있는 간단한 인터페이스를 제공해 복잡성을 낮춥니다.
- 변경 가능성
- 변경 가능한 로직이 외부에 노출될 경우 변경이 이루어졌을 때 파급 효과가 커집니다.
public class Person {
private static List<String> personList = List.of("홍길동", "이순신", "주몽", "유관순", "광개토대왕");
private static List<Boolean> isAttend = List.of(true, true, false, true, false);
private static List<String> sites = List.of("티스토리", "노션", "벨로그", "깃헙");
private static String bookName = "오브젝트";
private static String site = "티스토리";
private static String vehicle = "지하철";
public static void study() {
String contents = readingBook(bookName);
String link = organizedContents(contents, site);
move(vehicle, link);
}
private static String readingBook(String bookName) {
for (int i = 0; i < 3; i++) {
// 반복해서 책 읽기
}
return bookName;
}
private static String organizedContents(String contents, String site) {
return sites.stream()
.filter(s -> s.equals(site))
.map(s -> s + "에 " + contents + "를 정리합니다.")
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("해당 사이트는 존재하지 않습니다."));
}
private static void move(String vehicle, String link) {
LocalDate toDay = LocalDate.now();
LocalDate dueDay = LocalDate.of(2022, 10, 15);
if (toDay.compareTo(dueDay) == 0) {
System.out.println(link);
System.out.println(vehicle + "을 타고 이동하여 도착합니다.");
System.out.println("정리한 내용을 토대로 스터디를 진행합니다.");
} else {
throw new IllegalStateException("오늘은 스터디날이 아니네");
}
}
public static void checkedNotAttend() {
for (int i = 0; i < personList.size(); i++) {
System.out.println(personList.get(i) + message(isAttend.get(i)));
}
}
private static String message(boolean isAttend) {
if (isAttend) return "는 참여 가능합니다.";
return "참여가 불가능 합니다.";
}
}
public static void main(String[] args) {
Person.study(); // 함수 호출
Person.checkedNotAttend();
}
💡 모듈의 장점과 한계
- 모듈 내부의 변수가 변경되더라도 모듈 내부에서만 영향을 미칩니다.
- 비지니스 로직과 사용자 인터페이스에 대한 관심사를 분리합니다.
- 전역 변수와 전역 함수를 제거함으로써 네임스페이스 오염을 방지합니다.
- 모듈은 기능이 아닌 변경의 정도에 따라 시스템을 분해해야 합니다. 각 모듈은 외부로부터 감춰야 하는 비밀과 관련성 높은 데이터와 함수의 집합입니다. 따라서 모듈은 높은 응집도를 유지하게 되고, 모듈과 모듈 사이는 퍼블릭 인터페이스를 통해 통신함으로 낮은 결합도를 유지할 수 있습니다.
- 모듈의 핵심은 데이터 입니다. 하향식 기능 분해와 달리 모듈은 감춰야 할 데이터를 결정하고 이 데이터를 조작하는데 필요한 함수를 결정합니다. 즉 기능이 아닌 데이터를 중심으로 시스템을 분해하는 것입니다.
- 모듈의 가장 큰 단점은 인스턴스의 개념을 제공하지 않는다는 점입니다.
데이터 추상화와 추상 데이터 타입
💡 추상 데이터 타입
- 프로그래밍 언어에서 타입이란 변수에 저장할 수 있는 내용물의 종류와 변수에 적용될 수 있는 연산의 가짓수를 의미합니다.
- 추상 데이터 타입은 말 그대로 시스템의 상태를 저장할 데이터를 표현합니다.
추상 데이터 타입을 구현하기 위한 조건
- 타입 정의를 선언할 수 있어야 합니다.
- 타입의 인스턴스를 다루기 위해 사용할 수 있는 오퍼레이션의 집합을 정의할 수 있어야 합니다.
- 제공된 오퍼레이션을 통해서만 조작할 수 있도록 데이터를 위부로부터 보호할 수 있어야 합니다.
- 타입에 대해 여러 개의 인스턴스를 생성할 수 있어야 합니다.
클래스
💡 클래스는 추상 데이터 타입인가?
- 클래스와 추상 데이터 타입은 데이터 추상화를 기반으로 시스템을 분해하여 클래스를 추상 데이터 타입이라고 설명하곤 합니다. 이렇게 설명하는 이유는 두 매커니즘 모두 외부에서는 객체의 내부 속성에 직접 접근할 수 없으며 오직 퍼블릭 인터페이스만을 통해서 외부와 의사소통을 할 수 있습니다.
- 그러나 이는 명확한 의미에서는 동일하지 않습니다. 가장 큰 차이는 클래스는 상속과 다형성을 지원하는데 추상 데이터 타입은 지원하지 못합니다. 상속과 다형성을 지원하는 객체지향 프로그래밍과 구분하기 위해 상속과 다형성을 지원하지 않는 추상 데이터 타입을 객체 기반 프로그래밍이라 합니다.
- 추상 데이터 타입은 타입을 추상화한 것이고 클래스는 절차를 추상화한 것입니다.
예제
- 아래 예제는 참여 불참여를 기준으로 지불 금액을 달리 하고 있습니다.
- 여기서 강조할 점은 하나의 타입처럼 보이는 Person 내부에는 참여자 타입과 불참여자 타입이 공존하고 있습니다. 윌리엄 쿡은 하나의 대표적인 타입이 다수의 세부적인 타입을 감추기 때문에 이를 타입 추상화라고 불렀습니다.
- 타입 추상화는 개별 오퍼레이션이 모든 개념적인 타입(Person)에 대한 구현을 포괄하도록 함으로써 하나의 물리적인 타입 안에 전체 타입을 감춥니다. 따라서 타입 추상화는 오퍼레이션을 기준으로 타입을 통합하는 데이터 추상화 기법입니다.
- 추상 데이터 타입은 오퍼레이션을 기준으로 타입을 추상화하며 클래스는 타입을 기준으로 절차를 추상화합니다.
public class Person {
private static List<String> personList = List.of("홍길동", "이순신", "주몽", "유관순", "광개토대왕");
private static List<Boolean> isAttend = List.of(true, true, false, true, false);
private static int pay = 5000;
private static int fined = 10000;
... 생략
public static void defaultPay() {
for (int i = 0; i < personList.size(); i++) {
if (isAttend.get(i) == true) {
System.out.println(personList.get(i) + "의 지급 급액은 " + pay + "입니다.");
}
}
}
public static void finedPay() {
for (int i = 0; i < personList.size(); i++) {
if (isAttend.get(i) == false) {
System.out.println(personList.get(i) + "의 지급 급액은 " + fined + "입니다.");
}
}
}
}
💡 추상 데이터 타입에서 클래스로 변경하기
@Getter
@AllArgsConstructor
public abstract class Person {
protected String name;
protected Study study;
private static final List<String> sites = List.of("티스토리", "노션", "벨로그", "깃헙");
protected String readingBook(String bookName) {
for (int i = 0; i < 3; i++) {
// 반복해서 책 읽기
}
return bookName;
}
protected String organizedContents(String contents, String site) {
return sites.stream()
.filter(s -> s.equals(site))
.map(s -> s + "에 " + contents + "를 정리합니다.")
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("해당 사이트는 존재하지 않습니다."));
}
protected void move(String vehicle, String link) {
LocalDate toDay = LocalDate.now();
LocalDate dueDay = LocalDate.of(2022, 10, 14);
if (toDay.compareTo(dueDay) == 0) {
System.out.println(link);
System.out.println(vehicle + "을 타고 이동하여 도착합니다.");
System.out.println("정리한 내용을 토대로 스터디를 진행합니다.");
} else {
throw new IllegalStateException("오늘은 스터디날이 아니네");
}
}
protected abstract void study();
protected abstract void pay();
}
public class AttendPerson extends Person {
protected String vehicle;
public AttendPerson(String name, String vehicle, Study study) {
super(name, study);
this.vehicle = vehicle;
}
@Override
protected void study() {
System.out.println(getName() + "은 스터디에 참여합니다.");
String contents = readingBook(study.getBookName());
String link = organizedContents(contents, study.getSite());
move(vehicle, link);
}
@Override
protected void pay() {
System.out.println("기본 금액인 5천원을 지불합니다.");
}
}
public class NotAttendPerson extends Person {
public NotAttendPerson(String name, Study study) {
super(name, study);
}
@Override
protected void study() {
System.out.println(getName() + "는 스터디에 참여하지 않습니다.");
}
@Override
protected void pay() {
System.out.println("벌금 포함 금액인 1만원을 지불합니다.");
}
}
@Getter
@AllArgsConstructor
public class Study {
private String bookName;
private String site;
}
public class Main {
public static void main(String[] args) {
Study study = new Study("오브젝트", "티스토리");
Person personA = new AttendPerson("홍길동", "자전거", study);
Person personB = new AttendPerson("이순신", "지하철", study);
Person personC = new NotAttendPerson("주몽", study);
Person personD = new AttendPerson("유관순", "걷가", study);
Person personE = new NotAttendPerson("광개토대왕", study);
personA.study();
personB.study();
personC.study();
personD.study();
personE.study();
}
}
💡 변경을 기준으로 선택하라
- 단순히 클래스를 사용했다고해서 객체지향 프로그래밍을 한다는 것을 의미하지 않습니다. 타입을 기준으로 절차를 추상화하지 않았다면 그것은 객체지향 분해가 아닙니다.
- 객체지향에서는 타입 변수를 이용한 조건문을 다형성으로 대체합니다. 클라이언트가 객체의 타입을 확인한 후 적절한 메서드를 호출하는게 아닌 메시지가 적절한 객체를 선택하도록 해야합니다.
- 새로운 Person 유형이 추가되더라도 다형성을 통해 문제를 해결할 수 있습니다. 또한 기존 코드에 아무런 영향을 미치지 않고 새로운 객체 유형과 행위를 추가할 수 있는 객체지향 특성을 개방-폐쇄 원칙(OCP)라 부릅니다.
💡 협력이 중요하다
- 다형성을 통해 오퍼레이션 구현 방법을 달리 한다고 해서 객체지향적인 설계를 가지는게 아닙니다. 객체지향에서 중요한 것은 역할, 책임, 협력입니다.
- 객체지향은 애플리케이션의 기능을 수행하기 위해 객체들이 협력하는 방식에 집중합니다. 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션만 분리하여 구현했다고 해서 객체지향 설계를 가지는게 아닙니다.
✔️ 정리
- 객체가 참여할 협력을 결정하고 협력에 필요한 적절한 책임을 수행하기 위해 어떤 객체가 필요한지 고민하고, 그 책임을 다양한 방식으로 수행해야 할 때만 타입 계층 안에 각 절차를 추상화해야 합니다. 또한 타입 계층과 다형성은 협력이라는 문맥 안에서 책임을 수행하는 방법에 대해 고민한 결과물이여야 하며 그 자체가 목적이 되어서는 안됩니다.
728x90
반응형
'스터디 > 오브젝트' 카테고리의 다른 글
오브젝트 - 9장 유연한 설계 (0) | 2022.10.21 |
---|---|
오브젝트 - 8장 의존성 관리하기 (0) | 2022.10.17 |
오브젝트 - 6장 메시지와 인터페이스 (0) | 2022.10.09 |
오브젝트 - 5장 책임 할당하기 (0) | 2022.10.07 |
오브젝트 - 4장 설계 품질과 트레이드오프 (0) | 2022.10.02 |
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
TAG
- java userThread와 DaemonThread
- JDK Dynamic Proxy와 CGLIB의 차이
- polling publisher spring boot
- 트랜잭셔널 아웃박스 패턴 스프링부트
- 공간 기반 아키텍처
- 서비스 기반 아키텍처
- transactional outbox pattern spring boot
- pipeline architecture
- spring boot poi excel download
- spring boot redisson sorted set
- spring boot redisson 분산락 구현
- 자바 백엔드 개발자 추천 도서
- @ControllerAdvice
- spring boot redis 대기열 구현
- 트랜잭셔널 아웃박스 패턴 스프링 부트 예제
- spring boot excel download oom
- spring boot redisson destributed lock
- 람다 표현식
- microkernel architecture
- transactional outbox pattern
- redis sorted set으로 대기열 구현
- spring boot excel download paging
- spring boot 엑셀 다운로드
- pipe and filter architecture
- redis 대기열 구현
- space based architecture
- java ThreadLocal
- redis sorted set
- 레이어드 아키텍처란
- service based architecture
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
글 보관함