-
객체지향 프로그래밍 입문 강의 (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); } }
반응형'et cetera > TIL' 카테고리의 다른 글
객체지향 프로그래밍 입문 강의 (5) - 의존과 DI (4) 2022.01.02 객체지향 프로그래밍 입문 강의 (3) - 상속보단 조립 (0) 2021.12.30 객체지향 프로그래밍 입문 강의 (2) - 다형성과 추상화 (0) 2021.12.29 클린코드 자바스크립트 강의 (2) (0) 2021.12.29 객체지향 프로그래밍 입문 강의 (1) - 캡슐화 (0) 2021.12.27