11 시스템

Seunghee Ryu·2023년 12월 17일
0

클린 코드

목록 보기
11/18

  • 시스템 수준에서도 코드를 깨끗하게 유지하기 위한 방법에 대해서 설명한다

시스템 제작과 사용을 분리하라.

  • 제작(construction)과 사용(use)는 다르다
  • 소프트웨어 시스템은 (애플리케이션 객체를 제작하고 의존성을 서로 '연결'하는) 준비 과정과 (준비 과정 이후에 이어지는) 런타임 로직을 분리해야 한다
public Service getService() {
	if(service == null)
		service = new MyServiceImpl(...);
	return service;
}
  • 위 코드는 초기화 지연(Lazy Initialization) 혹은 계산 지연(Lazy Evaluation)이라는 기법이다
  • 실제로 getService를 호출하기 전까지는 service가 생성되지 않는다

장점

  • 필요할 때까지 객체를 생성하지 않아 과부하를 막는다
  • 애플리케이션 시작이 그만큼 빨라진다
  • Null을 반환하지 않는다

단점

  • MyServiceImpl과 생성자 인수에 명시적으로 의존한다
  • 실제로 MyServiceImpl객체를 사용하지 않더라도 의존성 해결이 안되면 컴파일이 안 된다
  • 테스트시 테스트 전용객체(Mock Object)를 할당해야 한다
  • 생성과 사용 로직이 섞여있어서 모든 실행 경로도 테스트 해야 한다
  • 책임이 여러개라는 말은 SRP(단일 책임 원칙)을 깬다
  • MyServiceImpl이 모든 상황에 적합한 객체인지 알 수 없다

결론

  • 결국 위와 같은 기법은 가볍게 한 번 정도 사용할때는 상관없지만 사용빈도가 높아질수록 문제가 많아진다
  • 시스템의 생성과 사용 로직을 분리해야 한다

Main 분리

  • 생성과 관련한 코드는 모두 main이나 main이 호출하는 모듈로 옮기고 나머지 시스템은 모든 객체가 생성 되었고 의존성이 연결되었다고 가정하는 방법
  • 그림의 화살표 방향을 보면 모든 화살표가 main에서 애플리케이션을 가리킨다
  • 애플리케이션은 main이나 객체가 생성되는 과정을 모르고 생성 되었을 거라고 가정한다

팩토리 Abstract Factory Pattern

  • 추상 팩토리 패턴을 사용해 애플리케이션이 아이템의 생성 시점을 결정할 수는 있지만 어떻게 생성하는지에 대해서는 몰라도 된다

의존성 주입(DI: Dependency Injection)

  • 제어 역전(Inversion of Control IoC)기법을 의존성 관리에 적용한 메커니즘
    - 제어 역전: 한 객체가 맡은 보조 책임을 새로운 객체에 전적으로 떠넘긴다
    - 새로운 객체는 떠맡은 책임만 담당하기에 SRP를 지원한다
  • 의존성 자체를 인스턴스로 만들 책임은 지지 않고 이런 책임을 다른 '전담' 메커니즘에 전달한다
  • 초기 설정은 시스템 전체에서 필요하기에 대게 '책임질' 메커니즘으로 'main'이나 특수 컨테이너를 사용
  • JNDI 검색은 의존성 주입을 '부분적으로' 구현한 기능이다
    MyService myService = (MyService)(jndiContext.lookup("NameOfMyService"));
  • 호출하는 객체는 실제로 반환되는 객체의 유형을 제어하지 않는다
  • 대신 의존성을 능동적으로 해결한다
  • 더 나은 방법은 클래스가 의존성을 해결하지 않고 의존성을 주입하는 방법으로 설정자(setter)메소드나 생성자 인수를(혹은 둘 다) 제공한다
  • 필요한 객체의 인스턴스를 만든 후 생성자 인수나 설정자 메서드를 사용해 의존성을 설정한다
  • 실제로 생성되는 객체 유형은 설정 파일& 특수 생성 모듈에서 명시한다

