일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- git
- 우테코
- 기능명세서
- 커밋 메시지
- 설계
- 플레이 스토어 20명
- 구글 플레이 비공개 테스트
- 객체지향설계
- 운영체제 #CS지식
- 플레이스토어 비공개 테스트
- 클린코드
- 구글 플레이 스토어 배포 방법
- 구글 비공개 테스트 20명
- 프리코스
- 객체지향
GYUD-TECH
[스터디: 오브젝트] 데이터 중심 설계 본문
데이터 중심 설계
데이터 중심 설계
책임중심설계는 객체의 책임을 생각하고, 책임 수행을 위한 행동을 정의한 후, 필요한 데이터를 저장한다.
반면, 데이터중심설계는 객체에 필요한 데이터를 먼저 정하고, 이후에 데이터를 조작하는데 필요한 행동을 정의한다.
데이터는 인터페이스가 아닌 객체의 구현에 속하기 때문에 자주 변하고 불안정하다는 특징이 있다.
이런 데이터를 중심으로 설계하면 설계가 자주 변할 것이고, 결국 변경에 취약해진다는 단점이 있다.
객체지향의 사실과 오해와 이전 장에서 이러한 개념을 반복하여 학습했지만 실제 코드로 보지 못해서 아쉬웠다.
이번 장에서는 코드를 통해 영화 예매 시스템의 객체 지향 설계와 데이터 중심 설계를 비교해 주어 내용을 더 깊이 학습할 수 있었다.
데이터 중심 설계 코드
Movie 객체는 영화 정보인 영화 제목, 상영시간, 기본 요금을 필요로 한다.
또한, 기본 요금에서 할인을 위해 아래 인스턴스들이 필요하다.
- 어떤 할인 정책을 사용하는지에 관한 MovieType 인스턴스
- 고정 할인이 적용되었을 때 할인 금액 양인 dicountAmount 인스턴스
- 비율 할인이 적용되었을 때 할인 금액 비율인 discountPercent 인스턴스
- 할인 정책을 적용하는 조건인 List<DiscountCondition> 인스턴스
이를 코드로 나타내면 아래와 같이 표현할 수 있다.
public class Movie {
private String title; // 영화 제목
private Duration runnintTime; // 상영 시간
private Movie fee; // 기본 요금
private List<DiscountCondition> conditions;
private MovieType movieType; // 할인 정책 사용 종류
private Money discountAmount; // 할인 금액 양
private double discountPercent; // 할인 퍼센트
}
public enum MovieType {
AMOUNT_DISCOUNT, // 금액 할인 정책
PERCENT_DISCOUNT, // 비율 할인 정책
NONE_DISCOUNT // 할인 없음
}
위의 방식은 movieType에 따라 discountAmount와 discountPercent 중 하나만 사용하기 때문에 비효율적이라고 생각했다.
일반적으로 데이터 중심으로 설계하면 위와 같이 상황에 따라 쓰이는 인스턴스가 생성된다는 문제점이 발생한다.
데이터베이스에서도 null 값이 많으면 정규화를 통해서 테이블을 분리하는데 이와 비슷하게 객체지향에서도 이를 분리함으로써 해결해 줘야 한다.
책에서는 먼저 getter와 setter를 사용해서 ReservationAgency 객체에서 Movie 객체의 행동을 제어하도록 했다.
public class ReservationAgency {
public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
Movie movie = screening.getMovie();
// 할인 조건을 보며 할인 가능 여부를 확인하는 로직
// 할인 여부를 보고 할인 정책에 따라 할인 요금을 계산하는 로직
}
이 부분을 읽으면서 외부에서 Movie 객체의 행동을 제어하도록 하는 것은 데이터 중심 설계의 문제점은 아니라고 생각했다.
데이터 중심으로 설계하더라도 충분히 Movie 객체 내에서 데이터들을 다루도록 설계 할 수 있다.
public class Movie {
private String title;
private Duration runnintTIme;
private Money fee;
private List<DiscountCondition> discountConditions;
private MovieType movieType;
private Money discountAmount;
private double discountPercent;
public MovieType getMovieType(){
return movieType;
}
public Money calculateAmountDiscountedFee() {
if (movieType != MovieType.AMOUNT_DISCOUNT) {
throw new IllegalArgumentException();
}
return fee.minus(discountAmount);
}
public Money calculatePercentDiscountedFee() {
if (movieType != MovieType.PERCENT_DISCOUNT) {
throw new IllegalArgumentException();
}
return fee.minus(fee.times(discountPercent));
}
public Money calculateNoneDiscountedFee() {
if (movieType != MovieType.NONE_DISCOUNT) {
throw new IllegalArgumentException();
}
return fee;
}
책에서는 위의 코드를 캡슐화한 코드라고 알려주었지만, 이것도 getType을 통해 외부에서 type을 제어하기 때문에 캡슐화가 완료되지 않은 코드이다.
데이터 중심 설계를 그대로 유지하면서 아래와 같이 코드를 수정할 수 있다.
public class Movie {
private String title;
private Duration runnintTIme;
private Money fee;
private List<DiscountCondition> discountConditions;
private MovieType movieType;
private Money discountAmount;
private double discountPercent;
public Money calculateFee(){
if (movieType == MovieType.AMOUNT_DISCOUNT){
return calculateAmountDiscountedFee();
}
if (movieType == MovieType.PERCENT_DISCOUNT){
return calculatePercentDiscountedFee();
}
return NoneDiscountedFee();
}
public Money calculateAmountDiscountedFee() {
return fee.minus(discountAmount);
}
public Money calculatePercentDiscountedFee() {
return fee.minus(fee.times(discountPercent));
}
public Money calculateNoneDiscountedFee() {
return fee;
}
이렇게 하면 외부에서는 calculateFee() 만 호출하면 되고, 어떻게 금액을 계산하는지는 알 수 없다.
책에서 언급한 코드보다 훨씬 캡슐화가 잘 작성된 코드이다.
나는 프로젝트를 할 때 주로 데이터로 객체를 먼저 설계하는 데이터 주도 방식을 사용했었다.
하지만 위와 같이 캡슐화를 지키기 위해서, 객체의 상태는 객체 내부에서만 다루도록 설계하였다.
그러면 이렇게 설계한 코드는 문제가 없는지 궁금했다.
좋은 설계
좋은 설계란 높은 응집도와 낮은 결합도를 가진 설계이다.
높은 응집도와 낮은 결합도를 통해서 구현이 변경되더라도 설계는 변경되지 않는 변화에 민감하지 않은 설계를 할 수 있다.
응집도
응집도란 모듈에 포함된 내부 요소들이 연관되어 있는 정도를 뜻한다.
모듈 내의 요소들이 하나의 목적으로 긴밀하게 협력하면 높은 응집도, 다른 목적을 추구하면 낮은 응집도라고 할 수 있다.
결합도
다른 객체와의 의존성 정도로 다른 모듈에 대해 얼마나 알고 있느냐를 뜻한다.
💡 응집도는 모듈 내의 객체들이 얼마나 단단하게 뭉쳐있느냐를 뜻한다.
💡 결합도는 외부와 모듈간의 관계를 뜻한다.
변경이 발생했을 때 모듈 전체가 함께 변경된다면 응집도가 높은 것이고, 일부만 변경된다면 응집도가 낮은 것이다.
만약 모듈 외부도 변경된다면 결합도가 높은 것이라고 할 수 있다.
응집도가 높고, 결합도가 낮은 설계를 한다면, 변경이 발생했을 때 해당 모듈만 변경하면 되기 때문에 변경해야하는 부분을 하나한 찾을 필요 없이 해당 모듈만 수정하면 되어 변경에 용이하다.
그러면 응집도를 높이고, 결합도를 낮추기 위해서는 어떻게 해야할까?
정답은, 캡슐화를 하면 된다.
캡슐화를 통해 응집도를 높이고, 결합도를 낮출 수 있다.
캡슐화
캡슐화란 자신의 상태는 자기 스스로 제어하도록 하는 것을 뜻한다.
캡슐화를 통해서 내부의 구현을 감추고, 인터페이스만 노출하여 변경 시 해당 모듈만 변경하도록 설계할 수 있다.
이제까지 데이터를 외부로부터 감추는 것을 캡슐화라고 생각했는데 이는 캡슐화의 일부인 데이터 캡슐화이다.
💡 진정한 캡슐화란, 단순히 데이터만 외부로 부터 감추는게 아니라 변할 수 있는 모든것을 내부로 숨기고, 잘 변하지 않는 인터페이스만 외부로 노출하는 것이다
결국 변경이 발생했을 때 얼마나 코드를 변경해야하는가로 캡슐화가 잘 되었는지 판단할 수 있다.
책을 읽고 내가 이전에 작성한 코드가 올바르게 캡슐화가 되었는지 생각해보았다.
public class Movie {
private String title;
private Duration runnintTIme;
private Money fee;
private List<DiscountCondition> discountConditions;
private MovieType movieType;
private Money discountAmount;
private double discountPercent;
public Money calculateFee(){
if (movieType == MovieType.AMOUNT_DISCOUNT){
return calculateAmountDiscountedFee();
}
if (movieType == MovieType.PERCENT_DISCOUNT){
return calculatePercentDiscountedFee();
}
return NoneDiscountedFee();
}
public Money calculateAmountDiscountedFee() {
return fee.minus(discountAmount);
}
public Money calculatePercentDiscountedFee() {
return fee.minus(fee.times(discountPercent));
}
public Money calculateNoneDiscountedFee() {
return fee;
}
만약 새로운 할인 정책이 추가할 때 할인 정책의 가격을 계산하는 Movie 객체만 변경하면 되면 캡슐화가 잘 된 것이다.
하지만 위 코드는 Movie 객체 뿐 만 아니라 MovieType에 새로운 할인정책을 추가해줘야 한다.
Movie 객체에서 MovieType에 어떤 종류가 있는지 이미 알고 있기 때문에 두 곳 모두를 변경해야 하는 것이다.
이를 해결하기 위해 MovieType에 따라 그에 맞는 할인 정책을 적용하도록 코드를 변경할 수 있다.
각각의 MovieType마다 calculateFee() 메서드가 필요하기 때문에 인터페이스로 선언하고 구현체에서 각각 다르게 구현하는 방식으로 설계할 수 있다.
public class Movie {
private String title;
private Duration runnintTime;
private Money fee;
private List<DiscountCondition> discountConditions;
private MovieType movieType;
public Money calculateFee(){
return movieType.calculate(fee);
}
}
public interface MovieType {
private Money calculateFee();
}
public class AmountMovieType implements MovieType {
private final int discountAmount;
@Override
private Money calculateFee(Money fee){
return fee.minus(discountAmount);
}
}
public class PercentMovieType implements MovieType {
private final double discountPercent;
@Override
private Money calculateFee(Money fee){
return fee.minus(fee.times(discountPercent));
}
}
public class NoneMovieType implements MovieType {
@Override
private Money calculateFee(Money fee){
return fee;
}
}
이렇게 하면 Movie 객체는 discountAmount와 discountPercent 필드를 가질 필요 없고, 해당 필드가 필요한 MovieType 구현체들만 이를 가지고 있으면 된다.
추가적으로 할인 조건은 MovieType에 포함되어 discountCondition 내부에서 수행하도록 변경하면 모든 객체가 캡슐화 된다.
눈치가 빠르다면, 이렇게 바꾸고 클래스와 인터페이스명만 바꾸면 이전 장의 코드와 동일해진다는 것을 알 수 있다.
public abstract class DiscountPolicy {
private List<DiscountCondition> conditions = new ArrayList<>();
public DiscountPolicy(DiscountCondition ... conditions) {
this.conditions = Arrays.asList(conditions);
}
public Money calculateFee(Screening screening) {
for (DiscountCondition each: conditions) {
if (each.isSatisfyBy(screening)){
return getDiscountAmount(screening);
}
}
return Money.ZERO;
}
...
}
이렇게 설계하면 새로운 할인 정책이 추가된다 하더라도 DiscountPolicy를 상속받는 클래스를 추가하기만 하면 다른 객체들에 영향을 주지 않고 쉽게 변경이 가능하다.
결국, 높은 응집도와 낮은 결합도를 위해 캡슐화를 수행하다보면, 책임 중심 설계 했던 방식과 동일해 진다는 것을 알 수 있었다.
따라서, 좋은 설계란 책임중심설계의 방식을 따르는 설계이다.
고민
우테코 프리코스를 하면서 객체지향설계를 배웠지만, 이를 진행중인 Spring Web 프로젝트에 적용하기 어려웠다.
책을 읽으면서 그 이유에 대해서 고민해 보았는데 첫번째 이유는 대부분의 요청이 CRUD 이기 때문이라고 생각했다.
create와 update 요청의 경우에는 객체의 필드 값들을 외부에서 전달해 주기 때문에 외부에서 내부 구현을 알 수 밖에 없다고 생각했다.
위 코드에서도 새로운 영화를 추가해야 한다면, Movie 객체 생성에 필요한 필드가 어떤것이 있는지 외부에서 알아야만 한다.
만약 특정 값을 외부에서 주입받지 않고 내부 협력에 의해서 계산할 수 있다면 협력에 의한 설계를 할 수 있을 것이다.
두번째 이유는 MVC 패턴에서 Service 계층의 사용이라고 생각했다.
MVC 패턴에서 사용하는 Service 계층에서는 domain 객체들을 getter로 가져와서 비즈니스 로직을 수행한다.
이 때문에 Service 계층은 사용하는 도메인 객체의 구현을 알아야 하고 캡슐화가 깨진다.
객체지향의 설계만 적용한다면, getter를 사용하지 않고, 해당 객체내부에서 로직을 수행하도록 하는게 맞다고 느꼈다.
느낀점
오브젝트 책은 객체지향의 사실과 오해와 달리 코드를 보여주기 때문에 생각할거리를 더 많이 주는 것 같다.
서비스 계층의 사용으로 인한 객체지향설계 원칙 위반에 대한 궁금증은, spring mvc에 대한 이해가 부족하여 고민하는 것이라 생각하여 spring mvc에 대해 공부중이다.
이 내용을 스터디원들과 함께 이야기 해보고, 개념을 정리하여 새로운 포스트로 정리하도록 하겠다.
'스터디' 카테고리의 다른 글
[스터디: 오브젝트] 디미터 법칙과 트레이드오프 (1) | 2024.02.19 |
---|---|
[스터디: 오브젝트] 설계 개선하기 (1) | 2024.02.18 |
[스터디: 오브젝트] 객체지향 프로그래밍 (2) | 2024.02.05 |
[스터디: 오브젝트] 좋은 설계 (1) | 2024.02.05 |
[스터디: 객체지향의 사실과 오해] 추상화기법, 상속, 합성 (0) | 2024.02.01 |