티스토리 뷰
● 객체란
- 객체의 핵심 -> 기능 제공
- 객체는 제공하는 기능으로 정의
- 내부적으로 가진 필드(데이터로) 정의하지 않음
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 |