Skip to content

Instantly share code, notes, and snippets.

@appkr
Last active November 24, 2018 08:18
Show Gist options
  • Select an option

  • Save appkr/a13a2172c943dbfd8e8a45fad4ee8045 to your computer and use it in GitHub Desktop.

Select an option

Save appkr/a13a2172c943dbfd8e8a45fad4ee8045 to your computer and use it in GitHub Desktop.
객체 지향 프로그래밍 입문 by 최범균님

객체 지향 프로그래밍 입문, 최범균님

01 들어가며

  • 코드 비용
  • if (foo == "bar" || foo == "baz") {} 같은 경우, 조건식이 계속 증가할 수 있으므로, 조건식을 객체에 캡슐화할 것.
  • 소프트웨어의 가치는 "변화"

Software maintenance is not "keep it working like before". It it "keep being useful in a changing world".

  • Jessica Kerr
  • Then how?
    • 패러다임: 객체지향, 함수형, 리액티브
    • 코드, 설계, 아키텍처: DRY, TDD, SOLID, DDD, 클린 아키텍처, MSA, ...
    • 업무 프로세스/문화: Agile, DevOps, ...
  • 객체 지향의 접근법? 캡슐화 + 다형성(추상화)

02 객체

  • 절차지향? 여러 프로시저가 같은 데이터를 공유하는 프로그래밍 패러다임
    • 데이터가 변경되면 데이터를 사용하는 코드도 변경해야 함
    • 요구사항이 추가되면 데이터를 사용하는 방식을 변경해야 함
  • 객체지향? 데이터와 프로시저를 객체에 캡슐화
    • 객체들간에는 프로시저를 호출해서 객체 안에 캡슐화된 데이터를 사용
  • 객체란? 기능(operation)을 제공한다. e.g. 회원객체: 암호 변경하기, 차단 여부 확인하기, ..
  • 메시지? 객체와 객체의 상호 작용. 호출, 응답, 예외 등 모두를 통칭

03 캡슐화

  • 캡슐화? 데이터와 관련 기능을 묶는 것
  • 객체의 기능을 사용하는 클라이언트 클래스에 영향을 주지 않고(또는 최소화), 객체의 내부의 구현을 변경할 수 있는 유연함
if (acc.hasRegularPermission()) {
    // 정회원 기능 ...
}

public class Account {
    private Membership membership;
    private Date expDate;

    public boolean hasRegularPermission() {
        // 요구사항이 변경되면, 아래 표현식을 변경하면 됨
        return membership == REGULAR && expDate.isAfter(now());
    }
}

04 다형성과 추상화

  • 다형성? 여러(poly) 모습(morph)을 갖는 것. 한 객체가 상속을 통해 여러 타입의 기능을 제공하는 것.
  • 구현 상속과 인터페이스 상속
  • 추상화 Abstraction? 데이터나 프로세스 등을 의미가 비슷한 개념이나 의미 있는 표현으로 정의하는 과정
    • 특정한 성질 e.g. Money: amount, currency
    • 공통 성질(일반화) e.g. Gpu: GForce, Radeon
+---------------------+
| uploadFileViaScp()  | --+
+---------------------+   |
+---------------------+   |    +-------------------+
| sendDateViaHttp()   |   +--> | sendPushMessage   |
+---------------------+   |    +-------------------+
+---------------------+   |
| insertDataIntoTbl() | --+
+---------------------+
  • 타입 추상화, 추상 타입을 이용한 프로그래밍은 내부 구현을 감춤으로서, 기능의 구현이 아닌 의도를 드러냄
+---------------------+
| EmailNotifier       | --+
+---------------------+   |    +-------------------+
+---------------------+   |    | <<I>> Notifier    |
| SMSNotifier         |   +-|> +-------------------+
+---------------------+   |    | + notify(noti: Notification)
+---------------------+   |    +-------------------+
| KakaoNotifier       | --+
+---------------------+

