2021-02-19-TIL

객체 지향 프로그래밍의 중요성

코드의 추가는 몇 줄 없는데, 추가하는 비용이 엄청나게 많이드는 상황이 발생한다.

long start = System.currentTimeMillis();
...
long end = System.currentTimeMillis();
long elapsed = end - start;
long start = System.nanoTime();
...
long end = System.nanoTime();
long elapsednano = end - start;

이러한 변경사항이 하나만 있으면 상관 없는데, 엄청나게 많은 경우가 대부분이다. 이러한 경우에 변경하는데 시간과 비용이 많이 발생하게 된다.

int mode = 10;
if (mode == 10) {
 ... a lot of statements
}
...
if (mode != 10) {
 ...
}
int mode = 10;
if (mode == 10) {
 ... 
	if (condition) {
		mode = 20; // update mode value
	}
}
...
if (mode != 10) { // cannot enter the condition block
 ...
}

내부의 조건문에서 값을 변경해버린다면, 다른 사람이 코드를 받았을 때 저 조건문을 찾기위해서 많은 시간을 투자해야한다.


  • 코드 분석 시간 증가
  • 코드 변경 시간 증가

소프트웨어의 가치 : 변화

Software maintanance is not "keep it working like before." It is "keep being useful in a changing world" - Jessica Kerr -

코드 한줄 추가하는데 며칠씩이나 걸린다면, 그 소프트웨어는 변화에 잘 적응하지 못하는 소프트웨어이다. 따라서 빠르게, 쉽게, 낮은 비용으로 변화시킬 수 있는 구조로 설계하는 것이 중요하다.

객체

절차 지향과 비용

// Authentification API
Account account findOne(id);
if (account.getState == DELETED) {

}

// password change API
Account account = findOne(id);
if (account.getState() == DELETED) {

}
// 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) {
		...
}
// 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 객체지향

객체는 각자 데이터와 프로시저를 가지고 있어서 이 데이터에 접근하려면 프로시저를 통해 접근하도록 제한할 수 있다.

  • 객체의 핵심은 기능을 제공하는 것이다.

기능 명세

메서드(오퍼레이션)를 이용해서 기능을 명세한다. 메서드는 이름, 파라미터, 결과로 구성되는 블록이다.

객체와 객체

객체와 객체는 기능(메서드)을 사용해서 서로 연결된다. 기능의 사용이 곧 메서드 호출과 같은 의미이다.

메시지

객체와 객체간에 상호 작용을 하는 것은 메시지를 주고 받는다고 표현한다. 메서드를 호출하는 메시지, 리턴하는 메시지, 익셉션 메시지 등의 형태가 있다.

객체?

public class Member {
	private String name;
	private String id;
	
	public void setName(Stirng name) {
	this.name = name;
	}
}

실질적으로 이 코드는 name, id필드에 접근하는 것 외에 부가적인 기능이 없다. 즉, 그냥 데이터 클래스의 형태를 가진다. 이는 C언어에서의 구조체와 기능은 동일하다고 볼 수 있다.

캡슐화(Encapsulation)

캡슐화는 데이터와 관련 기능을 묶는 것이다. 이는 보통 정보 은닉의 의미를 포함한다.

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

데이터를 공유하는 코드의 수정이 발생하면 변경되는 코드의 발생이 연쇄적으로 일어난다.

캡슐화를 한다면

기능을 제공하고 구현 상세를 감춘다.

캡슐화를 하면 연쇄적인 변경 전파를 최소화할 수 있다.

캡슐화와 기능

캡슐화를 시도한다는 것은 코드의 의도를 파악하는 과정을 거쳐야 하므로, 기능에 대한 (의도)이해를 높인다.

캡슐화를 위한 규칙

Tell, Don't Ask : 데이터를 달라고 하지 말고 해달라고 하기

즉, 데이터를 가져와서 처리를 하려고 하지말고 해당 메서드에게 해달라고 한다. 그렇게 하면 필요한 데이터를 가져와서 처리하는 로직은 그 메서드가 담당하도록 떠넘겨주면 된다.

Demeter's Law

  • 메서드에서 생성한 객체의 메서드만 호출
  • 파라미터로 받은 객체의 메서드만 호출
  • 필드로 참조하는 객체의 메서드만 호출

캡슐화 : 기능의 구현을 외부에 감춤

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

캡슐화 연습

Case1

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 데이터를 가져와서 직접 처리하지말고, 판단하는 로직 자체를 해당 클래스에 미리 정의해놓고 그 메서드를 호출하도록 해라.

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

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;
		}
}
public class Movie {
		public static int REGULAR = 0;
		public static int NEW_RELEASE = 1;
		private int priceCode;
		
		public int getPriceCode() {
				return priceCode;
		}
		...
}

영화 대여 포인트를 계산하는 코드이다.

public class Rental {
		private Movie movie;
		private int daysRented;
		
		public int getFrequentRenterPoints() {
				if (movie.isNewRelease && daysRended > 1)
						return 2;
				else
						return 1;
		}
}
public class Rental {
		private Movie movie;
		private int daysRented;
		
		public int getFrequentRenterPoints() {
				return movie.getFrequentRenterPoints(daysRented);
		}
}
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

Timer t = new Timer();
t.startTime = System.currentTimeMillis();
...
t.stopTime = System.currentTimeMillis();
long elapsedTime = t.stopTime - t.startTime;
public class Timer {
		public long startTime;
		public long stopTime;
}

시작 시간을 구하는 메서드, 끝난 시간을 구하는 메서드, 걸린 시간을 구하는 메서드를 Timer클래스가 가지고 있다고 한다면 다음과 같이 작성해볼 수 있다.

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

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문을 통째로 캡슐화 해보면 좋은 결과를 얻을 수 있다.

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)을 갖는것을 다형성 이라고 한다.

public class Timer {
		public void start() {..}
		public void stop() {..}
}

public interface Rechargeable {
		void charge();
}
public class IotTimer	extends Timer	implements Rechargeable {
    public void charge() {
    	...
    }
}
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의 메서드를 사용해도 되는가? 그에대한 테스트 부터 해야하는것이 아닌가?

public void addWhite(Piece piece) {
    if (piece.isWhite()) { // TODO: handle exception of getting pawn
        whitePieces.add(piece);
    }
}

사용하지 않는 exception 제거

테스트하지 않을 메서드를 선언하는 것이 바람직한가? 단순 출력만 하는 부분을 메서드로 빼는것이 바람직한가?

PieceList를 하나로 관리하고 위치에 대한 좌표값을 멤버로 갖는게 나을것같다.

프레디 PR 참고하기