1 객체

source: categories/study/oop_programming/opp_programming_1.md

1. 객체

1.1 객체

  • 절차 지향 vs 객체 지향 : 절차 지향
  • 절차 지향 : 데이터를 여러 프로시저가 공유하는 방식 (데이터 1을 프로시저 A, C가 사용, 데이터 4를 프로시터 B, D가 사용..)
    • 처음 프로그래밍을 배우면 이런 방식으로 프로그래밍을 하게됨
    • 절차지향이 처음에 쉽기 때문
    • 변수 선언하고 필드 선언하고, 변수, 필드 조작하고.. 쉽다. 그렇지만 문제가 있다.
    • 시간이 흘러갈수록 이런 데이터를 공유하는 방식은 구조를 점점 복잡하게 만들고, 그래서 수정을 어렵게 만드는 요인이 된다.

절차 지향과 비용

  • 수정사항 들어올 때마다 늘어나는 if 문..
  • 점점 많이 공유되는 변수, 함수들… 추가되는 데이터함수들..
  • 점점 어려워지는 코드 분석..
  • 시간이 갈수록 복잡해지고 수정이 어려워지는 코드

  • 수정하려면 어디서 어떻게 사용하고 있는지 파악하는 시간이 길어짐
  • 절차 지향을 하다보면 처음엔 빠른듯하나, 나중 갈수록 점점 데이터를 복사하게되고, 코드 자체를 자꾸 복사하게됨. 그 과정에서 코드가 점점 복잡해지고 수정이 어려워짐.

객체 지향

  • 객체 지향: 데이터, 프로시저를 객체 단위로 묶는다.
    • 그리고 해당 객체가 가지고 있는 데이터는 해당 객체의 프로시저만 접근 가능하게 한다.
    • 그래서 다른 객체에서는 그 데이터에 접근하지 못한다.

  • 그럼 각 객체간 서로 어떻게 데이터를 주고받냐?
    • 사실 서로 데이터를 주고받진 않는다.
    • 객체는 프로시저를 이용해서 외부에 기능을 제공한다.
    • 객체와 객체는 다른 객체에 있는 프로시저를 호출하는 방식으로 서로 연결이 된다.
    • 그런데 이 방식은 처음에는 조금 어려울 수도 있다. 데이터 / 프로시저를 알맞게 묶어야되기 때문에 그 묶는 과정 자체가 어려울 수 있다.
    • 그런데 시간이 흘러갈수록 이 객체 지향의 장점이 드러난다. 코드 수정이 수월해진다. (캡슐화)

객체란

  • 객체의 핵심 -> 기능 제공
    • 객체는 제공하는 기능으로 정의
      • 내부적으로 가진 필드(데이터)로 정의하지 않음

  • 예: 회원 객체
    • 암호 변경하기 기능
    • 차단 여부 확인하기 기능

  • 예: 소리 제어기
    • 소리 크기 증가하기 기능
    • 소리 크기 감소하기 기능

기능 명세

  • 메서드(오퍼레이션)를 이용해서 기능 명세
    • 이름, 파라미터, 결과로 구성

객체와 객체

  • 객체와 객체는 기능을 사용해서 연결
    • 기능 사용 = 메서드 호출

용어: 메시지

  • 객체와 객체 상호 작용 : 메시지를 주고 받는다고 표현
    • 메서드를 호출하는 메시지, 리턴하는 메시지, 익셉션 메시지

객체?



public class Member {
  private String name;
  private String id;

  public void setName(String name) {
    this.name = name;
  }
  public String getName() {
    return name;
  }

  public void setId(String id) {
    this.id;
  }
  public String getId() {
    return id;
  }
}


  • 필드(데이터), set, get 있다고 객체일까?
    • id와 name을 세팅하고 반환하는 기능을 가졌다고 볼 수 있지만, 그냥 해당 데이터에 접근하는 형태라 기능이라고 보기엔 애매하다.
    • 단순히 데이터에 접근하고 그 외의 부가적인 기능이 없다.
    • 이런 클래스는 객체라기보단 데이터에 가깝다. (데이터 클래스, 구조체에 더 가까운..)
    • 여기에 기능이 붙는다면 그 다음부턴 객체라고 볼 수 있을 것이다.
Tip

객체는 기능으로 정의한다는 것!!!! 꼭 기억!!

1.2 캡슐화

  • 캡슐화만 잘해도 좋은 코드를 작성할 가능성이 굉장히 높아진다.

