ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 객체지향 프로그래밍 입문 강의 (4) - 기능과 책임 분리
    et cetera/TIL 2021. 12. 31. 18:31
    반응형

    기능 분리

    - 하나의 기능은 여러 하위 기능으로 분리할 수 있다.

    (즉, 하나의 기능은 여러 하위 기능을 통해 구현하게 된다)

    ex) 암호 변경 → 변경 대상 확인, 대상 암호 변경

    변경 대상 확인 → 변경 대상 구하기, 대상이 없으면 오류 응답하기

    대상 암호 변경 → 암호 일치 여부 확인, 암호 데이터 변경

     

    - 분리한 하위 기능을 누가 제공할 지 결정하는 것이 바로 객체지향 설계의 기본이다.

    기능은 곧 책임이다. 분리한 각 기능을 각 객체에게 알맞게 분배해야 한다.

     

    - 하나의 클래스나 메서드가 크다면 절차 지향과 동일한 문제가 발생한다.

        - 큰 클래스 → 많은 필드를 많은 메서드가 공유

        - 큰 메서드 → 많은 변수를 많은 코드가 공유

        - 여러 기능이 한 클래스/메서드에 섞이게 됨

    필드와 변수는 결국 데이터이므로, 서로 다른 코드가 데이터를 읽고 변경하면서 

    점점 코드를 수정하기 어려운 구조로 바뀌게 된다.

    ⇒ 알맞은 객체로 분리해서 하위 기능으로 분배할 필요가 있다.

     

     

    책임 분리 방법

    - 패턴 적용

    - 계산 기능 분리

    - 외부 연동 분리

    - 조건 별 분기를 추상화

     

    1. 패턴 적용

    전형적인 역할 분리 패턴을 사용하는 방법.

    - 간단한 웹: Controller, Service, DAO

    - 복잡한 도메인: Entity, Value, Repository, Domain Service

    - AOP: Aspect(여러 기능에 공통으로 사용해야하는 하위 기능)

    - GoF: Factory, Builder, Strategy, Template method, Proxy/Decorator 등

     

    2. 계산 분리

    Member mem = memberRepository.findOne(id);
    Product prod = productRepository.findOne(prodId);
    
    int payAmount = prod.price() * orderReq.getAmount();
    double pointRate = 0.01;
    if (mem.getMembership() == GOLD) {
        pointRate = 0.03;
    } else if (mem.getMembership() == SILVER) {
        pointRate = 0.02;
    }
    if (isDoublePointTarget(prod)) {
        pointRate *= 2;
    }
    
    int point = (int)(PayAmount * pointRate);
    // ...

    상단의 코드에서 double pointRate = 0.01; 부터 시작하는

    포인트를 계산하는 코드를 PointCalculator라는 별도의 클래스로 분리해보자.

    // 사용부
    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 클래스
    public class PointCalculator {
       ...membership, payAmount, prodId 필드/생성자
    
        public int calculate() {
            double pointRate = 0.01;
            if (mem.getMembership() == GOLD) {
                pointRate = 0.03;
            } else if (mem.getMembership() == SILVER) {
                pointRate = 0.02;
            }
            if (isDoublePointTarget(prod)) {
                pointRate *= 2;
            }
    
            return (int)(PayAmount * pointRate);
        }
    }

    계산이 필요하면 분리한 클래스의 계산 기능을 사용하면 된다.

     

    3. 연동 분리

    - 네트워크, 메시징, 파일 I/O 와 같은 연동 처리 코드를 분리한다.

    하단의 코드에서는 HTTP를 이용해서 외부 연동을 하는 코드를 별도의 클래스로 분리한 모습을 볼 수 있다.

    // 분리 전
    Product prod = findOne(id);
    
    RestTemplate rest = new RestTemplate();
    List<RecoItem> recoItems = rest.get(
        "http://internal/recommend?id=" + prod.getId() + 
        "&user=" + userId + "&category=" + prod.getCategory(),
        RecoItem.class);
    
    // 분리 후
    Product prod = findOne(id);
    
    RecommendService recoService = new RecommendService();
    List<RecoItem> recoItems = 
        recoService.getRecoItems(prod.getId(), userId, prod.getCategory());

     

    4. 조건 별 분기를 추상화

    - 연속적인 if-else 블럭이 있는데, 각 if-else 블럭에서 하는 일이 비슷하다면 공통점을 이용해서 추상화

    // 분리 전
    String fileUrl = "";
    if (fileId.startsWith("local:")) {
        fileUrl = "/files/" + fileId.substring(6);
    } else if (fileId.startWith("ss:")) {
        fileUrl = "http://fileserver/files/" + fileId.substring(3);
    }
    
    // 분리 후
    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);
        }
    }
    
    FileInfo fileInfo = FileInfo.getFileInfo(fileUrl);
    String fileUrl = fileInfo.getUrl();

    fileId 값에 따라서 fileUrl을 구해주는데, 여기서는 fileUrl을 구한다는 것이 공통점.

    그래서 fileUrl을 제공하는 기능을 FileInfo로 추상화 해놓은 것.

    fileId에 따라 fileUrl을 구하는 기능은 FileInfo의 각 하위 클래스에서 알맞게 제공하게 된다.

     

    주의: 역할을 분리할 때는 의도가 잘 드러나는 이름을 사용해야 한다.

    ex) HTTP로 추천 데이터를 읽어오는 기능을 분리하는 경우

    HTTPDataService와 같은 이름보다는 RecommendService 같은 이름이 더 의도를 잘 드러낸다.

     

     

    역할 분리와 테스트

    이렇게 역할 분리가 잘 되면 테스트하기도 용이해진다.

    역할이 분리되기 전에는 다른 객체들의 코드를 실행해야 우리가 테스트하려는 코드 또한 실행할 수 있게 된다.

    하지만 역할이 잘 분리되면 우리가 원하는 특정 일부 기능만 테스트하는 것이 용이해진다.

     

     분리 연습

    public class CashClient {
        private SecretKeySpec keySpec;
        private IvParameterSpec 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)));
            
            // post 요청, 응답 받기
            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(encRespBody)));
            
            return jsonToObj(respBody);
        }
    }

    여기서는 암호화 코드와 복호화 코드를 분리해낼 수 있다.

    암호화와 복호화 시 필요한 데이터인 keySpec과 ivSpec 또한 함께 분리할 수 있을 것 같다.

    public class Cryptor {
        private SecretKeySpec keySpec;
        private IvParameterSpec 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(encRespBody)));
        }
    }
    
    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);
        }
    }

     

    반응형

    댓글

Designed by Tistory.