이번 포스팅에선 클린 코드 11장 - 시스템 (Systems)에 대해 알아보겠습니다.
이번 장 이름만 봐서는 유추가 안 되는데 차라리 관심사 분리로 이름을 바꾸는 게 더 좋은듯 하다.
왜 생성과 사용을 분리해야 할까?
생성 로직과 사용 로직을 분리하면 모듈성이 높아진다.
왜 그런지 알아보자.
public Service getService() {
if (service == null)
service = new MyServiceImpl(...);
return service;
}
위 코드를 보면 생성 로직과 사용 로직이 섞여있다.
생성 로직
if (service == null)
service = new MyServiceImpl(...);
사용 로직
return service;
장점
1. 실제로 getService를 호출하기 전까진 service가 생성되지 않는다. (lazy initialization, lazy evaluation 기법)
2. 어떤 경우에도 null을 반환하지 않는다.
단점
1. getService 메서드가 MyServiceImpl (new MyServiceImpl) 과 생성자 인수 (...)에 명시적으로 의존한다.
2. 책임이 2개고 테스트를 하기가 어렵다.
- getService를 테스트하기 위해선 MyServicecImpl을 Mock으로 만들어야 한다.
- service가 null인 경우도 테스트를 해야 한다.
3. MyServiceImpl가 모든 상황에 적합한 객체인가? 다른 객체가 들어올 일이 없나? 다른 객체가 들어오면 로직을 수정해야 한다.
생성과 사용을 어떻게 분리할 수 있을까?
1. Main 분리
1) Main에서 Application을 호출하기 전에 Builder에 생성 요청을 한다.
1.1) 생성 요청을 받은 Builder는 Configured Object를 생성해서 Main에 리턴한다.
2) Main은 Application에 Builder에서 넘겨준 Configured Object를 넘겨주어 로직에서 사용하게 한다.
장점
Application은 사용할 객체가 생성되는 과정을 전혀 모른다.
생성과 사용이 분리됐다.
예)
public class Main {
public long sellFruit() {
Fruit fruit = FruitBuilder.getFruit("apple");
return new FruitService().sellFruit(fruit);
}
}
public interface Fruit {}
public class FruitBuilder {
public static Fruit getFruit(String fruitName) {
if (fruitName.equals("apple"))
return new Apple();
else if (fruitName.equals("grape"))
return new Grape();
else if (fruitName.equals("orange"))
return new Orange();
else
throw new IllegalArgumentException("없는 과일");
}
}
public class FruitService {
public long sellFruit(Fruit fruit) {
if (fruit.equals("apple"))
return 1000;
else if (fruit.equals("grape"))
return 2000;
else if (fruit.equals("orange"))
return 1500;
else
throw new IllegalArgumentException("없는 과일");
}
}
2. 팩토리
객체가 생성되는 시점을 애플리케이션이 결정해야 하는 경우엔 팩토리 형식을 사용할 수 있다.
장점
Application은 FactoryImpl을 받아 필요할 때 Configured Object를 생성한다.
Application은 Configured Object가 언제 생성되는지 알 수가 없다.
생성과 사용이 분리되었다.
public class Main {
public long sellFruit() {
return new FruitService(new FruitFactoryImpl()).sellFruit("apple");
}
}
public interface Fruit {}
public interface FruitFactory {
Fruit getFruit(String fruitName);
}
public class FruitFactoryImpl implements FruitFactory {
public Fruit getFruit(String fruitName) {
if (fruitName.equals("apple"))
return new Apple();
else if (fruitName.equals("grape"))
return new Grape();
else if (fruitName.equals("orange"))
return new Orange();
else
throw new IllegalArgumentException("없는 과일");
}
}
public class FruitService {
private FruitFactory fruitFactory;
public FruitService(FruitFactoryImpl fruitFactory) {
this.fruitFactory = fruitFactory;
}
public long sellFruit(String fruitName) {
Fruit fruit = fruitFactory.getFruit(fruitName);
if (fruit.equals("apple"))
return 1000;
else if (fruit.equals("grape"))
return 2000;
else if (fruit.equals("orange"))
return 1500;
else
throw new IllegalArgumentException("없는 과일");
}
}
3. 의존성 주입 (Dependency Injection)
생성과 사용을 강력하게 분리하는 강력한 메카니즘입니다.
IOC 기법을 의존성 관리에 적용한 메커니즘이다.
의존성 주입은 이전에 정리해놓은 글이 있으니 그쪽을 참고 부탁드립니다.
관점 지향 프로그래밍 AOP(Aspect-Oriented Programming)
AOP는 관점 지향 프로그래밍으로 소스 A, B, C에 중복으로 들어가는 트랜잭션, 로깅, 보안 코드를 횡단 관심사로 분류해서 관점 (aspect)로 관리하는 방법을 말한다.
관점으로 분류해서 코드를 따로 관리하면 다른 코드들에 영향을 미치지 않고 해당 관점 코드만 변경 가능하다.
어떤 경우 AOP가 필요한지 예를 들어서 설명해보겠습니다.
예)
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() {}
}
아주 유명한 EJB2를 사용한 코드다.
EJB2에서 Bean을 정의하는 방법이라고 한다.
EJB2는 트랜잭션 동작 방식, 보안 제약조건 등은 XML로 따로 분리해서 사용했는데 위와 같이 빈 정의할 땐 위와 같이 사용해야 한다.
문제점
1. BankLocal은 java.ejb.EJBLocalObject, Bank는 javax.ejb.EntityBean에 아주 강하게 결합되어있다.
2. 다른 빈들을 또 생성한다고 하면 EJBLocalObject, EntityBean을 쌍으로 계속 만들어 줘야 하는데 안 쓰는 코드들이 너무 많다.
3. EJB하고 강하게 결합되어 있어 테스트를 하기가 힘들다.
해결 방법
트랜잭션 동작 방식, 보안 제약조건 등을 XML에 따로 분리해서 관리하듯 위와 같은 빈 등록 코드들도 따로 분리해서 관리할 수 있게 하면 된다. 관점 분리 적용하자
어떻게 관점 분리를 적용할 수 있을까?
관점 분리 기술
자바 프록시
자바 프록시는 단순한 상황에 적합하다.
개별 객체나 클래스에서 메서드 호출을 감싸는 경우가 좋은 예다.
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) : 어떤 프레임워크나 라이브러리에 의존하지 않는 객체를 말한다.)
문제점
1. 코드 '양'과 크기가 너무 크다. 장황해보인다.
2. 시스템 단위로 실행 '지점'을 명시하는 메커니즘도 제공하지 않는다.
이걸 해결해주는 기술이 있다.
순수 자바 AOP 프레임워크
순수 자바라는 뜻은 AOP 언어인 AspectJ를 사용하지 않는 방법을 말한다.
순수 자바 AOP 프레임워크에는 스프링 AOP, JBoss AOP 등이 있다.
위 프레임워크에서는 내부적으로 프록시를 사용한다. (스프링은 CGLIB)
스프링 AOP 프레임워크는 빈 생성 정보만 xml에 정의해놓으면 DI 컨테이너가 생성과 주입을 전부 처리해준다.
<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>
spring app.xml이다. 정의된 빈이다.
XmlBeanFactory bf = new XmlBeanFactory(new ClassPathResource("app.xml", getClass()));
Bank bank = (Bank) bf.getBean("bank");
등록된 빈은 위와 같이 꺼내쓸 수 있다.
장점
1. EJB2 시스템이 지녔던 강한 결합 문제가 모두 해결된다.
2. 자바 동적 프록시보다 간단하다.
스프링 프레임워크를 보고 EJB3을 아래와 같이 개선했다고 한다.
@Entity
@Table(name = "BANKS")
public class Bank implements Serializable {
@Id @GeneratedValue(strategy = GenerationType.AUTO)
private int id;
@Embeddable
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;
}
}
장점
1. 다른 라이브러리나 프레임워크에 종속되지 않은 POJO 객체만 남는다.
2. 테스트를 하기 쉬워졌다.
AspectJ
aop를 구현하는 방법을 담은 언어인 AspectJ는 스프링 AOP보다 많은 기능들을 제공한다.
하지만 우리가 사용하는 기능은 스프링 AOP에서 제공하는 걸로 80~90% 해결 가능하다.
정말 사용해야 한다면 사용하자. 아닌 경우엔 그냥 스프링 AOP로 해결하자.
결론
관심사 분리를 사용해 깨끗한 시스템을 만들자!!
이번 장에선 관심사 분리에 대해 알아봤는데 사실 이미 스프링에서 너무 잘 만들어놔서 이미 잘 분리가 되어있다.
하지만 각 비즈니스 로직은 우리가 짜는 거니 관심사 분리를 적용해서 할만한 곳이 있는지 잘 파악해서 적용하자.
이 글을 읽은 후 스프링 AOP에 대해서 공부해보는 것도 좋을 거 같다.
'Clean Code' 카테고리의 다른 글
[클린 코드] 13장 - 동시성 (Concurrency) (0) | 2022.09.05 |
---|---|
[클린 코드] 12장 - 창발성 (Emergence) (0) | 2022.03.18 |
[클린 코드] 10장 - 클래스 (Classes) (0) | 2022.03.11 |
[클린 코드] 9장 - 단위 테스트 (Unit Tests) (0) | 2022.03.11 |
[클린 코드] 8장 - 경계 (Boundaries) (0) | 2022.03.11 |
댓글