티스토리 뷰

● 객체란

- 객체의 핵심 -> 기능 제공
- 객체는 제공하는 기능으로 정의
- 내부적으로 가진 필드(데이터로) 정의하지 않음
ex) 회원 객체
- 암호 변경하기 기능
- 차단 여부 확인 하기 기능

ex) 소리 제어기
- 소리 크기 증가하기 기능
- 소리 크기 감소하기 기능
1. 기능 명세
- 메서드를 이용해서 기능 명세
- 이름, 파라미터, 결과로 구성

public class VolumeController {
    // 증가 기능
    public void increase(int inc) {
       ...
    }
    // 감소 기능
    public void decrease(int dec) {
       ...
    }
    // 볼륨 기능
    public int volume( ) {
       ...
    }
}

public Member {
    // 비번 변경 기능
    public void changePassword(String curPw, String newPw) {
      ...
    }
}


2. 객체와 객체
- 객체와 객체는 기능을 사용해서 연결
- 기능 사용 = 메서드 호출

VolumnController volCont = new VolumnController( );
volCont.increase(4);
volCont.decrease(3);
int currentVol = volCont.volumne( );


3. 메시지
- 객체와 객체 상호 작용 : 메시지를 주고받는다고 표현
- 메서드를 호출하는 메시지, 리턴하는 메시지, 익셉션 메시지

4. 객체가 아닌 것?

public class Member { 
	private String name;
	private String id;
	
	public void getName( ) {
	...
	}
	public void setName( ) {
	...
	}

특정한 기능이 없는 단순한 데이터를 접근하고
그 외 부가적인 기능이 없는 이런 클래스는 객체라기보다는 데이터 클래스에 더 가깝다.

● 캡슐화

- 데이터 + 관련 기능 묶기
- 객체가 기능을 어떻게 구현했는지 외부에 감추는 것
- 구현에 사용된 데이터의 상세 내용을 외부에 감춤
- 정보 은닉 의미
- 외부에 영향 없이 객체 내부 구현 변경 가능

1. 캡슐화가 없다면

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)))
)
) {
... 정회원 기능
}

만약 다른 객체에도 정회원 혜택의 조건을 넣는다면 코드가 길어지고 복잡하게 될 것이다.
요구사항의 변화가 데이터 구조/사용에 변화를 발생시킨다.

예를 들어
1. 장기 사용자에게 특정 기능 실행 권한을 연장
2. 계정을 차단하면 모든 실행 권한 없음
3. Date를 LocalDateTime으로 변경

2. 캡슐화하면

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.hasRegularPermission( )) {
	...정회원 기능
}

이처럼 실제 데이터가 이루어진 공간에서 메서드를 만들어서 필요한 곳에 호출하게 되면
코드 수가 줄어주는 이점이 있으며 정보 은닉을 할 수 있다.

- 캡슐화된 기능을 사용하는 코드 영향 최소
- 캡슐화를 시도하면 기능에 대한 (의도) 이해를 높임

3. 캡슐화를 위한 규칙
- Tell, Don't Ask
1. 데이터 달라하지 말로 해달라고 하기

// 데이터 바로 호출 X
if (acc.getMembership( ) == REGULAR) {
	...정회원 기능
}

// 기능이 들어간 메서드 호출
if (acc.hasRegularPermission( )) {
	...정회원 기능
}


- Demeter's Law

1. 메서드에서 생성한 객체의 메서드만 호출
2. 파라미터로 받은 객체의 메서드만 호출
3. 필드로 참조하는 객체의 메서드만 호출
정리
- 캡슐화 : 기능의 구현을 외부에 감춤
- 캡슐화를 통해 기능을 사용하는 코드에 최소화하고 내부 구현을 변경할 수 있는 유연함

4. 캡슐화 해보기

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;
}

// 캡슐화 Change

public AuthResult authenticate(String id, String pw) {
	Member mem = findOne(id);
	if (mem == null) return AuthResult.NO_MATCH;

	if (!mem.isEmailVerified()) {  // key point !!
		return AuthResult.NO_EMAIL_VERIFIED;
	}
	
	if (passwordEncoder.isPasswordValid(mem.getPassword( ), pw, mem.getId( ))) {
		return AuthResult.SUCCESS;
	}
	return AuthResult.NO_MATCH;
}

public class Member {
	private int verificationEmailStatus;
	
	public boolean isEmailVerified( ) {
		return verificationEmailStatus == 2;
	}
}

mem.getVerificationEmailStatus( ) 데이터를 가져와서 조건을 하는 부분을 캡슐화 해서
mem.isEmailVerified( ) 메서드를 통해 이메일을 확인하고 있다.


ex) 값을 파라미터로 통해 받아서 넘겨주는 캡슐화

public class Rental {
	private Movie movie;
	private int daysRented;
	
	public int getFrequentRenterPoints( ) {
		if(movie.getPriceCode( ) == movie.NEW_RELEASE && 
			daysRented > 1)
		   return 2;
		else
		   return 1;
	}
}

