Post

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 참고하기

This post is licensed under CC BY 4.0 by the author.