Java 객체 지향 - 의존관계와 캡슐화

hwanse·2021년 3월 3일
1

Java

목록 보기
14/14

이 글은 개발자가 반드시 정복해야할 객체지향과 디자인 패턴책을 읽고 내용을 정리한 글 입니다.




목표
1. 의존관계
2. 캡슐화





의존관계

객체지향 프로그래밍에서 한 객체가 다른 객체를 이용한다는 것은, 다른 객체를 생성하거나 다른 객체의 메소드를 호출하는 행위를 말한다. 이와 같이 한 객체가 다른객체를 생성 또는 메소드를 호출할 때, 그 객체에 의존한다고 표현한다.

class Player {
  private Weapon weapon;
  
  public void availWeapon() {
    weapon = new Sword();
    weapon.attack();
  }
}

interface Weapon {
  public void attack();
}

class Sword implements Weapon {
  @Override
  public void attack() {
    System.out.println("Sword");
  }
}

위 예제를 보면 Player는 Weapon을 생성하여 attack이라는 메소드를 호출하고 있으므로 Weapon에 의존한다고 볼 수 있다. 또한 파라미터를 전달 받는 경우에도 해당 파라미터에 대해 의존한다고 볼 수 있다. 의존성을 가진다는 것은 의존하고 있는 객체가 변경이 발생했을 때 그 객체를 이용하는 나 자신도 변경될 가능성이 있다는 것을 뜻한다. 이렇게 의존성을 가지고 있게 되면 변화에 유연하지 못한 구조를 가진 프로그램이라고 할 수 있다.


위 그림처럼 C 는 B를 의존하고 B는 A를 의존하고 있는 관계라고 가정한다면 A 클래스의 변경은 B클래스의 영향을 줄 수 있고, B클래스의 영향이 다시 C클래스에 영향으로 이어질수가 있다.


위 그림은 의존이 순환하는 구조인데 A클래스의 변화가 C클래스에 영향을 줄 수 있고, C클래스의 변화가 다시 A클래스의 영향으로 이어지게 되며 결국 A클래스의 변화에 대한 영향이 자기 자신에게 또 다른 영향을 줄 수 있다. 이러한 순환 의존 관계를 발생하지 않도록 하는 원칙이 하나 있는데 바로 의존 역전 원칙(Dependency inversion principle: DIP)이라고 한다.

결론적으로는 이러한 의존 관계를 가지게 되면 상호간에 영향을 줄 수 있다는 것이고, 즉 내가 변경되면 나를 의존하고 있는 코드들에 영향을 주게되는 것이고 새로운 요구사항이 들어올때 나의 변동사항이 내가 의존하고 있는 객체의 코드에 영향을 줄 수도 있다.



캡슐화

객체 지향의 장점은 구현 변경이 다른 곳에 변경에 대한 영향을 주지 않도록 할 수 있고, 원할한 수정을 할 수 있는 구조를 가지는 것이 객체지향 프로그래밍이라 할 수 있다. 객체지향에서는 이러한 기능 구현사항을 캡슐화를 통해서 한 곳의 변화가 다른 곳에 영향을 주는 것을 최소화 한다고 한다.

캡슐화란? 구현된 기능사항을 객체가 내부적으로 감추는 것이다. 구현 사항을 내부적으로 감춰 외부 객체 입장에서는 내부적인 기능사항이 변동되더라도 이 기능사항에 대해서 어떻게 구현되어 있는지 모르기 때문에 내부적으로 수정에 유연함을 가질 수 있다고 한다. 캡슐화가 정보를 은닉한다 정도는 알고 있었지만 아직 잘 이해가 안간다. 캡슐화를 이용하면 어떻게 수정에 대하여 프로그램이 유연함을 가질 수 있는 것일까?

다음 예제 코드는 회원의 만료 날짜에 따라 서비스 제한을 가정한 예시이며 절차지향적인 방식의 코드를 구성한 예제다.

절차 지향적 구조

public class Member {
  private Date expiryDate;
  private boolean mail;
  
  public Date getExpiryDate() {
    return expiryDAte;
  }
  
  public boolean isMale() {
    return mail;
  }
}

이러한 Member 객체를 통해 exipryDate 데이터 값과 현재 시간을 비교하여 서비스를 처리하는 로직은 다음과 같을 것이다.

// 서비스 제한을 하는 관련 로직
if (memeber.getExpiyDate() != null && 
    member,getExpiryDate().getDate() < System.currentTimeMillis()) {
  // 만료되었을 때 처리 로직...
}