3. 확장

  • 처음부터 너무 큰 확장성을 고려해서 설계 할 필요는 없다
    - 작은 마을을 설계할 때 발전할 것을 고려해 6차선을 뚫거나 영화관을 짓는 것은 오버다
  • 오늘 주어진 스토리에 맞춰 시스템을 구현하라
  • 새로운 스토리는 새로운 스토리가 나올 때 맞춰 조정하고 확장하면 된다
  • 깨끗한 코드는 코드 수준에서 시스템을 조정하고 확장하기 쉽게 만든다

관심사를 적절히 분리하지 못한 아키텍처 예제

  • EjB2아키텍처
    - Bank EJB용 EJB2 Inteface
package com.example.banking;
import java.util.Collections;
import javax.ejb.*;

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;
}
  • Bank 주소, 은행이 소유하는 계좌가 열거되어 있다.
package com.example.banking;
import java.util.Collections;
import javax.ejb.*;

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 context = new InitialContext();
        AccountHomeLocal accountHome = context.lookup("AccountHomeLocal");
        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() {}
}
  • 비즈니스 논리는 컨테이너와 강결합이다. 클래스를 생성할 때 컨테이너에서 파생해야 하며 컨테이너가 요구하는 생명주기 메서드도 제공해야 한다
  • 비즈니스 논리의 덩치가 매우 큰 컨테이너와 강결합 된 상태이기에 독자적인 단위테스트가 힘들다
  • 프레임워크 밖에서 재사용하기가 거의 불가능하다
  • 상속조차 불가능하다

횡단 관심사(cross-cutting)

  • EjB2는 이렇게 관심사가 잘 분리되지 않았지만 일부 영역에서는 또 제대로 분리된 부분이 있다
  • 원하는 트랜잭션, 보안, 일부 영속적인 동작은 소스 코드가 아닌 배치 기술자에서 정의한다
  • 관심사가 여러 객체에 흩어져있는 기능, 관심들을 횡단 관심사(cross-cutting-concerns)라 한다
  • 이러한 횡단관심사를 관점 지향 프로그래밍AOP(Aspect-Oriented-Programming) 방법론을 이용해 모듈성을 확보한다
    - AOP에서 관점(aspect) 라는 모듈 구성 개념은 "특정 관심사를 지원하려면 시스템에서 특정 지점들이 동작하는 방식을 일관성 있게 바꿔야 한다" 라고 명시한다
    - Ex: 프로그래머는 영속적으로 저장할 객체와 속성을 선언 후 영속성 책임을 영속성 프레임워크에 위임한다 그러면 AOP 프레임워크는 대상 코드에 영향을 미치지 않는 상태로 동작 방식을 변경한다

4. 자바에서 사용하는 관점(혹은 유사한) 메커니즘 세 가지

자바 프록시

  • 단순한 상황에 적합하다
    - 개별 객체나 클래스에서 메서드 호출을 감싸는 경우
  • JDK 에서 제공하는 동적 프록시는 인터페이스만 지원한다
  • Bank에서 계좌 목록을 조회/설정하는 예제
    import java.utils.*;
    
    // 은행 추상화
    public interface Bank {
        Collection<Account> getAccounts();
        void setAccounts(Collection<Account> accounts);
    }
    
    // BankImpl.java
    import java.utils.*;
    
    // 추상화를 위한 POJO("Plain Old Jaa Object") 
    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);
            }
        }
    }
    // BankProxyHandler.java
    import java.lang.reflect.*;
    import java.util.*;
    
    // 프록시 API가 필요한 "Invocationhandler"
    public class BankProxyHandler implements InvocationHandler {
        private Bank bank;
        
        public BankHandler (Bank bank) {
            this.bank = bank;
        }
        
        // InvocationHandler에 정의된 메서드
        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 (methodName.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())
    );
  • Bank 프록시객체에서는 InvocationHandler를 구현하여 invoke를 오버라이딩한다
  • 프록시에 호출되는 Bank 메소드를 구현하는데 사용하며 Reflection API를 사용해 제네릭스 메소드를 상응하는 BankImpl 메서드로 매핑한다
  • 프록시 객체에서는 인터페이스를 통해 실 구현체로부터 모델과 로직을 구분했다
  • 하지만, 단순한 예제에서도 코드가 많고 복잡하다. 즉, 프록시를 사용하면 깨끗한 코드 작성이 어렵다
  • 시스템 단위로 실행'지점'을 명시하는 메커니즘도 제공하지 않는다