public class Movie {
	public static int NEW_RELEASE = 1;
	private int priceCode;

	public int getPriceCode( ) {
	    return priceCode;
	}
}

// 캡슐화 Change

public class Rental {
	private Movie movie;
	private int daysRented;
	
	public int getFrequentRenterPoints( ) {
		return movie.getFrequendRenterPoints(daysRented);
	}
}

public class Movie {
	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;
	}
}



ex) 여러개의 메서드

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;
		}
	}
}

Timer t = new Timer( );
t.start( );
...
t.stop( );
long time = t.elapsedTime(MILLISECOND);



ex) 통으로 캡슐화

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);
	}
}

// 캡슐화 Change

public void verifyEmail(String token) {
	Member mem = findByToken(token);
	if (mem =null) throw new BadTokenException( );
	
	mem.verfiyEmail( );
}

public class Member {
	private int verificationEmailStatus;

	public void verifyEmail( ) {
	   if (isEmailVerified( ))
		throw new AlreadyVerifiedException( );
	   else
		this.verifiedcationEmailStatus = 2;
	}

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

 

● 다형성

- 여러 모습을 갖는 것
- 객체 지향에서는 한 객체가 여러 타입을 갖는 것
1. 즉 한 객체가 여러 타입의 기능을 제공
2. 타입 상속으로 다형성 구현
3. 하위 타입은 상위 타입도 됨

public class Timer {
	public void start( ) {...}
	public void stop( ) {..}
	
	public interface Rechargeable {
		void charge( );
	}
}

public class IotTimer extneds Timer implements RecharGeable {
	public void charge( ) {
		...
	}
}

IotTimer 클래스가 Timer 상속하여 값을 사용하는 코드로,

IotTimer it = new IotTimer( );
it.start( );
it.stop( );

Timer t = it;
t.start( );
t.stop( );

Rechargeable r = it;
r.charge( );

IotTimer 클래스가 Timer 메서드를 호출할 수 있다.

● 추상화

- 데이터나 프로세스 등을 의미가 비슷한 개념이나 의미 있는 표현으로 정의하는 과정
- 두 가지 방식의 추상화
1. 특정한 성질
2. 공통 성질(일반화)
- 간단한 예
1. DB의 USER 테이블 : 아이디, 이름, 이메일
2. Money 클래스 : 통화, 금액
3. 프린터 : HP MXXX, 삼성 SL-M2XXX

추상화 == 다형상
- 공통 성질을 뽑아내는 것과 연관성이 있다.


1. 타입 추상화
- 여러 구현 클래스를 대표하는 상위 타입 도출
- 흔히 인터페이스 타입으로 추상화
- 추상화 타입과 구현은 타입 상속으로 연결


2. 추상 타입 사용
- 추상 타입은 구현을 감춤
>> 기능의 구현이 아닌 의도를 더 잘 드러냄

- 추상 타입을 이용한 프로그래밍
Notifier notifier = getNotifier(...);
notifier.notify(someNoti)


3. 추상 타입 사용에 따른 이점 : 유연함
- 콘크리트 클래스를 직접 사용하면

public void cancel(String ono) {
   ...주문 취소 처리
   
   smsSender.sendSms(...);
}

// 추상화 Change

private SmsSender smsSender;
private KakaoPush kakaoPush;
private MailService mailSvc;

public void cancel(String ono) {
   ...주문 취소 처리
   
   if (pushEnabled) {
	kakaoPush.push(..);
   } else {
	smsSender.sendSms(...);
   } 
   mailSvc.sendMail(...);
}

요구 사항 변경에 따라 주문 취소 코드도 함께 변경한다.


ex) 도출한 추상 타입 사용

public void cancel(String ono) {
    ...주문 취소 처리

    Notifier notifier = getNotifier(...);
    notifier.notify(...);
}

private Notifier getNotifier(...) {
    if (pushEnabled)
	return new KakaoNotifier( );
    else
	return new SmsNotifier( );
}



4. 추상화는 의존 대상이 변경하는 시점에 사용
- 추상화 -> 추상 타입 증가 -> 복잡하고 증가
- 아직 존재하지 않는 기능에 대한 이른 추상화는 주의 : 잘못된 추상화 가능성, 복잡도만 증가
- 실제 변경, 확장이 발생할 때 추상화 시도

// 1
public class OrderService {
	private MailSender sender;
	
	public void order(...) {
		....
		sender.send(message);
	}
}

// 2
public class OrderService {
	private MailSender sender;
	private SmsService smsService;
	
	public void order(...) {
		....
		sender.send(message);
		....
		smsServie.send(smsMsg);
	}
}

// 3 
// Notifier 추상화
public class OrderService {
	private Notifier notifier;

	public void order(...) {
		....
		notifier.notify(noti);
	}
}

5. 추상화를 잘하려면
- 구현을 한 이유가 무엇 때문인지 생각해야 함


6. 추상화를 하지 않으면 어떻게 될까?
- 개발 시간이 증가한다
- 코드 구조가 길어지고 복잡해짐
- 관련 코드가 여러 곳에 분산됨
- if else 문구만 계속 추가 ( 클라우드 S, N, D )