캡슐화 (Encapsulation)

  • 데이터 + 관련 기능 묶기
  • 객체가 기능을 어떻게 구현했는지 외부에 감추는 것
    • 구현에 사용된 데이터의 상세 내용을 외부에 감춤
  • 정보 은닉(Information Hiding) 의미 포함
  • 캡슐화를 왜 해?
    • 외부에 영향 없이 객체 내부 구현 변경 가능

캡슐화 하지 않으면



if (acc.getMembership() === REGULAR && acc.getExpDate().isAfter(now())) {
  // 정회원 기능
}


  • 5년 이상 사용자
  • 일부 기능 정회원 혜택 1개월 무상 제공
  • 이렇게 수정이 되어야 할 때!


if (acc.getMembership() === REGULAR &&
  (
    (acc.getServiceDate().isAfter(fiveYearAgo) && acc.getExpDate().isAfter(now())) ||
    (acc.getServiceDate().isBefore(fiveYearAgo) && addMonth(acc.getExpDate()).isAfter(now()))
  )
) {
  // 정회원 기능
}


  • 위와 같이 수정하게 된다면 정말 마음 아플듯!
  • 이런 코드는 이렇게 수정해야될 부분이 한군데면 좋은데, 보통 이런 코드는 서비스가 5년 이상 살아남았다는 것은 이런 코드가 여러 군데 있을 가능성이 높다는 뜻
  • 그래서 이곳저곳 코드를 뒤져서 막 바꿔주게 될거임
  • 이게 의미하는 것은?

캡슐화하지 않으면

  • 요구사항의 변화가 데이터 구조/사용에 변화를 발생시킴
  • 해당 데이터를 사용하는 많은 코드에 수정을 발생시킴
  • 즉, 요구 사항이 바뀌면서 그 데이터를 사용하는 여러 코드에 수정이 발생하게 됨 -> 이것이 절차지향, 데이터를 공유하는 방식의 단점이다.

요구사항 변경 예

  • 장기 사용자에게 특정 기능 실행 권한을 연장(단 유효 일자는 그대로 유지)
  • 계정을 차단하면 모든 실행 권한 없음
  • Date를 LocalDateTime으로 변경

캡슐화하면

  • 기능을 제공하고 구현 상세를 감춤


if (acc.hasRegularPermission()) {
  // 정회원 기능
}




public class Account {
  private Membership membership;
  private Date expDate;

  public boolean hasRegularPermission() {
    return membership === REGULAR && expDate.isAfter(now())
  }
}


  • 위와 같은 상태에서 동일하게 요구사항이 바뀜
  • 그럼 아래와 같이 클래스 기능만 수정하면 됨
  • if 문으로 확인하는 hasRegularPermission 이건 변하지 않음
  • 클래스 내부 기능만 수정된다.


if (acc.hasRegularPermission()) {
  // 정회원 기능
}




public class Account {
  private Membership membership;
  private Date expDate;

  public boolean hasRegularPermission() {
    return membership === REGULAR && 
      ( expDate.isAfter(now()) ||
        (
          serviceDate.isBefore(fiveYearAgo()) &&
          addMonth(expDate).isAfter(now())
        )
      )
  }
}


캡슐화를 잘하면? 연쇄적인 변경 전파를 최소화

  • 요구사항의 변화가 내부 구현을 변경
  • 캡슐화된 기능을 사용하는 코드 영향 최소화

캡슐화 기능

  • 캡슐화 시도 -> 기능에 대한 (의도) 이해를 높일 수 있다.


if (acc.getMembership() === REGULAR) {
  // 정회원 기능
}


  • 멤버십이 REGULAR와 같은지 검사하는 이유는 실제로 무엇 때문인가?

  • 검사하는 이유는 계정이 REGULAR 권한을 가졌는지 확인하기 위함



if (acc.hasRegularPermission()) {
  // 정회원 기능
}




public class Account {
  private Membership membership;
  private Date expDate;

  public boolean hasRegularPermission() {
    return membership === REGULAR && 
      ( expDate.isAfter(now()) ||
        (
          serviceDate.isBefore(fiveYearAgo()) &&
          addMonth(expDate).isAfter(now())
        )
      )
  }
}


캡슐화를 위한 규칙

  • 캡슐화라는게 절로 잘 되지 않는다.
  • 하고싶다고해서 자동으로 캡슐화되는건 아니다.
  • 그래서 보통 캡슐화를 위해 두가지 규칙을 제시한다.

  • 첫번째 규칙, Tell, Don't Ask
    • 데이터 달라하지 말고 해달라고 하기


if (acc.getMembership() === REGULAR) {
  // 정회원 기능
}


  • 위와 같은 구조에서


if (acc.hasRegularPermission()) {
  // 정회원 기능
}


  • 이런식으로..

  • 두번째 규칙, Demeter's Law (데미테르의 법칙)
    • 메서드에서 생성한 객체의 메서드만 호출
    • 파라미터로 받은 객체의 메서드만 호출
    • 필드로 참조하는 객체의 메서드만 호출


acc.getExpDate().isAfter(now)




Date date = acc.getExpDate();
date.isAfter(now);


  • 위 두개는 사실 같은 코드, 위와 같이 하지 말라는 거임
  • 위와 같은 구조에서..


acc.isExpired()




acc.isValid(now)


  • 위와 같은 코드로..
  • 연속해서 부르던걸 특정 메소드를 호출하는 방식으로.. 메소드 하나로 호출하는 방식으로 수정하라는 뜻
  • 이것이 데미테르의 법칙
  • 위 데미테르 법칙을 하다보면 캡슐화가 될 가능성이 높아진다.
    • 코드 이해도가 높아짐
    • 그럼 특정 기능으로 묶는 능력이 상승함

정리

  • 캡슐화 : 기능의 구현을 외부에 감춤
  • 캡슐화를 통해 기능을 사용하는 코드에 영향을 주지 않고 (또는 최소화) 내부 구현을 변경할 수 있는 유연함

1.3 캡슐화 예제

캡슐화 연습 1 - 데이터를 받아와서 특정 판단을 하는 코드



// 인증과 관련된 코드
// id와 pw를 주면, id와 pw를 일치하는지 안하는지 확인한 다음에 일치하면 일치했다고 응답하고
// 일치하지 않았으면 일치하지 않은 이유를 응답하는 그런 코드이다.
public AuthResult authenticate(String id, String pw) {
  Member mem = findOne(id); // id에 해당하는 멤버가 있는지 확인
  if (mem == null) return AuthResult.NO_MATCH; // 멤버가 존재하지 않으면 NO_MATCH를 반환

  if (mem.getVerificationEmailStatus() != 2) { // 멤버의 getVerificationEmailStatus가 2가 아니면 NO_EMAIL_VERIFIED를 반환
    return AuthResult.NO_EMAIL_VERIFIED;
  }

  // isPasswordValid 암호가 일치하는지 확인
  // 일치한다면 SUCCESS 반환
  if (passwordEncoder.isPasswordValid(mem.getPassword(), pw, mem.getID())) {
    return AuthResult.SUCCESS;
  }

  // 유효하지 않으면 NO_MATCH 반환
  return AuthResult.NO_MATCH;
}


  • 위 코드에서 어떤 부분을 캡슐화 할 수 있을까?
  • 여기서 고려해야될 점은 Tell, Don't Ask
    • 데이터를 가져와서 판단하지 말고, 판단 해달라고 하라는 것!
    • 이 관점에서 위 코드를 살펴보자.
    • mem.getVerificationEmailStatus() != 2 이 부분이 캡슐화를 할 수 있는 대상이 될 것 같다.
    • 위 코드를 보면 데이터를 가지고와서 판단을 하고 있단말야? 이런 판단 자체를 기능으로 바꿔보자.

캡슐화 연습 1 답안



public class Member {
  private int verificationEmailStatus;

  public boolean isEmailVerified() {
    return verificationEmailStatus == 2;
  }
}




public AuthResult authenticate(String id, String pw) {
  Member mem = findOne(id);
  if (mem == null) return AuthResult.NO_MATCH;

  if (!mem.isEmailVerified()) {
    return AuthResult.NO_EMAIL_VERIFIED;
  }

  if (passwordEncoder.isPasswordValid(mem.getPassword(), pw, mem.getID())) {
    return AuthResult.SUCCESS;
  }

  return AuthResult.NO_MATCH;
}


캡슐화 연습 2 - 데이터를 받아와서 해당 데이터로 특정 부분을 판단하는 코드

  • 출처: 리팩토링(마틴 파울러 저)


// 대여하는 클래스
// movie는 비디오를 의미 daysRented는 대여일수
// getFrequentRenterPoints -> 새로 릴리즈된 영화이면서 랜탈을 2일 이상 했다면 포인트로 2를 주고 아니면 1을 주는 개념
public class Rental {
  private Movie movie;
  private int daysRented;

  public int getFrequentRenterPoints() {
    if (movie.getPriceCode() == Movie.NEW_RELEASE && dayRented > 1) return 2;
    else return 1;
  }

  // ...
}




// 영화와 관련된 정보를 담고 있는 클래스이다.
// 새로운 영화인지 아닌지를 1, 0으로 표현을 하고, priceCode에 0이나 1이 들어가게된다.
public class Movie {
  public static int REGULAR = 0;
  public static int NEW_RELEASE = 1;
  private int priceCode;

  public int getPriceCode() {
    return priceCode;
  }

  // ...
}


  • 위에서 우리는 어느 부분을 캡슐화 할 수 있을까?
  • 캡슐화 1번 예제와 비슷하게 데이터를 달라고 하는 부분이 있다.
    • movie.getPriceCode() == Movie.NEW_RELEASE priceCode를 가져와서 그 값이 NEW_RELEASE인지 비교하고 있다.
    • 이 코드를 if (movie.isNewRelease && daysRented > 1) 이런식으로 바꿔볼 수 있다.
    • 그리고 Movie 클래스에 isNewRelease 메소드를 추가해줄 수 있을 것이다.
    • 그런데 위와 같이해도, 새롭게 수정한 코드나 기존 코드나 뭔가 큰 차이가 없다.
    • 조금 더 적극적으로 캡슐화를 해보자.


public class Rental {
  private Movie movie;
  private int daysRented;

  public int getFrequentRenterPoints() {
    if (movie.getPriceCode() == Movie.NEW_RELEASE && dayRented > 1) return 2;
    else return 1;
  }

  // ...
}


  • getFrequentRenterPoints 전체 코드를 Movie 클래스에 캡슐화 시켜보자.
  • 그럼 어떻게 캡슐화를 하는지 보도록 하자.

캡슐화 연습 2 답안



public class Rental {
  private Movie movie;
  private int daysRented;

  public int getFrequentRenterPoints() {
    return movie.getFrequentRenterPoints(daysRented);
  }

  // ...
}


  • 포인트를 계산하는 기능을 추가하고 그 계산하는데 필요한 값을 파라미터로 전달한다.
  • 아래 getFrequentRenterPoints 함수에서 파라미터로 daysRented를 받고있다.


public class Movie {
  public static int REGULAR = 0;
  public static int NEW_RELEASE = 1;
  private int priceCode;

  public int getFrequentRenterPoints(int daysRented) {
    if (priceCode == NEW_RELEASE && daysRented > 1) return 2;
    else return 1;
  }

  // ...
}


  • 그리고 Movie 클래스는 본인이 들고있는 데이터인 priceCode와 파라미터로 전달 받은 daysRented 이 두 값을 사용해서 포인트를 계산합니다.
  • 위와 같이 캡슐화를 진행했다.
  • 이럴 경우 포인트를 구하는 공식이 바뀌었을 때, 혹은 daysRented의 값이 1이 아니라 2나 3으로 바뀌었을 때, Movie쪽의 코드만 수저하면 된다.
Tip

위 캡슐화의 예는 데이터를 들고있는 쪽의 기능을 추가하면서 그 기능을 구현하는데 필요한 다른 값을 파라미터로 받는 예가 되겠다.

캡슐화 연습 3 - 특정 class의 데이터를 가져와서 판단하고 어떤 기능을 수행하는 코드



// 아래 코드는 Timer 클래스를 이용해서 startTime, stopTime을 저장을 하고 
// 두 시간의 차이를 구해서 실행 시간을 구하는 코드입니다.
Timer t = new Timer();
t.startTime = System.currentTimeMillis();

// ...

t.stopTime = System.currentTimeMillis();

long elapsedTime = t.stopTime - t.startTime;




// Timer 클래스는 startTime, stopTime이라는 데이터를 두 개 갖고 있다.
public class Timer {
  public long startTime;
  public long stopTime;
}


  • 위 코드는 어떻게 캡슐화를 시도할 수 있을까?
  • t.startTime = System.currentTimeMillis();
  • t.stopTime = System.currentTimeMillis();
  • 위에 보면 데이터를 바꾸는 코드가 있다.
  • long elaspedTime = t.stopTime - t.startTime;
  • 그리고 실제 데이터를 구해서 뭔가 새로운 계산을 하고 있다.
  • 즉, 위 3가지 코드 모두 Timer의 데이터를 직접 사용하고 있다.
  • 굉장히 절차 지향적인 코드인 것이다.
  • 그럼 이런 절차지향적인 코드를 객체지향으로 바꾸려면 캡슐화를 해야하는데, 어떻게 해야될까?

  1. t.startTime = System.currentTimeMillis(); : 타이머를 시작하는 코드라고 볼 수 있다.
  2. t.stopTime = System.currentTimeMillis(); : 타이머를 멈추는 코드라고 볼 수 있다.
  3. long elaspedTime = t.stopTime - t.startTime; : 중지 시간하고 시작시간 사이에 흘러간 시간을 구하는 코드로 볼 수 있다.
  • 위와 같은 관점에서 캡슐화를 해보도록 하겠다.