이렇게 if문으로 날짜 데이터를 조회하고 그 현재 시간과 비교하여 서비스 로직을 구성하였다.
그러던 와중에 새로운 요구사항이 들어왔고 요구사항은 여성 회원일 경우에 서비스 만료 기간이 지났어도 30일은 더 이용 가능하도록 새로운 정책을 반영해달라는 상황이다. 만료 여부에 대한 규칙이 새롭게 추가되었으니 이 코드를 다음과 같이 변경하게 되었다.

// 서비스 제한을 하는 관련 로직
long day30 = 1000 * 60 * 60 * 24 * 30 // 30일 
if (
    (
     member.isMale() && memeber.getExpiyDate() != null &&
     member,getExipryDate().getDate() < System.currentTimeMillis() 
    )
    ||
    (
     !member.isMale() && memeber.getExpiyDate() != null &&
     member,getExipryDate().getDate() < System.currentTimeMillis() - day30
    )
    ) {
  
  // 만료되었을 때 처리 로직...
}

뭔가 보기도 너무 힘들고 만약에 이러한 검사 로직이 필요한 다른 클래스 곳곳에서도 똑같이 복사 붙여넣기로 구성되어 있는 구조라고 가정하고, 새로운 정책 요구사항이 들어오는 상황을 생각해보자 이러한 로직은 실수할 여지도 많고 프로그램의 사이즈가 커질수록 새로운 요구사항에 대해서 대응하기가 힘든 구조이며, 어찌저찌 새로운 요구사항 관련 로직을 추가해주었다고 해도 미쳐 확인하지 못한 위치의 코드를 새로운 정책 로직으로 변경 못했다면 이것은 버그로 직결된다. 만일 이러한 프로그램을 관리하는 개발자가 바뀌었다면 그 개발자에게는 최악의 상황일 것이다. 이러한 데이터를 직접 사용하는 절차지향적인 구조의 코드는 데이터의 변화나 새로운 정책 요구사항에 대응하는데 유연하지 못 한 구조다.


객체 지향적 구조
위 코드의 만료기한 관련 정책을 검사하는 if문의 로직을 객체 지향적으로 캡슐화를 이용하여 재구성해보자.

public class Member {
  private Date expiryDate;
  private boolean mail;
  
  public boolean isExpired() {
    return expiryDate != null && 
           expiryDate.getDate() < System.currentTimeMillis();
  }
}

만료기한 정책을 검사하는 로직을 Member 객체 내부에 isExpired()라는 메소드로 감쌌고 Member 객체 내부에서 바로 검사를 하면되기때문에 getter 메소드들도 지웠다. 이렇게 구성하면 Member 밖에있는 객체들은 Member 데이터가 어떤 상태인지 모르며 단지 Member는 자기 자신의 데이터를 활용하여 isExpired() 메소드를 통해 정책 검사에 대한 결과를 true/false로 반환한다.

// 서비스 제한을 하는 관련 로직
if (member.isExpired()) {
  // 만료되었을 때 처리 로직...
}
//

서비스를 처리하는 로직이 위와 같이 바뀌었다 가독성도 좋아졌고, 만료 정책 관련 구현 로직이 Member객체 내부 메소드로 캡슐화되었기 때문에 이 member.isExpired() 메소드를 사용하는 객체들은 내부가 어떻게 구성되어있는지 모른다. 이런 구조에서 아까처럼 여성에 대한 정책을 추가해보자.

public class Member {
  private Date expiryDate;
  private boolean mail;
  
  private static final long DAY30 = 1000 * 60 * 60 * 24 * 30 // 30일 
  
  public boolean isExpired() {
    if (mele) { 
      return expiryDate != null &&
	     expiryDate.getDate() < System.currentTimeMillis();
    }
    
    return expiryDate != null &&
	   expiryDate.getDate() < System.currentTimeMillis() - DAY30;
  }
}

절차지향적인 구조의 코드보다 가독성 측면에서도 간결해졌다. 이제 이 검사하는 로직을 활용한 서비스를 처리하는 객체의 상태를 보자.

// 서비스 제한을 하는 관련 로직
if (member.isExpired()) {
  // 만료되었을 때 처리 로직...
}
//

변한게 없다. 단지 Member에 isExpired() 메소드의 구현부만 바꾸었을 뿐 이를 사용하는 서비스 관련 처리 로직들에서는 수정할 부분이 없어졌다. 이렇게 캡슐화를 통해 정보를 은닉하고, 객체간의 결합도(의존성 정도)가 낮아지고 응집도는 높아지게 되었고, 재사용성이 높아졌으며, 기능 수정사항에 대해 유연함을 가지게 되었다.





출처

  • 개발자가 반드시 정복해야할 객체지향과 디자인 패턴 - 최범균 지음
profile
만사가 귀찮은 ISFP가 쓰는 학습 블로그

1개의 댓글

comment-user-thumbnail
2022년 4월 14일

감사합니다

답글 달기