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
의 데이터를 직접 사용하고 있다. - 굉장히 절차 지향적인 코드인 것이다.
- 그럼 이런 절차지향적인 코드를 객체지향으로 바꾸려면 캡슐화를 해야하는데, 어떻게 해야될까?
t.startTime = System.currentTimeMillis();
: 타이머를 시작하는 코드라고 볼 수 있다.t.stopTime = System.currentTimeMillis();
: 타이머를 멈추는 코드라고 볼 수 있다.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)
여기서 데이터를 직접 바꿔주고 있다.- 위 코드는 어떻게 해야 캡슐화가 잘 될까?
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;
}
}
- 아까의 코드를 위와 같이 통으로 수정한다.
- 위와 같이 특정 기능을 캡슐화 할 수 있다.
- 데이터를 받아와서 판단한 다음에 다시 그 데이터를 수정하는 코드는 위와 같이 통으로 캡슐화를 진행하는 것이 좋다.