+---------------------+      +---------------------+
| OrderCancelService  | ---> | <<I>> Notifier      | <|- Concretes
+---------------------+  |   +---------------------+
| +cancel(onum: Long) |  |   +---------------------+
+---------------------+  +-> |<<I>> NotifierFactory| <|- Concretes
                             +---------------------+
public void cancel(String orderNumber) {
    // 주문 취소 처리

    // 푸쉬를 보내는 로직이 아니라, "주문 취소"라는 본질에 집중할 수 있고,
    // 푸쉬 메시지를 보내는 정책을 getNotifier()로 캡슐화함으로써 정책 변경에 따른 영향을 최소화할 수 있음
    Notifier notifier = NotifierFactory.instance().getNotifier();
    notifier.notify(order.toNotification());
}
  • 추상화는 정책이 변경/확장으로 인해 추상화가 필요할 때 시도할 것

05 상속보단 조립

  • 상속은 상위 타입의 기능을 재사용, 확장하는 방법
  • 상속의 문제점
    • 상위 클래스 변경의 어려움: 상위 클래스는 어떤 하위 클래스가 추가될지 알 수 없는 상황에서, 상위 클래스 변경의 여파가 하위 클래스에 영향을 줌
    • 클래스 증가 e.g. public class EncryptedCompressedStorage extends EncryptedStorage
    • 상속 오용 e.g. public class Container extends ArrayList<Luggage>
  • 조립? 기능을 사용하고자 하는 클래스의 객체를 멤버 필드에 담아서 사용하는 것
+----------------------------+      +--------------------+
| Storage                    |      | Compressor         |
+----------------------------+ ---> +--------------------+
| - compressor: Compressor   |      | + compress()       |
| - encryptor: Encryptor     |      +--------------------+
| - cacheEngine: CacheEngine |      +--------------------+
| - useCompression: boolean  |      | Encryptor          |
| - useEncryption: boolean   | ---> +--------------------+
| - useCache: boolean        |      | + encrypt()        |
+----------------------------+      +--------------------+
                                    +--------------------+
                               ---> | CacheEngine        |
                                    +--------------------+
  • 상속보다는 조립
    • 상속하기에 앞서 조립으로 해결할 수 있는 지 검토
    • 진짜 하위 타입인 경우에만 상속 사용
  • PHP 예제

06 기능과 책임 분리

  • 하나의 기능은 여러 하위 기능으로 분해할 수 있음
암호 변경 --- 변경 대상 확인 --- 변경 대상 구함
         |               |
         |               +- 대상이 없으면 오류 응답
         +- 대상 암호 변경 --- 암호 일치 여부 확인 --- 불일치하면 암호 불일치 응답
                         |
                         +- 암호 변경
  • 기능은 곧 책임. 기능을 분리하고, 각 기능을 객체에 분배
ChangePasswordService collaborator
변경 대상 확인 MemberRepository
대상 암호 변경 Member
MemberRepository collaborator
회원 조회 Member
Member collaborator
회원 정보 유지  
  • 클래스나 메서드가 커지면 절차 지향의 문제 발생하므로, 책임에 따라 알맞게 코드 분리 필요
    • 큰 클래스는 여러 필드를 여러 메서드가 공유하게 됨
    • 큰 메서드는 여러 변수를 여러 군데서 공유
    • 여러 기능이 한 클래스나 메서드에 혼재되어 있을 가능성이 큼
  • 책임 분배/분리 방법
    • 패턴 적용
    • 계산 기능 분리
    • 연동 부분 분리
    • 조건별 분기를 추상화
  • 역할 분리할 때는 의도를 드러내는 이름을 지을 것 Intention Revealing Interface
  • 역할 분리가 잘 되면 테스트도 용이해지는 부수적인 효과가 있음, 바꾸어 말하면 테스트가 용이하지 않으면 설계가 구린 것임

패턴 적용

계산 분리

