[OOP] 객체지향프로그래밍 입문 - 최범균
최범균님의 객체지향프로그래밍 입문 강의 정리
1. 객체
1.1. 객체
절차지향 방식은 초기에는 코드 작성이 쉬우나 시간이 흐를수록 구조가 점점 복잡해지고 수정이 어려워짐.
- 객체의 핵심 :
기능
제공- 객체는 제공하는 기능으로 정의
- 내부적으로 가진 필드(데이터)로 정의하지 않음
- 기능 명세
- 메서드(오퍼레이션)를 이용해서 기능 명세
- 이름/파라미터/결과로 구성
- 객체와 객체
- 객체와 객체는 기능을 사용해서 연결
- 기능 사용 = 메서드 호출
- 메시지
- 객체와 객체 상호 작용
- 메서드를 호출하는 메시지, 리턴하는 메시지, 익셉션 메시지
단순히 getter/setter로 구성된 클래스는 객체라고 할 수 없음. 기능을 제공하고 있지 않기 때문
1.2. 캡슐화
- 데이터 + 관련 기능 묶기
- 객체가 기능을 어떻게 구현했는지 외부에 감추는 것
- 정보 은닉 의미 포함
- 외부에 영향 없이 객체 내부 구현 변경 가능
- 캡슐화는 연쇄적인 변경 전파를 최소화
- 캡슐화를 잘하면 기능에 대한 이해를 높일 수 있음
- 캡슐화를 위한 규칙
- Tell, Don’t Ask
- 데이터 달라 하지 말고 해달라고 하기 : 행위 위임하기
- Demeter’s Law
- 메서드에서 생성한 객체의 메서드만 호출
- 파라미터로 받은 객체의 메서드만 호출
- 필드로 참조하는 객체의 메서드만 호출 ```java // as-is acc.getExpDate().isAfter(now); // to-be acc.isExpired();
// as-is Date date = acc.getExpDate(); date.isAfter(now); // to-be acc.isValid(now); ```
- Tell, Don’t Ask
캡슐화 연습 1
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 (passwordEncode.isPasswordValid(mem.getPassword(), pw, mem.getId())) {
return AuthResult.SUCCESS;
}
return AuthResult.NO_MATCH;
}
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 (passwordEncode.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;
}
}
캡슐화 연습2
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 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() {
return movie.getFrequentRenterPoints(daysRented);
}
}
public class Movie {
public int REGULAR = 0;
public int NEW_RELEASE = 1;
private int priceCode;
public int getPriceCode() {
return priceCode;
}
public int getFrequentRenterPoints (int daysRented) {
if (movie.isNewRelease() && daysRented > 1)
return 2;
else
return 1;
}
public boolean isNewRelease() {
return priceCode == REGULAR;
}
}
캡슐화 연습 3
Timer t = new Timer();
t.startTime = System.currentTimeMillis();
// ...
t.stopTime = System.currentTimeMillis();
long elaspedTime = 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 {
public long startTime;
public long stopTime;
public long elaspedTime;
public void start() {
startTime = System.currentTimeMillis();
}
public void stop() {
stopTime = System.currentTimeMillis();
}
public long elapsedTime(TimeUnit unit) {
switch(unit) {
case MILLISECOND:
return stopTime - startTime;
// ....
}
}
}
캡슐화 연습 4
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 {
this.verificationEmailStatus = 2;
}
}
public boolean isEmailVerified() {
return verificationEmailStatus == 2;
}
}
2. 추상화
다형성
- 여러 모습을 갖는 것
- 객체지향에서는 한 객체가 여러 타입을 갖는 것
- 한 객체가 여러 타입의 기능을 제공
- 타입 상속으로 다형성 구현
추상화
- 데이터나 프로세스 등을 의미가 비슷한 개념이나 의미 있는 표현으로 정의하는 과정
- 두 가지 방식의 추상화
- 특정한 성질, 공통 성질(일반화)
- 타입 추상화
- 여러 구현 클래스를 대표하는 상위 타입 도출
- 흔히 인터페이스 타입으로 추상화
- 추상화 타입과 구현은 타입 상속으로 연결
- 추상 타입은 구현을 감춤
- 기능의 구현이 아닌 의도를 더 잘 드러냄
- 왜 사용?
- 유연함
- 추상화는 의존 대상이 변경하는 시점에 !
- 아직 존재하지 않는 기능에 대한 이른 추상화는 주의 : 잘못된 추상화 가능성, 복잡도만 증가
- 실제
변경
,확장이 발생할 때
추상화 시도
추상화 예시
- 기능 예시
- 클라우드 파일 통합 관리 기능 개발
- 대상 클라우드 : 드롭박스, 박스
- 주요기능
- 각 클라우드의 파일 목록 조회, 다운로드, 업로드, 삭제, 검색
추상화 X 코드
- 파일 목록 조회
public enum CloudId { DROPBOX, BOX; }
public class FileInfo {
private CloudId cloudId;
private String fileId;
private String name;
private long length;
}
public class CloudFileManager {
public List<FileInfo> getFileInfos(CloudId cloudId) {
if (cloudId == CloudId.DROPBOX) {
DropboxClient dc = ...;
List<DbFile> dbfiles = db.getFiles();
List<FileInfo> result = new ArrayList<>();
for (DbFile dbFile : dbFiles) {
FileInfo fi = new FileInfo();
fi.setCloudId(CloudId.DROPBOX);
fi.setFileId(dbFile.getFileId());
...
result.add(fi);
}
return result;
} else if (cloudId == CloudId.BOX) {
BoxService boxSvc = ...;
...
}
}
}
- 파일 다운로드
public void download(FileInfo file, File localTarget) { if (file.getCloudId() == CloudId.DROPBOX) { DropboxClient dc = ...; FileOutputStream out = new FileOutputStream(localTarget); dc.copy(file.getFileId(), out); out.close(); } else if (file.getCloudId() == CloudId.BOX) { BoxService boxSvc = ...; InputStream is = boxSvc.getInputStream(file.getId()); FileOutputStream out = new FileOutputStream(localTarget); CopyUtil.copy(is, out); } }
- 클라우드 추가
- 추가한 클라우드만큼
else if
블록이 추가됨
- 추가한 클라우드만큼
- 기능 추가 : 클라우드 간 파일 복사
왜 개발 시간이 증가하는가?
- 코드 구조가 길어지고 복잡해짐
- 새로운 클라우드 추가시 모든 메서드에 새로운 if 블록 추가
- 관련 코드가 여러 곳에 분산
- 결과적으로, 코드 가독성과 분석 속도 저하
- 코드 추가에 따른 노동시간 증가
- 실수하기 쉽고 이로 인한 불필요한 디버깅 시간 증가
추상화 O 코드
public class DropBoxFileSystem implements CloudFileSystem {
private DropBoxClient dbClient = new DropBoxClient(...);
@Override
public List<CloudFile> getFiles() {
List<DbFile> dbfiles = dbClient.getFiles();
List<FileInfo> result = new ArrayList<>();
for (DbFile dbFile : dbFiles) {
DropBoxCloudFile cf = new DropBoxCloudFile(file, dbClient);
result.add(cf);
}
return result;
}
}
public class DropBoxCloudFile implements CloudFile {
private DbFile dbFile;
private DropBoxClient dbClient;
public DropBoxCloudFile(DbFile dbFile, DropBoxClient dbClient) {
this.dbFile = dbFile;
this.dbClient = dbClient;
}
public String getId() {
return dbFile.getId();
}
public boolean hasUrl() {
return true;
}
public String getUrl() {
return dbFile.getFileUrl();
}
public String getName() {
return dbFile.getFileName();
}
public InputStream getInputStream() {
return dbClient.createStreamOfFile(dbFile);
}
public void write(OutputStream out) {
...
}
public void delete() {
dbClient.deleteFile(dbFile.getId());
}
}
- 파일 목록, 다운로드 기능 구현
public class CloudFileManager {
public List<CloudFile> getFileInfos(CloudId cloudId) {
CloudFileSystem fileSystem = CloudFileSystemFactory.getFileSystem(cloudId);
return fileSystem.getFiles();
}
public void download(CloudFile file, File localTarget) {
file.write(new FileOutputStream(localTarget));
}
}
OCP
: 기능을 변경하거나 확장할 수 있으면서, 기능을 사용하는 코드는 수정에 닫혀있어야 한다.
3. 상속보다 조립
- 상속의 단점
- 상위 클래서 변경 어려움
- 클래스 증가
- 상속 오용
- Composition
- 여러 객체를 묶어서 더 복잡한 기능을 제고
- 보통 필드로 다른 객체를 참조하는 방식으로 조립 또는 객체를 필요 시점에 생성/구함
- 상속하기에 앞서 조립으로 풀 수 없는지 검토 : has-a 관계
- 진짜 하위 타입인 경우에만 상속 사용 (
is-a
관계)
4. 기능과 책임의 분리
- 기능은 곧 책임 : 분리한 각 기능을 알맞게 분배
- 책임 분배/분리 방법
- 패턴 적용
- 계산 기능 분리
- 외부 연동 분리
- 조건별 분기는 추상화
1. 패턴 적용
- 전형적인 역할 분리
- 간단한 웹 : 컨트롤러/서비스/DAO
- 복잡한 도메인 : 엔티티/밸류/리포지토리/도메인 서비스
- AOP : Aspect(공통 기능)
- GoF : 팩토리, 빌더, 전략, 템플릿 메서드, 프록시, 데코레이터
2. 계산 기능 분리
3. 연동 분리
- 네트워크, 메시징, 파일 등 연동 처리 코드 분리
4. 조건 분기는 추상화
- 연속적인 if-else 는 추상화 고민
역할 분리 장점
- 테스트 용이해짐
분리 연습 1
public class ChshClient {
private SecretKeySpec keySpec;
private IvParamenterSpec ivSpec;
private Res post(Req req) {
String reqBody = toJson(req);
Cipher cipher = Cipher.getInstance(DEFAULT_TRANSFORM);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
String encReqBody = new String(Base64.getEncoder().encode(cipher.doFinal(reqBody)));
ResponseEntity<String> responseEntity = restTemplate.postForEntity(api, encReqBody, String.class);
String encRespBody = responseEntity.getBody();
Cipher cipher2 = Cipher.getInstance(DEFAULT_TRANSFORM);
cipher2.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
String respBody = new String(cipher.doFinal(Base64.getDecoder().decode(cipher.doFinal(encRespBody))));
return jsonToObj(respBody);
}
}
- 분리
public class CashClient {
private Cryptor cryptor;
private Res post(Req req) {
String reqBody = toJson(req);
String encReqBody = cryptor.encrypt(reqBody);
ResponseEntity<String> responseEntity = restTemplate.postForEntity(api, encReqBody, String.class);
String encRespBody = responseEntity.getBody();
String respBody = cryptor.decrypt(encRespBody);
return jsonToObj(respBody);
}
}
public class Cryptor {
private SecretKeySpec keySpec;
private IvParamenterSpec ivSpec;
public String encrypt(String plain) {
Cipher cipher = Cipher.getInstance(DEFAULT_TRANSFORM);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
return new String(Base64.getEncoder().encode(cipher.doFinal(plain)));
}
public String decrypt(String encrypted) {
Cipher cipher2 = Cipher.getInstance(DEFAULT_TRANSFORM);
cipher2.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
return new String(cipher.doFinal(Base64.getDecoder().decode(cipher.doFinal(encrypted))));
}
}
분리 연습 2
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 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() {
return movie.getFrequentRenterPoints(daysRented);
}
}
public abstract class abstract Movie {
public abstract int getFrequentRenterPoints(int daysRented);
}
public class NewReleaseMovie extends Movie {
public int getFrequentRenterPoints(int daysRented) {
return daysRented > 1 ? return 2 : 1;
}
}
public class RegularMovie extends Movie {
public int getFrequentRenterPoints(int daysRented) {
return 1;
}
}
분리 연습 3
- 기능 : 회원 가입
- 사용자는 이메일, 이름, 암호 입력 : 모두 필수 값
- 암호가 여러 개의 규칙을 통과하지 않으면 다시 입력
- 같은 이메일로 가입한 회원이 있으면 다시 입력
- 이메일 인증 위한 메일 발송 : 유효성 검증 위해 암호화된 토큰 사용
- 회원 가입 완료
5. 의존과 DI
의존
- 기능 구현을 위해 다른 구성 요소를 사용하는 것
- 예시) 객체 생성, 메서드 호출, 데이터 사용
- 의존은
변경이 전파
될 가능성을 의미- 의존 대상이 바뀌면 함께 바뀔 가능성이 높아짐
순환의존
- 변경 연쇄 전파 가능성
- 클래스, 패키지, 모듈 등 모든 수준에서 순환 의존 발생하지 않도록!
의존 정리하기
- 기능이 많은 경우 기능별로
분리
’ - 몇 가지 의존 대상을 단일 기능으로
묶기
DI
- 외부에서 의존 객체를 주입
- 생성자나 메서드를 이용해 주입
의존 대상 객체 직접 생성시?
- 생성 클래스가 바뀌면 의존하는 코드도 바뀜
- 의존 대상 객체를 직접 생성하지 않는 방법
- 팩토리, 빌더
- 의존성 주입
- 서비스 로케이터
- 조립기가 객체 생성, 의존 주입을 처리
- 예: 스프링 프레임워크
DI 장점
- 상위 타입을 사용할 경우 의존 대상이 바뀌면 조립기만 변경하면 됨
- 의존하는 객체 없이 대역 객체를 사용해서 테스트 가능
DIP
- 고수준 모듈
- 의미 있는 단일 기능을 제공
- 상위 수준의 정책 구현
- 저수준 모듈
- 고수준 모듈의 기능을 구현하기 위해 필요한 하위 기능의 실제 구현
- 의존 역전 원칙
- 고수준 모듈은 저수준 모듈의 구현에 의존하면 안됨
- 저수준 모듈이 고수준 모듈에서 정의한 추상타입에 의존해야 함
- DIP는 유연함을 높임
- 고수준 모듈의 변경을 최소화하면서 저수준 모듈의 변경 유연함을 높임