2021-02-19-TIL
2021-02-19-TIL
객체 지향 프로그래밍의 중요성
코드의 추가는 몇 줄 없는데, 추가하는 비용이 엄청나게 많이드는 상황이 발생한다.
1
2
3
4
long start = System.currentTimeMillis();
...
long end = System.currentTimeMillis();
long elapsed = end - start;
1
2
3
4
long start = System.nanoTime();
...
long end = System.nanoTime();
long elapsednano = end - start;
이러한 변경사항이 하나만 있으면 상관 없는데, 엄청나게 많은 경우가 대부분이다. 이러한 경우에 변경하는데 시간과 비용이 많이 발생하게 된다.
1
2
3
4
5
6
7
8
int mode = 10;
if (mode == 10) {
... a lot of statements
}
...
if (mode != 10) {
...
}
1
2
3
4
5
6
7
8
9
10
11
int mode = 10;
if (mode == 10) {
...
if (condition) {
mode = 20; // update mode value
}
}
...
if (mode != 10) { // cannot enter the condition block
...
}
내부의 조건문에서 값을 변경해버린다면, 다른 사람이 코드를 받았을 때 저 조건문을 찾기위해서 많은 시간을 투자해야한다.
1
- 코드 분석 시간 증가
- 코드 변경 시간 증가
소프트웨어의 가치 : 변화
Software maintanance is not “keep it working like before.” It is “keep being useful in a changing world” - Jessica Kerr -
코드 한줄 추가하는데 며칠씩이나 걸린다면, 그 소프트웨어는 변화에 잘 적응하지 못하는 소프트웨어이다. 따라서 빠르게, 쉽게, 낮은 비용으로 변화시킬 수 있는 구조로 설계하는 것이 중요하다.
객체
절차 지향과 비용
1
2
3
4
5
6
7
8
9
10
11
// Authentification API
Account account findOne(id);
if (account.getState == DELETED) {
}
// password change API
Account account = findOne(id);
if (account.getState() == DELETED) {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// Authentification API
Account account findOne(id);
if (account.getState == DELETED ||
account.getBlockCount() > 0) {
...
}
// password change API
Account account = findOne(id);
if (account.getState() == DELETED ||
account.getBlockCount() > 0) {
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Authentification API
Account account findOne(id);
if (account.getState == DELETED ||
account.getBlockCount() > 0 ||
account.getEmailVerifyStatus() == 0) {
...
}
// password change API
Account account = findOne(id);
if (account.getState() == DELETED ||
account.getBlockCount() > 0 ||
account.getEmailVerifyStatus() == 0) {
...
}
현재 계정의 활성상태 까지 검증하도록 추가되었다. 문제는 이 데이터를 사용하는 코드가 이곳에만 존재하는 것이 아니다. 따라서 수정을 할때는 이 코드를 사용하는 모든 곳에서 수정이 이루어져야 한다.
절차지향 vs 객체지향
객체는 각자 데이터와 프로시저를 가지고 있어서 이 데이터에 접근하려면 프로시저를 통해 접근하도록 제한할 수 있다.
- 객체의 핵심은 기능을 제공하는 것이다.
기능 명세
메서드(오퍼레이션)를 이용해서 기능을 명세한다. 메서드는 이름, 파라미터, 결과로 구성되는 블록이다.
객체와 객체
객체와 객체는 기능(메서드)을 사용해서 서로 연결된다. 기능의 사용이 곧 메서드 호출과 같은 의미이다.
메시지
객체와 객체간에 상호 작용을 하는 것은 메시지를 주고 받는다고 표현한다. 메서드를 호출하는 메시지, 리턴하는 메시지, 익셉션 메시지 등의 형태가 있다.
객체?
1
2
3
4
5
6
7
8
public class Member {
private String name;
private String id;
public void setName(Stirng name) {
this.name = name;
}
}
실질적으로 이 코드는 name, id필드에 접근하는 것 외에 부가적인 기능이 없다. 즉, 그냥 데이터 클래스의 형태를 가진다. 이는 C언어에서의 구조체와 기능은 동일하다고 볼 수 있다.
캡슐화(Encapsulation)
캡슐화는 데이터와 관련 기능을 묶는 것이다. 이는 보통 정보 은닉의 의미를 포함한다.
1
2
3
if(acc.getMembership() == REGULAR && acc.getExpDate().isAfter(now())) {
... 정회원 기능
}
1
2
3
4
5
if (acc.getMembership() == REGULAR &&
(
acc.getServiceDate().isAfter
)
)
데이터를 공유하는 코드의 수정이 발생하면 변경되는 코드의 발생이 연쇄적으로 일어난다.
캡슐화를 한다면
기능을 제공하고 구현 상세를 감춘다.
캡슐화를 하면 연쇄적인 변경 전파를 최소화할 수 있다.
캡슐화와 기능
캡슐화를 시도한다는 것은 코드의 의도를 파악하는 과정을 거쳐야 하므로, 기능에 대한 (의도)이해를 높인다.
캡슐화를 위한 규칙
Tell, Don’t Ask : 데이터를 달라고 하지 말고 해달라고 하기
즉, 데이터를 가져와서 처리를 하려고 하지말고 해당 메서드에게 해달라고 한다. 그렇게 하면 필요한 데이터를 가져와서 처리하는 로직은 그 메서드가 담당하도록 떠넘겨주면 된다.
Demeter’s Law
- 메서드에서 생성한 객체의 메서드만 호출
- 파라미터로 받은 객체의 메서드만 호출
- 필드로 참조하는 객체의 메서드만 호출
캡슐화 : 기능의 구현을 외부에 감춤
캡슐화를 통해 기능을 사용하는 코드에 영향을 주지 않고 (또는 최소화) 내부 구현을 변경할 수 있는 유연함
캡슐화 연습
Case1
1
2
3
4
5
6
7
8
9
10
11
public AuthResult authenticate(String id, String pw) {
Member mem = findOne(id);
if (mem == null) return AuthResult.NO_MATCH;
if (mem.getVerificationEmailStatus() != 2) {
return AuthResult.NO_EMAIL_VERIFIED;
}
if (passwordEncoder.isPasswordValid(mem.getPassword(), pw, mem.getId())) {
return AuthResult.SUCCESS;
}
return AuthResult.NO_MATCH;
}
Tell, Don’t Ask 데이터를 가져와서 직접 처리하지말고, 판단하는 로직 자체를 해당 클래스에 미리 정의해놓고 그 메서드를 호출하도록 해라.
1
2
3
4
5
6
7
8
9
10
11
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 (mem.verifyPassword(mem.password, pw)) {
return AuthResult.SUCCESS;
}
return AuthResult.NO_MATCH;
}
이렇게 하면 이메일 인증하는 authenticate의 코드는 변경하지 않으면서도 isEmailVerified()의 로직은 얼마든지 변경할 수 있다.
Case2
1
2
3
4
5
6
7
8
9
10
11
12
public class Rental {
private Movie movie;
private int daysRented;
public int getFrequentRenterPoints() {
if (movie.getPriceCode() == Movie.NEW_RELEASE &&
daysRended > 1)
return 2;
else
return 1;
}
}
1
2
3
4
5
6
7
8
9
10
public class Movie {
public static int REGULAR = 0;
public static int NEW_RELEASE = 1;
private int priceCode;
public int getPriceCode() {
return priceCode;
}
...
}
영화 대여 포인트를 계산하는 코드이다.
1
2
3
4
5
6
7
8
9
10
11
public class Rental {
private Movie movie;
private int daysRented;
public int getFrequentRenterPoints() {
if (movie.isNewRelease && daysRended > 1)
return 2;
else
return 1;
}
}
1
2
3
4
5
6
7
8
public class Rental {
private Movie movie;
private int daysRented;
public int getFrequentRenterPoints() {
return movie.getFrequentRenterPoints(daysRented);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Movie {
public static int REGULAR = 0;
public static int NEW_RELEASE = 1;
private int priceCode;
public int getFrequentRenterPoints() {
if (priceCode == NEW_RELEASE && daysRended > 1)
return 2;
else
return 1;
}
...
}
Case3
1
2
3
4
5
Timer t = new Timer();
t.startTime = System.currentTimeMillis();
...
t.stopTime = System.currentTimeMillis();
long elapsedTime = t.stopTime - t.startTime;
1
2
3
4
public class Timer {
public long startTime;
public long stopTime;
}
시작 시간을 구하는 메서드, 끝난 시간을 구하는 메서드, 걸린 시간을 구하는 메서드를 Timer클래스가 가지고 있다고 한다면 다음과 같이 작성해볼 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Timer {
private long startTime;
private 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;
...
}
}
}
Case4
1
2
3
4
5
6
7
8
9
10
public void verifyEmail(String token) {
Member mem = findByToken(token);
if (mem == null) throw new BadTokenException();
if (mem.getVerificationEmailStatus() == 2) {
throw new AlreadyVerifiedException();
} else {
mem.setVerificationEmailStatus(2);
}
// ... reflect modification on DB
}
첫번째, 데이터를 직접 가져와서 판단하고 있다. set을 데이터를 직접 바꾸고 있다.
mem.getVerificationEmailStatus() == 2
이 부분을 isEmailVerified()
로 바꾼다고 해도 좀 부족한 느낌이 있다. 이런경우 if~else문을 통째로 캡슐화 해보면 좋은 결과를 얻을 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Member {
private int verificationEmailStatus;
public void verifyEmail() {
if (isEmailVerified())
throw new AlreadyVerifiedException();
else
this.verificationEmailStatus = 2;
}
public voolean isEmailVerified() {
return verificationEmailStatus == 2;
}
}
다형성과 추상화
여러(poly) 모습(morph)을 갖는것을 다형성 이라고 한다.
1
2
3
4
5
6
7
8
public class Timer {
public void start() {..}
public void stop() {..}
}
public interface Rechargeable {
void charge();
}
1
2
3
4
5
public class IotTimer extends Timer implements Rechargeable {
public void charge() {
...
}
}
1
2
3
4
5
6
7
8
9
10
IotTimer it = new IotTimer();
it.start();
it.stop();
Timer t = it;
t.start();
t.stop();
Rechargeable r = it;
r.charge();
추상화(Abstraction)
- 특정한 성질을 뽑아내서 할 수 있다.
- 공통 성질을 뽑아내서 할 수 있다. (일반화) -> 다형성과 관련
Code Review
assertThat 앞에는 확인할 값 뒤에는 기댓값 -> 이 부분에 main의 메서드를 사용해도 되는가? 그에대한 테스트 부터 해야하는것이 아닌가?
1
2
3
4
5
public void addWhite(Piece piece) {
if (piece.isWhite()) { // TODO: handle exception of getting pawn
whitePieces.add(piece);
}
}
사용하지 않는 exception 제거
테스트하지 않을 메서드를 선언하는 것이 바람직한가? 단순 출력만 하는 부분을 메서드로 빼는것이 바람직한가?
PieceList를 하나로 관리하고 위치에 대한 좌표값을 멤버로 갖는게 나을것같다.
프레디 PR 참고하기