순수 자바 AOP 프레임워크

  • 대부분의 프록시 코드는 비슷하기에 자동화가 가능하며 그러한 도구가 스프링 AOP, jBoss AOP등의 프레임워크가 있다
    - 스프링은 비즈니스 논리를 순수하게 도메인에 초점을 맞춘 PJOJ로 구현하는데 POJO는 엔터프라이즈 프레임워크나 다른 도메인에 의존하지 않기에 테스트가 더 쉽고 간단하다 그렇기에 자기 도메인에 집중할 수 있게 해준다
  • 설정파일이나 API를 사용해 필수적인 애플리케이션 기반 구조를 구현한다
  • 스프링의 설정파일
     <beans>
        ...
        <bean id="appDataSource"
            class="org.apache.commons.dbcp.BasicDataSource"
            destroy-method="close"
            p:driverClassName="com.mysql.jdbc.Driver"
            p:url="jdbc:mysql://localhost:3306/mydb"
            p:username="me"/>
        
        <bean id="bankDataAccessObject"
            class="com.example.banking.persistence.BankDataAccessObject"
            p:dataSource-ref="appDataSource"/>
        
        <bean id="bank"
            class="com.example.banking.model.Bank"
            p:dataAccessObject-ref="bankDataAccessObject"/>
        ...
    </beans>

  • 위 설정파일을 도식화 한게 위 그림이다
  • 각각의 빈들은 상자속의 상자 마치 러시아 인형처럼 Bank 도메인 객체는 자료 접근 객체(Data Accessor Object, DAO)로 프록시 되었으며 이 객체도 JDBC 자료 소스로 프록시 되어있다
  • 사용자(client)는 Bank의 메소드를 호출한다고 생각하지만 실제로는 Bank POJO의 기본 동작을 확장한 중첩 DECORATOR 객체 집합의 가장 외곽과 통신한다
  • 여기서 필요하다면 트랜잭션이나 캐싱 등에도 DECORATOR를 추가할 수도 있다
  • 위와 같이 생성된 최상위 Bank 프록시 객체를 생성하는 방법은 아래와 같으며 사실상 애플리케이션은 스프링과 독립적이라고 볼 수 있다 기존의 복잡하던 프록시 객체 생성방법을 사용하지 않아도 된다
    XmlBeanFactory bf = new XmlBeanFactory(new ClassPathResource("app.xml", getClass()));
    Bank bank = (Bank) bf.getBean("bank");
  • XML은 장황하고 읽기 어렵고, 설정 파일에 명시된 정책이 겉으로 노출되지 않지만 자동으로 생성되는 프록시나 관점 논리보다 단순하다. 그래서 스프링 프레임워크에서는 EjB 버전 3을 완전히 뜯어 고쳐서 XML설정 파일과 자바 5 애너테이션 기능을 사용해 횡단 관심사를 선언적으로 지원하는 모델을 따른다
  • EjB3 Bank EJB 코드
    package com.example.banking.model;
    
    import javax.persistence.*;
    import java.util.ArrayList;
    import java.util.Collection;
    
    @Entity
    @Table(name = "BANKS")
    public class Bank implements java.io.Serializable {
        @Id @GeneratedValue(strategy=GenerationType.AUTO)
        private int id;
        
        @Embeddable // Bank의 데이터베이스 행에 '인라인으로 포함된' 객체
        public class Address {
            protected String streetAddr1;
            protected String streetAddr2;
            protected String city;
            protected String state;
            protected String zipCode;
        }
        
        @Embedded
        private Address address;
    
        @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy="bank")
        private Collection<Account> accounts = new ArrayList<Account>();
        public int getId() {
            return id;
        }
        
        public void setId(int id) {
            this.id = id;
        }
        
        public void addAccount(Account account) {
            account.setBank(this);
            accounts.add(account);
        }
        
        public Collection<Account> getAccounts() {
            return accounts;
        }
        
        public void setAccounts(Collection<Account> accounts) {
            this.accounts = accounts;
        }
    }
  • 훨씬 깨끗해졌다. 엔티티의 상세한 정보는 애너테이션 안에 기술되어 있기 때문에 코드는 깔끔하다
  • 여기서 애너테이션에 있는 영속성 정보를 XML 배치 기술자로 옮기면 POJO만 남는다