캡슐화 연습 3 답안



Timer t = new Timer();
t.start();

// ...

t.stop();

long time = t.elapsedTime(MILLISECOND);


  • Timer 기능을 사용 (시작, 중지, 흘러간 시간 구하기 / 밀리초 단위)


public class Timer {
  public long startTime;
  public long stopTime;

  public void start() {
    this.startTime = System.currentTimeMillis();
  }

  public void stop() {
    this.stopTime = System.currentTimeMillis();
  }

  public long elapsedTime(TimeUnit unit) {
    switch(unit) {
      case MILLISECOND:
        return stopTime - startTime;
      // ...
    }
  }
}


  • Timer 클래스를 보면 타이머를 시작하는 기능 start
  • 타이머를 중지하는 기능 stop
  • 중지 시간과 시작 시간 사이에 얼마나 시간이 흘러갔는지 구하는 기능 elapsedTime
  • 위 세가지 기능을 캡슐화하여 구현했다.

  • 위 기능을 밀리초 단위가 아닌 나노초 단위로 구하고 싶다면?
  • Timer 클래스 부분만 수정하면 된다.
  • elapsedTime에 나노초 단위로 구하는 식 추가하고 파라미터로 NANO를 넘기던지.. 그런식으로?
  • 강의에선 기존 MILLISECOND 식을 나노초 단위로 구하는 식으로 수정한다고 했는데.. 그래서 기능을 사용하는 코드 부분에서 수정을 안해도 된다고 했는데,
    • 그렇게하면 해당 단어의 의미와 기능이 좀 상이해지는거 아닌가?

캡슐화 연습 4 - 데이터를 받아와서 판단한 다음에 다시 그 데이터를 수정하는 코드



public void verifyEmail(String token) {
  Member mem = findByToken(token); // 특정 token과 관련된 멤버를 구한다.
  if (mem == null) throw new BadTokenException(); // 멤버가 없으면 잘못된 토큰이라고 에러를 던진다.

  if (mem.getVerificationEmailStatus() == 2) { // 해당 멤버의 이메일 Status가 2이면 
    throw new AlreadyVerifiedException(); // Exception을 내고
  } else {
    mem.setVerificationEmailStatus(2); // 2가 아니면 setVerificationEmailStatus를 2로 바꿔준다. 
  }

  // ... 수정사항 DB 반영
}


  • 위 코드는 의심가는 부분이 2군데가 있다.
  • mem.getVerificationEmailStatus() == 2 여기 데이터를 가져와서 판단하는 코드가 있다.
  • mem.setVerificationEmailStatus(2) 여기서 데이터를 직접 바꿔주고 있다.
  • 위 코드는 어떻게 해야 캡슐화가 잘 될까?

  1. mem.getVerificationEmailStatus() == 2 이 부분을 isEmailVerified() 이렇게 바꾼다해도 위 코드의 구조는 별로 변한게 없다.
    • 조금 나아지긴 하지만, 그렇다고 캡슐화가 잘 되었냐 했을 땐 좀 아쉽다.

  • 이렇게 데이터를 받아와서 판단한 다음에 다시 그 데이터를 바꾸는 식의 코드
  • 판단의 결과로 다시 해당 데이터를 수정하는 코드
  • 이런 패턴의 코드는 그 부분을 통채로 캡슐화를 시도해보면 좋은 결과를 얻을 가능성이 높아진다.

캡슐화 연습 4 답안



public void verifyEmail(String token) {
  Member mem = findByToken(token);
  if (mem == null)
    throw new BadTokenException();

  mem.verifyEmail();

  // ... 수정사항 DB 반영
}




public class Member {
  private int verificationEmailStatus;

  public void verifyEmail() {
    if (isEmailVerified()) 
      throw new AlreadyVerifiedException();
    else
      this.verificationEmailStatus = 2;
  }

  public boolean isEmailVerified() {
    return verificationEmailStatus = 2;
  }
}


  • 아까의 코드를 위와 같이 통으로 수정한다.
  • 위와 같이 특정 기능을 캡슐화 할 수 있다.
  • 데이터를 받아와서 판단한 다음에 다시 그 데이터를 수정하는 코드는 위와 같이 통으로 캡슐화를 진행하는 것이 좋다.