[OOP] 객체지향프로그래밍 입문 - 최범균

최범균님의 객체지향프로그래밍 입문 강의 정리

1. 객체

1.1. 객체

절차지향 방식은 초기에는 코드 작성이 쉬우나 시간이 흐를수록 구조가 점점 복잡해지고 수정이 어려워짐.

  • 객체의 핵심 : 기능 제공
    • 객체는 제공하는 기능으로 정의
    • 내부적으로 가진 필드(데이터)로 정의하지 않음
  • 기능 명세
    • 메서드(오퍼레이션)를 이용해서 기능 명세
    • 이름/파라미터/결과로 구성
  • 객체와 객체
    • 객체와 객체는 기능을 사용해서 연결
    • 기능 사용 = 메서드 호출
  • 메시지
    • 객체와 객체 상호 작용
    • 메서드를 호출하는 메시지, 리턴하는 메시지, 익셉션 메시지

단순히 getter/setter로 구성된 클래스는 객체라고 할 수 없음. 기능을 제공하고 있지 않기 때문

1.2. 캡슐화

  • 데이터 + 관련 기능 묶기
  • 객체가 기능을 어떻게 구현했는지 외부에 감추는 것
  • 정보 은닉 의미 포함
  • 외부에 영향 없이 객체 내부 구현 변경 가능
  • 캡슐화는 연쇄적인 변경 전파를 최소화
  • 캡슐화를 잘하면 기능에 대한 이해를 높일 수 있음
  • 캡슐화를 위한 규칙
    1. Tell, Don’t Ask
      • 데이터 달라 하지 말고 해달라고 하기 : 행위 위임하기
    2. 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); ```

캡슐화 연습 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. 상속보다 조립

  • 상속의 단점
    1. 상위 클래서 변경 어려움
    2. 클래스 증가
    3. 상속 오용
  • Composition
    • 여러 객체를 묶어서 더 복잡한 기능을 제고
    • 보통 필드로 다른 객체를 참조하는 방식으로 조립 또는 객체를 필요 시점에 생성/구함
  • 상속하기에 앞서 조립으로 풀 수 없는지 검토 : has-a 관계
  • 진짜 하위 타입인 경우에만 상속 사용 (is-a 관계)

4. 기능과 책임의 분리

  • 기능은 곧 책임 : 분리한 각 기능을 알맞게 분배
  • 책임 분배/분리 방법
    1. 패턴 적용
    2. 계산 기능 분리
    3. 외부 연동 분리
    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

  • 기능 : 회원 가입
    • 사용자는 이메일, 이름, 암호 입력 : 모두 필수 값
    • 암호가 여러 개의 규칙을 통과하지 않으면 다시 입력
    • 같은 이메일로 가입한 회원이 있으면 다시 입력
    • 이메일 인증 위한 메일 발송 : 유효성 검증 위해 암호화된 토큰 사용
    • 회원 가입 완료

regist

5. 의존과 DI

의존

  • 기능 구현을 위해 다른 구성 요소를 사용하는 것
  • 예시) 객체 생성, 메서드 호출, 데이터 사용
  • 의존은 변경이 전파될 가능성을 의미
    • 의존 대상이 바뀌면 함께 바뀔 가능성이 높아짐

순환의존

  • 변경 연쇄 전파 가능성
  • 클래스, 패키지, 모듈 등 모든 수준에서 순환 의존 발생하지 않도록!

의존 정리하기

  • 기능이 많은 경우 기능별로 분리
  • 몇 가지 의존 대상을 단일 기능으로 묶기

DI

  • 외부에서 의존 객체를 주입
    • 생성자나 메서드를 이용해 주입

의존 대상 객체 직접 생성시?

  • 생성 클래스가 바뀌면 의존하는 코드도 바뀜
  • 의존 대상 객체를 직접 생성하지 않는 방법
    • 팩토리, 빌더
    • 의존성 주입
    • 서비스 로케이터
  • 조립기가 객체 생성, 의존 주입을 처리
    • 예: 스프링 프레임워크

DI 장점

  1. 상위 타입을 사용할 경우 의존 대상이 바뀌면 조립기만 변경하면 됨
  2. 의존하는 객체 없이 대역 객체를 사용해서 테스트 가능

DIP

  • 고수준 모듈
    • 의미 있는 단일 기능을 제공
    • 상위 수준의 정책 구현
  • 저수준 모듈
    • 고수준 모듈의 기능을 구현하기 위해 필요한 하위 기능의 실제 구현
  • 의존 역전 원칙
    • 고수준 모듈은 저수준 모듈의 구현에 의존하면 안됨
    • 저수준 모듈이 고수준 모듈에서 정의한 추상타입에 의존해야 함
  • DIP는 유연함을 높임
    • 고수준 모듈의 변경을 최소화하면서 저수준 모듈의 변경 유연함을 높임

참조