결과적으로 코드 증가와 분석 속도 저하
OCP (Open-Closed Principle)
- 개방 폐쇄 원칙
- 기능 변경하거나 확장할 수 있으면서 그 기능은 수정하지 않아야 한다
ex) 클라우드 시스템은 그대로이면서 새로운 클라우드는 확장할 수 있는 원칙

● 상속

1. 상속을 통한 기능 재사용시 발생할 수 있는 단점
- 상위 클래스 변경 어려움
- 클래스 증가
- 상속 오용

2. 기능 분해
- 기능은 하위 기능으로 분해

ex)
암호 변경 - 변경 대상 확인 - 변경 대상 구함
- 대상 없으면 오류 응답
- 대상 암호 변경 - 암호 일치 여부 확인 - 불일치하면 암호 불일치 응답
- 암호 데이터 변경
분리한 기능을 누가 제공할건지 결정하는 것!!
>> 이것이 객체지향설계 기본과정
기능을 분리하고 각 객체에게 분리한 기능을 제공할 책임을 준다.
각 객체에 알맞은 기능을 준다.

public class ChangePasswordService {
	public Result changePassword(String id, String oldPw, String newPw) {
		Member mem = memberRepository.findOne(id);
		if (mem == null) {
			return Result.NO_MEMBER;
		}
		try {
			mem.changePassword(oldPw, newPw);
			return Result.SUCCESS;
		}    catch(BadPasswordException ex) {
			return Result.BAD_PASSWORD;
		}
	}
}


3. 큰 클래스, 큰 메서드 문제점
- 클래스나 메서드가 커지면 절차 지향의 문제 발생
큰 클래스 -> 많은 필드를 많은 메서드가 공유
큰 메서드 -> 많은 변수를 많은 코드가 공유

책임에 따라 알맞게 코드 분리 필요

어떻게?
1. 패턴 적용
2. 계산 기능 분리
3. 외부 연동 분리
4. 조건별 분기는 추상화
패턴 적용
- 전형적인 역할 분리
1. 간단한 웹
>> 컨트롤러, 서비스, DAO
2. 복잡한 도메인
>> 엔티티, 밸류, 리포지토리, 도메인 서비스
3. AOP
>> Aspect(공통 기능)
4. GoF
>> 팩토리, 빌더 전략, 템플릿 메서드, 프록시/데코레이터 등

● 의존

- 기능 구현을 위해 다른 구성 요소를 사용하는 것
ex) 객체 생성, 메서드 호출, 데이터 사용
- 의존은 변경이 전파될 가능성을 의미
- 의존하는 대상이 바뀌면 바뀔 가능성이 높아짐
ex) 호출하는 메서드의 파라미터가 변경하면 나도 변경, 익셉션 타입 추가

의존하는 대상이 적을수록 좋다!

// Before
public class UserService {
	public void regist(...) {
		...
	}
	public void changePw(...) {
		...
	}
	public void blockUser(...) {
		...
	}
}

// After
public class UserRegistService {
	public void regist(...) {
		...
	}
}
public class ChangePwService {
	public void changePw(...) {
		...
	}
}
public class UserBlockService {
	public void blockUser(...) {
		...
	}
}

- 기능 별로 분리 고려
- 클래스는 증가하지만 각 클래스마다 의존이 줄어들게 된다.
- 한 기능을 수정하면 다른 기능을 수정하는 일이 적어진다.
- 테스트할 때도 수월하다.

의존 대상 객체를 직접 생성하면?
- 생성 클래스가 바뀌면 의존하는 코드도 바뀜
- 의존 대상 객체를 직접 생성하지 않는 방법
1. 팩토리, 빌더
2. 의존 주입(Dependency Injection)
3. 서비스 로케이터(Service Locator)
의존 주입
- 의존하는 대상을 직접 생성하지 않고 생성자나 메서드를 이용해서 전달받는 방식

public class ScheduleService {
	private UserRepository repository;
	private Calculator cal;
	
	// 생성자
	public ScheduleService(UserRepository repository) {
		this.repository =repository;
	}

	// 메서드
	public void setCalculator(Calculator cal) {
		this.cal=cal;
	}
}

// 초기화 코드
UserRepository userRepo = new DbUserRepository( );
Calculator cal = new Calculator( );

ScheduleService schSvc = new ScheduleService(userRepo);
schSvc.setCalculator(cal);

DI 장점
- 상위 타입을 사용할 경우 의존 대상이 바뀌면 설정만 변경하면 됨
- 의존하는 객체 없이 대역 객체를 사용해서 테스트 가능

'∙Java & Spring' 카테고리의 다른 글

ThreadPoolTaskExecutor 이용하여 성능 개선하기  (2) 2022.05.29
Junit Mock 사용하기  (0) 2022.05.06
인프런 스프링 핵심 원리 정리  (0) 2022.04.20
JWT 구조  (0) 2022.04.16
자바의 정석 정리(2)  (0) 2022.03.09