// CreateOrderService.java
Member mem = memberRepository.findOne(id);
Product prod = productRepository.findOne(prodId);
int payAmount = prod.price() * orderReq.getAmount();
PointCalculator = cal = new PointCalculator(payAmount, mem.getMembership(), prod.getId());
int point = cal.calculate();

// PointCalculator.java
public class PointCalculator {
    public int calculate() {
        double pointRate = 0.01;
        if (membership == GOLD) {
            pointRate = 0.03;
        } else if (membership == SILVER) {
            pointRate = 0.03;
        }
        return (int)(payAmount * pointRate);
    }
}

연동 분리

Product prod = findOne(id);
RecommendService = recoService = new RecommendService();
List<RecoItem> recoItems = recoService.getRecoItems(prod.getId(), userId, prod.getCategory());

조건별 분기를 추상화

  • 아래 예제 코드의 조건문에서 fileUrl을 구한다는 공통점이 있음
  • 조건 분기는 추상화를 통한 하위 클래스 형태로 코드 변경 가능
// before
String fileUrl = "";
if (fileId.startsWith("local:")) {
    fileUrl = "/files/" + fileId.substring(6);
} else if (fileId.startsWith("ss:")) {
    fileUrl = "http://fileserver/files/" + fileId.substring(3);
}

// after
FileInfo fileInfo = FileInfo.getFileInfo(fileUrl);
String fileUrl = fileInfo.getUrl();

public interface FileInfo {
    String getUrl();
    static FileInfo getFile(..) {..}
}

public class SSFileInfo implements FileInfo {
    private String fileId;

    public String getUrl() {
        return "http://fileserver/files/" + fileId.substring(3);
    }
}

연습 문제

07 의존과 의존 주입

  • 의존? 기능 구현을 위해 다른 구성 요소를 사용하는 것. 객체 생성, 메서드 호출, 데이터 사용
  • 의존은 변경이 전파될 가능성을 의미 e.g. 함수의 파라미터 변경, 예외 타입 추가 등
  • 의존하는 대상이 많다면, 변경의 영향을 받을 가능성이 크므로, 의존하는 대상이 적을수록 좋음.
    • 특히, 한 클래스에서 많은 기능을 제공하는 경우, 의존의 개수가 많아질 수 있음.
    • 한 클래스에 기능이 많은 경우, 기능별로 분리를 고려
  • 의존 대상 객체를 직접 생성하지 않고 주입
    • 팩토리, 빌더
    • 의존 주입
    • 서비스 로케이터
  • 의존 주입? 생성자 또는 setter 메서드를 이용해서 외부에서 의존 객체 주입
  • 의존 주입의 장점
    • 상위 타입을 의존으로 설정할 경우, 의존 대상이 바뀌면 조립기 설정만 변경하면 됨
    • 의존 객체의 구현체 없이 모의 객체(mock)를 사용해서 코드 테스트 가능

08 DIP

  • DIP Dependency Inversion Principle, 의존 역전 원칙? 고수준 모듈은 저수준 모듈의 구현에 의존하지 않고, 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 함
  • 고수준 모듈과 저수준 모듈
고수준 저수준
도면 이미지를 저장하고 NAS에 이미지를 저장한다
측정 정보를 저장하고 foo 테이블에 저장한다
도면 수정 의뢰를 한다 bar 테이블에 저장한다
  • 고수준이 저수준에 직접 의존하면? 저수준 모듈의 변경이 고수준 모듈에 영향을 미치게 됨. e.g. 도면 이미지를 저장한다는 고수준 정책은 변경되지 않았으나, NAS 대신 S3에 저장한다는 저수준 요구사항 변경으로 인해 고수준 코드의 변경이 필요하게 됨
+----------------------+
| MeasureService       |
+----------------------+
            ↓
+----------------------+
| <<I>> StorageService |
+----------------------+
                          고수준 모듈
------------------------------------
            ↑             저수준 모듈
+----------------------+
| NasStorage           |
+----------------------+ +
  | AwsS3Storage         |
  +----------------------+
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment