-
[클린코드] 시스템📚 개발 도서/클린코드 2023. 7. 1. 16:18
“복잡성은 죽음이다. 개발자에게서 생기를 앗아가며, 제품을 계획하고 제작하고 테스트하기 어렵게 만든다."
- 레이 오지, 마이크로소프트 최고 기술 책임자이 장에서는 높은 추상화 수준, 즉 시스템 수준에서도 깨끗함을 유지하는 방법을 살펴본다.
시스템 제작과 시스템 사용을 분리하라
construction과 use는 아주 다르다.
소프트웨어 시스템은 (애플리케이션 객체를 제작하고 의존성을 서로 ‘연결’하는) 준비 과정과 (준비 과정 이후에 이어지는) 런타임 로직을 분리해야 한다.
설정 논리는 일반 실행 논리와 분리해야 모듈성이 높아진다.
public Service getService() { if (service == null) service = new MyServiceImpl(...); return service; }
위 코드를 보면 생성 로직과 사용 로직이 섞여있다.
장점
- 실제로 getService를 호출하기 전까진 service가 생성되지 않는다. (lazy initialization, lazy evaluation)
- 어떤 경우에도 null을 반환하지 않는다.
단점
- getService 메서드가 MyServiceImpl (new MyServiceImpl()) 과 생성자 인수에 명시적으로 의존한다.
- 책임이 2개고 테스트를 하기가 어렵다.
- getService를 테스트하기 위해선 MyServicecImpl을 Mock으로 만들어야 한다.
- service가 null인 경우도 테스트를 해야 한다.
- MyServiceImpl가 모든 상황에 적합한 객체인가? 다른 객체가 들어올 일이 없나? 다른 객체가 들어오면 로직을 수정해야 한다.
모듈성은 저조하며 대개 중복이 심각하다
방법
- 1. Main 분리
- 생성과 관련한 코드는 모두 main이나 main이 호출하는 모듈로 옮기고, 나머지 시스템은 모든 객체가 생성되었고 모든 의존성이 연결되었다고 가정한다.
→ main 함수에서는 시스템에 필요한 객체를 생성한 후 이를 애플리케이션에 넘긴다. 애플리케이션은 그저 객체를 사용할 뿐이다. - 애플리케이션은 main이나 객체가 생성되는 과정을 전혀 모른다는 뜻이다.
- 생성과 관련한 코드는 모두 main이나 main이 호출하는 모듈로 옮기고, 나머지 시스템은 모든 객체가 생성되었고 모든 의존성이 연결되었다고 가정한다.
- 2. 팩토리
객체가 생성되는 시점을 애플리케이션이 결정해야 하는 경우엔 팩토리 형식을 사용할 수 있다.
Application은 FactoryImpl을 받아 필요할 때 Configured Object를 생성한다.
Application은 Configured Object가 언제 생성되는지 알 수가 없다. - 3. 의존성 주입
- 생성과 사용을 분리하는 강력한 메커니즘. IoC 기법을 의존성 관리에 적용한 메커니즘이다.
- IoC : 기존에 개발자들이 직접 생성하던 객체를, 다른 수단(e.g. 스프링)이 대신하게 끔해서 ‘역전’이다.
여기서 의존성을 스프링이 ‘주입’ injection 해주는 것
객체의 생명주기는 프레임워크가 알아서 관리해주고, 개발자는 비즈니스 로직에만 집중할 수 있게된다.
- IoC : 기존에 개발자들이 직접 생성하던 객체를, 다른 수단(e.g. 스프링)이 대신하게 끔해서 ‘역전’이다.
- 제어 역전에서는 한 객체가 맡은 보조 책임을 새로운 객체에게 전적으로 떠넘긴다. 새로운 객체는 넘겨받은 책임만 맡으므로 단일 책임 원칙(SRP)을 지키게 된다.
의존성 관리의 관점에서 객체는 의존성 자체를 인스턴스로 만드는 책임을 지지 않는다.(자신의 의존성을 직접 생성하지 않는다.) 대신 다른 전담 메커니즘에게 제어를 역전한다.
- 생성과 사용을 분리하는 강력한 메커니즘. IoC 기법을 의존성 관리에 적용한 메커니즘이다.
확장
관심사를 적절히 분리해 관리한다면 소프트퉤어 아키텍처는 점진적으로 분리할 수 있다
EJB2에서 Bean을 정의하는 방법.
EJB2는 트랜잭션 동작 방식, 보안 제약조건 등은 XML로 따로 분리해서 사용했는데 위와 같이 빈 정의할 땐 위와 같이 사용해야 한다.
public interface BankLocal extends java.ejb.EJBLocalObject { String getStreetAddr1() throws EJBException; String getStreetAddr2() throws EJBException; String getCity() throws EJBException; String getState() throws EJBException; String getZipCode() throws EJBException; void setStreetAddr1(String street1) throws EJBException; void setStreetAddr2(String street2) throws EJBException; void setCity(String city) throws EJBException; void setState(String state) throws EJBException; void setZipCode(String zip) throws EJBException; Collection getAccounts() throws EJBException; void setAccounts(Collection accounts) throws EJBException; void addAccount(AccountDTO accountDTO) throws EJBException; } public abstract class Bank implements javax.ejb.EntityBean { public abstract String getStreetAddr1(); public abstract String getStreetAddr2(); public abstract String getCity(); public abstract String getState(); public abstract String getZipCode(); public abstract void setStreetAddr1(String street1); public abstract void setStreetAddr2(String street2); public abstract void setCity(String city); public abstract void setState(String state); public abstract void setZipCode(String zip); public abstract Collection getAccounts(); public abstract void setAccounts(Collection accounts); public void addAccount(AccountDTO accountDTO) { InitialContext contet = new InitialContext(); AccountHomeLocal accountHome = context.lookup("AcccountHomeLocal"); AccountLocal account = accountHome.create(accountDTO); Collection accounts = getAccounts(); accounts.add(account); } // EJB 컨데이터 로직 public abstract void setId(Integer id); public abstract Integer getId(); public Integer ejbCreate(Integer id) { ... } public void ejbPostCreate(Integer id) { ... } // 웬만하면 아래 로직은 다 빈 로직으로 선언 public void setEntityContext(EntityContext ctx) {} public void unsetEntityContext() {} public void ejbActivate() {} public void ejbPassivate() {} public void ejbLoad() {} public void ejbStore() {} public void ejbRemove() {} }
문제점
- BankLocal은 java.ejb.EJBLocalObject, Bank는 javax.ejb.EntityBean에 아주 강하게 결합되어있다.
- 다른 빈들을 또 생성한다고 하면 EJBLocalObject, EntityBean을 쌍으로 계속 만들어 줘야 하는데 안 쓰는 코드들이 너무 많다.
- EJB하고 강하게 결합되어 있어 테스트를 하기가 힘들다.
해결 방법
트랜잭션 동작 방식, 보안 제약조건 등을 XML에 따로 분리해서 관리하듯 위와 같은 빈 등록 코드들도 따로 분리해서 관리할 수 있게 하면 된다. 관점 분리 적용하자
어떻게 관점 분리를 적용할 수 있을까?
횡단(cross-cutting) 관심사
- AOP에서는 관점이라는 모듈 구성 개념은 “특정 관ㅅ심사를 지우너하려면 시스템에서 특정 지점들을 동작하는 방식을 일관성있게 바꿔야 한다”라고 명시한다.
자바에서 사용하는 관점 혹은 관점과 유사한 메커니즘 3개
- 1. 자바 프록시
- 단순한 상황에 적합하다. 개별 객체나 클래스에서 메서드 호출을 감싸는 경우가 좋은 예다.
- JDK에서 제공하는 동적 프록시는 인터페이스만 지원한다. 클래스 프록시를 사용하려면 CGLIB, ASM, Javassist 등과 같은 바이트 코드 처리 라이브러리가 필요하다.
public interface Bank { Collection<Account> getAccounts(); void setAccounts(Collection<Account> accounts); } public class BankImpl implements Bank { private List<Account> accounts; public Collection<Account> getAccounts() { return accounts; } public void setAccounts(Collection<Account> accounts) { this.accounts = new ArrayList<Account>(); for (Account account : accounts) { this.accounts.add(account); } } } public class BankProxyHandler implements InvocationHandler { private Bank bank; public BankProxyHandler(Bank bank) { this.bank = bank; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); if (methodName.equals("getAccounts")) { bank.setAccounts(getAccountsFromDatabase()); return bank.getAccounts(); } else if (mehtodName.equals("setAccounts")) { bank.setAccounts((Collection<Account>) args[0]); setAccountsToDatabase(bank.getAccounts()); return null; } else { ... } } protected Collection<Account> getAccountsFromDatabase() { ... } protected void setAccountsToDatabase(Collection<Account> accounts) { ... } } Bank bank = (Bank) Proxy.newProxyInstance( Bank.class.getClassLoader(), new Class[] { Bank.class }, new BankProxyHandler(new BankImpl()));
- 자바 동적 프록시를 이용해서 위와 같이 아까 EJB2에서 사용한 코드를 바꿔보았다. 프록시로 감쌀 Bank 인터페이스와 비즈니스 로직을 구현하는 BankImpl을 POJO로 정의했다.(POJO (Plain Old Java Object) : 어떤 프레임워크나 라이브러리에 의존하지 않는 객체를 말한다.)
- 문제점
- 코드 '양'과 크기가 너무 크다. 장황해보인다.
- 시스템 단위로 실행 '지점'을 명시하는 메커니즘도 제공하지 않는다.
- 2. 순수 자바 AOP 프레임워크
- 순수 자바: AOP 언어인 AspectJ를 사용하지 않는 방법을 말한다.
- 대부분의 프록시 코드는 판박이라 도구로 자동화될 수 있다.
- 순수 자바 AOP 프레임워크에는 스프링 AOP, JBoss AOP 등이 있다. 위 프레임워크에서는 내부적으로 프록시를 사용한다. (스프링은 CGLIB)
- 스프링은 비즈니스 논리를 POJO로 구현한다.
- 프로그래머는 설정 파일이나 API를 사용해 필수적인 애플리케이션 기반 구조를 구현한다. → 여기에는 영속성, 트랜잭션, 보안 등과 같은 횡단 관심사도 포함된다.
- 장점
- EJB2 시스템이 지녔던 강한 결합 문제가 모두 해결된다.
- 자바 동적 프록시보다 간단하다.
- 3. AspectJ 관점
aop를 구현하는 방법을 담은 언어인 AspectJ는 스프링 AOP보다 많은 기능들을 제공한다.
하지만 우리가 사용하는 기능은 스프링 AOP에서 제공하는 걸로 80~90% 해결 가능하다.
정말 사용해야 한다면 사용하자. 아닌 경우엔 그냥 스프링 AOP로 해결하자.
관심사 분리를 사용해 깨끗한 시스템을 만들자!!
반응형'📚 개발 도서 > 클린코드' 카테고리의 다른 글
[클린코드] 오류 처리 (0) 2023.07.18 [클린코드] 객체와 자료구조 (0) 2023.07.16 [클린코드] 클래스 (0) 2023.06.15 [클린코드] 함수 (0) 2023.05.07 [클린코드] 의미 있는 이름 (0) 2023.04.29