OOP - 캡슐화

이성준·2022년 8월 10일
0
post-thumbnail

캡슐화란?

  • 데이터 + 관련 기능 묶기
  • 객체가 기능을 어떻게 구현했는지 외부에 감추는 것
    • 구현에 사용된 데이터의 상세 내용을 외부에 감춤
  • 외부에 영향 없이 객체 내부 구현 변경 가능
  • 기능을 사용하는 코드에 영향을 주지않고 내부 구현을 변경할 수 있는 유연함을 가질수 있다.

캡슐화를 왜 해야하는가

캡슐화 하지 않으면
ex) 정회원을 판별하는 로직

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

근데 이벤트로 5년이상 사용자한테 한달동안 정회원 혜택을 줘야함

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

그럼 이와같이 변경할수있는데, 저렇게 되어있는곳을 다 바꿔야된다.


그렇기 떄문에 우리는 캡슐화를 시도한다. 캡슐화를 하면
캡슐화를 하면

if(acc.hasRegularPermission()){
	// 정회원 기능
}
public class Account {
	private Membership membership;
	private Date expDate;

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

정회원 판별하는 로직을 hasRegularPermission안으로 감췄기때문에 if구문에 메소드를 호출하는코드는 저 안에 뭐가 들어있는지 알수가없고 알 필요도 없다. 또한 로직 변경이 일어나도 hasRegualrPermission만 고치면된다

또한 hasRegularPermission 이라는 이름으로 아 이 메소드는 정회원판별을 해주는 메소드구나 의도를 짐작할수 있다.
-> 기능에 대한 의도 이해를 높임

캡슐화를 위한 규칙

  1. TELL, DON'T ASK
    판단을 해달라고 메시지를 보낸다

  2. Demeter's Law
    메서드에서 생성한 객체의 메서드만 호출
    파라미터로 받은 객체의 메서드만 호출
    필드로 참조하는 객체의 메서드만 호출
    -> 메서드 하나만 호출

캡슐화 연습

  1. 예시의 코드는 id와 password를 파라미터로 받아서 안에 로직을 거친뒤 알맞은 리턴값을 보내는 메소드다
  public AuthResult authenticate(String id, String pw){
        Member member = findOne(id);
        if(member==null) return AuthResult.No_MATCH;

        //1. 캡슐화 전 -> 데이터를 가져와서 자기가 판단 
        if(member.getVerificationEmailStatus() !=2){ 
            return AuthResult.No_EMAIL_VERIFIED;
        }
        //2. 캡슐화 후 -> 판단을 isEmailVerfied()메소드에 맡김
        if(member.isEmailVerified()){
            return AuthResult.No_EMAIL_VERIFIED;
        }

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

        return AuthResult.No_MATCH;

    }

규칙1번 관점에서 보면 주석1번은 member에서 데이터를 가져와가지고 자기가 판단을 하는 코드다, 하지만 규칙1번은 이런 판단 자체를 해달라고 메시지를 보내자는 거기 때문에 이부분을 멤버가 제공하는 기능으로 바꿔서 2번 주석처럼 만들수 있고 의도도 더 확실하게 할 수 있다.

  1. 영화가 신작이고 하루이상 대여날짜에 따라 포인트를 주는 코드
    public class Rental {
        private Movie movie;
        private int daysRented;

        public int getFrequentRenterPoints(){
        //1. 데이터 가져와서 자기가 판단
            if(movie.getPriceCode() == Movie.NEW_RELEASE &&
                    daysRented > 1)
                return 2;
            else
                return 1;
        }
   }
   	   //2. 데이터 판단 메시지
       public int getFrequentRenterPoints(){
            if(movie.isNewRelease() &&
                    daysRented > 1)
                return 2;
            else
                return 1;
        }
        //4. 전체 계산 기능 불러오기
         public int getFrequentRenterPoints(){
          movie.getFrequentRenterPoints(daysRented);
        }
   
    public class Movie {
        public static int REGULAR = 0;
        public static int NEW_RELEASE = 1;
        private int priceCode;

        public int getPriceCode(){
            return priceCode;
        }
        //3. 전체계산기능 추가 
        public int getFrequentRenterPoints(int daysRented){
            if(priceCode == NEW_RELEASE &&
                    daysRented > 1)
                return 2;
            else
                return 1;

        }

규칙 1번 관점에 따라서 주석1번을 주석2번으로 바꿨지만 그런데 1번주석이랑 2번주석이랑 별 차이가 없다 그래서 좀더 적극적으로 RenterPoint구하는 전체 계산을 캡슐화 해보자 새로운 주석 3번 메소드는 계산하는데 필요한 값을 파라미터로 넘겨줬기때문에 추후에 daysRented의 값이 바뀌어도 movie쪽 코드만 수정하면 된다.

  1. 시간계산 메소드
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 t = new Timer();
t.start()
//...
t.stop();

long time = t.elapsedTime(MILLISECOND);

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

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

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

Millis 지만 추후 정밀한 시간계산이 필요해서 Nano로 바뀌게 된다면? start와 stop 부분을 nano초로 바꾸고 elapsedTime의 case 구문을 추가해주면 된다

이 예시도 역시 캡슐화 성공후에는 기능을 사용하는 코드는 바뀌지 않는다.

  1. 이메일 인증하는 코드
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);
	}
	// 수정 사항 DB 반영
}

이 코드는 조건을 판단한다음 판단한 결과로 다시 그 데이터를 바꾼다 이런 패턴의 코드는 통으로 캡슐화하면 좋다

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 {
			verificationEmailStatus = 2;
		}
	}

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

참조
https://www.inflearn.com/course/%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EC%9E%85%EB%AC%B8

0개의 댓글