AspectJ 관점

  • 관심사를 관점으로 분리하는 가장 강력한 도구
  • 언어 차원에서 관점을 모듈화 구성으로 지원하는 자바 언어 확장이다
  • 새 도구를 사용하고 새 언어 문법과 사용법을 익혀야 한다는 단점이 있다
    • AjpectJ 애노테이션 폼이 부담을 어느정도 완화 해 주기는 한다

5. 테스트 주도 시스템 아키텍처 구축

  • 관점 혹은 유사 개념으로 관심사를 분리하는 방식은 매우 강력하여 애플리케이션 논리를 POJO로 작성할 수 있다면 즉 코드 수준에서 아키텍처 관심사 분리가 가능하다면 진정한 테스트 주도 아키텍처 구축이 가능하다
  • 즉 처음부터 크게 디자인하는 BDUF(Big Design Up Front)를 추구하지 않아도 된다
  • 작지만 멋지게 분리된 아키텍처를 진행해 빠른 결과를 낸 후 기반 구조를 추가하여 확장을 할 수 있다는 의미이다
  • 최선의 시스템 구조는 각기 POJO(또는 다른) 객체로 구현되는 모듈화 된 관심사 영역(도메인)으로 구성된다
  • 이렇게 서로 다른 영역은 해당 영역 코드에 최소한의 영향을 미치는 관점이나 유사한 도구를 사용해 통합한다
  • 이런 구조 역시 코드와 마찬가지로 테스트 주도 기법을 적용할 수 있다

6. 의사 결정을 최적화하라

  • 책임은 가장 적합한 사람에게 맡기면 가장 좋다
  • 최대한 정보를 모아 최선의 결정을 하기 위해 가장한 마지막 순간까지 결정을 미루는게 좋다
    - 성급한 결정은 충분치 않은 지식과 자료로 내린 결정이다

7. 명백한 가치가 있을 때 현명하게 사용하라

  • 표준이라는 의미로 충분히 필요가 없을때도 불필요하게 사용하는 것은 좋지 않다
    - EjB2는 표준이라는 이유로 더 가볍고 간단한 설계만으로 충분한 프로젝트에서도 사용해서 무거워지는 경우가 있었다
  • 표준을 사용하면 재사용성과 지식이 있는 기술자 구인이 쉽지만 표준이 너무 방대할 경우 표준이 나오는 시기가 너무 오래 걸려 업계가 기다리지 못해 무용지물이 될 수 있다

8. 시스템은 도메인 특화 언어가 필요하다

  • 최근 DSL(Domain Specific Language)이 조명받기 시작했다
  • DSL은 간단한 스크립트 언어나 표준 언어로 구현한 API를 말한다
  • 좋은 DSL은 도메인 개념과 그 개념을 구현한 코드 사이에 존재하는 의사소통 간극을 줄여준다.
    - 애자일 기법이 팀과 프로젝트 이해관계자 사이에 의사소통 간극을 줄여주는 것처럼
  • 도메인 특화 언어를 사용하면 고차원 정책에서 저차원 세부사항에 이르기까지 모든 추상화 수준과 모든 도메인을 POJO로 표현할 수 있다

9. 결론

  • 코드, 클래스와 마찬가지로 시스템도 깨끗해야 한다
  • 아키텍처가 깨끗하지 못하면 도메인 논리를 흐리며 기민성을 떨어트린다
  • 도메인 논리가 흐려지면 제품의 품질이 저하되며 버그가 생길 위험이 높아진다
  • 모든 추상화 단계에서 의도는 명확히 표현해야 하는데 이를 위해 POJO를 작성해 관점 혹은 유사한 메커니즘을 사용해 구현 관심사를 분리해야 한다
  • 결과적으로 시스템이던 클래스던 컴팩트하게 작성하여 사용해야 한다

개인적인 감상

  • 시스템은 도메인 특화 언어가 필요하다는 것이 잘 와닿지 않았다
  • 이론적으로는 이해가 되지만 실제로 깨끗한 시스템을 본 적이 없어서 더 와닿지 않는것이지 않을까 싶었다
  • 결국 간결하고 가독성이 좋은 코드로 이루어진 가벼운 시스템을 만드는 것이 중요하다는 결론에 도달할 수는 있었다

도메인 특화 언어

0개의 댓글