소프트웨어 시스템은 제작과 사용을 분리시켜야한다.
제작 : APP 에서 필요한객체를 제작하고 의존성을 서로 연결하는 과정
사용 : 그 이후에 실행하는 런타임 로직
/* Code 1-1 */
public Service getService() {
if (service == null)
service = new MyServiceImpl(...); // Good enough default for most cases?
return service;
}
Lazy Initialization/Evaluation(게으른 초기화)
- 장점 :실제 필요한 객체를 필요할떄까지 생성하지 않아 메모리 부하가 적어지고 앱 실행 속도도 그만큼 빨라진다.
- 단점 :MyServiceImpl 객체에 대한 의존성을 가지게 되었고 MyServiceImpl를 사용하지 않더라도이 의존성을 만족해야 함
테스트에서도 MyServiceImpl이 무거운 객체라면 테스트 전용객체 (Mock Object)를 사용해서 필드에 할당해야하고, 이로 인해 모든 경우 수를 Mocking 해야하는 의존성이 생긴다.
이러한 생성/사용의 분산은 모듈성을 저해하고 코드의 중복을 가져온다.
잘 정돈된 견고한 시스템을 만들기 위해서는 전역적이고 일관된 의존성 해결 방법을 통해 위와 같은 작은 편의 코드들이 모듈성의 저해를 가져오는 것을 막아야 한다.
Main 분리
시스템 생성과 시스템 사용을 분리하는 방법으로 생성과 관련한 코드는 main이나 main이 호출하는 모듈로 옮기고
어플리케이션에서는 사용할 객체들은 main에서 잘 생성되었을 것이라 가정한다.
팩토리
객체의 생성 시기를 직접 결정하려면 main에서 완성된 객체를 던져주기 보다 factory 객체를 만들어서 던져주자.
만약 자세한 구현을 숨기고 싶다면 Abstract Factory 패턴을 사용하자
의존성 주입
-제어 역전(IoC)을 의존성 관리에 적용한 메커니즘
초기 객체 설정은 main 루틴이나 특수 컨테이너(Spring IoC 컨테이너 등)을 사용해서 주입해준다.
/* Code 1-3 */
MyService myService = (MyService)(jndiContext.lookup(“NameOfMyService”));
클래스는 완전히 수동적으로 의존성을 주입하는 방법으로 설정자 (Setter)나 생성자 메서드만을 제공하고, DI 컨테이너는 이를 사용하여 의존성을 설정한다.
확장
처음부터 올바른 시스템을 만들수 있다는 믿음은 미신이다. 대신에 오늘 주어진 요구사항에 맞춘 시스템을 구현해야한다. 내일은 새로운 시스템을 조정하고 확장하면 된다. 이것이 반복적이고 점진적인 에자일 방식의 핵심.
소프트트웨어 시스템은 물리적인 시스템과 다르다. 관심사를 적질히 분리해 관리한다면 소프트웨어 아키텍처를 점진적으로 발전시킬수 있다.
cross-cutting 관심사 (횡단 관심사)
이론적으로는 독립된 형태로 구분될 수 있지만 실제로는 코드에 산재하기 쉬운 부분들을 뜻한다 (인증, 트랜잭션 관리, 로깅 처리 등등) AOP
AOP에서는 "코드의 어느 부분에 어떤 추가적인 기능을 삽입할까"에 대한 정의를 aspect라는 형태로 제공한다.
자바 프록시
/* Code 3-1(Listing 11-3): JDK Proxy Example */
// Bank.java (suppressing package names...)
import java.utils.*;
// The abstraction of a bank.
public interface Bank {
Collection<Account> getAccounts();
void setAccounts(Collection<Account> accounts);
}
// BankImpl.java
import java.utils.*;
// The “Plain Old Java Object” (POJO) implementing the abstraction.
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.*;
// “InvocationHandler” required by the proxy API.
public class BankProxyHandler implements InvocationHandler {
private Bank bank;
public BankHandler (Bank bank) {
this.bank = bank;
}
// Method defined in 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 {
...
}
}
// Lots of details here:
protected Collection<Account> getAccountsFromDatabase() { ... }
protected void setAccountsToDatabase(Collection<Account> accounts) { ... }
}
// Somewhere else...
Bank bank = (Bank) Proxy.newProxyInstance(
Bank.class.getClassLoader(),
new Class[] { Bank.class },
new BankProxyHandler(new BankImpl())
);
프록시 API 에는 InvocationHalndler를 넘겨줘야하고 이는 프록시에서 호출되는 Bank 메서드를 구현하는데 사용된다.
자바 리플렉션 API를 사용해 제네릭스 메서드를 상응하는 BankImpl 메서드를 만들어준다.
- 프록시 코드는 코드가 길고 더러워보이지만, 자동화할수 있다.(아무도 모르게 프록시나 바이트코드 라이브러리를 사용하여 프록시를 구현한다.)
- 스프링 AOP 등에서는 내부적으로 프록시를 사용하여 구현한다.
<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.getAcccounts()가 호출될거라고 생각하지만 실제로는 Bank의 POJO 동작을 확장한 Decorator의 가장외곽과 통신한다. 필요하다면 여기에 트랙잭션 로깅 처리등을 Decorator에 추가할수 있다.
/* Code 3-3: Code 3-2의 활용법 */
XmlBeanFactory bf = new XmlBeanFactory(new ClassPathResource("app.xml", getClass()));
Bank bank = (Bank) bf.getBean("bank");
/* Code 3-4(Listing 11-5): An EBJ3 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 // An object “inlined” in Bank’s DB row
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;
}
}
AspectJ
- 일반적인 경우에는 Spring AOP로도 충분하지만 AspectJ는 훨씬 더 강력한 AOP 기능을 제공한다.
https://wjdtn7823.tistory.com/64
의사 결정을 최소화하라
모듈을 나누고 관심사를 분리하면 지엽적인 관리와 결정이 가능해진다.
한사람이 모든 결정을 내리기보다는 가장 적합한 사람에게 책임을 맡긴다.
시스템은 도메인 특화 언어가 필요하다.
좋은 DSL 은 도메인 개념과 그 개념을 구현한 코드사이에 존재하는 의사소통 간극을 줄여준다.
효과적으로 DSL를 사용한다면 추상화 수준을 코드 관용궁나 디자인 패턴 이상으로 올려준다.
'Kotlin' 카테고리의 다른 글
Kotlin 익명 클래스 (0) | 2021.08.21 |
---|---|
by lazy vs lateinit (0) | 2021.03.10 |
JVM 언어 의 공변 (0) | 2021.02.22 |
코틀린 제네릭스 (0) | 2021.02.19 |